mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
uses base models for almost everything
This commit is contained in:
@@ -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
|
||||
|
||||
25
core/base.py
25
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
|
||||
|
||||
22
mint/app.py
22
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}
|
||||
|
||||
12
mint/crud.py
12
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),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user