Mint: add websockets for quote updates (#413)

* add websockets for quote updates

* add test (not working)

* wip: emit events to everyone

* wip: emit events to everyone

* wip, lots of things broken but invoice callback works

* wip

* add wip files

* tests almost passing

* add task

* refactor nut constants

* startup fix

* works with old mints

* wip cli

* fix mypy

* remove automatic invoice test now with websockets

* remove comment

* better logging

* send back response

* add rate limiter to websocket

* add rate limiter to subscriptions

* refactor websocket ratelimit

* websocket tests

* subscription kinds

* doesnt start

* remove circular import

* update

* fix mypy

* move test file in test because it fails if it runs later... dunno why

* adjust websocket NUT-06 settings

* local import and small fix

* disable websockets in CLI if "no_check" is selected

* move subscription test to where it was

* check proof state with callback, add tests

* tests: run mint fixture per module instead of per session

* subscription command name fix

* test per session again

* update test race conditions

* fix tests

* clean up

* tmp

* fix db issues and remove cached secrets

* fix tests

* blindly try pipeline

* remove comments

* comments
This commit is contained in:
callebtc
2024-06-25 19:20:03 +02:00
committed by GitHub
parent 75987beaf1
commit e846acf946
43 changed files with 1701 additions and 519 deletions

View File

@@ -52,16 +52,20 @@ from ..lightning.base import (
)
from ..mint.crud import LedgerCrudSqlite
from .conditions import LedgerSpendingConditions
from .db.read import DbReadHelper
from .db.write import DbWriteHelper
from .events.events import LedgerEventManager
from .features import LedgerFeatures
from .tasks import LedgerTasks
from .verification import LedgerVerification
class Ledger(LedgerVerification, LedgerSpendingConditions):
class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFeatures):
backends: Mapping[Method, Mapping[Unit, LightningBackend]] = {}
locks: Dict[str, asyncio.Lock] = {} # holds multiprocessing locks
proofs_pending_lock: asyncio.Lock = (
asyncio.Lock()
) # holds locks for proofs_pending database
keysets: Dict[str, MintKeyset] = {}
events = LedgerEventManager()
db_read: DbReadHelper
def __init__(
self,
@@ -92,17 +96,17 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
self.crud = crud
self.backends = backends
self.pubkey = derive_pubkey(self.seed)
self.spent_proofs: Dict[str, Proof] = {}
self.db_read = DbReadHelper(self.db, self.crud)
self.db_write = DbWriteHelper(self.db, self.crud, self.events)
# ------- STARTUP -------
async def startup_ledger(self):
await self._startup_ledger()
await self._check_pending_proofs_and_melt_quotes()
await self.dispatch_listeners()
async def _startup_ledger(self):
if settings.mint_cache_secrets:
await self.load_used_proofs()
await self.init_keysets()
for derivation_path in settings.mint_derivation_path_list:
@@ -158,12 +162,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
proofs=pending_proofs, quote_id=quote.quote
)
# unset pending
await self._unset_proofs_pending(pending_proofs)
await self.db_write._unset_proofs_pending(pending_proofs)
elif payment.failed:
logger.info(f"Melt quote {quote.quote} state: failed")
# unset pending
await self._unset_proofs_pending(pending_proofs)
await self.db_write._unset_proofs_pending(pending_proofs, spent=False)
elif payment.pending:
logger.info(f"Melt quote {quote.quote} state: pending")
pass
@@ -291,13 +295,15 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
proofs (List[Proof]): Proofs to add to known secret table.
conn: (Optional[Connection], optional): Database connection to reuse. Will create a new one if not given. Defaults to None.
"""
self.spent_proofs.update({p.Y: p for p in proofs})
async with get_db_connection(self.db, conn) as conn:
# store in db
for p in proofs:
await self.crud.invalidate_proof(
proof=p, db=self.db, quote_id=quote_id, conn=conn
)
await self.events.submit(
ProofState(Y=p.Y, state=SpentState.spent, witness=p.witness or None)
)
async def _generate_change_promises(
self,
@@ -426,10 +432,9 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
created_time=int(time.time()),
expiry=expiry,
)
await self.crud.store_mint_quote(
quote=quote,
db=self.db,
)
await self.crud.store_mint_quote(quote=quote, db=self.db)
await self.events.submit(quote)
return quote
async def get_mint_quote(self, quote_id: str) -> MintQuote:
@@ -462,6 +467,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
quote.paid = True
quote.paid_time = int(time.time())
await self.crud.update_mint_quote(quote=quote, db=self.db)
await self.events.submit(quote)
return quote
@@ -510,12 +516,16 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
if quote.expiry and quote.expiry > int(time.time()):
raise TransactionError("quote expired")
promises = await self._generate_promises(outputs)
logger.trace("generated promises")
logger.trace(f"crud: setting quote {quote_id} as issued")
quote.issued = True
await self.crud.update_mint_quote(quote=quote, db=self.db)
promises = await self._generate_promises(outputs)
logger.trace("generated promises")
# submit the quote update to the event manager
await self.events.submit(quote)
del self.locks[quote_id]
return promises
@@ -605,9 +615,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# check if there is a mint quote with the same payment request
# so that we would be able to handle the transaction internally
# and therefore respond with internal transaction fees (0 for now)
mint_quote = await self.crud.get_mint_quote_by_request(
request=request, db=self.db
)
mint_quote = await self.crud.get_mint_quote(request=request, db=self.db)
if mint_quote:
payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote)
@@ -655,6 +663,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
expiry=expiry,
)
await self.crud.store_melt_quote(quote=quote, db=self.db)
await self.events.submit(quote)
return PostMeltQuoteResponse(
quote=quote.quote,
amount=quote.amount,
@@ -689,7 +699,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# we only check the state with the backend if there is no associated internal
# mint quote for this melt quote
mint_quote = await self.crud.get_mint_quote_by_request(
mint_quote = await self.crud.get_mint_quote(
request=melt_quote.request, db=self.db
)
@@ -710,6 +720,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
melt_quote.proof = status.preimage
melt_quote.paid_time = int(time.time())
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
await self.events.submit(melt_quote)
return melt_quote
@@ -733,7 +744,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
"""
# first we check if there is a mint quote with the same payment request
# so that we can handle the transaction internally without the backend
mint_quote = await self.crud.get_mint_quote_by_request(
mint_quote = await self.crud.get_mint_quote(
request=melt_quote.request, db=self.db
)
if not mint_quote:
@@ -774,10 +785,13 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
mint_quote.paid = True
mint_quote.paid_time = melt_quote.paid_time
async with self.db.connect() as conn:
async with get_db_connection(self.db) as conn:
await self.crud.update_melt_quote(quote=melt_quote, db=self.db, conn=conn)
await self.crud.update_mint_quote(quote=mint_quote, db=self.db, conn=conn)
await self.events.submit(melt_quote)
await self.events.submit(mint_quote)
return melt_quote
async def melt(
@@ -847,7 +861,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
await self.verify_inputs_and_outputs(proofs=proofs)
# set proofs to pending to avoid race conditions
await self._set_proofs_pending(proofs, quote_id=melt_quote.quote)
await self.db_write._set_proofs_pending(proofs, quote_id=melt_quote.quote)
try:
# settle the transaction internally if there is a mint quote with the same payment request
melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs)
@@ -875,6 +889,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
melt_quote.paid = True
melt_quote.paid_time = int(time.time())
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
await self.events.submit(melt_quote)
# melt successful, invalidate proofs
await self._invalidate_proofs(proofs=proofs, quote_id=melt_quote.quote)
@@ -894,7 +909,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
raise e
finally:
# delete proofs from pending list
await self._unset_proofs_pending(proofs)
await self.db_write._unset_proofs_pending(proofs)
return melt_quote.proof or "", return_promises
@@ -928,7 +943,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# verify spending inputs, outputs, and spending conditions
await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs)
await self._set_proofs_pending(proofs)
await self.db_write._set_proofs_pending(proofs)
try:
# Mark proofs as used and prepare new promises
async with get_db_connection(self.db) as conn:
@@ -941,7 +956,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
raise e
finally:
# delete proofs from pending list
await self._unset_proofs_pending(proofs)
await self.db_write._unset_proofs_pending(proofs)
logger.trace("split successful")
return promises
@@ -951,7 +966,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
signatures: List[BlindedSignature] = []
return_outputs: List[BlindedMessage] = []
async with self.db.connect() as conn:
async with get_db_connection(self.db) as conn:
for output in outputs:
logger.trace(f"looking for promise: {output}")
promise = await self.crud.get_promise(
@@ -1030,105 +1045,3 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
)
signatures.append(signature)
return signatures
# ------- PROOFS -------
async def load_used_proofs(self) -> None:
"""Load all used proofs from database."""
if not settings.mint_cache_secrets:
raise Exception("MINT_CACHE_SECRETS must be set to TRUE")
logger.debug("Loading used proofs into memory")
spent_proofs_list = await self.crud.get_spent_proofs(db=self.db) or []
logger.debug(f"Loaded {len(spent_proofs_list)} used proofs")
self.spent_proofs = {p.Y: p for p in spent_proofs_list}
async def check_proofs_state(self, Ys: List[str]) -> List[ProofState]:
"""Checks if provided proofs are spend or are pending.
Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction.
Returns two lists that are in the same order as the provided proofs. Wallet must match the list
to the proofs they have provided in order to figure out which proof is spendable or pending
and which isn't.
Args:
Ys (List[str]): List of Y's of proofs to check
Returns:
List[bool]: List of which proof is still spendable (True if still spendable, else False)
List[bool]: List of which proof are pending (True if pending, else False)
"""
states: List[ProofState] = []
proofs_spent = await self._get_proofs_spent(Ys)
proofs_pending = await self._get_proofs_pending(Ys)
for Y in Ys:
if Y not in proofs_spent and Y not in proofs_pending:
states.append(ProofState(Y=Y, state=SpentState.unspent))
elif Y not in proofs_spent and Y in proofs_pending:
states.append(ProofState(Y=Y, state=SpentState.pending))
else:
states.append(
ProofState(
Y=Y,
state=SpentState.spent,
witness=proofs_spent[Y].witness,
)
)
return states
async def _set_proofs_pending(
self, proofs: List[Proof], quote_id: Optional[str] = None
) -> None:
"""If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
the list of pending proofs or removes them. Used as a mutex for proofs.
Args:
proofs (List[Proof]): Proofs to add to pending table.
quote_id (Optional[str]): Melt quote ID. If it is not set, we assume the pending tokens to be from a swap.
Raises:
Exception: At least one proof already in pending table.
"""
# first we check whether these proofs are pending already
async with self.proofs_pending_lock:
async with self.db.connect() as conn:
await self._validate_proofs_pending(proofs, conn)
try:
for p in proofs:
await self.crud.set_proof_pending(
proof=p, db=self.db, quote_id=quote_id, conn=conn
)
except Exception as e:
logger.error(f"Failed to set proofs pending: {e}")
raise TransactionError("Failed to set proofs pending.")
async def _unset_proofs_pending(self, proofs: List[Proof]) -> None:
"""Deletes proofs from pending table.
Args:
proofs (List[Proof]): Proofs to delete.
"""
async with self.proofs_pending_lock:
async with self.db.connect() as conn:
for p in proofs:
await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn)
async def _validate_proofs_pending(
self, proofs: List[Proof], conn: Optional[Connection] = None
) -> None:
"""Checks if any of the provided proofs is in the pending proofs table.
Args:
proofs (List[Proof]): Proofs to check.
Raises:
Exception: At least one of the proofs is in the pending table.
"""
if not (
len(
await self.crud.get_proofs_pending(
Ys=[p.Y for p in proofs], db=self.db, conn=conn
)
)
== 0
):
raise TransactionError("proofs are pending.")