multiple keysets per mint

This commit is contained in:
callebtc
2022-10-08 19:25:03 +02:00
parent a54ca6e656
commit 244ea4a343
10 changed files with 328 additions and 114 deletions

View File

@@ -29,14 +29,8 @@ class KeyBase(BaseModel):
) )
from typing import Optional class WalletKeyset:
from cashu.core.db import Connection, Database
class Keyset:
id: str id: str
private_keys: Dict[int, PrivateKey]
public_keys: Dict[int, PublicKey] public_keys: Dict[int, PublicKey]
mint_url: Union[str, None] = None mint_url: Union[str, None] = None
valid_from: Union[str, None] = None valid_from: Union[str, None] = None
@@ -46,17 +40,24 @@ class Keyset:
def __init__( def __init__(
self, self,
seed: Union[None, str] = None, pubkeys: Dict[int, PublicKey] = None,
derivation_path: str = "0", mint_url=None,
pubkeys: Union[None, Dict[int, PublicKey]] = None, id=None,
valid_from=None,
valid_to=None,
first_seen=None,
active=None,
): ):
if seed: self.id = id
self.private_keys = derive_keys(seed, derivation_path) self.valid_from = valid_from
self.public_keys = derive_pubkeys(self.private_keys) self.valid_to = valid_to
self.first_seen = first_seen
self.active = active
self.mint_url = mint_url
if pubkeys: if pubkeys:
self.public_keys = pubkeys self.public_keys = pubkeys
self.id = derive_keyset_id(self.public_keys) self.id = derive_keyset_id(self.public_keys)
logger.debug(f"Mint keyset id: {self.id}") logger.debug(f"Wallet keyset id: {self.id}")
@classmethod @classmethod
def from_row(cls, row: Row): def from_row(cls, row: Row):
@@ -71,6 +72,56 @@ class Keyset:
active=row[5], 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): def get_keybase(self):
return { return {
k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex()) 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): class P2SHScript(BaseModel):
script: str script: str
signature: str signature: str

View File

@@ -1,74 +1,74 @@
from typing import Optional # from typing import Optional
from cashu.core.base import KeyBase, Keyset # from cashu.core.base import KeyBase, Keyset
from cashu.core.db import Connection, Database # from cashu.core.db import Connection, Database
async def store_keyset( # async def store_keyset(
keyset: Keyset, # keyset: Keyset,
mint_url: str = None, # mint_url: str = None,
db: Database = None, # db: Database = None,
conn: Optional[Connection] = None, # conn: Optional[Connection] = None,
): # ):
await (conn or db).execute( # await (conn or db).execute(
""" # """
INSERT INTO keysets # INSERT INTO keysets
(id, mint_url, valid_from, valid_to, first_seen, active) # (id, mint_url, valid_from, valid_to, first_seen, active)
VALUES (?, ?, ?, ?, ?, ?) # VALUES (?, ?, ?, ?, ?, ?)
""", # """,
( # (
keyset.id, # keyset.id,
mint_url or keyset.mint_url, # mint_url or keyset.mint_url,
keyset.valid_from, # keyset.valid_from,
keyset.valid_to, # keyset.valid_to,
keyset.first_seen, # keyset.first_seen,
True, # True,
), # ),
) # )
async def get_keyset( # async def get_keyset(
id: str = None, # id: str = None,
mint_url: str = None, # mint_url: str = None,
db: Database = None, # db: Database = None,
conn: Optional[Connection] = None, # conn: Optional[Connection] = None,
): # ):
clauses = [] # clauses = []
values = [] # values = []
clauses.append("active = ?") # clauses.append("active = ?")
values.append(True) # values.append(True)
if id: # if id:
clauses.append("id = ?") # clauses.append("id = ?")
values.append(id) # values.append(id)
if mint_url: # if mint_url:
clauses.append("mint_url = ?") # clauses.append("mint_url = ?")
values.append(mint_url) # values.append(mint_url)
where = "" # where = ""
if clauses: # if clauses:
where = f"WHERE {' AND '.join(clauses)}" # where = f"WHERE {' AND '.join(clauses)}"
row = await (conn or db).fetchone( # row = await (conn or db).fetchone(
f""" # f"""
SELECT * from keysets # SELECT * from keysets
{where} # {where}
""", # """,
tuple(values), # tuple(values),
) # )
return Keyset.from_row(row) if row is not None else None # return Keyset.from_row(row) if row is not None else None
async def store_mint_pubkey( # async def store_mint_pubkey(
key: KeyBase, # key: KeyBase,
db: Database, # db: Database,
conn: Optional[Connection] = None, # conn: Optional[Connection] = None,
): # ):
await (conn or db).execute( # await (conn or db).execute(
""" # """
INSERT INTO mint_pubkeys # INSERT INTO mint_pubkeys
(id, amount, pubkey) # (id, amount, pubkey)
VALUES (?, ?, ?) # VALUES (?, ?, ?)
""", # """,
(key.id, key.amount, key.pubkey), # (key.id, key.amount, key.pubkey),
) # )

View File

@@ -1,6 +1,6 @@
from typing import Optional 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 from cashu.core.db import Connection, Database
@@ -110,3 +110,57 @@ async def update_lightning_invoice(
hash, 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]

View File

@@ -3,12 +3,18 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c
""" """
import math 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.b_dhke as b_dhke
import cashu.core.bolt11 as bolt11 import cashu.core.bolt11 as bolt11
from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Keyset, Proof from cashu.core.base import (
from cashu.core.crud import get_keyset, store_keyset BlindedMessage,
BlindedSignature,
Invoice,
MintKeyset,
MintKeysets,
Proof,
)
from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys
from cashu.core.db import Database from cashu.core.db import Database
from cashu.core.helpers import fee_reserve from cashu.core.helpers import fee_reserve
@@ -24,6 +30,8 @@ from cashu.mint.crud import (
store_lightning_invoice, store_lightning_invoice,
store_promise, store_promise,
update_lightning_invoice, update_lightning_invoice,
get_keyset,
store_keyset,
) )
@@ -31,15 +39,26 @@ class Ledger:
def __init__(self, secret_key: str, db: str): def __init__(self, secret_key: str, db: str):
self.proofs_used: Set[str] = set() self.proofs_used: Set[str] = set()
self.master_key = secret_key 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) self.db: Database = Database("mint", db)
async def load_used_proofs(self): async def load_used_proofs(self):
self.proofs_used = set(await get_proofs_used(db=self.db)) self.proofs_used = set(await get_proofs_used(db=self.db))
async def store_keyset(self): async def init_keysets(self):
keyset_local: Keyset = await get_keyset(self.keyset.id, db=self.db) """Loads all past keysets and stores the active one if not already in db"""
if keyset_local is None: # 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) await store_keyset(keyset=self.keyset, db=self.db)
async def _generate_promises(self, amounts: List[int], B_s: List[str]): 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.""" """Verifies that the proof of promise was issued by this ledger."""
if not self._check_spendable(proof): if not self._check_spendable(proof):
raise Exception(f"tokens already spent. Secret: {proof.secret}") raise Exception(f"tokens already spent. Secret: {proof.secret}")
secret_key = self.keyset.private_keys[ # if no keyset id is given in proof, assume the current one
proof.amount if not proof.id:
] # Get the correct key to check against 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) C = PublicKey(bytes.fromhex(proof.C), raw=True)
return b_dhke.verify(secret_key, C, proof.secret) return b_dhke.verify(secret_key, C, proof.secret)

View File

@@ -95,13 +95,13 @@ async def m003_mint_keysets(db: Database):
f""" f"""
CREATE TABLE IF NOT EXISTS keysets ( CREATE TABLE IF NOT EXISTS keysets (
id TEXT NOT NULL, id TEXT NOT NULL,
mint_url TEXT, derivation_path TEXT,
valid_from TIMESTAMP DEFAULT {db.timestamp_now}, valid_from TIMESTAMP DEFAULT {db.timestamp_now},
valid_to TIMESTAMP DEFAULT {db.timestamp_now}, valid_to TIMESTAMP DEFAULT {db.timestamp_now},
first_seen TIMESTAMP DEFAULT {db.timestamp_now}, first_seen TIMESTAMP DEFAULT {db.timestamp_now},
active BOOL DEFAULT TRUE, active BOOL DEFAULT TRUE,
UNIQUE (id, mint_url) UNIQUE (derivation_path)
); );
""" """

View File

@@ -26,6 +26,12 @@ def keys():
return ledger.get_keyset() 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") @router.get("/mint")
async def request_mint(amount: int = 0): async def request_mint(amount: int = 0):
""" """

View File

@@ -14,7 +14,7 @@ async def load_ledger():
await migrate_databases(ledger.db, migrations) await migrate_databases(ledger.db, migrations)
# await asyncio.wait([m001_initial(ledger.db)]) # await asyncio.wait([m001_initial(ledger.db)])
await ledger.load_used_proofs() await ledger.load_used_proofs()
await ledger.store_keyset() await ledger.init_keysets()
if LIGHTNING: if LIGHTNING:
error_message, balance = await WALLET.status() error_message, balance = await WALLET.status()

View File

@@ -4,6 +4,9 @@ from typing import Any, List, Optional
from cashu.core.base import P2SHScript, Proof from cashu.core.base import P2SHScript, Proof
from cashu.core.db import Connection, Database 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( async def store_proof(
proof: Proof, proof: Proof,
@@ -180,3 +183,57 @@ async def update_p2sh_used(
f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?", f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?",
(*values, str(p2sh.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

View File

@@ -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. 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 ( CREATE TABLE IF NOT EXISTS keysets (
id TEXT NOT NULL, id TEXT NOT NULL,
mint_url TEXT NOT NULL, mint_url TEXT NOT NULL,
valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, valid_from TIMESTAMP DEFAULT {db.timestamp_now},
valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, valid_to TIMESTAMP DEFAULT {db.timestamp_now},
first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, first_seen TIMESTAMP DEFAULT {db.timestamp_now},
active BOOL NOT NULL DEFAULT TRUE, active BOOL DEFAULT TRUE,
UNIQUE (id, mint_url) UNIQUE (id, mint_url)
); );
""" """
) )
await db.execute( # await db.execute(
f""" # f"""
CREATE TABLE IF NOT EXISTS mint_pubkeys ( # CREATE TABLE IF NOT EXISTS mint_pubkeys (
id TEXT NOT NULL, # id TEXT NOT NULL,
amount INTEGER NOT NULL, # amount INTEGER NOT NULL,
pubkey TEXT 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 ADD COLUMN id TEXT")
await db.execute("ALTER TABLE proofs_used ADD COLUMN id TEXT") await db.execute("ALTER TABLE proofs_used ADD COLUMN id TEXT")

View File

@@ -14,14 +14,13 @@ from cashu.core.base import (
BlindedSignature, BlindedSignature,
CheckFeesRequest, CheckFeesRequest,
CheckRequest, CheckRequest,
Keyset, WalletKeyset,
MeltRequest, MeltRequest,
MintRequest, MintRequest,
P2SHScript, P2SHScript,
Proof, Proof,
SplitRequest, SplitRequest,
) )
from cashu.core.crud import get_keyset, store_keyset
from cashu.core.db import Database from cashu.core.db import Database
from cashu.core.script import ( from cashu.core.script import (
step0_carol_checksig_redeemscrip, step0_carol_checksig_redeemscrip,
@@ -39,6 +38,8 @@ from cashu.wallet.crud import (
store_p2sh, store_p2sh,
store_proof, store_proof,
update_proof_reserved, update_proof_reserved,
get_keyset,
store_keyset,
) )
@@ -57,9 +58,14 @@ class LedgerAPI:
int(amt): PublicKey(bytes.fromhex(val), raw=True) int(amt): PublicKey(bytes.fromhex(val), raw=True)
for amt, val in keys.items() for amt, val in keys.items()
} }
keyset = Keyset(pubkeys=keyset_keys) keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
return keyset 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 @staticmethod
def _get_output_split(amount): def _get_output_split(amount):
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
@@ -97,7 +103,8 @@ class LedgerAPI:
self.url self.url
), "Ledger not initialized correctly: mint URL not specified yet. " ), "Ledger not initialized correctly: mint URL not specified yet. "
keyset = await self._get_keys(self.url) 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: if keyset_local is None:
await store_keyset(keyset=keyset, db=self.db) await store_keyset(keyset=keyset, db=self.db)
# keyset_local: Keyset = await get_keyset(keyset.id, self.url, 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) # await store_keyset(keyset=keyset, db=self.db)
self.keys = keyset.public_keys self.keys = keyset.public_keys
self.keyset_id = keyset.id self.keyset_id = keyset.id
self.keysets = keysets["keysets"]
assert len(self.keys) > 0, "did not receive keys from mint." assert len(self.keys) > 0, "did not receive keys from mint."
def request_mint(self, amount): def request_mint(self, amount):
@@ -370,8 +378,14 @@ class Wallet(LedgerAPI):
return token return token
async def _get_spendable_proofs(self, proofs: List[Proof]): 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 = [ 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 ] # "or not p.id" is for backwards compatibility with proofs without a keyset id
proofs = [p for p in proofs if not p.reserved] proofs = [p for p in proofs if not p.reserved]
return proofs return proofs