From 2c8ebaf225c2c0504f2dcd829b6f7e57aa517649 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 13:44:26 +0300 Subject: [PATCH] feat: return promises --- lnbits/extensions/cashu/core/b_dhke.py | 93 ++++++++++++++++++++++++++ lnbits/extensions/cashu/crud.py | 2 +- lnbits/extensions/cashu/mint.py | 47 +++++++------ lnbits/extensions/cashu/mint_helper.py | 25 +++---- lnbits/extensions/cashu/models.py | 3 +- lnbits/extensions/cashu/views_api.py | 34 ++++++---- 6 files changed, 158 insertions(+), 46 deletions(-) create mode 100644 lnbits/extensions/cashu/core/b_dhke.py diff --git a/lnbits/extensions/cashu/core/b_dhke.py b/lnbits/extensions/cashu/core/b_dhke.py new file mode 100644 index 00000000..be9a141b --- /dev/null +++ b/lnbits/extensions/cashu/core/b_dhke.py @@ -0,0 +1,93 @@ +# Don't trust me with cryptography. + +""" +Implementation of https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406 +Alice: +A = a*G +return A +Bob: +Y = hash_to_point(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_point(secret_message) +C == a*Y +If true, C must have originated from Alice +""" + +import hashlib + +from secp256k1 import PrivateKey, PublicKey + + +def hash_to_point(secret_msg): + """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 + msg = secret_msg + while point is None: + _hash = hashlib.sha256(msg).hexdigest().encode("utf-8") + try: + # We construct compressed pub which has x coordinate encoded with even y + _hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes + _hash[0] = 0x02 # set first byte to represent even y coord + _hash = bytes(_hash) + point = PublicKey(_hash, raw=True) + except: + msg = _hash + + return point + + +def step1_alice(secret_msg): + secret_msg = secret_msg.encode("utf-8") + Y = hash_to_point(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_point(secret_msg.encode("utf-8")) + 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/crud.py b/lnbits/extensions/cashu/crud.py index 733f4737..39af1f8c 100644 --- a/lnbits/extensions/cashu/crud.py +++ b/lnbits/extensions/cashu/crud.py @@ -93,7 +93,7 @@ async def delete_cashu(cashu_id) -> None: ########################################## -async def store_promise(amount: int, B_: str, C_: str, cashu_id): +async def store_promise(amount: int, B_: str, C_: str, cashu_id: str): promise_id = urlsafe_short_hash() await db.execute( diff --git a/lnbits/extensions/cashu/mint.py b/lnbits/extensions/cashu/mint.py index c2568313..b1bdd042 100644 --- a/lnbits/extensions/cashu/mint.py +++ b/lnbits/extensions/cashu/mint.py @@ -1,5 +1,12 @@ -from .mint_helper import derive_keys, derive_pubkeys -from .models import Cashu +from typing import List + +from .core.b_dhke import step2_bob +from .core.base import BlindedSignature +from .core.secp import PublicKey +from .mint_helper import derive_key, derive_keys, derive_pubkeys + +# todo: extract const +MAX_ORDER = 64 def get_pubkeys(xpriv: str): @@ -11,22 +18,24 @@ def get_pubkeys(xpriv: str): return {a: p.serialize().hex() for a, p in pub_keys.items()} -# async def mint(self, B_s: List[PublicKey], amounts: List[int], payment_hash=None): -# """Mints a promise for coins for B_.""" -# # check if lightning invoice was paid -# if LIGHTNING: -# try: -# paid = await self._check_lightning_invoice(payment_hash) -# except: -# raise Exception("could not check invoice.") -# if not paid: -# raise Exception("Lightning invoice not paid yet.") +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}.") + 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 self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts) -# ] -# return promises + 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_key(master_prvkey, amount) # Get the correct key + C_ = step2_bob(B_, secret_key) + return BlindedSignature(amount=amount, C_=C_.serialize().hex()) diff --git a/lnbits/extensions/cashu/mint_helper.py b/lnbits/extensions/cashu/mint_helper.py index 1cf631b4..50227733 100644 --- a/lnbits/extensions/cashu/mint_helper.py +++ b/lnbits/extensions/cashu/mint_helper.py @@ -1,7 +1,7 @@ import hashlib -from typing import List, Set +from typing import List -from .core.secp import PrivateKey, PublicKey +from .core.secp import PrivateKey # todo: extract const MAX_ORDER = 64 @@ -9,16 +9,17 @@ 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) - } + return {2**i: derive_key(master_key, i) for i in range(MAX_ORDER)} + + +def derive_key(master_key: str, i: int): + """Deterministic derivation of keys for a particular value.""" + return PrivateKey( + hashlib.sha256((str(master_key) + str(i)).encode("utf-8")) + .hexdigest() + .encode("utf-8")[:32], + raw=True, + ) def derive_pubkeys(keys: List[PrivateKey]): diff --git a/lnbits/extensions/cashu/models.py b/lnbits/extensions/cashu/models.py index 8b5a3417..8b7558d2 100644 --- a/lnbits/extensions/cashu/models.py +++ b/lnbits/extensions/cashu/models.py @@ -147,7 +147,8 @@ class MeltPayload(BaseModel): amount: int invoice: str + class CreateTokens(BaseModel): # cashu_id: str = Query(None) payloads: MintPayloads - payment_hash: Union[str, None] = None \ No newline at end of file + payment_hash: Union[str, None] = None diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 6f1e161a..93902356 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -24,9 +24,10 @@ from .crud import ( get_cashus, get_lightning_invoice, store_lightning_invoice, + store_promise, ) from .ledger import mint, request_mint -from .mint import get_pubkeys +from .mint import generate_promises, get_pubkeys from .models import ( Cashu, CheckPayload, @@ -280,22 +281,29 @@ async def mint_coins( # if invoice.issued == True: # todo: give old tokens? - status: PaymentStatus = await check_transaction_status(cashu.wallet, data.payment_hash) - if status.paid != True: + status: PaymentStatus = await check_transaction_status( + cashu.wallet, data.payment_hash + ) + # todo: revert to: status.paid != True: + if status.paid == False: raise HTTPException( status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." ) - # amounts = [] - # B_s = [] - # for payload in payloads.blinded_messages: - # amounts.append(payload.amount) - # B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True)) - # try: - # promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash) - # return promises - # except Exception as exc: - # return CashuError(error=str(exc)) + amounts = [] + B_s = [] + for payload in data.payloads.blinded_messages: + amounts.append(payload.amount) + B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True)) + print("### amounts", amounts) + print("### B_s", B_s) + + try: + promises = await generate_promises(cashu.prvkey, amounts, B_s) + # await store_promise(amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), cashu_id) + return promises + except Exception as exc: + return CashuError(error=str(exc)) @cashu_ext.post("/melt")