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
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

View File

@@ -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

View File

@@ -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}

View File

@@ -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),
),
)

View File

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

View File

@@ -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 = []

View File

@@ -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