From 4f5297390874700ae9fbdf8c855199608d81f5c4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:52:20 +0100 Subject: [PATCH 1/2] Update PostRestoreResponse class to use "signatures" instead of "promises" (#467) * update nut-09 endpoint * add to deprecated router and wallet --- cashu/core/base.py | 8 +++++++- cashu/mint/router.py | 2 +- cashu/mint/router_deprecated.py | 2 +- cashu/wallet/wallet.py | 9 ++++++++- cashu/wallet/wallet_deprecated.py | 9 ++++++++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d9f1dc0a..4ae524ef 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -530,7 +530,13 @@ class CheckFeesResponse_deprecated(BaseModel): class PostRestoreResponse(BaseModel): outputs: List[BlindedMessage] = [] - promises: List[BlindedSignature] = [] + signatures: List[BlindedSignature] = [] + promises: Optional[List[BlindedSignature]] = [] # deprecated since 0.15.1 + + # duplicate value of "signatures" for backwards compatibility with old clients < 0.15.1 + def __init__(self, **data): + super().__init__(**data) + self.promises = self.signatures # ------- KEYSETS ------- diff --git a/cashu/mint/router.py b/cashu/mint/router.py index d0cac90a..de3a07c7 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -355,4 +355,4 @@ async def check_state( async def restore(payload: PostMintRequest) -> PostRestoreResponse: assert payload.outputs, Exception("no outputs provided.") outputs, promises = await ledger.restore(payload.outputs) - return PostRestoreResponse(outputs=outputs, promises=promises) + return PostRestoreResponse(outputs=outputs, signatures=promises) diff --git a/cashu/mint/router_deprecated.py b/cashu/mint/router_deprecated.py index a2ac71b7..f2cb96c4 100644 --- a/cashu/mint/router_deprecated.py +++ b/cashu/mint/router_deprecated.py @@ -360,4 +360,4 @@ async def check_spendable_deprecated( async def restore(payload: PostMintRequest_deprecated) -> PostRestoreResponse: assert payload.outputs, Exception("no outputs provided.") outputs, promises = await ledger.restore(payload.outputs) - return PostRestoreResponse(outputs=outputs, promises=promises) + return PostRestoreResponse(outputs=outputs, signatures=promises) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 9dd07b82..54735068 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -697,7 +697,14 @@ async def restore_promises( self.raise_on_error_request(resp) response_dict = resp.json() returnObj = PostRestoreResponse.parse_obj(response_dict) - return returnObj.outputs, returnObj.promises + + # BEGIN backwards compatibility < 0.15.1 + # if the mint returns promises, duplicate into signatures + if returnObj.promises: + returnObj.signatures = returnObj.promises + # END backwards compatibility < 0.15.1 + + return returnObj.outputs, returnObj.signatures class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets): diff --git a/cashu/wallet/wallet_deprecated.py b/cashu/wallet/wallet_deprecated.py index cca86fb3..db5e927a 100644 --- a/cashu/wallet/wallet_deprecated.py +++ b/cashu/wallet/wallet_deprecated.py @@ -408,7 +408,14 @@ async def restore_promises_deprecated( self.raise_on_error(resp) response_dict = resp.json() returnObj = PostRestoreResponse.parse_obj(response_dict) - return returnObj.outputs, returnObj.promises + + # BEGIN backwards compatibility < 0.15.1 + # if the mint returns promises, duplicate into signatures + if returnObj.promises: + returnObj.signatures = returnObj.promises + # END backwards compatibility < 0.15.1 + + return returnObj.outputs, returnObj.signatures @async_set_httpx_client @async_ensure_mint_loaded_deprecated From 150195d66aa90f62ae155eac67d208aa85ce94c8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:53:18 +0100 Subject: [PATCH 2/2] Token state check with Y (#468) * Token state check with Y * remove backwards compat for v1 --- cashu/core/base.py | 4 +- cashu/mint/crud.py | 74 +++++++++++++++++++++------------ cashu/mint/ledger.py | 32 +++++++------- cashu/mint/router.py | 2 +- cashu/mint/router_deprecated.py | 2 +- cashu/mint/verification.py | 56 +++++++++++-------------- cashu/wallet/wallet.py | 8 ++-- tests/test_mint_api.py | 5 ++- tests/test_mint_operations.py | 4 +- 9 files changed, 99 insertions(+), 88 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 4ae524ef..d0a3eff3 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -486,7 +486,7 @@ class PostSplitResponse_Very_Deprecated(BaseModel): class PostCheckStateRequest(BaseModel): - secrets: List[str] = Field(..., max_items=settings.mint_max_request_length) + Ys: List[str] = Field(..., max_items=settings.mint_max_request_length) class SpentState(Enum): @@ -499,7 +499,7 @@ def __str__(self): class ProofState(BaseModel): - secret: str + Y: str state: SpentState witness: Optional[str] = None diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 853ff094..9a1dd54c 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -34,7 +34,8 @@ async def get_keyset( derivation_path: str = "", seed: str = "", conn: Optional[Connection] = None, - ) -> List[MintKeyset]: ... + ) -> List[MintKeyset]: + ... @abstractmethod async def get_spent_proofs( @@ -42,7 +43,8 @@ async def get_spent_proofs( *, db: Database, conn: Optional[Connection] = None, - ) -> List[Proof]: ... + ) -> List[Proof]: + ... async def get_proof_used( self, @@ -50,7 +52,8 @@ async def get_proof_used( Y: str, db: Database, conn: Optional[Connection] = None, - ) -> Optional[Proof]: ... + ) -> Optional[Proof]: + ... @abstractmethod async def invalidate_proof( @@ -59,16 +62,18 @@ async def invalidate_proof( db: Database, proof: Proof, conn: Optional[Connection] = None, - ) -> None: ... + ) -> None: + ... @abstractmethod async def get_proofs_pending( self, *, - proofs: List[Proof], + Ys: List[str], db: Database, conn: Optional[Connection] = None, - ) -> List[Proof]: ... + ) -> List[Proof]: + ... @abstractmethod async def set_proof_pending( @@ -77,12 +82,14 @@ async def set_proof_pending( db: Database, proof: Proof, conn: Optional[Connection] = None, - ) -> None: ... + ) -> None: + ... @abstractmethod async def unset_proof_pending( self, *, proof: Proof, db: Database, conn: Optional[Connection] = None - ) -> None: ... + ) -> None: + ... @abstractmethod async def store_keyset( @@ -91,14 +98,16 @@ async def store_keyset( db: Database, keyset: MintKeyset, conn: Optional[Connection] = None, - ) -> None: ... + ) -> None: + ... @abstractmethod async def get_balance( self, db: Database, conn: Optional[Connection] = None, - ) -> int: ... + ) -> int: + ... @abstractmethod async def store_promise( @@ -112,7 +121,8 @@ async def store_promise( e: str = "", s: str = "", conn: Optional[Connection] = None, - ) -> None: ... + ) -> None: + ... @abstractmethod async def get_promise( @@ -121,7 +131,8 @@ async def get_promise( db: Database, B_: str, conn: Optional[Connection] = None, - ) -> Optional[BlindedSignature]: ... + ) -> Optional[BlindedSignature]: + ... @abstractmethod async def store_mint_quote( @@ -130,7 +141,8 @@ async def store_mint_quote( quote: MintQuote, db: Database, conn: Optional[Connection] = None, - ) -> None: ... + ) -> None: + ... @abstractmethod async def get_mint_quote( @@ -139,7 +151,8 @@ async def get_mint_quote( quote_id: str, db: Database, conn: Optional[Connection] = None, - ) -> Optional[MintQuote]: ... + ) -> Optional[MintQuote]: + ... @abstractmethod async def get_mint_quote_by_checking_id( @@ -148,7 +161,8 @@ async def get_mint_quote_by_checking_id( checking_id: str, db: Database, conn: Optional[Connection] = None, - ) -> Optional[MintQuote]: ... + ) -> Optional[MintQuote]: + ... @abstractmethod async def update_mint_quote( @@ -157,7 +171,8 @@ async def update_mint_quote( quote: MintQuote, db: Database, conn: Optional[Connection] = None, - ) -> None: ... + ) -> None: + ... # @abstractmethod # async def update_mint_quote_paid( @@ -176,7 +191,8 @@ async def store_melt_quote( quote: MeltQuote, db: Database, conn: Optional[Connection] = None, - ) -> None: ... + ) -> None: + ... @abstractmethod async def get_melt_quote( @@ -186,7 +202,8 @@ async def get_melt_quote( db: Database, checking_id: Optional[str] = None, conn: Optional[Connection] = None, - ) -> Optional[MeltQuote]: ... + ) -> Optional[MeltQuote]: + ... @abstractmethod async def update_melt_quote( @@ -195,7 +212,8 @@ async def update_melt_quote( quote: MeltQuote, db: Database, conn: Optional[Connection] = None, - ) -> None: ... + ) -> None: + ... class LedgerCrudSqlite(LedgerCrud): @@ -256,9 +274,11 @@ async def get_spent_proofs( db: Database, conn: Optional[Connection] = None, ) -> List[Proof]: - rows = await (conn or db).fetchall(f""" + rows = await (conn or db).fetchall( + f""" SELECT * from {table_with_schema(db, 'proofs_used')} - """) + """ + ) return [Proof(**r) for r in rows] if rows else [] async def invalidate_proof( @@ -289,16 +309,16 @@ async def invalidate_proof( async def get_proofs_pending( self, *, - proofs: List[Proof], + Ys: List[str], db: Database, conn: Optional[Connection] = None, ) -> List[Proof]: rows = await (conn or db).fetchall( f""" SELECT * from {table_with_schema(db, 'proofs_pending')} - WHERE Y IN ({','.join(['?']*len(proofs))}) + WHERE Y IN ({','.join(['?']*len(Ys))}) """, - tuple(proof.Y for proof in proofs), + tuple(Ys), ) return [Proof(**r) for r in rows] @@ -549,9 +569,11 @@ async def get_balance( db: Database, conn: Optional[Connection] = None, ) -> int: - row = await (conn or db).fetchone(f""" + row = await (conn or db).fetchone( + f""" SELECT * from {table_with_schema(db, 'balance')} - """) + """ + ) assert row, "Balance not found" return int(row[0]) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index ce1ce7b1..532df73f 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -886,7 +886,7 @@ async def load_used_proofs(self) -> None: logger.debug(f"Loaded {len(spent_proofs_list)} used proofs") self.spent_proofs = {p.Y: p for p in spent_proofs_list} - async def check_proofs_state(self, secrets: List[str]) -> List[ProofState]: + async def check_proofs_state(self, Ys: List[str]) -> List[ProofState]: """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. @@ -895,32 +895,26 @@ async def check_proofs_state(self, secrets: List[str]) -> List[ProofState]: and which isn't. Args: - proofs (List[Proof]): List of proofs to check. + Ys (List[str]): List of Y's 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) """ states: List[ProofState] = [] - proofs_spent_idx_secret = await self._get_proofs_spent_idx_secret(secrets) - proofs_pending_idx_secret = await self._get_proofs_pending_idx_secret(secrets) - for secret in secrets: - if ( - secret not in proofs_spent_idx_secret - and secret not in proofs_pending_idx_secret - ): - states.append(ProofState(secret=secret, state=SpentState.unspent)) - elif ( - secret not in proofs_spent_idx_secret - and secret in proofs_pending_idx_secret - ): - states.append(ProofState(secret=secret, state=SpentState.pending)) + proofs_spent = await self._get_proofs_spent(Ys) + proofs_pending = await self._get_proofs_pending(Ys) + for Y in Ys: + if Y not in proofs_spent and Y not in proofs_pending: + states.append(ProofState(Y=Y, state=SpentState.unspent)) + elif Y not in proofs_spent and Y in proofs_pending: + states.append(ProofState(Y=Y, state=SpentState.pending)) else: states.append( ProofState( - secret=secret, + Y=Y, state=SpentState.spent, - witness=proofs_spent_idx_secret[secret].witness, + witness=proofs_spent[Y].witness, ) ) return states @@ -971,7 +965,9 @@ async def _validate_proofs_pending( """ assert ( len( - await self.crud.get_proofs_pending(proofs=proofs, db=self.db, conn=conn) + await self.crud.get_proofs_pending( + Ys=[p.Y for p in proofs], db=self.db, conn=conn + ) ) == 0 ), TransactionError("proofs are pending.") diff --git a/cashu/mint/router.py b/cashu/mint/router.py index de3a07c7..4c96d162 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -338,7 +338,7 @@ async def check_state( ) -> PostCheckStateResponse: """Check whether a secret has been spent already or not.""" logger.trace(f"> POST /v1/checkstate: {payload}") - proof_states = await ledger.check_proofs_state(payload.secrets) + proof_states = await ledger.check_proofs_state(payload.Ys) return PostCheckStateResponse(states=proof_states) diff --git a/cashu/mint/router_deprecated.py b/cashu/mint/router_deprecated.py index f2cb96c4..f71a8a34 100644 --- a/cashu/mint/router_deprecated.py +++ b/cashu/mint/router_deprecated.py @@ -328,7 +328,7 @@ async def check_spendable_deprecated( ) -> CheckSpendableResponse_deprecated: """Check whether a secret has been spent already or not.""" logger.trace(f"> POST /check: {payload}") - proofs_state = await ledger.check_proofs_state([p.secret for p in payload.proofs]) + proofs_state = await ledger.check_proofs_state([p.Y for p in payload.proofs]) spendableList: List[bool] = [] pendingList: List[bool] = [] for proof_state in proofs_state: diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index e3da1d54..754c8b1c 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -51,10 +51,7 @@ async def verify_inputs_and_outputs( """ # Verify inputs # Verify proofs are spendable - if ( - not len(await self._get_proofs_spent_idx_secret([p.secret for p in proofs])) - == 0 - ): + if not len(await self._get_proofs_spent([p.Y for p in proofs])) == 0: raise TokenAlreadySpentError() # Verify amounts of inputs if not all([self._verify_amount(p.amount) for p in proofs]): @@ -87,11 +84,13 @@ async def verify_inputs_and_outputs( # Verify that input keyset units are the same as output keyset unit # We have previously verified that all outputs have the same keyset id in `_verify_outputs` assert outputs[0].id, "output id not set" - if not all([ - self.keysets[p.id].unit == self.keysets[outputs[0].id].unit - for p in proofs - if p.id - ]): + if not all( + [ + self.keysets[p.id].unit == self.keysets[outputs[0].id].unit + for p in proofs + if p.id + ] + ): raise TransactionError("input and output keysets have different units.") # Verify output spending conditions @@ -143,39 +142,34 @@ async def _check_outputs_issued_before(self, outputs: List[BlindedMessage]): result.append(False if promise is None else True) return result - async def _get_proofs_pending_idx_secret( - self, secrets: List[str] - ) -> Dict[str, Proof]: - """Returns only those proofs that are pending.""" - all_proofs_pending = await self.crud.get_proofs_pending( - proofs=[Proof(secret=s) for s in secrets], db=self.db - ) - proofs_pending = list(filter(lambda p: p.secret in secrets, all_proofs_pending)) - proofs_pending_dict = {p.secret: p for p in proofs_pending} + async def _get_proofs_pending(self, Ys: List[str]) -> Dict[str, Proof]: + """Returns a dictionary of only those proofs that are pending. + The key is the Y=h2c(secret) and the value is the proof. + """ + proofs_pending = await self.crud.get_proofs_pending(Ys=Ys, db=self.db) + proofs_pending_dict = {p.Y: p for p in proofs_pending} return proofs_pending_dict - async def _get_proofs_spent_idx_secret( - self, secrets: List[str] - ) -> Dict[str, Proof]: - """Returns all proofs that are spent.""" - proofs = [Proof(secret=s) for s in secrets] - proofs_spent: List[Proof] = [] + async def _get_proofs_spent(self, Ys: List[str]) -> Dict[str, Proof]: + """Returns a dictionary of all proofs that are spent. + The key is the Y=h2c(secret) and the value is the proof. + """ + proofs_spent_dict: Dict[str, Proof] = {} if settings.mint_cache_secrets: # check used secrets in memory - for proof in proofs: - spent_proof = self.spent_proofs.get(proof.Y) + for Y in Ys: + spent_proof = self.spent_proofs.get(Y) if spent_proof: - proofs_spent.append(spent_proof) + proofs_spent_dict[Y] = spent_proof else: # check used secrets in database async with self.db.connect() as conn: - for proof in proofs: + for Y in Ys: spent_proof = await self.crud.get_proof_used( - db=self.db, Y=proof.Y, conn=conn + db=self.db, Y=Y, conn=conn ) if spent_proof: - proofs_spent.append(spent_proof) - proofs_spent_dict = {p.secret: p for p in proofs_spent} + proofs_spent_dict[Y] = spent_proof return proofs_spent_dict def _verify_secret_criteria(self, proof: Proof) -> Literal[True]: diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 54735068..d4a5479c 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -654,7 +654,7 @@ async def check_proof_state(self, proofs: List[Proof]) -> PostCheckStateResponse """ Checks whether the secrets in proofs are already spent or not and returns a list of booleans. """ - payload = PostCheckStateRequest(secrets=[p.secret for p in proofs]) + payload = PostCheckStateRequest(Ys=[p.Y for p in proofs]) resp = await self.httpx.post( join(self.url, "/v1/checkstate"), json=payload.dict(), @@ -667,11 +667,11 @@ async def check_proof_state(self, proofs: List[Proof]) -> PostCheckStateResponse states: List[ProofState] = [] for spendable, pending, p in zip(ret.spendable, ret.pending, proofs): if spendable and not pending: - states.append(ProofState(secret=p.secret, state=SpentState.unspent)) + states.append(ProofState(Y=p.Y, state=SpentState.unspent)) elif spendable and pending: - states.append(ProofState(secret=p.secret, state=SpentState.pending)) + states.append(ProofState(Y=p.Y, state=SpentState.pending)) else: - states.append(ProofState(secret=p.secret, state=SpentState.spent)) + states.append(ProofState(Y=p.Y, state=SpentState.spent)) ret = PostCheckStateResponse(states=states) return ret # END backwards compatibility < 0.15.0 diff --git a/tests/test_mint_api.py b/tests/test_mint_api.py index 22d8e30c..6685af30 100644 --- a/tests/test_mint_api.py +++ b/tests/test_mint_api.py @@ -54,7 +54,8 @@ async def test_api_keys(ledger: Ledger): "id": keyset.id, "unit": keyset.unit.name, "keys": { - str(k): v.serialize().hex() for k, v in keyset.public_keys.items() # type: ignore + str(k): v.serialize().hex() + for k, v in keyset.public_keys.items() # type: ignore }, } for keyset in ledger.keysets.values() @@ -378,7 +379,7 @@ async def test_melt_external(ledger: Ledger, wallet: Wallet): reason="settings.debug_mint_only_deprecated is set", ) async def test_api_check_state(ledger: Ledger): - payload = PostCheckStateRequest(secrets=["asdasdasd", "asdasdasd1"]) + payload = PostCheckStateRequest(Ys=["asdasdasd", "asdasdasd1"]) response = httpx.post( f"{BASE_URL}/v1/checkstate", json=payload.dict(), diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index 46c05ec0..d5bca559 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -357,7 +357,5 @@ async def test_check_proof_state(wallet1: Wallet, ledger: Ledger): keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) - proof_states = await ledger.check_proofs_state( - secrets=[p.secret for p in send_proofs] - ) + proof_states = await ledger.check_proofs_state(Ys=[p.Y for p in send_proofs]) assert all([p.state.value == "UNSPENT" for p in proof_states])