From a5b147bc13dfedda6e21cdf1341d33f04c8090af Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 30 Jul 2024 12:20:51 +0200 Subject: [PATCH] Database shenanigans --- cashu/core/errors.py | 7 ++++++ cashu/mint/crud.py | 52 ++++++++++++++++++++++++++++++++++++++++ cashu/mint/db/read.py | 9 ++++++- cashu/mint/db/write.py | 51 ++++++++++++++++++++++++++++++++++++++- cashu/mint/ledger.py | 32 ++++++++++++++++++------- cashu/mint/migrations.py | 1 + 6 files changed, 142 insertions(+), 10 deletions(-) diff --git a/cashu/core/errors.py b/cashu/core/errors.py index 936bbe47..1d4701c4 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -106,4 +106,11 @@ class DlcVerificationFail(CashuError): def __init__(self, **kwargs): super().__init__(self.detail, self.code) self.bad_inputs = kwargs['bad_inputs'] + +class DlcAlreadyRegisteredError(CashuError): + detail = "dlc already registered" + code = 30001 + + def __init__(self, **kwargs): + super().__init__(self.detail, self.code) \ No newline at end of file diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 0ef4af9d..9566b37e 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -1,6 +1,7 @@ import json from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional +from .base import DiscreteLogContract from ..core.base import ( BlindedSignature, @@ -243,6 +244,23 @@ async def update_melt_quote( ) -> None: ... + @abstractmethod + async def get_registered_dlc( + self, + dlc_root: str, + db: Database, + conn: Optional[Connection] = None, + ) -> DiscreteLogContract: + ... + + @abstractmethod + async def store_dlc( + self, + dlc: DiscreteLogContract, + db: Database, + conn: Optional[Connection] = None, + ) -> None: + ... class LedgerCrudSqlite(LedgerCrud): """Implementation of LedgerCrud for sqlite. @@ -741,3 +759,37 @@ async def get_proofs_used( values = {f"y_{i}": Ys[i] for i in range(len(Ys))} rows = await (conn or db).fetchall(query, values) return [Proof(**r) for r in rows] if rows else [] + + async def get_registered_dlc( + self, + dlc_root: str, + db: Database, + conn: Optional[Connection] = None, + ) -> Optional[DiscreteLogContract]: + 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 + + async def store_dlc( + self, + dlc: DiscreteLogContract, + db: Database, + conn: Optional[Connection] = None, + ) -> None: + query = f""" + INSERT INTO {db.table_with_schema('dlc')} + (dlc_root, settled, funding_amount, unit) + VALUES (:dlc_root, :settled, :funding_amount, :unit) + """ + await (conn or db).execute( + query, + { + "dlc_root": dlc.dlc_root, + "settled": dlc.settled, + "funding_amount": dlc.funding_amount, + "unit": dlc.unit, + }, + ) diff --git a/cashu/mint/db/read.py b/cashu/mint/db/read.py index f3231780..6b77d05e 100644 --- a/cashu/mint/db/read.py +++ b/cashu/mint/db/read.py @@ -2,7 +2,7 @@ from ...core.base import Proof, ProofSpentState, ProofState from ...core.db import Connection, Database -from ...core.errors import TokenAlreadySpentError +from ...core.errors import TokenAlreadySpentError, DlcAlreadyRegisteredError from ..crud import LedgerCrud @@ -91,3 +91,10 @@ async def _verify_proofs_spendable( async with self.db.get_connection(conn) as conn: if not len(await self._get_proofs_spent([p.Y for p in proofs], conn)) == 0: raise TokenAlreadySpentError() + + async def _verify_dlc_registrable( + 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: + raise DlcAlreadyRegisteredError() \ No newline at end of file diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index 242e659d..6a1221bc 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union +from typing import List, Optional, Union, Tuple from loguru import logger @@ -10,10 +10,15 @@ Proof, ProofSpentState, ProofState, + DiscreteLogContract, + DlcFundingProof, + DlcBadInput, ) from ...core.db import Connection, Database from ...core.errors import ( TransactionError, + TokenAlreadySpentError, + DlcAlreadyRegisteredError, ) from ..crud import LedgerCrud from ..events.events import LedgerEventManager @@ -223,3 +228,47 @@ async def _unset_melt_quote_pending( await self.events.submit(quote_copy) return quote_copy + + async def _verify_proofs_and_dlc_registrations( + self, + registrations: List[Tuple[DiscreteLogContract, DlcFundingProof]], + is_atomic: bool, + ) -> Tuple[List[Tuple[DiscreteLogContract, DlcFundingProof]], List[DlcFundingProof]]: + ok: List[Tuple[DiscreteLogContract, DlcFundingProof]] = [] + errors: List[DlcFundingProof]= [] + logger.trace("_verify_proofs_and_dlc_registrations acquiring lock") + async with self.db.get_connection(lock_table="proofs_used") as conn: + for registration in registrations: + reg = registration[0] + logger.trace("checking whether proofs are already spent") + try: + assert reg.inputs + await self.db_read._verify_proofs_spendable(reg.inputs, conn) + await self.db_read._verify_dlc_registrable(reg.dlc_root, conn) + ok.append(registration) + except (TokenAlreadySpentError, DlcAlreadyRegisteredError) as e: + logger.trace(f"Proofs already spent for registration {reg.dlc_root}") + errors.append(DlcFundingProof( + dlc_root=reg.dlc_root, + bad_inputs=[DlcBadInput( + index=-1, + detail=e.detail + )] + )) + + # Do not continue if errors on atomic + if is_atomic and len(errors) > 0: + return (ok, errors) + + for registration in ok: + reg = registration[0] + assert reg.inputs + for p in reg.inputs: + logger.trace(f"Invalidating proof {p.Y}") + 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) + logger.trace("_verify_proofs_and_dlc_registrations lock released") + return (ok, errors) \ No newline at end of file diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 0ab962da..afa321c5 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -50,6 +50,7 @@ PostMeltQuoteResponse, PostMintQuoteRequest, PostDlcRegistrationRequest, + PostDlcRegistrationResponse, ) from ..core.settings import settings from ..core.split import amount_split @@ -1094,9 +1095,9 @@ async def _generate_promises( signatures.append(signature) return signatures - async def register_dlc(self, request: PostDlcRegistrationRequest): + async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegistrationResponse: logger.trace("register called") - is_atomic = request.atomic + is_atomic = request.atomic or False funded: List[Tuple[DiscreteLogContract, DlcFundingProof]] = [] errors: List[DlcFundingProof] = [] for registration in request.registrations: @@ -1114,11 +1115,11 @@ async def register_dlc(self, request: PostDlcRegistrationRequest): # We use the funding proof private key ''' signature = sign_dlc( - registration.dlc_root, - registration.funding_amount, - registration.unit, - self.funding_proof_private_key - ) + registration.dlc_root, + registration.funding_amount, + registration.unit, + self.funding_proof_private_key + ) funding_proof = DlcFundingProof( dlc_root=registration.dlc_root, signature=signature.hex() @@ -1127,6 +1128,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest): settled=False, dlc_root=registration.dlc_root, funding_amount=amount_provided, + inputs=registration.inputs, unit=registration.unit, ) funded.append((dlc, funding_proof)) @@ -1147,4 +1149,18 @@ async def register_dlc(self, request: PostDlcRegistrationRequest): errors.append(DlcFundingProof( dlc_root=registration.dlc_root, bad_inputs=e.bad_inputs, - )) \ No newline at end of file + )) + # If `atomic` register and there are errors, abort + if is_atomic and len(errors) > 0: + return PostDlcRegistrationResponse(errors=errors) + # Database dance: + funded, db_errors = await self.db_write._verify_proofs_and_dlc_registrations(funded, is_atomic) + errors += db_errors + if is_atomic and len(errors) > 0: + return PostDlcRegistrationResponse(errors=errors) + + # ALL OK + return PostDlcRegistrationResponse( + funded=[f[1] for f in funded], + errors=errors if len(errors) > 0 else None, + ) \ No newline at end of file diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 3143690a..74cbadfb 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -834,6 +834,7 @@ async def m022_add_dlc_table(db: Database): dlc_root TEXT NOT NULL, settled BOOL NOT NULL DEFAULT FALSE, funding_amount {db.big_int} NOT NULL, + unit TEXT NOT NULL, debts TEXT, UNIQUE (dlc_root),