From d08b8a00f6470f7bad061d2d3543ff7b2196b239 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 14 Dec 2022 22:50:25 +0100 Subject: [PATCH 1/5] check pending proofs --- cashu/mint/crud.py | 61 ++++++++++++++++++++++++++++++++++++++++ cashu/mint/ledger.py | 45 ++++++++++++++++++++++++++++- cashu/mint/migrations.py | 18 ++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 313dace..aa53bd5 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -29,6 +29,18 @@ class LedgerCrud: return await invalidate_proof(*args, **kwags) + async def get_proofs_pending(*args, **kwags): + + return await get_proofs_pending(*args, **kwags) + + async def set_proof_pending(*args, **kwags): + + return await set_proof_pending(*args, **kwags) + + async def unset_proof_pending(*args, **kwags): + + return await unset_proof_pending(*args, **kwags) + async def store_keyset(*args, **kwags): return await store_keyset(*args, **kwags) @@ -102,6 +114,55 @@ async def invalidate_proof( ) +async def get_proofs_pending( + db: Database, + conn: Optional[Connection] = None, +): + + rows = await (conn or db).fetchall( + f""" + SELECT * from {table_with_schema(db, 'proofs_pending')} + """ + ) + return [Proof(**r) for r in rows] + + +async def set_proof_pending( + db: Database, + proof: Proof, + conn: Optional[Connection] = None, +): + + # we add the proof and secret to the used list + await (conn or db).execute( + f""" + INSERT INTO {table_with_schema(db, 'proofs_pending')} + (amount, C, secret) + VALUES (?, ?, ?) + """, + ( + proof.amount, + str(proof.C), + str(proof.secret), + ), + ) + + +async def unset_proof_pending( + proof: Proof, + db: Database, + conn: Optional[Connection] = None, +): + + await (conn or db).execute( + f""" + DELETE FROM {table_with_schema(db, 'proofs_pending')} + WHERE secret = ? + """, + (str(proof["secret"]),), + ) + + async def store_lightning_invoice( db: Database, invoice: Invoice, diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 8f328ce..316a831 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -258,7 +258,10 @@ class Ledger: return ok, preimage async def _invalidate_proofs(self, proofs: List[Proof]): - """Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" + """ + Adds secrets of proofs to the list of known secrets and stores them in the db. + Removes proofs from pending table. + """ # Mark proofs as used and prepare new promises proof_msgs = set([p.secret for p in proofs]) self.proofs_used |= proof_msgs @@ -266,6 +269,38 @@ class Ledger: for p in proofs: await self.crud.invalidate_proof(proof=p, db=self.db) + # delete proofs from pending list + await self._unset_proofs_pending(proofs) + + async def _set_proofs_pending(self, proofs: List[Proof]): + """ + 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. + """ + # first we check whether these proofs are pending aready + await self._validate_proofs_pending(proofs) + for p in proofs: + await self.crud.set_proof_pending(proof=p, db=self.db) + + async def _unset_proofs_pending(self, proofs: List[Proof]): + """Deletes proofs from pending table.""" + # we try: except: this block in order to avoid that any errors here + # could block the _invalidate_proofs() call that happens afterwards. + try: + for p in proofs: + await self.crud.unset_proof_pending(proof=p, db=self.db) + except Exception as e: + print(e) + pass + + async def _validate_proofs_pending(self, proofs: List[Proof]): + """Checks if any of the provided proofs is in the pending proofs table. Raises exception for at least one match.""" + proofs_pending = await self.crud.get_proofs_pending(db=self.db) + for p in proofs: + for pp in proofs_pending: + if p.secret == pp.secret: + raise Exception("proofs are pending.") + # Public methods def get_keyset(self, keyset_id: str = None): keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset @@ -308,6 +343,10 @@ class Ledger: async def melt(self, proofs: List[Proof], invoice: str): """Invalidates proofs and pays a Lightning invoice.""" + + # validate and set proofs as pending + await self._set_proofs_pending(proofs) + # Verify proofs if not all([self._verify_proof(p) for p in proofs]): raise Exception("could not verify proofs.") @@ -354,6 +393,10 @@ class Ledger: keyset: MintKeyset = None, ): """Consumes proofs and prepares new promises based on the amount split.""" + + # set proofs as pending + await self._set_proofs_pending(proofs) + total = sum_proofs(proofs) # verify that amount is kosher diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 51a6459..4b6b5e3 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -128,3 +128,21 @@ async def m004_keysets_add_version(db: Database): await db.execute( f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT" ) + + +async def m005_pending_proofs_table(db: Database) -> None: + """ + Store pending proofs. + """ + await db.execute( + f""" + CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} ( + amount INTEGER NOT NULL, + C TEXT NOT NULL, + secret TEXT NOT NULL, + + UNIQUE (secret) + + ); + """ + ) From cdabc86ba9d45d96e93ee902938bba898fe28946 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 14 Dec 2022 23:27:40 +0100 Subject: [PATCH 2/5] defer unpending --- cashu/mint/ledger.py | 97 +++++++++++++++++++++++++------------------- tests/test_wallet.py | 2 +- 2 files changed, 57 insertions(+), 42 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 316a831..b322383 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -110,9 +110,11 @@ class Ledger: return not proof.secret in self.proofs_used def _verify_secret_criteria(self, proof: Proof): - """Verifies that a secret is present""" + """Verifies that a secret is present and is not too long (DOS prevention).""" if proof.secret is None or proof.secret == "": raise Exception("no secret in proof.") + if len(proof.secret) > 64: + raise Exception("secret too long.") return True def _verify_proof(self, proof: Proof): @@ -269,9 +271,6 @@ class Ledger: for p in proofs: await self.crud.invalidate_proof(proof=p, db=self.db) - # delete proofs from pending list - await self._unset_proofs_pending(proofs) - async def _set_proofs_pending(self, proofs: List[Proof]): """ If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to @@ -280,7 +279,10 @@ class Ledger: # first we check whether these proofs are pending aready await self._validate_proofs_pending(proofs) for p in proofs: - await self.crud.set_proof_pending(proof=p, db=self.db) + try: + await self.crud.set_proof_pending(proof=p, db=self.db) + except: + raise Exception("proofs already pending.") async def _unset_proofs_pending(self, proofs: List[Proof]): """Deletes proofs from pending table.""" @@ -347,24 +349,31 @@ class Ledger: # validate and set proofs as pending await self._set_proofs_pending(proofs) - # Verify proofs - if not all([self._verify_proof(p) for p in proofs]): - raise Exception("could not verify proofs.") + try: + # Verify proofs + if not all([self._verify_proof(p) for p in proofs]): + raise Exception("could not verify proofs.") - total_provided = sum_proofs(proofs) - invoice_obj = bolt11.decode(invoice) - amount = math.ceil(invoice_obj.amount_msat / 1000) - fees_msat = await self.check_fees(invoice) - assert total_provided >= amount + fees_msat / 1000, Exception( - "provided proofs not enough for Lightning payment." - ) + total_provided = sum_proofs(proofs) + invoice_obj = bolt11.decode(invoice) + amount = math.ceil(invoice_obj.amount_msat / 1000) + fees_msat = await self.check_fees(invoice) + assert total_provided >= amount + fees_msat / 1000, Exception( + "provided proofs not enough for Lightning payment." + ) + + if LIGHTNING: + status, preimage = await self._pay_lightning_invoice(invoice, fees_msat) + else: + status, preimage = True, "preimage" + if status == True: + await self._invalidate_proofs(proofs) + except Exception as e: + raise e + finally: + # delete proofs from pending list + await self._unset_proofs_pending(proofs) - if LIGHTNING: - status, preimage = await self._pay_lightning_invoice(invoice, fees_msat) - else: - status, preimage = True, "preimage" - if status == True: - await self._invalidate_proofs(proofs) return status, preimage async def check_spendable(self, proofs: List[Proof]): @@ -399,27 +408,33 @@ class Ledger: total = sum_proofs(proofs) - # verify that amount is kosher - self._verify_split_amount(amount) - # verify overspending attempt - if amount > total: - raise Exception("split amount is higher than the total sum.") + try: + # verify that amount is kosher + self._verify_split_amount(amount) + # verify overspending attempt + if amount > total: + raise Exception("split amount is higher than the total sum.") - # Verify scripts - if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]): - raise Exception("script verification failed.") - # Verify secret criteria - if not all([self._verify_secret_criteria(p) for p in proofs]): - raise Exception("secrets do not match criteria.") - # verify that only unique proofs and outputs were used - if not self._verify_no_duplicates(proofs, outputs): - raise Exception("duplicate proofs or promises.") - # verify that outputs have the correct amount - if not self._verify_outputs(total, amount, outputs): - raise Exception("split of promises is not as expected.") - # Verify proofs - if not all([self._verify_proof(p) for p in proofs]): - raise Exception("could not verify proofs.") + # Verify scripts + if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]): + raise Exception("script verification failed.") + # Verify secret criteria + if not all([self._verify_secret_criteria(p) for p in proofs]): + raise Exception("secrets do not match criteria.") + # verify that only unique proofs and outputs were used + if not self._verify_no_duplicates(proofs, outputs): + raise Exception("duplicate proofs or promises.") + # verify that outputs have the correct amount + if not self._verify_outputs(total, amount, outputs): + raise Exception("split of promises is not as expected.") + # Verify proofs + if not all([self._verify_proof(p) for p in proofs]): + raise Exception("could not verify proofs.") + except Exception as e: + raise e + finally: + # delete proofs from pending list + await self._unset_proofs_pending(proofs) # Mark proofs as used and prepare new promises await self._invalidate_proofs(proofs) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 08ca8aa..f06e530 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -137,7 +137,7 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet): doublespend = await wallet1.mint(64) await assert_err( wallet1.split(wallet1.proofs + doublespend, 20), - "Mint Error: duplicate proofs or promises.", + "Mint Error: proofs already pending.", ) assert wallet1.balance == 64 assert wallet1.available_balance == 64 From 1082f2c9d1e050d68d2882df1936275e362d9218 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 14 Dec 2022 23:41:36 +0100 Subject: [PATCH 3/5] refactor --- cashu/mint/ledger.py | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index b322383..066d7aa 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -117,7 +117,7 @@ class Ledger: raise Exception("secret too long.") return True - def _verify_proof(self, proof: Proof): + def _verify_proof_bdhke(self, proof: Proof): """Verifies that the proof of promise was issued by this ledger.""" if not self._check_spendable(proof): raise Exception(f"tokens already spent. Secret: {proof.secret}") @@ -181,10 +181,13 @@ class Ledger: given = [o.amount for o in outputs] return given == expected - def _verify_no_duplicates(self, proofs: List[Proof], outputs: List[BlindedMessage]): + def _verify_no_duplicate_proofs(self, proofs: List[Proof]): secrets = [p.secret for p in proofs] if len(secrets) != len(list(set(secrets))): return False + return True + + def _verify_no_duplicate_outputs(self, outputs: List[BlindedMessage]): B_s = [od.B_ for od in outputs] if len(B_s) != len(list(set(B_s))): return False @@ -303,6 +306,21 @@ class Ledger: if p.secret == pp.secret: raise Exception("proofs are pending.") + async def _verify_proofs(self, proofs: List[Proof]): + """Checks a series of criteria for the verification of proofs.""" + # Verify scripts + if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]): + raise Exception("script validation failed.") + # Verify secret criteria + if not all([self._verify_secret_criteria(p) for p in proofs]): + raise Exception("secrets do not match criteria.") + # verify that only unique proofs were used + if not self._verify_no_duplicate_proofs(proofs): + raise Exception("duplicate proofs.") + # Verify proofs + if not all([self._verify_proof_bdhke(p) for p in proofs]): + raise Exception("could not verify proofs.") + # Public methods def get_keyset(self, keyset_id: str = None): keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset @@ -350,9 +368,7 @@ class Ledger: await self._set_proofs_pending(proofs) try: - # Verify proofs - if not all([self._verify_proof(p) for p in proofs]): - raise Exception("could not verify proofs.") + await self._verify_proofs(proofs) total_provided = sum_proofs(proofs) invoice_obj = bolt11.decode(invoice) @@ -415,21 +431,14 @@ class Ledger: if amount > total: raise Exception("split amount is higher than the total sum.") - # Verify scripts - if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]): - raise Exception("script verification failed.") - # Verify secret criteria - if not all([self._verify_secret_criteria(p) for p in proofs]): - raise Exception("secrets do not match criteria.") - # verify that only unique proofs and outputs were used - if not self._verify_no_duplicates(proofs, outputs): - raise Exception("duplicate proofs or promises.") + await self._verify_proofs(proofs) + + # verify that only unique outputs were used + if not self._verify_no_duplicate_outputs(outputs): + raise Exception("duplicate promises.") # verify that outputs have the correct amount if not self._verify_outputs(total, amount, outputs): raise Exception("split of promises is not as expected.") - # Verify proofs - if not all([self._verify_proof(p) for p in proofs]): - raise Exception("could not verify proofs.") except Exception as e: raise e finally: From 4e965b05b2121d8c49a492bc2c4e73b90a11c31e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 14 Dec 2022 23:51:56 +0100 Subject: [PATCH 4/5] bump to 0.6.0 --- README.md | 2 +- cashu/core/settings.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4b7cbfe..712cc39 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ cashu info Returns: ```bash -Version: 0.5.5 +Version: 0.6.0 Debug: False Cashu dir: /home/user/.cashu Wallet: wallet diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 84fd329..effd384 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -53,4 +53,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None) LNBITS_KEY = env.str("LNBITS_KEY", default=None) MAX_ORDER = 64 -VERSION = "0.5.5" +VERSION = "0.6.0" diff --git a/pyproject.toml b/pyproject.toml index d4755ba..835dcfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.5.5" +version = "0.6.0" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" diff --git a/setup.py b/setup.py index 66da4c1..128f113 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]} setuptools.setup( name="cashu", - version="0.5.5", + version="0.6.0", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown", From 710d3ba6914c4cee7e667bde14b0b9148fd0ef8e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Dec 2022 00:08:31 +0100 Subject: [PATCH 5/5] remove macos tests until black installation works again --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4b1c79a..bf68747 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest] python-version: ["3.9"] poetry-version: ["1.2.1"] steps: