This commit is contained in:
callebtc
2022-10-07 16:30:50 +02:00
parent 01b201d245
commit 1bdb4568c4
8 changed files with 149 additions and 28 deletions

View File

@@ -1,5 +1,5 @@
from sqlite3 import Row from sqlite3 import Row
from typing import List, Union from typing import List, Union, Dict
from pydantic import BaseModel from pydantic import BaseModel
@@ -9,6 +9,26 @@ class CashuError(BaseModel):
error = "CashuError" 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): class P2SHScript(BaseModel):
script: str script: str
signature: str signature: str
@@ -25,6 +45,7 @@ class P2SHScript(BaseModel):
class Proof(BaseModel): class Proof(BaseModel):
id: str = ""
amount: int amount: int
secret: str = "" secret: str = ""
C: str C: str
@@ -60,10 +81,10 @@ class Proof(BaseModel):
) )
def to_dict(self): 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): 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): def __getitem__(self, key):
return self.__getattribute__(key) return self.__getattribute__(key)

View File

@@ -11,6 +11,7 @@ from typing import List, Set
import cashu.core.b_dhke as b_dhke import cashu.core.b_dhke as b_dhke
import cashu.core.bolt11 as bolt11 import cashu.core.bolt11 as bolt11
from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Proof 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.db import Database
from cashu.core.helpers import fee_reserve from cashu.core.helpers import fee_reserve
from cashu.core.script import verify_script from cashu.core.script import verify_script
@@ -34,6 +35,7 @@ class Ledger:
self.master_key = secret_key self.master_key = secret_key
self.keys = self._derive_keys(self.master_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.pub_keys = self._derive_pubkeys(self.keys)
self.db: Database = Database("mint", db) self.db: Database = Database("mint", db)
@@ -41,12 +43,12 @@ class Ledger:
self.proofs_used = set(await get_proofs_used(db=self.db)) self.proofs_used = set(await get_proofs_used(db=self.db))
@staticmethod @staticmethod
def _derive_keys(master_key: str): def _derive_keys(master_key: str, keyset_id: str = ""):
"""Deterministic derivation of keys for 2^n values.""" """Deterministic derivation of keys for 2^n values."""
return { return {
2 2
** i: PrivateKey( ** 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() .hexdigest()
.encode("utf-8")[:32], .encode("utf-8")[:32],
raw=True, raw=True,
@@ -221,6 +223,9 @@ class Ledger:
"""Returns public keys for possible amounts.""" """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.pub_keys.items()}
def get_keyset(self):
return {"id": self.keyset_id, "keys": self.get_pubkeys()}
async def request_mint(self, amount): async def request_mint(self, amount):
"""Returns Lightning invoice and stores it in the db.""" """Returns Lightning invoice and stores it in the db."""
payment_request, checking_id = await self._request_lightning_invoice(amount) payment_request, checking_id = await self._request_lightning_invoice(amount)

View File

@@ -23,7 +23,7 @@ router: APIRouter = APIRouter()
@router.get("/keys") @router.get("/keys")
def keys(): def keys():
"""Get the public keys of the mint""" """Get the public keys of the mint"""
return ledger.get_pubkeys() return ledger.get_keyset()
@router.get("/mint") @router.get("/mint")

View File

@@ -78,7 +78,7 @@ def coro(f):
@coro @coro
async def mint(ctx, amount: int, hash: str): async def mint(ctx, amount: int, hash: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
wallet.load_mint() await wallet.load_mint()
wallet.status() wallet.status()
if not LIGHTNING: if not LIGHTNING:
r = await wallet.mint(amount) r = await wallet.mint(amount)
@@ -122,7 +122,7 @@ async def mint(ctx, amount: int, hash: str):
@coro @coro
async def pay(ctx, invoice: str): async def pay(ctx, invoice: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
wallet.load_mint() await wallet.load_mint()
wallet.status() wallet.status()
decoded_invoice: Invoice = bolt11.decode(invoice) decoded_invoice: Invoice = bolt11.decode(invoice)
# check if it's an internal payment # 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: if lock and len(lock.split("P2SH:")) == 2:
p2sh = True p2sh = True
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
wallet.load_mint() await wallet.load_mint()
wallet.status() wallet.status()
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock) _, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock)
await wallet.set_reserved(send_proofs, reserved=True) await wallet.set_reserved(send_proofs, reserved=True)
@@ -181,7 +181,7 @@ async def send(ctx, amount: int, lock: str):
@coro @coro
async def receive(ctx, coin: str, lock: str): async def receive(ctx, coin: str, lock: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
wallet.load_mint() await wallet.load_mint()
wallet.status() wallet.status()
if lock: if lock:
# load the script and signature of this address from the database # load the script and signature of this address from the database
@@ -211,7 +211,7 @@ async def receive(ctx, coin: str, lock: str):
@coro @coro
async def burn(ctx, coin: str, all: bool, force: bool): async def burn(ctx, coin: str, all: bool, force: bool):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
wallet.load_mint() await wallet.load_mint()
if not (all or coin or force) or (coin and all): if not (all or coin or force) or (coin and all):
print( print(
"Error: enter a coin or use --all to burn all pending coins or --force to check all coins." "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 @coro
async def pending(ctx): async def pending(ctx):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
wallet.load_mint() await wallet.load_mint()
reserved_proofs = await get_reserved_proofs(wallet.db) reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs): if len(reserved_proofs):
print(f"--------------------------\n") print(f"--------------------------\n")

View File

@@ -1,7 +1,7 @@
import time 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 from cashu.core.db import Connection, Database
@@ -180,3 +180,50 @@ async def update_p2sh_used(
f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?", f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?",
(*values, str(p2sh.address)), (*values, str(p2sh.address)),
) )
async def store_keyset(
keyset: 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)

View File

@@ -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)
);
"""
)

View File

@@ -2,7 +2,7 @@ import base64
import json import json
import secrets as scrts import secrets as scrts
import uuid import uuid
from typing import List from typing import List, Dict
import requests import requests
from loguru import logger from loguru import logger
@@ -18,6 +18,7 @@ from cashu.core.base import (
P2SHScript, P2SHScript,
Proof, Proof,
SplitRequest, SplitRequest,
Keyset,
) )
from cashu.core.db import Database from cashu.core.db import Database
from cashu.core.script import ( from cashu.core.script import (
@@ -36,20 +37,37 @@ from cashu.wallet.crud import (
store_p2sh, store_p2sh,
store_proof, store_proof,
update_proof_reserved, update_proof_reserved,
store_keyset,
get_keyset,
) )
class LedgerAPI: class LedgerAPI:
keys: Dict[int, str]
keyset: str
def __init__(self, url): def __init__(self, url):
self.url = url self.url = url
@staticmethod def _get_keys(self, url):
def _get_keys(url):
resp = requests.get(url + "/keys").json() resp = requests.get(url + "/keys").json()
return { keyset_id = resp["id"]
int(amt): PublicKey(bytes.fromhex(val), raw=True) keys = resp["keys"]
for amt, val in resp.items() # 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 @staticmethod
def _get_output_split(amount): def _get_output_split(amount):
@@ -69,7 +87,12 @@ class LedgerAPI:
for promise, secret, r in zip(promises, secrets, rs): for promise, secret, r in zip(promises, secrets, rs):
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
C = b_dhke.step3_alice(C_, r, self.keys[promise.amount]) 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) proofs.append(proof)
return proofs return proofs
@@ -78,11 +101,16 @@ class LedgerAPI:
"""Returns base64 encoded random string.""" """Returns base64 encoded random string."""
return scrts.token_urlsafe(randombits // 8) return scrts.token_urlsafe(randombits // 8)
def _load_mint(self): async def _load_mint(self):
assert len( assert len(
self.url self.url
), "Ledger not initialized correctly: mint URL not specified yet. " ), "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." assert len(self.keys) > 0, "did not receive keys from mint."
def request_mint(self, amount): def request_mint(self, amount):
@@ -238,8 +266,8 @@ class Wallet(LedgerAPI):
self.proofs: List[Proof] = [] self.proofs: List[Proof] = []
self.name = name self.name = name
def load_mint(self): async def load_mint(self):
super()._load_mint() await super()._load_mint()
async def load_proofs(self): async def load_proofs(self):
self.proofs = await get_proofs(db=self.db) self.proofs = await get_proofs(db=self.db)

View File

@@ -31,12 +31,12 @@ def assert_amt(proofs, expected):
async def run_test(): async def run_test():
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1") wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
await migrate_databases(wallet1.db, migrations) await migrate_databases(wallet1.db, migrations)
wallet1.load_mint() await wallet1.load_mint()
wallet1.status() wallet1.status()
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2") wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
await migrate_databases(wallet2.db, migrations) await migrate_databases(wallet2.db, migrations)
wallet2.load_mint() await wallet2.load_mint()
wallet2.status() wallet2.status()
proofs = [] proofs = []