Skip to content

Commit

Permalink
rename DiscreteLogContract to DiscreetLogContract, added `status_…
Browse files Browse the repository at this point in the history
…dlc`
  • Loading branch information
lollerfirst committed Aug 1, 2024
1 parent 932fa7a commit 10b6e9f
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 37 deletions.
12 changes: 11 additions & 1 deletion cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,7 @@ def parse_obj(cls, token_dict: dict):

# -------- DLC STUFF --------

class DiscreteLogContract(BaseModel):
class DiscreetLogContract(BaseModel):
"""
A discrete log contract
"""
Expand All @@ -1223,6 +1223,16 @@ class DiscreteLogContract(BaseModel):
inputs: Optional[List[Proof]] = None # Need to verify these are indeed SCT proofs
debts: Optional[Dict[str, int]] = None # We save who we owe money to here

@classmethod
def from_row(cls, row: Row):
return cls(
dlc_root=row["dlc_root"],
settled=bool(row["settled"]),
funding_amount=int(row["funding_amount"]),
unit=row["unit"],
debts=row["debts"] or None,
)

class DlcBadInput(BaseModel):
index: int
detail: str
Expand Down
8 changes: 7 additions & 1 deletion cashu/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,10 @@ class DlcAlreadyRegisteredError(CashuError):

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


class DlcNotFoundError(CashuError):
detail = "dlc not found"
code = 30002

def __init__(self, **kwargs):
super().__init__(self.detail, self.code)
10 changes: 5 additions & 5 deletions cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
BlindedMessage,
BlindedMessage_Deprecated,
BlindedSignature,
DiscreteLogContract,
DiscreetLogContract,
DlcFundingProof,
DlcPayout,
DlcPayoutForm,
Expand Down Expand Up @@ -334,7 +334,7 @@ def __init__(self, **data):
# ------- API: DLC REGISTRATION -------

class PostDlcRegistrationRequest(BaseModel):
registrations: List[DiscreteLogContract]
registrations: List[DiscreetLogContract]

class PostDlcRegistrationResponse(BaseModel):
funded: List[DlcFundingProof] = []
Expand All @@ -351,7 +351,6 @@ class PostDlcSettleResponse(BaseModel):

# ------- API: DLC PAYOUT -------
class PostDlcPayoutRequest(BaseModel):
atomic: Optional[bool]
payouts: List[DlcPayoutForm]

class PostDlcPayoutResponse(BaseModel):
Expand All @@ -362,5 +361,6 @@ class PostDlcPayoutResponse(BaseModel):

class GetDlcStatusResponse(BaseModel):
settled: bool
funding_amount: Optional[int]
debts: Optional[Dict[str, int]]
unit: Optional[str] = None
funding_amount: Optional[int] = None
debts: Optional[Dict[str, int]] = None
16 changes: 9 additions & 7 deletions cashu/mint/crud.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
from ..core.base import DiscreteLogContract
from ..core.base import DiscreetLogContract

from ..core.base import (
BlindedSignature,
Expand Down Expand Up @@ -250,13 +250,13 @@ async def get_registered_dlc(
dlc_root: str,
db: Database,
conn: Optional[Connection] = None,
) -> Optional[DiscreteLogContract]:
) -> Optional[DiscreetLogContract]:
...

@abstractmethod
async def store_dlc(
self,
dlc: DiscreteLogContract,
dlc: DiscreetLogContract,
db: Database,
conn: Optional[Connection] = None,
) -> None:
Expand Down Expand Up @@ -765,17 +765,19 @@ async def get_registered_dlc(
dlc_root: str,
db: Database,
conn: Optional[Connection] = None,
) -> Optional[DiscreteLogContract]:
) -> Optional[DiscreetLogContract]:
query = f"""
SELECT * from {db.table_with_schema('dlc')}
WHERE dlc_root = :dlc_root
"""
result = await (conn or db).fetchone(query, {"dlc_root": dlc_root})
return result
row = await (conn or db).fetchone(query, {"dlc_root": dlc_root})
if not row:
return None
return DiscreetLogContract.from_row(row)

async def store_dlc(
self,
dlc: DiscreteLogContract,
dlc: DiscreetLogContract,
db: Database,
conn: Optional[Connection] = None,
) -> None:
Expand Down
16 changes: 14 additions & 2 deletions cashu/mint/db/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from ...core.base import Proof, ProofSpentState, ProofState
from ...core.db import Connection, Database
from ...core.errors import TokenAlreadySpentError, DlcAlreadyRegisteredError
from ...core.errors import (
TokenAlreadySpentError,
DlcAlreadyRegisteredError,
DlcNotFoundError,
)
from ..crud import LedgerCrud


Expand Down Expand Up @@ -97,4 +101,12 @@ async def _verify_dlc_registrable(
):
async with self.db.get_connection(conn) as conn:
if await self.crud.get_registered_dlc(dlc_root, self.db, conn) is not None:
raise DlcAlreadyRegisteredError()
raise DlcAlreadyRegisteredError()

async def _get_registered_dlc(self, dlc_root: str, conn: Optional[Connection] = None):
async with self.db.get_connection(conn) as conn:
dlc = await self.crud.get_registered_dlc(dlc_root, self.db, conn)
if dlc is None:
raise DlcNotFoundError()
return dlc

14 changes: 7 additions & 7 deletions cashu/mint/db/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Proof,
ProofSpentState,
ProofState,
DiscreteLogContract,
DiscreetLogContract,
DlcFundingProof,
DlcBadInput,
)
Expand Down Expand Up @@ -231,19 +231,19 @@ async def _unset_melt_quote_pending(

async def _verify_proofs_and_dlc_registrations(
self,
registrations: List[Tuple[DiscreteLogContract, DlcFundingProof]],
) -> Tuple[List[Tuple[DiscreteLogContract, DlcFundingProof]], List[DlcFundingProof]]:
registrations: List[Tuple[DiscreetLogContract, DlcFundingProof]],
) -> Tuple[List[Tuple[DiscreetLogContract, DlcFundingProof]], List[DlcFundingProof]]:
"""
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[DiscreteLogContract, DlcFundingProof]]): List of registrations.
registrations (List[Tuple[DiscreetLogContract, DlcFundingProof]]): List of registrations.
Returns:
List[Tuple[DiscreteLogContract, DlcFundingProof]]: a list of registered DLCs
List[Tuple[DiscreetLogContract, DlcFundingProof]]: a list of registered DLCs
List[DlcFundingProof]: a list of errors
"""
checked: List[Tuple[DiscreteLogContract, DlcFundingProof]] = []
registered: List[Tuple[DiscreteLogContract, DlcFundingProof]] = []
checked: List[Tuple[DiscreetLogContract, DlcFundingProof]] = []
registered: List[Tuple[DiscreetLogContract, DlcFundingProof]] = []
errors: List[DlcFundingProof]= []
if len(registrations) == 0:
logger.trace("Received 0 registrations")
Expand Down
32 changes: 29 additions & 3 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
DlcBadInput,
DlcFundingProof,
DLCWitness,
DiscreteLogContract
DiscreetLogContract
)
from ..core.crypto import b_dhke
from ..core.crypto.dlc import sign_dlc
Expand Down Expand Up @@ -51,6 +51,7 @@
PostMintQuoteRequest,
PostDlcRegistrationRequest,
PostDlcRegistrationResponse,
GetDlcStatusResponse,
)
from ..core.settings import settings
from ..core.split import amount_split
Expand Down Expand Up @@ -1095,6 +1096,31 @@ async def _generate_promises(
signatures.append(signature)
return signatures

async def status_dlc(self, dlc_root: str) -> GetDlcStatusResponse:
"""Gets the status of a particular DLC
Args:
dlc_root (str): the root hash of the contract
Returns:
GetDlcStatusResponse: a response containing the status of the DLC, if it was found.
Raises:
DlcNotFoundError: no DLC with dlc_root was found
"""
logger.trace("status_dlc called")
dlc = await self.db_read._get_registered_dlc(dlc_root)
if not dlc.settled:
return GetDlcStatusResponse(
settled=dlc.settled,
funding_amount=dlc.funding_amount,
unit=dlc.unit,
debts=None
)
else:
return GetDlcStatusResponse(
settled=dlc.settled,
debts=dlc.debts,
)

async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegistrationResponse:
"""Validates and registers DiscreteLogContracts
Args:
Expand All @@ -1103,7 +1129,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[DiscreteLogContract, DlcFundingProof]] = []
funded: List[Tuple[DiscreetLogContract, DlcFundingProof]] = []
errors: List[DlcFundingProof] = []
for registration in request.registrations:
try:
Expand Down Expand Up @@ -1136,7 +1162,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi
dlc_root=registration.dlc_root,
signature=signature.hex()
)
dlc = DiscreteLogContract(
dlc = DiscreetLogContract(
settled=False,
dlc_root=registration.dlc_root,
funding_amount=amount_provided,
Expand Down
29 changes: 22 additions & 7 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
PostSwapRequest,
PostSwapResponse,
PostDlcRegistrationRequest,
PostDlcRegistrationResponse
PostDlcRegistrationResponse,
GetDlcStatusResponse,
)
from ..core.settings import settings
from ..mint.startup import ledger
Expand Down Expand Up @@ -379,16 +380,30 @@ async def restore(payload: PostRestoreRequest) -> PostRestoreResponse:
return PostRestoreResponse(outputs=outputs, signatures=signatures)

@router.post(
"v1/register",
name="Register",
summary="Register a DLC batch",
"v1/dlc/fund",
name="Fund",
summary="Register and fund a DLC batch",
response_model=PostDlcRegistrationResponse,
response_description=(
"Two lists describing which DLC were registered and which encountered errors respectively."
)
)
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def register(request: Request, payload: PostDlcRegistrationRequest) -> PostDlcRegistrationResponse:
logger.trace(f"> POST /v1/register: {payload}")
async def dlc_fund(request: Request, payload: PostDlcRegistrationRequest) -> PostDlcRegistrationResponse:
logger.trace(f"> POST /v1/dlc/fund: {payload}")
assert len(payload.registrations) > 0, "No registrations provided"
return await ledger.register_dlc(payload)
return await ledger.register_dlc(payload)

@router.get(
"v1/dlc/status/{dlc_root}",
name="",
summary="Register a DLC batch",
response_model=GetDlcStatusResponse,
response_description=(
"Two lists describing which DLC were registered and which encountered errors respectively."
)
)
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def dlc_status(request: Request, dlc_root: str) -> GetDlcStatusResponse:
logger.trace(f"> GET /v1/dlc/status/{dlc_root}")
return await ledger.status_dlc(dlc_root)
61 changes: 57 additions & 4 deletions tests/test_dlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from cashu.wallet.wallet import Wallet
from cashu.core.secret import Secret, SecretKind
from cashu.core.errors import CashuError
from cashu.core.base import DLCWitness, Proof, TokenV4, Unit, DiscreteLogContract
from cashu.core.base import DLCWitness, Proof, TokenV4, Unit, DiscreetLogContract
from cashu.core.models import PostDlcRegistrationRequest, PostDlcRegistrationResponse
from cashu.mint.ledger import Ledger
from cashu.wallet.helpers import send
Expand Down Expand Up @@ -283,7 +283,7 @@ async def test_registration_vanilla_proofs(wallet: Wallet, ledger: Ledger):
pubkey = next(iter(active_keyset_for_unit.public_keys.values()))

dlc_root = sha256("TESTING".encode()).hexdigest()
dlc = DiscreteLogContract(
dlc = DiscreetLogContract(
funding_amount=64,
unit="sat",
dlc_root=dlc_root,
Expand Down Expand Up @@ -319,7 +319,7 @@ async def test_registration_dlc_locked_proofs(wallet: Wallet, ledger: Ledger):
active_keyset_for_unit = next(filter(lambda k: k.active and k.unit == Unit["sat"], keysets))
pubkey = next(iter(active_keyset_for_unit.public_keys.values()))

dlc = DiscreteLogContract(
dlc = DiscreetLogContract(
funding_amount=64,
unit="sat",
dlc_root=dlc_root,
Expand All @@ -335,4 +335,57 @@ async def test_registration_dlc_locked_proofs(wallet: Wallet, ledger: Ledger):
assert (
verify_dlc_signature(dlc_root, 64, bytes.fromhex(funding_proof.signature), pubkey),
"Could not verify funding proof"
)
)

@pytest.mark.asyncio
async def test_fund_same_dlc_twice(wallet: Wallet, ledger: Ledger):
invoice = await wallet.request_mint(128)
await pay_if_regtest(invoice.bolt11)
minted = await wallet.mint(128, id=invoice.id)

dlc_root = sha256("TESTING".encode()).hexdigest()
proofs2, proofs1 = await wallet.split(minted, 64)

dlc1 = DiscreetLogContract(
funding_amount=64,
unit="sat",
dlc_root=dlc_root,
inputs=proofs1,
)
dlc2 = DiscreetLogContract(
funding_amount=64,
unit="sat",
dlc_root=dlc_root,
inputs=proofs2,
)
request = PostDlcRegistrationRequest(registrations=[dlc1])
response = await ledger.register_dlc(request)
assert response.errors is None, f"Funding proofs error: {response.errors[0].bad_inputs}"
request = PostDlcRegistrationRequest(registrations=[dlc2])
response = await ledger.register_dlc(request)
assert response.errors and response.errors[0].bad_inputs[0].detail == "dlc already registered"

@pytest.mark.asyncio
async def test_fund_same_dlc_twice_same_batch(wallet: Wallet, ledger: Ledger):
invoice = await wallet.request_mint(128)
await pay_if_regtest(invoice.bolt11)
minted = await wallet.mint(128, id=invoice.id)

dlc_root = sha256("TESTING".encode()).hexdigest()
proofs2, proofs1 = await wallet.split(minted, 64)

dlc1 = DiscreetLogContract(
funding_amount=64,
unit="sat",
dlc_root=dlc_root,
inputs=proofs1,
)
dlc2 = DiscreetLogContract(
funding_amount=64,
unit="sat",
dlc_root=dlc_root,
inputs=proofs2,
)
request = PostDlcRegistrationRequest(registrations=[dlc1, dlc2])
response = await ledger.register_dlc(request)
assert response.errors and len(response.errors) == 1, f"Funding proofs error: {response.errors[0].bad_inputs}"

0 comments on commit 10b6e9f

Please sign in to comment.