diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 56531a6e..f736f828 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -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 @@ -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") @@ -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) @@ -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. """ diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index 188b433f..531207b1 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -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): @@ -87,7 +87,7 @@ 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]): @@ -95,6 +95,28 @@ def _verify_outputs(self, outputs: List[BlindedMessage]): # 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.""" diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index 464e2529..b2aef3ef 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -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 @@ -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)