Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor handling of DLC input validation #1

Merged
merged 5 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class P2PKWitness(BaseModel):
def from_witness(cls, witness: str):
return cls(**json.loads(witness))

class DLCWitness(BaseModel):
class SCTWitness(BaseModel):
leaf_secret: str
merkle_proof: List[str]
witness: Optional[str] = None
Expand Down Expand Up @@ -213,12 +213,12 @@ def p2pksigs(self) -> List[str]:
@property
def dlc_leaf_secret(self) -> str:
assert self.witness, "Witness is missing for dlc leaf secret"
return DLCWitness.from_witness(self.witness).leaf_secret
return SCTWitness.from_witness(self.witness).leaf_secret

@property
def dlc_merkle_proof(self) -> List[str]:
assert self.witness, "Witness is missing for dlc merkle proof"
return DLCWitness.from_witness(self.witness).merkle_proof
return SCTWitness.from_witness(self.witness).merkle_proof

@property
def htlcpreimage(self) -> Union[str, None]:
Expand Down
3 changes: 2 additions & 1 deletion cashu/core/crypto/dlc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from hashlib import sha256
from typing import List, Optional, Tuple

from secp256k1 import PrivateKey, PublicKey


Expand Down Expand Up @@ -85,4 +86,4 @@ def verify_dlc_signature(
+str(funding_amount).encode("utf-8")
)
message_hash = sha256(message).digest()
return pubkey.schnorr_verify(message_hash, signature, None, raw=True)
return pubkey.schnorr_verify(message_hash, signature, None, raw=True)
8 changes: 4 additions & 4 deletions cashu/core/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional, List
from .base import DlcBadInput
from typing import Optional


class CashuError(Exception):
code: int
Expand Down Expand Up @@ -110,7 +110,7 @@ def __init__(self, **kwargs):
class DlcAlreadyRegisteredError(CashuError):
detail = "dlc already registered"
code = 30001

def __init__(self, **kwargs):
super().__init__(self.detail, self.code)

Expand All @@ -127,4 +127,4 @@ class DlcSettlementFail(CashuError):

def __init__(self, **kwargs):
super().__init__(self.detail, self.code)
self.detail += kwargs['detail']
self.detail += kwargs['detail']
76 changes: 60 additions & 16 deletions cashu/mint/conditions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import hashlib
import time
from typing import List
from typing import List, Optional

from loguru import logger

from ..core.base import BlindedMessage, DLCWitness, HTLCWitness, Proof
from ..core.base import (
BlindedMessage,
DiscreetLogContract,
HTLCWitness,
Proof,
SCTWitness,
)
from ..core.crypto.dlc import merkle_verify
from ..core.crypto.secp import PublicKey
from ..core.errors import (
Expand Down Expand Up @@ -193,15 +199,47 @@ def _verify_htlc_spending_conditions(self, proof: Proof, secret: Secret) -> bool
# no pubkeys were included, anyone can spend
return True

def _verify_sct_spending_conditions(self, proof: Proof, secret: Secret) -> bool:
def _verify_dlc_spending_conditions(self, p: Proof, secret: Secret, funding_dlc: DiscreetLogContract) -> bool:
"""
Verify DLC spending conditions for a single input.
"""
# Verify secret is of kind DLC
if secret.kind != SecretKind.DLC.value:
raise TransactionError('expected secret of kind DLC')

# Verify dlc_root is the one referenced in the secret
if secret.data != funding_dlc.dlc_root:
raise TransactionError('attempted to use kind:DLC secret to fund a DLC with the wrong root hash')

# Verify the DLC funding amount meets the threshold tag.
# If the threshold tag is invalid or missing, ignore it and allow
# spending with any nonzero funding amount.
try:
tag = secret.tags.get_tag('threshold')
if tag is not None:
threshold = int(tag)
except Exception:
threshold = 0

if funding_dlc.funding_amount < threshold:
raise TransactionError('DLC funding_amount does not satisfy DLC secret threshold tag')

return True

def _verify_sct_spending_conditions(
self,
proof: Proof,
secret: Secret,
funding_dlc: Optional[DiscreetLogContract] = None,
) -> bool:
"""
Verify SCT spending conditions for a single input
"""
if proof.witness is None:
return False
raise TransactionError('missing SCT secret witness')

witness = DLCWitness.from_witness(proof.witness)
assert witness, TransactionError("No or corrupt DLC witness data provided for a secret kind SCT")
witness = SCTWitness.from_witness(proof.witness)
assert witness, TransactionError("invalid witness data provided for secret kind SCT")

spending_condition = False
try:
Expand All @@ -220,27 +258,31 @@ def _verify_sct_spending_conditions(self, proof: Proof, secret: Secret) -> bool:
valid = merkle_verify(merkle_root_bytes, leaf_secret_bytes, merkle_proof_bytes)

if not valid:
return False
raise TransactionError('SCT secret merkle proof verification failed')

if not spending_condition: # means that it is valid and a normal secret
return True

# leaf_secret is a secret of another kind: verify that kind
# We only ever need the secret and the witness data
new_proof = Proof(
secret=witness.leaf_secret,
witness=witness.witness
)
return self._verify_input_spending_conditions(new_proof)
return self._verify_input_spending_conditions(new_proof, funding_dlc)

def _verify_input_spending_conditions(self, proof: Proof) -> bool:
def _verify_input_spending_conditions(
self,
proof: Proof,
funding_dlc: Optional[DiscreetLogContract] = None,
) -> bool:
"""
Verify spending conditions:
Condition: P2PK - Checks if signature in proof.witness is valid for pubkey in proof.secret
Condition: HTLC - Checks if preimage in proof.witness is valid for hash in proof.secret
Condition: SCT - Checks if leaf_secret in proof.witness is a leaf of the Merkle Tree with
root proof.secret.data according to proof.witness.merkle_proof
Condition: DLC - NEVER SPEND (can only be registered)
Condition: DLC - NEVER SPEND unless for funding a DLC
"""

try:
Expand All @@ -258,14 +300,16 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool:
# HTLC
if SecretKind(secret.kind) == SecretKind.HTLC:
return self._verify_htlc_spending_conditions(proof, secret)

# SCT
if SecretKind(secret.kind) == SecretKind.SCT:
return self._verify_sct_spending_conditions(proof, secret)
return self._verify_sct_spending_conditions(proof, secret, funding_dlc)

# DLC
if SecretKind(secret.kind) == SecretKind.DLC:
return False
if funding_dlc is None:
raise TransactionError('cannot spend secret kind DLC unless funding a DLC')
return self._verify_dlc_spending_conditions(proof, secret, funding_dlc)

# no spending condition present
return True
Expand Down
4 changes: 2 additions & 2 deletions cashu/mint/crud.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import json
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
from ..core.base import DiscreetLogContract

from ..core.base import (
BlindedSignature,
DiscreetLogContract,
MeltQuote,
MintKeyset,
MintQuote,
Expand Down Expand Up @@ -824,4 +824,4 @@ async def set_dlc_settled_and_debts(
"dlc_root": dlc_root,
"debts": debts
},
)
)
5 changes: 2 additions & 3 deletions cashu/mint/db/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from ...core.base import Proof, ProofSpentState, ProofState
from ...core.db import Connection, Database
from ...core.errors import (
TokenAlreadySpentError,
DlcAlreadyRegisteredError,
DlcNotFoundError,
TokenAlreadySpentError,
)
from ..crud import LedgerCrud

Expand Down Expand Up @@ -97,7 +97,7 @@ async def _verify_proofs_spendable(
raise TokenAlreadySpentError()

async def _verify_dlc_registrable(
self, dlc_root: str, conn: Optional[Connection] = None,
self, dlc_root: str, conn: Optional[Connection] = None,
):
async with self.db.get_connection(conn) as conn:
if await self.crud.get_registered_dlc(dlc_root, self.db, conn) is not None:
Expand All @@ -109,4 +109,3 @@ async def _get_registered_dlc(self, dlc_root: str, conn: Optional[Connection] =
if dlc is None:
raise DlcNotFoundError()
return dlc

21 changes: 10 additions & 11 deletions cashu/mint/db/write.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
from typing import List, Optional, Union, Tuple
from typing import List, Optional, Tuple, Union

from loguru import logger

from ...core.base import (
DiscreetLogContract,
DlcBadInput,
DlcFundingProof,
DlcSettlement,
MeltQuote,
MeltQuoteState,
MintQuote,
MintQuoteState,
Proof,
ProofSpentState,
ProofState,
DiscreetLogContract,
DlcFundingProof,
DlcBadInput,
DlcSettlement,
)
from ...core.db import Connection, Database
from ...core.errors import (
TransactionError,
TokenAlreadySpentError,
DlcAlreadyRegisteredError,
DlcSettlementFail,
TokenAlreadySpentError,
TransactionError,
)
from ..crud import LedgerCrud
from ..events.events import LedgerEventManager
Expand Down Expand Up @@ -279,7 +278,7 @@ async def _verify_proofs_and_dlc_registrations(
await self.crud.invalidate_proof(
proof=p, db=self.db, conn=conn
)

logger.trace(f"Registering DLC {reg.dlc_root}")
await self.crud.store_dlc(reg, self.db, conn)
registered.append(registration)
Expand Down Expand Up @@ -317,7 +316,7 @@ async def _settle_dlc(
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)
Expand All @@ -326,4 +325,4 @@ async def _settle_dlc(
dlc_root=settlement.dlc_root,
details=f"error with the DB: {str(e)}"
))
return (settled, errors)
return (settled, errors)
26 changes: 5 additions & 21 deletions cashu/mint/dlc.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
from ..core.nuts import DLC_NUT
from ..core.base import Proof
from ..core.secret import Secret, SecretKind
from typing import Dict

from ..core.errors import TransactionError
from ..core.nuts import DLC_NUT
from .features import LedgerFeatures

from json.decoder import JSONDecodeError

from typing import List, Tuple, Dict
class LedgerDLC(LedgerFeatures):

async def filter_sct_proofs(self, proofs: List[Proof]) -> Tuple[List[Proof], List[Proof]]:
deserializable = []
non_sct_proofs = []
for p in proofs:
try:
Secret.deserialize(p.secret)
deserializable.append(p)
except JSONDecodeError:
non_sct_proofs.append(p)

sct_proofs = list(filter(lambda p: Secret.deserialize(p.secret).kind == SecretKind.SCT.value, deserializable))
non_sct_proofs += list(filter(lambda p: p not in sct_proofs, deserializable))
return (sct_proofs, non_sct_proofs)

async def get_dlc_fees(self, fa_unit: str) -> Dict[str, int]:
try:
fees = self.mint_features()[DLC_NUT]
Expand All @@ -32,5 +16,5 @@ async def get_dlc_fees(self, fa_unit: str) -> Dict[str, int]:
fees = fees[fa_unit]
assert isinstance(fees, dict)
return fees
except Exception as e:
raise TransactionError("could not get fees for the specified funding_amount denomination")
except Exception:
raise TransactionError("could not get fees for the specified funding_amount denomination")
Loading
Loading