Skip to content

Commit

Permalink
Mint/verify_outputs_not_already_signed (#377)
Browse files Browse the repository at this point in the history
* test output duplication in mint

* add comments to the db connection reuse
  • Loading branch information
callebtc authored Dec 3, 2023
1 parent 6c8b1a8 commit 7d4ed95
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 5 deletions.
11 changes: 9 additions & 2 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ async def _invalidate_proofs(
Args:
proofs (List[Proof]): Proofs to add to known secret table.
conn: (Optional[Connection], optional): Database connection to reuse. Will create a new one if not given. Defaults to None.
"""
secrets = set([p.secret for p in proofs])
self.secrets_used |= secrets
Expand Down Expand Up @@ -316,7 +317,7 @@ async def mint(
await self.crud.update_lightning_invoice(id=id, issued=True, db=self.db)
del self.locks[id]

self._verify_outputs(B_s)
await self._verify_outputs(B_s)

promises = await self._generate_promises(B_s, keyset)
logger.trace("generated promises")
Expand Down Expand Up @@ -479,6 +480,7 @@ async def split(

# Mark proofs as used and prepare new promises
async with get_db_connection(self.db) as conn:
# we do this in a single db transaction
promises = await self._generate_promises(outputs, keyset, conn)
await self._invalidate_proofs(proofs, conn)

Expand Down Expand Up @@ -525,10 +527,15 @@ async def _generate_promises(
) -> list[BlindedSignature]:
"""Generates a promises (Blind signatures) for given amount and returns a pair (amount, C').
Important: When a promises is once created it should be considered issued to the user since the user
will always be able to restore promises later through the backup restore endpoint. That means that additional
checks in the code that might decide not to return these promises should be avoided once this function is
called. Only call this function if the transaction is fully validated!
Args:
B_s (List[BlindedMessage]): Blinded secret (point on curve)
keyset (Optional[MintKeyset], optional): Which keyset to use. Private keys will be taken from this keyset. Defaults to None.
conn: (Optional[Connection], optional): Database connection to reuse. Will create a new one if not given. Defaults to None.
Returns:
list[BlindedSignature]: Generated BlindedSignatures.
"""
Expand Down
26 changes: 24 additions & 2 deletions cashu/mint/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async def verify_inputs_and_outputs(
self._verify_equation_balanced(proofs, outputs)

# Verify outputs
self._verify_outputs(outputs)
await self._verify_outputs(outputs)

# Verify inputs and outputs together
if not self._verify_input_output_amounts(proofs, outputs):
Expand All @@ -87,14 +87,36 @@ async def verify_inputs_and_outputs(
if outputs and not self._verify_output_spending_conditions(proofs, outputs):
raise TransactionError("validation of output spending conditions failed.")

def _verify_outputs(self, outputs: List[BlindedMessage]):
async def _verify_outputs(self, outputs: List[BlindedMessage]):
"""Verify that the outputs are valid."""
# Verify amounts of outputs
if not all([self._verify_amount(o.amount) for o in outputs]):
raise TransactionError("invalid amount.")
# verify that only unique outputs were used
if not self._verify_no_duplicate_outputs(outputs):
raise TransactionError("duplicate outputs.")
# verify that outputs have not been signed previously
if any(await self._check_outputs_issued_before(outputs)):
raise TransactionError("outputs have already been signed before.")

async def _check_outputs_issued_before(self, outputs: List[BlindedMessage]):
"""Checks whether the provided outputs have previously been signed by the mint
(which would lead to a duplication error later when trying to store these outputs again).
Args:
outputs (List[BlindedMessage]): Outputs to check
Returns:
result (List[bool]): Whether outputs are already present in the database.
"""
result = []
async with self.db.connect() as conn:
for output in outputs:
promise = await self.crud.get_promise(
B_=output.B_, db=self.db, conn=conn
)
result.append(False if promise is None else True)
return result

async def _check_proofs_spendable(self, proofs: List[Proof]) -> List[bool]:
"""Checks whether the proof was already spent."""
Expand Down
23 changes: 22 additions & 1 deletion tests/test_mint_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger):
# try to spend other proofs with the same outputs again
await assert_err(
ledger.split(proofs=inputs2, outputs=outputs),
"UNIQUE constraint failed: promises.B_b",
"outputs have already been signed before.",
)

# try to spend inputs2 again with new outputs
Expand All @@ -155,6 +155,27 @@ async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger):
await ledger.split(proofs=inputs2, outputs=outputs)


@pytest.mark.asyncio
async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128)
pay_if_regtest(invoice.bolt11)
output_amounts = [128]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
await ledger.mint(outputs, id=invoice.id)

# now try to mint with the same outputs again
invoice2 = await wallet1.request_mint(128)
pay_if_regtest(invoice2.bolt11)

await assert_err(
ledger.mint(outputs, id=invoice2.id),
"outputs have already been signed before.",
)


@pytest.mark.asyncio
async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64)
Expand Down

0 comments on commit 7d4ed95

Please sign in to comment.