uses base models for almost everything

This commit is contained in:
callebtc
2022-09-16 14:02:54 +03:00
parent 908977d678
commit 0f12ddea48
7 changed files with 128 additions and 83 deletions

View File

@@ -121,7 +121,7 @@ Balance: 420 sat (Available: 351 sat in 7 tokens)
#### Receive tokens #### Receive tokens
To receive tokens, another user enters: To receive tokens, another user enters:
```bash ```bash
poetry run cashu receive W3siYW1vdW50IjogMSwgIkMiOi... cashu receive W3siYW1vdW50IjogMSwgIkMiOi...
``` ```
You should see the balance increase: You should see the balance increase:
```bash ```bash
@@ -132,7 +132,7 @@ Balance: 69 sat (Available: 69 sat in 3 tokens)
#### Burn 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): 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 ```bash
poetry run cashu burn W3siYW1vdW50IjogMSwgIkMiOi... cashu burn W3siYW1vdW50IjogMSwgIkMiOi...
``` ```
Returns: Returns:
```bash ```bash

View File

@@ -70,16 +70,37 @@ class Invoice(BaseModel):
) )
class MintPayload(BaseModel): class BlindedMessage(BaseModel):
amount: int amount: int
B_: BasePoint 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): class MintPayloads(BaseModel):
payloads: List[MintPayload] = [] blinded_messages: List[BlindedMessage] = []
class SplitPayload(BaseModel): class SplitPayload(BaseModel):
proofs: List[Proof] proofs: List[Proof]
amount: int amount: int
output_data: MintPayloads output_data: MintPayloads
class MeltPayload(BaseModel):
proofs: List[Proof]
amount: int
invoice: str

View File

@@ -10,7 +10,7 @@ from fastapi import FastAPI
from loguru import logger from loguru import logger
import core.settings as settings 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 core.settings import MINT_PRIVATE_KEY, MINT_SERVER_HOST, MINT_SERVER_PORT
from lightning import WALLET from lightning import WALLET
from mint.ledger import Ledger from mint.ledger import Ledger
@@ -123,7 +123,7 @@ async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None):
""" """
amounts = [] amounts = []
B_s = [] B_s = []
for payload in payloads.payloads: for payload in payloads.blinded_messages:
v = payload.dict() v = payload.dict()
amounts.append(v["amount"]) amounts.append(v["amount"])
x = int(v["B_"]["x"]) x = int(v["B_"]["x"])
@@ -137,12 +137,22 @@ async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None):
return {"error": str(exc)} 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") @app.post("/split")
async def split(payload: SplitPayload): async def split(payload: SplitPayload):
v = payload.dict() """
proofs = v["proofs"] Requetst a set of tokens with amount "total" to be split into two
amount = v["amount"] newly minted sets with amount "split" and "total-split".
output_data = v["output_data"]["payloads"] """
proofs = payload.proofs
amount = payload.amount
output_data = payload.output_data.blinded_messages
try: try:
fst_promises, snd_promises = await ledger.split(proofs, amount, output_data) fst_promises, snd_promises = await ledger.split(proofs, amount, output_data)
return {"fst": fst_promises, "snd": snd_promises} return {"fst": fst_promises, "snd": snd_promises}

View File

@@ -1,7 +1,7 @@
import secrets import secrets
from typing import Optional from typing import Optional
from core.base import Invoice from core.base import Invoice, Proof
from core.db import Connection, Database from core.db import Connection, Database
@@ -45,7 +45,7 @@ async def get_proofs_used(
async def invalidate_proof( async def invalidate_proof(
proof: dict, proof: Proof,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ):
@@ -58,10 +58,10 @@ async def invalidate_proof(
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
""", """,
( (
proof["amount"], proof.amount,
str(proof["C"]["x"]), str(proof.C.x),
str(proof["C"]["y"]), str(proof.C.y),
str(proof["secret"]), str(proof.secret),
), ),
) )

View File

@@ -7,15 +7,23 @@ import hashlib
from ecc.curve import Point, secp256k1 from ecc.curve import Point, secp256k1
from ecc.key import gen_keypair 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 import core.b_dhke as b_dhke
from core.base import Invoice from core.base import Invoice
from core.db import Database from core.db import Database
from core.settings import MAX_ORDER from core.settings import MAX_ORDER
from core.split import amount_split from core.split import amount_split
from lightning import WALLET from lightning import WALLET
from mint.crud import (get_lightning_invoice, get_proofs_used, from mint.crud import (
invalidate_proof, store_lightning_invoice, get_lightning_invoice,
store_promise, update_lightning_invoice) get_proofs_used,
invalidate_proof,
store_lightning_invoice,
store_promise,
update_lightning_invoice,
)
class Ledger: class Ledger:
@@ -53,39 +61,39 @@ class Ledger:
async def _generate_promises(self, amounts, B_s): async def _generate_promises(self, amounts, B_s):
"""Generates promises that sum to the given amount.""" """Generates promises that sum to the given amount."""
return [ 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) 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').""" """Generates a promise for given amount and returns a pair (amount, C')."""
secret_key = self.keys[amount] # Get the correct key secret_key = self.keys[amount] # Get the correct key
C_ = b_dhke.step2_alice(B_, secret_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) 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.""" """Verifies that the proof of promise was issued by this ledger."""
if proof["secret"] in self.proofs_used: if proof.secret in self.proofs_used:
raise Exception(f"tokens already spent. Secret: {proof['secret']}") 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.keys[proof.amount] # Get the correct key to check against
C = Point(proof["C"]["x"], proof["C"]["y"], secp256k1) C = Point(proof.C.x, proof.C.y, secp256k1)
return b_dhke.verify(secret_key, C, proof["secret"]) 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""" """Verifies the expected split was correctly computed"""
fst_amt, snd_amt = total - amount, amount # we have two amounts to split to fst_amt, snd_amt = total - amount, amount # we have two amounts to split to
fst_outputs = amount_split(fst_amt) fst_outputs = amount_split(fst_amt)
snd_outputs = amount_split(snd_amt) snd_outputs = amount_split(snd_amt)
expected = fst_outputs + snd_outputs expected = fst_outputs + snd_outputs
given = [o["amount"] for o in output_data] given = [o.amount for o in output_data]
return given == expected return given == expected
def _verify_no_duplicates(self, proofs, output_data): def _verify_no_duplicates(self, proofs: List[Proof], output_data):
secrets = [p["secret"] for p in proofs] secrets = [p.secret for p in proofs]
if len(secrets) != len(list(set(secrets))): if len(secrets) != len(list(set(secrets))):
return False 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))): if len(B_xs) != len(list(set(B_xs))):
return False return False
return True return True
@@ -105,10 +113,12 @@ class Ledger:
raise Exception("invalid amount: " + str(amount)) raise Exception("invalid amount: " + str(amount))
return 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.""" """Verify that Σoutputs - Σinputs = 0."""
sum_inputs = sum(self._verify_amount(p["amount"]) for p in proofs) 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_outputs = sum(self._verify_amount(p.amount) for p in outs)
assert sum_outputs - sum_inputs == 0 assert sum_outputs - sum_inputs == 0
def _get_output_split(self, amount): def _get_output_split(self, amount):
@@ -122,6 +132,7 @@ class Ledger:
return rv return rv
async def _request_lightning_invoice(self, amount): async def _request_lightning_invoice(self, amount):
"""Returns an invoice from the Lightning backend."""
error, balance = await WALLET.status() error, balance = await WALLET.status()
if error: if error:
raise Exception(f"Lightning wallet not responding: {error}") raise Exception(f"Lightning wallet not responding: {error}")
@@ -131,6 +142,7 @@ class Ledger:
return payment_request, checking_id return payment_request, checking_id
async def _check_lightning_invoice(self, payment_hash): 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) invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
if invoice.issued: if invoice.issued:
raise Exception("tokens already issued for this invoice") 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) await update_lightning_invoice(payment_hash, issued=True, db=self.db)
return status.paid return status.paid
# async def _wait_for_lightning_invoice(self, amount): async def _invalidate_proofs(self, proofs: List[Proof]):
# timeout = time.time() + 60 # 1 minute to pay invoice """Adds secrets of proofs to the list of knwon secrets and stores them in the db."""
# while True: # Mark proofs as used and prepare new promises
# status = await WALLET.get_invoice_status(checking_id) proof_msgs = set([p.secret for p in proofs])
# if status.pending and time.time() > timeout: self.proofs_used |= proof_msgs
# print("Timeout") # store in db
# return False for p in proofs:
# if not status.pending: await invalidate_proof(p, db=self.db)
# print("paid")
# return True
# time.sleep(5)
# Public methods # Public methods
def get_pubkeys(self): def get_pubkeys(self):
@@ -183,7 +192,9 @@ class Ledger:
promises += [await self._generate_promise(amount, B_) for a in split] promises += [await self._generate_promise(amount, B_) for a in split]
return promises 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.""" """Consumes proofs and prepares new promises based on the amount split."""
self._verify_split_amount(amount) self._verify_split_amount(amount)
# Verify proofs are valid # Verify proofs are valid
@@ -199,19 +210,13 @@ class Ledger:
if not self._verify_outputs(total, amount, output_data): if not self._verify_outputs(total, amount, output_data):
raise Exception("split of promises is not as expected") 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 # Mark proofs as used and prepare new promises
self.proofs_used |= proof_msgs await self._invalidate_proofs(proofs)
# store in db
for p in proofs:
await invalidate_proof(p, db=self.db)
outs_fst = amount_split(total - amount) outs_fst = amount_split(total - amount)
outs_snd = amount_split(amount) outs_snd = amount_split(amount)
B_fst = [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) :]] B_snd = [od.B_ for od in output_data[len(outs_fst) :]]
prom_fst, prom_snd = await self._generate_promises( prom_fst, prom_snd = await self._generate_promises(
outs_fst, B_fst outs_fst, B_fst
), await self._generate_promises(outs_snd, B_snd) ), await self._generate_promises(outs_snd, B_snd)

View File

@@ -1,7 +1,10 @@
import asyncio import asyncio
from core.helpers import async_unwrap 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 Wallet1
from wallet.wallet import Wallet as Wallet2 from wallet.wallet import Wallet as Wallet2
@@ -25,11 +28,11 @@ 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 m001_initial(wallet1.db) await migrate_databases(wallet1.db, migrations)
wallet1.status() wallet1.status()
wallet2 = Wallet1(SERVER_ENDPOINT, "data/wallet2", "wallet2") wallet2 = Wallet1(SERVER_ENDPOINT, "data/wallet2", "wallet2")
await m001_initial(wallet2.db) await migrate_databases(wallet2.db, migrations)
wallet2.status() wallet2.status()
proofs = [] proofs = []

View File

@@ -5,7 +5,14 @@ import requests
from ecc.curve import Point, secp256k1 from ecc.curve import Point, secp256k1
import core.b_dhke as b_dhke 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.db import Database
from core.split import amount_split from core.split import amount_split
from wallet.crud import get_proofs, invalidate_proof, store_proof, update_proof_reserved from wallet.crud import get_proofs, invalidate_proof, store_proof, update_proof_reserved
@@ -33,14 +40,14 @@ class LedgerAPI:
rv.append(2**pos) rv.append(2**pos)
return rv return rv
def _construct_proofs(self, promises, secrets): def _construct_proofs(self, promises: List[BlindedSignature], secrets: List[str]):
"""Returns proofs of promise from promises.""" """Returns proofs of promise from promises."""
proofs = [] proofs = []
for promise, (r, secret) in zip(promises, secrets): for promise, (r, secret) in zip(promises, secrets):
C_ = Point(promise["C'"]["x"], promise["C'"]["y"], secp256k1) C_ = Point(promise.C_.x, promise.C_.y, secp256k1)
C = b_dhke.step3_bob(C_, r, self.keys[promise["amount"]]) C = b_dhke.step3_bob(C_, r, self.keys[promise.amount])
c_point = BasePoint(x=C.x, y=C.y) 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) proofs.append(proof)
return proofs return proofs
@@ -60,15 +67,16 @@ class LedgerAPI:
B_, r = b_dhke.step1_bob(secret) B_, r = b_dhke.step1_bob(secret)
rs.append(r) rs.append(r)
blinded_point = BasePoint(x=str(B_.x), y=str(B_.y)) blinded_point = BasePoint(x=str(B_.x), y=str(B_.y))
payload: MintPayload = MintPayload(amount=amount, B_=blinded_point) payload: BlindedMessage = BlindedMessage(amount=amount, B_=blinded_point)
payloads.payloads.append(payload) payloads.blinded_messages.append(payload)
promises = requests.post( promises_dict = requests.post(
self.url + "/mint", self.url + "/mint",
json=payloads.dict(), json=payloads.dict(),
params={"payment_hash": payment_hash}, params={"payment_hash": payment_hash},
).json() ).json()
if "error" in promises: if "error" in promises_dict:
raise Exception("Error: {}".format(promises["error"])) 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)]) return self._construct_proofs(promises, [(r, s) for r, s in zip(rs, secrets)])
def split(self, proofs, amount): def split(self, proofs, amount):
@@ -79,30 +87,28 @@ class LedgerAPI:
snd_outputs = amount_split(snd_amt) snd_outputs = amount_split(snd_amt)
secrets = [] secrets = []
# output_data = []
payloads: MintPayloads = MintPayloads() payloads: MintPayloads = MintPayloads()
for output_amt in fst_outputs + snd_outputs: for output_amt in fst_outputs + snd_outputs:
secret = str(random.getrandbits(128)) secret = str(random.getrandbits(128))
B_, r = b_dhke.step1_bob(secret) B_, r = b_dhke.step1_bob(secret)
secrets.append((r, secret)) secrets.append((r, secret))
blinded_point = BasePoint(x=str(B_.x), y=str(B_.y)) blinded_point = BasePoint(x=str(B_.x), y=str(B_.y))
payload: MintPayload = MintPayload(amount=output_amt, B_=blinded_point) payload: BlindedMessage = BlindedMessage(
payloads.payloads.append(payload) amount=output_amt, B_=blinded_point
)
payloads.blinded_messages.append(payload)
split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads) split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads)
promises = requests.post( promises_dict = requests.post(
self.url + "/split", self.url + "/split",
json=split_payload.dict(), json=split_payload.dict(),
).json() ).json()
if "error" in promises: if "error" in promises_dict:
raise Exception("Error: {}".format(promises["error"])) 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 # Obtain proofs from promises
fst_proofs = self._construct_proofs( fst_proofs = self._construct_proofs(promises_fst, secrets[: len(promises_fst)])
promises["fst"], secrets[: len(promises["fst"])] snd_proofs = self._construct_proofs(promises_snd, secrets[len(promises_fst) :])
)
snd_proofs = self._construct_proofs(
promises["snd"], secrets[len(promises["fst"]) :]
)
return fst_proofs, snd_proofs return fst_proofs, snd_proofs