From 0f12ddea48b9b94045b1de911403cf75f573dc87 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:02:54 +0300 Subject: [PATCH] uses base models for almost everything --- README.md | 4 +- core/base.py | 25 ++++++++++++- mint/app.py | 22 ++++++++--- mint/crud.py | 12 +++--- mint/ledger.py | 87 +++++++++++++++++++++++--------------------- tests/test_wallet.py | 9 +++-- wallet/wallet.py | 52 ++++++++++++++------------ 7 files changed, 128 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index d8306fa..d6fc25a 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Balance: 420 sat (Available: 351 sat in 7 tokens) #### Receive tokens To receive tokens, another user enters: ```bash -poetry run cashu receive W3siYW1vdW50IjogMSwgIkMiOi... +cashu receive W3siYW1vdW50IjogMSwgIkMiOi... ``` You should see the balance increase: ```bash @@ -132,7 +132,7 @@ Balance: 69 sat (Available: 69 sat in 3 tokens) #### Burn tokens The sending user needs to burn (invalidate) their tokens from above, otherwise they will try to double spend them (which won't work because the server keeps a list of all spent tokens): ```bash -poetry run cashu burn W3siYW1vdW50IjogMSwgIkMiOi... +cashu burn W3siYW1vdW50IjogMSwgIkMiOi... ``` Returns: ```bash diff --git a/core/base.py b/core/base.py index c213bb8..3e717a0 100644 --- a/core/base.py +++ b/core/base.py @@ -70,16 +70,37 @@ class Invoice(BaseModel): ) -class MintPayload(BaseModel): +class BlindedMessage(BaseModel): amount: int B_: BasePoint +class BlindedSignature(BaseModel): + amount: int + C_: BasePoint + + @classmethod + def from_dict(cls, d: dict): + return cls( + amount=d["amount"], + C_=dict( + x=int(d["C_"]["x"]), + y=int(d["C_"]["y"]), + ), + ) + + class MintPayloads(BaseModel): - payloads: List[MintPayload] = [] + blinded_messages: List[BlindedMessage] = [] class SplitPayload(BaseModel): proofs: List[Proof] amount: int output_data: MintPayloads + + +class MeltPayload(BaseModel): + proofs: List[Proof] + amount: int + invoice: str diff --git a/mint/app.py b/mint/app.py index d290751..5aa4318 100644 --- a/mint/app.py +++ b/mint/app.py @@ -10,7 +10,7 @@ from fastapi import FastAPI from loguru import logger import core.settings as settings -from core.base import MintPayloads, SplitPayload +from core.base import MintPayloads, SplitPayload, MeltPayload, Proof from core.settings import MINT_PRIVATE_KEY, MINT_SERVER_HOST, MINT_SERVER_PORT from lightning import WALLET from mint.ledger import Ledger @@ -123,7 +123,7 @@ async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None): """ amounts = [] B_s = [] - for payload in payloads.payloads: + for payload in payloads.blinded_messages: v = payload.dict() amounts.append(v["amount"]) x = int(v["B_"]["x"]) @@ -137,12 +137,22 @@ async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None): return {"error": str(exc)} +@app.post("/melt") +async def melt(payload: MeltPayload): + """ + Requests tokens to be destroyed and sent out via Lightning. + """ + + @app.post("/split") async def split(payload: SplitPayload): - v = payload.dict() - proofs = v["proofs"] - amount = v["amount"] - output_data = v["output_data"]["payloads"] + """ + Requetst a set of tokens with amount "total" to be split into two + newly minted sets with amount "split" and "total-split". + """ + proofs = payload.proofs + amount = payload.amount + output_data = payload.output_data.blinded_messages try: fst_promises, snd_promises = await ledger.split(proofs, amount, output_data) return {"fst": fst_promises, "snd": snd_promises} diff --git a/mint/crud.py b/mint/crud.py index d53fe10..26f72b6 100644 --- a/mint/crud.py +++ b/mint/crud.py @@ -1,7 +1,7 @@ import secrets from typing import Optional -from core.base import Invoice +from core.base import Invoice, Proof from core.db import Connection, Database @@ -45,7 +45,7 @@ async def get_proofs_used( async def invalidate_proof( - proof: dict, + proof: Proof, db: Database, conn: Optional[Connection] = None, ): @@ -58,10 +58,10 @@ async def invalidate_proof( VALUES (?, ?, ?, ?) """, ( - proof["amount"], - str(proof["C"]["x"]), - str(proof["C"]["y"]), - str(proof["secret"]), + proof.amount, + str(proof.C.x), + str(proof.C.y), + str(proof.secret), ), ) diff --git a/mint/ledger.py b/mint/ledger.py index 36fa5a1..b0d3795 100644 --- a/mint/ledger.py +++ b/mint/ledger.py @@ -7,15 +7,23 @@ import hashlib from ecc.curve import Point, secp256k1 from ecc.key import gen_keypair +from typing import List +from core.base import Proof, BlindedMessage, BlindedSignature, BasePoint + import core.b_dhke as b_dhke from core.base import Invoice from core.db import Database from core.settings import MAX_ORDER from core.split import amount_split from lightning import WALLET -from mint.crud import (get_lightning_invoice, get_proofs_used, - invalidate_proof, store_lightning_invoice, - store_promise, update_lightning_invoice) +from mint.crud import ( + get_lightning_invoice, + get_proofs_used, + invalidate_proof, + store_lightning_invoice, + store_promise, + update_lightning_invoice, +) class Ledger: @@ -53,39 +61,39 @@ class Ledger: async def _generate_promises(self, amounts, B_s): """Generates promises that sum to the given amount.""" return [ - await self._generate_promise(amount, Point(B_["x"], B_["y"], secp256k1)) + await self._generate_promise(amount, Point(B_.x, B_.y, secp256k1)) for (amount, B_) in zip(amounts, B_s) ] - async def _generate_promise(self, amount, B_): + async def _generate_promise(self, amount: int, B_): """Generates a promise for given amount and returns a pair (amount, C').""" secret_key = self.keys[amount] # Get the correct key C_ = b_dhke.step2_alice(B_, secret_key) await store_promise(amount, B_x=B_.x, B_y=B_.y, C_x=C_.x, C_y=C_.y, db=self.db) - return {"amount": amount, "C'": C_} + return BlindedSignature(amount=amount, C_=BasePoint(x=C_.x, y=C_.y)) - def _verify_proof(self, proof): + def _verify_proof(self, proof: Proof): """Verifies that the proof of promise was issued by this ledger.""" - if proof["secret"] in self.proofs_used: - raise Exception(f"tokens already spent. Secret: {proof['secret']}") - secret_key = self.keys[proof["amount"]] # Get the correct key to check against - C = Point(proof["C"]["x"], proof["C"]["y"], secp256k1) - return b_dhke.verify(secret_key, C, proof["secret"]) + if proof.secret in self.proofs_used: + raise Exception(f"tokens already spent. Secret: {proof.secret}") + secret_key = self.keys[proof.amount] # Get the correct key to check against + C = Point(proof.C.x, proof.C.y, secp256k1) + return b_dhke.verify(secret_key, C, proof.secret) - def _verify_outputs(self, total, amount, output_data): + def _verify_outputs(self, total: int, amount: int, output_data): """Verifies the expected split was correctly computed""" fst_amt, snd_amt = total - amount, amount # we have two amounts to split to fst_outputs = amount_split(fst_amt) snd_outputs = amount_split(snd_amt) expected = fst_outputs + snd_outputs - given = [o["amount"] for o in output_data] + given = [o.amount for o in output_data] return given == expected - def _verify_no_duplicates(self, proofs, output_data): - secrets = [p["secret"] for p in proofs] + def _verify_no_duplicates(self, proofs: List[Proof], output_data): + secrets = [p.secret for p in proofs] if len(secrets) != len(list(set(secrets))): return False - B_xs = [od["B_"]["x"] for od in output_data] + B_xs = [od.B_.x for od in output_data] if len(B_xs) != len(list(set(B_xs))): return False return True @@ -105,10 +113,12 @@ class Ledger: raise Exception("invalid amount: " + str(amount)) return amount - def _verify_equation_balanced(self, proofs, outs): + def _verify_equation_balanced( + self, proofs: List[Proof], outs: List[BlindedMessage] + ): """Verify that Σoutputs - Σinputs = 0.""" - sum_inputs = sum(self._verify_amount(p["amount"]) for p in proofs) - sum_outputs = sum(self._verify_amount(p["amount"]) for p in outs) + sum_inputs = sum(self._verify_amount(p.amount) for p in proofs) + sum_outputs = sum(self._verify_amount(p.amount) for p in outs) assert sum_outputs - sum_inputs == 0 def _get_output_split(self, amount): @@ -122,6 +132,7 @@ class Ledger: return rv async def _request_lightning_invoice(self, amount): + """Returns an invoice from the Lightning backend.""" error, balance = await WALLET.status() if error: raise Exception(f"Lightning wallet not responding: {error}") @@ -131,6 +142,7 @@ class Ledger: return payment_request, checking_id async def _check_lightning_invoice(self, payment_hash): + """Checks with the Lightning backend whether an invoice with this payment_hash was paid.""" invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db) if invoice.issued: raise Exception("tokens already issued for this invoice") @@ -139,17 +151,14 @@ class Ledger: await update_lightning_invoice(payment_hash, issued=True, db=self.db) return status.paid - # async def _wait_for_lightning_invoice(self, amount): - # timeout = time.time() + 60 # 1 minute to pay invoice - # while True: - # status = await WALLET.get_invoice_status(checking_id) - # if status.pending and time.time() > timeout: - # print("Timeout") - # return False - # if not status.pending: - # print("paid") - # return True - # time.sleep(5) + async def _invalidate_proofs(self, proofs: List[Proof]): + """Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" + # Mark proofs as used and prepare new promises + proof_msgs = set([p.secret for p in proofs]) + self.proofs_used |= proof_msgs + # store in db + for p in proofs: + await invalidate_proof(p, db=self.db) # Public methods def get_pubkeys(self): @@ -183,7 +192,9 @@ class Ledger: promises += [await self._generate_promise(amount, B_) for a in split] return promises - async def split(self, proofs, amount, output_data): + async def split( + self, proofs: List[Proof], amount: int, output_data: List[BlindedMessage] + ): """Consumes proofs and prepares new promises based on the amount split.""" self._verify_split_amount(amount) # Verify proofs are valid @@ -199,19 +210,13 @@ class Ledger: if not self._verify_outputs(total, amount, output_data): raise Exception("split of promises is not as expected") - # Perform split - proof_msgs = set([p["secret"] for p in proofs]) # Mark proofs as used and prepare new promises - self.proofs_used |= proof_msgs - - # store in db - for p in proofs: - await invalidate_proof(p, db=self.db) + await self._invalidate_proofs(proofs) outs_fst = amount_split(total - amount) outs_snd = amount_split(amount) - B_fst = [od["B_"] for od in output_data[: len(outs_fst)]] - B_snd = [od["B_"] for od in output_data[len(outs_fst) :]] + B_fst = [od.B_ for od in output_data[: len(outs_fst)]] + B_snd = [od.B_ for od in output_data[len(outs_fst) :]] prom_fst, prom_snd = await self._generate_promises( outs_fst, B_fst ), await self._generate_promises(outs_snd, B_snd) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 107269b..c5cd4bd 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1,7 +1,10 @@ import asyncio from core.helpers import async_unwrap -from wallet.migrations import m001_initial + +from core.migrations import migrate_databases +from wallet import migrations + from wallet.wallet import Wallet as Wallet1 from wallet.wallet import Wallet as Wallet2 @@ -25,11 +28,11 @@ def assert_amt(proofs, expected): async def run_test(): wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1") - await m001_initial(wallet1.db) + await migrate_databases(wallet1.db, migrations) wallet1.status() wallet2 = Wallet1(SERVER_ENDPOINT, "data/wallet2", "wallet2") - await m001_initial(wallet2.db) + await migrate_databases(wallet2.db, migrations) wallet2.status() proofs = [] diff --git a/wallet/wallet.py b/wallet/wallet.py index 616a581..81621c2 100644 --- a/wallet/wallet.py +++ b/wallet/wallet.py @@ -5,7 +5,14 @@ import requests from ecc.curve import Point, secp256k1 import core.b_dhke as b_dhke -from core.base import BasePoint, MintPayload, MintPayloads, Proof, SplitPayload +from core.base import ( + BasePoint, + BlindedMessage, + MintPayloads, + Proof, + SplitPayload, + BlindedSignature, +) from core.db import Database from core.split import amount_split from wallet.crud import get_proofs, invalidate_proof, store_proof, update_proof_reserved @@ -33,14 +40,14 @@ class LedgerAPI: rv.append(2**pos) return rv - def _construct_proofs(self, promises, secrets): + def _construct_proofs(self, promises: List[BlindedSignature], secrets: List[str]): """Returns proofs of promise from promises.""" proofs = [] for promise, (r, secret) in zip(promises, secrets): - C_ = Point(promise["C'"]["x"], promise["C'"]["y"], secp256k1) - C = b_dhke.step3_bob(C_, r, self.keys[promise["amount"]]) + C_ = Point(promise.C_.x, promise.C_.y, secp256k1) + C = b_dhke.step3_bob(C_, r, self.keys[promise.amount]) c_point = BasePoint(x=C.x, y=C.y) - proof = Proof(amount=promise["amount"], C=c_point, secret=secret) + proof = Proof(amount=promise.amount, C=c_point, secret=secret) proofs.append(proof) return proofs @@ -60,15 +67,16 @@ class LedgerAPI: B_, r = b_dhke.step1_bob(secret) rs.append(r) blinded_point = BasePoint(x=str(B_.x), y=str(B_.y)) - payload: MintPayload = MintPayload(amount=amount, B_=blinded_point) - payloads.payloads.append(payload) - promises = requests.post( + payload: BlindedMessage = BlindedMessage(amount=amount, B_=blinded_point) + payloads.blinded_messages.append(payload) + promises_dict = requests.post( self.url + "/mint", json=payloads.dict(), params={"payment_hash": payment_hash}, ).json() - if "error" in promises: - raise Exception("Error: {}".format(promises["error"])) + if "error" in promises_dict: + raise Exception("Error: {}".format(promises_dict["error"])) + promises = [BlindedSignature.from_dict(p) for p in promises_dict] return self._construct_proofs(promises, [(r, s) for r, s in zip(rs, secrets)]) def split(self, proofs, amount): @@ -79,30 +87,28 @@ class LedgerAPI: snd_outputs = amount_split(snd_amt) secrets = [] - # output_data = [] payloads: MintPayloads = MintPayloads() for output_amt in fst_outputs + snd_outputs: secret = str(random.getrandbits(128)) B_, r = b_dhke.step1_bob(secret) secrets.append((r, secret)) blinded_point = BasePoint(x=str(B_.x), y=str(B_.y)) - payload: MintPayload = MintPayload(amount=output_amt, B_=blinded_point) - payloads.payloads.append(payload) + payload: BlindedMessage = BlindedMessage( + amount=output_amt, B_=blinded_point + ) + payloads.blinded_messages.append(payload) split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads) - promises = requests.post( + promises_dict = requests.post( self.url + "/split", json=split_payload.dict(), ).json() - if "error" in promises: - raise Exception("Error: {}".format(promises["error"])) - + if "error" in promises_dict: + raise Exception("Error: {}".format(promises_dict["error"])) + promises_fst = [BlindedSignature.from_dict(p) for p in promises_dict["fst"]] + promises_snd = [BlindedSignature.from_dict(p) for p in promises_dict["snd"]] # Obtain proofs from promises - fst_proofs = self._construct_proofs( - promises["fst"], secrets[: len(promises["fst"])] - ) - snd_proofs = self._construct_proofs( - promises["snd"], secrets[len(promises["fst"]) :] - ) + fst_proofs = self._construct_proofs(promises_fst, secrets[: len(promises_fst)]) + snd_proofs = self._construct_proofs(promises_snd, secrets[len(promises_fst) :]) return fst_proofs, snd_proofs