Skip to content

Commit

Permalink
* mint DB add dlc table migration
Browse files Browse the repository at this point in the history
* started working on dlc registration
  • Loading branch information
lollerfirst committed Jul 18, 2024
1 parent ea56b57 commit 6a3e8d3
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 6 deletions.
12 changes: 6 additions & 6 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,13 +1212,13 @@ class DiscreteLogContract(BaseModel):
"""
A discrete log contract
"""
settled: bool = False
settled: Optional[bool] = False
dlc_root: str
funding_amount: int
inputs: List[Proof] # Need to verify these are indeed SCT proofs
debts: Dict[str, int] = {} # We save who we owe money to here
inputs: Optional[List[Proof]] # Need to verify these are indeed SCT proofs
debts: Optional[Dict[str, int]] = None # We save who we owe money to here

class DlcBadInputs(BaseModel):
class DlcBadInput(BaseModel):
index: int
detail: str

Expand All @@ -1227,8 +1227,8 @@ class DlcFundingProof(BaseModel):
A dlc merkle root with its signature
"""
dlc_root: str
signature: Optional[str]
bad_inputs: Optional[List[DlcBadInputs]] = None # Used to specify potential errors
signature: Optional[str] = None
bad_inputs: Optional[List[DlcBadInput]] = None # Used to specify potential errors

class DlcOutcome(BaseModel):
"""
Expand Down
98 changes: 98 additions & 0 deletions cashu/mint/dlc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from .ledger import Ledger
from ..core.models import PostDlcRegistrationRequest, PostDlcRegistrationResponse
from ..core.base import DlcBadInput, DlcFundingProof, Proof, DLCWitness
from ..core.secret import Secret, SecretKind
from ..core.crypto.dlc import list_hash, merkle_verify
from ..core.errors import TransactionError
from ..core.nuts import DLC_NUT


from hashlib import sha256
from loguru import logger
from typing import List, Dict, Optional

class LedgerDLC(Ledger):

async def _verify_dlc_input_spending_conditions(self, dlc_root: str, p: Proof) -> bool:
if not p.witness:
return False
witness = DLCWitness.from_witness(p.witness)
leaf_secret = Secret.deserialize(witness.leaf_secret)
secret = Secret.deserialize(p.secret)
# Verify secret is of kind SCT
if secret.kind != SecretKind.SCT.value:
return False
# Verify leaf_secret is of kind DLC
if leaf_secret.kind != SecretKind.DLC.value:
return False
# Verify dlc_root is the one referenced in the secret
if leaf_secret.data != dlc_root:
return False
# (Optional?) Verify inclusion of leaf_secret in the SCT root hash
leaf_hash_bytes = sha256(witness.leaf_secret.encode()).digest()
merkle_proof_bytes = [bytes.fromhex(m) for m in witness.merkle_proof]
sct_root_hash_bytes = bytes.fromhex(secret.data)
if not merkle_verify(sct_root_hash_bytes, leaf_hash_bytes, merkle_proof_bytes):
return False

return True


async def _verify_dlc_inputs(self, dlc_root: str, proofs: Optional[List[Proof]]):
# Verify inputs
if not proofs:
raise TransactionError("no proofs provided.")
# Verify amounts of inputs
if not all([self._verify_amount(p.amount) for p in proofs]):
raise TransactionError("invalid amount.")
# Verify secret criteria
if not all([self._verify_secret_criteria(p) for p in proofs]):
raise TransactionError("secrets do not match criteria.")
# verify that only unique proofs were used
if not self._verify_no_duplicate_proofs(proofs):
raise TransactionError("duplicate proofs.")
# Verify ecash signatures
if not all([self._verify_proof_bdhke(p) for p in proofs]):
raise TransactionError("could not verify proofs.")
# Verify input spending conditions
if not all([self._verify_dlc_input_spending_conditions(dlc_root, p) for p in proofs]):
raise TransactionError("validation of input spending conditions failed.")


async def _verify_dlc_amount_fees_coverage(self, funding_amount: int, proofs: List[Proof]):
# Verify proofs of the same denomination
u = self.keysets[proofs[0].id].unit
if not all([self.keysets[p.id].unit == u for p in proofs]):
raise TransactionError("all the inputs must be of the same denomination")
fees = self.mint_features()[DLC_NUT]
assert isinstance(fees, dict)
fees = fees['fees']
assert isinstance(fees, dict)
amount_provided = sum([p.amount for p in proofs])
amount_needed = funding_amount + fees['base'] + (funding_amount * fees['ppk'] // 1000)
if amount_needed < amount_provided:
raise TransactionError("funds provided do not cover the DLC funding amount")

# UNFINISHED
async def register_dlc(self, request: PostDlcRegistrationRequest):
logger.trace("swap called")
is_atomic = request.atomic
funded: List[DlcFundingProof] = []
errors: List[DlcFundingProof] = []
for registration in request.registrations:
try:
logger.trace(f"processing registration {registration.dlc_root}")
await self._verify_dlc_inputs(registration.dlc_root, registration.inputs)
assert registration.inputs is not None
await self._verify_dlc_amount_fees_coverage(registration.funding_amount, registration.inputs)
await self.db_write._verify_spent_proofs_and_set_pending(registration.inputs)
except TransactionError as e:
# I know this is horrificly out of spec -- I want to get it to work
errors.append(DlcFundingProof(
dlc_root=registration.dlc_root,
bad_inputs=[DlcBadInput(
index=-1,
detail=e.detail
)]
))

16 changes: 16 additions & 0 deletions cashu/mint/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,3 +825,19 @@ async def m021_add_change_and_expiry_to_melt_quotes(db: Database):
await conn.execute(
f"ALTER TABLE {db.table_with_schema('melt_quotes')} ADD COLUMN expiry TIMESTAMP"
)

async def m022_add_dlc_table(db: Database):
async with db.connect() as conn:
await conn.execute(
f"""
CREATE TABLE IF NOT EXISTS {db.table_with_schema('dlc')} (
dlc_root TEXT NOT NULL,
settled BOOL NOT NULL DEFAULT FALSE,
funding_amount {db.big_int} NOT NULL,
debts MEDIUMTEXT,
UNIQUE (dlc_root),
CHECK (funding_amount > 0)
);
"""
)
4 changes: 4 additions & 0 deletions cashu/wallet/dlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ async def filter_proofs_by_dlc_root(self, dlc_root: str, proofs: List[Proof]) ->
return list(filter(lambda p: p.dlc_root == dlc_root, proofs))

async def filter_non_dlc_proofs(self, proofs: List[Proof]) -> List[Proof]:
"""Returns a list of proofs each having None or empty dlc root
"""
return list(filter(lambda p: p.dlc_root is None or p.dlc_root == "", proofs))

async def filter_dlc_proofs(self, proofs: List[Proof]) -> List[Proof]:
"""Returns a list of proofs each having a non empty dlc root
"""
return list(filter(lambda p: p.dlc_root is not None and p.dlc_root != "", proofs))

0 comments on commit 6a3e8d3

Please sign in to comment.