diff --git a/cashu/core/base.py b/cashu/core/base.py index 9a7b387..4f68c82 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -29,14 +29,8 @@ class KeyBase(BaseModel): ) -from typing import Optional - -from cashu.core.db import Connection, Database - - -class Keyset: +class WalletKeyset: id: str - private_keys: Dict[int, PrivateKey] public_keys: Dict[int, PublicKey] mint_url: Union[str, None] = None valid_from: Union[str, None] = None @@ -46,17 +40,24 @@ class Keyset: def __init__( self, - seed: Union[None, str] = None, - derivation_path: str = "0", - pubkeys: Union[None, Dict[int, PublicKey]] = None, + pubkeys: Dict[int, PublicKey] = None, + mint_url=None, + id=None, + valid_from=None, + valid_to=None, + first_seen=None, + active=None, ): - if seed: - self.private_keys = derive_keys(seed, derivation_path) - self.public_keys = derive_pubkeys(self.private_keys) + self.id = id + self.valid_from = valid_from + self.valid_to = valid_to + self.first_seen = first_seen + self.active = active + self.mint_url = mint_url if pubkeys: self.public_keys = pubkeys - self.id = derive_keyset_id(self.public_keys) - logger.debug(f"Mint keyset id: {self.id}") + self.id = derive_keyset_id(self.public_keys) + logger.debug(f"Wallet keyset id: {self.id}") @classmethod def from_row(cls, row: Row): @@ -71,6 +72,56 @@ class Keyset: active=row[5], ) + +class MintKeyset: + id: str + derivation_path: str + private_keys: Dict[int, PrivateKey] + public_keys: Dict[int, PublicKey] = None + valid_from: Union[str, None] = None + valid_to: Union[str, None] = None + first_seen: Union[str, None] = None + active: bool = True + + def __init__( + self, + id=None, + valid_from=None, + valid_to=None, + first_seen=None, + active=None, + seed: Union[None, str] = None, + derivation_path: str = "0", + ): + self.derivation_path = derivation_path + self.id = id + self.valid_from = valid_from + self.valid_to = valid_to + self.first_seen = first_seen + self.active = active + # generate keys from seed + if seed: + self.generate_keys(seed) + + def generate_keys(self, seed): + self.private_keys = derive_keys(seed, self.derivation_path) + self.public_keys = derive_pubkeys(self.private_keys) + self.id = derive_keyset_id(self.public_keys) + logger.debug(f"Mint keyset id: {self.id}") + + @classmethod + def from_row(cls, row: Row): + if row is None: + return cls + return cls( + id=row[0], + derivation_path=row[1], + valid_from=row[2], + valid_to=row[3], + first_seen=row[4], + active=row[5], + ) + def get_keybase(self): return { k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex()) @@ -78,6 +129,16 @@ class Keyset: } +class MintKeysets: + keysets: Dict[str, MintKeyset] + + def __init__(self, keysets: List[MintKeyset]): + self.keysets: Dict[str, MintKeyset] = {k.id: k for k in keysets} + + def get_ids(self): + return [k for k, _ in self.keysets.items()] + + class P2SHScript(BaseModel): script: str signature: str diff --git a/cashu/core/crud.py b/cashu/core/crud.py index 235094c..c1b01ab 100644 --- a/cashu/core/crud.py +++ b/cashu/core/crud.py @@ -1,74 +1,74 @@ -from typing import Optional +# from typing import Optional -from cashu.core.base import KeyBase, Keyset -from cashu.core.db import Connection, Database +# from cashu.core.base import KeyBase, Keyset +# from cashu.core.db import Connection, Database -async def store_keyset( - keyset: Keyset, - mint_url: str = None, - db: Database = None, - conn: Optional[Connection] = None, -): +# async def store_keyset( +# keyset: Keyset, +# mint_url: str = None, +# db: Database = None, +# conn: Optional[Connection] = None, +# ): - await (conn or db).execute( - """ - INSERT INTO keysets - (id, mint_url, valid_from, valid_to, first_seen, active) - VALUES (?, ?, ?, ?, ?, ?) - """, - ( - keyset.id, - mint_url or keyset.mint_url, - keyset.valid_from, - keyset.valid_to, - keyset.first_seen, - True, - ), - ) +# await (conn or db).execute( +# """ +# INSERT INTO keysets +# (id, mint_url, valid_from, valid_to, first_seen, active) +# VALUES (?, ?, ?, ?, ?, ?) +# """, +# ( +# keyset.id, +# mint_url or keyset.mint_url, +# keyset.valid_from, +# keyset.valid_to, +# keyset.first_seen, +# True, +# ), +# ) -async def get_keyset( - id: str = None, - mint_url: str = None, - db: Database = None, - conn: Optional[Connection] = None, -): - clauses = [] - values = [] - clauses.append("active = ?") - values.append(True) - if id: - clauses.append("id = ?") - values.append(id) - if mint_url: - clauses.append("mint_url = ?") - values.append(mint_url) - where = "" - if clauses: - where = f"WHERE {' AND '.join(clauses)}" +# async def get_keyset( +# id: str = None, +# mint_url: str = None, +# db: Database = None, +# conn: Optional[Connection] = None, +# ): +# clauses = [] +# values = [] +# clauses.append("active = ?") +# values.append(True) +# if id: +# clauses.append("id = ?") +# values.append(id) +# if mint_url: +# clauses.append("mint_url = ?") +# values.append(mint_url) +# where = "" +# if clauses: +# where = f"WHERE {' AND '.join(clauses)}" - row = await (conn or db).fetchone( - f""" - SELECT * from keysets - {where} - """, - tuple(values), - ) - return Keyset.from_row(row) if row is not None else None +# row = await (conn or db).fetchone( +# f""" +# SELECT * from keysets +# {where} +# """, +# tuple(values), +# ) +# return Keyset.from_row(row) if row is not None else None -async def store_mint_pubkey( - key: KeyBase, - db: Database, - conn: Optional[Connection] = None, -): +# async def store_mint_pubkey( +# key: KeyBase, +# db: Database, +# conn: Optional[Connection] = None, +# ): - await (conn or db).execute( - """ - INSERT INTO mint_pubkeys - (id, amount, pubkey) - VALUES (?, ?, ?) - """, - (key.id, key.amount, key.pubkey), - ) +# await (conn or db).execute( +# """ +# INSERT INTO mint_pubkeys +# (id, amount, pubkey) +# VALUES (?, ?, ?) +# """, +# (key.id, key.amount, key.pubkey), +# ) diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 0afc285..83b7e9d 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -1,6 +1,6 @@ from typing import Optional -from cashu.core.base import Invoice, Proof +from cashu.core.base import Invoice, Proof, MintKeyset from cashu.core.db import Connection, Database @@ -110,3 +110,57 @@ async def update_lightning_invoice( hash, ), ) + + +async def store_keyset( + keyset: MintKeyset, + mint_url: str = None, + db: Database = None, + conn: Optional[Connection] = None, +): + + await (conn or db).execute( + """ + INSERT INTO keysets + (id, derivation_path, valid_from, valid_to, first_seen, active) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + keyset.id, + keyset.derivation_path, + keyset.valid_from, + keyset.valid_to, + keyset.first_seen, + True, + ), + ) + + +async def get_keyset( + id: str = None, + derivation_path: str = None, + db: Database = None, + conn: Optional[Connection] = None, +): + clauses = [] + values = [] + clauses.append("active = ?") + values.append(True) + if id: + clauses.append("id = ?") + values.append(id) + if derivation_path: + clauses.append("derivation_path = ?") + values.append(derivation_path) + where = "" + if clauses: + where = f"WHERE {' AND '.join(clauses)}" + + rows = await (conn or db).fetchall( + f""" + SELECT * from keysets + {where} + """, + tuple(values), + ) + return [MintKeyset.from_row(row) for row in rows] diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 4d321f8..6d96fb0 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -3,12 +3,18 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c """ import math -from typing import List, Set - +from typing import List, Set, Dict +from loguru import logger import cashu.core.b_dhke as b_dhke import cashu.core.bolt11 as bolt11 -from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Keyset, Proof -from cashu.core.crud import get_keyset, store_keyset +from cashu.core.base import ( + BlindedMessage, + BlindedSignature, + Invoice, + MintKeyset, + MintKeysets, + Proof, +) from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys from cashu.core.db import Database from cashu.core.helpers import fee_reserve @@ -24,6 +30,8 @@ from cashu.mint.crud import ( store_lightning_invoice, store_promise, update_lightning_invoice, + get_keyset, + store_keyset, ) @@ -31,15 +39,26 @@ class Ledger: def __init__(self, secret_key: str, db: str): self.proofs_used: Set[str] = set() self.master_key = secret_key - self.keyset = Keyset(self.master_key) + self.keyset = MintKeyset(seed=self.master_key, derivation_path="1") self.db: Database = Database("mint", db) async def load_used_proofs(self): self.proofs_used = set(await get_proofs_used(db=self.db)) - async def store_keyset(self): - keyset_local: Keyset = await get_keyset(self.keyset.id, db=self.db) - if keyset_local is None: + async def init_keysets(self): + """Loads all past keysets and stores the active one if not already in db""" + # get all past keysets + tmp_keysets: List[MintKeyset] = await get_keyset(db=self.db) + self.keysets = MintKeysets(tmp_keysets) + for _, v in self.keysets.keysets.items(): + v.generate_keys(self.master_key) + if len(self.keysets.keysets): + logger.debug(f"Loaded {len(self.keysets.keysets)} keysets from db.") + current_keyset_local: List[MintKeyset] = await get_keyset( + id=self.keyset.id, db=self.db + ) + if not len(current_keyset_local): + logger.debug(f"Storing keyset {self.keyset.id}") await store_keyset(keyset=self.keyset, db=self.db) async def _generate_promises(self, amounts: List[int], B_s: List[str]): @@ -76,9 +95,12 @@ class Ledger: """Verifies that the proof of promise was issued by this ledger.""" if not self._check_spendable(proof): raise Exception(f"tokens already spent. Secret: {proof.secret}") - secret_key = self.keyset.private_keys[ - proof.amount - ] # Get the correct key to check against + # if no keyset id is given in proof, assume the current one + if not proof.id: + secret_key = self.keyset.private_keys[proof.amount] + else: + secret_key = self.keysets.keysets[proof.id].private_keys[proof.amount] + C = PublicKey(bytes.fromhex(proof.C), raw=True) return b_dhke.verify(secret_key, C, proof.secret) diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index c3b2559..15c8233 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -95,13 +95,13 @@ async def m003_mint_keysets(db: Database): f""" CREATE TABLE IF NOT EXISTS keysets ( id TEXT NOT NULL, - mint_url TEXT, + derivation_path TEXT, valid_from TIMESTAMP DEFAULT {db.timestamp_now}, valid_to TIMESTAMP DEFAULT {db.timestamp_now}, first_seen TIMESTAMP DEFAULT {db.timestamp_now}, active BOOL DEFAULT TRUE, - UNIQUE (id, mint_url) + UNIQUE (derivation_path) ); """ diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 1ea848a..a502398 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -26,6 +26,12 @@ def keys(): return ledger.get_keyset() +@router.get("/keysets") +def keysets(): + """Get all active keysets of the mint""" + return {"keysets": ledger.keysets.get_ids()} + + @router.get("/mint") async def request_mint(amount: int = 0): """ diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index 1f3dd66..f148e2c 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -14,7 +14,7 @@ async def load_ledger(): await migrate_databases(ledger.db, migrations) # await asyncio.wait([m001_initial(ledger.db)]) await ledger.load_used_proofs() - await ledger.store_keyset() + await ledger.init_keysets() if LIGHTNING: error_message, balance = await WALLET.status() diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index 4486023..50e3676 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -4,6 +4,9 @@ from typing import Any, List, Optional from cashu.core.base import P2SHScript, Proof from cashu.core.db import Connection, Database +from cashu.core.base import KeyBase, WalletKeyset +from cashu.core.db import Connection, Database + async def store_proof( proof: Proof, @@ -180,3 +183,57 @@ async def update_p2sh_used( f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?", (*values, str(p2sh.address)), ) + + +async def store_keyset( + keyset: WalletKeyset, + mint_url: str = None, + db: Database = None, + conn: Optional[Connection] = None, +): + + await (conn or db).execute( + """ + INSERT INTO keysets + (id, mint_url, valid_from, valid_to, first_seen, active) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + keyset.id, + mint_url or keyset.mint_url, + keyset.valid_from, + keyset.valid_to, + keyset.first_seen, + True, + ), + ) + + +async def get_keyset( + id: str = None, + mint_url: str = None, + db: Database = None, + conn: Optional[Connection] = None, +): + clauses = [] + values = [] + clauses.append("active = ?") + values.append(True) + if id: + clauses.append("id = ?") + values.append(id) + if mint_url: + clauses.append("mint_url = ?") + values.append(mint_url) + where = "" + if clauses: + where = f"WHERE {' AND '.join(clauses)}" + + row = await (conn or db).fetchone( + f""" + SELECT * from keysets + {where} + """, + tuple(values), + ) + return WalletKeyset.from_row(row) if row is not None else None diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 6cddadf..886f8ca 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -100,7 +100,7 @@ async def m004_p2sh_locks(db: Database): ) -async def m005_mint_keysets(db: Database): +async def m005_wallet_keysets(db: Database): """ Stores mint keysets from different mints and epochs. """ @@ -109,28 +109,28 @@ async def m005_mint_keysets(db: Database): CREATE TABLE IF NOT EXISTS keysets ( id TEXT NOT NULL, mint_url TEXT NOT NULL, - valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, - valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, - first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, - active BOOL NOT NULL DEFAULT TRUE, + valid_from TIMESTAMP DEFAULT {db.timestamp_now}, + valid_to TIMESTAMP DEFAULT {db.timestamp_now}, + first_seen TIMESTAMP DEFAULT {db.timestamp_now}, + active BOOL DEFAULT TRUE, UNIQUE (id, mint_url) ); """ ) - await db.execute( - f""" - CREATE TABLE IF NOT EXISTS mint_pubkeys ( - id TEXT NOT NULL, - amount INTEGER NOT NULL, - pubkey TEXT NOT NULL, + # await db.execute( + # f""" + # CREATE TABLE IF NOT EXISTS mint_pubkeys ( + # id TEXT NOT NULL, + # amount INTEGER NOT NULL, + # pubkey TEXT NOT NULL, - UNIQUE (id, pubkey) + # UNIQUE (id, pubkey) - ); - """ - ) + # ); + # """ + # ) await db.execute("ALTER TABLE proofs ADD COLUMN id TEXT") await db.execute("ALTER TABLE proofs_used ADD COLUMN id TEXT") diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 30fc154..a75ce22 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -14,14 +14,13 @@ from cashu.core.base import ( BlindedSignature, CheckFeesRequest, CheckRequest, - Keyset, + WalletKeyset, MeltRequest, MintRequest, P2SHScript, Proof, SplitRequest, ) -from cashu.core.crud import get_keyset, store_keyset from cashu.core.db import Database from cashu.core.script import ( step0_carol_checksig_redeemscrip, @@ -39,6 +38,8 @@ from cashu.wallet.crud import ( store_p2sh, store_proof, update_proof_reserved, + get_keyset, + store_keyset, ) @@ -57,9 +58,14 @@ class LedgerAPI: int(amt): PublicKey(bytes.fromhex(val), raw=True) for amt, val in keys.items() } - keyset = Keyset(pubkeys=keyset_keys) + keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url) return keyset + async def _get_keysets(self, url): + keysets = requests.get(url + "/keysets").json() + assert len(keysets), Exception("did not receive any keysets") + return keysets + @staticmethod def _get_output_split(amount): """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" @@ -97,7 +103,8 @@ class LedgerAPI: self.url ), "Ledger not initialized correctly: mint URL not specified yet. " keyset = await self._get_keys(self.url) - keyset_local: Keyset = await get_keyset(keyset.id, db=self.db) + keysets = await self._get_keysets(self.url) + keyset_local: WalletKeyset = await get_keyset(keyset.id, db=self.db) if keyset_local is None: await store_keyset(keyset=keyset, db=self.db) # keyset_local: Keyset = await get_keyset(keyset.id, self.url, db=self.db) @@ -105,6 +112,7 @@ class LedgerAPI: # await store_keyset(keyset=keyset, db=self.db) self.keys = keyset.public_keys self.keyset_id = keyset.id + self.keysets = keysets["keysets"] assert len(self.keys) > 0, "did not receive keys from mint." def request_mint(self, amount): @@ -370,8 +378,14 @@ class Wallet(LedgerAPI): return token async def _get_spendable_proofs(self, proofs: List[Proof]): + """ + Selects proofs that can be used with the current mint. + Chooses: + 1) Proofs that are not marked as reserved + 2) Proofs that have a keyset id that is in self.keysets (active keysets of mint) - !!! optional for backwards compatibility with legacy clients + """ proofs = [ - p for p in proofs if p.id == self.keyset_id or not p.id + p for p in proofs if p.id in self.keysets or not p.id ] # "or not p.id" is for backwards compatibility with proofs without a keyset id proofs = [p for p in proofs if not p.reserved] return proofs