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) + + ); + """ + )