From 1bdb4568c4dbdca78524639fa1bf1eb28959531f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 7 Oct 2022 16:30:50 +0200 Subject: [PATCH] started --- cashu/core/base.py | 27 +++++++++++++++++--- cashu/mint/ledger.py | 9 +++++-- cashu/mint/router.py | 2 +- cashu/wallet/cli.py | 12 ++++----- cashu/wallet/crud.py | 51 +++++++++++++++++++++++++++++++++++-- cashu/wallet/migrations.py | 20 +++++++++++++++ cashu/wallet/wallet.py | 52 +++++++++++++++++++++++++++++--------- tests/test_wallet.py | 4 +-- 8 files changed, 149 insertions(+), 28 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 0aa0244..428432b 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import List, Union +from typing import List, Union, Dict from pydantic import BaseModel @@ -9,6 +9,26 @@ class CashuError(BaseModel): error = "CashuError" +class Keyset(BaseModel): + id: str + keys: Dict + mint_url: Union[str, None] = None + first_seen: Union[str, None] = None + active: bool = True + + @classmethod + def from_row(cls, row: Row): + if row is None: + return cls + return cls( + id=row[0], + keys=row[1], + mint_url=row[2], + first_seen=row[3], + active=row[4], + ) + + class P2SHScript(BaseModel): script: str signature: str @@ -25,6 +45,7 @@ class P2SHScript(BaseModel): class Proof(BaseModel): + id: str = "" amount: int secret: str = "" C: str @@ -60,10 +81,10 @@ class Proof(BaseModel): ) def to_dict(self): - return dict(amount=self.amount, secret=self.secret, C=self.C) + return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C) def to_dict_no_secret(self): - return dict(amount=self.amount, C=self.C) + return dict(id=self.id, amount=self.amount, C=self.C) def __getitem__(self, key): return self.__getattribute__(key) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 501eaa4..d194863 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -11,6 +11,7 @@ from typing import List, Set import cashu.core.b_dhke as b_dhke import cashu.core.bolt11 as bolt11 from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Proof +from cashu.core.crypto import derive_keyset_id from cashu.core.db import Database from cashu.core.helpers import fee_reserve from cashu.core.script import verify_script @@ -34,6 +35,7 @@ class Ledger: self.master_key = secret_key self.keys = self._derive_keys(self.master_key) + self.keyset_id = derive_keyset_id(self.keys) self.pub_keys = self._derive_pubkeys(self.keys) self.db: Database = Database("mint", db) @@ -41,12 +43,12 @@ class Ledger: self.proofs_used = set(await get_proofs_used(db=self.db)) @staticmethod - def _derive_keys(master_key: str): + def _derive_keys(master_key: str, keyset_id: str = ""): """Deterministic derivation of keys for 2^n values.""" return { 2 ** i: PrivateKey( - hashlib.sha256((str(master_key) + str(i)).encode("utf-8")) + hashlib.sha256((str(master_key) + str(i) + keyset_id).encode("utf-8")) .hexdigest() .encode("utf-8")[:32], raw=True, @@ -221,6 +223,9 @@ class Ledger: """Returns public keys for possible amounts.""" return {a: p.serialize().hex() for a, p in self.pub_keys.items()} + def get_keyset(self): + return {"id": self.keyset_id, "keys": self.get_pubkeys()} + async def request_mint(self, amount): """Returns Lightning invoice and stores it in the db.""" payment_request, checking_id = await self._request_lightning_invoice(amount) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index fec009f..43b7c5b 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -23,7 +23,7 @@ router: APIRouter = APIRouter() @router.get("/keys") def keys(): """Get the public keys of the mint""" - return ledger.get_pubkeys() + return ledger.get_keyset() @router.get("/mint") diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 39086ac..b2fb2d4 100755 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -78,7 +78,7 @@ def coro(f): @coro async def mint(ctx, amount: int, hash: str): wallet: Wallet = ctx.obj["WALLET"] - wallet.load_mint() + await wallet.load_mint() wallet.status() if not LIGHTNING: r = await wallet.mint(amount) @@ -122,7 +122,7 @@ async def mint(ctx, amount: int, hash: str): @coro async def pay(ctx, invoice: str): wallet: Wallet = ctx.obj["WALLET"] - wallet.load_mint() + await wallet.load_mint() wallet.status() decoded_invoice: Invoice = bolt11.decode(invoice) # check if it's an internal payment @@ -163,7 +163,7 @@ async def send(ctx, amount: int, lock: str): if lock and len(lock.split("P2SH:")) == 2: p2sh = True wallet: Wallet = ctx.obj["WALLET"] - wallet.load_mint() + await wallet.load_mint() wallet.status() _, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock) await wallet.set_reserved(send_proofs, reserved=True) @@ -181,7 +181,7 @@ async def send(ctx, amount: int, lock: str): @coro async def receive(ctx, coin: str, lock: str): wallet: Wallet = ctx.obj["WALLET"] - wallet.load_mint() + await wallet.load_mint() wallet.status() if lock: # load the script and signature of this address from the database @@ -211,7 +211,7 @@ async def receive(ctx, coin: str, lock: str): @coro async def burn(ctx, coin: str, all: bool, force: bool): wallet: Wallet = ctx.obj["WALLET"] - wallet.load_mint() + await wallet.load_mint() if not (all or coin or force) or (coin and all): print( "Error: enter a coin or use --all to burn all pending coins or --force to check all coins." @@ -238,7 +238,7 @@ async def burn(ctx, coin: str, all: bool, force: bool): @coro async def pending(ctx): wallet: Wallet = ctx.obj["WALLET"] - wallet.load_mint() + await wallet.load_mint() reserved_proofs = await get_reserved_proofs(wallet.db) if len(reserved_proofs): print(f"--------------------------\n") diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index da29451..91c917d 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -1,7 +1,7 @@ import time -from typing import Any, List, Optional +from typing import Any, List, Optional, Dict -from cashu.core.base import P2SHScript, Proof +from cashu.core.base import P2SHScript, Proof, Keyset from cashu.core.db import Connection, Database @@ -180,3 +180,50 @@ async def update_p2sh_used( f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?", (*values, str(p2sh.address)), ) + + +async def store_keyset( + keyset: Keyset, + mint_url: str, + db: Database, + conn: Optional[Connection] = None, +): + + await (conn or db).execute( + """ + INSERT INTO keysets + (id, mint_url, keys, first_seen, active) + VALUES (?, ?, ?, ?, ?) + """, + (keyset.id, mint_url, keyset.keys, 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 Keyset.from_row(row) diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 68b4b64..34f19f2 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -98,3 +98,23 @@ async def m004_p2sh_locks(db: Database): ); """ ) + + +async def m005_mint_keysets(db: Database): + """ + Stores mint keysets from different mints and epochs. + """ + await db.execute( + f""" + CREATE TABLE IF NOT EXISTS keysets ( + id TEXT NOT NULL, + keys TEXT NOT NULL, + mint_url TEXT NOT NULL, + first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, + active BOOL NOT NULL DEFAULT TRUE, + + UNIQUE (id, mint_url) + + ); + """ + ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 719ffee..0d1e2a5 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -2,7 +2,7 @@ import base64 import json import secrets as scrts import uuid -from typing import List +from typing import List, Dict import requests from loguru import logger @@ -18,6 +18,7 @@ from cashu.core.base import ( P2SHScript, Proof, SplitRequest, + Keyset, ) from cashu.core.db import Database from cashu.core.script import ( @@ -36,20 +37,37 @@ from cashu.wallet.crud import ( store_p2sh, store_proof, update_proof_reserved, + store_keyset, + get_keyset, ) class LedgerAPI: + keys: Dict[int, str] + keyset: str + def __init__(self, url): self.url = url - @staticmethod - def _get_keys(url): + def _get_keys(self, url): resp = requests.get(url + "/keys").json() - return { - int(amt): PublicKey(bytes.fromhex(val), raw=True) - for amt, val in resp.items() - } + keyset_id = resp["id"] + keys = resp["keys"] + # return { + # "id": keyset_id, + # "keys": { + # int(amt): PublicKey(bytes.fromhex(val), raw=True) + # for amt, val in keys.items() + # }, + # } + keyset_keys = ( + { + int(amt): PublicKey(bytes.fromhex(val), raw=True) + for amt, val in keys.items() + }, + ) + print(resp) + return Keyset(id=keyset_id, keys=keyset_keys, mint_url=self.url) @staticmethod def _get_output_split(amount): @@ -69,7 +87,12 @@ class LedgerAPI: for promise, secret, r in zip(promises, secrets, rs): C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) C = b_dhke.step3_alice(C_, r, self.keys[promise.amount]) - proof = Proof(amount=promise.amount, C=C.serialize().hex(), secret=secret) + proof = Proof( + id=self.keyset_id, + amount=promise.amount, + C=C.serialize().hex(), + secret=secret, + ) proofs.append(proof) return proofs @@ -78,11 +101,16 @@ class LedgerAPI: """Returns base64 encoded random string.""" return scrts.token_urlsafe(randombits // 8) - def _load_mint(self): + async def _load_mint(self): assert len( self.url ), "Ledger not initialized correctly: mint URL not specified yet. " - self.keys = self._get_keys(self.url) + keyset = self._get_keys(self.url) + keyset_local: Keyset = await get_keyset(keyset.id, self.url, db=self.db) + if keyset_local is None: + await store_keyset(keyset=keyset, db=self.db) + self.keys = keyset.keys + self.keyset_id = keyset.id assert len(self.keys) > 0, "did not receive keys from mint." def request_mint(self, amount): @@ -238,8 +266,8 @@ class Wallet(LedgerAPI): self.proofs: List[Proof] = [] self.name = name - def load_mint(self): - super()._load_mint() + async def load_mint(self): + await super()._load_mint() async def load_proofs(self): self.proofs = await get_proofs(db=self.db) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 36bb3c5..c14cc7c 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -31,12 +31,12 @@ def assert_amt(proofs, expected): async def run_test(): wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1") await migrate_databases(wallet1.db, migrations) - wallet1.load_mint() + await wallet1.load_mint() wallet1.status() wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2") await migrate_databases(wallet2.db, migrations) - wallet2.load_mint() + await wallet2.load_mint() wallet2.status() proofs = []