This commit is contained in:
callebtc
2022-09-13 21:36:18 +03:00
parent 075fb57093
commit f2228e6a38
16 changed files with 251 additions and 131 deletions

31
cashu
View File

@@ -1,17 +1,16 @@
#!/usr/bin/env python
from wallet.wallet import Wallet as Wallet
from wallet.migrations import m001_initial
import asyncio
import click
import json
import base64
from bech32 import bech32_encode, bech32_decode, convertbits
import asyncio
import json
from functools import wraps
import click
from bech32 import bech32_decode, bech32_encode, convertbits
from wallet.migrations import m001_initial
from wallet.wallet import Wallet as Wallet
# https://github.com/pallets/click/issues/85#issuecomment-503464628
def coro(f):
@@ -23,20 +22,26 @@ def coro(f):
@click.command("mint")
@click.option("--host", default="http://localhost:3338", help="Mint tokens.")
@click.option("--wallet", default="wallet", help="Mint tokens.")
@click.option("--host", default="http://localhost:3338", help="Mint hostname.")
@click.option("--wallet", default="wallet", help="Wallet to use.")
@click.option("--mint", default=0, help="Mint tokens.")
@click.option("--hash", default="", help="Hash of the paid invoice.")
@click.option("--send", default=0, help="Send tokens.")
@click.option("--receive", default="", help="Receive tokens.")
@click.option("--invalidate", default="", help="Invalidate tokens.")
@coro
async def main(host, wallet, mint, send, receive, invalidate):
async def main(host, wallet, mint, hash, send, receive, invalidate):
wallet = Wallet(host, f"data/{wallet}", wallet)
await m001_initial(db=wallet.db)
await wallet.load_proofs()
if mint:
if mint and not hash:
print(f"Balance: {wallet.balance}")
await wallet.mint(mint)
r = await wallet.request_mint(mint)
print(r)
if mint and hash:
print(f"Balance: {wallet.balance}")
await wallet.mint(mint, hash)
print(f"Balance: {wallet.balance}")
if send:

View File

@@ -30,9 +30,9 @@ If true, C must have originated from Alice
"""
import hashlib
from ecc.curve import secp256k1, Point
from ecc.key import gen_keypair
from ecc.curve import Point, secp256k1
from ecc.key import gen_keypair
G = secp256k1.G

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel
from typing import List
from sqlite3 import Row
from typing import List
from pydantic import BaseModel
class BasePoint(BaseModel):
@@ -27,6 +28,22 @@ class Proof(BaseModel):
)
class Invoice(BaseModel):
amount: int
pr: str
hash: str
issued: bool = False
@classmethod
def from_row(cls, row: Row):
return cls(
amount=int(row[0]),
pr=str(row[1]),
hash=str(row[2]),
issued=bool(row[3]),
)
class MintPayload(BaseModel):
amount: int
B_: BasePoint

View File

@@ -9,7 +9,6 @@ from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH"
SQLITE = "SQLITE"

View File

@@ -5,10 +5,15 @@ env.read_env()
DEBUG = env.bool("DEBUG", default=False)
MINT_PRIVATE_KEY = env.str("MINT_PRIVATE_KEY")
MINT_SERVER_HOST = env.str("MINT_SERVER_HOST", default="127.0.0.1")
MINT_SERVER_PORT = env.int("MINT_SERVER_PORT", default=3338)
MINT_HOST = env.str("MINT_HOST", default="127.0.0.1")
MINT_PORT = env.int("MINT_PORT", default=3338)
LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
MAX_ORDER = 64

View File

@@ -6,22 +6,19 @@ from typing import AsyncGenerator, Dict, Optional
import requests
from .base import (
InvoiceResponse,
PaymentResponse,
PaymentStatus,
StatusResponse,
Wallet,
)
from core.settings import LNBITS_ENDPOINT, LNBITS_KEY
from .base import (InvoiceResponse, PaymentResponse, PaymentStatus,
StatusResponse, Wallet)
class LNbitsWallet(Wallet):
"""https://github.com/lnbits/lnbits"""
def __init__(self):
self.endpoint = getenv("LNBITS_ENDPOINT")
self.endpoint = LNBITS_ENDPOINT
key = getenv("LNBITS_KEY")
key = LNBITS_KEY
self.key = {"X-Api-Key": key}
self.s = requests.Session()
self.s.auth = ("user", "pass")

View File

@@ -1,58 +1,23 @@
from ecc.curve import secp256k1, Point
from fastapi import FastAPI
from fastapi.routing import APIRouter
from fastapi.params import Depends, Query, Body
import sys
import asyncio
import logging
import uvicorn
import sys
from ast import Param
from typing import Union
from ecc.curve import Point, secp256k1
from fastapi import FastAPI
from fastapi.params import Body, Depends, Query
from fastapi.routing import APIRouter
from loguru import logger
import core.settings as settings
from core.base import MintPayloads, SplitPayload
from core.settings import MINT_PRIVATE_KEY
from lightning import WALLET
from mint.ledger import Ledger
from mint.migrations import m001_initial
from lightning import WALLET
import core.settings as settings
from core.base import MintPayload, MintPayloads, SplitPayload
# from .app import create_app
# Ledger pubkey
ledger = Ledger("supersecretprivatekey", "data/mint")
# class MyFlaskApp(Flask):
# """
# We overload the Flask class so we can run a startup script (migration).
# Stupid Flask.
# """
# def __init__(self, *args, **kwargs):
# async def create_tasks_func():
# await asyncio.wait([m001_initial(ledger.db)])
# await ledger.load_used_proofs()
# error_message, balance = await WALLET.status()
# if error_message:
# print(
# f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
# RuntimeWarning,
# )
# print(f"Lightning balance: {balance} sat")
# print("Mint started.")
# loop = asyncio.get_event_loop()
# loop.run_until_complete(create_tasks_func())
# loop.close()
# return super().__init__(*args, **kwargs)
# def run(self, *args, **options):
# super(MyFlaskApp, self).run(*args, **options)
ledger = Ledger(MINT_PRIVATE_KEY, "data/mint")
def startup(app: FastAPI):
@@ -124,18 +89,39 @@ def create_app(config_object="core.settings") -> FastAPI:
app = create_app()
@app.get("/")
async def root():
return {"Hello": "world"}
@app.get("/keys")
def keys():
"""Get the public keys of the mint"""
return ledger.get_pubkeys()
@app.get("/mint")
async def request_mint(amount: int = 0):
"""Request minting of tokens. Server responds with a Lightning invoice."""
payment_request, payment_hash = await ledger.request_mint(amount)
print(f"Lightning invoice: {payment_request}")
return {"pr": payment_request, "hash": payment_hash}
@app.post("/mint")
async def mint(payloads: MintPayloads):
async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None):
"""
Requests the minting of tokens belonging to a paid payment request.
Parameters:
pr: payment_request of the Lightning paid invoice.
Body (JSON):
payloads: contains a list of blinded messages waiting to be signed.
NOTE:
- This needs to be replaced by the preimage otherwise someone knowing
the payment_request can request the tokens instead of the rightful
owner.
- The blinded message should ideally be provided to the server *before* payment
in the GET /mint endpoint so that the server knows to sign only these tokens
when the invoice is paid.
"""
amounts = []
B_s = []
for payload in payloads.payloads:
@@ -146,7 +132,7 @@ async def mint(payloads: MintPayloads):
B_ = Point(x, y, secp256k1)
B_s.append(B_)
try:
promises = await ledger.mint(B_s, amounts)
promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash)
return promises
except Exception as exc:
return {"error": str(exc)}

View File

@@ -1,5 +1,7 @@
import secrets
from typing import Optional
from core.base import Invoice
from core.db import Connection, Database
@@ -62,3 +64,52 @@ async def invalidate_proof(
str(proof["secret"]),
),
)
async def store_lightning_invoice(
invoice: Invoice,
db: Database,
conn: Optional[Connection] = None,
):
await (conn or db).execute(
"""
INSERT INTO invoices
(amount, pr, hash, issued)
VALUES (?, ?, ?, ?)
""",
(
invoice.amount,
invoice.pr,
invoice.hash,
invoice.issued,
),
)
async def get_lightning_invoice(
hash: str,
db: Database,
conn: Optional[Connection] = None,
):
row = await (conn or db).fetchone(
"""
SELECT * from invoices
WHERE hash = ?
""",
hash,
)
return Invoice.from_row(row)
async def update_lightning_invoice(
hash: str,
issued: bool,
db: Database,
conn: Optional[Connection] = None,
):
await (conn or db).execute(
"UPDATE invoices SET issued = ? WHERE hash = ?",
(issued, hash),
)

View File

@@ -5,15 +5,18 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c
import hashlib
import time
from ecc.curve import secp256k1, Point
from ecc.curve import Point, secp256k1
from ecc.key import gen_keypair
import core.b_dhke as b_dhke
from core.base import Invoice
from core.db import Database
from core.split import amount_split
from core.settings import MAX_ORDER
from mint.crud import store_promise, invalidate_proof, get_proofs_used
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)
class Ledger:
@@ -65,7 +68,7 @@ class Ledger:
def _verify_proof(self, proof):
"""Verifies that the proof of promise was issued by this ledger."""
if proof["secret"] in self.proofs_used:
raise Exception(f"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
C = Point(proof["C"]["x"], proof["C"]["y"], secp256k1)
return b_dhke.verify(secret_key, C, proof["secret"])
@@ -94,13 +97,13 @@ class Ledger:
self._verify_amount(amount)
except:
# For better error message
raise Exception("Invalid split amount: " + str(amount))
raise Exception("invalid split amount: " + str(amount))
def _verify_amount(self, amount):
"""Any amount used should be a positive integer not larger than 2^MAX_ORDER."""
valid = isinstance(amount, int) and amount > 0 and amount < 2**MAX_ORDER
if not valid:
raise Exception("Invalid amount: " + str(amount))
raise Exception("invalid amount: " + str(amount))
return amount
def _verify_equation_balanced(self, proofs, outs):
@@ -119,42 +122,62 @@ class Ledger:
rv.append(2**pos)
return rv
async def _request_lightning(self, amount):
async def _request_lightning_invoice(self, amount):
error, balance = await WALLET.status()
if error:
raise Exception(f"Lightning wallet not responding: {error}")
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
amount, "cashu deposit"
)
print(payment_request)
return payment_request, checking_id
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 _check_lightning_invoice(self, payment_hash):
invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
if invoice.issued:
raise Exception("tokens already issued for this invoice")
status = await WALLET.get_invoice_status(payment_hash)
if status.paid:
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)
# Public methods
def get_pubkeys(self):
"""Returns public keys for possible amounts."""
return self.pub_keys
async def mint(self, B_s, amounts, lightning=False):
async def request_mint(self, amount):
"""Returns Lightning invoice and stores it in the db."""
payment_request, checking_id = await self._request_lightning_invoice(amount)
invoice = Invoice(
amount=amount, pr=payment_request, hash=checking_id, issued=False
)
if not payment_request or not checking_id:
raise Exception(f"Could not create Lightning invoice.")
await store_lightning_invoice(invoice, db=self.db)
return payment_request, checking_id
async def mint(self, B_s, amounts, payment_hash=None):
"""Mints a promise for coins for B_."""
# check if lightning invoice was paid
if payment_hash and not await self._check_lightning_invoice(payment_hash):
raise Exception("Lightning invoice not paid yet.")
for amount in amounts:
if amount not in [2**i for i in range(MAX_ORDER)]:
raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.")
if lightning:
paid = await self._request_lightning(sum(amounts))
if not paid:
raise Exception(f"Did not receive payment in time.")
promises = []
for B_, amount in zip(B_s, amounts):
split = amount_split(amount)

View File

@@ -33,6 +33,20 @@ async def m001_initial(db: Database):
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS invoices (
amount INTEGER NOT NULL,
pr TEXT NOT NULL,
hash TEXT NOT NULL,
issued BOOL NOT NULL,
UNIQUE (hash)
);
"""
)
await db.execute(
"""
CREATE VIEW IF NOT EXISTS balance_issued AS

20
poetry.lock generated
View File

@@ -237,6 +237,20 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "isort"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.6.1,<4.0"
[package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "itsdangerous"
version = "2.1.1"
@@ -641,7 +655,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "ed00e50dd88910eebcd5db652d99e722f77226f4f102e3bdd1032a4b1700dd99"
content-hash = "6c846ea3f88e2a806b202709a65d5e825cd2e5c8ea42892b1731fdeb7c718124"
[metadata.files]
anyio = [
@@ -737,6 +751,10 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
itsdangerous = [
{file = "itsdangerous-2.1.1-py3-none-any.whl", hash = "sha256:935642cd4b987cdbee7210080004033af76306757ff8b4c0a506a4b6e06f02cf"},
{file = "itsdangerous-2.1.1.tar.gz", hash = "sha256:7b7d3023cd35d9cb0c1fd91392f8c95c6fa02c59bf8ad64b8849be3401b95afb"},

View File

@@ -34,6 +34,7 @@ loguru = "^0.6.0"
[tool.poetry.dev-dependencies]
black = {version = "^22.8.0", allow-prereleases = true}
isort = "^5.10.1"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@@ -1,10 +1,9 @@
import asyncio
from core.helpers import async_unwrap
from wallet.migrations import m001_initial
from wallet.wallet import Wallet as Wallet1
from wallet.wallet import Wallet as Wallet2
from wallet.migrations import m001_initial
SERVER_ENDPOINT = "http://localhost:3338"
@@ -56,7 +55,7 @@ async def run_test():
# print(exc.args[0])
await assert_err(
wallet1.split(wallet1.proofs + proofs, 20),
f"Error: Already spent. Secret: {proofs[0]['secret']}",
f"Error: tokens already spent. Secret: {proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64
wallet1.status()
@@ -73,7 +72,7 @@ async def run_test():
# Error: We try to double-spend and it fails
await assert_err(
wallet1.split([proofs[0]], 10),
f"Error: Already spent. Secret: {proofs[0]['secret']}",
f"Error: tokens already spent. Secret: {proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64
@@ -102,7 +101,7 @@ async def run_test():
# Error: We try to double-spend and it fails
await assert_err(
wallet1.split(w1_snd_proofs, 5),
f"Error: Already spent. Secret: {w1_snd_proofs[0]['secret']}",
f"Error: tokens already spent. Secret: {w1_snd_proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64 - 20
@@ -113,7 +112,7 @@ async def run_test():
await assert_err(
wallet1.split(w1_snd_proofs, -500),
"Error: Invalid split amount: -500",
"Error: invalid split amount: -500",
)

View File

@@ -1,3 +1,3 @@
# from core.db import Database
import sys
# db = Database("database", "data/wallet")
sys.tracebacklimit = None

View File

@@ -1,9 +1,9 @@
import secrets
from typing import Optional
from core.db import Connection, Database
# from wallet import db
from core.base import Proof
from core.db import Connection, Database
async def store_proof(

View File

@@ -1,18 +1,15 @@
import random
import asyncio
import random
from typing import List
import requests
from ecc.curve import secp256k1, Point
from typing import List
from core.base import Proof, BasePoint
from ecc.curve import Point, secp256k1
import core.b_dhke as b_dhke
from core.base import BasePoint, MintPayload, MintPayloads, Proof, SplitPayload
from core.db import Database
from core.split import amount_split
from wallet.crud import store_proof, invalidate_proof, get_proofs
from core.base import MintPayload, MintPayloads, SplitPayload
from wallet.crud import get_proofs, invalidate_proof, store_proof
class LedgerAPI:
@@ -48,7 +45,12 @@ class LedgerAPI:
proofs.append(proof.dict())
return proofs
def mint(self, amounts):
def request_mint(self, amount):
"""Requests a mint from the server and returns Lightning invoice."""
r = requests.get(self.url + "/mint", params={"amount": amount})
return r.json()
def mint(self, amounts, payment_hash=None):
"""Mints new coins and returns a proof of promise."""
payloads: MintPayloads = MintPayloads()
secrets = []
@@ -61,13 +63,13 @@ class LedgerAPI:
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(
self.url + "/mint",
json=payloads.dict(),
params={"payment_hash": payment_hash},
).json()
if "detail" in promises:
raise Exception("Error: {}".format(promises["detail"]))
if "error" in promises:
raise Exception("Error: {}".format(promises["error"]))
return self._construct_proofs(promises, [(r, s) for r, s in zip(rs, secrets)])
def split(self, proofs, amount):
@@ -122,9 +124,12 @@ class Wallet(LedgerAPI):
for proof in proofs:
await store_proof(proof, db=self.db)
async def mint(self, amount):
async def request_mint(self, amount):
return super().request_mint(amount)
async def mint(self, amount, payment_hash=None):
split = amount_split(amount)
proofs = super().mint(split)
proofs = super().mint(split, payment_hash)
if proofs == []:
raise Exception("received no proofs")
await self._store_proofs(proofs)
@@ -154,7 +159,7 @@ class Wallet(LedgerAPI):
try:
await self.split(proofs, sum(p["amount"] for p in proofs))
except Exception as exc:
assert exc.args[0].startswith("Error: Already spent."), Exception(
assert exc.args[0].startswith("Error: tokens already spent."), Exception(
"invalidating unspent tokens"
)