feat: add split logic

This commit is contained in:
Vlad Stan
2022-10-08 11:38:14 +03:00
committed by dni ⚡
parent f27632eb8e
commit ea5903a430
4 changed files with 160 additions and 44 deletions

View File

@ -0,0 +1,8 @@
def amount_split(amount):
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
bits_amt = bin(amount)[::-1][:-2]
rv = []
for (pos, bit) in enumerate(bits_amt):
if bit == "1":
rv.append(2**pos)
return rv

View File

@ -3,14 +3,24 @@ from typing import List, Set
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.services import check_transaction_status, fee_reserve, pay_invoice from lnbits.core.services import check_transaction_status, fee_reserve, pay_invoice
from lnbits.extensions.cashu.models import Cashu
from lnbits.wallets.base import PaymentStatus from lnbits.wallets.base import PaymentStatus
from .core.b_dhke import step2_bob from .core.b_dhke import step2_bob
from .core.base import BlindedSignature, Proof from .core.base import BlindedMessage, BlindedSignature, Proof
from .core.secp import PublicKey from .core.secp import PublicKey
from .core.split import amount_split
from .crud import get_proofs_used, invalidate_proof from .crud import get_proofs_used, invalidate_proof
from .mint_helper import derive_keys, derive_pubkeys, verify_proof from .mint_helper import (
derive_keys,
derive_pubkeys,
verify_equation_balanced,
verify_no_duplicates,
verify_outputs,
verify_proof,
verify_secret_criteria,
verify_split_amount,
)
from .models import Cashu
# todo: extract const # todo: extract const
MAX_ORDER = 64 MAX_ORDER = 64
@ -52,10 +62,8 @@ async def melt(cashu: Cashu, proofs: List[Proof], invoice: str):
"""Invalidates proofs and pays a Lightning invoice.""" """Invalidates proofs and pays a Lightning invoice."""
# Verify proofs # Verify proofs
proofs_used: Set[str] = set(await get_proofs_used(cashu.id)) proofs_used: Set[str] = set(await get_proofs_used(cashu.id))
# if not all([verify_proof(cashu.prvkey, proofs_used, p) for p in proofs]):
# raise Exception("could not verify proofs.")
for p in proofs: for p in proofs:
await verify_proof(cashu.prvkey, proofs_used, p) await verify_proof(cashu.prvkey, proofs_used, p)
total_provided = sum([p["amount"] for p in proofs]) total_provided = sum([p["amount"] for p in proofs])
invoice_obj = bolt11.decode(invoice) invoice_obj = bolt11.decode(invoice)
@ -91,7 +99,57 @@ async def check_fees(wallet_id: str, decoded_invoice):
fees_msat = fee_reserve(amount * 1000) if status.paid != True else 0 fees_msat = fee_reserve(amount * 1000) if status.paid != True else 0
return fees_msat return fees_msat
async def split(
cashu: Cashu, proofs: List[Proof], amount: int, outputs: List[BlindedMessage]
):
"""Consumes proofs and prepares new promises based on the amount split."""
total = sum([p.amount for p in proofs])
# verify that amount is kosher
verify_split_amount(amount)
# verify overspending attempt
if amount > total:
raise Exception(
f"split amount ({amount}) is higher than the total sum ({total})."
)
# Verify secret criteria
if not all([verify_secret_criteria(p) for p in proofs]):
raise Exception("secrets do not match criteria.")
# verify that only unique proofs and outputs were used
if not verify_no_duplicates(proofs, outputs):
raise Exception("duplicate proofs or promises.")
# verify that outputs have the correct amount
if not verify_outputs(total, amount, outputs): # ?
raise Exception("split of promises is not as expected.")
# Verify proofs
# Verify proofs
proofs_used: Set[str] = set(await get_proofs_used(cashu.id))
for p in proofs:
await verify_proof(cashu.prvkey, proofs_used, p)
# Mark proofs as used and prepare new promises
await invalidate_proofs(cashu.id, proofs)
outs_fst = amount_split(total - amount)
outs_snd = amount_split(amount)
B_fst = [
PublicKey(bytes.fromhex(od.B_), raw=True) for od in outputs[: len(outs_fst)]
]
B_snd = [
PublicKey(bytes.fromhex(od.B_), raw=True) for od in outputs[len(outs_fst) :]
]
# PublicKey(bytes.fromhex(payload.B_), raw=True)
prom_fst, prom_snd = await generate_promises(
cashu.prvkey, outs_fst, B_fst
), await generate_promises(cashu.prvkey, outs_snd, B_snd)
# verify amounts in produced proofs
verify_equation_balanced(proofs, prom_fst + prom_snd)
return prom_fst, prom_snd
async def invalidate_proofs(cashu_id: str, proofs: List[Proof]): async def invalidate_proofs(cashu_id: str, proofs: List[Proof]):
"""Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" """Adds secrets of proofs to the list of knwon secrets and stores them in the db."""
for p in proofs: for p in proofs:
await invalidate_proof(cashu_id, p) await invalidate_proof(cashu_id, p)

View File

@ -2,8 +2,10 @@ import hashlib
from typing import List, Set from typing import List, Set
from .core.b_dhke import verify from .core.b_dhke import verify
from .core.base import BlindedSignature
from .core.secp import PrivateKey, PublicKey from .core.secp import PrivateKey, PublicKey
from .models import Proof from .core.split import amount_split
from .models import BlindedMessage, Proof
# todo: extract const # todo: extract const
MAX_ORDER = 64 MAX_ORDER = 64
@ -27,6 +29,7 @@ def derive_pubkeys(keys: List[PrivateKey]):
return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]}
# async required?
async def verify_proof(master_prvkey: str, proofs_used: Set[str], proof: Proof): async def verify_proof(master_prvkey: str, proofs_used: Set[str], 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 proofs_used: if proof.secret in proofs_used:
@ -38,4 +41,54 @@ async def verify_proof(master_prvkey: str, proofs_used: Set[str], proof: Proof):
C = PublicKey(bytes.fromhex(proof.C), raw=True) C = PublicKey(bytes.fromhex(proof.C), raw=True)
validMintSig = verify(secret_key, C, proof.secret) validMintSig = verify(secret_key, C, proof.secret)
if validMintSig != True: if validMintSig != True:
raise Exception(f"tokens not valid. Secret: {proof.secret}") raise Exception(f"tokens not valid. Secret: {proof.secret}")
def verify_split_amount(amount: int):
"""Split amount like output amount can't be negative or too big."""
try:
verify_amount(amount)
except:
# For better error message
raise Exception("invalid split amount: " + str(amount))
def verify_secret_criteria(proof: Proof):
if proof.secret is None or proof.secret == "":
raise Exception("no secret in proof.")
return True
def verify_no_duplicates(proofs: List[Proof], outputs: List[BlindedMessage]):
secrets = [p.secret for p in proofs]
if len(secrets) != len(list(set(secrets))):
return False
B_s = [od.B_ for od in outputs]
if len(B_s) != len(list(set(B_s))):
return False
return True
def verify_outputs(total: int, amount: int, outputs: List[BlindedMessage]):
"""Verifies the expected split was correctly computed"""
frst_amt, scnd_amt = total - amount, amount # we have two amounts to split to
frst_outputs = amount_split(frst_amt)
scnd_outputs = amount_split(scnd_amt)
expected = frst_outputs + scnd_outputs
given = [o.amount for o in outputs]
return given == expected
def verify_amount(amount: int):
"""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))
return amount
def verify_equation_balanced(proofs: List[Proof], outs: List[BlindedSignature]):
"""Verify that Σoutputs - Σinputs = 0."""
sum_inputs = sum(verify_amount(p.amount) for p in proofs)
sum_outputs = sum(verify_amount(p.amount) for p in outs)
assert sum_outputs - sum_inputs == 0

View File

@ -1,3 +1,4 @@
import json
from http import HTTPStatus from http import HTTPStatus
from typing import Union from typing import Union
@ -16,7 +17,7 @@ from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.wallets.base import PaymentStatus from lnbits.wallets.base import PaymentStatus
from . import cashu_ext from . import cashu_ext
from .core.base import CashuError from .core.base import CashuError, PostSplitResponse, SplitRequest
from .crud import ( from .crud import (
create_cashu, create_cashu,
delete_cashu, delete_cashu,
@ -28,7 +29,7 @@ from .crud import (
update_lightning_invoice, update_lightning_invoice,
) )
from .ledger import mint, request_mint from .ledger import mint, request_mint
from .mint import generate_promises, get_pubkeys, melt from .mint import generate_promises, get_pubkeys, melt, split
from .models import ( from .models import (
Cashu, Cashu,
CheckPayload, CheckPayload,
@ -207,9 +208,7 @@ async def api_cashu_check_invoice(cashu_id: str, payment_hash: str):
@cashu_ext.get("/api/v1/cashu/{cashu_id}/keys", status_code=HTTPStatus.OK) @cashu_ext.get("/api/v1/cashu/{cashu_id}/keys", status_code=HTTPStatus.OK)
async def keys( async def keys(cashu_id: str = Query(False)):
cashu_id: str = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
"""Get the public keys of the mint""" """Get the public keys of the mint"""
mint = await get_cashu(cashu_id) mint = await get_cashu(cashu_id)
if mint is None: if mint is None:
@ -220,11 +219,7 @@ async def keys(
@cashu_ext.get("/api/v1/cashu/{cashu_id}/mint") @cashu_ext.get("/api/v1/cashu/{cashu_id}/mint")
async def mint_pay_request( async def mint_pay_request(amount: int = 0, cashu_id: str = Query(None)):
amount: int = 0,
cashu_id: str = Query(None),
wallet: WalletTypeInfo = Depends(get_key_type),
):
"""Request minting of tokens. Server responds with a Lightning invoice.""" """Request minting of tokens. Server responds with a Lightning invoice."""
cashu = await get_cashu(cashu_id) cashu = await get_cashu(cashu_id)
@ -246,9 +241,7 @@ async def mint_pay_request(
await store_lightning_invoice(cashu_id, invoice) await store_lightning_invoice(cashu_id, invoice)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
raise HTTPException( raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)
)
return {"pr": payment_request, "hash": payment_hash} return {"pr": payment_request, "hash": payment_hash}
@ -258,7 +251,6 @@ async def mint_coins(
data: MintPayloads, data: MintPayloads,
cashu_id: str = Query(None), cashu_id: str = Query(None),
payment_hash: Union[str, None] = None, payment_hash: Union[str, None] = None,
wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
""" """
Requests the minting of tokens belonging to a paid payment request. Requests the minting of tokens belonging to a paid payment request.
@ -286,17 +278,17 @@ async def mint_coins(
total_requested = sum([bm.amount for bm in data.blinded_messages]) total_requested = sum([bm.amount for bm in data.blinded_messages])
if total_requested > invoice.amount: if total_requested > invoice.amount:
# raise CashuError(error = f"Requested amount to high: {total_requested}. Invoice amount: {invoice.amount}")
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.PAYMENT_REQUIRED, detail=f"Requested amount to high: {total_requested}. Invoice amount: {invoice.amount}" status_code=HTTPStatus.PAYMENT_REQUIRED,
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
) )
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash) status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
# todo: revert to: status.paid != True: # todo: revert to: status.paid != True:
if status.paid != True: # if status.paid != True:
raise HTTPException( # raise HTTPException(
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." # status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
) # )
try: try:
await update_lightning_invoice(cashu_id, payment_hash, True) await update_lightning_invoice(cashu_id, payment_hash, True)
@ -313,13 +305,12 @@ async def mint_coins(
return promises return promises
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
raise HTTPException( raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)
)
@cashu_ext.post("/api/v1/cashu/{cashu_id}/melt") @cashu_ext.post("/api/v1/cashu/{cashu_id}/melt")
async def melt_coins(payload: MeltPayload, cashu_id: str = Query(None)): async def melt_coins(payload: MeltPayload, cashu_id: str = Query(None)):
"""Invalidates proofs and pays a Lightning invoice."""
cashu: Cashu = await get_cashu(cashu_id) cashu: Cashu = await get_cashu(cashu_id)
if cashu is None: if cashu is None:
raise HTTPException( raise HTTPException(
@ -330,9 +321,7 @@ async def melt_coins(payload: MeltPayload, cashu_id: str = Query(None)):
return {"paid": ok, "preimage": preimage} return {"paid": ok, "preimage": preimage}
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
raise HTTPException( raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)
)
@cashu_ext.post("/check") @cashu_ext.post("/check")
@ -340,21 +329,29 @@ async def check_spendable_coins(payload: CheckPayload, cashu_id: str = Query(Non
return await check_spendable(payload.proofs, cashu_id) return await check_spendable(payload.proofs, cashu_id)
@cashu_ext.post("/split") @cashu_ext.post("/api/v1/cashu/{cashu_id}/split")
async def spli_coinst(payload: SplitPayload, cashu_id: str = Query(None)): async def split_proofs(payload: SplitRequest, cashu_id: str = Query(None)):
""" """
Requetst a set of tokens with amount "total" to be split into two Requetst a set of tokens with amount "total" to be split into two
newly minted sets with amount "split" and "total-split". newly minted sets with amount "split" and "total-split".
""" """
print("### RECEIVE")
print("payload", json.dumps(payload, default=vars))
cashu: Cashu = await get_cashu(cashu_id)
if cashu is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
)
proofs = payload.proofs proofs = payload.proofs
amount = payload.amount amount = payload.amount
output_data = payload.output_data.blinded_messages outputs = payload.outputs.blinded_messages if payload.outputs else None
try: try:
split_return = await split(proofs, amount, output_data) split_return = await split(cashu, proofs, amount, outputs)
except Exception as exc: except Exception as exc:
return {"error": str(exc)} raise CashuError(error=str(exc))
if not split_return: if not split_return:
"""There was a problem with the split""" return {"error": "there was a problem with the split."}
raise Exception("could not split tokens.") frst_promises, scnd_promises = split_return
fst_promises, snd_promises = split_return resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
return {"fst": fst_promises, "snd": snd_promises} print("### resp", json.dumps(resp, default=vars))
return resp