From c6e06e74e5e3e12df16e9d3de88e7a677311b5e8 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 2 Aug 2024 18:09:42 +0200 Subject: [PATCH] settlement --- cashu/core/base.py | 8 +++---- cashu/core/errors.py | 10 +++++++- cashu/mint/ledger.py | 47 +++++++++++++++++++++++++++++++++----- cashu/mint/verification.py | 25 ++++++++++++++++++++ 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 8a68dc9f..f12adda5 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1250,17 +1250,17 @@ class DlcOutcome(BaseModel): """ Describes a DLC outcome """ - k: Optional[str] # The discrete log revealed by the oracle + k: Optional[str] # The blinded attestation secret t: Optional[int] # The timeout (claim when time is over) - P: str # The payout structure associated with k + P: str # The payout structure associated with this outcome class DlcSettlement(BaseModel): """ Data used to settle an outcome of a DLC """ dlc_root: str - outcome: DlcOutcome - merkle_proof: List[str] + outcome: Optional[DlcOutcome] + merkle_proof: Optional[List[str]] details: Optional[str] class DlcPayoutForm(BaseModel): diff --git a/cashu/core/errors.py b/cashu/core/errors.py index 1ec3ddfc..a6b50e47 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -119,4 +119,12 @@ class DlcNotFoundError(CashuError): code = 30002 def __init__(self, **kwargs): - super().__init__(self.detail, self.code) \ No newline at end of file + super().__init__(self.detail, self.code) + +class DlcSettlementFail(CashuError): + detail = "settlement verification failed: " + code = 30003 + + def __init__(self, **kwargs): + super().__init__(self.detail, self.code) + self.detail += kwargs['detail'] \ No newline at end of file diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 6539695a..e6b829de 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -23,7 +23,8 @@ DlcBadInput, DlcFundingProof, DLCWitness, - DiscreetLogContract + DiscreetLogContract, + DlcSettlement, ) from ..core.crypto import b_dhke from ..core.crypto.dlc import sign_dlc @@ -51,6 +52,8 @@ PostMintQuoteRequest, PostDlcRegistrationRequest, PostDlcRegistrationResponse, + PostDlcSettleRequest, + PostDlcSettleResponse, GetDlcStatusResponse, ) from ..core.settings import settings @@ -1148,7 +1151,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi active_keyset_for_unit = next( filter( lambda k: k.active and k.unit == Unit[registration.unit], - self.keysets.values() + self.keysets.values(), ) ) funding_privkey = next(iter(active_keyset_for_unit.private_keys.values())) @@ -1157,10 +1160,9 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi registration.funding_amount, funding_privkey, ) - funding_proof = DlcFundingProof( dlc_root=registration.dlc_root, - signature=signature.hex() + signature=signature.hex(), ) dlc = DiscreetLogContract( settled=False, @@ -1178,7 +1180,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi dlc_root=registration.dlc_root, bad_inputs=[DlcBadInput( index=-1, - detail=e.detail + detail=e.detail, )] )) # DLC verification fail @@ -1195,4 +1197,37 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi return PostDlcRegistrationResponse( funded=[reg[1] for reg in registered], errors=errors if len(errors) > 0 else None, - ) \ No newline at end of file + ) + + ''' + # UNFINISHED + async def settle_dlc(self, request: PostDlcSettleRequest) -> PostDlcSettleResponse: + """Settle DLCs once the oracle reveals the attestation secret or the timeout is over. + Args: + request (PostDlcSettleRequest): a request formatted following NUT-DLC spec + Returns: + PostDlcSettleResponse: Indicates which DLCs have been settled and potential errors. + """ + logger.trace("settle called") + verified: List[DlcSettlement] = [] + errors: List[DlcSettlement] = [] + for settlement in request: + try: + # Verify inclusion of payout structure and associated attestation in the DLC + assert settlement.outcome and settlement.merkle_proof, "outcome or merkle proof not provided" + await self.verify_dlc_inclusion(settlement.dlc_root, settlement.outcome, settlement.merkle_proof) + verified.append(settlement) + except DlcSettlementFail, AssertionError as e: + errors.append(DlcSettlement( + dlc_root=settlement.dlc_root, + details=e.details if isinstance(e, DlcSettlementFail) else str(e) + )) + # Database dance: + settled, db_errors = await self.db_write._settle_dlc(verified) + errors += db_errors + + return PostDlcSettleResponse( + settled=settled, + errors=errors if len(errors) > 0 else None, + ) + ''' \ No newline at end of file diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index fd0f87f2..6db7e63b 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -1,5 +1,7 @@ from typing import Dict, List, Literal, Optional, Tuple, Union +import json + from loguru import logger from hashlib import sha256 @@ -12,6 +14,7 @@ Unit, DlcBadInput, DLCWitness, + DlcOutcome, ) from ..core.crypto import b_dhke from ..core.crypto.dlc import merkle_verify @@ -25,6 +28,7 @@ TransactionError, TransactionUnitError, DlcVerificationFail, + DlcSettlementFail, ) from ..core.settings import settings from ..lightning.base import LightningBackend @@ -475,3 +479,24 @@ def raise_if_err(err): detail=exc.detail if exc else "input spending conditions verification failed" )) raise_if_err(errors) + + async def _verify_dlc_payout(self, P: str): + try: + payout = json.loads(P) + if not isinstance(payout, dict): + raise DlcSettlementFail(detail="Provided payout structure is not a dictionary") + if not all([isinstance(k, str) and isinstance(v, int) for k, v in payout.items()]): + raise DlcSettlementFail(detail="Provided payout structure is not a dictionary mapping strings to integers") + for v in payout.values(): + try: + b = bytes.fromhex(v) + if b[0] != b'\x02': + raise DlcSettlementFail(detail="Provided payout structure contains incorrect public keys") + except ValueError as e: + raise DlcSettlementFail(detail=str(e)) + except json.JSONDecodeError as e: + raise DlcSettlementFail(detail="cannot decode the provided payout structure") + + async def _verify_dlc_inclusion(self, dlc_root: str, outcome: DlcOutcome, merkle_proof: List[str]): + # Verify payout structure + await self._verify_dlc_payout(outcome.P)