From 449cb689da3f2ffb363d6b55bd7fbdda879785ab Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 24 Sep 2023 12:33:22 +0200 Subject: [PATCH 1/5] refactor mint verification --- cashu/mint/ledger.py | 108 +++++++++++-------------------------- cashu/mint/router.py | 2 +- cashu/mint/verification.py | 25 ++++++--- 3 files changed, 51 insertions(+), 84 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 7a3f5bd8..7cffd838 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -24,7 +24,6 @@ KeysetNotFoundError, LightningError, NotAllowedError, - TokenAlreadySpentError, TransactionError, ) from ..core.helpers import fee_reserve, sum_proofs @@ -198,11 +197,6 @@ async def _generate_promise( dleq=DLEQ(e=e.serialize(), s=s.serialize()), ) - def _check_proofs_spendable(self, proofs: List[Proof]): - """Checks whether the proofs were already spent.""" - if not all([p.secret not in self.proofs_used for p in proofs]): - raise TokenAlreadySpentError() - def _check_spendable(self, proof: Proof): """Checks whether the proof was already spent.""" return proof.secret not in self.proofs_used @@ -324,9 +318,7 @@ async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int): Returns: Tuple[bool, string, int]: Returns payment status, preimage of invoice, paid fees (in Millisatoshi) """ - logger.trace(f"_pay_lightning_invoice: paying Lightning invoice {invoice}") error, balance = await self.lightning.status() - logger.trace(f"_pay_lightning_invoice: Lightning wallet balance: {balance}") if error: raise LightningError(f"Lightning wallet not responding: {error}") ( @@ -352,10 +344,8 @@ async def _invalidate_proofs(self, proofs: List[Proof]): proof_msgs = set([p.secret for p in proofs]) self.proofs_used |= proof_msgs # store in db - logger.trace("crud: storing proofs") for p in proofs: await self.crud.invalidate_proof(proof=p, db=self.db) - logger.trace("crud: stored proofs") async def _set_proofs_pending( self, proofs: List[Proof], conn: Optional[Connection] = None @@ -374,13 +364,7 @@ async def _set_proofs_pending( await self._validate_proofs_pending(proofs, conn) for p in proofs: try: - logger.trace( - f"crud: _set_proofs_pending setting proof {p.secret} as pending" - ) await self.crud.set_proof_pending(proof=p, db=self.db, conn=conn) - logger.trace( - f"crud: _set_proofs_pending proof {p.secret} set as pending" - ) except Exception: raise TransactionError("proofs already pending.") @@ -392,22 +376,9 @@ async def _unset_proofs_pending( Args: proofs (List[Proof]): Proofs to delete. """ - # we try: except: this block in order to avoid that any errors here - # could block the _invalidate_proofs() call that happens afterwards. async with self.proofs_pending_lock: - try: - for p in proofs: - logger.trace( - f"crud: _unset_proofs_pending unsetting proof {p.secret} as" - " pending" - ) - await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn) - logger.trace( - f"crud: _unset_proofs_pending proof {p.secret} unset as pending" - ) - except Exception as e: - print(e) - pass + for p in proofs: + await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn) async def _validate_proofs_pending( self, proofs: List[Proof], conn: Optional[Connection] = None @@ -420,9 +391,7 @@ async def _validate_proofs_pending( Raises: Exception: At least one of the proofs is in the pending table. """ - logger.trace("crud: _validate_proofs_pending validating proofs") proofs_pending = await self.crud.get_proofs_pending(db=self.db, conn=conn) - logger.trace("crud: _validate_proofs_pending got proofs pending") for p in proofs: for pp in proofs_pending: if p.secret == pp.secret: @@ -615,49 +584,44 @@ async def melt( raise NotAllowedError( f"Maximum melt amount is {settings.mint_max_peg_out} sat." ) - fees_msat = await self.check_fees(invoice) + fees_sat = await self.get_melt_fees(invoice) # verify overspending attempt - assert ( - total_provided >= invoice_amount + fees_msat / 1000 - ), TransactionError("provided proofs not enough for Lightning payment.") + assert total_provided >= invoice_amount + fees_sat, TransactionError( + "provided proofs not enough for Lightning payment." + ) - # verify that proofs have not been spent yet - self._check_proofs_spendable(proofs) # verify spending inputs, outputs, and spending conditions - await self._verify_proofs_and_outputs(proofs, outputs) + await self.verify_inputs_and_outputs(proofs, outputs) - # promises to return for overpaid fees - return_promises: List[BlindedSignature] = [] if settings.lightning: logger.trace("paying lightning invoice") status, preimage, fee_msat = await self._pay_lightning_invoice( - invoice, fees_msat + invoice, fees_sat * 1000 ) logger.trace("paid lightning invoice") else: status, preimage, fee_msat = True, "preimage", 0 - logger.trace( - f"status: {status}, preimage: {preimage}, fee_msat: {fee_msat}" + logger.debug( + f"Melt status: {status}, preimage: {preimage}, fee_msat: {fee_msat}" ) - if status: - logger.trace("invalidating proofs") - await self._invalidate_proofs(proofs) - logger.trace("invalidated proofs") - # prepare change to compensate wallet for overpaid fees - assert fee_msat is not None, TransactionError("fees not valid") - if outputs: - return_promises = await self._generate_change_promises( - total_provided=total_provided, - invoice_amount=invoice_amount, - ln_fee_msat=fee_msat, - outputs=outputs, - ) - else: - logger.trace("lightning payment unsuccessful") + if not status: raise LightningError("Lightning payment unsuccessful.") + # melt successful, invalidate proofs + await self._invalidate_proofs(proofs) + + # prepare change to compensate wallet for overpaid fees + return_promises: List[BlindedSignature] = [] + if outputs and fee_msat: + return_promises = await self._generate_change_promises( + total_provided=total_provided, + invoice_amount=invoice_amount, + ln_fee_msat=fee_msat, + outputs=outputs, + ) + except Exception as e: logger.trace(f"exception: {e}") raise e @@ -688,7 +652,7 @@ async def check_proof_state( pending = await self._check_pending(proofs) return spendable, pending - async def check_fees(self, pr: str): + async def get_melt_fees(self, pr: str) -> int: """Returns the fee reserve (in sat) that a wallet must add to its proofs in order to pay a Lightning invoice. @@ -702,18 +666,18 @@ async def check_fees(self, pr: str): # if id does not exist (not internal), it returns paid = None if settings.lightning: decoded_invoice = bolt11.decode(pr) - amount = math.ceil(decoded_invoice.amount_msat / 1000) + amount_msat = decoded_invoice.amount_msat logger.trace( - "check_fees: checking lightning invoice:" + "get_melt_fees: checking lightning invoice:" f" {decoded_invoice.payment_hash}" ) paid = await self.lightning.get_invoice_status(decoded_invoice.payment_hash) - logger.trace(f"check_fees: paid: {paid}") + logger.trace(f"get_melt_fees: paid: {paid}") internal = paid.paid is False else: - amount = 0 + amount_msat = 0 internal = True - fees_msat = fee_reserve(amount * 1000, internal) + fees_msat = fee_reserve(amount_msat, internal) fee_sat = math.ceil(fees_msat / 1000) return fee_sat @@ -743,21 +707,11 @@ async def split( logger.trace("split called") await self._set_proofs_pending(proofs) - - total_amount = sum_proofs(proofs) - try: - # verify that amount is kosher - self._verify_amount(total_amount) - # verify overspending attempt - self._verify_equation_balanced(proofs, outputs) - # verify that proofs have not been spent yet - self._check_proofs_spendable(proofs) # verify spending inputs, outputs, and spending conditions - await self._verify_proofs_and_outputs(proofs, outputs) + await self.verify_inputs_and_outputs(proofs, outputs) # Mark proofs as used and prepare new promises await self._invalidate_proofs(proofs) - except Exception as e: logger.trace(f"split failed: {e}") raise e diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 3ede2c6b..8b00d0c1 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -232,7 +232,7 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse: This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu). """ logger.trace(f"> POST /checkfees: {payload}") - fees_sat = await ledger.check_fees(payload.pr) + fees_sat = await ledger.get_melt_fees(payload.pr) logger.trace(f"< POST /checkfees: {fees_sat}") return CheckFeesResponse(fee=fees_sat) diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index 59a0b2f9..b7bb6662 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -15,6 +15,7 @@ NoSecretInProofsError, NotAllowedError, SecretTooLongError, + TokenAlreadySpentError, TransactionError, ) from ..core.settings import settings @@ -27,8 +28,9 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets): keyset: MintKeyset keysets: MintKeysets + proofs_used: List[Proof] - async def _verify_proofs_and_outputs( + async def verify_inputs_and_outputs( self, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None ): """Checks all proofs and outputs for validity. @@ -45,25 +47,31 @@ async def _verify_proofs_and_outputs( Exception: BDHKE verification failed. """ # Verify inputs - + # Verify proofs are spendable + self._check_proofs_spendable(proofs) + # Verify amounts of inputs + if not all([self._verify_amount(p.amount) for p in proofs]): + raise TransactionError("invalid amount.") # Verify secret criteria if not all([self._verify_secret_criteria(p) for p in proofs]): raise TransactionError("secrets do not match criteria.") # verify that only unique proofs were used if not self._verify_no_duplicate_proofs(proofs): raise TransactionError("duplicate proofs.") - # Verify input spending conditions - if not all([self._verify_input_spending_conditions(p) for p in proofs]): - raise TransactionError("validation of input spending conditions failed.") # Verify ecash signatures if not all([self._verify_proof_bdhke(p) for p in proofs]): raise TransactionError("could not verify proofs.") + # Verify input spending conditions + if not all([self._verify_input_spending_conditions(p) for p in proofs]): + raise TransactionError("validation of input spending conditions failed.") if not outputs: return # Verify outputs - + # 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 promises.") @@ -73,6 +81,11 @@ async def _verify_proofs_and_outputs( if outputs and not self._verify_output_spending_conditions(proofs, outputs): raise TransactionError("validation of output spending conditions failed.") + def _check_proofs_spendable(self, proofs: List[Proof]): + """Checks whether the proofs were already spent.""" + if not all([p.secret not in self.proofs_used for p in proofs]): + raise TokenAlreadySpentError() + def _verify_secret_criteria(self, proof: Proof) -> Literal[True]: """Verifies that a secret is present and is not too long (DOS prevention).""" if proof.secret is None or proof.secret == "": From 1f586f56647904f0afd7857d716169f27dde3150 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 24 Sep 2023 12:47:43 +0200 Subject: [PATCH 2/5] test with lightning=true --- tests/conftest.py | 2 +- tests/test_mint.py | 7 ++-- tests/test_wallet.py | 67 +++++++++++++++++++++++++-------------- tests/test_wallet_htlc.py | 27 ++++++++++------ tests/test_wallet_p2pk.py | 39 +++++++++++++++-------- tests/test_wallet_p2sh.py | 9 ++++-- 6 files changed, 100 insertions(+), 51 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1404216c..2a7ac40b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,7 @@ settings.mint_host = "0.0.0.0" settings.mint_listen_port = SERVER_PORT settings.mint_url = SERVER_ENDPOINT -settings.lightning = False +settings.lightning = True settings.tor = False settings.mint_lightning_backend = "FakeWallet" settings.mint_database = "./test_data/test_mint" diff --git a/tests/test_mint.py b/tests/test_mint.py index cf95caca..07908e86 100644 --- a/tests/test_mint.py +++ b/tests/test_mint.py @@ -66,13 +66,14 @@ async def test_get_keyset(ledger: Ledger): @pytest.mark.asyncio async def test_mint(ledger: Ledger): + invoice, payment_hash = await ledger.request_mint(8) blinded_messages_mock = [ BlindedMessage( amount=8, B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239", ) ] - promises = await ledger.mint(blinded_messages_mock) + promises = await ledger.mint(blinded_messages_mock, hash=payment_hash) assert len(promises) assert promises[0].amount == 8 assert ( @@ -83,6 +84,7 @@ async def test_mint(ledger: Ledger): @pytest.mark.asyncio async def test_mint_invalid_blinded_message(ledger: Ledger): + invoice, payment_hash = await ledger.request_mint(8) blinded_messages_mock_invalid_key = [ BlindedMessage( amount=8, @@ -90,7 +92,8 @@ async def test_mint_invalid_blinded_message(ledger: Ledger): ) ] await assert_err( - ledger.mint(blinded_messages_mock_invalid_key), "invalid public key" + ledger.mint(blinded_messages_mock_invalid_key, hash=payment_hash), + "invalid public key", ) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index b4e5cb07..b99427f2 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -136,15 +136,17 @@ async def test_get_keyset_ids(wallet1: Wallet): @pytest.mark.asyncio async def test_mint(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) assert wallet1.balance == 64 @pytest.mark.asyncio async def test_mint_amounts(wallet1: Wallet): """Mint predefined amounts""" + invoice = await wallet1.request_mint(64) amts = [1, 1, 1, 2, 2, 4, 16] - await wallet1.mint(amount=sum(amts), split=amts) + await wallet1.mint(amount=sum(amts), split=amts, hash=invoice.hash) assert wallet1.balance == 27 assert wallet1.proof_amounts == amts @@ -171,7 +173,8 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet): @pytest.mark.asyncio async def test_split(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) assert wallet1.balance == 64 p1, p2 = await wallet1.split(wallet1.proofs, 20) assert wallet1.balance == 64 @@ -185,7 +188,8 @@ async def test_split(wallet1: Wallet): @pytest.mark.asyncio async def test_split_to_send(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) keep_proofs, spendable_proofs = await wallet1.split_to_send( wallet1.proofs, 32, set_reserved=True ) @@ -199,7 +203,8 @@ async def test_split_to_send(wallet1: Wallet): @pytest.mark.asyncio async def test_split_more_than_balance(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) await assert_err( wallet1.split(wallet1.proofs, 128), # "Mint Error: inputs do not have same amount as outputs", @@ -210,7 +215,8 @@ async def test_split_more_than_balance(wallet1: Wallet): @pytest.mark.asyncio async def test_split_to_send_more_than_balance(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) await assert_err( wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True), "balance too low.", @@ -221,7 +227,8 @@ async def test_split_to_send_more_than_balance(wallet1: Wallet): @pytest.mark.asyncio async def test_double_spend(wallet1: Wallet): - doublespend = await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + doublespend = await wallet1.mint(64, hash=invoice.hash) await wallet1.split(wallet1.proofs, 20) await assert_err( wallet1.split(doublespend, 20), @@ -233,7 +240,8 @@ async def test_double_spend(wallet1: Wallet): @pytest.mark.asyncio async def test_duplicate_proofs_double_spent(wallet1: Wallet): - doublespend = await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + doublespend = await wallet1.mint(64, hash=invoice.hash) await assert_err( wallet1.split(wallet1.proofs + doublespend, 20), "Mint Error: proofs already pending.", @@ -244,7 +252,8 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet): @pytest.mark.asyncio async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) _, spendable_proofs = await wallet1.split_to_send( wallet1.proofs, 32, set_reserved=True ) @@ -261,7 +270,8 @@ async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_invalidate_unspent_proofs(wallet1: Wallet): """Try to invalidate proofs that have not been spent yet. Should not work!""" - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) await wallet1.invalidate(wallet1.proofs) assert wallet1.balance == 64 @@ -269,14 +279,16 @@ async def test_invalidate_unspent_proofs(wallet1: Wallet): @pytest.mark.asyncio async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet): """Try to invalidate proofs that have not been spent yet but force no check.""" - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) await wallet1.invalidate(wallet1.proofs, check_spendable=False) assert wallet1.balance == 0 @pytest.mark.asyncio async def test_split_invalid_amount(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) await assert_err( wallet1.split(wallet1.proofs, -1), "amount must be positive.", @@ -285,14 +297,16 @@ async def test_split_invalid_amount(wallet1: Wallet): @pytest.mark.asyncio async def test_create_p2pk_pubkey(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey = await wallet1.create_p2pk_pubkey() PublicKey(bytes.fromhex(pubkey), raw=True) @pytest.mark.asyncio async def test_p2sh(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) _ = await wallet1.create_p2sh_address_and_store() # receiver side _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8) # sender side @@ -305,7 +319,8 @@ async def test_p2sh(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) wallet1_address = await wallet1.create_p2sh_address_and_store() # receiver side secret_lock = await wallet1.create_p2sh_lock(wallet1_address) # sender side _, send_proofs = await wallet1.split_to_send( @@ -316,7 +331,8 @@ async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_token_state(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) assert wallet1.balance == 64 resp = await wallet1.check_proof_state(wallet1.proofs) assert resp.dict()["spendable"] @@ -382,7 +398,8 @@ async def test_generate_secrets_from_to(wallet3: Wallet): @pytest.mark.asyncio async def test_restore_wallet_after_mint(wallet3: Wallet): await reset_wallet_db(wallet3) - await wallet3.mint(64) + invoice = await wallet3.request_mint(64) + await wallet3.mint(64, hash=invoice.hash) assert wallet3.balance == 64 await reset_wallet_db(wallet3) await wallet3.load_proofs() @@ -411,7 +428,8 @@ async def test_restore_wallet_after_split_to_send(wallet3: Wallet): ) await reset_wallet_db(wallet3) - await wallet3.mint(64) + invoice = await wallet3.request_mint(64) + await wallet3.mint(64, hash=invoice.hash) assert wallet3.balance == 64 _, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore @@ -432,8 +450,8 @@ async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: W "hello rug want adapt talent together lunar method bean expose beef position" ) await reset_wallet_db(wallet3) - - await wallet3.mint(64) + invoice = await wallet3.request_mint(64) + await wallet3.mint(64, hash=invoice.hash) assert wallet3.balance == 64 _, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore @@ -472,7 +490,8 @@ async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet): ) await reset_wallet_db(wallet3) - await wallet3.mint(64) + invoice = await wallet3.request_mint(64) + await wallet3.mint(64, hash=invoice.hash) assert wallet3.balance == 64 _, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore @@ -497,7 +516,8 @@ async def test_restore_wallet_after_send_twice( wallet3.private_key = PrivateKey() await reset_wallet_db(wallet3) - await wallet3.mint(2) + invoice = await wallet3.request_mint(2) + await wallet3.mint(2, hash=invoice.hash) box.add(wallet3.proofs) assert wallet3.balance == 2 @@ -550,7 +570,8 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value( ) await reset_wallet_db(wallet3) - await wallet3.mint(64) + invoice = await wallet3.request_mint(64) + await wallet3.mint(64, hash=invoice.hash) box.add(wallet3.proofs) assert wallet3.balance == 64 diff --git a/tests/test_wallet_htlc.py b/tests/test_wallet_htlc.py index 3303e099..5128b3ab 100644 --- a/tests/test_wallet_htlc.py +++ b/tests/test_wallet_htlc.py @@ -58,7 +58,8 @@ async def wallet2(mint): @pytest.mark.asyncio async def test_create_htlc_secret(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock(preimage=preimage) @@ -67,7 +68,8 @@ async def test_create_htlc_secret(wallet1: Wallet): @pytest.mark.asyncio async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock(preimage=preimage) @@ -79,7 +81,8 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock(preimage=preimage) @@ -92,7 +95,8 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock(preimage=preimage[:-1] + "1") @@ -107,7 +111,8 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet) @pytest.mark.asyncio async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -126,7 +131,8 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -149,7 +155,8 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet @pytest.mark.asyncio async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -171,7 +178,8 @@ async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wall async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( wallet1: Wallet, wallet2: Wallet ): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -205,7 +213,8 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature( wallet1: Wallet, wallet2: Wallet ): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index db16f54a..883e64bd 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -59,14 +59,16 @@ async def wallet2(mint): @pytest.mark.asyncio async def test_create_p2pk_pubkey(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey = await wallet1.create_p2pk_pubkey() PublicKey(bytes.fromhex(pubkey), raw=True) @pytest.mark.asyncio async def test_p2pk(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side @@ -78,7 +80,8 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side @@ -97,7 +100,8 @@ async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wal async def test_p2pk_short_locktime_receive_with_wrong_private_key( wallet1: Wallet, wallet2: Wallet ): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side secret_lock = await wallet1.create_p2pk_lock( @@ -121,7 +125,8 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key( @pytest.mark.asyncio async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side garbage_pubkey = PrivateKey().pubkey @@ -148,7 +153,8 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet @pytest.mark.asyncio async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) await wallet2.create_p2pk_pubkey() # receiver side # sender side garbage_pubkey = PrivateKey().pubkey @@ -182,7 +188,8 @@ async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: async def test_p2pk_locktime_with_second_refund_pubkey( wallet1: Wallet, wallet2: Wallet ): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -212,7 +219,8 @@ async def test_p2pk_locktime_with_second_refund_pubkey( @pytest.mark.asyncio async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() assert pubkey_wallet1 != pubkey_wallet2 @@ -232,7 +240,8 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() assert pubkey_wallet1 != pubkey_wallet2 @@ -254,7 +263,8 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle @pytest.mark.asyncio async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() assert pubkey_wallet1 != pubkey_wallet2 @@ -273,7 +283,8 @@ async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wal @pytest.mark.asyncio async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() assert pubkey_wallet1 != pubkey_wallet2 @@ -296,7 +307,8 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal @pytest.mark.asyncio async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test secret_lock = await wallet1.create_p2pk_lock( @@ -312,7 +324,8 @@ async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: async def test_p2pk_multisig_with_wrong_first_private_key( wallet1: Wallet, wallet2: Wallet ): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() wrong_pubklic_key = PrivateKey().pubkey diff --git a/tests/test_wallet_p2sh.py b/tests/test_wallet_p2sh.py index 95402047..0ae72185 100644 --- a/tests/test_wallet_p2sh.py +++ b/tests/test_wallet_p2sh.py @@ -56,14 +56,16 @@ async def wallet2(mint): @pytest.mark.asyncio async def test_create_p2pk_pubkey(wallet1: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) pubkey = await wallet1.create_p2pk_pubkey() PublicKey(bytes.fromhex(pubkey), raw=True) @pytest.mark.asyncio async def test_p2sh(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) _ = await wallet1.create_p2sh_address_and_store() # receiver side _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8) # sender side @@ -76,7 +78,8 @@ async def test_p2sh(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet): - await wallet1.mint(64) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) wallet1_address = await wallet1.create_p2sh_address_and_store() # receiver side secret_lock = await wallet1.create_p2sh_lock(wallet1_address) # sender side _, send_proofs = await wallet1.split_to_send( From 51c27531c86cfda6d529dc0ba196e7f9327b2fcf Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 24 Sep 2023 13:01:37 +0200 Subject: [PATCH 3/5] rename proofs_used to secrets_used and refactor --- cashu/mint/crud.py | 6 +- cashu/mint/ledger.py | 317 +++++++++++++++++++------------------ cashu/mint/verification.py | 6 +- 3 files changed, 170 insertions(+), 159 deletions(-) diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 5759ccfc..7faa1bc0 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -17,8 +17,8 @@ async def get_keyset(*args, **kwags): async def get_lightning_invoice(*args, **kwags): return await get_lightning_invoice(*args, **kwags) # type: ignore - async def get_proofs_used(*args, **kwags): - return await get_proofs_used(*args, **kwags) # type: ignore + async def get_secrets_used(*args, **kwags): + return await get_secrets_used(*args, **kwags) # type: ignore async def invalidate_proof(*args, **kwags): return await invalidate_proof(*args, **kwags) # type: ignore @@ -91,7 +91,7 @@ async def get_promise( return BlindedSignature(amount=row[0], C_=row[2], id=row[3]) if row else None -async def get_proofs_used( +async def get_secrets_used( db: Database, conn: Optional[Connection] = None, ): diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 7cffd838..56186b67 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -49,7 +49,7 @@ def __init__( derivation_path="", crud=LedgerCrud, ): - self.proofs_used: Set[str] = set() + self.secrets_used: Set[str] = set() self.master_key = seed self.derivation_path = derivation_path @@ -59,12 +59,7 @@ def __init__( self.pubkey = derive_pubkey(self.master_key) self.keysets = MintKeysets([]) - async def load_used_proofs(self): - """Load all used proofs from database.""" - logger.trace("crud: loading used proofs") - proofs_used = await self.crud.get_proofs_used(db=self.db) - logger.trace(f"crud: loaded {len(proofs_used)} used proofs") - self.proofs_used = set(proofs_used) + # ------- KEYS ------- async def load_keyset(self, derivation_path, autosave=True) -> MintKeyset: """Load the keyset for a derivation path if it already exists. If not generate new one and store in the db. @@ -143,72 +138,15 @@ async def init_keysets(self, autosave=True): # load the current keyset self.keyset = await self.load_keyset(self.derivation_path, autosave) - async def _generate_promises( - self, B_s: List[BlindedMessage], keyset: Optional[MintKeyset] = None - ) -> list[BlindedSignature]: - """Generates promises that sum to the given amount. - - Args: - B_s (List[BlindedMessage]): _description_ - keyset (Optional[MintKeyset], optional): _description_. Defaults to None. - - Returns: - list[BlindedSignature]: _description_ - """ - return [ - await self._generate_promise( - b.amount, PublicKey(bytes.fromhex(b.B_), raw=True), keyset - ) - for b in B_s - ] - - async def _generate_promise( - self, amount: int, B_: PublicKey, keyset: Optional[MintKeyset] = None - ) -> BlindedSignature: - """Generates a promise (Blind signature) for given amount and returns a pair (amount, C'). - - Args: - amount (int): Amount of the promise. - B_ (PublicKey): Blinded secret (point on curve) - keyset (Optional[MintKeyset], optional): Which keyset to use. Private keys will be taken from this keyset. Defaults to None. - - Returns: - BlindedSignature: Generated promise. - """ - keyset = keyset if keyset else self.keyset - logger.trace(f"Generating promise with keyset {keyset.id}.") - private_key_amount = keyset.private_keys[amount] - C_, e, s = b_dhke.step2_bob(B_, private_key_amount) - logger.trace(f"crud: _generate_promise storing promise for {amount}") - await self.crud.store_promise( - amount=amount, - B_=B_.serialize().hex(), - C_=C_.serialize().hex(), - e=e.serialize(), - s=s.serialize(), - db=self.db, - id=keyset.id, - ) - logger.trace(f"crud: _generate_promise stored promise for {amount}") - return BlindedSignature( - id=keyset.id, - amount=amount, - C_=C_.serialize().hex(), - dleq=DLEQ(e=e.serialize(), s=s.serialize()), - ) - - def _check_spendable(self, proof: Proof): - """Checks whether the proof was already spent.""" - return proof.secret not in self.proofs_used + def get_keyset(self, keyset_id: Optional[str] = None): + """Returns a dictionary of hex public keys of a specific keyset for each supported amount""" + if keyset_id and keyset_id not in self.keysets.keysets: + raise KeysetNotFoundError() + keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset + assert keyset.public_keys, KeysetError("no public keys for this keyset") + return {a: p.serialize().hex() for a, p in keyset.public_keys.items()} - async def _check_pending(self, proofs: List[Proof]): - """Checks whether the proof is still pending.""" - proofs_pending = await self.crud.get_proofs_pending(db=self.db) - pending_secrets = [pp.secret for pp in proofs_pending] - pending_states = [ - True if p.secret in pending_secrets else False for p in proofs - ] - return pending_states + # ------- LIGHTNING ------- async def _request_lightning_invoice(self, amount: int): """Generate a Lightning invoice using the funding source backend. @@ -333,6 +271,8 @@ async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int): fee_msat = abs(fee_msat) if fee_msat else fee_msat return ok, preimage, fee_msat + # ------- ECASH ------- + async def _invalidate_proofs(self, proofs: List[Proof]): """Adds secrets of proofs to the list of known secrets and stores them in the db. Removes proofs from pending table. This is executed if the ecash has been redeemed. @@ -341,62 +281,12 @@ async def _invalidate_proofs(self, proofs: List[Proof]): proofs (List[Proof]): Proofs to add to known secret table. """ # Mark proofs as used and prepare new promises - proof_msgs = set([p.secret for p in proofs]) - self.proofs_used |= proof_msgs + secrets = set([p.secret for p in proofs]) + self.secrets_used |= secrets # store in db for p in proofs: await self.crud.invalidate_proof(proof=p, db=self.db) - async def _set_proofs_pending( - self, proofs: List[Proof], conn: Optional[Connection] = None - ): - """If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to - the list of pending proofs or removes them. Used as a mutex for proofs. - - Args: - proofs (List[Proof]): Proofs to add to pending table. - - Raises: - Exception: At least one proof already in pending table. - """ - # first we check whether these proofs are pending aready - async with self.proofs_pending_lock: - await self._validate_proofs_pending(proofs, conn) - for p in proofs: - try: - await self.crud.set_proof_pending(proof=p, db=self.db, conn=conn) - except Exception: - raise TransactionError("proofs already pending.") - - async def _unset_proofs_pending( - self, proofs: List[Proof], conn: Optional[Connection] = None - ): - """Deletes proofs from pending table. - - Args: - proofs (List[Proof]): Proofs to delete. - """ - async with self.proofs_pending_lock: - for p in proofs: - await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn) - - async def _validate_proofs_pending( - self, proofs: List[Proof], conn: Optional[Connection] = None - ): - """Checks if any of the provided proofs is in the pending proofs table. - - Args: - proofs (List[Proof]): Proofs to check. - - Raises: - Exception: At least one of the proofs is in the pending table. - """ - proofs_pending = await self.crud.get_proofs_pending(db=self.db, conn=conn) - for p in proofs: - for pp in proofs_pending: - if p.secret == pp.secret: - raise TransactionError("proofs are pending.") - async def _generate_change_promises( self, total_provided: int, @@ -459,14 +349,7 @@ async def _generate_change_promises( else: return [] - # Public methods - def get_keyset(self, keyset_id: Optional[str] = None): - """Returns a dictionary of hex public keys of a specific keyset for each supported amount""" - if keyset_id and keyset_id not in self.keysets.keysets: - raise KeysetNotFoundError() - keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset - assert keyset.public_keys, KeysetError("no public keys for this keyset") - return {a: p.serialize().hex() for a, p in keyset.public_keys.items()} + # ------- TRANSACTIONS ------- async def request_mint(self, amount: int): """Returns Lightning invoice and stores it in the db. @@ -631,27 +514,6 @@ async def melt( return status, preimage, return_promises - async def check_proof_state( - self, proofs: List[Proof] - ) -> Tuple[List[bool], List[bool]]: - """Checks if provided proofs are spend or are pending. - Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction. - - Returns two lists that are in the same order as the provided proofs. Wallet must match the list - to the proofs they have provided in order to figure out which proof is spendable or pending - and which isn't. - - Args: - proofs (List[Proof]): List of proofs to check. - - Returns: - List[bool]: List of which proof is still spendable (True if still spendable, else False) - List[bool]: List of which proof are pending (True if pending, else False) - """ - spendable = [self._check_spendable(p) for p in proofs] - pending = await self._check_pending(proofs) - return spendable, pending - async def get_melt_fees(self, pr: str) -> int: """Returns the fee reserve (in sat) that a wallet must add to its proofs in order to pay a Lightning invoice. @@ -769,3 +631,152 @@ async def restore( return_outputs.append(output) logger.trace(f"promise found: {promise}") return return_outputs, promises + + # ------- BLIND SIGNATURES ------- + + async def _generate_promises( + self, B_s: List[BlindedMessage], keyset: Optional[MintKeyset] = None + ) -> list[BlindedSignature]: + """Generates promises that sum to the given amount. + + Args: + B_s (List[BlindedMessage]): _description_ + keyset (Optional[MintKeyset], optional): _description_. Defaults to None. + + Returns: + list[BlindedSignature]: _description_ + """ + return [ + await self._generate_promise( + b.amount, PublicKey(bytes.fromhex(b.B_), raw=True), keyset + ) + for b in B_s + ] + + async def _generate_promise( + self, amount: int, B_: PublicKey, keyset: Optional[MintKeyset] = None + ) -> BlindedSignature: + """Generates a promise (Blind signature) for given amount and returns a pair (amount, C'). + + Args: + amount (int): Amount of the promise. + B_ (PublicKey): Blinded secret (point on curve) + keyset (Optional[MintKeyset], optional): Which keyset to use. Private keys will be taken from this keyset. Defaults to None. + + Returns: + BlindedSignature: Generated promise. + """ + keyset = keyset if keyset else self.keyset + logger.trace(f"Generating promise with keyset {keyset.id}.") + private_key_amount = keyset.private_keys[amount] + C_, e, s = b_dhke.step2_bob(B_, private_key_amount) + logger.trace(f"crud: _generate_promise storing promise for {amount}") + await self.crud.store_promise( + amount=amount, + B_=B_.serialize().hex(), + C_=C_.serialize().hex(), + e=e.serialize(), + s=s.serialize(), + db=self.db, + id=keyset.id, + ) + logger.trace(f"crud: _generate_promise stored promise for {amount}") + return BlindedSignature( + id=keyset.id, + amount=amount, + C_=C_.serialize().hex(), + dleq=DLEQ(e=e.serialize(), s=s.serialize()), + ) + + # ------- PROOFS ------- + + async def load_used_proofs(self): + """Load all used proofs from database.""" + logger.trace("crud: loading used proofs") + secrets_used = await self.crud.get_secrets_used(db=self.db) + logger.trace(f"crud: loaded {len(secrets_used)} used proofs") + self.secrets_used = set(secrets_used) + + def _check_spendable(self, proof: Proof): + """Checks whether the proof was already spent.""" + return proof.secret not in self.secrets_used + + async def _check_pending(self, proofs: List[Proof]): + """Checks whether the proof is still pending.""" + proofs_pending = await self.crud.get_proofs_pending(db=self.db) + pending_secrets = [pp.secret for pp in proofs_pending] + pending_states = [ + True if p.secret in pending_secrets else False for p in proofs + ] + return pending_states + + async def check_proof_state( + self, proofs: List[Proof] + ) -> Tuple[List[bool], List[bool]]: + """Checks if provided proofs are spend or are pending. + Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction. + + Returns two lists that are in the same order as the provided proofs. Wallet must match the list + to the proofs they have provided in order to figure out which proof is spendable or pending + and which isn't. + + Args: + proofs (List[Proof]): List of proofs to check. + + Returns: + List[bool]: List of which proof is still spendable (True if still spendable, else False) + List[bool]: List of which proof are pending (True if pending, else False) + """ + spendable = [self._check_spendable(p) for p in proofs] + pending = await self._check_pending(proofs) + return spendable, pending + + async def _set_proofs_pending( + self, proofs: List[Proof], conn: Optional[Connection] = None + ): + """If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to + the list of pending proofs or removes them. Used as a mutex for proofs. + + Args: + proofs (List[Proof]): Proofs to add to pending table. + + Raises: + Exception: At least one proof already in pending table. + """ + # first we check whether these proofs are pending aready + async with self.proofs_pending_lock: + await self._validate_proofs_pending(proofs, conn) + for p in proofs: + try: + await self.crud.set_proof_pending(proof=p, db=self.db, conn=conn) + except Exception: + raise TransactionError("proofs already pending.") + + async def _unset_proofs_pending( + self, proofs: List[Proof], conn: Optional[Connection] = None + ): + """Deletes proofs from pending table. + + Args: + proofs (List[Proof]): Proofs to delete. + """ + async with self.proofs_pending_lock: + for p in proofs: + await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn) + + async def _validate_proofs_pending( + self, proofs: List[Proof], conn: Optional[Connection] = None + ): + """Checks if any of the provided proofs is in the pending proofs table. + + Args: + proofs (List[Proof]): Proofs to check. + + Raises: + Exception: At least one of the proofs is in the pending table. + """ + proofs_pending = await self.crud.get_proofs_pending(db=self.db, conn=conn) + for p in proofs: + for pp in proofs_pending: + if p.secret == pp.secret: + raise TransactionError("proofs are pending.") diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index b7bb6662..61df9dc3 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -1,4 +1,4 @@ -from typing import List, Literal, Optional, Union +from typing import List, Literal, Optional, Set, Union from loguru import logger @@ -28,7 +28,7 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets): keyset: MintKeyset keysets: MintKeysets - proofs_used: List[Proof] + secrets_used: Set[str] async def verify_inputs_and_outputs( self, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None @@ -83,7 +83,7 @@ async def verify_inputs_and_outputs( def _check_proofs_spendable(self, proofs: List[Proof]): """Checks whether the proofs were already spent.""" - if not all([p.secret not in self.proofs_used for p in proofs]): + if not all([p.secret not in self.secrets_used for p in proofs]): raise TokenAlreadySpentError() def _verify_secret_criteria(self, proof: Proof) -> Literal[True]: From 45a4956886a6592c562e716a0b2850ceb8cd314f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 24 Sep 2023 13:55:32 +0200 Subject: [PATCH 4/5] test with lightning --- cashu/mint/ledger.py | 66 +++++++++++++------------------------- cashu/mint/verification.py | 18 +++++++---- cashu/wallet/secrets.py | 3 ++ cashu/wallet/wallet.py | 3 ++ tests/test_wallet.py | 17 ++++++++++ 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 56186b67..ad49c49c 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -198,50 +198,26 @@ async def _check_lightning_invoice( Returns: bool: True if invoice has been paid, else False """ - logger.trace(f"crud: _check_lightning_invoice: checking invoice {hash}") invoice: Union[Invoice, None] = await self.crud.get_lightning_invoice( hash=hash, db=self.db, conn=conn ) - logger.trace(f"crud: _check_lightning_invoice: invoice: {invoice}") if invoice is None: raise LightningError("invoice not found.") if invoice.issued: raise LightningError("tokens already issued for this invoice.") + if amount > invoice.amount: + raise LightningError( + f"requested amount too high: {amount}. Invoice amount: {invoice.amount}" + ) assert invoice.payment_hash, "invoice has no payment hash." - - # set this invoice as issued - logger.trace(f"crud: setting invoice {invoice.payment_hash} as issued") - await self.crud.update_lightning_invoice( - hash=hash, issued=True, db=self.db, conn=conn + status = await self.lightning.get_invoice_status(invoice.payment_hash) + logger.trace( + f"_check_lightning_invoice: invoice {invoice.payment_hash} status: {status}" ) - logger.trace(f"crud: invoice {invoice.payment_hash} set as issued") + if not status.paid: + raise InvoiceNotPaidError() - try: - if amount > invoice.amount: - raise LightningError( - f"requested amount too high: {amount}. Invoice amount:" - f" {invoice.amount}" - ) - logger.trace( - f"_check_lightning_invoice: checking invoice {invoice.payment_hash}" - ) - status = await self.lightning.get_invoice_status(invoice.payment_hash) - logger.trace( - f"_check_lightning_invoice: invoice {invoice.payment_hash} status:" - f" {status}" - ) - if status.paid: - return status.paid - else: - raise InvoiceNotPaidError() - except Exception as e: - # unset issued - logger.trace(f"crud: unsetting invoice {invoice.payment_hash} as issued") - await self.crud.update_lightning_invoice( - hash=hash, issued=False, db=self.db, conn=conn - ) - logger.trace(f"crud: invoice {invoice.payment_hash} unset as issued") - raise e + return status.paid async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int): """Pays a Lightning invoice via the funding source backend. @@ -413,8 +389,7 @@ async def mint( List[BlindedSignature]: Signatures on the outputs. """ logger.trace("called mint") - amounts = [b.amount for b in B_s] - amount = sum(amounts) + amount_outputs = sum([b.amount for b in B_s]) if settings.lightning: if not hash: @@ -423,15 +398,17 @@ async def mint( self.locks.get(hash) or asyncio.Lock() ) # create a new lock if it doesn't exist async with self.locks[hash]: - # will raise an exception if the invoice is not paid or tokens are already issued - await self._check_lightning_invoice(amount, hash) - del self.locks[hash] + # will raise an exception if the invoice is not paid or tokens are + # already issued or the requested amount is too high + await self._check_lightning_invoice(amount_outputs, hash) - for amount in amounts: - if amount not in [2**i for i in range(settings.max_order)]: - raise NotAllowedError( - f"Can only mint amounts with 2^n up to {2**settings.max_order}." + logger.trace(f"crud: setting invoice {hash} as issued") + await self.crud.update_lightning_invoice( + hash=hash, issued=True, db=self.db ) + del self.locks[hash] + + self._verify_outputs(B_s) promises = await self._generate_promises(B_s, keyset) logger.trace("generated promises") @@ -470,7 +447,8 @@ async def melt( fees_sat = await self.get_melt_fees(invoice) # verify overspending attempt assert total_provided >= invoice_amount + fees_sat, TransactionError( - "provided proofs not enough for Lightning payment." + "provided proofs not enough for Lightning payment. Provided:" + f" {total_provided}, needed: {invoice_amount + fees_sat}" ) # verify spending inputs, outputs, and spending conditions diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index 61df9dc3..434dca78 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -69,18 +69,24 @@ async def verify_inputs_and_outputs( return # Verify outputs - # 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 promises.") + self._verify_outputs(outputs) + + # Verify inputs and outputs together if not self._verify_input_output_amounts(proofs, outputs): raise TransactionError("input amounts less than output.") # Verify output spending conditions 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]): + """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.") + def _check_proofs_spendable(self, proofs: List[Proof]): """Checks whether the proofs were already spent.""" if not all([p.secret not in self.secrets_used for p in proofs]): diff --git a/cashu/wallet/secrets.py b/cashu/wallet/secrets.py index 7c5342a2..07732682 100644 --- a/cashu/wallet/secrets.py +++ b/cashu/wallet/secrets.py @@ -148,6 +148,9 @@ async def generate_n_secrets( Tuple[List[str], List[PrivateKey], List[str]]: Secrets, blinding factors, derivation paths """ + if n < 1: + return [], [], [] + secret_counters_start = await bump_secret_derivation( db=self.db, keyset_id=self.keyset_id, by=n, skip=skip_bump ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 7fd04e75..15a7a4bf 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -1220,6 +1220,9 @@ async def split_to_send( set_reserved (bool, optional): If set, the proofs are marked as reserved. Should be set to False if a payment attempt is made with the split that could fail (like a Lightning payment). Should be set to True if the token to be sent is displayed to the user to be then sent to someone else. Defaults to False. + + Returns: + Tuple[List[Proof], List[Proof]]: Tuple of proofs to keep and proofs to send """ if secret_lock: logger.debug(f"Spending conditions: {secret_lock}") diff --git a/tests/test_wallet.py b/tests/test_wallet.py index b99427f2..7f0d8e27 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -213,6 +213,23 @@ async def test_split_more_than_balance(wallet1: Wallet): assert wallet1.balance == 64 +@pytest.mark.asyncio +async def test_melt(wallet1: Wallet): + # mint twice so we have enough to pay the second invoice back + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) + assert wallet1.balance == 128 + total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(invoice.pr) + _, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) + + await wallet1.pay_lightning( + send_proofs, invoice=invoice.pr, fee_reserve_sat=fee_reserve_sat + ) + assert wallet1.balance == 128 - total_amount + + @pytest.mark.asyncio async def test_split_to_send_more_than_balance(wallet1: Wallet): invoice = await wallet1.request_mint(64) From 3bd10755858e47c4129bdcc55b62b9c3b6b4a607 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 24 Sep 2023 14:25:21 +0200 Subject: [PATCH 5/5] spelling fixes --- cashu/mint/ledger.py | 4 ++-- tests/test_wallet.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index ad49c49c..b324f96f 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -455,7 +455,7 @@ async def melt( await self.verify_inputs_and_outputs(proofs, outputs) if settings.lightning: - logger.trace("paying lightning invoice") + logger.trace(f"paying lightning invoice {invoice}") status, preimage, fee_msat = await self._pay_lightning_invoice( invoice, fees_sat * 1000 ) @@ -464,7 +464,7 @@ async def melt( status, preimage, fee_msat = True, "preimage", 0 logger.debug( - f"Melt status: {status}, preimage: {preimage}, fee_msat: {fee_msat}" + f"Melt status: {status}: preimage: {preimage}, fee_msat: {fee_msat}" ) if not status: diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 7f0d8e27..d401be53 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -362,11 +362,11 @@ async def test_bump_secret_derivation(wallet3: Wallet): "half depart obvious quality work element tank gorilla view sugar picture" " humble" ) - secrets1, rs1, derivaion_paths1 = await wallet3.generate_n_secrets(5) - secrets2, rs2, derivaion_paths2 = await wallet3.generate_secrets_from_to(0, 4) + secrets1, rs1, derivation_paths1 = await wallet3.generate_n_secrets(5) + secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(0, 4) assert secrets1 == secrets2 assert [r.private_key for r in rs1] == [r.private_key for r in rs2] - assert derivaion_paths1 == derivaion_paths2 + assert derivation_paths1 == derivation_paths2 assert secrets1 == [ "9bfb12704297fe90983907d122838940755fcce370ce51e9e00a4275a347c3fe", "dbc5e05f2b1f24ec0e2ab6e8312d5e13f57ada52594d4caf429a697d9c742490", @@ -374,7 +374,7 @@ async def test_bump_secret_derivation(wallet3: Wallet): "652d08c804bd2c5f2c1f3e3d8895860397df394b30473753227d766affd15e89", "654e5997f8a20402f7487296b6f7e463315dd52fc6f6cc5a4e35c7f6ccac77e0", ] - assert derivaion_paths1 == [ + assert derivation_paths1 == [ "m/129372'/0'/2004500376'/0'", "m/129372'/0'/2004500376'/1'", "m/129372'/0'/2004500376'/2'", @@ -389,11 +389,11 @@ async def test_bump_secret_derivation_two_steps(wallet3: Wallet): "half depart obvious quality work element tank gorilla view sugar picture" " humble" ) - secrets1_1, rs1_1, derivaion_paths1 = await wallet3.generate_n_secrets(2) - secrets1_2, rs1_2, derivaion_paths2 = await wallet3.generate_n_secrets(3) + secrets1_1, rs1_1, derivation_paths1 = await wallet3.generate_n_secrets(2) + secrets1_2, rs1_2, derivation_paths2 = await wallet3.generate_n_secrets(3) secrets1 = secrets1_1 + secrets1_2 rs1 = rs1_1 + rs1_2 - secrets2, rs2, derivaion_paths = await wallet3.generate_secrets_from_to(0, 4) + secrets2, rs2, derivation_paths = await wallet3.generate_secrets_from_to(0, 4) assert secrets1 == secrets2 assert [r.private_key for r in rs1] == [r.private_key for r in rs2] @@ -404,9 +404,9 @@ async def test_generate_secrets_from_to(wallet3: Wallet): "half depart obvious quality work element tank gorilla view sugar picture" " humble" ) - secrets1, rs1, derivaion_paths1 = await wallet3.generate_secrets_from_to(0, 4) + secrets1, rs1, derivation_paths1 = await wallet3.generate_secrets_from_to(0, 4) assert len(secrets1) == 5 - secrets2, rs2, derivaion_paths2 = await wallet3.generate_secrets_from_to(2, 4) + secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(2, 4) assert len(secrets2) == 3 assert secrets1[2:] == secrets2 assert [r.private_key for r in rs1[2:]] == [r.private_key for r in rs2]