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 01/22] 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 = [] From 6ce15da527f11b5f21705415d214f6d94161ab87 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 13:56:01 +0200 Subject: [PATCH 02/22] keyset working --- cashu/core/base.py | 68 +++++++++++++++++++++++++++++------ cashu/core/crud.py | 74 ++++++++++++++++++++++++++++++++++++++ cashu/core/crypto.py | 31 ++++++++++++++++ cashu/core/errors.py | 21 +++++++++++ cashu/core/migrations.py | 2 -- cashu/mint/crud.py | 1 - cashu/mint/ledger.py | 48 +++++++------------------ cashu/mint/migrations.py | 33 +++++++++++++++++ cashu/mint/startup.py | 6 ++-- cashu/wallet/crud.py | 63 +++++--------------------------- cashu/wallet/migrations.py | 18 +++++++++- cashu/wallet/wallet.py | 72 ++++++++++++++++++++----------------- 12 files changed, 299 insertions(+), 138 deletions(-) create mode 100644 cashu/core/crud.py create mode 100644 cashu/core/crypto.py create mode 100644 cashu/core/errors.py diff --git a/cashu/core/base.py b/cashu/core/base.py index 428432b..75d6a61 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1,5 +1,9 @@ from sqlite3 import Row -from typing import List, Union, Dict +from typing import List, Union, Dict, Any +from cashu.core.crypto import derive_keyset_id, derive_keys, derive_pubkeys +from cashu.core.secp import PrivateKey, PublicKey + +from loguru import logger from pydantic import BaseModel @@ -9,12 +13,10 @@ class CashuError(BaseModel): error = "CashuError" -class Keyset(BaseModel): +class KeyBase(BaseModel): id: str - keys: Dict - mint_url: Union[str, None] = None - first_seen: Union[str, None] = None - active: bool = True + amount: int + pubkey: str @classmethod def from_row(cls, row: Row): @@ -22,13 +24,55 @@ class Keyset(BaseModel): return cls return cls( id=row[0], - keys=row[1], - mint_url=row[2], - first_seen=row[3], - active=row[4], + amount=int(row[1]), + pubkey=row[2], ) +class Keyset: + id: str + private_keys: Dict[int, PrivateKey] + public_keys: Dict[int, PublicKey] + mint_url: Union[str, None] = 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, + seed: Union[None, str] = None, + derivation_path: str = "0", + pubkeys: Union[None, Dict[int, PublicKey]] = None, + ): + if seed: + self.private_keys = derive_keys(seed, derivation_path) + self.public_keys = derive_pubkeys(self.private_keys) + if pubkeys: + self.public_keys = pubkeys + 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], + mint_url=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()) + for k, v in self.public_keys.items() + } + + class P2SHScript(BaseModel): script: str signature: str @@ -65,6 +109,7 @@ class Proof(BaseModel): send_id=row[4] or "", time_created=row[5] or "", time_reserved=row[6] or "", + id=row[7] or "", ) @classmethod @@ -116,17 +161,20 @@ class Invoice(BaseModel): class BlindedMessage(BaseModel): + id: str = "" amount: int B_: str class BlindedSignature(BaseModel): + id: str = "" amount: int C_: str @classmethod def from_dict(cls, d: dict): return cls( + id=d["id"], amount=d["amount"], C_=d["C_"], ) diff --git a/cashu/core/crud.py b/cashu/core/crud.py new file mode 100644 index 0000000..6c107ea --- /dev/null +++ b/cashu/core/crud.py @@ -0,0 +1,74 @@ +from typing import Optional + +from cashu.core.base import Keyset, KeyBase +from cashu.core.db import Connection, Database + + +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, valid_from, valid_to, first_seen, active) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + keyset.id, + 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 Keyset.from_row(row) + + +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), + ) diff --git a/cashu/core/crypto.py b/cashu/core/crypto.py new file mode 100644 index 0000000..c57aa4b --- /dev/null +++ b/cashu/core/crypto.py @@ -0,0 +1,31 @@ +import hashlib +from typing import Dict, List +from cashu.core.secp import PrivateKey, PublicKey +from cashu.core.settings import MAX_ORDER + + +def derive_keys(master_key: str, derivation_path: str = ""): + """ + Deterministic derivation of keys for 2^n values. + TODO: Implement BIP32. + """ + return { + 2 + ** i: PrivateKey( + hashlib.sha256((str(master_key) + derivation_path + str(i)).encode("utf-8")) + .hexdigest() + .encode("utf-8")[:32], + raw=True, + ) + for i in range(MAX_ORDER) + } + + +def derive_pubkeys(keys: Dict[int, PrivateKey]): + return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} + + +def derive_keyset_id(keys: Dict[str, PublicKey]): + """Deterministic derivation keyset_id from set of public keys.""" + pubkeys_concat = "".join([p.serialize().hex() for _, p in keys.items()]) + return hashlib.sha256((pubkeys_concat).encode("utf-8")).hexdigest()[:16] diff --git a/cashu/core/errors.py b/cashu/core/errors.py new file mode 100644 index 0000000..a2ea6c3 --- /dev/null +++ b/cashu/core/errors.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel + + +class CashuError(Exception, BaseModel): + code = "000" + error = "CashuError" + + +class MintException(CashuError): + code = 100 + error = "Mint" + + +class LightningException(MintException): + code = 200 + error = "Lightning" + + +class InvoiceNotPaidException(LightningException): + code = 201 + error = "invoice not paid." diff --git a/cashu/core/migrations.py b/cashu/core/migrations.py index 6beaa91..cc5041f 100644 --- a/cashu/core/migrations.py +++ b/cashu/core/migrations.py @@ -1,7 +1,5 @@ import re -from loguru import logger - from cashu.core.db import COCKROACH, POSTGRES, SQLITE, Database diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index a608967..0afc285 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -1,4 +1,3 @@ -import secrets from typing import Optional from cashu.core.base import Invoice, Proof diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 1779743..bb689e8 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -2,20 +2,17 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c """ -import hashlib import math -from inspect import signature -from signal import signal 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.base import BlindedMessage, BlindedSignature, Invoice, Proof, Keyset +from cashu.core.crypto import derive_keyset_id, derive_keys, derive_pubkeys from cashu.core.db import Database from cashu.core.helpers import fee_reserve from cashu.core.script import verify_script -from cashu.core.secp import PrivateKey, PublicKey +from cashu.core.secp import PublicKey from cashu.core.settings import LIGHTNING, MAX_ORDER from cashu.core.split import amount_split from cashu.lightning import WALLET @@ -32,34 +29,13 @@ from cashu.mint.crud import ( class Ledger: def __init__(self, secret_key: str, db: str): self.proofs_used: Set[str] = set() - 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.keyset = Keyset(self.master_key) self.db: Database = Database("mint", db) async def load_used_proofs(self): self.proofs_used = set(await get_proofs_used(db=self.db)) - @staticmethod - 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) + keyset_id).encode("utf-8")) - .hexdigest() - .encode("utf-8")[:32], - raw=True, - ) - for i in range(MAX_ORDER) - } - - @staticmethod - def _derive_pubkeys(keys: List[PrivateKey]): - return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} - async def _generate_promises(self, amounts: List[int], B_s: List[str]): """Generates promises that sum to the given amount.""" return [ @@ -69,7 +45,7 @@ class Ledger: async def _generate_promise(self, amount: int, B_: PublicKey): """Generates a promise for given amount and returns a pair (amount, C').""" - secret_key = self.keys[amount] # Get the correct key + secret_key = self.keyset.private_keys[amount] # Get the correct key C_ = b_dhke.step2_bob(B_, secret_key) await store_promise( amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db @@ -94,7 +70,9 @@ 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.keys[proof.amount] # Get the correct key to check against + secret_key = self.keyset.private_keys[ + proof.amount + ] # Get the correct key to check against C = PublicKey(bytes.fromhex(proof.C), raw=True) return b_dhke.verify(secret_key, C, proof.secret) @@ -125,7 +103,7 @@ class Ledger: assert len(proof.secret.split(":")) == 3, "secret format wrong." assert proof.secret.split(":")[1] == str( txin_p2sh_address - ), f"secret does not contain correct P2SH address: {proof.secret.split(':')[1]}!={txin_p2sh_address}." + ), f"secret does not contain correct P2SH address: {proof.secret.split(':')[1]} is not {txin_p2sh_address}." return valid def _verify_outputs(self, total: int, amount: int, outputs: List[BlindedMessage]): @@ -223,13 +201,13 @@ class Ledger: for p in proofs: await invalidate_proof(p, db=self.db) - # Public methods - def get_pubkeys(self): + def _serialize_pubkeys(self): """Returns public keys for possible amounts.""" - return {a: p.serialize().hex() for a, p in self.pub_keys.items()} + return {a: p.serialize().hex() for a, p in self.keyset.public_keys.items()} + # Public methods def get_keyset(self): - return {"id": self.keyset_id, "keys": self.get_pubkeys()} + return {"id": self.keyset.id, "keys": self._serialize_pubkeys()} async def request_mint(self, amount): """Returns Lightning invoice and stores it in the db.""" diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 967b9d3..b1d7329 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -85,3 +85,36 @@ async def m001_initial(db: Database): ); """ ) + + +async def m003_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, + 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, + + 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, + + UNIQUE (id, pubkey) + + ); + """ + ) diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index 1207091..043e9a0 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -4,13 +4,15 @@ from loguru import logger from cashu.core.settings import CASHU_DIR, LIGHTNING from cashu.lightning import WALLET -from cashu.mint.migrations import m001_initial +from cashu.mint import migrations +from cashu.core.migrations import migrate_databases from . import ledger async def load_ledger(): - await asyncio.wait([m001_initial(ledger.db)]) + await migrate_databases(ledger.db, migrations) + # await asyncio.wait([m001_initial(ledger.db)]) await ledger.load_used_proofs() if LIGHTNING: diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index 91c917d..4486023 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -1,7 +1,7 @@ import time -from typing import Any, List, Optional, Dict +from typing import Any, List, Optional -from cashu.core.base import P2SHScript, Proof, Keyset +from cashu.core.base import P2SHScript, Proof from cashu.core.db import Connection, Database @@ -14,10 +14,10 @@ async def store_proof( await (conn or db).execute( """ INSERT INTO proofs - (amount, C, secret, time_created) - VALUES (?, ?, ?, ?) + (id, amount, C, secret, time_created) + VALUES (?, ?, ?, ?, ?) """, - (proof.amount, str(proof.C), str(proof.secret), int(time.time())), + (proof.id, proof.amount, str(proof.C), str(proof.secret), int(time.time())), ) @@ -65,10 +65,10 @@ async def invalidate_proof( await (conn or db).execute( """ INSERT INTO proofs_used - (amount, C, secret, time_used) - VALUES (?, ?, ?, ?) + (amount, C, secret, time_used, id) + VALUES (?, ?, ?, ?, ?) """, - (proof.amount, str(proof.C), str(proof.secret), int(time.time())), + (proof.amount, str(proof.C), str(proof.secret), int(time.time()), proof.id), ) @@ -180,50 +180,3 @@ 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 34f19f2..6cddadf 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -108,8 +108,9 @@ async def m005_mint_keysets(db: Database): f""" CREATE TABLE IF NOT EXISTS keysets ( id TEXT NOT NULL, - keys 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, @@ -118,3 +119,18 @@ async def m005_mint_keysets(db: Database): ); """ ) + 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) + + ); + """ + ) + + 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 6f0cf91..e257abf 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -37,9 +37,8 @@ from cashu.wallet.crud import ( store_p2sh, store_proof, update_proof_reserved, - store_keyset, - get_keyset, ) +from cashu.core.crud import store_keyset, get_keyset class LedgerAPI: @@ -49,37 +48,18 @@ class LedgerAPI: def __init__(self, url): self.url = url -<<<<<<< HEAD def _get_keys(self, url): resp = requests.get(url + "/keys").json() 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_keys(url): - resp = requests.get(url + "/keys") - resp.raise_for_status() - data = resp.json() - return { + assert len(keys), Exception("did not receive any keys") + keyset_keys = { int(amt): PublicKey(bytes.fromhex(val), raw=True) - for amt, val in data.items() + for amt, val in keys.items() } ->>>>>>> main + keyset = Keyset(pubkeys=keyset_keys) + assert keyset_id == keyset.id, Exception("mint keyset id not valid.") + return keyset @staticmethod def _get_output_split(amount): @@ -121,7 +101,7 @@ class LedgerAPI: 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.keys = keyset.public_keys self.keyset_id = keyset.id assert len(self.keys) > 0, "did not receive keys from mint." @@ -215,9 +195,19 @@ class LedgerAPI: await self._check_used_secrets(secrets) payloads, rs = self._construct_outputs(amounts, secrets) split_payload = SplitRequest(proofs=proofs, amount=amount, outputs=payloads) + + def _splitrequest_include_fields(proofs): + """strips away fields from the model that aren't necessary for the /split""" + proofs_include = {"id", "amount", "secret", "C", "script"} + return { + "amount": ..., + "outputs": ..., + "proofs": {i: proofs_include for i in range(len(proofs))}, + } + resp = requests.post( self.url + "/split", - json=split_payload.dict(), + json=split_payload.dict(include=_splitrequest_include_fields(proofs)), ) resp.raise_for_status() try: @@ -263,9 +253,19 @@ class LedgerAPI: async def pay_lightning(self, proofs: List[Proof], invoice: str): payload = MeltRequest(proofs=proofs, invoice=invoice) + + def _meltequest_include_fields(proofs): + """strips away fields from the model that aren't necessary for the /melt""" + proofs_include = {"id", "amount", "secret", "C", "script"} + return { + "amount": ..., + "invoice": ..., + "proofs": {i: proofs_include for i in range(len(proofs))}, + } + resp = requests.post( self.url + "/melt", - json=payload.dict(), + json=payload.dict(include=_meltequest_include_fields(proofs)), ) resp.raise_for_status() @@ -357,14 +357,22 @@ class Wallet(LedgerAPI): ).decode() return token + async def _get_spendable_proofs(self, proofs: List[Proof]): + print(f"Debug: only loading proofs with id: {self.keyset_id}") + proofs = [p for p in proofs if p.id == self.keyset_id or not p.id] + proofs = [p for p in proofs if not p.reserved] + return proofs + async def split_to_send(self, proofs: List[Proof], amount, scnd_secret: str = None): """Like self.split but only considers non-reserved tokens.""" if scnd_secret: logger.debug(f"Spending conditions: {scnd_secret}") - if len([p for p in proofs if not p.reserved]) <= 0: + spendable_proofs = await self._get_spendable_proofs(proofs) + print(f"Balance: {sum([p.amount for p in spendable_proofs])}") + if sum([p.amount for p in spendable_proofs]) < amount: raise Exception("balance too low.") return await self.split( - [p for p in proofs if not p.reserved], amount, scnd_secret + [p for p in spendable_proofs if not p.reserved], amount, scnd_secret ) async def set_reserved(self, proofs: List[Proof], reserved: bool): From 52250fae77f741aa0378fa2c7553de603525cb74 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 13:57:00 +0200 Subject: [PATCH 03/22] make format --- cashu/core/base.py | 8 ++++---- cashu/core/crud.py | 2 +- cashu/core/crypto.py | 1 + cashu/mint/ledger.py | 4 ++-- cashu/mint/startup.py | 2 +- cashu/wallet/wallet.py | 6 +++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 75d6a61..6310df0 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1,12 +1,12 @@ from sqlite3 import Row -from typing import List, Union, Dict, Any -from cashu.core.crypto import derive_keyset_id, derive_keys, derive_pubkeys -from cashu.core.secp import PrivateKey, PublicKey +from typing import Any, Dict, List, Union from loguru import logger - from pydantic import BaseModel +from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys +from cashu.core.secp import PrivateKey, PublicKey + class CashuError(BaseModel): code = "000" diff --git a/cashu/core/crud.py b/cashu/core/crud.py index 6c107ea..0ed3f44 100644 --- a/cashu/core/crud.py +++ b/cashu/core/crud.py @@ -1,6 +1,6 @@ from typing import Optional -from cashu.core.base import Keyset, KeyBase +from cashu.core.base import KeyBase, Keyset from cashu.core.db import Connection, Database diff --git a/cashu/core/crypto.py b/cashu/core/crypto.py index c57aa4b..76255f0 100644 --- a/cashu/core/crypto.py +++ b/cashu/core/crypto.py @@ -1,5 +1,6 @@ import hashlib from typing import Dict, List + from cashu.core.secp import PrivateKey, PublicKey from cashu.core.settings import MAX_ORDER diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index bb689e8..e3ab10c 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -7,8 +7,8 @@ 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, Keyset -from cashu.core.crypto import derive_keyset_id, derive_keys, derive_pubkeys +from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Keyset, 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 from cashu.core.script import verify_script diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index 043e9a0..0fdc108 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -2,10 +2,10 @@ import asyncio from loguru import logger +from cashu.core.migrations import migrate_databases from cashu.core.settings import CASHU_DIR, LIGHTNING from cashu.lightning import WALLET from cashu.mint import migrations -from cashu.core.migrations import migrate_databases from . import ledger diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index e257abf..5a42241 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, Dict +from typing import Dict, List import requests from loguru import logger @@ -13,13 +13,14 @@ from cashu.core.base import ( BlindedSignature, CheckFeesRequest, CheckRequest, + Keyset, MeltRequest, MintRequest, P2SHScript, Proof, SplitRequest, - Keyset, ) +from cashu.core.crud import get_keyset, store_keyset from cashu.core.db import Database from cashu.core.script import ( step0_carol_checksig_redeemscrip, @@ -38,7 +39,6 @@ from cashu.wallet.crud import ( store_proof, update_proof_reserved, ) -from cashu.core.crud import store_keyset, get_keyset class LedgerAPI: From 8b36aca8b6bd054b13f62b83982e0e5ae1381108 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 14:27:41 +0200 Subject: [PATCH 04/22] keyset balances --- cashu/mint/ledger.py | 2 +- cashu/wallet/cli.py | 13 +++++++++++-- cashu/wallet/wallet.py | 26 +++++++++++++++++++++----- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index e3ab10c..d98553a 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -207,7 +207,7 @@ class Ledger: # Public methods def get_keyset(self): - return {"id": self.keyset.id, "keys": self._serialize_pubkeys()} + return self._serialize_pubkeys() async def request_mint(self, amount): """Returns Lightning invoice and stores it in the db.""" diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index b45ee31..7c7aeb5 100755 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -148,7 +148,16 @@ async def pay(ctx, invoice: str): @coro async def balance(ctx): wallet: Wallet = ctx.obj["WALLET"] - wallet.status() + keyset_balances = wallet.balance_per_keyset() + if len(keyset_balances) > 1: + for k, v in keyset_balances.items(): + print( + f"Keyset: {k} Balance: {v['balance']} sat (available: {v['available']})" + ) + print("") + print( + f"Balance: {wallet.balance} sat (available: {wallet.available_balance} sat in {len([p for p in wallet.proofs if not p.reserved])} tokens)" + ) @cli.command("send", help="Send coins.") @@ -192,7 +201,7 @@ async def receive(ctx, coin: str, lock: str): address_split = lock.split("P2SH:")[1] p2shscripts = await get_unused_locks(address_split, db=wallet.db) - assert len(p2shscripts) == 1 + assert len(p2shscripts) == 1, Exception("lock not found.") script = p2shscripts[0].script signature = p2shscripts[0].signature else: diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 5a42241..4d9f6d9 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -3,6 +3,7 @@ import json import secrets as scrts import uuid from typing import Dict, List +from itertools import groupby import requests from loguru import logger @@ -50,15 +51,13 @@ class LedgerAPI: def _get_keys(self, url): resp = requests.get(url + "/keys").json() - keyset_id = resp["id"] - keys = resp["keys"] + keys = resp assert len(keys), Exception("did not receive any keys") keyset_keys = { int(amt): PublicKey(bytes.fromhex(val), raw=True) for amt, val in keys.items() } keyset = Keyset(pubkeys=keyset_keys) - assert keyset_id == keyset.id, Exception("mint keyset id not valid.") return keyset @staticmethod @@ -358,7 +357,6 @@ class Wallet(LedgerAPI): return token async def _get_spendable_proofs(self, proofs: List[Proof]): - print(f"Debug: only loading proofs with id: {self.keyset_id}") proofs = [p for p in proofs if p.id == self.keyset_id or not p.id] proofs = [p for p in proofs if not p.reserved] return proofs @@ -368,7 +366,6 @@ class Wallet(LedgerAPI): if scnd_secret: logger.debug(f"Spending conditions: {scnd_secret}") spendable_proofs = await self._get_spendable_proofs(proofs) - print(f"Balance: {sum([p.amount for p in spendable_proofs])}") if sum([p.amount for p in spendable_proofs]) < amount: raise Exception("balance too low.") return await self.split( @@ -428,5 +425,24 @@ class Wallet(LedgerAPI): f"Balance: {self.balance} sat (available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)" ) + @staticmethod + def _sum_proofs(proofs: List[Proof], available_only=False): + if available_only: + return sum([p.amount for p in proofs if not p.reserved]) + return sum([p.amount for p in proofs]) + + @staticmethod + def _get_proofs_per_keyset(proofs: List[Proof]): + return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)} + + def balance_per_keyset(self): + return { + key: { + "balance": self._sum_proofs(proofs), + "available": self._sum_proofs(proofs, available_only=True), + } + for key, proofs in self._get_proofs_per_keyset(self.proofs).items() + } + def proof_amounts(self): return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])] From de8a5a8e43bb21b5aea0846900d9c62d30332d8b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 14:36:02 +0200 Subject: [PATCH 05/22] better cli --- cashu/wallet/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 7c7aeb5..2ce2272 100755 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -150,9 +150,11 @@ async def balance(ctx): wallet: Wallet = ctx.obj["WALLET"] keyset_balances = wallet.balance_per_keyset() if len(keyset_balances) > 1: + print(f"You have balances in {len(keyset_balances)} keysets:") + print("") for k, v in keyset_balances.items(): print( - f"Keyset: {k} Balance: {v['balance']} sat (available: {v['available']})" + f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']})" ) print("") print( From 5c7c7f9965ca67e80640fd6bf988297b9e60119e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 14:36:13 +0200 Subject: [PATCH 06/22] make format --- cashu/wallet/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 4d9f6d9..25e206c 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -2,8 +2,8 @@ import base64 import json import secrets as scrts import uuid -from typing import Dict, List from itertools import groupby +from typing import Dict, List import requests from loguru import logger From fad9b91061a0f17bca099b6d3dd45546a1b88793 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 15:08:20 +0200 Subject: [PATCH 07/22] mint saves keyset ids --- cashu/core/base.py | 4 ++++ cashu/core/crud.py | 8 ++++---- cashu/mint/ledger.py | 6 ++++++ cashu/mint/migrations.py | 10 +++++----- cashu/mint/startup.py | 1 + cashu/wallet/wallet.py | 33 +++++++++++++++++++-------------- 6 files changed, 39 insertions(+), 23 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 6310df0..1cb946d 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -29,6 +29,10 @@ class KeyBase(BaseModel): ) +from typing import Optional +from cashu.core.db import Connection, Database + + class Keyset: id: str private_keys: Dict[int, PrivateKey] diff --git a/cashu/core/crud.py b/cashu/core/crud.py index 0ed3f44..235094c 100644 --- a/cashu/core/crud.py +++ b/cashu/core/crud.py @@ -6,8 +6,8 @@ from cashu.core.db import Connection, Database async def store_keyset( keyset: Keyset, - mint_url: str, - db: Database, + mint_url: str = None, + db: Database = None, conn: Optional[Connection] = None, ): @@ -19,7 +19,7 @@ async def store_keyset( """, ( keyset.id, - mint_url, + mint_url or keyset.mint_url, keyset.valid_from, keyset.valid_to, keyset.first_seen, @@ -55,7 +55,7 @@ async def get_keyset( """, tuple(values), ) - return Keyset.from_row(row) + return Keyset.from_row(row) if row is not None else None async def store_mint_pubkey( diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index d98553a..c495947 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -16,6 +16,7 @@ from cashu.core.secp import PublicKey from cashu.core.settings import LIGHTNING, MAX_ORDER from cashu.core.split import amount_split from cashu.lightning import WALLET +from cashu.core.crud import get_keyset, store_keyset from cashu.mint.crud import ( get_lightning_invoice, get_proofs_used, @@ -36,6 +37,11 @@ class Ledger: 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: + await store_keyset(keyset=self.keyset, db=self.db) + async def _generate_promises(self, amounts: List[int], B_s: List[str]): """Generates promises that sum to the given amount.""" return [ diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index b1d7329..c3b2559 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -95,11 +95,11 @@ async def m003_mint_keysets(db: Database): f""" 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, + mint_url 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) diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index 0fdc108..1f3dd66 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -14,6 +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() if LIGHTNING: error_message, balance = await WALLET.status() diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 25e206c..30fc154 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -49,7 +49,7 @@ class LedgerAPI: def __init__(self, url): self.url = url - def _get_keys(self, url): + async def _get_keys(self, url): resp = requests.get(url + "/keys").json() keys = resp assert len(keys), Exception("did not receive any keys") @@ -96,10 +96,13 @@ class LedgerAPI: assert len( self.url ), "Ledger not initialized correctly: mint URL not specified yet. " - keyset = self._get_keys(self.url) - keyset_local: Keyset = await get_keyset(keyset.id, self.url, db=self.db) + keyset = await self._get_keys(self.url) + keyset_local: Keyset = 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) + # if keyset_local is None: + # await store_keyset(keyset=keyset, db=self.db) self.keys = keyset.public_keys self.keyset_id = keyset.id assert len(self.keys) > 0, "did not receive keys from mint." @@ -291,6 +294,16 @@ class Wallet(LedgerAPI): for proof in proofs: await store_proof(proof, db=self.db) + @staticmethod + def _sum_proofs(proofs: List[Proof], available_only=False): + if available_only: + return sum([p.amount for p in proofs if not p.reserved]) + return sum([p.amount for p in proofs]) + + @staticmethod + def _get_proofs_per_keyset(proofs: List[Proof]): + return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)} + async def request_mint(self, amount): return super().request_mint(amount) @@ -357,7 +370,9 @@ class Wallet(LedgerAPI): return token async def _get_spendable_proofs(self, proofs: List[Proof]): - proofs = [p for p in proofs if p.id == self.keyset_id or not p.id] + proofs = [ + p for p in proofs if p.id == self.keyset_id 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 @@ -425,16 +440,6 @@ class Wallet(LedgerAPI): f"Balance: {self.balance} sat (available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)" ) - @staticmethod - def _sum_proofs(proofs: List[Proof], available_only=False): - if available_only: - return sum([p.amount for p in proofs if not p.reserved]) - return sum([p.amount for p in proofs]) - - @staticmethod - def _get_proofs_per_keyset(proofs: List[Proof]): - return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)} - def balance_per_keyset(self): return { key: { From a54ca6e6567ebcf878ce707c8355b32eda7494a0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 15:08:33 +0200 Subject: [PATCH 08/22] make format --- cashu/core/base.py | 1 + cashu/mint/ledger.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 1cb946d..9a7b387 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -30,6 +30,7 @@ class KeyBase(BaseModel): from typing import Optional + from cashu.core.db import Connection, Database diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index c495947..4d321f8 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -8,6 +8,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, Keyset, Proof +from cashu.core.crud import get_keyset, store_keyset 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 @@ -16,7 +17,6 @@ from cashu.core.secp import PublicKey from cashu.core.settings import LIGHTNING, MAX_ORDER from cashu.core.split import amount_split from cashu.lightning import WALLET -from cashu.core.crud import get_keyset, store_keyset from cashu.mint.crud import ( get_lightning_invoice, get_proofs_used, From 244ea4a3430cf7a02248e854dd75cada9e29fc24 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 19:25:03 +0200 Subject: [PATCH 09/22] multiple keysets per mint --- cashu/core/base.py | 91 +++++++++++++++++++++----- cashu/core/crud.py | 128 ++++++++++++++++++------------------- cashu/mint/crud.py | 56 +++++++++++++++- cashu/mint/ledger.py | 44 +++++++++---- cashu/mint/migrations.py | 4 +- cashu/mint/router.py | 6 ++ cashu/mint/startup.py | 2 +- cashu/wallet/crud.py | 57 +++++++++++++++++ cashu/wallet/migrations.py | 30 ++++----- cashu/wallet/wallet.py | 24 +++++-- 10 files changed, 328 insertions(+), 114 deletions(-) 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 From c5303d6b43dc748383ce688f5cbabf084302533c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 19:25:14 +0200 Subject: [PATCH 10/22] make format --- cashu/mint/crud.py | 2 +- cashu/mint/ledger.py | 8 +++++--- cashu/wallet/crud.py | 5 +---- cashu/wallet/wallet.py | 6 +++--- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 83b7e9d..19d0f0c 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, MintKeyset +from cashu.core.base import Invoice, MintKeyset, Proof from cashu.core.db import Connection, Database diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 6d96fb0..561e273 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -3,8 +3,10 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c """ import math -from typing import List, Set, Dict +from typing import Dict, List, Set + from loguru import logger + import cashu.core.b_dhke as b_dhke import cashu.core.bolt11 as bolt11 from cashu.core.base import ( @@ -24,14 +26,14 @@ from cashu.core.settings import LIGHTNING, MAX_ORDER from cashu.core.split import amount_split from cashu.lightning import WALLET from cashu.mint.crud import ( + get_keyset, get_lightning_invoice, get_proofs_used, invalidate_proof, + store_keyset, store_lightning_invoice, store_promise, update_lightning_invoice, - get_keyset, - store_keyset, ) diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index 50e3676..12d8401 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -1,10 +1,7 @@ import time 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.base import KeyBase, P2SHScript, Proof, WalletKeyset from cashu.core.db import Connection, Database diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index a75ce22..32de07c 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -14,12 +14,12 @@ from cashu.core.base import ( BlindedSignature, CheckFeesRequest, CheckRequest, - WalletKeyset, MeltRequest, MintRequest, P2SHScript, Proof, SplitRequest, + WalletKeyset, ) from cashu.core.db import Database from cashu.core.script import ( @@ -32,14 +32,14 @@ from cashu.core.secp import PublicKey from cashu.core.settings import DEBUG from cashu.core.split import amount_split from cashu.wallet.crud import ( + get_keyset, get_proofs, invalidate_proof, secret_used, + store_keyset, store_p2sh, store_proof, update_proof_reserved, - get_keyset, - store_keyset, ) From 65d629137a950e0ff52d5c617707cd4c7006d62f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 19:34:29 +0200 Subject: [PATCH 11/22] bump --- cashu/wallet/migrations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 886f8ca..0a0a73e 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -109,10 +109,10 @@ async def m005_wallet_keysets(db: Database): CREATE TABLE IF NOT EXISTS keysets ( id TEXT NOT NULL, mint_url TEXT NOT NULL, - 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, + 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) From 13e583e492e0c127549548cbfc9708304f392661 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 19:44:36 +0200 Subject: [PATCH 12/22] load mint --- cashu/mint/ledger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 561e273..0813ce0 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -38,10 +38,10 @@ from cashu.mint.crud import ( class Ledger: - def __init__(self, secret_key: str, db: str): + def __init__(self, secret_key: str, db: str, derivation_path=""): self.proofs_used: Set[str] = set() self.master_key = secret_key - self.keyset = MintKeyset(seed=self.master_key, derivation_path="1") + self.derivation_path = derivation_path self.db: Database = Database("mint", db) async def load_used_proofs(self): @@ -49,6 +49,9 @@ class Ledger: async def init_keysets(self): """Loads all past keysets and stores the active one if not already in db""" + self.keyset = MintKeyset( + seed=self.master_key, derivation_path=self.derivation_path + ) # get all past keysets tmp_keysets: List[MintKeyset] = await get_keyset(db=self.db) self.keysets = MintKeysets(tmp_keysets) From 4cee8851310c932a568aa947dfb24abf045fb513 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 19:50:11 +0200 Subject: [PATCH 13/22] etza --- cashu/core/base.py | 3 --- cashu/wallet/wallet.py | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 4f68c82..7cc775d 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1,7 +1,6 @@ from sqlite3 import Row from typing import Any, Dict, List, Union -from loguru import logger from pydantic import BaseModel from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys @@ -57,7 +56,6 @@ class WalletKeyset: if pubkeys: self.public_keys = pubkeys self.id = derive_keyset_id(self.public_keys) - logger.debug(f"Wallet keyset id: {self.id}") @classmethod def from_row(cls, row: Row): @@ -107,7 +105,6 @@ class MintKeyset: 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): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 32de07c..6e96871 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -103,7 +103,9 @@ class LedgerAPI: self.url ), "Ledger not initialized correctly: mint URL not specified yet. " keyset = await self._get_keys(self.url) + logger.debug(f"Current mint keyset: {keyset.id}") keysets = await self._get_keysets(self.url) + logger.debug(f"Mint keysets: {keysets}") keyset_local: WalletKeyset = await get_keyset(keyset.id, db=self.db) if keyset_local is None: await store_keyset(keyset=keyset, db=self.db) From 59aab85c6a25145ee41128ec0c25f49ae7e6680e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 19:54:18 +0200 Subject: [PATCH 14/22] pk for github --- .github/workflows/tests.yml | 1 + cashu/core/crud.py | 74 ------------------------------------- 2 files changed, 1 insertion(+), 74 deletions(-) delete mode 100644 cashu/core/crud.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 33455d0..f32ad3d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,6 +25,7 @@ jobs: - name: Run mint env: LIGHTNING: False + MINT_PRIVATE_KEY: "testingkey" MINT_SERVER_HOST: 0.0.0.0 MINT_SERVER_PORT: 3338 run: | diff --git a/cashu/core/crud.py b/cashu/core/crud.py deleted file mode 100644 index c1b01ab..0000000 --- a/cashu/core/crud.py +++ /dev/null @@ -1,74 +0,0 @@ -# from typing import Optional - -# 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, -# ): - -# 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 Keyset.from_row(row) if row is not None else 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), -# ) From 45b8aa5fc08c8520ec432c49c0bd65c2825c493e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 20:00:05 +0200 Subject: [PATCH 15/22] test --- cashu/mint/ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 0813ce0..4c8e3dd 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -38,7 +38,7 @@ from cashu.mint.crud import ( class Ledger: - def __init__(self, secret_key: str, db: str, derivation_path=""): + def __init__(self, secret_key: str, db: str, derivation_path="0"): self.proofs_used: Set[str] = set() self.master_key = secret_key self.derivation_path = derivation_path From eb46fd7b7470b6d4b5f272482588f394bd9cf5c6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 20:03:46 +0200 Subject: [PATCH 16/22] load keysets after storing them lol --- cashu/mint/ledger.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 4c8e3dd..5ac08d3 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -38,7 +38,7 @@ from cashu.mint.crud import ( class Ledger: - def __init__(self, secret_key: str, db: str, derivation_path="0"): + def __init__(self, secret_key: str, db: str, derivation_path=""): self.proofs_used: Set[str] = set() self.master_key = secret_key self.derivation_path = derivation_path @@ -52,13 +52,6 @@ class Ledger: self.keyset = MintKeyset( seed=self.master_key, derivation_path=self.derivation_path ) - # 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 ) @@ -66,6 +59,15 @@ class Ledger: logger.debug(f"Storing keyset {self.keyset.id}") await store_keyset(keyset=self.keyset, db=self.db) + # get all past keysets + tmp_keysets: List[MintKeyset] = await get_keyset(db=self.db) + self.keysets = MintKeysets(tmp_keysets) + logger.debug(f"Keysets {self.keysets.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.") + async def _generate_promises(self, amounts: List[int], B_s: List[str]): """Generates promises that sum to the given amount.""" return [ From 1687cc0ab910d82f25d18a577946e7621a7674a8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 20:17:28 +0200 Subject: [PATCH 17/22] comments --- cashu/wallet/wallet.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 6e96871..a3d4d37 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -99,23 +99,31 @@ class LedgerAPI: return scrts.token_urlsafe(randombits // 8) async def _load_mint(self): + """ + Loads the current keys and the active keyset of the map. + """ assert len( self.url ), "Ledger not initialized correctly: mint URL not specified yet. " + # get current keyset keyset = await self._get_keys(self.url) logger.debug(f"Current mint keyset: {keyset.id}") + # get all active keysets keysets = await self._get_keysets(self.url) logger.debug(f"Mint keysets: {keysets}") + + # check if current keyset is in db 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) - # if keyset_local is None: - # await store_keyset(keyset=keyset, db=self.db) + + # store current keyset + assert len(self.keys) > 0, "did not receive keys from mint." self.keys = keyset.public_keys self.keyset_id = keyset.id + + # store active keysets self.keysets = keysets["keysets"] - assert len(self.keys) > 0, "did not receive keys from mint." def request_mint(self, amount): """Requests a mint from the server and returns Lightning invoice.""" From 7f1b026bb93ed1a1b93e54200b4b012b0ecd9bcf Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 20:23:41 +0200 Subject: [PATCH 18/22] comments --- cashu/mint/ledger.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 5ac08d3..8d3c558 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -49,9 +49,11 @@ class Ledger: async def init_keysets(self): """Loads all past keysets and stores the active one if not already in db""" + # generate current keyset from seed and current derivation path self.keyset = MintKeyset( seed=self.master_key, derivation_path=self.derivation_path ) + # check if current keyset is stored in db and store if not current_keyset_local: List[MintKeyset] = await get_keyset( id=self.keyset.id, db=self.db ) @@ -59,12 +61,15 @@ class Ledger: logger.debug(f"Storing keyset {self.keyset.id}") await store_keyset(keyset=self.keyset, db=self.db) - # get all past keysets + # load all past keysets from db + # this needs two steps because the types of tmp_keysets and the argument of MintKeysets() are different tmp_keysets: List[MintKeyset] = await get_keyset(db=self.db) self.keysets = MintKeysets(tmp_keysets) logger.debug(f"Keysets {self.keysets.keysets}") + # generate all derived keys from stored derivation paths of past 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.") @@ -106,6 +111,7 @@ class Ledger: if not proof.id: secret_key = self.keyset.private_keys[proof.amount] else: + # use the appropriate active keyset for this proof.id secret_key = self.keysets.keysets[proof.id].private_keys[proof.amount] C = PublicKey(bytes.fromhex(proof.C), raw=True) From 359bbaac0ac3bc032b4e95ad0d9edb396e7f16a5 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Oct 2022 20:26:54 +0200 Subject: [PATCH 19/22] bump version --- cashu/core/settings.py | 2 +- cashu/wallet/wallet.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 68a74ba..1f23b89 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -48,4 +48,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None) LNBITS_KEY = env.str("LNBITS_KEY", default=None) MAX_ORDER = 64 -VERSION = "0.2.6" +VERSION = "0.3.0" diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index a3d4d37..4c150e0 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -118,7 +118,7 @@ class LedgerAPI: await store_keyset(keyset=keyset, db=self.db) # store current keyset - assert len(self.keys) > 0, "did not receive keys from mint." + assert len(keyset.public_keys) > 0, "did not receive keys from mint." self.keys = keyset.public_keys self.keyset_id = keyset.id diff --git a/pyproject.toml b/pyproject.toml index 62eb8f5..1c4d8a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.2.6" +version = "0.3.0" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" diff --git a/setup.py b/setup.py index 2cc3078..d06a8c9 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]} setuptools.setup( name="cashu", - version="0.2.6", + version="0.3.0", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown", From 9f1e180274a03948a8efc3fb9e2481228544e9ed Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 10:31:10 +0200 Subject: [PATCH 20/22] file --- cashu/core/base.py | 253 +++++++++++++++++++++---------------------- cashu/core/crypto.py | 6 +- cashu/core/errors.py | 25 +++-- cashu/mint/router.py | 3 +- 4 files changed, 145 insertions(+), 142 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 7cc775d..6ab1de4 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -7,135 +7,6 @@ from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys from cashu.core.secp import PrivateKey, PublicKey -class CashuError(BaseModel): - code = "000" - error = "CashuError" - - -class KeyBase(BaseModel): - id: str - amount: int - pubkey: str - - @classmethod - def from_row(cls, row: Row): - if row is None: - return cls - return cls( - id=row[0], - amount=int(row[1]), - pubkey=row[2], - ) - - -class WalletKeyset: - id: str - public_keys: Dict[int, PublicKey] - mint_url: Union[str, None] = 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, - pubkeys: Dict[int, PublicKey] = None, - mint_url=None, - id=None, - valid_from=None, - valid_to=None, - first_seen=None, - active=None, - ): - 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) - - @classmethod - def from_row(cls, row: Row): - if row is None: - return cls - return cls( - id=row[0], - mint_url=row[1], - valid_from=row[2], - valid_to=row[3], - first_seen=row[4], - 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) - - @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()) - for k, v in self.public_keys.items() - } - - -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 @@ -297,3 +168,127 @@ class MeltRequest(BaseModel): proofs: List[Proof] amount: int = None # deprecated invoice: str + + +class KeyBase(BaseModel): + id: str + amount: int + pubkey: str + + @classmethod + def from_row(cls, row: Row): + if row is None: + return cls + return cls( + id=row[0], + amount=int(row[1]), + pubkey=row[2], + ) + + +class WalletKeyset: + id: str + public_keys: Dict[int, PublicKey] + mint_url: Union[str, None] = 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, + pubkeys: Dict[int, PublicKey] = None, + mint_url=None, + id=None, + valid_from=None, + valid_to=None, + first_seen=None, + active=None, + ): + 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) + + @classmethod + def from_row(cls, row: Row): + if row is None: + return cls + return cls( + id=row[0], + mint_url=row[1], + valid_from=row[2], + valid_to=row[3], + first_seen=row[4], + 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) + + @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()) + for k, v in self.public_keys.items() + } + + +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()] diff --git a/cashu/core/crypto.py b/cashu/core/crypto.py index 76255f0..15a8023 100644 --- a/cashu/core/crypto.py +++ b/cashu/core/crypto.py @@ -1,6 +1,6 @@ import hashlib from typing import Dict, List - +import base64 from cashu.core.secp import PrivateKey, PublicKey from cashu.core.settings import MAX_ORDER @@ -29,4 +29,6 @@ def derive_pubkeys(keys: Dict[int, PrivateKey]): def derive_keyset_id(keys: Dict[str, PublicKey]): """Deterministic derivation keyset_id from set of public keys.""" pubkeys_concat = "".join([p.serialize().hex() for _, p in keys.items()]) - return hashlib.sha256((pubkeys_concat).encode("utf-8")).hexdigest()[:16] + return base64.b64encode(hashlib.sha256((pubkeys_concat).encode("utf-8")).digest())[ + :12 + ] diff --git a/cashu/core/errors.py b/cashu/core/errors.py index a2ea6c3..f770896 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -1,21 +1,26 @@ from pydantic import BaseModel -class CashuError(Exception, BaseModel): +class CashuError(BaseModel): code = "000" error = "CashuError" -class MintException(CashuError): - code = 100 - error = "Mint" +# class CashuError(Exception, BaseModel): +# code = "000" +# error = "CashuError" -class LightningException(MintException): - code = 200 - error = "Lightning" +# class MintException(CashuError): +# code = 100 +# error = "Mint" -class InvoiceNotPaidException(LightningException): - code = 201 - error = "invoice not paid." +# class LightningException(MintException): +# code = 200 +# error = "Lightning" + + +# class InvoiceNotPaidException(LightningException): +# code = 201 +# error = "invoice not paid." diff --git a/cashu/mint/router.py b/cashu/mint/router.py index a502398..e7842d7 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -4,7 +4,6 @@ from fastapi import APIRouter from secp256k1 import PublicKey from cashu.core.base import ( - CashuError, CheckFeesRequest, CheckFeesResponse, CheckRequest, @@ -15,6 +14,8 @@ from cashu.core.base import ( PostSplitResponse, SplitRequest, ) +from cashu.core.errors import CashuError + from cashu.mint import ledger router: APIRouter = APIRouter() From 590a1a349af86b281f054d23e7af916bf5b9e203 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 10:52:38 +0200 Subject: [PATCH 21/22] binary to string --- cashu/core/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 6ab1de4..dd9a248 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -269,7 +269,7 @@ class MintKeyset: if row is None: return cls return cls( - id=row[0], + id=row[0].decode("ascii"), derivation_path=row[1], valid_from=row[2], valid_to=row[3], From 2c93fee5652ce58094bbcef690b702f8c2d24582 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 10:54:42 +0200 Subject: [PATCH 22/22] fix --- cashu/core/base.py | 4 +++- cashu/core/crypto.py | 3 ++- cashu/mint/router.py | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index dd9a248..d54913a 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -268,8 +268,10 @@ class MintKeyset: def from_row(cls, row: Row): if row is None: return cls + # fix to convert byte to string, unclear why this is necessary + id = row[0].decode("ascii") if type(row[0]) == bytes else row[0] return cls( - id=row[0].decode("ascii"), + id=id, derivation_path=row[1], valid_from=row[2], valid_to=row[3], diff --git a/cashu/core/crypto.py b/cashu/core/crypto.py index 15a8023..379d3ed 100644 --- a/cashu/core/crypto.py +++ b/cashu/core/crypto.py @@ -1,6 +1,7 @@ +import base64 import hashlib from typing import Dict, List -import base64 + from cashu.core.secp import PrivateKey, PublicKey from cashu.core.settings import MAX_ORDER diff --git a/cashu/mint/router.py b/cashu/mint/router.py index e7842d7..ae78859 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -15,7 +15,6 @@ from cashu.core.base import ( SplitRequest, ) from cashu.core.errors import CashuError - from cashu.mint import ledger router: APIRouter = APIRouter()