From 2a3049e70470bc7d340be8ef9d003064026973fc Mon Sep 17 00:00:00 2001 From: conduition Date: Fri, 23 Aug 2024 05:02:28 +0000 Subject: [PATCH 1/3] separate error types for registration/settlement responses --- cashu/core/base.py | 25 ++++++++++++++++++++----- cashu/core/models.py | 9 ++++++--- cashu/mint/db/write.py | 21 ++++++++++++--------- cashu/mint/ledger.py | 12 +++++++----- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index a3202610..41e649d3 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1243,8 +1243,11 @@ class DlcFundingProof(BaseModel): or a dlc merkle root with bad inputs. """ dlc_root: str - signature: Optional[str] = None - bad_inputs: Optional[List[DlcBadInput]] = None # Used to specify potential errors + signature: str + +class DlcFundingError(BaseModel): + dlc_root: str + bad_inputs: Optional[List[DlcBadInput]] # Used to specify potential errors class DlcOutcome(BaseModel): """ @@ -1259,9 +1262,21 @@ class DlcSettlement(BaseModel): Data used to settle an outcome of a DLC """ dlc_root: str - outcome: Optional[DlcOutcome] = None - merkle_proof: Optional[List[str]] = None - details: Optional[str] = None + outcome: DlcOutcome + merkle_proof: List[str] + +class DlcSettlementAck(BaseModel): + """ + Used by the mint to indicate the success of a DLC's funding, settlement, etc. + """ + dlc_root: str + +class DlcSettlementError(BaseModel): + """ + Indicates to the client that a DLC operation (funding, settlement, etc) failed. + """ + dlc_root: str + details: str class DlcPayoutForm(BaseModel): dlc_root: str diff --git a/cashu/core/models.py b/cashu/core/models.py index 8acdb71b..9fd71810 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -7,10 +7,13 @@ BlindedMessage_Deprecated, BlindedSignature, DiscreetLogContract, + DlcFundingError, DlcFundingProof, DlcPayout, DlcPayoutForm, DlcSettlement, + DlcSettlementAck, + DlcSettlementError, MeltQuote, MintQuote, Proof, @@ -338,7 +341,7 @@ class PostDlcRegistrationRequest(BaseModel): class PostDlcRegistrationResponse(BaseModel): funded: List[DlcFundingProof] = [] - errors: Optional[List[DlcFundingProof]] = None + errors: Optional[List[DlcFundingError]] = None # ------- API: DLC SETTLEMENT ------- @@ -346,8 +349,8 @@ class PostDlcSettleRequest(BaseModel): settlements: List[DlcSettlement] class PostDlcSettleResponse(BaseModel): - settled: List[DlcSettlement] = [] - errors: Optional[List[DlcSettlement]] = None + settled: List[DlcSettlementAck] = [] + errors: Optional[List[DlcSettlementError]] = None # ------- API: DLC PAYOUT ------- class PostDlcPayoutRequest(BaseModel): diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index ee9c9a89..e2b9d99b 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -5,8 +5,11 @@ from ...core.base import ( DiscreetLogContract, DlcBadInput, + DlcFundingError, DlcFundingProof, DlcSettlement, + DlcSettlementAck, + DlcSettlementError, MeltQuote, MeltQuoteState, MintQuote, @@ -233,7 +236,7 @@ async def _unset_melt_quote_pending( async def _verify_proofs_and_dlc_registrations( self, registrations: List[Tuple[DiscreetLogContract, DlcFundingProof]], - ) -> Tuple[List[Tuple[DiscreetLogContract, DlcFundingProof]], List[DlcFundingProof]]: + ) -> Tuple[List[Tuple[DiscreetLogContract, DlcFundingProof]], List[DlcFundingError]]: """ Method to check if proofs are already spent or registrations already registered. If they are not, we set them as spent and registered respectively @@ -245,7 +248,7 @@ async def _verify_proofs_and_dlc_registrations( """ checked: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] registered: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] - errors: List[DlcFundingProof]= [] + errors: List[DlcFundingError]= [] if len(registrations) == 0: logger.trace("Received 0 registrations") return [], [] @@ -261,7 +264,7 @@ async def _verify_proofs_and_dlc_registrations( checked.append(registration) except (TokenAlreadySpentError, DlcAlreadyRegisteredError) as e: logger.trace(f"Proofs already spent for registration {reg.dlc_root}") - errors.append(DlcFundingProof( + errors.append(DlcFundingError( dlc_root=reg.dlc_root, bad_inputs=[DlcBadInput( index=-1, @@ -284,7 +287,7 @@ async def _verify_proofs_and_dlc_registrations( registered.append(registration) except Exception as e: logger.trace(f"Failed to register {reg.dlc_root}: {str(e)}") - errors.append(DlcFundingProof( + errors.append(DlcFundingError( dlc_root=reg.dlc_root, bad_inputs=[DlcBadInput( index=-1, @@ -297,7 +300,7 @@ async def _verify_proofs_and_dlc_registrations( async def _settle_dlc( self, settlements: List[DlcSettlement] - ) -> Tuple[List[DlcSettlement], List[DlcSettlement]]: + ) -> Tuple[List[DlcSettlementAck], List[DlcSettlementError]]: settled = [] errors = [] async with self.db.get_connection(lock_table="dlc") as conn: @@ -306,22 +309,22 @@ async def _settle_dlc( # We verify the dlc_root is in the DB dlc = await self.crud.get_registered_dlc(settlement.dlc_root, self.db, conn) if dlc is None: - errors.append(DlcSettlement( + errors.append(DlcSettlementError( dlc_root=settlement.dlc_root, details="no DLC with this root hash" )) continue if dlc.settled is True: - errors.append(DlcSettlement( + errors.append(DlcSettlementError( dlc_root=settlement.dlc_root, details="DLC already settled" )) assert settlement.outcome await self.crud.set_dlc_settled_and_debts(settlement.dlc_root, settlement.outcome.P, self.db, conn) - settled.append(settlement) + settled.append(DlcSettlementAck(dlc_root=settlement.dlc_root)) except Exception as e: - errors.append(DlcSettlement( + errors.append(DlcSettlementError( dlc_root=settlement.dlc_root, details=f"error with the DB: {str(e)}" )) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 0e5278c3..d4d2b2c5 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -12,8 +12,10 @@ BlindedSignature, DiscreetLogContract, DlcBadInput, + DlcFundingError, DlcFundingProof, DlcSettlement, + DlcSettlementError, MeltQuote, MeltQuoteState, Method, @@ -1133,7 +1135,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi """ logger.trace("register called") funded: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] - errors: List[DlcFundingProof] = [] + errors: List[DlcFundingError] = [] for registration in request.registrations: try: logger.trace(f"processing registration {registration.dlc_root}") @@ -1175,7 +1177,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi logger.error(f"registration {registration.dlc_root} failed") # Generic Error if isinstance(e, TransactionError): - errors.append(DlcFundingProof( + errors.append(DlcFundingError( dlc_root=registration.dlc_root, bad_inputs=[DlcBadInput( index=-1, @@ -1184,7 +1186,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi )) # DLC verification fail else: - errors.append(DlcFundingProof( + errors.append(DlcFundingError( dlc_root=registration.dlc_root, bad_inputs=e.bad_inputs, )) @@ -1207,7 +1209,7 @@ async def settle_dlc(self, request: PostDlcSettleRequest) -> PostDlcSettleRespon """ logger.trace("settle called") verified: List[DlcSettlement] = [] - errors: List[DlcSettlement] = [] + errors: List[DlcSettlementError] = [] for settlement in request.settlements: try: # Verify inclusion of payout structure and associated attestation in the DLC @@ -1215,7 +1217,7 @@ async def settle_dlc(self, request: PostDlcSettleRequest) -> PostDlcSettleRespon await self._verify_dlc_inclusion(settlement.dlc_root, settlement.outcome, settlement.merkle_proof) verified.append(settlement) except (DlcSettlementFail, AssertionError) as e: - errors.append(DlcSettlement( + errors.append(DlcSettlementError( dlc_root=settlement.dlc_root, details=e.detail if isinstance(e, DlcSettlementFail) else str(e) )) From 971b43e55cec92f5bd86ba098eee959e66a2196e Mon Sep 17 00:00:00 2001 From: conduition Date: Fri, 23 Aug 2024 05:15:57 +0000 Subject: [PATCH 2/3] update registration response to include funding proof keyset id The spec now includes the `funding_proof` as a sub-object, which includes a reference to the keyset used to create the funding proof. --- cashu/core/base.py | 6 +++++- cashu/core/models.py | 4 ++-- cashu/mint/db/write.py | 16 ++++++++-------- cashu/mint/ledger.py | 12 ++++++++---- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 41e649d3..b373da77 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1242,9 +1242,13 @@ class DlcFundingProof(BaseModel): A dlc merkle root with its signature or a dlc merkle root with bad inputs. """ - dlc_root: str + keyset: str signature: str +class DlcFundingAck(BaseModel): + dlc_root: str + funding_proof: DlcFundingProof + class DlcFundingError(BaseModel): dlc_root: str bad_inputs: Optional[List[DlcBadInput]] # Used to specify potential errors diff --git a/cashu/core/models.py b/cashu/core/models.py index 9fd71810..3c4cae6e 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -7,8 +7,8 @@ BlindedMessage_Deprecated, BlindedSignature, DiscreetLogContract, + DlcFundingAck, DlcFundingError, - DlcFundingProof, DlcPayout, DlcPayoutForm, DlcSettlement, @@ -340,7 +340,7 @@ class PostDlcRegistrationRequest(BaseModel): registrations: List[DiscreetLogContract] class PostDlcRegistrationResponse(BaseModel): - funded: List[DlcFundingProof] = [] + funded: List[DlcFundingAck] = [] errors: Optional[List[DlcFundingError]] = None # ------- API: DLC SETTLEMENT ------- diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index e2b9d99b..5914332e 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -5,8 +5,8 @@ from ...core.base import ( DiscreetLogContract, DlcBadInput, + DlcFundingAck, DlcFundingError, - DlcFundingProof, DlcSettlement, DlcSettlementAck, DlcSettlementError, @@ -235,19 +235,19 @@ async def _unset_melt_quote_pending( async def _verify_proofs_and_dlc_registrations( self, - registrations: List[Tuple[DiscreetLogContract, DlcFundingProof]], - ) -> Tuple[List[Tuple[DiscreetLogContract, DlcFundingProof]], List[DlcFundingError]]: + registrations: List[Tuple[DiscreetLogContract, DlcFundingAck]], + ) -> Tuple[List[Tuple[DiscreetLogContract, DlcFundingAck]], List[DlcFundingError]]: """ Method to check if proofs are already spent or registrations already registered. If they are not, we set them as spent and registered respectively Args: - registrations (List[Tuple[DiscreetLogContract, DlcFundingProof]]): List of registrations. + registrations (List[Tuple[DiscreetLogContract, DlcFundingAck]]): List of registrations. Returns: - List[Tuple[DiscreetLogContract, DlcFundingProof]]: a list of registered DLCs - List[DlcFundingProof]: a list of errors + List[Tuple[DiscreetLogContract, DlcFundingAck]]: a list of registered DLCs + List[DlcFundingError]: a list of errors """ - checked: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] - registered: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] + checked: List[Tuple[DiscreetLogContract, DlcFundingAck]] = [] + registered: List[Tuple[DiscreetLogContract, DlcFundingAck]] = [] errors: List[DlcFundingError]= [] if len(registrations) == 0: logger.trace("Received 0 registrations") diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index d4d2b2c5..d3926370 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -12,6 +12,7 @@ BlindedSignature, DiscreetLogContract, DlcBadInput, + DlcFundingAck, DlcFundingError, DlcFundingProof, DlcSettlement, @@ -1134,7 +1135,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi PostDlcRegistrationResponse: Indicating the funded and registered DLCs as well as the errors. """ logger.trace("register called") - funded: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] + funded: List[Tuple[DiscreetLogContract, DlcFundingAck]] = [] errors: List[DlcFundingError] = [] for registration in request.registrations: try: @@ -1161,9 +1162,12 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi registration.funding_amount, funding_privkey, ) - funding_proof = DlcFundingProof( + funding_ack = DlcFundingAck( dlc_root=registration.dlc_root, - signature=signature.hex(), + funding_proof=DlcFundingProof( + keyset=active_keyset_for_unit.id, + signature=signature.hex(), + ), ) dlc = DiscreetLogContract( settled=False, @@ -1172,7 +1176,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi inputs=registration.inputs, unit=registration.unit, ) - funded.append((dlc, funding_proof)) + funded.append((dlc, funding_ack)) except (TransactionError, DlcVerificationFail) as e: logger.error(f"registration {registration.dlc_root} failed") # Generic Error From be81d78013fa7d5f019f71a62cb4600d6f233d6a Mon Sep 17 00:00:00 2001 From: conduition Date: Mon, 26 Aug 2024 17:08:41 +0000 Subject: [PATCH 3/3] update tests to reflect new response types --- tests/conftest.py | 2 +- tests/test_dlc.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 76d39d30..f985a287 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -139,6 +139,6 @@ def mint(): server = UvicornServer(config=config) server.start() - time.sleep(1) + time.sleep(5) yield server server.stop() diff --git a/tests/test_dlc.py b/tests/test_dlc.py index 0a7d3321..a07b894f 100644 --- a/tests/test_dlc.py +++ b/tests/test_dlc.py @@ -310,8 +310,8 @@ async def test_registration_vanilla_proofs(wallet: Wallet, ledger: Ledger): response = await ledger.register_dlc(request) assert len(response.funded) == 1, "Funding proofs len != 1" - funding_proof = response.funded[0] - assert verify_dlc_signature(dlc_root, 64, bytes.fromhex(funding_proof.signature), pubkey),\ + ack = response.funded[0] + assert verify_dlc_signature(dlc_root, 64, bytes.fromhex(ack.funding_proof.signature), pubkey),\ "Could not verify funding proof" @pytest.mark.asyncio @@ -345,8 +345,8 @@ async def test_registration_dlc_locked_proofs(wallet: Wallet, ledger: Ledger): assert response.errors is None, f"Funding proofs error: {response.errors[0].bad_inputs}" assert len(response.funded) == 1, "Funding proofs len != 1" - funding_proof = response.funded[0] - assert verify_dlc_signature(dlc_root, 64, bytes.fromhex(funding_proof.signature), pubkey), \ + ack = response.funded[0] + assert verify_dlc_signature(dlc_root, 64, bytes.fromhex(ack.funding_proof.signature), pubkey), \ "Could not verify funding proof" @@ -508,7 +508,7 @@ async def test_settle_dlc(wallet: Wallet, ledger: Ledger): request = PostDlcRegistrationRequest(registrations=[dlc]) response = await ledger.register_dlc(request) - assert response.errors is None, f"Funding proofs error: {response.errors[0].bad_inputs}" + assert response.errors is None, f"Funding proofs error: {response.errors[0].bad_inputs}" outcome = DlcOutcome( P=json.dumps(payouts[1]), @@ -558,7 +558,7 @@ async def test_settle_dlc_timeout(wallet: Wallet, ledger: Ledger): request = PostDlcRegistrationRequest(registrations=[dlc]) response = await ledger.register_dlc(request) - assert response.errors is None, f"Funding proofs error: {response.errors[0].bad_inputs}" + assert response.errors is None, f"Funding proofs error: {response.errors[0].bad_inputs}" outcome = DlcOutcome( P=json.dumps(payouts[2]), @@ -574,4 +574,4 @@ async def test_settle_dlc_timeout(wallet: Wallet, ledger: Ledger): response = await ledger.settle_dlc(request) assert response.errors is None, f"Response contains errors: {response.errors}" - assert len(response.settled) > 0, "Response contains zero settlements." \ No newline at end of file + assert len(response.settled) > 0, "Response contains zero settlements."