diff --git a/lnbits/extensions/cashu/__init__.py b/lnbits/extensions/cashu/__init__.py index cf277664..cb62ffca 100644 --- a/lnbits/extensions/cashu/__init__.py +++ b/lnbits/extensions/cashu/__init__.py @@ -1,6 +1,7 @@ import asyncio from fastapi import APIRouter +from fastapi.staticfiles import StaticFiles from lnbits.db import Database from lnbits.helpers import template_renderer @@ -10,13 +11,24 @@ db = Database("ext_cashu") cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"]) +cashu_static_files = [ + { + "path": "/cashu/static", + "app": StaticFiles(directory="lnbits/extensions/cashu/static"), + "name": "cashu_static", + } +] + + def cashu_renderer(): return template_renderer(["lnbits/extensions/cashu/templates"]) + from .tasks import wait_for_paid_invoices from .views import * # noqa from .views_api import * # noqa + def cashu_start(): loop = asyncio.get_event_loop() loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/cashu/config.json b/lnbits/extensions/cashu/config.json index 4f097e8b..fe802034 100644 --- a/lnbits/extensions/cashu/config.json +++ b/lnbits/extensions/cashu/config.json @@ -2,5 +2,6 @@ "name": "Cashu Ecash", "short_description": "Ecash mints with LN peg in/out", "icon": "approval", - "contributors": ["arcbtc", "calle"] + "contributors": ["arcbtc", "calle"], + "hidden": false } diff --git a/lnbits/extensions/cashu/config.json.example b/lnbits/extensions/cashu/config.json.example new file mode 100644 index 00000000..e798e2ef --- /dev/null +++ b/lnbits/extensions/cashu/config.json.example @@ -0,0 +1,7 @@ +{ + "name": "Cashu Ecash", + "short_description": "Ecash mints with LN peg in/out", + "icon": "approval", + "contributors": ["arcbtc", "calle"], + "hidden": true +} diff --git a/lnbits/extensions/cashu/core/b_dhke.py b/lnbits/extensions/cashu/core/b_dhke.py new file mode 100644 index 00000000..ff0bc515 --- /dev/null +++ b/lnbits/extensions/cashu/core/b_dhke.py @@ -0,0 +1,88 @@ +# Don't trust me with cryptography. + +""" +Implementation of https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406 +Alice: +A = a*G +return A +Bob: +Y = hash_to_curve(secret_message) +r = random blinding factor +B'= Y + r*G +return B' +Alice: +C' = a*B' + (= a*Y + a*r*G) +return C' +Bob: +C = C' - r*A + (= C' - a*r*G) + (= a*Y) +return C, secret_message +Alice: +Y = hash_to_curve(secret_message) +C == a*Y +If true, C must have originated from Alice +""" + +import hashlib + +from secp256k1 import PrivateKey, PublicKey + + +def hash_to_curve(message: bytes): + """Generates a point from the message hash and checks if the point lies on the curve. + If it does not, it tries computing again a new x coordinate from the hash of the coordinate.""" + point = None + msg_to_hash = message + while point is None: + try: + _hash = hashlib.sha256(msg_to_hash).digest() + point = PublicKey(b"\x02" + _hash, raw=True) + except: + msg_to_hash = _hash + return point + + +def step1_alice(secret_msg): + secret_msg = secret_msg + Y = hash_to_curve(secret_msg) + r = PrivateKey() + B_ = Y + r.pubkey + return B_, r + + +def step2_bob(B_, a): + C_ = B_.mult(a) + return C_ + + +def step3_alice(C_, r, A): + C = C_ - A.mult(r) + return C + + +def verify(a, C, secret_msg): + Y = hash_to_curve(secret_msg) + return C == Y.mult(a) + + +### Below is a test of a simple positive and negative case + +# # Alice's keys +# a = PrivateKey() +# A = a.pubkey +# secret_msg = "test" +# B_, r = step1_alice(secret_msg) +# C_ = step2_bob(B_, a) +# C = step3_alice(C_, r, A) +# print("C:{}, secret_msg:{}".format(C, secret_msg)) +# assert verify(a, C, secret_msg) +# assert verify(a, C + C, secret_msg) == False # adding C twice shouldn't pass +# assert verify(a, A, secret_msg) == False # A shouldn't pass + +# # Test operations +# b = PrivateKey() +# B = b.pubkey +# assert -A -A + A == -A # neg +# assert B.mult(a) == A.mult(b) # a*B = A*b diff --git a/lnbits/extensions/cashu/core/base.py b/lnbits/extensions/cashu/core/base.py new file mode 100644 index 00000000..947da987 --- /dev/null +++ b/lnbits/extensions/cashu/core/base.py @@ -0,0 +1,168 @@ +from sqlite3 import Row +from typing import List, Union + +from pydantic import BaseModel + + +class CashuError(BaseException): + code = "000" + error = "CashuError" + + +class P2SHScript(BaseModel): + script: str + signature: str + address: Union[str, None] = None + + @classmethod + def from_row(cls, row: Row): + return cls( + address=row[0], + script=row[1], + signature=row[2], + used=row[3], + ) + + +class Proof(BaseModel): + amount: int + secret: str = "" + C: str + script: Union[P2SHScript, None] = None + reserved: bool = False # whether this proof is reserved for sending + send_id: str = "" # unique ID of send attempt + time_created: str = "" + time_reserved: str = "" + + @classmethod + def from_row(cls, row: Row): + return cls( + amount=row[0], + C=row[1], + secret=row[2], + reserved=row[3] or False, + send_id=row[4] or "", + time_created=row[5] or "", + time_reserved=row[6] or "", + ) + + @classmethod + def from_dict(cls, d: dict): + assert "amount" in d, "no amount in proof" + return cls( + amount=d.get("amount"), + C=d.get("C"), + secret=d.get("secret") or "", + reserved=d.get("reserved") or False, + send_id=d.get("send_id") or "", + time_created=d.get("time_created") or "", + time_reserved=d.get("time_reserved") or "", + ) + + def to_dict(self): + return dict(amount=self.amount, secret=self.secret, C=self.C) + + def to_dict_no_secret(self): + return dict(amount=self.amount, C=self.C) + + def __getitem__(self, key): + return self.__getattribute__(key) + + def __setitem__(self, key, val): + self.__setattr__(key, val) + + +class Proofs(BaseModel): + """TODO: Use this model""" + + proofs: List[Proof] + + +class Invoice(BaseModel): + amount: int + pr: str + hash: str + issued: bool = False + + @classmethod + def from_row(cls, row: Row): + return cls( + cashu_id=str(row[0]), + amount=int(row[1]), + pr=str(row[2]), + hash=str(row[3]), + issued=bool(row[4]), + ) + + +class BlindedMessage(BaseModel): + amount: int + B_: str + + +class BlindedSignature(BaseModel): + amount: int + C_: str + + @classmethod + def from_dict(cls, d: dict): + return cls( + amount=d["amount"], + C_=d["C_"], + ) + + +class MintRequest(BaseModel): + blinded_messages: List[BlindedMessage] = [] + + +class GetMintResponse(BaseModel): + pr: str + hash: str + + +class GetMeltResponse(BaseModel): + paid: Union[bool, None] + preimage: Union[str, None] + + +class SplitRequest(BaseModel): + proofs: List[Proof] + amount: int + output_data: Union[ + MintRequest, None + ] = None # backwards compatibility with clients < v0.2.2 + outputs: Union[MintRequest, None] = None + + def __init__(self, **data): + super().__init__(**data) + self.backwards_compatibility_v021() + + def backwards_compatibility_v021(self): + # before v0.2.2: output_data, after: outputs + if self.output_data: + self.outputs = self.output_data + self.output_data = None + + +class PostSplitResponse(BaseModel): + fst: List[BlindedSignature] + snd: List[BlindedSignature] + + +class CheckRequest(BaseModel): + proofs: List[Proof] + + +class CheckFeesRequest(BaseModel): + pr: str + + +class CheckFeesResponse(BaseModel): + fee: Union[int, None] + + +class MeltRequest(BaseModel): + proofs: List[Proof] + amount: int = None # deprecated + invoice: str diff --git a/lnbits/extensions/cashu/core/secp.py b/lnbits/extensions/cashu/core/secp.py new file mode 100644 index 00000000..33416434 --- /dev/null +++ b/lnbits/extensions/cashu/core/secp.py @@ -0,0 +1,52 @@ +from secp256k1 import PrivateKey, PublicKey + + +# We extend the public key to define some operations on points +# Picked from https://github.com/WTRMQDev/secp256k1-zkp-py/blob/master/secp256k1_zkp/__init__.py +class PublicKeyExt(PublicKey): + def __add__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + new_pub = PublicKey() + new_pub.combine([self.public_key, pubkey2.public_key]) + return new_pub + else: + raise TypeError("Cant add pubkey and %s" % pubkey2.__class__) + + def __neg__(self): + serialized = self.serialize() + first_byte, remainder = serialized[:1], serialized[1:] + # flip odd/even byte + first_byte = {b"\x03": b"\x02", b"\x02": b"\x03"}[first_byte] + return PublicKey(first_byte + remainder, raw=True) + + def __sub__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + return self + (-pubkey2) + else: + raise TypeError("Can't add pubkey and %s" % pubkey2.__class__) + + def mult(self, privkey): + if isinstance(privkey, PrivateKey): + return self.tweak_mul(privkey.private_key) + else: + raise TypeError("Can't multiply with non privatekey") + + def __eq__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + seq1 = self.to_data() + seq2 = pubkey2.to_data() + return seq1 == seq2 + else: + raise TypeError("Can't compare pubkey and %s" % pubkey2.__class__) + + def to_data(self): + return [self.public_key.data[i] for i in range(64)] + + +# Horrible monkeypatching +PublicKey.__add__ = PublicKeyExt.__add__ +PublicKey.__neg__ = PublicKeyExt.__neg__ +PublicKey.__sub__ = PublicKeyExt.__sub__ +PublicKey.mult = PublicKeyExt.mult +PublicKey.__eq__ = PublicKeyExt.__eq__ +PublicKey.to_data = PublicKeyExt.to_data diff --git a/lnbits/extensions/cashu/core/split.py b/lnbits/extensions/cashu/core/split.py new file mode 100644 index 00000000..44b9cf51 --- /dev/null +++ b/lnbits/extensions/cashu/core/split.py @@ -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 diff --git a/lnbits/extensions/cashu/crud.py b/lnbits/extensions/cashu/crud.py index 7a9c25c3..c8f3c72b 100644 --- a/lnbits/extensions/cashu/crud.py +++ b/lnbits/extensions/cashu/crud.py @@ -1,21 +1,18 @@ import os - +import random +from binascii import hexlify, unhexlify from typing import List, Optional, Union +from embit import bip32, bip39, ec, script +from embit.networks import NETWORKS +from loguru import logger + from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Cashu, Pegs, Proof, Promises +from .core.base import Invoice +from .models import Cashu, Pegs, Promises, Proof -from embit import script -from embit import ec -from embit.networks import NETWORKS -from embit import bip32 -from embit import bip39 -from binascii import unhexlify, hexlify -import random - -from loguru import logger async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: cashu_id = urlsafe_short_hash() @@ -24,7 +21,7 @@ async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: mnemonic = bip39.mnemonic_from_bytes(entropy) seed = bip39.mnemonic_to_seed(mnemonic) root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"]) - + bip44_xprv = root.derive("m/44h/1h/0h") bip44_xpub = bip44_xprv.to_public() @@ -42,7 +39,7 @@ async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: data.maxsats, data.coins, bip44_xprv.to_base58(), - bip44_xpub.to_base58() + bip44_xpub.to_base58(), ), ) @@ -56,11 +53,16 @@ async def update_cashu_keys(cashu_id, wif: str = None) -> Optional[Cashu]: mnemonic = bip39.mnemonic_from_bytes(entropy) seed = bip39.mnemonic_to_seed(mnemonic) root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"]) - + bip44_xprv = root.derive("m/44h/1h/0h") bip44_xpub = bip44_xprv.to_public() - await db.execute("UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?", bip44_xprv.to_base58(), bip44_xpub.to_base58(), cashu_id) + await db.execute( + "UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?", + bip44_xprv.to_base58(), + bip44_xpub.to_base58(), + cashu_id, + ) row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,)) return Cashu(**row) if row else None @@ -90,54 +92,95 @@ async def delete_cashu(cashu_id) -> None: ###############MINT STUFF################# ########################################## -async def store_promise( - amount: int, - B_: str, - C_: str, - cashu_id + +async def store_promises( + amounts: List[int], B_s: List[str], C_s: List[str], cashu_id: str ): + for amount, B_, C_ in zip(amounts, B_s, C_s): + await store_promise(amount, B_, C_, cashu_id) + + +async def store_promise(amount: int, B_: str, C_: str, cashu_id: str): promise_id = urlsafe_short_hash() - await (conn or db).execute( + await db.execute( """ INSERT INTO cashu.promises (id, amount, B_b, C_b, cashu_id) VALUES (?, ?, ?, ?, ?) """, - ( - promise_id, - amount, - str(B_), - str(C_), - cashu_id - ), + (promise_id, amount, str(B_), str(C_), cashu_id), ) + async def get_promises(cashu_id) -> Optional[Cashu]: - row = await db.fetchall("SELECT * FROM cashu.promises WHERE cashu_id = ?", (promises_id,)) + row = await db.fetchall( + "SELECT * FROM cashu.promises WHERE cashu_id = ?", (cashu_id,) + ) return Promises(**row) if row else None + async def get_proofs_used(cashu_id): - rows = await db.fetchall("SELECT secret from cashu.proofs_used WHERE id = ?", (cashu_id,)) + rows = await db.fetchall( + "SELECT secret from cashu.proofs_used WHERE cashu_id = ?", (cashu_id,) + ) return [row[0] for row in rows] -async def invalidate_proof( - proof: Proof, - cashu_id -): +async def invalidate_proof(cashu_id: str, proof: Proof): invalidate_proof_id = urlsafe_short_hash() - await (conn or db).execute( + await db.execute( """ INSERT INTO cashu.proofs_used (id, amount, C, secret, cashu_id) VALUES (?, ?, ?, ?, ?) """, + (invalidate_proof_id, proof.amount, str(proof.C), str(proof.secret), cashu_id), + ) + + +######################################## +############ MINT INVOICES ############# +######################################## + + +async def store_lightning_invoice(cashu_id: str, invoice: Invoice): + await db.execute( + """ + INSERT INTO cashu.invoices + (cashu_id, amount, pr, hash, issued) + VALUES (?, ?, ?, ?, ?) + """, ( - invalidate_proof_id, - proof.amount, - str(proof.C), - str(proof.secret), - cashu_id + cashu_id, + invoice.amount, + invoice.pr, + invoice.hash, + invoice.issued, ), - ) \ No newline at end of file + ) + + +async def get_lightning_invoice(cashu_id: str, hash: str): + row = await db.fetchone( + """ + SELECT * from cashu.invoices + WHERE cashu_id =? AND hash = ? + """, + ( + cashu_id, + hash, + ), + ) + return Invoice.from_row(row) + + +async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool): + await db.execute( + "UPDATE cashu.invoices SET issued = ? WHERE cashu_id = ? AND hash = ?", + ( + issued, + cashu_id, + hash, + ), + ) diff --git a/lnbits/extensions/cashu/ledger.py b/lnbits/extensions/cashu/ledger.py index 404f7ee8..a28dc97a 100644 --- a/lnbits/extensions/cashu/ledger.py +++ b/lnbits/extensions/cashu/ledger.py @@ -1,13 +1,15 @@ import hashlib from typing import List, Set -from .models import BlindedMessage, BlindedSignature, Invoice, Proof -from secp256k1 import PublicKey, PrivateKey - from fastapi import Query -from .crud import get_cashu +from secp256k1 import PrivateKey, PublicKey + from lnbits.core.services import check_transaction_status, create_invoice +from .crud import get_cashu +from .models import BlindedMessage, BlindedSignature, Invoice, Proof + + def _derive_keys(master_key: str, cashu_id: str = Query(None)): """Deterministic derivation of keys for 2^n values.""" return { @@ -21,29 +23,34 @@ def _derive_keys(master_key: str, cashu_id: str = Query(None)): for i in range(MAX_ORDER) } + def _derive_pubkeys(keys: List[PrivateKey], cashu_id: str = Query(None)): return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} -async def _generate_promises(amounts: List[int], B_s: List[str], cashu_id: str = Query(None)): + +async def _generate_promises( + amounts: List[int], B_s: List[str], cashu_id: str = Query(None) +): """Generates promises that sum to the given amount.""" return [ await self._generate_promise(amount, PublicKey(bytes.fromhex(B_), raw=True)) for (amount, B_) in zip(amounts, B_s) ] + async def _generate_promise(amount: int, B_: PublicKey, cashu_id: str = Query(None)): """Generates a promise for given amount and returns a pair (amount, C').""" secret_key = self.keys[amount] # Get the correct key C_ = step2_bob(B_, secret_key) - await store_promise( - amount, B_=B_.serialize().hex(), C_=C_.serialize().hex() - ) + await store_promise(amount, B_=B_.serialize().hex(), C_=C_.serialize().hex()) return BlindedSignature(amount=amount, C_=C_.serialize().hex()) + def _check_spendable(proof: Proof, cashu_id: str = Query(None)): """Checks whether the proof was already spent.""" return not proof.secret in self.proofs_used + def _verify_proof(proof: Proof, cashu_id: str = Query(None)): """Verifies that the proof of promise was issued by this ledger.""" if not self._check_spendable(proof): @@ -52,7 +59,13 @@ def _verify_proof(proof: Proof, cashu_id: str = Query(None)): C = PublicKey(bytes.fromhex(proof.C), raw=True) return verify(secret_key, C, proof.secret) -def _verify_outputs(total: int, amount: int, output_data: List[BlindedMessage], cashu_id: str = Query(None)): + +def _verify_outputs( + total: int, + amount: int, + output_data: List[BlindedMessage], + cashu_id: str = Query(None), +): """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) @@ -61,7 +74,10 @@ def _verify_outputs(total: int, amount: int, output_data: List[BlindedMessage], given = [o.amount for o in output_data] return given == expected -def _verify_no_duplicates(proofs: List[Proof], output_data: List[BlindedMessage], cashu_id: str = Query(None)): + +def _verify_no_duplicates( + proofs: List[Proof], output_data: List[BlindedMessage], cashu_id: str = Query(None) +): secrets = [p.secret for p in proofs] if len(secrets) != len(list(set(secrets))): return False @@ -70,6 +86,7 @@ def _verify_no_duplicates(proofs: List[Proof], output_data: List[BlindedMessage] return False return True + def _verify_split_amount(amount: int, cashu_id: str = Query(None)): """Split amount like output amount can't be negative or too big.""" try: @@ -78,6 +95,7 @@ def _verify_split_amount(amount: int, cashu_id: str = Query(None)): # For better error message raise Exception("invalid split amount: " + str(amount)) + def _verify_amount(amount: int, cashu_id: str = Query(None)): """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 @@ -85,12 +103,16 @@ def _verify_amount(amount: int, cashu_id: str = Query(None)): raise Exception("invalid amount: " + str(amount)) return amount -def _verify_equation_balanced(proofs: List[Proof], outs: List[BlindedMessage], cashu_id: str = Query(None)): + +def _verify_equation_balanced( + proofs: List[Proof], outs: List[BlindedMessage], cashu_id: str = Query(None) +): """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) assert sum_outputs - sum_inputs == 0 + def _get_output_split(amount: int, cashu_id: str): """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" self._verify_amount(amount) @@ -101,6 +123,7 @@ def _get_output_split(amount: int, cashu_id: str): rv.append(2**pos) return rv + async def _invalidate_proofs(proofs: List[Proof], cashu_id: str = Query(None)): """Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" # Mark proofs as used and prepare new promises @@ -110,10 +133,12 @@ async def _invalidate_proofs(proofs: List[Proof], cashu_id: str = Query(None)): for p in proofs: await invalidate_proof(p) + def get_pubkeys(cashu_id: str = Query(None)): """Returns public keys for possible amounts.""" return {a: p.serialize().hex() for a, p in self.pub_keys.items()} + async def request_mint(amount, cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: @@ -125,9 +150,7 @@ async def request_mint(amount, cashu_id: str = Query(None)): amount=amount, memo=cashu.name, unhashed_description=cashu.name.encode("utf-8"), - extra={ - "tag": "Cashu" - }, + extra={"tag": "Cashu"}, ) invoice = Invoice( @@ -137,15 +160,23 @@ async def request_mint(amount, cashu_id: str = Query(None)): raise Exception(f"Could not create Lightning invoice.") return payment_request, payment_hash -async def mint(B_s: List[PublicKey], amounts: List[int], payment_hash: str = Query(None), cashu_id: str = Query(None)): + +async def mint( + B_s: List[PublicKey], + amounts: List[int], + payment_hash: str = Query(None), + cashu_id: str = Query(None), +): cashu = await get_cashu(cashu_id) if not cashu: raise Exception(f"Could not find Cashu") """Mints a promise for coins for B_.""" # check if lightning invoice was paid - if payment_hash: - if not await check_transaction_status(wallet_id=cashu.wallet, payment_hash=payment_hash): + if payment_hash: + if not await check_transaction_status( + wallet_id=cashu.wallet, payment_hash=payment_hash + ): raise Exception("Lightning invoice not paid yet.") for amount in amounts: @@ -157,11 +188,14 @@ async def mint(B_s: List[PublicKey], amounts: List[int], payment_hash: str = Que ] return promises -async def melt(proofs: List[Proof], amount: int, invoice: str, cashu_id: str = Query(None)): + +async def melt( + proofs: List[Proof], amount: int, invoice: str, cashu_id: str = Query(None) +): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") - + raise Exception(f"Could not find Cashu") + """Invalidates proofs and pays a Lightning invoice.""" # if not LIGHTNING: total = sum([p["amount"] for p in proofs]) @@ -181,6 +215,7 @@ async def melt(proofs: List[Proof], amount: int, invoice: str, cashu_id: str = Q await self._invalidate_proofs(proofs) return status, payment_hash + async def check_spendable(proofs: List[Proof], cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: @@ -189,7 +224,13 @@ async def check_spendable(proofs: List[Proof], cashu_id: str = Query(None)): """Checks if all provided proofs are valid and still spendable (i.e. have not been spent).""" return {i: self._check_spendable(p) for i, p in enumerate(proofs)} -async def split(proofs: List[Proof], amount: int, output_data: List[BlindedMessage], cashu_id: str = Query(None)): + +async def split( + proofs: List[Proof], + amount: int, + output_data: List[BlindedMessage], + cashu_id: str = Query(None), +): cashu = await get_cashu(cashu_id) if not cashu: raise Exception(f"Could not find Cashu") @@ -226,18 +267,19 @@ async def split(proofs: List[Proof], amount: int, output_data: List[BlindedMessa async def fee_reserve(amount_msat: int, cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") - + raise Exception(f"Could not find Cashu") + """Function for calculating the Lightning fee reserve""" return max( int(LIGHTNING_RESERVE_FEE_MIN), int(amount_msat * LIGHTNING_FEE_PERCENT / 100.0) ) + async def amount_split(amount, cashu_id: str): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") - + raise Exception(f"Could not find Cashu") + """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" bits_amt = bin(amount)[::-1][:-2] rv = [] @@ -246,11 +288,12 @@ async def amount_split(amount, cashu_id: str): rv.append(2**pos) return rv + async def hash_to_point(secret_msg, cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") - + raise Exception(f"Could not find Cashu") + """Generates x coordinate from the message hash and checks if the point lies on the curve. If it does not, it tries computing again a new x coordinate from the hash of the coordinate.""" point = None @@ -280,10 +323,11 @@ async def step1_alice(secret_msg, cashu_id: str = Query(None)): B_ = Y + r.pubkey return B_, r + async def step2_bob(B_, a, cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") + raise Exception(f"Could not find Cashu") C_ = B_.mult(a) return C_ diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py index f7d8f4f0..d06baef1 100644 --- a/lnbits/extensions/cashu/migrations.py +++ b/lnbits/extensions/cashu/migrations.py @@ -43,6 +43,7 @@ async def m001_initial(db): B_b TEXT NOT NULL, C_b TEXT NOT NULL, cashu_id TEXT NOT NULL + UNIQUE (B_b) ); """ ) @@ -60,4 +61,19 @@ async def m001_initial(db): cashu_id TEXT NOT NULL ); """ - ) \ No newline at end of file + ) + + await db.execute( + """ + CREATE TABLE IF NOT EXISTS cashu.invoices ( + cashu_id TEXT NOT NULL, + amount INTEGER NOT NULL, + pr TEXT NOT NULL, + hash TEXT NOT NULL, + issued BOOL NOT NULL, + + UNIQUE (hash) + + ); + """ + ) diff --git a/lnbits/extensions/cashu/mint.py b/lnbits/extensions/cashu/mint.py new file mode 100644 index 00000000..4153fb30 --- /dev/null +++ b/lnbits/extensions/cashu/mint.py @@ -0,0 +1,155 @@ +import math +from typing import List, Set + +from lnbits import bolt11 +from lnbits.core.services import check_transaction_status, fee_reserve, pay_invoice +from lnbits.wallets.base import PaymentStatus + +from .core.b_dhke import step2_bob +from .core.base import BlindedMessage, BlindedSignature, Proof +from .core.secp import PublicKey +from .core.split import amount_split +from .crud import get_proofs_used, invalidate_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 +MAX_ORDER = 64 + + +def get_pubkeys(xpriv: str): + """Returns public keys for possible amounts.""" + + keys = derive_keys(xpriv) + pub_keys = derive_pubkeys(keys) + + return {a: p.serialize().hex() for a, p in pub_keys.items()} + + +async def generate_promises( + master_prvkey: str, amounts: List[int], B_s: List[PublicKey] +): + """Mints a promise for coins for B_.""" + + 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}.") + + promises = [ + await generate_promise(master_prvkey, amount, B_) + for B_, amount in zip(B_s, amounts) + ] + return promises + + +async def generate_promise(master_prvkey: str, amount: int, B_: PublicKey): + """Generates a promise for given amount and returns a pair (amount, C').""" + secret_key = derive_keys(master_prvkey)[amount] # Get the correct key + C_ = step2_bob(B_, secret_key) + return BlindedSignature(amount=amount, C_=C_.serialize().hex()) + + +async def melt(cashu: Cashu, proofs: List[Proof], invoice: str): + """Invalidates proofs and pays a Lightning invoice.""" + # 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) + + total_provided = sum([p["amount"] for p in proofs]) + invoice_obj = bolt11.decode(invoice) + amount = math.ceil(invoice_obj.amount_msat / 1000) + + fees_msat = await check_fees(cashu.wallet, invoice_obj) + assert total_provided >= amount + fees_msat / 1000, Exception( + f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)." + ) + + await pay_invoice( + wallet_id=cashu.wallet, + payment_request=invoice, + description=f"pay cashu invoice", + extra={"tag": "cashu", "cahsu_name": cashu.name}, + ) + + status: PaymentStatus = await check_transaction_status( + cashu.wallet, invoice_obj.payment_hash + ) + if status.paid == True: + await invalidate_proofs(cashu.id, proofs) + return status.paid, status.preimage + return False, "" + + +async def check_fees(wallet_id: str, decoded_invoice): + """Returns the fees (in msat) required to pay this pr.""" + amount = math.ceil(decoded_invoice.amount_msat / 1000) + status: PaymentStatus = await check_transaction_status( + wallet_id, decoded_invoice.payment_hash + ) + fees_msat = fee_reserve(amount * 1000) if status.paid != True else 0 + 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]): + """Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" + for p in proofs: + await invalidate_proof(cashu_id, p) diff --git a/lnbits/extensions/cashu/mint_helper.py b/lnbits/extensions/cashu/mint_helper.py new file mode 100644 index 00000000..8e7e2275 --- /dev/null +++ b/lnbits/extensions/cashu/mint_helper.py @@ -0,0 +1,97 @@ +import base64 +import hashlib +from typing import List, Set + +from .core.b_dhke import verify +from .core.base import BlindedSignature +from .core.secp import PrivateKey, PublicKey +from .core.split import amount_split +from .models import BlindedMessage, Proof + +# todo: extract const +MAX_ORDER = 64 + + +def derive_keys(master_key: str): + """Deterministic derivation of keys for 2^n values.""" + return { + 2 + ** i: PrivateKey( + hashlib.sha256((str(master_key) + str(i)).encode("utf-8")) + .hexdigest() + .encode("utf-8")[:32], + raw=True, + ) + for i in range(MAX_ORDER) + } + + +def derive_pubkeys(keys: List[PrivateKey]): + 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): + """Verifies that the proof of promise was issued by this ledger.""" + if proof.secret in proofs_used: + raise Exception(f"tokens already spent. Secret: {proof.secret}") + + secret_key = derive_keys(master_prvkey)[ + proof.amount + ] # Get the correct key to check against + C = PublicKey(bytes.fromhex(proof.C), raw=True) + secret = base64.standard_b64decode(proof.secret) + print("### secret", secret) + validMintSig = verify(secret_key, C, secret) + if validMintSig != True: + 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 diff --git a/lnbits/extensions/cashu/models.py b/lnbits/extensions/cashu/models.py index 094966ff..596db047 100644 --- a/lnbits/extensions/cashu/models.py +++ b/lnbits/extensions/cashu/models.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import Optional, List +from typing import List, Union from fastapi import Query from pydantic import BaseModel @@ -20,20 +20,22 @@ class Cashu(BaseModel): def from_row(cls, row: Row) -> "TPoS": return cls(**dict(row)) + class Pegs(BaseModel): id: str wallet: str inout: str amount: str - @classmethod def from_row(cls, row: Row) -> "TPoS": return cls(**dict(row)) + class PayLnurlWData(BaseModel): lnurl: str + class Promises(BaseModel): id: str amount: int @@ -41,6 +43,7 @@ class Promises(BaseModel): C_b: str cashu_id: str + class Proof(BaseModel): amount: int secret: str @@ -142,4 +145,4 @@ class CheckPayload(BaseModel): class MeltPayload(BaseModel): proofs: List[Proof] amount: int - invoice: str \ No newline at end of file + invoice: str diff --git a/lnbits/extensions/cashu/static/js/base64.js b/lnbits/extensions/cashu/static/js/base64.js new file mode 100644 index 00000000..b150882f --- /dev/null +++ b/lnbits/extensions/cashu/static/js/base64.js @@ -0,0 +1,37 @@ +function unescapeBase64Url(str) { + return (str + '==='.slice((str.length + 3) % 4)) + .replace(/-/g, '+') + .replace(/_/g, '/') +} + +function escapeBase64Url(str) { + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') +} + +const uint8ToBase64 = (function (exports) { + 'use strict' + + var fromCharCode = String.fromCharCode + var encode = function encode(uint8array) { + var output = [] + + for (var i = 0, length = uint8array.length; i < length; i++) { + output.push(fromCharCode(uint8array[i])) + } + + return btoa(output.join('')) + } + + var asCharCode = function asCharCode(c) { + return c.charCodeAt(0) + } + + var decode = function decode(chars) { + return Uint8Array.from(atob(chars), asCharCode) + } + + exports.decode = decode + exports.encode = encode + + return exports +})({}) diff --git a/lnbits/extensions/cashu/static/js/dhke.js b/lnbits/extensions/cashu/static/js/dhke.js new file mode 100644 index 00000000..c9e2d146 --- /dev/null +++ b/lnbits/extensions/cashu/static/js/dhke.js @@ -0,0 +1,36 @@ +async function hashToCurve(secretMessage) { + console.log( + '### secretMessage', + nobleSecp256k1.utils.bytesToHex(secretMessage) + ) + let point + while (!point) { + const hash = await nobleSecp256k1.utils.sha256(secretMessage) + const hashHex = nobleSecp256k1.utils.bytesToHex(hash) + const pointX = '02' + hashHex + console.log('### pointX', pointX) + try { + point = nobleSecp256k1.Point.fromHex(pointX) + console.log('### point', point.toHex()) + } catch (error) { + secretMessage = await nobleSecp256k1.utils.sha256(secretMessage) + } + } + return point +} + +async function step1Bob(secretMessage) { + const Y = await hashToCurve(secretMessage) + const randomBlindingFactor = bytesToNumber( + nobleSecp256k1.utils.randomPrivateKey() + ) + const P = nobleSecp256k1.Point.fromPrivateKey(randomBlindingFactor) + const B_ = Y.add(P) + return {B_: B_.toHex(true), randomBlindingFactor} +} + +function step3Bob(C_, r, A) { + const rInt = BigInt(r) + const C = C_.subtract(A.multiply(rInt)) + return C +} diff --git a/lnbits/extensions/cashu/static/js/noble-secp256k1.js b/lnbits/extensions/cashu/static/js/noble-secp256k1.js new file mode 100644 index 00000000..6a6bd441 --- /dev/null +++ b/lnbits/extensions/cashu/static/js/noble-secp256k1.js @@ -0,0 +1,1178 @@ +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + ? factory(exports) + : typeof define === 'function' && define.amd + ? define(['exports'], factory) + : ((global = + typeof globalThis !== 'undefined' ? globalThis : global || self), + factory((global.nobleSecp256k1 = {}))) +})(this, function (exports) { + 'use strict' + + const _nodeResolve_empty = {} + + const nodeCrypto = /*#__PURE__*/ Object.freeze({ + __proto__: null, + default: _nodeResolve_empty + }) + + /*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ + const _0n = BigInt(0) + const _1n = BigInt(1) + const _2n = BigInt(2) + const _3n = BigInt(3) + const _8n = BigInt(8) + const POW_2_256 = _2n ** BigInt(256) + const CURVE = { + a: _0n, + b: BigInt(7), + P: POW_2_256 - _2n ** BigInt(32) - BigInt(977), + n: POW_2_256 - BigInt('432420386565659656852420866394968145599'), + h: _1n, + Gx: BigInt( + '55066263022277343669578718895168534326250603453777594175500187360389116729240' + ), + Gy: BigInt( + '32670510020758816978083085130507043184471273380659243275938904335757337482424' + ), + beta: BigInt( + '0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee' + ) + } + function weistrass(x) { + const {a, b} = CURVE + const x2 = mod(x * x) + const x3 = mod(x2 * x) + return mod(x3 + a * x + b) + } + const USE_ENDOMORPHISM = CURVE.a === _0n + class JacobianPoint { + constructor(x, y, z) { + this.x = x + this.y = y + this.z = z + } + static fromAffine(p) { + if (!(p instanceof Point)) { + throw new TypeError('JacobianPoint#fromAffine: expected Point') + } + return new JacobianPoint(p.x, p.y, _1n) + } + static toAffineBatch(points) { + const toInv = invertBatch(points.map(p => p.z)) + return points.map((p, i) => p.toAffine(toInv[i])) + } + static normalizeZ(points) { + return JacobianPoint.toAffineBatch(points).map(JacobianPoint.fromAffine) + } + equals(other) { + if (!(other instanceof JacobianPoint)) + throw new TypeError('JacobianPoint expected') + const {x: X1, y: Y1, z: Z1} = this + const {x: X2, y: Y2, z: Z2} = other + const Z1Z1 = mod(Z1 ** _2n) + const Z2Z2 = mod(Z2 ** _2n) + const U1 = mod(X1 * Z2Z2) + const U2 = mod(X2 * Z1Z1) + const S1 = mod(mod(Y1 * Z2) * Z2Z2) + const S2 = mod(mod(Y2 * Z1) * Z1Z1) + return U1 === U2 && S1 === S2 + } + negate() { + return new JacobianPoint(this.x, mod(-this.y), this.z) + } + double() { + const {x: X1, y: Y1, z: Z1} = this + const A = mod(X1 ** _2n) + const B = mod(Y1 ** _2n) + const C = mod(B ** _2n) + const D = mod(_2n * (mod((X1 + B) ** _2n) - A - C)) + const E = mod(_3n * A) + const F = mod(E ** _2n) + const X3 = mod(F - _2n * D) + const Y3 = mod(E * (D - X3) - _8n * C) + const Z3 = mod(_2n * Y1 * Z1) + return new JacobianPoint(X3, Y3, Z3) + } + add(other) { + if (!(other instanceof JacobianPoint)) + throw new TypeError('JacobianPoint expected') + const {x: X1, y: Y1, z: Z1} = this + const {x: X2, y: Y2, z: Z2} = other + if (X2 === _0n || Y2 === _0n) return this + if (X1 === _0n || Y1 === _0n) return other + const Z1Z1 = mod(Z1 ** _2n) + const Z2Z2 = mod(Z2 ** _2n) + const U1 = mod(X1 * Z2Z2) + const U2 = mod(X2 * Z1Z1) + const S1 = mod(mod(Y1 * Z2) * Z2Z2) + const S2 = mod(mod(Y2 * Z1) * Z1Z1) + const H = mod(U2 - U1) + const r = mod(S2 - S1) + if (H === _0n) { + if (r === _0n) { + return this.double() + } else { + return JacobianPoint.ZERO + } + } + const HH = mod(H ** _2n) + const HHH = mod(H * HH) + const V = mod(U1 * HH) + const X3 = mod(r ** _2n - HHH - _2n * V) + const Y3 = mod(r * (V - X3) - S1 * HHH) + const Z3 = mod(Z1 * Z2 * H) + return new JacobianPoint(X3, Y3, Z3) + } + subtract(other) { + return this.add(other.negate()) + } + multiplyUnsafe(scalar) { + const P0 = JacobianPoint.ZERO + if (typeof scalar === 'bigint' && scalar === _0n) return P0 + let n = normalizeScalar(scalar) + if (n === _1n) return this + if (!USE_ENDOMORPHISM) { + let p = P0 + let d = this + while (n > _0n) { + if (n & _1n) p = p.add(d) + d = d.double() + n >>= _1n + } + return p + } + let {k1neg, k1, k2neg, k2} = splitScalarEndo(n) + let k1p = P0 + let k2p = P0 + let d = this + while (k1 > _0n || k2 > _0n) { + if (k1 & _1n) k1p = k1p.add(d) + if (k2 & _1n) k2p = k2p.add(d) + d = d.double() + k1 >>= _1n + k2 >>= _1n + } + if (k1neg) k1p = k1p.negate() + if (k2neg) k2p = k2p.negate() + k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z) + return k1p.add(k2p) + } + precomputeWindow(W) { + const windows = USE_ENDOMORPHISM ? 128 / W + 1 : 256 / W + 1 + const points = [] + let p = this + let base = p + for (let window = 0; window < windows; window++) { + base = p + points.push(base) + for (let i = 1; i < 2 ** (W - 1); i++) { + base = base.add(p) + points.push(base) + } + p = base.double() + } + return points + } + wNAF(n, affinePoint) { + if (!affinePoint && this.equals(JacobianPoint.BASE)) + affinePoint = Point.BASE + const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1 + if (256 % W) { + throw new Error( + 'Point#wNAF: Invalid precomputation window, must be power of 2' + ) + } + let precomputes = affinePoint && pointPrecomputes.get(affinePoint) + if (!precomputes) { + precomputes = this.precomputeWindow(W) + if (affinePoint && W !== 1) { + precomputes = JacobianPoint.normalizeZ(precomputes) + pointPrecomputes.set(affinePoint, precomputes) + } + } + let p = JacobianPoint.ZERO + let f = JacobianPoint.ZERO + const windows = 1 + (USE_ENDOMORPHISM ? 128 / W : 256 / W) + const windowSize = 2 ** (W - 1) + const mask = BigInt(2 ** W - 1) + const maxNumber = 2 ** W + const shiftBy = BigInt(W) + for (let window = 0; window < windows; window++) { + const offset = window * windowSize + let wbits = Number(n & mask) + n >>= shiftBy + if (wbits > windowSize) { + wbits -= maxNumber + n += _1n + } + if (wbits === 0) { + let pr = precomputes[offset] + if (window % 2) pr = pr.negate() + f = f.add(pr) + } else { + let cached = precomputes[offset + Math.abs(wbits) - 1] + if (wbits < 0) cached = cached.negate() + p = p.add(cached) + } + } + return {p, f} + } + multiply(scalar, affinePoint) { + let n = normalizeScalar(scalar) + let point + let fake + if (USE_ENDOMORPHISM) { + const {k1neg, k1, k2neg, k2} = splitScalarEndo(n) + let {p: k1p, f: f1p} = this.wNAF(k1, affinePoint) + let {p: k2p, f: f2p} = this.wNAF(k2, affinePoint) + if (k1neg) k1p = k1p.negate() + if (k2neg) k2p = k2p.negate() + k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z) + point = k1p.add(k2p) + fake = f1p.add(f2p) + } else { + const {p, f} = this.wNAF(n, affinePoint) + point = p + fake = f + } + return JacobianPoint.normalizeZ([point, fake])[0] + } + toAffine(invZ = invert(this.z)) { + const {x, y, z} = this + const iz1 = invZ + const iz2 = mod(iz1 * iz1) + const iz3 = mod(iz2 * iz1) + const ax = mod(x * iz2) + const ay = mod(y * iz3) + const zz = mod(z * iz1) + if (zz !== _1n) throw new Error('invZ was invalid') + return new Point(ax, ay) + } + } + JacobianPoint.BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, _1n) + JacobianPoint.ZERO = new JacobianPoint(_0n, _1n, _0n) + const pointPrecomputes = new WeakMap() + class Point { + constructor(x, y) { + this.x = x + this.y = y + } + _setWindowSize(windowSize) { + this._WINDOW_SIZE = windowSize + pointPrecomputes.delete(this) + } + static fromCompressedHex(bytes) { + const isShort = bytes.length === 32 + const x = bytesToNumber(isShort ? bytes : bytes.subarray(1)) + if (!isValidFieldElement(x)) throw new Error('Point is not on curve') + const y2 = weistrass(x) + let y = sqrtMod(y2) + const isYOdd = (y & _1n) === _1n + if (isShort) { + if (isYOdd) y = mod(-y) + } else { + const isFirstByteOdd = (bytes[0] & 1) === 1 + if (isFirstByteOdd !== isYOdd) y = mod(-y) + } + const point = new Point(x, y) + point.assertValidity() + return point + } + static fromUncompressedHex(bytes) { + const x = bytesToNumber(bytes.subarray(1, 33)) + const y = bytesToNumber(bytes.subarray(33, 65)) + const point = new Point(x, y) + point.assertValidity() + return point + } + static fromHex(hex) { + const bytes = ensureBytes(hex) + const len = bytes.length + const header = bytes[0] + if (len === 32 || (len === 33 && (header === 0x02 || header === 0x03))) { + return this.fromCompressedHex(bytes) + } + if (len === 65 && header === 0x04) return this.fromUncompressedHex(bytes) + throw new Error( + `Point.fromHex: received invalid point. Expected 32-33 compressed bytes or 65 uncompressed bytes, not ${len}` + ) + } + static fromPrivateKey(privateKey) { + return Point.BASE.multiply(normalizePrivateKey(privateKey)) + } + static fromSignature(msgHash, signature, recovery) { + msgHash = ensureBytes(msgHash) + const h = truncateHash(msgHash) + const {r, s} = normalizeSignature(signature) + if (recovery !== 0 && recovery !== 1) { + throw new Error('Cannot recover signature: invalid recovery bit') + } + const prefix = recovery & 1 ? '03' : '02' + const R = Point.fromHex(prefix + numTo32bStr(r)) + const {n} = CURVE + const rinv = invert(r, n) + const u1 = mod(-h * rinv, n) + const u2 = mod(s * rinv, n) + const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2) + if (!Q) throw new Error('Cannot recover signature: point at infinify') + Q.assertValidity() + return Q + } + toRawBytes(isCompressed = false) { + return hexToBytes(this.toHex(isCompressed)) + } + toHex(isCompressed = false) { + const x = numTo32bStr(this.x) + if (isCompressed) { + const prefix = this.y & _1n ? '03' : '02' + return `${prefix}${x}` + } else { + return `04${x}${numTo32bStr(this.y)}` + } + } + toHexX() { + return this.toHex(true).slice(2) + } + toRawX() { + return this.toRawBytes(true).slice(1) + } + assertValidity() { + const msg = 'Point is not on elliptic curve' + const {x, y} = this + if (!isValidFieldElement(x) || !isValidFieldElement(y)) + throw new Error(msg) + const left = mod(y * y) + const right = weistrass(x) + if (mod(left - right) !== _0n) throw new Error(msg) + } + equals(other) { + return this.x === other.x && this.y === other.y + } + negate() { + return new Point(this.x, mod(-this.y)) + } + double() { + return JacobianPoint.fromAffine(this).double().toAffine() + } + add(other) { + return JacobianPoint.fromAffine(this) + .add(JacobianPoint.fromAffine(other)) + .toAffine() + } + subtract(other) { + return this.add(other.negate()) + } + multiply(scalar) { + return JacobianPoint.fromAffine(this).multiply(scalar, this).toAffine() + } + multiplyAndAddUnsafe(Q, a, b) { + const P = JacobianPoint.fromAffine(this) + const aP = + a === _0n || a === _1n || this !== Point.BASE + ? P.multiplyUnsafe(a) + : P.multiply(a) + const bQ = JacobianPoint.fromAffine(Q).multiplyUnsafe(b) + const sum = aP.add(bQ) + return sum.equals(JacobianPoint.ZERO) ? undefined : sum.toAffine() + } + } + Point.BASE = new Point(CURVE.Gx, CURVE.Gy) + Point.ZERO = new Point(_0n, _0n) + function sliceDER(s) { + return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s + } + function parseDERInt(data) { + if (data.length < 2 || data[0] !== 0x02) { + throw new Error(`Invalid signature integer tag: ${bytesToHex(data)}`) + } + const len = data[1] + const res = data.subarray(2, len + 2) + if (!len || res.length !== len) { + throw new Error(`Invalid signature integer: wrong length`) + } + if (res[0] === 0x00 && res[1] <= 0x7f) { + throw new Error('Invalid signature integer: trailing length') + } + return {data: bytesToNumber(res), left: data.subarray(len + 2)} + } + function parseDERSignature(data) { + if (data.length < 2 || data[0] != 0x30) { + throw new Error(`Invalid signature tag: ${bytesToHex(data)}`) + } + if (data[1] !== data.length - 2) { + throw new Error('Invalid signature: incorrect length') + } + const {data: r, left: sBytes} = parseDERInt(data.subarray(2)) + const {data: s, left: rBytesLeft} = parseDERInt(sBytes) + if (rBytesLeft.length) { + throw new Error( + `Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}` + ) + } + return {r, s} + } + class Signature { + constructor(r, s) { + this.r = r + this.s = s + this.assertValidity() + } + static fromCompact(hex) { + const arr = isUint8a(hex) + const name = 'Signature.fromCompact' + if (typeof hex !== 'string' && !arr) + throw new TypeError(`${name}: Expected string or Uint8Array`) + const str = arr ? bytesToHex(hex) : hex + if (str.length !== 128) throw new Error(`${name}: Expected 64-byte hex`) + return new Signature( + hexToNumber(str.slice(0, 64)), + hexToNumber(str.slice(64, 128)) + ) + } + static fromDER(hex) { + const arr = isUint8a(hex) + if (typeof hex !== 'string' && !arr) + throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`) + const {r, s} = parseDERSignature(arr ? hex : hexToBytes(hex)) + return new Signature(r, s) + } + static fromHex(hex) { + return this.fromDER(hex) + } + assertValidity() { + const {r, s} = this + if (!isWithinCurveOrder(r)) + throw new Error('Invalid Signature: r must be 0 < r < n') + if (!isWithinCurveOrder(s)) + throw new Error('Invalid Signature: s must be 0 < s < n') + } + hasHighS() { + const HALF = CURVE.n >> _1n + return this.s > HALF + } + normalizeS() { + return this.hasHighS() ? new Signature(this.r, CURVE.n - this.s) : this + } + toDERRawBytes(isCompressed = false) { + return hexToBytes(this.toDERHex(isCompressed)) + } + toDERHex(isCompressed = false) { + const sHex = sliceDER(numberToHexUnpadded(this.s)) + if (isCompressed) return sHex + const rHex = sliceDER(numberToHexUnpadded(this.r)) + const rLen = numberToHexUnpadded(rHex.length / 2) + const sLen = numberToHexUnpadded(sHex.length / 2) + const length = numberToHexUnpadded(rHex.length / 2 + sHex.length / 2 + 4) + return `30${length}02${rLen}${rHex}02${sLen}${sHex}` + } + toRawBytes() { + return this.toDERRawBytes() + } + toHex() { + return this.toDERHex() + } + toCompactRawBytes() { + return hexToBytes(this.toCompactHex()) + } + toCompactHex() { + return numTo32bStr(this.r) + numTo32bStr(this.s) + } + } + function concatBytes(...arrays) { + if (!arrays.every(isUint8a)) throw new Error('Uint8Array list expected') + if (arrays.length === 1) return arrays[0] + const length = arrays.reduce((a, arr) => a + arr.length, 0) + const result = new Uint8Array(length) + for (let i = 0, pad = 0; i < arrays.length; i++) { + const arr = arrays[i] + result.set(arr, pad) + pad += arr.length + } + return result + } + function isUint8a(bytes) { + return bytes instanceof Uint8Array + } + const hexes = Array.from({length: 256}, (v, i) => + i.toString(16).padStart(2, '0') + ) + function bytesToHex(uint8a) { + if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array') + let hex = '' + for (let i = 0; i < uint8a.length; i++) { + hex += hexes[uint8a[i]] + } + return hex + } + function numTo32bStr(num) { + if (num > POW_2_256) throw new Error('Expected number < 2^256') + return num.toString(16).padStart(64, '0') + } + function numTo32b(num) { + return hexToBytes(numTo32bStr(num)) + } + function numberToHexUnpadded(num) { + const hex = num.toString(16) + return hex.length & 1 ? `0${hex}` : hex + } + function hexToNumber(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToNumber: expected string, got ' + typeof hex) + } + return BigInt(`0x${hex}`) + } + function hexToBytes(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToBytes: expected string, got ' + typeof hex) + } + if (hex.length % 2) + throw new Error('hexToBytes: received invalid unpadded hex' + hex.length) + const array = new Uint8Array(hex.length / 2) + for (let i = 0; i < array.length; i++) { + const j = i * 2 + const hexByte = hex.slice(j, j + 2) + const byte = Number.parseInt(hexByte, 16) + if (Number.isNaN(byte) || byte < 0) + throw new Error('Invalid byte sequence') + array[i] = byte + } + return array + } + function bytesToNumber(bytes) { + return hexToNumber(bytesToHex(bytes)) + } + function ensureBytes(hex) { + return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex) + } + function normalizeScalar(num) { + if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0) + return BigInt(num) + if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num + throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n') + } + function mod(a, b = CURVE.P) { + const result = a % b + return result >= _0n ? result : b + result + } + function pow2(x, power) { + const {P} = CURVE + let res = x + while (power-- > _0n) { + res *= res + res %= P + } + return res + } + function sqrtMod(x) { + const {P} = CURVE + const _6n = BigInt(6) + const _11n = BigInt(11) + const _22n = BigInt(22) + const _23n = BigInt(23) + const _44n = BigInt(44) + const _88n = BigInt(88) + const b2 = (x * x * x) % P + const b3 = (b2 * b2 * x) % P + const b6 = (pow2(b3, _3n) * b3) % P + const b9 = (pow2(b6, _3n) * b3) % P + const b11 = (pow2(b9, _2n) * b2) % P + const b22 = (pow2(b11, _11n) * b11) % P + const b44 = (pow2(b22, _22n) * b22) % P + const b88 = (pow2(b44, _44n) * b44) % P + const b176 = (pow2(b88, _88n) * b88) % P + const b220 = (pow2(b176, _44n) * b44) % P + const b223 = (pow2(b220, _3n) * b3) % P + const t1 = (pow2(b223, _23n) * b22) % P + const t2 = (pow2(t1, _6n) * b2) % P + return pow2(t2, _2n) + } + function invert(number, modulo = CURVE.P) { + if (number === _0n || modulo <= _0n) { + throw new Error( + `invert: expected positive integers, got n=${number} mod=${modulo}` + ) + } + let a = mod(number, modulo) + let b = modulo + let x = _0n, + u = _1n + while (a !== _0n) { + const q = b / a + const r = b % a + const m = x - u * q + ;(b = a), (a = r), (x = u), (u = m) + } + const gcd = b + if (gcd !== _1n) throw new Error('invert: does not exist') + return mod(x, modulo) + } + function invertBatch(nums, p = CURVE.P) { + const scratch = new Array(nums.length) + const lastMultiplied = nums.reduce((acc, num, i) => { + if (num === _0n) return acc + scratch[i] = acc + return mod(acc * num, p) + }, _1n) + const inverted = invert(lastMultiplied, p) + nums.reduceRight((acc, num, i) => { + if (num === _0n) return acc + scratch[i] = mod(acc * scratch[i], p) + return mod(acc * num, p) + }, inverted) + return scratch + } + const divNearest = (a, b) => (a + b / _2n) / b + const POW_2_128 = _2n ** BigInt(128) + function splitScalarEndo(k) { + const {n} = CURVE + const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15') + const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3') + const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8') + const b2 = a1 + const c1 = divNearest(b2 * k, n) + const c2 = divNearest(-b1 * k, n) + let k1 = mod(k - c1 * a1 - c2 * a2, n) + let k2 = mod(-c1 * b1 - c2 * b2, n) + const k1neg = k1 > POW_2_128 + const k2neg = k2 > POW_2_128 + if (k1neg) k1 = n - k1 + if (k2neg) k2 = n - k2 + if (k1 > POW_2_128 || k2 > POW_2_128) { + throw new Error('splitScalarEndo: Endomorphism failed, k=' + k) + } + return {k1neg, k1, k2neg, k2} + } + function truncateHash(hash) { + const {n} = CURVE + const byteLength = hash.length + const delta = byteLength * 8 - 256 + let h = bytesToNumber(hash) + if (delta > 0) h = h >> BigInt(delta) + if (h >= n) h -= n + return h + } + class HmacDrbg { + constructor() { + this.v = new Uint8Array(32).fill(1) + this.k = new Uint8Array(32).fill(0) + this.counter = 0 + } + hmac(...values) { + return utils.hmacSha256(this.k, ...values) + } + hmacSync(...values) { + if (typeof utils.hmacSha256Sync !== 'function') + throw new Error('utils.hmacSha256Sync is undefined, you need to set it') + const res = utils.hmacSha256Sync(this.k, ...values) + if (res instanceof Promise) + throw new Error('To use sync sign(), ensure utils.hmacSha256 is sync') + return res + } + incr() { + if (this.counter >= 1000) { + throw new Error('Tried 1,000 k values for sign(), all were invalid') + } + this.counter += 1 + } + async reseed(seed = new Uint8Array()) { + this.k = await this.hmac(this.v, Uint8Array.from([0x00]), seed) + this.v = await this.hmac(this.v) + if (seed.length === 0) return + this.k = await this.hmac(this.v, Uint8Array.from([0x01]), seed) + this.v = await this.hmac(this.v) + } + reseedSync(seed = new Uint8Array()) { + this.k = this.hmacSync(this.v, Uint8Array.from([0x00]), seed) + this.v = this.hmacSync(this.v) + if (seed.length === 0) return + this.k = this.hmacSync(this.v, Uint8Array.from([0x01]), seed) + this.v = this.hmacSync(this.v) + } + async generate() { + this.incr() + this.v = await this.hmac(this.v) + return this.v + } + generateSync() { + this.incr() + this.v = this.hmacSync(this.v) + return this.v + } + } + function isWithinCurveOrder(num) { + return _0n < num && num < CURVE.n + } + function isValidFieldElement(num) { + return _0n < num && num < CURVE.P + } + function kmdToSig(kBytes, m, d) { + const k = bytesToNumber(kBytes) + if (!isWithinCurveOrder(k)) return + const {n} = CURVE + const q = Point.BASE.multiply(k) + const r = mod(q.x, n) + if (r === _0n) return + const s = mod(invert(k, n) * mod(m + d * r, n), n) + if (s === _0n) return + const sig = new Signature(r, s) + const recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n) + return {sig, recovery} + } + function normalizePrivateKey(key) { + let num + if (typeof key === 'bigint') { + num = key + } else if ( + typeof key === 'number' && + Number.isSafeInteger(key) && + key > 0 + ) { + num = BigInt(key) + } else if (typeof key === 'string') { + if (key.length !== 64) throw new Error('Expected 32 bytes of private key') + num = hexToNumber(key) + } else if (isUint8a(key)) { + if (key.length !== 32) throw new Error('Expected 32 bytes of private key') + num = bytesToNumber(key) + } else { + throw new TypeError('Expected valid private key') + } + if (!isWithinCurveOrder(num)) + throw new Error('Expected private key: 0 < key < n') + return num + } + function normalizePublicKey(publicKey) { + if (publicKey instanceof Point) { + publicKey.assertValidity() + return publicKey + } else { + return Point.fromHex(publicKey) + } + } + function normalizeSignature(signature) { + if (signature instanceof Signature) { + signature.assertValidity() + return signature + } + try { + return Signature.fromDER(signature) + } catch (error) { + return Signature.fromCompact(signature) + } + } + function getPublicKey(privateKey, isCompressed = false) { + return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed) + } + function recoverPublicKey( + msgHash, + signature, + recovery, + isCompressed = false + ) { + return Point.fromSignature(msgHash, signature, recovery).toRawBytes( + isCompressed + ) + } + function isPub(item) { + const arr = isUint8a(item) + const str = typeof item === 'string' + const len = (arr || str) && item.length + if (arr) return len === 33 || len === 65 + if (str) return len === 66 || len === 130 + if (item instanceof Point) return true + return false + } + function getSharedSecret(privateA, publicB, isCompressed = false) { + if (isPub(privateA)) + throw new TypeError('getSharedSecret: first arg must be private key') + if (!isPub(publicB)) + throw new TypeError('getSharedSecret: second arg must be public key') + const b = normalizePublicKey(publicB) + b.assertValidity() + return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed) + } + function bits2int(bytes) { + const slice = bytes.length > 32 ? bytes.slice(0, 32) : bytes + return bytesToNumber(slice) + } + function bits2octets(bytes) { + const z1 = bits2int(bytes) + const z2 = mod(z1, CURVE.n) + return int2octets(z2 < _0n ? z1 : z2) + } + function int2octets(num) { + if (typeof num !== 'bigint') throw new Error('Expected bigint') + const hex = numTo32bStr(num) + return hexToBytes(hex) + } + function initSigArgs(msgHash, privateKey, extraEntropy) { + if (msgHash == null) + throw new Error(`sign: expected valid message hash, not "${msgHash}"`) + const h1 = ensureBytes(msgHash) + const d = normalizePrivateKey(privateKey) + const seedArgs = [int2octets(d), bits2octets(h1)] + if (extraEntropy != null) { + if (extraEntropy === true) extraEntropy = utils.randomBytes(32) + const e = ensureBytes(extraEntropy) + if (e.length !== 32) + throw new Error('sign: Expected 32 bytes of extra data') + seedArgs.push(e) + } + const seed = concatBytes(...seedArgs) + const m = bits2int(h1) + return {seed, m, d} + } + function finalizeSig(recSig, opts) { + let {sig, recovery} = recSig + const {canonical, der, recovered} = Object.assign( + {canonical: true, der: true}, + opts + ) + if (canonical && sig.hasHighS()) { + sig = sig.normalizeS() + recovery ^= 1 + } + const hashed = der ? sig.toDERRawBytes() : sig.toCompactRawBytes() + return recovered ? [hashed, recovery] : hashed + } + async function sign(msgHash, privKey, opts = {}) { + const {seed, m, d} = initSigArgs(msgHash, privKey, opts.extraEntropy) + let sig + const drbg = new HmacDrbg() + await drbg.reseed(seed) + while (!(sig = kmdToSig(await drbg.generate(), m, d))) await drbg.reseed() + return finalizeSig(sig, opts) + } + function signSync(msgHash, privKey, opts = {}) { + const {seed, m, d} = initSigArgs(msgHash, privKey, opts.extraEntropy) + let sig + const drbg = new HmacDrbg() + drbg.reseedSync(seed) + while (!(sig = kmdToSig(drbg.generateSync(), m, d))) drbg.reseedSync() + return finalizeSig(sig, opts) + } + const vopts = {strict: true} + function verify(signature, msgHash, publicKey, opts = vopts) { + let sig + try { + sig = normalizeSignature(signature) + msgHash = ensureBytes(msgHash) + } catch (error) { + return false + } + const {r, s} = sig + if (opts.strict && sig.hasHighS()) return false + const h = truncateHash(msgHash) + let P + try { + P = normalizePublicKey(publicKey) + } catch (error) { + return false + } + const {n} = CURVE + const sinv = invert(s, n) + const u1 = mod(h * sinv, n) + const u2 = mod(r * sinv, n) + const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2) + if (!R) return false + const v = mod(R.x, n) + return v === r + } + function finalizeSchnorrChallenge(ch) { + return mod(bytesToNumber(ch), CURVE.n) + } + function hasEvenY(point) { + return (point.y & _1n) === _0n + } + class SchnorrSignature { + constructor(r, s) { + this.r = r + this.s = s + this.assertValidity() + } + static fromHex(hex) { + const bytes = ensureBytes(hex) + if (bytes.length !== 64) + throw new TypeError( + `SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}` + ) + const r = bytesToNumber(bytes.subarray(0, 32)) + const s = bytesToNumber(bytes.subarray(32, 64)) + return new SchnorrSignature(r, s) + } + assertValidity() { + const {r, s} = this + if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) + throw new Error('Invalid signature') + } + toHex() { + return numTo32bStr(this.r) + numTo32bStr(this.s) + } + toRawBytes() { + return hexToBytes(this.toHex()) + } + } + function schnorrGetPublicKey(privateKey) { + return Point.fromPrivateKey(privateKey).toRawX() + } + function initSchnorrSigArgs(message, privateKey, auxRand) { + if (message == null) + throw new TypeError(`sign: Expected valid message, not "${message}"`) + const m = ensureBytes(message) + const d0 = normalizePrivateKey(privateKey) + const rand = ensureBytes(auxRand) + if (rand.length !== 32) + throw new TypeError('sign: Expected 32 bytes of aux randomness') + const P = Point.fromPrivateKey(d0) + const px = P.toRawX() + const d = hasEvenY(P) ? d0 : CURVE.n - d0 + return {m, P, px, d, rand} + } + function initSchnorrNonce(d, t0h) { + return numTo32b(d ^ bytesToNumber(t0h)) + } + function finalizeSchnorrNonce(k0h) { + const k0 = mod(bytesToNumber(k0h), CURVE.n) + if (k0 === _0n) + throw new Error('sign: Creation of signature failed. k is zero') + const R = Point.fromPrivateKey(k0) + const rx = R.toRawX() + const k = hasEvenY(R) ? k0 : CURVE.n - k0 + return {R, rx, k} + } + function finalizeSchnorrSig(R, k, e, d) { + return new SchnorrSignature(R.x, mod(k + e * d, CURVE.n)).toRawBytes() + } + async function schnorrSign( + message, + privateKey, + auxRand = utils.randomBytes() + ) { + const {m, px, d, rand} = initSchnorrSigArgs(message, privateKey, auxRand) + const t = initSchnorrNonce(d, await utils.taggedHash(TAGS.aux, rand)) + const {R, rx, k} = finalizeSchnorrNonce( + await utils.taggedHash(TAGS.nonce, t, px, m) + ) + const e = finalizeSchnorrChallenge( + await utils.taggedHash(TAGS.challenge, rx, px, m) + ) + const sig = finalizeSchnorrSig(R, k, e, d) + const isValid = await schnorrVerify(sig, m, px) + if (!isValid) throw new Error('sign: Invalid signature produced') + return sig + } + function schnorrSignSync(message, privateKey, auxRand = utils.randomBytes()) { + const {m, px, d, rand} = initSchnorrSigArgs(message, privateKey, auxRand) + const t = initSchnorrNonce(d, utils.taggedHashSync(TAGS.aux, rand)) + const {R, rx, k} = finalizeSchnorrNonce( + utils.taggedHashSync(TAGS.nonce, t, px, m) + ) + const e = finalizeSchnorrChallenge( + utils.taggedHashSync(TAGS.challenge, rx, px, m) + ) + const sig = finalizeSchnorrSig(R, k, e, d) + const isValid = schnorrVerifySync(sig, m, px) + if (!isValid) throw new Error('sign: Invalid signature produced') + return sig + } + function initSchnorrVerify(signature, message, publicKey) { + const raw = signature instanceof SchnorrSignature + const sig = raw ? signature : SchnorrSignature.fromHex(signature) + if (raw) sig.assertValidity() + return { + ...sig, + m: ensureBytes(message), + P: normalizePublicKey(publicKey) + } + } + function finalizeSchnorrVerify(r, P, s, e) { + const R = Point.BASE.multiplyAndAddUnsafe( + P, + normalizePrivateKey(s), + mod(-e, CURVE.n) + ) + if (!R || !hasEvenY(R) || R.x !== r) return false + return true + } + async function schnorrVerify(signature, message, publicKey) { + try { + const {r, s, m, P} = initSchnorrVerify(signature, message, publicKey) + const e = finalizeSchnorrChallenge( + await utils.taggedHash(TAGS.challenge, numTo32b(r), P.toRawX(), m) + ) + return finalizeSchnorrVerify(r, P, s, e) + } catch (error) { + return false + } + } + function schnorrVerifySync(signature, message, publicKey) { + try { + const {r, s, m, P} = initSchnorrVerify(signature, message, publicKey) + const e = finalizeSchnorrChallenge( + utils.taggedHashSync(TAGS.challenge, numTo32b(r), P.toRawX(), m) + ) + return finalizeSchnorrVerify(r, P, s, e) + } catch (error) { + return false + } + } + const schnorr = { + Signature: SchnorrSignature, + getPublicKey: schnorrGetPublicKey, + sign: schnorrSign, + verify: schnorrVerify, + signSync: schnorrSignSync, + verifySync: schnorrVerifySync + } + Point.BASE._setWindowSize(8) + const crypto = { + node: nodeCrypto, + web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined + } + const TAGS = { + challenge: 'BIP0340/challenge', + aux: 'BIP0340/aux', + nonce: 'BIP0340/nonce' + } + const TAGGED_HASH_PREFIXES = {} + const utils = { + isValidPrivateKey(privateKey) { + try { + normalizePrivateKey(privateKey) + return true + } catch (error) { + return false + } + }, + privateAdd: (privateKey, tweak) => { + const p = normalizePrivateKey(privateKey) + const t = normalizePrivateKey(tweak) + return numTo32b(mod(p + t, CURVE.n)) + }, + privateNegate: privateKey => { + const p = normalizePrivateKey(privateKey) + return numTo32b(CURVE.n - p) + }, + pointAddScalar: (p, tweak, isCompressed) => { + const P = Point.fromHex(p) + const t = normalizePrivateKey(tweak) + const Q = Point.BASE.multiplyAndAddUnsafe(P, t, _1n) + if (!Q) throw new Error('Tweaked point at infinity') + return Q.toRawBytes(isCompressed) + }, + pointMultiply: (p, tweak, isCompressed) => { + const P = Point.fromHex(p) + const t = bytesToNumber(ensureBytes(tweak)) + return P.multiply(t).toRawBytes(isCompressed) + }, + hashToPrivateKey: hash => { + hash = ensureBytes(hash) + if (hash.length < 40 || hash.length > 1024) + throw new Error('Expected 40-1024 bytes of private key as per FIPS 186') + const num = mod(bytesToNumber(hash), CURVE.n - _1n) + _1n + return numTo32b(num) + }, + randomBytes: (bytesLength = 32) => { + if (crypto.web) { + return crypto.web.getRandomValues(new Uint8Array(bytesLength)) + } else if (crypto.node) { + const {randomBytes} = crypto.node + return Uint8Array.from(randomBytes(bytesLength)) + } else { + throw new Error("The environment doesn't have randomBytes function") + } + }, + randomPrivateKey: () => { + return utils.hashToPrivateKey(utils.randomBytes(40)) + }, + bytesToHex, + hexToBytes, + concatBytes, + mod, + invert, + sha256: async (...messages) => { + if (crypto.web) { + const buffer = await crypto.web.subtle.digest( + 'SHA-256', + concatBytes(...messages) + ) + return new Uint8Array(buffer) + } else if (crypto.node) { + const {createHash} = crypto.node + const hash = createHash('sha256') + messages.forEach(m => hash.update(m)) + return Uint8Array.from(hash.digest()) + } else { + throw new Error("The environment doesn't have sha256 function") + } + }, + hmacSha256: async (key, ...messages) => { + if (crypto.web) { + const ckey = await crypto.web.subtle.importKey( + 'raw', + key, + {name: 'HMAC', hash: {name: 'SHA-256'}}, + false, + ['sign'] + ) + const message = concatBytes(...messages) + const buffer = await crypto.web.subtle.sign('HMAC', ckey, message) + return new Uint8Array(buffer) + } else if (crypto.node) { + const {createHmac} = crypto.node + const hash = createHmac('sha256', key) + messages.forEach(m => hash.update(m)) + return Uint8Array.from(hash.digest()) + } else { + throw new Error("The environment doesn't have hmac-sha256 function") + } + }, + sha256Sync: undefined, + hmacSha256Sync: undefined, + taggedHash: async (tag, ...messages) => { + let tagP = TAGGED_HASH_PREFIXES[tag] + if (tagP === undefined) { + const tagH = await utils.sha256( + Uint8Array.from(tag, c => c.charCodeAt(0)) + ) + tagP = concatBytes(tagH, tagH) + TAGGED_HASH_PREFIXES[tag] = tagP + } + return utils.sha256(tagP, ...messages) + }, + taggedHashSync: (tag, ...messages) => { + if (typeof utils.sha256Sync !== 'function') + throw new Error('utils.sha256Sync is undefined, you need to set it') + let tagP = TAGGED_HASH_PREFIXES[tag] + if (tagP === undefined) { + const tagH = utils.sha256Sync( + Uint8Array.from(tag, c => c.charCodeAt(0)) + ) + tagP = concatBytes(tagH, tagH) + TAGGED_HASH_PREFIXES[tag] = tagP + } + return utils.sha256Sync(tagP, ...messages) + }, + precompute(windowSize = 8, point = Point.BASE) { + const cached = point === Point.BASE ? point : new Point(point.x, point.y) + cached._setWindowSize(windowSize) + cached.multiply(_3n) + return cached + } + } + + exports.CURVE = CURVE + exports.Point = Point + exports.Signature = Signature + exports.getPublicKey = getPublicKey + exports.getSharedSecret = getSharedSecret + exports.recoverPublicKey = recoverPublicKey + exports.schnorr = schnorr + exports.sign = sign + exports.signSync = signSync + exports.utils = utils + exports.verify = verify + + Object.defineProperty(exports, '__esModule', {value: true}) +}) diff --git a/lnbits/extensions/cashu/static/js/utils.js b/lnbits/extensions/cashu/static/js/utils.js new file mode 100644 index 00000000..cf852b58 --- /dev/null +++ b/lnbits/extensions/cashu/static/js/utils.js @@ -0,0 +1,23 @@ +function splitAmount(value) { + const chunks = [] + for (let i = 0; i < 32; i++) { + const mask = 1 << i + if ((value & mask) !== 0) chunks.push(Math.pow(2, i)) + } + return chunks +} + +function bytesToNumber(bytes) { + return hexToNumber(nobleSecp256k1.utils.bytesToHex(bytes)) +} + +function bigIntStringify(key, value) { + return typeof value === 'bigint' ? value.toString() : value +} + +function hexToNumber(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToNumber: expected string, got ' + typeof hex) + } + return BigInt(`0x${hex}`) +} diff --git a/lnbits/extensions/cashu/tasks.py b/lnbits/extensions/cashu/tasks.py index 5fbdde8e..fe00a591 100644 --- a/lnbits/extensions/cashu/tasks.py +++ b/lnbits/extensions/cashu/tasks.py @@ -9,6 +9,7 @@ from lnbits.tasks import internal_invoice_queue, register_invoice_listener from .crud import get_cashu + async def wait_for_paid_invoices(): invoice_queue = asyncio.Queue() register_invoice_listener(invoice_queue) diff --git a/lnbits/extensions/cashu/templates/cashu/_api_docs.html b/lnbits/extensions/cashu/templates/cashu/_api_docs.html index 7378eb08..3476d41a 100644 --- a/lnbits/extensions/cashu/templates/cashu/_api_docs.html +++ b/lnbits/extensions/cashu/templates/cashu/_api_docs.html @@ -71,7 +71,8 @@
Curl example
curl -X DELETE {{ request.base_url - }}cashu/api/v1/cashus/<cashu_id> -H "X-Api-Key: <admin_key>" + }}cashu/api/v1/cashus/<cashu_id> -H "X-Api-Key: + <admin_key>" diff --git a/lnbits/extensions/cashu/templates/cashu/_cashu.html b/lnbits/extensions/cashu/templates/cashu/_cashu.html index 3c2a38f5..f5af738f 100644 --- a/lnbits/extensions/cashu/templates/cashu/_cashu.html +++ b/lnbits/extensions/cashu/templates/cashu/_cashu.html @@ -2,13 +2,12 @@

- Make Ecash mints with peg in/out to a wallet, that can create and manage ecash. + Make Ecash mints with peg in/out to a wallet, that can create and manage + ecash.

Created by - Calle.Calle.
diff --git a/lnbits/extensions/cashu/templates/cashu/index.html b/lnbits/extensions/cashu/templates/cashu/index.html index 3cd57d45..4e5ba911 100644 --- a/lnbits/extensions/cashu/templates/cashu/index.html +++ b/lnbits/extensions/cashu/templates/cashu/index.html @@ -4,7 +4,9 @@
- New Mint + New Mint @@ -18,8 +20,14 @@ Export to CSV
- + {% raw %}