From 347de240307cdde83a3c17e1275cfb31bf47728a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 28 Jan 2023 01:44:16 +0100 Subject: [PATCH 01/59] produce dleq --- cashu/core/b_dhke.py | 60 +++++++++++++++++++++++++++++++++++++++--- cashu/core/base.py | 21 +++++++++++++++ cashu/mint/ledger.py | 12 ++++++--- cashu/wallet/wallet.py | 2 ++ tests/test_cli.py | 1 + 5 files changed, 89 insertions(+), 7 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index 80735efb..306ed773 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -28,6 +28,26 @@ Y = hash_to_curve(secret_message) C == a*Y If true, C must have originated from Bob + + +# DLEQ Proof + +(These steps occur once Bob returns C') + +Bob: + r = random nonce +R1 = r*G +R2 = r*B' + e = hash(R1,R2,A,C') + s = r + e*a +return e, s + +Alice: +R1 = e*A - s*G +R2 = e*C'- s*B' +e == hash(R1,R2,A,C') + +If true, a in A = a*G must be equal to a in C' = a*B' """ import hashlib @@ -59,21 +79,53 @@ def step1_alice(secret_msg: str, blinding_factor: bytes = None): return B_, r -def step2_bob(B_, a): +def step2_bob(B_: PublicKey, a: PrivateKey): C_ = B_.mult(a) - return C_ + + # produce dleq proof + e, s = step2_bob_dleq(B_, a) + return C_, e, s def step3_alice(C_, r, A): C = C_ - A.mult(r) return C - -def verify(a, C, secret_msg): +def bob_verify(a, C, secret_msg): Y = hash_to_curve(secret_msg.encode("utf-8")) return C == Y.mult(a) +# DLEQ + +# Bob: +# r = random nonce +# R1 = r*G +# R2 = r*B' +# e = hash(R1,R2,A,C') +# s = r + e*a +# return e, s + +# Alice: +# R1 = e*A - s*G +# R2 = e*C'- s*B' +# e == hash(R1,R2,A,C') + +def step2_bob_dleq(B_: PublicKey, a: PrivateKey): + r = PrivateKey() # generate random value + R1 = PrivateKey(privkey=r.private_key, raw=True) + R2 = B_.mult(r) + e = hashlib.sha256( + R1.serialize().encode() + + PrivateKey(R2.serialize()[1:], raw=True).serialize().encode() + + r.serialize().encode() + + a.serialize().encode() + ).digest() + s = r.pubkey + a.pubkey.mult(PrivateKey(privkey=e, raw=True)) + return e, s + +def alice_verify_dleq(): + ### Below is a test of a simple positive and negative case # # Alice's keys diff --git a/cashu/core/base.py b/cashu/core/base.py index 5ad63445..39b865ab 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -19,6 +19,15 @@ class P2SHScript(BaseModel): address: Union[str, None] = None +class DLEQ(BaseModel): + """ + Discrete Log Equality (DLEQ) Proof + """ + + e: str + s: str + + class Proof(BaseModel): """ Value token @@ -31,6 +40,7 @@ class Proof(BaseModel): secret: str = "" # secret or message to be blinded and signed C: str = "" # signature on secret, unblinded by wallet script: Union[P2SHScript, None] = None # P2SH spending condition + dleq: Union[DLEQ, None] = None # DLEQ proof reserved: Union[ None, bool ] = False # whether this proof is reserved for sending, used for coin management in the wallet @@ -41,6 +51,16 @@ class Proof(BaseModel): time_reserved: Union[None, str] = "" def to_dict(self): + # dictionary without the fields that don't need to be send to Carol + return dict( + id=self.id, + amount=self.amount, + secret=self.secret, + C=self.C, + dleq=self.dleq.dict(), + ) + + def to_dict_no_dleq(self): # dictionary without the fields that don't need to be send to Carol return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C) @@ -77,6 +97,7 @@ class BlindedSignature(BaseModel): id: Union[str, None] = None amount: int C_: str # Hex-encoded signature + dleq: DLEQ class BlindedMessages(BaseModel): diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 34874eaf..80cf292a 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -17,6 +17,7 @@ MintKeyset, MintKeysets, Proof, + DLEQ, ) from cashu.core.db import Database from cashu.core.helpers import fee_reserve, sum_proofs @@ -99,11 +100,16 @@ async def _generate_promise( """Generates a promise for given amount and returns a pair (amount, C').""" keyset = keyset if keyset else self.keyset private_key_amount = keyset.private_keys[amount] - C_ = b_dhke.step2_bob(B_, private_key_amount) + C_, e, s = b_dhke.step2_bob(B_, private_key_amount) await self.crud.store_promise( amount=amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db ) - return BlindedSignature(id=keyset.id, amount=amount, C_=C_.serialize().hex()) + return BlindedSignature( + id=keyset.id, + amount=amount, + C_=C_.serialize().hex(), + dleq=DLEQ(e=e.hex(), s=s.serialize().hex()), + ) def _check_spendable(self, proof: Proof): """Checks whether the proof was already spent.""" @@ -140,7 +146,7 @@ def _verify_proof_bdhke(self, proof: Proof): except: pass - return b_dhke.verify(private_key_amount, C, proof.secret) + return b_dhke.bob_verify(private_key_amount, C, proof.secret) def _verify_script(self, idx: int, proof: Proof): """ diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index a64809bb..b2394019 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -102,6 +102,7 @@ def _construct_proofs( amount=promise.amount, C=C.serialize().hex(), secret=secret, + dleq=promise.dleq, ) proofs.append(proof) return proofs @@ -517,6 +518,7 @@ async def _make_token(self, proofs: List[Proof], include_mints=True): """ # build token token = TokenV2(proofs=proofs) + # add mint information to the token, if requested if include_mints: # dummy object to hold information about the mint diff --git a/tests/test_cli.py b/tests/test_cli.py index 604ba58b..4e8a1dba 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -117,6 +117,7 @@ def test_receive_tokenv1(mint): print(result.output) +@pytest.mark.skip @pytest.mark.asyncio() def test_nostr_send(mint): runner = CliRunner() From 562ffa5895d50644657c2e11779939f6fd9f8150 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 28 Jan 2023 02:07:08 +0100 Subject: [PATCH 02/59] start working on verification --- cashu/core/b_dhke.py | 12 +++++- cashu/core/base.py | 2 + cashu/wallet/wallet.py | 87 +++++++++++++++++++++++++++--------------- 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index 306ed773..78df9c12 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -91,6 +91,7 @@ def step3_alice(C_, r, A): C = C_ - A.mult(r) return C + def bob_verify(a, C, secret_msg): Y = hash_to_curve(secret_msg.encode("utf-8")) return C == Y.mult(a) @@ -111,6 +112,7 @@ def bob_verify(a, C, secret_msg): # R2 = e*C'- s*B' # e == hash(R1,R2,A,C') + def step2_bob_dleq(B_: PublicKey, a: PrivateKey): r = PrivateKey() # generate random value R1 = PrivateKey(privkey=r.private_key, raw=True) @@ -124,7 +126,15 @@ def step2_bob_dleq(B_: PublicKey, a: PrivateKey): s = r.pubkey + a.pubkey.mult(PrivateKey(privkey=e, raw=True)) return e, s -def alice_verify_dleq(): + +def alice_verify_dleq(e, s, A, C_, B_): + print(len(s)) + # return True + R1 = A.mult( + PrivateKey(privkey=e, raw=True) + ) # - PrivateKey(privkey=s[:1], raw=True) + return True + ### Below is a test of a simple positive and negative case diff --git a/cashu/core/base.py b/cashu/core/base.py index 39b865ab..50afe103 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -26,6 +26,8 @@ class DLEQ(BaseModel): e: str s: str + B_: Union[str, None] = None + C_: Union[str, None] = None class Proof(BaseModel): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index b2394019..d76a9b4a 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -90,11 +90,15 @@ def _set_requests(self): return s def _construct_proofs( - self, promises: List[BlindedSignature], secrets: List[str], rs: List[str] + self, + promises: List[BlindedSignature], + secrets: List[str], + rs: List[str], + outputs: List[BlindedMessage], ): """Returns proofs of promise from promises. Wants secrets and blinding factors rs.""" proofs = [] - for promise, secret, r in zip(promises, secrets, rs): + for promise, secret, r, output in zip(promises, secrets, rs, outputs): C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) C = b_dhke.step3_alice(C_, r, self.keys[promise.amount]) proof = Proof( @@ -104,6 +108,8 @@ def _construct_proofs( secret=secret, dleq=promise.dleq, ) + proof.dleq.B_ = output.B_ + proof.dleq.C_ = promise.C_ proofs.append(proof) return proofs @@ -269,7 +275,7 @@ async def mint(self, amounts, payment_hash=None): except: promises = PostMintResponse.parse_obj(reponse_dict).promises - return self._construct_proofs(promises, secrets, rs) + return self._construct_proofs(promises, secrets, rs, outputs) async def split(self, proofs, amount, scnd_secret: Optional[str] = None): """Consume proofs and create new promises based on amount split. @@ -328,10 +334,16 @@ def _splitrequest_include_fields(proofs): promises_snd = [BlindedSignature(**p) for p in promises_dict["snd"]] # Construct proofs from promises (i.e., unblind signatures) frst_proofs = self._construct_proofs( - promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)] + promises_fst, + secrets[: len(promises_fst)], + rs[: len(promises_fst)], + outputs[: len(promises_fst)], ) scnd_proofs = self._construct_proofs( - promises_snd, secrets[len(promises_fst) :], rs[len(promises_fst) :] + promises_snd, + secrets[len(promises_fst) :], + rs[len(promises_fst) :], + outputs[len(promises_fst) :], ) return frst_proofs, scnd_proofs @@ -457,6 +469,10 @@ async def split( frst_proofs, scnd_proofs = await super().split(proofs, amount, scnd_secret) if len(frst_proofs) == 0 and len(scnd_proofs) == 0: raise Exception("received no splits.") + # DLEQ verify + self.verify_proofs_dleq(frst_proofs) + self.verify_proofs_dleq(scnd_proofs) + used_secrets = [p["secret"] for p in proofs] self.proofs = list( filter(lambda p: p["secret"] not in used_secrets, self.proofs) @@ -518,7 +534,7 @@ async def _make_token(self, proofs: List[Proof], include_mints=True): """ # build token token = TokenV2(proofs=proofs) - + # add mint information to the token, if requested if include_mints: # dummy object to hold information about the mint @@ -548,6 +564,30 @@ async def _make_token(self, proofs: List[Proof], include_mints=True): token.mints = list(mints.values()) return token + async def _select_proofs_to_send(self, proofs: List[Proof], amount_to_send: int): + """ + Selects proofs that can be used with the current mint. + Chooses: + 1) Proofs that are not marked as reserved + 2) Proofs that have a keyset id that is in self.keysets (active keysets of mint) - !!! optional for backwards compatibility with legacy clients + """ + # select proofs that are in the active keysets of the mint + proofs = [ + p for p in proofs if p.id in self.keysets or not p.id + ] # "or not p.id" is for backwards compatibility with proofs without a keyset id + # select proofs that are not reserved + proofs = [p for p in proofs if not p.reserved] + # check that enough spendable proofs exist + if sum_proofs(proofs) < amount_to_send: + raise Exception("balance too low.") + + # coinselect based on amount to send + sorted_proofs = sorted(proofs, key=lambda p: p.amount) + send_proofs: List[Proof] = [] + while sum_proofs(send_proofs) < amount_to_send: + send_proofs.append(sorted_proofs[len(send_proofs)]) + return send_proofs + async def _serialize_token_base64(self, token: TokenV2): """ Takes a TokenV2 and serializes it in urlsafe_base64. @@ -574,30 +614,6 @@ async def serialize_proofs( token = await self._make_token(proofs, include_mints) return await self._serialize_token_base64(token) - async def _select_proofs_to_send(self, proofs: List[Proof], amount_to_send: int): - """ - Selects proofs that can be used with the current mint. - Chooses: - 1) Proofs that are not marked as reserved - 2) Proofs that have a keyset id that is in self.keysets (active keysets of mint) - !!! optional for backwards compatibility with legacy clients - """ - # select proofs that are in the active keysets of the mint - proofs = [ - p for p in proofs if p.id in self.keysets or not p.id - ] # "or not p.id" is for backwards compatibility with proofs without a keyset id - # select proofs that are not reserved - proofs = [p for p in proofs if not p.reserved] - # check that enough spendable proofs exist - if sum_proofs(proofs) < amount_to_send: - raise Exception("balance too low.") - - # coinselect based on amount to send - sorted_proofs = sorted(proofs, key=lambda p: p.amount) - send_proofs: List[Proof] = [] - while sum_proofs(send_proofs) < amount_to_send: - send_proofs.append(sorted_proofs[len(send_proofs)]) - return send_proofs - async def set_reserved(self, proofs: List[Proof], reserved: bool): """Mark a proof as reserved to avoid reuse or delete marking.""" uuid_str = str(uuid.uuid1()) @@ -678,6 +694,17 @@ async def create_p2sh_lock(self): await store_p2sh(p2shScript, db=self.db) return p2shScript + # ---------- DLEQ PROOFS ---------- + + def verify_proofs_dleq(self, proofs: List[Proof]): + for proof in proofs: + dleq = proof.dleq + assert dleq, "no DLEQ proof" + if not b_dhke.alice_verify_dleq( + dleq.e, dleq.s, self.keys[proof.amount], dleq.C_, dleq.B_ + ): + raise Exception("DLEQ proof invalid.") + # ---------- BALANCE CHECKS ---------- @property From f73e0da0347c8db30b4e6ed8818759e5b11cf5e9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 25 Mar 2023 13:42:28 +0100 Subject: [PATCH 03/59] wip dleq --- cashu/core/b_dhke.py | 50 ++++++++++++++++++++++++++++-------------- cashu/core/base.py | 5 +++-- cashu/mint/ledger.py | 2 +- cashu/wallet/wallet.py | 22 +++++++++++-------- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index f3ce253a..7875674b 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -115,27 +115,43 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: # e == hash(R1,R2,A,C') +def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey): + _R1 = R1.serialize(compressed=False).hex() + _R2 = R2.serialize(compressed=False).hex() + _K = K.serialize(compressed=False).hex() + _C_ = C_.serialize(compressed=False).hex() + e_ = f"{_R1}{_R2}{_K}{_C_}" + e = hashlib.sha256(e_.encode("utf-8")).digest() + return e + + def step2_bob_dleq(B_: PublicKey, a: PrivateKey): - r = PrivateKey() # generate random value - R1 = PrivateKey(privkey=r.private_key, raw=True) - R2 = B_.mult(r) - e = hashlib.sha256( - R1.serialize().encode() - + PrivateKey(R2.serialize()[1:], raw=True).serialize().encode() - + r.serialize().encode() - + a.serialize().encode() - ).digest() - s = r.pubkey + a.pubkey.mult(PrivateKey(privkey=e, raw=True)) + p = PrivateKey() # generate random value + R1 = p.pubkey # R1 = pG + R2 = B_.mult(p) # R2 = pB_ + print(f"R1 is: {R1.serialize().hex()}") + print(f"R2 is: {R2.serialize().hex()}") + C_ = B_.mult(a) # C_ = aB_ + K = a.pubkey + e = hash_e(R1, R2, K, C_) # e = hash(R1, R2, K, C_) + s = p.tweak_add(a.tweak_mul(e)) # s = p + ek return e, s -def alice_verify_dleq(e, s, A, C_, B_): - print(len(s)) - # return True - R1 = A.mult( - PrivateKey(privkey=e, raw=True) - ) # - PrivateKey(privkey=s[:1], raw=True) - return True +def alice_verify_dleq(e: bytes, s: bytes, A: PublicKey, C: bytes, r: bytes, B_: bytes): + epk = PrivateKey(e, raw=True) + spk = PrivateKey(s, raw=True) + ck = PublicKey(C, raw=True) + bk = PublicKey(B_, raw=True) + R1 = spk.pubkey - A.mult(epk) + R2 = bk.mult(spk) - ck.mult(epk) + print(f"R1 is: {R1.serialize().hex()}") + print(f"R2 is: {R2.serialize().hex()}") + if e == hash_e(R1, R2, A, ck): + print("DLEQ proof ok!") + else: + print("DLEQ proof broken") + return e == hash_e(R1, R2, A, ck) ### Below is a test of a simple positive and negative case diff --git a/cashu/core/base.py b/cashu/core/base.py index f7109aad..ea07de1e 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -26,8 +26,9 @@ class DLEQ(BaseModel): e: str s: str - B_: Union[str, None] = None - C_: Union[str, None] = None + B_: str + C_: str + r: str = "" class Proof(BaseModel): diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 33bcafb0..9e004dfa 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -136,7 +136,7 @@ async def _generate_promise( id=keyset.id, amount=amount, C_=C_.serialize().hex(), - dleq=DLEQ(e=e.hex(), s=s.serialize().hex()), + dleq=DLEQ(e=e.hex(), s=s.hex(), B_=B_.serialize().hex()), ) def _check_spendable(self, proof: Proof): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index ce9f0c7c..e92b974b 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -116,13 +116,8 @@ def _construct_proofs( outputs: List[BlindedMessage], ): """Returns proofs of promise from promises. Wants secrets and blinding factors rs.""" -<<<<<<< HEAD - proofs = [] - for promise, secret, r, output in zip(promises, secrets, rs, outputs): -======= proofs: List[Proof] = [] - for promise, secret, r in zip(promises, secrets, rs): ->>>>>>> main + for promise, secret, r, output in zip(promises, secrets, rs, outputs): C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount]) proof = Proof( @@ -132,7 +127,6 @@ def _construct_proofs( secret=secret, dleq=promise.dleq, ) - proof.dleq.B_ = output.B_ proof.dleq.C_ = promise.C_ proofs.append(proof) return proofs @@ -511,6 +505,7 @@ async def mint(self, amount: int, payment_hash: Optional[str] = None): """ split = amount_split(amount) proofs = await super().mint(split, payment_hash) + print(proofs) if proofs == []: raise Exception("received no proofs.") await self._store_proofs(proofs) @@ -575,8 +570,11 @@ async def split( frst_proofs, scnd_proofs = await super().split(proofs, amount, scnd_secret) if len(frst_proofs) == 0 and len(scnd_proofs) == 0: raise Exception("received no splits.") - + # DLEQ verify + print(f"before:{scnd_proofs[0].C}") + scnd_proofs[0].C = "1" + scnd_proofs[0].C[1:] + print(f"after: {scnd_proofs[0].C}") self.verify_proofs_dleq(frst_proofs) self.verify_proofs_dleq(scnd_proofs) @@ -837,8 +835,14 @@ def verify_proofs_dleq(self, proofs: List[Proof]): for proof in proofs: dleq = proof.dleq assert dleq, "no DLEQ proof" + assert self.keys.public_keys if not b_dhke.alice_verify_dleq( - dleq.e, dleq.s, self.keys[proof.amount], dleq.C_, dleq.B_ + bytes.fromhex(dleq.e), + bytes.fromhex(dleq.s), + self.keys.public_keys[proof.amount], + bytes.fromhex(proof.C), + bytes.fromhex(dleq.r), + bytes.fromhex(dleq.B_), ): raise Exception("DLEQ proof invalid.") From e0bd8bc8cc533fb8a89e876c6ca514defe7d58a6 Mon Sep 17 00:00:00 2001 From: moonsettler Date: Wed, 26 Apr 2023 10:00:22 -0700 Subject: [PATCH 04/59] Use C_ instead of C in verify DLEQ! (#176) * Fix comments (DLEQ sign error) * Fix alice_verify_dleq in d_dhke.py * Fix_generate_promise in ledger.py * Fix verify_proofs_dleq in wallet.py --- cashu/core/b_dhke.py | 12 ++++++------ cashu/mint/ledger.py | 2 +- cashu/wallet/wallet.py | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index 7875674b..8f738122 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -43,8 +43,8 @@ return e, s Alice: -R1 = e*A - s*G -R2 = e*C'- s*B' +R1 = s*G - e*A +R2 = s*B' - e*C' e == hash(R1,R2,A,C') If true, a in A = a*G must be equal to a in C' = a*B' @@ -110,8 +110,8 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: # return e, s # Alice: -# R1 = e*A - s*G -# R2 = e*C'- s*B' +# R1 = s*G - e*A +# R2 = s*B' - e*C' # e == hash(R1,R2,A,C') @@ -138,11 +138,11 @@ def step2_bob_dleq(B_: PublicKey, a: PrivateKey): return e, s -def alice_verify_dleq(e: bytes, s: bytes, A: PublicKey, C: bytes, r: bytes, B_: bytes): +def alice_verify_dleq(e: bytes, s: bytes, A: PublicKey, B_: bytes, C_: bytes): epk = PrivateKey(e, raw=True) spk = PrivateKey(s, raw=True) - ck = PublicKey(C, raw=True) bk = PublicKey(B_, raw=True) + ck = PublicKey(C_, raw=True) R1 = spk.pubkey - A.mult(epk) R2 = bk.mult(spk) - ck.mult(epk) print(f"R1 is: {R1.serialize().hex()}") diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 9e004dfa..230c4f97 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -136,7 +136,7 @@ async def _generate_promise( id=keyset.id, amount=amount, C_=C_.serialize().hex(), - dleq=DLEQ(e=e.hex(), s=s.hex(), B_=B_.serialize().hex()), + dleq=DLEQ(e=e.hex(), s=s.hex(), B_=B_.serialize().hex(), C_=C_.serialize().hex()), ) def _check_spendable(self, proof: Proof): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index e92b974b..500b0b4f 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -840,9 +840,8 @@ def verify_proofs_dleq(self, proofs: List[Proof]): bytes.fromhex(dleq.e), bytes.fromhex(dleq.s), self.keys.public_keys[proof.amount], - bytes.fromhex(proof.C), - bytes.fromhex(dleq.r), bytes.fromhex(dleq.B_), + bytes.fromhex(dleq.C_), ): raise Exception("DLEQ proof invalid.") From 65be72b845e870544fae871fbd6ec96b2a56e455 Mon Sep 17 00:00:00 2001 From: moonsettler Date: Fri, 28 Apr 2023 04:44:39 -0700 Subject: [PATCH 05/59] Fix: invalid public key (#182) * Use C_ instead of C in verify DLEQ! * Fix comments (DLEQ sign error) * Fix alice_verify_dleq in d_dhke.py * Fix_generate_promise in ledger.py * Fix verify_proofs_dleq in wallet.py * Fix: invalid public key * Exception: Mint Error: invalid public key * Update cashu/wallet/wallet.py --------- Co-authored-by: calle <93376500+callebtc@users.noreply.github.com> --- cashu/wallet/wallet.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 500b0b4f..e1672d90 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -572,9 +572,6 @@ async def split( raise Exception("received no splits.") # DLEQ verify - print(f"before:{scnd_proofs[0].C}") - scnd_proofs[0].C = "1" + scnd_proofs[0].C[1:] - print(f"after: {scnd_proofs[0].C}") self.verify_proofs_dleq(frst_proofs) self.verify_proofs_dleq(scnd_proofs) From 9002824c4126aad2a35f7a736c15cef9d506cf8c Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Fri, 28 Apr 2023 13:47:09 +0200 Subject: [PATCH 06/59] Update cashu/core/b_dhke.py --- cashu/core/b_dhke.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index 8f738122..6d9c7748 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -99,20 +99,6 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: return C == Y.mult(a) -# DLEQ - -# Bob: -# r = random nonce -# R1 = r*G -# R2 = r*B' -# e = hash(R1,R2,A,C') -# s = r + e*a -# return e, s - -# Alice: -# R1 = s*G - e*A -# R2 = s*B' - e*C' -# e == hash(R1,R2,A,C') def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey): From a53bb0a91258bfea3f37602c5ca0ce6d40ecc9a1 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Fri, 28 Apr 2023 13:51:21 +0200 Subject: [PATCH 07/59] Update tests/test_cli.py --- tests/test_cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 3ec1cfd7..7c1b63f9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -116,7 +116,6 @@ def test_receive_tokenv1(mint): print(result.output) -@pytest.mark.skip @pytest.mark.asyncio() def test_nostr_send(mint): runner = CliRunner() From aa535a8b33517421ba7105cd7678a9202539d893 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Apr 2023 22:39:34 +0200 Subject: [PATCH 08/59] verify all constructed proofs --- cashu/wallet/wallet.py | 46 ++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index b1998ff7..083f4bbd 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -110,6 +110,22 @@ async def _init_s(self): """Dummy function that can be called from outside to use LedgerAPI.s""" return + # ---------- DLEQ PROOFS ---------- + + def verify_proofs_dleq(self, proofs: List[Proof]): + for proof in proofs: + dleq = proof.dleq + assert dleq, "no DLEQ proof" + assert self.keys.public_keys + if not b_dhke.alice_verify_dleq( + bytes.fromhex(dleq.e), + bytes.fromhex(dleq.s), + self.keys.public_keys[proof.amount], + bytes.fromhex(dleq.B_), + bytes.fromhex(dleq.C_), + ): + raise Exception("DLEQ proof invalid.") + def _construct_proofs( self, promises: List[BlindedSignature], secrets: List[str], rs: List[PrivateKey] ): @@ -131,8 +147,12 @@ def _construct_proofs( secret=secret, dleq=promise.dleq, ) - proof.dleq.C_ = promise.C_ + if proof.dleq: + proof.dleq.C_ = promise.C_ proofs.append(proof) + + # DLEQ verify + self.verify_proofs_dleq(proofs) return proofs @staticmethod @@ -333,7 +353,7 @@ async def mint(self, amounts, payment_hash=None): except: promises = PostMintResponse.parse_obj(reponse_dict).promises - return self._construct_proofs(promises, secrets, rs, outputs) + return self._construct_proofs(promises, secrets, rs) @async_set_requests async def split(self, proofs, amount, scnd_secret: Optional[str] = None): @@ -396,13 +416,11 @@ def _splitrequest_include_fields(proofs): promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)], - outputs[: len(promises_fst)], ) scnd_proofs = self._construct_proofs( promises_snd, secrets[len(promises_fst) :], rs[len(promises_fst) :], - outputs[len(promises_fst) :], ) return frst_proofs, scnd_proofs @@ -584,10 +602,6 @@ async def split( if len(frst_proofs) == 0 and len(scnd_proofs) == 0: raise Exception("received no splits.") - # DLEQ verify - self.verify_proofs_dleq(frst_proofs) - self.verify_proofs_dleq(scnd_proofs) - # remove used proofs from wallet and add new ones used_secrets = [p.secret for p in proofs] self.proofs = list(filter(lambda p: p.secret not in used_secrets, self.proofs)) @@ -905,22 +919,6 @@ async def create_p2sh_lock(self): await store_p2sh(p2shScript, db=self.db) return p2shScript - # ---------- DLEQ PROOFS ---------- - - def verify_proofs_dleq(self, proofs: List[Proof]): - for proof in proofs: - dleq = proof.dleq - assert dleq, "no DLEQ proof" - assert self.keys.public_keys - if not b_dhke.alice_verify_dleq( - bytes.fromhex(dleq.e), - bytes.fromhex(dleq.s), - self.keys.public_keys[proof.amount], - bytes.fromhex(dleq.B_), - bytes.fromhex(dleq.C_), - ): - raise Exception("DLEQ proof invalid.") - # ---------- BALANCE CHECKS ---------- @property From 4c8622d9399bce3decb8a12dde212fadfe62de51 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Apr 2023 22:42:10 +0200 Subject: [PATCH 09/59] dleq upon receive --- cashu/wallet/wallet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 083f4bbd..f4460e27 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -584,6 +584,9 @@ async def redeem( scnd_script: Optional[str] = None, scnd_siganture: Optional[str] = None, ): + # DLEQ verify + self.verify_proofs_dleq(proofs) + if scnd_script and scnd_siganture: logger.debug(f"Unlock script: {scnd_script}") # attach unlock scripts to proofs From d9cc699d0cd614ffdef85afe9f266a3e5318f29b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Apr 2023 23:07:46 +0200 Subject: [PATCH 10/59] serialize without dleq --- cashu/core/base.py | 18 +++++++++++------- tests/test_core.py | 9 ++++++++- tests/test_crypto.py | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 7df91b11..33d63020 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -55,8 +55,12 @@ class Proof(BaseModel): time_created: Union[None, str] = "" time_reserved: Union[None, str] = "" - def to_dict(self): + def to_dict(self, include_dleq=True): # dictionary without the fields that don't need to be send to Carol + if not include_dleq: + return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C) + + assert self.dleq, "DLEQ proof is missing" return dict( id=self.id, amount=self.amount, @@ -376,8 +380,8 @@ class TokenV3Token(BaseModel): mint: Optional[str] = None proofs: List[Proof] - def to_dict(self): - return_dict = dict(proofs=[p.to_dict() for p in self.proofs]) + def to_dict(self, include_dleq=True): + return_dict = dict(proofs=[p.to_dict(include_dleq) for p in self.proofs]) if self.mint: return_dict.update(dict(mint=self.mint)) # type: ignore return return_dict @@ -391,8 +395,8 @@ class TokenV3(BaseModel): token: List[TokenV3Token] = [] memo: Optional[str] = None - def to_dict(self): - return_dict = dict(token=[t.to_dict() for t in self.token]) + def to_dict(self, include_dleq=True): + return_dict = dict(token=[t.to_dict(include_dleq) for t in self.token]) if self.memo: return_dict.update(dict(memo=self.memo)) # type: ignore return return_dict @@ -419,7 +423,7 @@ def deserialize(cls, tokenv3_serialized: str): token = json.loads(base64.urlsafe_b64decode(token_base64)) return cls.parse_obj(token) - def serialize(self): + def serialize(self, include_dleq=True): """ Takes a TokenV3 and serializes it as "cashuA. """ @@ -427,6 +431,6 @@ def serialize(self): tokenv3_serialized = prefix # encode the token as a base64 string tokenv3_serialized += base64.urlsafe_b64encode( - json.dumps(self.to_dict()).encode() + json.dumps(self.to_dict(include_dleq)).encode() ).decode() return tokenv3_serialized diff --git a/tests/test_core.py b/tests/test_core.py index 882cae19..f5cc20e8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -19,6 +19,13 @@ def test_tokenv3_get_proofs(): def test_tokenv3_deserialize_serialize(): - token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnciLCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiMDIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" + token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4IiwgInIiOiAiIn19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIiwgInIiOiAiIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" token = TokenV3.deserialize(token_str) assert token.serialize() == token_str + + +def test_tokenv3_deserialize_serialize_no_dleq(): + token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4IiwgInIiOiAiIn19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIiwgInIiOiAiIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" + token_str_no_dleq = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJNU3BHTDBvUHN3MlNnRTk3bzZod3pnIiwgIkMiOiAiMDNhMmIyZjEwYzhiNjI0NDg0YWJjZmE3NzM1MGI4YjVlNTQwMGVhYWJkMTYwNGU0Y2I5YmFkMjYyZGZhYTk4ZmE4In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" + token = TokenV3.deserialize(token_str) + assert token.serialize(include_dleq=False) == token_str_no_dleq diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 64ca78b9..c8bde883 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -70,7 +70,7 @@ def test_step2(): ), raw=True, ) - C_ = step2_bob(B_, a) + C_, e, s = step2_bob(B_, a) assert ( C_.serialize().hex() == "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" From 64c50700e9f2c06330eb5cf6dedcb4f1cbf33443 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Apr 2023 23:17:21 +0200 Subject: [PATCH 11/59] all tests passing --- cashu/wallet/cli/cli_helpers.py | 4 ++-- cashu/wallet/wallet.py | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cashu/wallet/cli/cli_helpers.py b/cashu/wallet/cli/cli_helpers.py index 3413d602..20a26bde 100644 --- a/cashu/wallet/cli/cli_helpers.py +++ b/cashu/wallet/cli/cli_helpers.py @@ -230,7 +230,7 @@ async def serialize_TokenV2_to_TokenV3(wallet: Wallet, tokenv2: TokenV2): tokenv3 = TokenV3(token=[TokenV3Token(proofs=tokenv2.proofs)]) if tokenv2.mints: tokenv3.token[0].mint = tokenv2.mints[0].url - token_serialized = tokenv3.serialize() + token_serialized = tokenv3.serialize(include_dleq=False) return token_serialized @@ -243,5 +243,5 @@ async def serialize_TokenV1_to_TokenV3(wallet: Wallet, tokenv1: TokenV1): TokenV3: TokenV3 """ tokenv3 = TokenV3(token=[TokenV3Token(proofs=tokenv1.__root__)]) - token_serialized = tokenv3.serialize() + token_serialized = tokenv3.serialize(include_dleq=False) return token_serialized diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index f4460e27..a7b7ce3d 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -115,14 +115,16 @@ async def _init_s(self): def verify_proofs_dleq(self, proofs: List[Proof]): for proof in proofs: dleq = proof.dleq - assert dleq, "no DLEQ proof" + if not proof.dleq: + logger.warning("no DLEQ proof") + return assert self.keys.public_keys if not b_dhke.alice_verify_dleq( - bytes.fromhex(dleq.e), - bytes.fromhex(dleq.s), + bytes.fromhex(proof.dleq.e), + bytes.fromhex(proof.dleq.s), self.keys.public_keys[proof.amount], - bytes.fromhex(dleq.B_), - bytes.fromhex(dleq.C_), + bytes.fromhex(proof.dleq.B_), + bytes.fromhex(proof.dleq.C_), ): raise Exception("DLEQ proof invalid.") From 80d40db7bb81415245a72bf9339753bb09d0c6ff Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Apr 2023 23:19:25 +0200 Subject: [PATCH 12/59] make format --- cashu/core/b_dhke.py | 2 -- cashu/mint/ledger.py | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index 6d9c7748..96ee1247 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -99,8 +99,6 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: return C == Y.mult(a) - - def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey): _R1 = R1.serialize(compressed=False).hex() _R2 = R2.serialize(compressed=False).hex() diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index c5fc8fc3..38552c4e 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -7,13 +7,13 @@ import cashu.core.bolt11 as bolt11 import cashu.core.legacy as legacy from cashu.core.base import ( + DLEQ, BlindedMessage, BlindedSignature, Invoice, MintKeyset, MintKeysets, Proof, - DLEQ, ) from cashu.core.crypto import derive_pubkey from cashu.core.db import Database @@ -150,7 +150,9 @@ async def _generate_promise( id=keyset.id, amount=amount, C_=C_.serialize().hex(), - dleq=DLEQ(e=e.hex(), s=s.hex(), B_=B_.serialize().hex(), C_=C_.serialize().hex()), + dleq=DLEQ( + e=e.hex(), s=s.hex(), B_=B_.serialize().hex(), C_=C_.serialize().hex() + ), ) def _check_spendable(self, proof: Proof): From 6d153001d1f3dd450159a2f33df249da3df4cac6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Apr 2023 23:20:04 +0200 Subject: [PATCH 13/59] remove print --- cashu/wallet/wallet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index a7b7ce3d..9e4f72e1 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -538,7 +538,6 @@ async def mint(self, amount: int, payment_hash: Optional[str] = None): """ split = amount_split(amount) proofs = await super().mint(split, payment_hash) - print(proofs) if proofs == []: raise Exception("received no proofs.") await self._store_proofs(proofs) From 5e5e958537b75e82c4475a187464ef6a03b1223f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Apr 2023 23:51:55 +0200 Subject: [PATCH 14/59] remove debug --- cashu/core/b_dhke.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index 96ee1247..e8c02851 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -112,11 +112,11 @@ def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey): def step2_bob_dleq(B_: PublicKey, a: PrivateKey): p = PrivateKey() # generate random value R1 = p.pubkey # R1 = pG - R2 = B_.mult(p) # R2 = pB_ - print(f"R1 is: {R1.serialize().hex()}") - print(f"R2 is: {R2.serialize().hex()}") - C_ = B_.mult(a) # C_ = aB_ + assert R1 + R2 = B_.mult(p) # R2 = pB_ # type: ignore + C_ = B_.mult(a) # C_ = aB_ # type: ignore K = a.pubkey + assert K e = hash_e(R1, R2, K, C_) # e = hash(R1, R2, K, C_) s = p.tweak_add(a.tweak_mul(e)) # s = p + ek return e, s @@ -127,10 +127,8 @@ def alice_verify_dleq(e: bytes, s: bytes, A: PublicKey, B_: bytes, C_: bytes): spk = PrivateKey(s, raw=True) bk = PublicKey(B_, raw=True) ck = PublicKey(C_, raw=True) - R1 = spk.pubkey - A.mult(epk) - R2 = bk.mult(spk) - ck.mult(epk) - print(f"R1 is: {R1.serialize().hex()}") - print(f"R2 is: {R2.serialize().hex()}") + R1 = spk.pubkey - A.mult(epk) # type: ignore + R2 = bk.mult(spk) - ck.mult(epk) # type: ignore if e == hash_e(R1, R2, A, ck): print("DLEQ proof ok!") else: From 4348d4f2271ea05ca5ae9700c5e2e4001298dec9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 00:00:12 +0200 Subject: [PATCH 15/59] option to send with dleq --- cashu/core/b_dhke.py | 4 ---- cashu/wallet/cli/cli.py | 14 ++++++++++++-- cashu/wallet/wallet.py | 6 ++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index e8c02851..c58086a8 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -129,10 +129,6 @@ def alice_verify_dleq(e: bytes, s: bytes, A: PublicKey, B_: bytes, C_: bytes): ck = PublicKey(C_, raw=True) R1 = spk.pubkey - A.mult(epk) # type: ignore R2 = bk.mult(spk) - ck.mult(epk) # type: ignore - if e == hash_e(R1, R2, A, ck): - print("DLEQ proof ok!") - else: - print("DLEQ proof broken") return e == hash_e(R1, R2, A, ck) diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 78a29e2e..8aa4226a 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -218,7 +218,7 @@ async def balance(ctx: Context, verbose): print(f"Balance: {wallet.available_balance} sat") -async def send(ctx: Context, amount: int, lock: str, legacy: bool): +async def send(ctx: Context, amount: int, lock: str, dleq: bool, legacy: bool): """ Prints token to send to stdout. """ @@ -238,6 +238,7 @@ async def send(ctx: Context, amount: int, lock: str, legacy: bool): token = await wallet.serialize_proofs( send_proofs, include_mints=True, + include_dleq=dleq, ) print(token) @@ -265,6 +266,14 @@ async def send(ctx: Context, amount: int, lock: str, legacy: bool): type=str, ) @click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", type=str) +@click.option( + "--dleq", + "-d", + default=False, + is_flag=True, + help="Send with DLEQ proof.", + type=bool, +) @click.option( "--legacy", "-l", @@ -292,12 +301,13 @@ async def send_command( nostr: str, nopt: str, lock: str, + dleq: bool, legacy: bool, verbose: bool, yes: bool, ): if not nostr and not nopt: - await send(ctx, amount, lock, legacy) + await send(ctx, amount, lock, dleq, legacy) else: await send_nostr(ctx, amount, nostr or nopt, verbose, yes) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 9e4f72e1..8bea6b4b 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -113,6 +113,7 @@ async def _init_s(self): # ---------- DLEQ PROOFS ---------- def verify_proofs_dleq(self, proofs: List[Proof]): + """Verifies DLEQ proofs in proofs.""" for proof in proofs: dleq = proof.dleq if not proof.dleq: @@ -127,6 +128,7 @@ def verify_proofs_dleq(self, proofs: List[Proof]): bytes.fromhex(proof.dleq.C_), ): raise Exception("DLEQ proof invalid.") + logger.debug("DLEQ proofs verified.") def _construct_proofs( self, promises: List[BlindedSignature], secrets: List[str], rs: List[PrivateKey] @@ -734,7 +736,7 @@ async def _make_token(self, proofs: List[Proof], include_mints=True): return token async def serialize_proofs( - self, proofs: List[Proof], include_mints=True, legacy=False + self, proofs: List[Proof], include_mints=True, include_dleq=True, legacy=False ): """ Produces sharable token with proofs and mint information. @@ -753,7 +755,7 @@ async def serialize_proofs( # V3 tokens token = await self._make_token(proofs, include_mints) - return token.serialize() + return token.serialize(include_dleq) async def _make_token_v2(self, proofs: List[Proof], include_mints=True): """ From e16010d6b6c45be13891882f5cbbb91dafb7bea4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 17:19:56 +0200 Subject: [PATCH 16/59] add tests --- tests/test_cli.py | 34 ++++++++++++++++++++++++++++++++-- tests/test_core.py | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index eecdde47..f7fc982e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,6 +9,7 @@ from cashu.wallet.cli.cli import cli from cashu.wallet.wallet import Wallet from tests.conftest import SERVER_ENDPOINT, mint +from cashu.core.base import TokenV2, TokenV3 @pytest.fixture(autouse=True, scope="session") @@ -98,9 +99,38 @@ def test_send(mint, cli_prefix): [*cli_prefix, "send", "10"], ) assert result.exception is None - print("SEND") print(result.output) - assert "cashuA" in result.output, "output does not have a token" + token_str = result.output.split("\n")[0] + assert "cashuA" in token_str, "output does not have a token" + token = TokenV3.deserialize(token_str) + assert token.token[0].proofs[0].dleq is None, "dleq included" + + +@pytest.mark.asyncio +def test_send_with_dleq(mint, cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "send", "10", "--dleq"], + ) + assert result.exception is None + print(result.output) + token_str = result.output.split("\n")[0] + assert "cashuA" in token_str, "output does not have a token" + token = TokenV3.deserialize(token_str) + assert token.token[0].proofs[0].dleq is not None, "no dleq included" + + + def test_send_legacy(mint, cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "send", "10", "--legacy"], + ) + assert result.exception is None + print(result.output) + token_str = result.output.split("\n")[4] + assert token_str.startswith("eyJwcm9v"), "output is not as expected" @pytest.mark.asyncio diff --git a/tests/test_core.py b/tests/test_core.py index f5cc20e8..e33705c6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -18,7 +18,7 @@ def test_tokenv3_get_proofs(): assert len(token.get_proofs()) == 2 -def test_tokenv3_deserialize_serialize(): +def test_tokenv3_deserialize_serialize_with_dleq(): token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4IiwgInIiOiAiIn19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIiwgInIiOiAiIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" token = TokenV3.deserialize(token_str) assert token.serialize() == token_str From 9a025fecb7e52f6be9163802a610ac46c32998fa Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 17:20:49 +0200 Subject: [PATCH 17/59] fix test --- tests/test_cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index f7fc982e..8e441bc2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -121,7 +121,8 @@ def test_send_with_dleq(mint, cli_prefix): assert token.token[0].proofs[0].dleq is not None, "no dleq included" - def test_send_legacy(mint, cli_prefix): +@pytest.mark.asyncio +def test_send_legacy(mint, cli_prefix): runner = CliRunner() result = runner.invoke( cli, From f676c700776859b5deca88ecf9eb05a5fd8280a0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 18:43:39 +0200 Subject: [PATCH 18/59] deterministic p in step2_dleq and fix mypy error for hash_to_curve --- cashu/core/b_dhke.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index c58086a8..a7ef4d54 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -57,12 +57,13 @@ def hash_to_curve(message: bytes) -> PublicKey: """Generates a point from the message hash and checks if the point lies on the curve. - If it does not, it tries computing a new point from the hash.""" + If it does not, iteratively tries to compute a new point from the hash.""" point = None msg_to_hash = message while point is None: + _hash = hashlib.sha256(msg_to_hash).digest() try: - _hash = hashlib.sha256(msg_to_hash).digest() + # will error if point does not lie on curve point = PublicKey(b"\x02" + _hash, raw=True) except: msg_to_hash = _hash @@ -70,19 +71,19 @@ def hash_to_curve(message: bytes) -> PublicKey: def step1_alice( - secret_msg: str, blinding_factor: bytes = None + secret_msg: str, blinding_factor: bytes = b"" ) -> tuple[PublicKey, PrivateKey]: Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8")) if blinding_factor: r = PrivateKey(privkey=blinding_factor, raw=True) else: r = PrivateKey() - B_: PublicKey = Y + r.pubkey + B_: PublicKey = Y + r.pubkey # type: ignore return B_, r def step2_bob(B_: PublicKey, a: PrivateKey): - C_ = B_.mult(a) + C_ = B_.mult(a) # type: ignore # produce dleq proof e, s = step2_bob_dleq(B_, a) @@ -90,13 +91,13 @@ def step2_bob(B_: PublicKey, a: PrivateKey): def step3_alice(C_: PublicKey, r: PrivateKey, A: PublicKey) -> PublicKey: - C: PublicKey = C_ - A.mult(r) + C: PublicKey = C_ - A.mult(r) # type: ignore return C def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8")) - return C == Y.mult(a) + return C == Y.mult(a) # type: ignore def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey): @@ -109,8 +110,14 @@ def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey): return e -def step2_bob_dleq(B_: PublicKey, a: PrivateKey): - p = PrivateKey() # generate random value +def step2_bob_dleq(B_: PublicKey, a: PrivateKey, p_bytes: bytes = b""): + if p_bytes: + # deterministic p for testing + p = PrivateKey(privkey=p_bytes, raw=True) + else: + # normally, we generate a random p + p = PrivateKey() + R1 = p.pubkey # R1 = pG assert R1 R2 = B_.mult(p) # R2 = pB_ # type: ignore From d907d6cd841b668234d00ff41d7820d1289d270d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 18:44:18 +0200 Subject: [PATCH 19/59] test crypto/hash_e and crypto/step2_bob_dleq --- tests/test_cli.py | 1 + tests/test_crypto.py | 75 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8e441bc2..ca18682d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -130,6 +130,7 @@ def test_send_legacy(mint, cli_prefix): ) assert result.exception is None print(result.output) + # this is the legacy token in the output token_str = result.output.split("\n")[4] assert token_str.startswith("eyJwcm9v"), "output is not as expected" diff --git a/tests/test_crypto.py b/tests/test_crypto.py index c8bde883..0f75f8ec 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -1,6 +1,13 @@ import pytest -from cashu.core.b_dhke import hash_to_curve, step1_alice, step2_bob, step3_alice +from cashu.core.b_dhke import ( + hash_to_curve, + step1_alice, + step2_bob, + step3_alice, + hash_e, + step2_bob_dleq, +) from cashu.core.secp import PrivateKey, PublicKey @@ -104,3 +111,69 @@ def test_step3(): C.serialize().hex() == "03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd" ) + + +def test_hash_e(): + C_ = PublicKey( + bytes.fromhex( + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + ), + raw=True, + ) + K = PublicKey( + pubkey=b"\x02" + + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001", + ), + raw=True, + ) + R1 = PublicKey( + pubkey=b"\x02" + + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001", + ), + raw=True, + ) + R2 = PublicKey( + pubkey=b"\x02" + + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001", + ), + raw=True, + ) + e = hash_e(R1, R2, K, C_) + assert e.hex() == "a4dc034b74338c28c6bc3ea49731f2a24440fc7c4affc08b31a93fc9fbe6401e" + + +def test_step2_bob_dleq(): + B_, _ = step1_alice( + "test_message", + blinding_factor=bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001" + ), # 32 bytes + ) + a = PrivateKey( + privkey=bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001" + ), + raw=True, + ) + p_bytes = bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001" + ) # 32 bytes + e, s = step2_bob_dleq(B_, a, p_bytes) + assert e.hex() == "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9" + assert ( + s.hex() == "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da" + ) # differs from e only in least significant byte because `a = 0x1` + + # change `a` + a = PrivateKey( + privkey=bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000001111" + ), + raw=True, + ) + e, s = step2_bob_dleq(B_, a, p_bytes) + assert e.hex() == "df1984d5c22f7e17afe33b8669f02f530f286ae3b00a1978edaf900f4721f65e" + assert s.hex() == "828404170c86f240c50ae0f5fc17bb6b82612d46b355e046d7cd84b0a3c934a0" From bc5f4786e8415121b7b21d04973ef6c2d603f47b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 19:02:21 +0200 Subject: [PATCH 20/59] rename A to K in b_dhke.py and test_alice_verify_dleq --- cashu/core/b_dhke.py | 6 ++--- tests/test_crypto.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index a7ef4d54..5e85d5a0 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -129,14 +129,14 @@ def step2_bob_dleq(B_: PublicKey, a: PrivateKey, p_bytes: bytes = b""): return e, s -def alice_verify_dleq(e: bytes, s: bytes, A: PublicKey, B_: bytes, C_: bytes): +def alice_verify_dleq(e: bytes, s: bytes, K: PublicKey, B_: bytes, C_: bytes): epk = PrivateKey(e, raw=True) spk = PrivateKey(s, raw=True) bk = PublicKey(B_, raw=True) ck = PublicKey(C_, raw=True) - R1 = spk.pubkey - A.mult(epk) # type: ignore + R1 = spk.pubkey - K.mult(epk) # type: ignore R2 = bk.mult(spk) - ck.mult(epk) # type: ignore - return e == hash_e(R1, R2, A, ck) + return e == hash_e(R1, R2, K, ck) ### Below is a test of a simple positive and negative case diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 0f75f8ec..d9706fb5 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -7,6 +7,7 @@ step3_alice, hash_e, step2_bob_dleq, + alice_verify_dleq, ) from cashu.core.secp import PrivateKey, PublicKey @@ -177,3 +178,63 @@ def test_step2_bob_dleq(): e, s = step2_bob_dleq(B_, a, p_bytes) assert e.hex() == "df1984d5c22f7e17afe33b8669f02f530f286ae3b00a1978edaf900f4721f65e" assert s.hex() == "828404170c86f240c50ae0f5fc17bb6b82612d46b355e046d7cd84b0a3c934a0" + + +def test_alice_verify_dleq(): + # e from test_step2_bob_dleq for a=0x1 + e = bytes.fromhex( + "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9" + ) + # s from test_step2_bob_dleq for a=0x1 + s = bytes.fromhex( + "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da" + ) + # pubkey of a=0x1 + K = PublicKey( + bytes.fromhex( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ), + raw=True, + ) + + # B_ is the same as we did: + # B_, _ = step1_alice( + # "test_message", + # blinding_factor=bytes.fromhex( + # "0000000000000000000000000000000000000000000000000000000000000001" + # ), # 32 bytes + # ) + B_ = bytes.fromhex( + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + ) + + # # C_ is the same as if we did: + # a = PrivateKey( + # privkey=bytes.fromhex( + # "0000000000000000000000000000000000000000000000000000000000000001" + # ), + # raw=True, + # ) + # C_, e, s = step2_bob(B_, a) + + C_ = bytes.fromhex( + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + ) + + assert alice_verify_dleq(e, s, K, B_, C_) + + # test again with B_ and C_ as per step1 and step2 + B_, _ = step1_alice( + "test_message", + blinding_factor=bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001" + ), # 32 bytes + ) + a = PrivateKey( + privkey=bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001" + ), + raw=True, + ) + C_, e, s = step2_bob(B_, a) + assert alice_verify_dleq(e, s, K, B_.serialize(), C_.serialize()) From 8f3d1c2e8eaf8cd61fbf587f59ca398d4e6fa884 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 19:03:53 +0200 Subject: [PATCH 21/59] rename tests --- tests/test_crypto.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index d9706fb5..37bd250b 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -114,7 +114,7 @@ def test_step3(): ) -def test_hash_e(): +def test_dleq_hash_e(): C_ = PublicKey( bytes.fromhex( "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" @@ -146,7 +146,7 @@ def test_hash_e(): assert e.hex() == "a4dc034b74338c28c6bc3ea49731f2a24440fc7c4affc08b31a93fc9fbe6401e" -def test_step2_bob_dleq(): +def test_dleq_step2_bob_dleq(): B_, _ = step1_alice( "test_message", blinding_factor=bytes.fromhex( @@ -180,7 +180,7 @@ def test_step2_bob_dleq(): assert s.hex() == "828404170c86f240c50ae0f5fc17bb6b82612d46b355e046d7cd84b0a3c934a0" -def test_alice_verify_dleq(): +def test_dleq_alice_verify_dleq(): # e from test_step2_bob_dleq for a=0x1 e = bytes.fromhex( "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9" @@ -223,7 +223,8 @@ def test_alice_verify_dleq(): assert alice_verify_dleq(e, s, K, B_, C_) - # test again with B_ and C_ as per step1 and step2 + # ----- test again with B_ and C_ as per step1 and step2 + B_, _ = step1_alice( "test_message", blinding_factor=bytes.fromhex( From 84591057cc6f7d0d3039493f8a52ff67bfd9ed4d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 20:17:16 +0200 Subject: [PATCH 22/59] make format --- tests/test_cli.py | 2 +- tests/test_crypto.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index ca18682d..12740e6d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,13 +3,13 @@ import pytest from click.testing import CliRunner +from cashu.core.base import TokenV2, TokenV3 from cashu.core.migrations import migrate_databases from cashu.core.settings import settings from cashu.wallet import migrations from cashu.wallet.cli.cli import cli from cashu.wallet.wallet import Wallet from tests.conftest import SERVER_ENDPOINT, mint -from cashu.core.base import TokenV2, TokenV3 @pytest.fixture(autouse=True, scope="session") diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 37bd250b..ca1dac56 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -1,13 +1,13 @@ import pytest from cashu.core.b_dhke import ( + alice_verify_dleq, + hash_e, hash_to_curve, step1_alice, step2_bob, - step3_alice, - hash_e, step2_bob_dleq, - alice_verify_dleq, + step3_alice, ) from cashu.core.secp import PrivateKey, PublicKey From 55e406f057c23effdadf916dff7418ebe4878889 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 22:17:32 +0200 Subject: [PATCH 23/59] store dleq in mint db (and readd balance view) --- cashu/core/base.py | 1 - cashu/mint/crud.py | 32 +++++------------ cashu/mint/ledger.py | 7 +++- cashu/mint/migrations.py | 76 +++++++++++++++++++++++----------------- 4 files changed, 58 insertions(+), 58 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 33d63020..e57f1aac 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -30,7 +30,6 @@ class DLEQ(BaseModel): s: str B_: str C_: str - r: str = "" class Proof(BaseModel): diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 27c11905..3e5c2589 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -14,47 +14,36 @@ class LedgerCrud: """ async def get_keyset(*args, **kwags): - return await get_keyset(*args, **kwags) async def get_lightning_invoice(*args, **kwags): - return await get_lightning_invoice(*args, **kwags) async def get_proofs_used(*args, **kwags): - return await get_proofs_used(*args, **kwags) async def invalidate_proof(*args, **kwags): - return await invalidate_proof(*args, **kwags) async def get_proofs_pending(*args, **kwags): - return await get_proofs_pending(*args, **kwags) async def set_proof_pending(*args, **kwags): - return await set_proof_pending(*args, **kwags) async def unset_proof_pending(*args, **kwags): - return await unset_proof_pending(*args, **kwags) async def store_keyset(*args, **kwags): - return await store_keyset(*args, **kwags) async def store_lightning_invoice(*args, **kwags): - return await store_lightning_invoice(*args, **kwags) async def store_promise(*args, **kwags): - return await store_promise(*args, **kwags) async def update_lightning_invoice(*args, **kwags): - return await update_lightning_invoice(*args, **kwags) @@ -63,19 +52,22 @@ async def store_promise( amount: int, B_: str, C_: str, + e: str = "", + s: str = "", conn: Optional[Connection] = None, ): - await (conn or db).execute( f""" INSERT INTO {table_with_schema(db, 'promises')} - (amount, B_b, C_b) - VALUES (?, ?, ?) + (amount, B_b, C_b, e, s) + VALUES (?, ?, ?, ?, ?) """, ( amount, - str(B_), - str(C_), + B_, + C_, + e, + s, ), ) @@ -84,7 +76,6 @@ async def get_proofs_used( db: Database, conn: Optional[Connection] = None, ): - rows = await (conn or db).fetchall( f""" SELECT secret from {table_with_schema(db, 'proofs_used')} @@ -98,7 +89,6 @@ async def invalidate_proof( proof: Proof, conn: Optional[Connection] = None, ): - # we add the proof and secret to the used list await (conn or db).execute( f""" @@ -118,7 +108,6 @@ async def get_proofs_pending( db: Database, conn: Optional[Connection] = None, ): - rows = await (conn or db).fetchall( f""" SELECT * from {table_with_schema(db, 'proofs_pending')} @@ -132,7 +121,6 @@ async def set_proof_pending( proof: Proof, conn: Optional[Connection] = None, ): - # we add the proof and secret to the used list await (conn or db).execute( f""" @@ -153,7 +141,6 @@ async def unset_proof_pending( db: Database, conn: Optional[Connection] = None, ): - await (conn or db).execute( f""" DELETE FROM {table_with_schema(db, 'proofs_pending')} @@ -168,7 +155,6 @@ async def store_lightning_invoice( invoice: Invoice, conn: Optional[Connection] = None, ): - await (conn or db).execute( f""" INSERT INTO {table_with_schema(db, 'invoices')} @@ -189,7 +175,6 @@ async def get_lightning_invoice( hash: str, conn: Optional[Connection] = None, ): - row = await (conn or db).fetchone( f""" SELECT * from {table_with_schema(db, 'invoices')} @@ -220,7 +205,6 @@ async def store_keyset( keyset: MintKeyset, conn: Optional[Connection] = None, ): - await (conn or db).execute( # type: ignore f""" INSERT INTO {table_with_schema(db, 'keysets')} diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 38552c4e..e6f377d7 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -144,7 +144,12 @@ async def _generate_promise( private_key_amount = keyset.private_keys[amount] C_, e, s = b_dhke.step2_bob(B_, private_key_amount) await self.crud.store_promise( - amount=amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db + amount=amount, + B_=B_.serialize().hex(), + C_=C_.serialize().hex(), + e=e.hex(), + s=s.hex(), + db=self.db, ) return BlindedSignature( id=keyset.id, diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 4b6b5e38..b8033586 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -54,38 +54,38 @@ async def m001_initial(db: Database): """ ) - # await db.execute( - # f""" - # CREATE VIEW {table_with_schema(db, 'balance_issued')} AS - # SELECT COALESCE(SUM(s), 0) AS balance FROM ( - # SELECT SUM(amount) AS s - # FROM {table_with_schema(db, 'promises')} - # WHERE amount > 0 - # ); - # """ - # ) - - # await db.execute( - # f""" - # CREATE VIEW {table_with_schema(db, 'balance_used')} AS - # SELECT COALESCE(SUM(s), 0) AS balance FROM ( - # SELECT SUM(amount) AS s - # FROM {table_with_schema(db, 'proofs_used')} - # WHERE amount > 0 - # ); - # """ - # ) - - # await db.execute( - # f""" - # CREATE VIEW {table_with_schema(db, 'balance')} AS - # SELECT s_issued - s_used AS balance FROM ( - # SELECT bi.balance AS s_issued, bu.balance AS s_used - # FROM {table_with_schema(db, 'balance_issued')} bi - # CROSS JOIN {table_with_schema(db, 'balance_used')} bu - # ); - # """ - # ) + await db.execute( + f""" + CREATE VIEW {table_with_schema(db, 'balance_issued')} AS + SELECT COALESCE(SUM(s), 0) AS balance FROM ( + SELECT SUM(amount) AS s + FROM {table_with_schema(db, 'promises')} + WHERE amount > 0 + ); + """ + ) + + await db.execute( + f""" + CREATE VIEW {table_with_schema(db, 'balance_redeemed')} AS + SELECT COALESCE(SUM(s), 0) AS balance FROM ( + SELECT SUM(amount) AS s + FROM {table_with_schema(db, 'proofs_used')} + WHERE amount > 0 + ); + """ + ) + + await db.execute( + f""" + CREATE VIEW {table_with_schema(db, 'balance')} AS + SELECT s_issued - s_used AS balance FROM ( + SELECT bi.balance AS s_issued, bu.balance AS s_used + FROM {table_with_schema(db, 'balance_issued')} bi + CROSS JOIN {table_with_schema(db, 'balance_redeemed')} bu + ); + """ + ) async def m003_mint_keysets(db: Database): @@ -146,3 +146,15 @@ async def m005_pending_proofs_table(db: Database) -> None: ); """ ) + + +async def m006_promises_dleq(db: Database): + """ + Add columns for DLEQ proof to promises table. + """ + await db.execute( + f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN e TEXT" + ) + await db.execute( + f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN s TEXT" + ) From 279e8f31bd3d72e66bc698d3592042de2319f924 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 22:27:55 +0200 Subject: [PATCH 24/59] remove `r` from dleq in tests --- cashu/wallet/wallet.py | 2 +- tests/test_core.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 8bea6b4b..0f7cefe7 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -117,7 +117,7 @@ def verify_proofs_dleq(self, proofs: List[Proof]): for proof in proofs: dleq = proof.dleq if not proof.dleq: - logger.warning("no DLEQ proof") + logger.warning("no DLEQ proof included.") return assert self.keys.public_keys if not b_dhke.alice_verify_dleq( diff --git a/tests/test_core.py b/tests/test_core.py index e33705c6..8e3ff166 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -19,13 +19,13 @@ def test_tokenv3_get_proofs(): def test_tokenv3_deserialize_serialize_with_dleq(): - token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4IiwgInIiOiAiIn19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIiwgInIiOiAiIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" + token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" token = TokenV3.deserialize(token_str) assert token.serialize() == token_str def test_tokenv3_deserialize_serialize_no_dleq(): - token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4IiwgInIiOiAiIn19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIiwgInIiOiAiIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" + token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" token_str_no_dleq = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJNU3BHTDBvUHN3MlNnRTk3bzZod3pnIiwgIkMiOiAiMDNhMmIyZjEwYzhiNjI0NDg0YWJjZmE3NzM1MGI4YjVlNTQwMGVhYWJkMTYwNGU0Y2I5YmFkMjYyZGZhYTk4ZmE4In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" token = TokenV3.deserialize(token_str) assert token.serialize(include_dleq=False) == token_str_no_dleq From 1050c49225c0279849e6134e1073f0a68afd55fc Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Apr 2023 22:59:52 +0200 Subject: [PATCH 25/59] add pending output --- cashu/wallet/cli/cli.py | 3 ++- tests/test_cli.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 8aa4226a..b7ebae68 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -515,7 +515,8 @@ async def pending(ctx: Context, legacy, number: int, offset: int): number, ): grouped_proofs = list(value) - token = await wallet.serialize_proofs(grouped_proofs) + # TODO: we can't return DLEQ because we don't store it + token = await wallet.serialize_proofs(grouped_proofs, include_dleq=False) # token_hidden_secret = await wallet.serialize_proofs(grouped_proofs) reserved_date = datetime.utcfromtimestamp( int(grouped_proofs[0].time_reserved) diff --git a/tests/test_cli.py b/tests/test_cli.py index 12740e6d..6426bf75 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -214,3 +214,15 @@ def test_nostr_send(mint, cli_prefix): assert result.exception is None print("NOSTR_SEND") print(result.output) + + +@pytest.mark.asyncio +def test_pending(cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "pending"], + ) + assert result.exception is None + print(result.output) + assert result.exit_code == 0 From 12be7b1e701dd1c7289ca30c8cd759b814b6e8e6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 1 May 2023 22:54:25 +0200 Subject: [PATCH 26/59] make format --- cashu/mint/ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 57710ae6..ce80f90d 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -7,13 +7,13 @@ from ..core import bolt11 as bolt11 from ..core import legacy as legacy from ..core.base import ( + DLEQ, BlindedMessage, BlindedSignature, Invoice, MintKeyset, MintKeysets, Proof, - DLEQ, ) from ..core.crypto import derive_pubkey from ..core.db import Database From 3fb099c7ca509b12e48e3a4895570a3f3e5269ae Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 2 May 2023 01:01:11 +0200 Subject: [PATCH 27/59] works with pre-dleq mints --- cashu/core/base.py | 4 ++-- cashu/wallet/wallet.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index b21f940a..264ec602 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -104,10 +104,10 @@ class BlindedSignature(BaseModel): Blinded signature or "promise" which is the signature on a `BlindedMessage` """ - id: Union[str, None] = None + id: str amount: int C_: str # Hex-encoded signature - dleq: DLEQ + dleq: Optional[DLEQ] = None # DLEQ proof class BlindedMessages(BaseModel): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 08ece435..3b9a164e 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -117,7 +117,7 @@ def verify_proofs_dleq(self, proofs: List[Proof]): for proof in proofs: dleq = proof.dleq if not proof.dleq: - logger.warning("no DLEQ proof included.") + logger.debug("Warning: no DLEQ proof included.") return assert self.keys.public_keys if not b_dhke.alice_verify_dleq( @@ -128,7 +128,6 @@ def verify_proofs_dleq(self, proofs: List[Proof]): bytes.fromhex(proof.dleq.C_), ): raise Exception("DLEQ proof invalid.") - logger.debug("DLEQ proofs verified.") def _construct_proofs( self, promises: List[BlindedSignature], secrets: List[str], rs: List[PrivateKey] From 13873b101479f34a9a64db07b8f2fcb0c7d78d70 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 2 May 2023 01:14:23 +0200 Subject: [PATCH 28/59] fix comments --- cashu/core/b_dhke.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index 5e85d5a0..804cd0b2 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -35,11 +35,11 @@ (These steps occur once Bob returns C') Bob: - r = random nonce +r = random nonce R1 = r*G R2 = r*B' - e = hash(R1,R2,A,C') - s = r + e*a +e = hash(R1,R2,A,C') +s = r + e*a return e, s Alice: From 0495befb93de867628504e10a916b8891a1679cf Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 25 Jul 2023 23:43:26 +0200 Subject: [PATCH 29/59] make format --- tests/test_crypto.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 9ce723ff..6ab4fcf7 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -1,13 +1,13 @@ import pytest from cashu.core.crypto.b_dhke import ( + alice_verify_dleq, + hash_e, hash_to_curve, step1_alice, step2_bob, - step3_alice, - alice_verify_dleq, step2_bob_dleq, - hash_e, + step3_alice, ) from cashu.core.crypto.secp import PrivateKey, PublicKey From 8321ca3aaca0509dece46b1c8fe55ed86dbd4c43 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Jul 2023 00:08:24 +0200 Subject: [PATCH 30/59] fix some tests --- cashu/core/base.py | 8 ++++---- cashu/wallet/api/router.py | 4 ++-- cashu/wallet/cli/cli.py | 13 +++++++++++-- cashu/wallet/helpers.py | 10 +++++++++- cashu/wallet/nostr.py | 4 +++- cashu/wallet/wallet.py | 11 +++++++---- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d56e9d90..dd1954a3 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -188,7 +188,7 @@ class Proof(BaseModel): time_reserved: Union[None, str] = "" derivation_path: Union[None, str] = "" # derivation path of the proof - def to_dict(self, include_dleq=True): + def to_dict(self, include_dleq=False): # dictionary without the fields that don't need to be send to Carol if not include_dleq: return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C) @@ -577,7 +577,7 @@ class TokenV3Token(BaseModel): mint: Optional[str] = None proofs: List[Proof] - def to_dict(self, include_dleq=True): + def to_dict(self, include_dleq=False): return_dict = dict(proofs=[p.to_dict(include_dleq) for p in self.proofs]) if self.mint: return_dict.update(dict(mint=self.mint)) # type: ignore @@ -592,7 +592,7 @@ class TokenV3(BaseModel): token: List[TokenV3Token] = [] memo: Optional[str] = None - def to_dict(self, include_dleq=True): + def to_dict(self, include_dleq=False): return_dict = dict(token=[t.to_dict(include_dleq) for t in self.token]) if self.memo: return_dict.update(dict(memo=self.memo)) # type: ignore @@ -620,7 +620,7 @@ def deserialize(cls, tokenv3_serialized: str) -> "TokenV3": token = json.loads(base64.urlsafe_b64decode(token_base64)) return cls.parse_obj(token) - def serialize(self, include_dleq=True) -> str: + def serialize(self, include_dleq=False) -> str: """ Takes a TokenV3 and serializes it as "cashuA. """ diff --git a/cashu/wallet/api/router.py b/cashu/wallet/api/router.py index d0a26c82..6c0089f8 100644 --- a/cashu/wallet/api/router.py +++ b/cashu/wallet/api/router.py @@ -216,11 +216,11 @@ async def send_command( global wallet if not nostr: balance, token = await send( - wallet, amount, lock, legacy=False, split=not nosplit + wallet, amount=amount, lock=lock, legacy=False, split=not nosplit ) return SendResponse(balance=balance, token=token) else: - token, pubkey = await send_nostr(wallet, amount, nostr) + token, pubkey = await send_nostr(wallet, amount=amount, pubkey=nostr) return SendResponse(balance=wallet.available_balance, token=token, npub=pubkey) diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 4fe2c5ca..4c9da190 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -378,9 +378,18 @@ async def send_command( ): wallet: Wallet = ctx.obj["WALLET"] if not nostr and not nopt: - await send(wallet, amount, lock, legacy, split=not nosplit) + await send( + wallet, + amount=amount, + lock=lock, + legacy=legacy, + split=not nosplit, + include_dleq=dleq, + ) else: - await send_nostr(wallet, amount, nostr or nopt, verbose, yes) + await send_nostr( + wallet, amount=amount, pubkey=nostr or nopt, verbose=verbose, yes=yes + ) @cli.command("receive", help="Receive tokens.") diff --git a/cashu/wallet/helpers.py b/cashu/wallet/helpers.py index 3449014d..0697260c 100644 --- a/cashu/wallet/helpers.py +++ b/cashu/wallet/helpers.py @@ -151,7 +151,13 @@ async def receive( async def send( - wallet: Wallet, amount: int, lock: str, legacy: bool, split: bool = True + wallet: Wallet, + *, + amount: int, + lock: str, + legacy: bool, + split: bool = True, + include_dleq: bool = False, ): """ Prints token to send to stdout. @@ -202,6 +208,7 @@ async def send( token = await wallet.serialize_proofs( send_proofs, include_mints=True, + include_dleq=include_dleq, ) print(token) @@ -212,6 +219,7 @@ async def send( token = await wallet.serialize_proofs( send_proofs, legacy=True, + include_dleq=include_dleq, ) print(token) diff --git a/cashu/wallet/nostr.py b/cashu/wallet/nostr.py index 1ea1918c..64cf5f51 100644 --- a/cashu/wallet/nostr.py +++ b/cashu/wallet/nostr.py @@ -45,10 +45,12 @@ async def nip5_to_pubkey(wallet: Wallet, address: str): async def send_nostr( wallet: Wallet, + *, amount: int, pubkey: str, verbose: bool = False, yes: bool = True, + include_dleq=False, ): """ Sends tokens via nostr. @@ -62,7 +64,7 @@ async def send_nostr( _, send_proofs = await wallet.split_to_send( wallet.proofs, amount, set_reserved=True ) - token = await wallet.serialize_proofs(send_proofs) + token = await wallet.serialize_proofs(send_proofs, include_dleq=include_dleq) if pubkey.startswith("npub"): pubkey_to = PublicKey().from_npub(pubkey) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index c057bc3f..698a959d 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -143,15 +143,14 @@ async def _init_s(self): """Dummy function that can be called from outside to use LedgerAPI.s""" return - # ---------- DLEQ PROOFS ---------- + # ---------- DLEQ PROOFS ---------- def verify_proofs_dleq(self, proofs: List[Proof]): """Verifies DLEQ proofs in proofs.""" for proof in proofs: - dleq = proof.dleq if not proof.dleq: - logger.debug("Warning: no DLEQ proof included.") return + logger.debug("Verifying DLEQ proof.") assert self.keys.public_keys if not b_dhke.alice_verify_dleq( bytes.fromhex(proof.dleq.e), @@ -1070,6 +1069,10 @@ async def redeem( Args: proofs (List[Proof]): Proofs to be redeemed. """ + # verify DLEQ of incoming proofs + logger.debug("Verifying DLEQ of incoming proofs.") + self.verify_proofs_dleq(proofs) + logger.debug("DLEQ verified.") return await self.split(proofs, sum_proofs(proofs)) async def split( @@ -1302,7 +1305,7 @@ async def _make_token(self, proofs: List[Proof], include_mints=True) -> TokenV3: return token async def serialize_proofs( - self, proofs: List[Proof], include_mints=True, include_dleq=True, legacy=False + self, proofs: List[Proof], include_mints=True, include_dleq=False, legacy=False ) -> str: """Produces sharable token with proofs and mint information. From ab8a1f05ac9136ef860ad2b11b18880d2138cf2f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Jul 2023 00:11:35 +0200 Subject: [PATCH 31/59] fix last test --- tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index 07a7357f..3d02bf9f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -24,7 +24,7 @@ def test_tokenv3_get_proofs(): def test_tokenv3_deserialize_serialize_with_dleq(): token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" token = TokenV3.deserialize(token_str) - assert token.serialize() == token_str + assert token.serialize(include_dleq=True) == token_str def test_tokenv3_deserialize_serialize_no_dleq(): From 5fccea349ea94174f8c95ca941b6d56e6f3f8b8d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 28 Jul 2023 20:14:39 +0200 Subject: [PATCH 32/59] test serialize dleq fix --- tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index 01736865..103dd2da 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -45,7 +45,7 @@ def test_tokenv3_deserialize_serialize(): "NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" ) token = TokenV3.deserialize(token_str) - assert token.serialize(include_dleq=True) == token_str + assert token.serialize() == token_str def test_tokenv3_deserialize_serialize_no_dleq(): From 402027330cf1ac1c68b3504e9d03443fe5e1a911 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 28 Jul 2023 20:15:57 +0200 Subject: [PATCH 33/59] flake --- cashu/core/crypto/b_dhke.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/core/crypto/b_dhke.py b/cashu/core/crypto/b_dhke.py index 478318c9..ad261028 100644 --- a/cashu/core/crypto/b_dhke.py +++ b/cashu/core/crypto/b_dhke.py @@ -136,7 +136,7 @@ def alice_verify_dleq(e: bytes, s: bytes, K: PublicKey, B_: bytes, C_: bytes): return e == hash_e(R1, R2, K, ck) -### Below is a test of a simple positive and negative case +# Below is a test of a simple positive and negative case # # Alice's keys # a = PrivateKey() From 43b04b6b1f0bf9fd9ec65817fee1b9ac0002edf4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 28 Jul 2023 20:23:26 +0200 Subject: [PATCH 34/59] flake --- Makefile | 4 ++-- tests/test_core.py | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 9c9ddc36..6e5d0b47 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,10 @@ black-check: poetry run black . --exclude cashu/nostr --check mypy: - poetry run mypy cashu --ignore-missing + poetry run mypy cashu tests --ignore-missing flake8: - poetry run flake8 cashu + poetry run flake8 cashu tests format: isort black diff --git a/tests/test_core.py b/tests/test_core.py index 103dd2da..89fbd866 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -32,7 +32,18 @@ def test_tokenv3_get_proofs(): def test_tokenv3_deserialize_serialize_with_dleq(): - token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" + token_str = ( + "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0Ei" + "LCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTM" + "zMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWR" + "lYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzE" + "yM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4" + "ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiN" + "WU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOT" + "c0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICI" + "wMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQx" + "ZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" + ) token = TokenV3.deserialize(token_str) assert token.serialize(include_dleq=True) == token_str @@ -49,8 +60,26 @@ def test_tokenv3_deserialize_serialize(): def test_tokenv3_deserialize_serialize_no_dleq(): - token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" - token_str_no_dleq = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJNU3BHTDBvUHN3MlNnRTk3bzZod3pnIiwgIkMiOiAiMDNhMmIyZjEwYzhiNjI0NDg0YWJjZmE3NzM1MGI4YjVlNTQwMGVhYWJkMTYwNGU0Y2I5YmFkMjYyZGZhYTk4ZmE4In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" + token_str = ( + "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mV" + "UZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZ" + "SI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyM" + "mYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY" + "2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiY" + "zc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQ" + "yI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0Y" + "zRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4M" + "WRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjM" + "DA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Z" + "jc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" + ) + token_str_no_dleq = ( + "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMG" + "k1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU" + "4OTMwY2YwYzYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJNU3BHTDBvUHN3MlNnRTk3bzZod3pn" + "IiwgIkMiOiAiMDNhMmIyZjEwYzhiNjI0NDg0YWJjZmE3NzM1MGI4YjVlNTQwMGVhYWJkMTYwNGU0Y2I5YmFkMjYyZGZhYTk4ZmE4In1dLCAib" + "WludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" + ) token = TokenV3.deserialize(token_str) assert token.serialize(include_dleq=False) == token_str_no_dleq From 8b337d9ee44f26e41aabeab7e0fb116a71946f67 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 28 Jul 2023 20:29:26 +0200 Subject: [PATCH 35/59] keyset.id must be str --- cashu/core/base.py | 2 +- tests/test_wallet.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 473dfec7..882de6f6 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -476,7 +476,7 @@ class MintKeyset: def __init__( self, - id=None, + id="", valid_from=None, valid_to=None, first_seen=None, diff --git a/tests/test_wallet.py b/tests/test_wallet.py index fdcf6647..37583447 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -93,8 +93,7 @@ async def test_get_keys(wallet1: Wallet): assert wallet1.keys.public_keys assert len(wallet1.keys.public_keys) == settings.max_order keyset = await wallet1._get_keys(wallet1.url) - assert keyset.id is not None - assert type(keyset.id) == str + assert keyset.id assert len(keyset.id) > 0 From 7ab9539f6f8fb308d1dd109df9847eb3c896c574 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 28 Jul 2023 20:34:45 +0200 Subject: [PATCH 36/59] fix test decorators --- tests/test_cli.py | 36 ++++++++++++++++++------------------ tests/test_wallet.py | 4 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c62c59bf..c896a61f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -24,7 +24,7 @@ async def init_wallet(): return wallet -@pytest.mark.asyncio + def test_info(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -38,7 +38,7 @@ def test_info(cli_prefix): assert result.exit_code == 0 -@pytest.mark.asyncio + def test_info_with_mint(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -52,7 +52,7 @@ def test_info_with_mint(cli_prefix): assert result.exit_code == 0 -@pytest.mark.asyncio + def test_info_with_mnemonic(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -66,7 +66,7 @@ def test_info_with_mnemonic(cli_prefix): assert result.exit_code == 0 -@pytest.mark.asyncio + def test_balance(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -81,7 +81,7 @@ def test_balance(cli_prefix): assert result.exit_code == 0 -@pytest.mark.asyncio + def test_invoice(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -97,7 +97,7 @@ def test_invoice(mint, cli_prefix): assert result.exit_code == 0 -@pytest.mark.asyncio + def test_invoice_with_split(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -109,7 +109,7 @@ def test_invoice_with_split(mint, cli_prefix): # assert wallet.proof_amounts.count(1) >= 10 -@pytest.mark.asyncio + def test_wallets(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -124,7 +124,7 @@ def test_wallets(cli_prefix): assert result.exit_code == 0 -@pytest.mark.asyncio + def test_send(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -139,7 +139,7 @@ def test_send(mint, cli_prefix): assert token.token[0].proofs[0].dleq is None, "dleq included" -@pytest.mark.asyncio + def test_send_with_dleq(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -154,7 +154,7 @@ def test_send_with_dleq(mint, cli_prefix): assert token.token[0].proofs[0].dleq is not None, "no dleq included" -@pytest.mark.asyncio + def test_send_legacy(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -168,7 +168,7 @@ def test_send_legacy(mint, cli_prefix): assert token_str.startswith("eyJwcm9v"), "output is not as expected" -@pytest.mark.asyncio + def test_send_without_split(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -181,7 +181,7 @@ def test_send_without_split(mint, cli_prefix): assert "cashuA" in result.output, "output does not have a token" -@pytest.mark.asyncio + def test_send_without_split_but_wrong_amount(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -191,7 +191,7 @@ def test_send_without_split_but_wrong_amount(mint, cli_prefix): assert "No proof with this amount found" in str(result.exception) -@pytest.mark.asyncio + def test_receive_tokenv3(mint, cli_prefix): runner = CliRunner() token = ( @@ -213,7 +213,7 @@ def test_receive_tokenv3(mint, cli_prefix): print(result.output) -@pytest.mark.asyncio + def test_receive_tokenv3_no_mint(mint, cli_prefix): # this test works only if the previous test succeeds because we simulate the case where the mint URL is not in the token # therefore, we need to know the mint keyset already and have the mint URL in the db @@ -237,7 +237,7 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix): print(result.output) -@pytest.mark.asyncio + def test_receive_tokenv2(mint, cli_prefix): runner = CliRunner() token = ( @@ -255,7 +255,7 @@ def test_receive_tokenv2(mint, cli_prefix): print(result.output) -@pytest.mark.asyncio + def test_receive_tokenv1(mint, cli_prefix): runner = CliRunner() token = ( @@ -272,7 +272,7 @@ def test_receive_tokenv1(mint, cli_prefix): print(result.output) -@pytest.mark.asyncio() +() def test_nostr_send(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -291,7 +291,7 @@ def test_nostr_send(mint, cli_prefix): print(result.output) -@pytest.mark.asyncio + def test_pending(cli_prefix): runner = CliRunner() result = runner.invoke( diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 37583447..634c3e8f 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -311,7 +311,7 @@ async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet): ) # sender side await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver - +@pytest.mark.asyncio async def test_token_state(wallet1: Wallet): await wallet1.mint(64) assert wallet1.balance == 64 @@ -319,7 +319,7 @@ async def test_token_state(wallet1: Wallet): assert resp.dict()["spendable"] assert resp.dict()["pending"] - +@pytest.mark.asyncio async def test_bump_secret_derivation(wallet3: Wallet): await wallet3._init_private_key( "half depart obvious quality work element tank gorilla view sugar picture humble" From 22c5eefaaab81020a41d88b81b43cb39851cf6f5 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Jul 2023 11:44:47 +0200 Subject: [PATCH 37/59] start removing the duplicate fields from the dleq --- cashu/core/base.py | 2 -- cashu/mint/ledger.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 882de6f6..089f65bc 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -161,8 +161,6 @@ class DLEQ(BaseModel): e: str s: str - B_: str - C_: str class Proof(BaseModel): diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 2440e22e..c7cd5cc2 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -202,7 +202,7 @@ async def _generate_promise( amount=amount, C_=C_.serialize().hex(), dleq=DLEQ( - e=e.hex(), s=s.hex(), B_=B_.serialize().hex(), C_=C_.serialize().hex() + e=e.hex(), s=s.hex() ), ) From 068224e0272b0de7a5583a16a638f77224bbfaaa Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Jul 2023 02:25:16 +0200 Subject: [PATCH 38/59] format --- cashu/core/base.py | 17 ++++++- cashu/core/crypto/b_dhke.py | 57 +++++++++++++++------- cashu/mint/ledger.py | 8 ++-- cashu/wallet/wallet.py | 33 +++++++++---- tests/test_cli.py | 19 +------- tests/test_core.py | 78 ++++++++++++++++++------------ tests/test_crypto.py | 94 ++++++++++++++++++++++++++++--------- tests/test_wallet.py | 2 + 8 files changed, 208 insertions(+), 100 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 089f65bc..1629134a 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -163,6 +163,18 @@ class DLEQ(BaseModel): s: str +class DLEQWallet(BaseModel): + """ + Discrete Log Equality (DLEQ) Proof + """ + + e: str + s: str + r: str # blinding_factor, unknown to mint but sent from wallet to wallet for DLEQ proof + # B_: Union[str, None] = None # blinded message, sent to the mint by the wallet + # C_: Union[str, None] = None # blinded signature, received by the mint + + class Proof(BaseModel): """ Value token @@ -171,10 +183,12 @@ class Proof(BaseModel): id: Union[ None, str ] = "" # NOTE: None for backwards compatibility for old clients that do not include the keyset id < 0.3 + amount: int = 0 secret: str = "" # secret or message to be blinded and signed C: str = "" # signature on secret, unblinded by wallet - dleq: Union[DLEQ, None] = None # DLEQ proof + dleq: Union[DLEQWallet, None] = None # DLEQ proof + p2pksigs: Union[List[str], None] = [] # P2PK signature p2shscript: Union[P2SHScript, None] = None # P2SH spending condition reserved: Union[ @@ -193,6 +207,7 @@ def to_dict(self, include_dleq=False): return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C) assert self.dleq, "DLEQ proof is missing" + print(self.dleq) return dict( id=self.id, amount=self.amount, diff --git a/cashu/core/crypto/b_dhke.py b/cashu/core/crypto/b_dhke.py index ad261028..6f30576e 100644 --- a/cashu/core/crypto/b_dhke.py +++ b/cashu/core/crypto/b_dhke.py @@ -80,7 +80,7 @@ def step1_alice( return B_, r -def step2_bob(B_: PublicKey, a: PrivateKey) -> Tuple[PublicKey, bytes, bytes]: +def step2_bob(B_: PublicKey, a: PrivateKey) -> Tuple[PublicKey, PrivateKey, PrivateKey]: C_: PublicKey = B_.mult(a) # type: ignore # produce dleq proof e, s = step2_bob_dleq(B_, a) @@ -97,7 +97,7 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: return C == Y.mult(a) # type: ignore -def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey): +def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey) -> bytes: _R1 = R1.serialize(compressed=False).hex() _R2 = R2.serialize(compressed=False).hex() _K = K.serialize(compressed=False).hex() @@ -107,7 +107,9 @@ def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey): return e -def step2_bob_dleq(B_: PublicKey, a: PrivateKey, p_bytes: bytes = b""): +def step2_bob_dleq( + B_: PublicKey, a: PrivateKey, p_bytes: bytes = b"" +) -> Tuple[PrivateKey, PrivateKey]: if p_bytes: # deterministic p for testing p = PrivateKey(privkey=p_bytes, raw=True) @@ -117,23 +119,42 @@ def step2_bob_dleq(B_: PublicKey, a: PrivateKey, p_bytes: bytes = b""): R1 = p.pubkey # R1 = pG assert R1 - R2 = B_.mult(p) # R2 = pB_ # type: ignore - C_ = B_.mult(a) # C_ = aB_ # type: ignore - K = a.pubkey - assert K - e = hash_e(R1, R2, K, C_) # e = hash(R1, R2, K, C_) + R2: PublicKey = B_.mult(p) # R2 = pB_ # type: ignore + C_: PublicKey = B_.mult(a) # C_ = aB_ # type: ignore + A = a.pubkey + assert A + e = hash_e(R1, R2, A, C_) # e = hash(R1, R2, A, C_) s = p.tweak_add(a.tweak_mul(e)) # s = p + ek - return e, s - - -def alice_verify_dleq(e: bytes, s: bytes, K: PublicKey, B_: bytes, C_: bytes): - epk = PrivateKey(e, raw=True) spk = PrivateKey(s, raw=True) - bk = PublicKey(B_, raw=True) - ck = PublicKey(C_, raw=True) - R1 = spk.pubkey - K.mult(epk) # type: ignore - R2 = bk.mult(spk) - ck.mult(epk) # type: ignore - return e == hash_e(R1, R2, K, ck) + epk = PrivateKey(e, raw=True) + return epk, spk + + +def alice_verify_dleq( + e: PrivateKey, s: PrivateKey, A: PublicKey, B_: PublicKey, C_: PublicKey +): + R1 = s.pubkey - A.mult(e) # type: ignore + R2 = B_.mult(s) - C_.mult(e) # type: ignore + e_bytes = e.private_key + return e_bytes == hash_e(R1, R2, A, C_) + + +def carol_verify_dleq( + secret_msg: str, + r: PrivateKey, + C: PublicKey, + e: PrivateKey, + s: PrivateKey, + A: PublicKey, +): + Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8")) + C_: PublicKey = C + A.mult(r) # type: ignore + B_: PublicKey = Y + r.pubkey # type: ignore + return alice_verify_dleq(e, s, A, B_, C_) + # R1 = s.pubkey - A.mult(e) # type: ignore + # R2 = B_.mult(s) - C_.mult(e) # type: ignore + # e_bytes = e.private_key + # return e_bytes == hash_e(R1, R2, A, C_) # Below is a test of a simple positive and negative case diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index c7cd5cc2..3519646c 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -191,8 +191,8 @@ async def _generate_promise( amount=amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), - e=e.hex(), - s=s.hex(), + e=e.serialize(), + s=s.serialize(), db=self.db, id=keyset.id, ) @@ -201,9 +201,7 @@ async def _generate_promise( id=keyset.id, amount=amount, C_=C_.serialize().hex(), - dleq=DLEQ( - e=e.hex(), s=s.hex() - ), + dleq=DLEQ(e=e.serialize(), s=s.serialize()), ) def _check_spendable(self, proof: Proof): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index c08f4198..9ebb80c5 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -22,6 +22,7 @@ CheckFeesRequest, CheckSpendableRequest, CheckSpendableResponse, + DLEQWallet, GetInfoResponse, GetMeltResponse, GetMintResponse, @@ -148,12 +149,21 @@ def verify_proofs_dleq(self, proofs: List[Proof]): return logger.debug("Verifying DLEQ proof.") assert self.keys.public_keys - if not b_dhke.alice_verify_dleq( - bytes.fromhex(proof.dleq.e), - bytes.fromhex(proof.dleq.s), - self.keys.public_keys[proof.amount], - bytes.fromhex(proof.dleq.B_), - bytes.fromhex(proof.dleq.C_), + # if not b_dhke.alice_verify_dleq( + # e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), + # s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), + # A=self.keys.public_keys[proof.amount], + # B_=PublicKey(bytes.fromhex(proof.B_), raw=True), + # C_=PublicKey(bytes.fromhex(proof.C_), raw=True), + # ): + # raise Exception("Alice: DLEQ proof invalid.") + if not b_dhke.carol_verify_dleq( + secret_msg=proof.secret, + C=PublicKey(bytes.fromhex(proof.C), raw=True), + r=PrivateKey(bytes.fromhex(proof.dleq.r), raw=True), + e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), + s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), + A=self.keys.public_keys[proof.amount], ): raise Exception("DLEQ proof invalid.") @@ -188,17 +198,22 @@ def _construct_proofs( C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount]) + B_, r = b_dhke.step1_alice(secret, r) # recompute B_ for dleq proofs proof = Proof( id=promise.id, amount=promise.amount, C=C.serialize().hex(), secret=secret, - dleq=promise.dleq, derivation_path=path, ) - if proof.dleq: - proof.dleq.C_ = promise.C_ + + # if the mint returned a dleq proof, we add it to the proof + if promise.dleq: + proof.dleq = DLEQWallet( + e=promise.dleq.e, s=promise.dleq.s, r=r.serialize() + ) + proofs.append(proof) logger.trace( diff --git a/tests/test_cli.py b/tests/test_cli.py index c896a61f..1521e1c0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -24,7 +24,6 @@ async def init_wallet(): return wallet - def test_info(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -38,7 +37,6 @@ def test_info(cli_prefix): assert result.exit_code == 0 - def test_info_with_mint(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -52,7 +50,6 @@ def test_info_with_mint(cli_prefix): assert result.exit_code == 0 - def test_info_with_mnemonic(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -66,7 +63,6 @@ def test_info_with_mnemonic(cli_prefix): assert result.exit_code == 0 - def test_balance(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -81,7 +77,6 @@ def test_balance(cli_prefix): assert result.exit_code == 0 - def test_invoice(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -97,7 +92,6 @@ def test_invoice(mint, cli_prefix): assert result.exit_code == 0 - def test_invoice_with_split(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -109,7 +103,6 @@ def test_invoice_with_split(mint, cli_prefix): # assert wallet.proof_amounts.count(1) >= 10 - def test_wallets(cli_prefix): runner = CliRunner() result = runner.invoke( @@ -124,7 +117,6 @@ def test_wallets(cli_prefix): assert result.exit_code == 0 - def test_send(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -139,7 +131,6 @@ def test_send(mint, cli_prefix): assert token.token[0].proofs[0].dleq is None, "dleq included" - def test_send_with_dleq(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -154,7 +145,6 @@ def test_send_with_dleq(mint, cli_prefix): assert token.token[0].proofs[0].dleq is not None, "no dleq included" - def test_send_legacy(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -168,7 +158,6 @@ def test_send_legacy(mint, cli_prefix): assert token_str.startswith("eyJwcm9v"), "output is not as expected" - def test_send_without_split(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -181,7 +170,6 @@ def test_send_without_split(mint, cli_prefix): assert "cashuA" in result.output, "output does not have a token" - def test_send_without_split_but_wrong_amount(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -191,7 +179,6 @@ def test_send_without_split_but_wrong_amount(mint, cli_prefix): assert "No proof with this amount found" in str(result.exception) - def test_receive_tokenv3(mint, cli_prefix): runner = CliRunner() token = ( @@ -213,7 +200,6 @@ def test_receive_tokenv3(mint, cli_prefix): print(result.output) - def test_receive_tokenv3_no_mint(mint, cli_prefix): # this test works only if the previous test succeeds because we simulate the case where the mint URL is not in the token # therefore, we need to know the mint keyset already and have the mint URL in the db @@ -237,7 +223,6 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix): print(result.output) - def test_receive_tokenv2(mint, cli_prefix): runner = CliRunner() token = ( @@ -255,7 +240,6 @@ def test_receive_tokenv2(mint, cli_prefix): print(result.output) - def test_receive_tokenv1(mint, cli_prefix): runner = CliRunner() token = ( @@ -273,6 +257,8 @@ def test_receive_tokenv1(mint, cli_prefix): () + + def test_nostr_send(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -291,7 +277,6 @@ def test_nostr_send(mint, cli_prefix): print(result.output) - def test_pending(cli_prefix): runner = CliRunner() result = runner.invoke( diff --git a/tests/test_core.py b/tests/test_core.py index 89fbd866..e41df38c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -33,16 +33,23 @@ def test_tokenv3_get_proofs(): def test_tokenv3_deserialize_serialize_with_dleq(): token_str = ( - "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mVUZUb0Ei" - "LCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZSI6ICJjMmJkNTM" - "zMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyMmYzNzJkMzRjZDhlNmY5MWR" - "lYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzE" - "yM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiYzc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4" - "ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQyI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiN" - "WU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0YzRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOT" - "c0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4MWRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICI" - "wMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjMDA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQx" - "ZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Zjc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" + "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93M" + "SIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjZmZjFiY2VlOGUzMzk2NGE4ZDNjNGQ5NzYwNzdiZ" + "DI4ZGVkZWJkODYyMDU0MDQzNDY4ZjU5ZDFiZjI1OTQzN2QiLCAiQyI6ICIwM2I3ZD" + "lkMzIzYTAxOWJlNTE4NzRlOGE5OGY1NDViOTg3Y2JmNmU5MWUwMDc1YTFhZjQ3MjY2NDMxOGRlZ" + "TQzZTUiLCAiZGxlcSI6IHsiZSI6ICI1ZjkxMGQ4NTc0M2U0OTI0ZjRiNjlkNzhjM" + "jFjYTc1ZjEzNzg3Zjc3OTE1NWRmMjMzMjJmYTA1YjU5ODdhYzNmIiwgInMiOiAiZTc4Y2U0MzNiZ" + "WNlZTNjNGU1NzM4ZDdjMzRlNDQyZWQ0MmJkMzk0MjI0ZTc3MjE4OGFjMmI5MzZmM" + "jA2Y2QxYSIsICJyIjogIjI3MzM3ODNmOTQ4MWZlYzAxNzdlYmM4ZjBhOTI2OWVjOGFkNzU5MDU2ZT" + "k3MTRiMWEwYTEwMDQ3MmY2Y2Y5YzIifX0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIi" + "wgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiMmFkNDMyZDRkNTg2MzJiMmRlMzI0ZmQxYmE5OTcyZmE" + "4MDljNmU3ZGE1ZTkyZWVmYjBiNjYxMmQ5M2Q3ZTAwMCIsICJDIjogIjAzMmFmYjg" + "zOWQwMmRmMWNhOGY5ZGZjNTI1NzUxN2Q0MzY4YjdiMTc0MzgzM2JlYWUzZDQzNmExYmQwYmJkYjVk" + "OCIsICJkbGVxIjogeyJlIjogImY0NjM2MzU5YTUzZGQxNGEyNmUyNTMyMDQxZWIx" + "MDE2OTk1ZTg4NzgwODY0OWFlY2VlNTcwZTA5ZTk2NTU3YzIiLCAicyI6ICJmZWYzMGIzMDcwMDJkMW" + "VjNWZiZjg0ZGZhZmRkMGEwOTdkNDJlMDYxNTZiNzdiMTMzMmNjNGZjNGNjYWEyOD" + "JmIiwgInIiOiAiODQ5MjQxNzBlYzc3ZjhjMDNmZDRlZTkyZTA3MjdlMzYyNTliZjRhYTc4NTBjZTc2" + "NDExMDQ0MmNlNmVlM2FjYyJ9fV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzOCJ9XX0=" ) token = TokenV3.deserialize(token_str) assert token.serialize(include_dleq=True) == token_str @@ -50,9 +57,12 @@ def test_tokenv3_deserialize_serialize_with_dleq(): def test_tokenv3_deserialize_serialize(): token_str = ( - "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci" - "LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW" - "1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiMDIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0" + "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJh" + "bW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci" + "LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOG" + "UyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW" + "1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiM" + "DIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0" "NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" ) token = TokenV3.deserialize(token_str) @@ -61,24 +71,34 @@ def test_tokenv3_deserialize_serialize(): def test_tokenv3_deserialize_serialize_no_dleq(): token_str = ( - "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMGk1LVgyQy0zc29mV" - "UZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU4OTMwY2YwYzYiLCAiZGxlcSI6IHsiZ" - "SI6ICJjMmJkNTMzMTkyZDI2MzExMzM2MjU1OTUxZmQyZTI2OWEwY2E2ODViMzgxMDcwYTFhMzRiMDViYjVlYTBkNTAzIiwgInMiOiAiNzUxOTc5MDczZjk3YjcyM" - "mYzNzJkMzRjZDhlNmY5MWRlYzA0ODE4MTg5NjI3YjYwZWFmYjZjMTY3MjFiZjVmOCIsICJCXyI6ICIwMjVhZjliM2ViNmFkZjgxN2M0YzBjMzhhYWYwZjNhYmJjY" - "2MxODFhY2VmMmNjZTQ4Mzg3MWIwYzEyM2UwOTI1NDgiLCAiQ18iOiAiMDNlMDA2YTRiNGI1ODNlMzI5NTNmYzFmMWI4MmQyZTE1MmQyMjdlYTFhZTgyZTEyOWNiY" - "zc3OWQ5NzYwMTc1Mzg4In19LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk1TcEdMMG9Qc3cyU2dFOTdvNmh3emciLCAiQ" - "yI6ICIwM2EyYjJmMTBjOGI2MjQ0ODRhYmNmYTc3MzUwYjhiNWU1NDAwZWFhYmQxNjA0ZTRjYjliYWQyNjJkZmFhOThmYTgiLCAiZGxlcSI6IHsiZSI6ICJkODI0Y" - "zRjNGExNTBmZTQxM2JjM2YzOGNkMGE2NjAxZDU1NWVhYzhjOTNmNDMyOTc0NzQxMGEwOGMzZmYyNTg4IiwgInMiOiAiMjZkZjdhZTk4NzdjN2YyZTE2N2FkZGI4M" - "WRkYjFlNjg1NTE5NmY1NDE3ZDI3MDFiYTdkZmM0NDlkNjYwNTQyZiIsICJCXyI6ICIwMmJlNzQ0NzllOTM0NWU3NWRhNWUzYzliYzcxYjBhMTZlNzZhNDJkMDVjM" - "DA3M2ZjYjgzZmNlYTg3YWJmZGFhYTciLCAiQ18iOiAiMDNkYjA3MjdjYWNjMTQwYTljMzg0YjQxZjJhMjUyYTg3ODI5YWZhMWU4OWJjZTFlMGY4YWQyMGJkNGQ5Z" - "jc4YjgyIn19XSwgIm1pbnQiOiAiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ==" + "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhb" + "W91bnQiOiAyLCAic2VjcmV0IjogIjZmZjFiY2VlOGUzMzk2NGE4ZDNjNGQ5NzYwNzdiZ" + "DI4ZGVkZWJkODYyMDU0MDQzNDY4ZjU5ZDFiZjI1OTQzN2QiLCAiQyI6ICIwM2I3ZDlkMzIzY" + "TAxOWJlNTE4NzRlOGE5OGY1NDViOTg3Y2JmNmU5MWUwMDc1YTFhZjQ3MjY2NDMxOGRlZ" + "TQzZTUiLCAiZGxlcSI6IHsiZSI6ICI1ZjkxMGQ4NTc0M2U0OTI0ZjRiNjlkNzhjMjFjYTc1Z" + "jEzNzg3Zjc3OTE1NWRmMjMzMjJmYTA1YjU5ODdhYzNmIiwgInMiOiAiZTc4Y2U0MzNiZ" + "WNlZTNjNGU1NzM4ZDdjMzRlNDQyZWQ0MmJkMzk0MjI0ZTc3MjE4OGFjMmI5MzZmMjA2Y2QxY" + "SIsICJyIjogIjI3MzM3ODNmOTQ4MWZlYzAxNzdlYmM4ZjBhOTI2OWVjOGFkNzU5MDU2ZT" + "k3MTRiMWEwYTEwMDQ3MmY2Y2Y5YzIifX0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3" + "VudCI6IDgsICJzZWNyZXQiOiAiMmFkNDMyZDRkNTg2MzJiMmRlMzI0ZmQxYmE5OTcyZmE" + "4MDljNmU3ZGE1ZTkyZWVmYjBiNjYxMmQ5M2Q3ZTAwMCIsICJDIjogIjAzMmFmYjgzOWQwMmR" + "mMWNhOGY5ZGZjNTI1NzUxN2Q0MzY4YjdiMTc0MzgzM2JlYWUzZDQzNmExYmQwYmJkYjVk" + "OCIsICJkbGVxIjogeyJlIjogImY0NjM2MzU5YTUzZGQxNGEyNmUyNTMyMDQxZWIxMDE2OTk1" + "ZTg4NzgwODY0OWFlY2VlNTcwZTA5ZTk2NTU3YzIiLCAicyI6ICJmZWYzMGIzMDcwMDJkMW" + "VjNWZiZjg0ZGZhZmRkMGEwOTdkNDJlMDYxNTZiNzdiMTMzMmNjNGZjNGNjYWEyODJmIiwgIn" + "IiOiAiODQ5MjQxNzBlYzc3ZjhjMDNmZDRlZTkyZTA3MjdlMzYyNTliZjRhYTc4NTBjZTc2" + "NDExMDQ0MmNlNmVlM2FjYyJ9fV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzOCJ9XX0=" ) token_str_no_dleq = ( - "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogInFvS01lMG" - "k1LVgyQy0zc29mVUZUb0EiLCAiQyI6ICIwMmIzYWVlMjAxZDRmYzlhODJkYTJiZTVmOTQzODlmOWIxMzg4YzBkMDA3N2M2ODg1MDhjODIzZmU" - "4OTMwY2YwYzYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJNU3BHTDBvUHN3MlNnRTk3bzZod3pn" - "IiwgIkMiOiAiMDNhMmIyZjEwYzhiNjI0NDg0YWJjZmE3NzM1MGI4YjVlNTQwMGVhYWJkMTYwNGU0Y2I5YmFkMjYyZGZhYTk4ZmE4In1dLCAib" - "WludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" + "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bn" + "QiOiAyLCAic2VjcmV0IjogIjZmZjFiY2VlOGUzMzk2NGE4ZDNjNGQ5NzYwNzdiZDI4" + "ZGVkZWJkODYyMDU0MDQzNDY4ZjU5ZDFiZjI1OTQzN2QiLCAiQyI6ICIwM2I3ZDlkMzIzYTAxOWJlN" + "TE4NzRlOGE5OGY1NDViOTg3Y2JmNmU5MWUwMDc1YTFhZjQ3MjY2NDMxOGRlZTQzZTU" + "ifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICIyYWQ0MzJkN" + "GQ1ODYzMmIyZGUzMjRmZDFiYTk5NzJmYTgwOWM2ZTdkYTVlOTJlZWZiMGI2NjEyZD" + "kzZDdlMDAwIiwgIkMiOiAiMDMyYWZiODM5ZDAyZGYxY2E4ZjlkZmM1MjU3NTE3ZDQzNjhiN2IxNzQz" + "ODMzYmVhZTNkNDM2YTFiZDBiYmRiNWQ4In1dLCAibWludCI6ICJodHRwOi8vbG9jY" + "Wxob3N0OjMzMzgifV19" ) token = TokenV3.deserialize(token_str) assert token.serialize(include_dleq=False) == token_str_no_dleq diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 053b8c10..f8e055db 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -1,5 +1,6 @@ from cashu.core.crypto.b_dhke import ( alice_verify_dleq, + carol_verify_dleq, hash_e, hash_to_curve, step1_alice, @@ -46,9 +47,9 @@ def test_hash_to_curve_iteration(): def test_step1(): - """""" + secret_msg = "test_message" B_, blinding_factor = step1_alice( - "test_message", + secret_msg, blinding_factor=PrivateKey( privkey=bytes.fromhex( "0000000000000000000000000000000000000000000000000000000000000001" @@ -169,9 +170,13 @@ def test_dleq_step2_bob_dleq(): "0000000000000000000000000000000000000000000000000000000000000001" ) # 32 bytes e, s = step2_bob_dleq(B_, a, p_bytes) - assert e.hex() == "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9" assert ( - s.hex() == "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da" + e.serialize() + == "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9" + ) + assert ( + s.serialize() + == "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da" ) # differs from e only in least significant byte because `a = 0x1` # change `a` @@ -182,27 +187,40 @@ def test_dleq_step2_bob_dleq(): raw=True, ) e, s = step2_bob_dleq(B_, a, p_bytes) - assert e.hex() == "df1984d5c22f7e17afe33b8669f02f530f286ae3b00a1978edaf900f4721f65e" - assert s.hex() == "828404170c86f240c50ae0f5fc17bb6b82612d46b355e046d7cd84b0a3c934a0" + assert ( + e.serialize() + == "df1984d5c22f7e17afe33b8669f02f530f286ae3b00a1978edaf900f4721f65e" + ) + assert ( + s.serialize() + == "828404170c86f240c50ae0f5fc17bb6b82612d46b355e046d7cd84b0a3c934a0" + ) def test_dleq_alice_verify_dleq(): # e from test_step2_bob_dleq for a=0x1 - e = bytes.fromhex( - "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9" + e = PrivateKey( + bytes.fromhex( + "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9" + ), + raw=True, ) # s from test_step2_bob_dleq for a=0x1 - s = bytes.fromhex( - "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da" - ) - # pubkey of a=0x1 - K = PublicKey( + s = PrivateKey( bytes.fromhex( - "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da" ), raw=True, ) + a = PrivateKey( + privkey=bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001" + ), + raw=True, + ) + A = a.pubkey + assert A # B_ is the same as we did: # B_, _ = step1_alice( # "test_message", @@ -210,8 +228,11 @@ def test_dleq_alice_verify_dleq(): # "0000000000000000000000000000000000000000000000000000000000000001" # ), # 32 bytes # ) - B_ = bytes.fromhex( - "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + B_ = PublicKey( + bytes.fromhex( + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + ), + raw=True, ) # # C_ is the same as if we did: @@ -223,14 +244,26 @@ def test_dleq_alice_verify_dleq(): # ) # C_, e, s = step2_bob(B_, a) - C_ = bytes.fromhex( - "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + C_ = PublicKey( + bytes.fromhex( + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + ), + raw=True, ) - assert alice_verify_dleq(e, s, K, B_, C_) + assert alice_verify_dleq(e, s, A, B_, C_) - # ----- test again with B_ and C_ as per step1 and step2 +def test_dleq_alice_direct_verify_dleq(): + # ----- test again with B_ and C_ as per step1 and step2 + a = PrivateKey( + privkey=bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001" + ), + raw=True, + ) + A = a.pubkey + assert A B_, _ = step1_alice( "test_message", blinding_factor=PrivateKey( @@ -240,11 +273,30 @@ def test_dleq_alice_verify_dleq(): raw=True, ), ) + C_, e, s = step2_bob(B_, a) + assert alice_verify_dleq(e, s, A, B_, C_) + + +def test_dleq_carol_varify_from_bob(): a = PrivateKey( privkey=bytes.fromhex( "0000000000000000000000000000000000000000000000000000000000000001" ), raw=True, ) + A = a.pubkey + assert A + secret_msg = "test_message" + r = PrivateKey( + privkey=bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000001" + ), + raw=True, + ) + B_, _ = step1_alice(secret_msg, r) C_, e, s = step2_bob(B_, a) - assert alice_verify_dleq(e, s, K, B_.serialize(), C_.serialize()) + assert alice_verify_dleq(e, s, A, B_, C_) + C = step3_alice(C_, r, A) + + # carol does not know B_ and C_, but she receives C and r from Alice + assert carol_verify_dleq(secret_msg=secret_msg, C=C, r=r, e=e, s=s, A=A) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 634c3e8f..c0df41bf 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -311,6 +311,7 @@ async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet): ) # sender side await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver + @pytest.mark.asyncio async def test_token_state(wallet1: Wallet): await wallet1.mint(64) @@ -319,6 +320,7 @@ async def test_token_state(wallet1: Wallet): assert resp.dict()["spendable"] assert resp.dict()["pending"] + @pytest.mark.asyncio async def test_bump_secret_derivation(wallet3: Wallet): await wallet3._init_private_key( From e4c245b54bd0333dded551e188cf7595e20a9bf6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Jul 2023 02:30:28 +0200 Subject: [PATCH 39/59] remove print --- cashu/core/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 1629134a..5d23ab55 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -207,7 +207,6 @@ def to_dict(self, include_dleq=False): return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C) assert self.dleq, "DLEQ proof is missing" - print(self.dleq) return dict( id=self.id, amount=self.amount, From 749d5dfb9dd9eb6bb09713564783f3adde5a47c4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Jul 2023 14:10:58 +0200 Subject: [PATCH 40/59] cleanup --- cashu/core/crypto/b_dhke.py | 8 ++------ cashu/wallet/wallet.py | 5 ++++- tests/test_crypto.py | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/cashu/core/crypto/b_dhke.py b/cashu/core/crypto/b_dhke.py index 6f30576e..bfcffbcf 100644 --- a/cashu/core/crypto/b_dhke.py +++ b/cashu/core/crypto/b_dhke.py @@ -131,7 +131,7 @@ def step2_bob_dleq( def alice_verify_dleq( - e: PrivateKey, s: PrivateKey, A: PublicKey, B_: PublicKey, C_: PublicKey + B_: PublicKey, C_: PublicKey, e: PrivateKey, s: PrivateKey, A: PublicKey ): R1 = s.pubkey - A.mult(e) # type: ignore R2 = B_.mult(s) - C_.mult(e) # type: ignore @@ -150,11 +150,7 @@ def carol_verify_dleq( Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8")) C_: PublicKey = C + A.mult(r) # type: ignore B_: PublicKey = Y + r.pubkey # type: ignore - return alice_verify_dleq(e, s, A, B_, C_) - # R1 = s.pubkey - A.mult(e) # type: ignore - # R2 = B_.mult(s) - C_.mult(e) # type: ignore - # e_bytes = e.private_key - # return e_bytes == hash_e(R1, R2, A, C_) + return alice_verify_dleq(B_, C_, e, s, A) # Below is a test of a simple positive and negative case diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 9ebb80c5..ba93d3ea 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -146,8 +146,9 @@ def verify_proofs_dleq(self, proofs: List[Proof]): """Verifies DLEQ proofs in proofs.""" for proof in proofs: if not proof.dleq: + logger.trace("No DLEQ proof in proof.") return - logger.debug("Verifying DLEQ proof.") + logger.trace("Verifying DLEQ proof.") assert self.keys.public_keys # if not b_dhke.alice_verify_dleq( # e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), @@ -166,6 +167,8 @@ def verify_proofs_dleq(self, proofs: List[Proof]): A=self.keys.public_keys[proof.amount], ): raise Exception("DLEQ proof invalid.") + else: + logger.debug("DLEQ proof valid.") def _construct_proofs( self, diff --git a/tests/test_crypto.py b/tests/test_crypto.py index f8e055db..59b94849 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -251,7 +251,7 @@ def test_dleq_alice_verify_dleq(): raw=True, ) - assert alice_verify_dleq(e, s, A, B_, C_) + assert alice_verify_dleq(B_, C_, e, s, A) def test_dleq_alice_direct_verify_dleq(): @@ -274,7 +274,7 @@ def test_dleq_alice_direct_verify_dleq(): ), ) C_, e, s = step2_bob(B_, a) - assert alice_verify_dleq(e, s, A, B_, C_) + assert alice_verify_dleq(B_, C_, e, s, A) def test_dleq_carol_varify_from_bob(): @@ -295,7 +295,7 @@ def test_dleq_carol_varify_from_bob(): ) B_, _ = step1_alice(secret_msg, r) C_, e, s = step2_bob(B_, a) - assert alice_verify_dleq(e, s, A, B_, C_) + assert alice_verify_dleq(B_, C_, e, s, A) C = step3_alice(C_, r, A) # carol does not know B_ and C_, but she receives C and r from Alice From 5b5b8fa4f8df93b17e93c78ec0d1be13fe85c0c0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:14:49 +0200 Subject: [PATCH 41/59] add type anotations to dleq functions --- cashu/core/crypto/b_dhke.py | 4 ++-- cashu/wallet/wallet.py | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/cashu/core/crypto/b_dhke.py b/cashu/core/crypto/b_dhke.py index bfcffbcf..457349b2 100644 --- a/cashu/core/crypto/b_dhke.py +++ b/cashu/core/crypto/b_dhke.py @@ -132,7 +132,7 @@ def step2_bob_dleq( def alice_verify_dleq( B_: PublicKey, C_: PublicKey, e: PrivateKey, s: PrivateKey, A: PublicKey -): +) -> bool: R1 = s.pubkey - A.mult(e) # type: ignore R2 = B_.mult(s) - C_.mult(e) # type: ignore e_bytes = e.private_key @@ -146,7 +146,7 @@ def carol_verify_dleq( e: PrivateKey, s: PrivateKey, A: PublicKey, -): +) -> bool: Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8")) C_: PublicKey = C + A.mult(r) # type: ignore B_: PublicKey = Y + r.pubkey # type: ignore diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index ba93d3ea..1cebed7f 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -150,14 +150,6 @@ def verify_proofs_dleq(self, proofs: List[Proof]): return logger.trace("Verifying DLEQ proof.") assert self.keys.public_keys - # if not b_dhke.alice_verify_dleq( - # e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), - # s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), - # A=self.keys.public_keys[proof.amount], - # B_=PublicKey(bytes.fromhex(proof.B_), raw=True), - # C_=PublicKey(bytes.fromhex(proof.C_), raw=True), - # ): - # raise Exception("Alice: DLEQ proof invalid.") if not b_dhke.carol_verify_dleq( secret_msg=proof.secret, C=PublicKey(bytes.fromhex(proof.C), raw=True), From 1a359a262aec7cbfe46adbb73db03eeb8e7e1581 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:37:14 +0200 Subject: [PATCH 42/59] remove unnecessary fields from BlindedSignature --- cashu/core/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 85931e51..c808abd6 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -171,8 +171,6 @@ class DLEQWallet(BaseModel): e: str s: str r: str # blinding_factor, unknown to mint but sent from wallet to wallet for DLEQ proof - # B_: Union[str, None] = None # blinded message, sent to the mint by the wallet - # C_: Union[str, None] = None # blinded signature, received by the mint class Proof(BaseModel): From beca6987af2dbb1c0231be34cc85beb89f9aed2d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:21:41 +0200 Subject: [PATCH 43/59] tests not working yet --- cashu/core/base.py | 58 ++++++++++++++++++++++----------------- cashu/mint/ledger.py | 26 +++++++++++------- cashu/wallet/wallet.py | 10 +++++-- tests/test_wallet_p2pk.py | 26 ++++++++++-------- 4 files changed, 70 insertions(+), 50 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index c808abd6..aa9032ea 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -55,7 +55,8 @@ def get_tag_all(self, tag_name: str) -> List[str]: all_tags = [] for tag in self.__root__: if tag[0] == tag_name: - all_tags.append(tag[1]) + for t in tag[1:]: + all_tags.append(t) return all_tags @@ -92,29 +93,12 @@ def deserialize(cls, from_proof: str): logger.debug(f"Deserialized Secret: {kind}, {data}, {nonce}, {tags}") return cls(kind=kind, data=data, nonce=nonce, tags=tags) - @property - def locktime(self) -> Union[None, int]: - if self.tags: - locktime = self.tags.get_tag("locktime") - if locktime: - return int(locktime) - return None - - @property - def sigflag(self) -> Union[None, str]: - if self.tags: - sigflag = self.tags.get_tag("sigflag") - if sigflag: - return sigflag - return None - @property - def n_sigs(self) -> Union[None, int]: - if self.tags: - n_sigs = self.tags.get_tag("n_sigs") - if n_sigs: - return int(n_sigs) - return None +class P2PKSecret(Secret): + @classmethod + def from_secret(cls, secret: Secret): + assert secret.kind == SecretKind.P2PK, "Secret is not a P2PK secret" + return cls(**secret.dict()) def get_p2pk_pubkey_from_secret(self) -> List[str]: """Gets the P2PK pubkey from a Secret depending on the locktime @@ -127,8 +111,8 @@ def get_p2pk_pubkey_from_secret(self) -> List[str]: """ pubkeys: List[str] = [self.data] # for now we only support one pubkey # get all additional pubkeys from tags for multisig - if self.tags and self.tags.get_tag("pubkey"): - pubkeys += self.tags.get_tag_all("pubkey") + if self.tags and self.tags.get_tag("pubkeys"): + pubkeys += self.tags.get_tag_all("pubkeys") now = time.time() if self.locktime and self.locktime < now: @@ -143,6 +127,30 @@ def get_p2pk_pubkey_from_secret(self) -> List[str]: return [] return pubkeys + @property + def locktime(self) -> Union[None, int]: + if self.tags: + locktime = self.tags.get_tag("locktime") + if locktime: + return int(locktime) + return None + + @property + def sigflag(self) -> Union[None, str]: + if self.tags: + sigflag = self.tags.get_tag("sigflag") + if sigflag: + return sigflag + return None + + @property + def n_sigs(self) -> Union[None, int]: + if self.tags: + n_sigs = self.tags.get_tag("n_sigs") + if n_sigs: + return int(n_sigs) + return None + class P2SHScript(BaseModel): """ diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 9e42a2ea..1fb47407 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -13,6 +13,7 @@ Invoice, MintKeyset, MintKeysets, + P2PKSecret, Proof, Secret, SecretKind, @@ -261,12 +262,13 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: # secret is not a spending condition so we treat is a normal secret return True if secret.kind == SecretKind.P2SH: + p2pk_secret = P2PKSecret.from_secret(secret) # check if locktime is in the past now = time.time() - if secret.locktime and secret.locktime < now: - logger.trace(f"p2sh locktime ran out ({secret.locktime}<{now}).") + if p2pk_secret.locktime and p2pk_secret.locktime < now: + logger.trace(f"p2sh locktime ran out ({p2pk_secret.locktime}<{now}).") return True - logger.trace(f"p2sh locktime still active ({secret.locktime}>{now}).") + logger.trace(f"p2sh locktime still active ({p2pk_secret.locktime}>{now}).") if ( proof.p2shscript is None @@ -291,8 +293,9 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: # P2PK if secret.kind == SecretKind.P2PK: + p2pk_secret = P2PKSecret.from_secret(secret) # check if locktime is in the past - pubkeys = secret.get_p2pk_pubkey_from_secret() + pubkeys = p2pk_secret.get_p2pk_pubkey_from_secret() assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique." logger.trace(f"pubkeys: {pubkeys}") # we will get an empty list if the locktime has passed and no refund pubkey is present @@ -314,7 +317,7 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: # INPUTS: check signatures proof.p2pksigs against pubkey # we expect the signature to be on the pubkey (=message) itself - n_sigs_required = secret.n_sigs or 1 + n_sigs_required = p2pk_secret.n_sigs or 1 assert n_sigs_required > 0, "n_sigs must be positive." # check if enough signatures are present @@ -328,9 +331,9 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: for input_sig in proof.p2pksigs: for pubkey in pubkeys: logger.trace(f"verifying signature {input_sig} by pubkey {pubkey}.") - logger.trace(f"Message: {secret.serialize().encode('utf-8')}") + logger.trace(f"Message: {p2pk_secret.serialize().encode('utf-8')}") if verify_p2pk_signature( - message=secret.serialize().encode("utf-8"), + message=p2pk_secret.serialize().encode("utf-8"), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), signature=bytes.fromhex(input_sig), ): @@ -377,7 +380,7 @@ def _verify_output_spending_conditions( n_sigs = [] for proof in proofs: try: - secret = Secret.deserialize(proof.secret) + secret = P2PKSecret.deserialize(proof.secret) # get all p2pk pubkeys from secrets pubkeys_per_proof.append(secret.get_p2pk_pubkey_from_secret()) # get signature threshold from secrets @@ -410,7 +413,10 @@ def _verify_output_spending_conditions( # now we check if any of the secrets has sigflag==SIG_ALL if not any( - [Secret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL for p in proofs] + [ + P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL + for p in proofs + ] ): # no secret has sigflag==SIG_ALL return True @@ -805,7 +811,7 @@ async def _generate_change_promises( return_amounts_sorted = sorted(return_amounts, reverse=True) # we need to imprint these amounts into the blanket outputs for i in range(len(outputs)): - outputs[i].amount = return_amounts_sorted[i] + outputs[i].amount = return_amounts_sorted[i] # type: ignore if not self._verify_no_duplicate_outputs(outputs): raise TransactionError("duplicate promises.") return_promises = await self._generate_promises(outputs, keyset) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index e3f52ae6..b9e7838e 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -28,6 +28,7 @@ GetMintResponse, Invoice, KeysetsResponse, + P2PKSecret, P2SHScript, PostMeltRequest, PostMintRequest, @@ -987,7 +988,10 @@ async def add_witnesses_to_outputs( # if any of the proofs provided require SIG_ALL, we must provide it if any( - [Secret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL for p in proofs] + [ + P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL + for p in proofs + ] ): # p2pk_signatures = await self.sign_p2pk_outputs(outputs) # for o, s in zip(outputs, p2pk_signatures): @@ -1558,7 +1562,7 @@ async def create_p2pk_lock( tags: Optional[Tags] = None, sig_all: bool = False, n_sigs: int = 1, - ) -> Secret: + ) -> P2PKSecret: logger.debug(f"Provided tags: {tags}") if not tags: tags = Tags() @@ -1571,7 +1575,7 @@ async def create_p2pk_lock( if n_sigs > 1: tags["n_sigs"] = str(n_sigs) logger.debug(f"After tags: {tags}") - return Secret( + return P2PKSecret( kind=SecretKind.P2PK, data=pubkey, tags=tags, diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index 50101af9..c0f066f8 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -182,15 +182,15 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): assert pubkey_wallet1 != pubkey_wallet2 # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet1]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) - # add signatures of wallet2 + # add signatures of wallet1 send_proofs = await wallet1.add_p2pk_witnesses_to_proofs(send_proofs) - # here we add the signatures of wallet1 + # here we add the signatures of wallet2 await wallet2.redeem(send_proofs) @@ -202,15 +202,15 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle assert pubkey_wallet1 != pubkey_wallet2 # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet1]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) - # add signatures of wallet2 + # add signatures of wallet2 – this is a duplicate signature send_proofs = await wallet2.add_p2pk_witnesses_to_proofs(send_proofs) - # here we add the signatures of wallet1 + # here we add the signatures of wallet2 await assert_err( wallet2.redeem(send_proofs), "Mint Error: p2pk signatures must be unique." ) @@ -224,7 +224,7 @@ async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wal assert pubkey_wallet1 != pubkey_wallet2 # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet1]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock @@ -243,7 +243,7 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal assert pubkey_wallet1 != pubkey_wallet2 # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet1]]), n_sigs=3 + pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=3 ) _, send_proofs = await wallet1.split_to_send( @@ -264,7 +264,7 @@ async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet2]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet2]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock @@ -287,7 +287,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key( # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkey", wrong_public_key_hex]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkeys", wrong_public_key_hex]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock @@ -300,14 +300,16 @@ async def test_p2pk_multisig_with_wrong_first_private_key( def test_tags(): - tags = Tags([["key1", "value1"], ["key2", "value2"], ["key2", "value3"]]) + tags = Tags( + [["key1", "value1"], ["key2", "value2", "value2_1"], ["key2", "value3"]] + ) assert tags.get_tag("key1") == "value1" assert tags["key1"] == "value1" assert tags.get_tag("key2") == "value2" assert tags["key2"] == "value2" assert tags.get_tag("key3") is None assert tags["key3"] is None - assert tags.get_tag_all("key2") == ["value2", "value3"] + assert tags.get_tag_all("key2") == ["value2", "value2_1", "value3"] @pytest.mark.asyncio From 8aaf0f822ef2a04bec9eeffccb4201679b98106b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:02:14 +0200 Subject: [PATCH 44/59] spelling mistakes --- cashu/wallet/wallet.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index b9e7838e..8886135f 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -569,7 +569,7 @@ def _splitrequest_include_fields(proofs: List[Proof]): @async_set_requests async def check_proof_state(self, proofs: List[Proof]): """ - Cheks whether the secrets in proofs are already spent or not and returns a list of booleans. + Checks whether the secrets in proofs are already spent or not and returns a list of booleans. """ payload = CheckSpendableRequest(proofs=proofs) @@ -640,8 +640,8 @@ async def restore_promises( payload = PostMintRequest(outputs=outputs) resp = self.s.post(self.url + "/restore", json=payload.dict()) self.raise_on_error(resp) - reponse_dict = resp.json() - returnObj = PostRestoreResponse.parse_obj(reponse_dict) + response_dict = resp.json() + returnObj = PostRestoreResponse.parse_obj(response_dict) return returnObj.outputs, returnObj.promises @@ -795,16 +795,16 @@ async def generate_determinstic_secret( self, counter: int ) -> Tuple[bytes, bytes, str]: """ - Determinstically generates two secrets (one as the secret message, + Deterministically generates two secrets (one as the secret message, one as the blinding factor). """ assert self.bip32, "BIP32 not initialized yet." # integer keyset id modulo max number of bip32 child keys - keyest_id = int.from_bytes(base64.b64decode(self.keyset_id), "big") % ( + keyset_id = int.from_bytes(base64.b64decode(self.keyset_id), "big") % ( 2**31 - 1 ) - logger.trace(f"keyset id: {self.keyset_id} becomes {keyest_id}") - token_derivation_path = f"m/129372'/0'/{keyest_id}'/{counter}'" + logger.trace(f"keyset id: {self.keyset_id} becomes {keyset_id}") + token_derivation_path = f"m/129372'/0'/{keyset_id}'/{counter}'" # for secret secret_derivation_path = f"{token_derivation_path}/0" logger.trace(f"secret derivation path: {secret_derivation_path}") From 57f396762da4eb93c55b955a286b5e54a368c81a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:02:47 +0200 Subject: [PATCH 45/59] spelling mistakes --- cashu/core/script.py | 6 +++--- cashu/wallet/wallet.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cashu/core/script.py b/cashu/core/script.py index 0fc2a8ea..19827090 100644 --- a/cashu/core/script.py +++ b/cashu/core/script.py @@ -25,7 +25,7 @@ def step0_carol_privkey(): return seckey -def step0_carol_checksig_redeemscrip(carol_pubkey): +def step0_carol_checksig_redeemscript(carol_pubkey): """Create script""" txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG]) # txin_redeemScript = CScript([-123, OP_CHECKLOCKTIMEVERIFY]) @@ -111,7 +111,7 @@ def verify_bitcoin_script(txin_redeemScript_b64, txin_signature_b64): # --------- # CAROL defines scripthash and ALICE mints them alice_privkey = step0_carol_privkey() - txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub) + txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub) print("Script:", txin_redeemScript.__repr__()) txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) print(f"Carol sends Alice secret = P2SH:{txin_p2sh_address}") @@ -128,7 +128,7 @@ def verify_bitcoin_script(txin_redeemScript_b64, txin_signature_b64): # CAROL redeems with MINT # CAROL PRODUCES txin_redeemScript and txin_signature to send to MINT - txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub) + txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub) txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 8886135f..26d7eedf 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -54,7 +54,7 @@ from ..core.migrations import migrate_databases from ..core.p2pk import sign_p2pk_sign from ..core.script import ( - step0_carol_checksig_redeemscrip, + step0_carol_checksig_redeemscript, step0_carol_privkey, step1_carol_create_p2sh_address, step2_carol_sign_tx, @@ -787,11 +787,11 @@ async def _generate_secret(self, randombits=128) -> str: db=self.db, keyset_id=self.keyset_id ) logger.trace(f"secret_counter: {secret_counter}") - s, _, _ = await self.generate_determinstic_secret(secret_counter) + s, _, _ = await self.generate_deterministic_secret(secret_counter) # return s.decode("utf-8") return hashlib.sha256(s).hexdigest() - async def generate_determinstic_secret( + async def generate_deterministic_secret( self, counter: int ) -> Tuple[bytes, bytes, str]: """ @@ -840,7 +840,7 @@ async def generate_n_secrets( f"Generating secret nr {secret_counters[0]} to {secret_counters[-1]}." ) secrets_rs_derivationpaths = [ - await self.generate_determinstic_secret(s) for s in secret_counters + await self.generate_deterministic_secret(s) for s in secret_counters ] # secrets are supplied as str secrets = [hashlib.sha256(s[0]).hexdigest() for s in secrets_rs_derivationpaths] @@ -874,7 +874,7 @@ async def generate_secrets_from_to( ), "from_counter must be smaller than to_counter" secret_counters = [c for c in range(from_counter, to_counter + 1)] secrets_rs_derivationpaths = [ - await self.generate_determinstic_secret(s) for s in secret_counters + await self.generate_deterministic_secret(s) for s in secret_counters ] # secrets are supplied as str secrets = [hashlib.sha256(s[0]).hexdigest() for s in secrets_rs_derivationpaths] @@ -1532,7 +1532,7 @@ async def split_to_send( async def create_p2sh_address_and_store(self) -> str: """Creates a P2SH lock script and stores the script and signature in the database.""" alice_privkey = step0_carol_privkey() - txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub) + txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub) txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() From 877c164c63d0f8fda34b045aed10ff3681d14a72 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:05:36 +0200 Subject: [PATCH 46/59] fix more spelling mistakes --- cashu/wallet/wallet.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 26d7eedf..9d178b57 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -523,9 +523,9 @@ async def mint(self, amounts: List[int], hash: Optional[str] = None) -> List[Pro }, ) self.raise_on_error(resp) - reponse_dict = resp.json() + response_dict = resp.json() logger.trace("Lightning invoice checked. POST /mint") - promises = PostMintResponse.parse_obj(reponse_dict).promises + promises = PostMintResponse.parse_obj(response_dict).promises # bump secret counter in database await bump_secret_derivation( @@ -672,7 +672,7 @@ def __init__( self.name = name super().__init__(url=url, db=self.db) - logger.debug(f"Wallet initalized with mint URL {url}") + logger.debug(f"Wallet initialized with mint URL {url}") @classmethod async def with_db( @@ -1133,9 +1133,9 @@ async def split( logger.debug(f"Creating proofs with custom secrets: {secret_locks}") assert len(secret_locks) == len( scnd_outputs - ), "number of secret_locks does not match number of ouptus." + ), "number of secret_locks does not match number of outputs." # append predefined secrets (to send) to random secrets (to keep) - # generate sercets to keep + # generate secrets to keep secrets = [ await self._generate_secret() for s in range(len(frst_outputs)) ] + secret_locks @@ -1681,7 +1681,7 @@ async def restore_wallet_from_mnemonic( Args: mnemonic (Optional[str]): The mnemonic to restore the wallet from. If None, the mnemonic is loaded from the db. - to (int, optional): The number of consecutive empty reponses to stop restoring. Defaults to 2. + to (int, optional): The number of consecutive empty responses to stop restoring. Defaults to 2. batch (int, optional): The number of proofs to restore in one batch. Defaults to 25. """ await self._init_private_key(mnemonic) @@ -1741,7 +1741,7 @@ async def restore_promises_from_to( ) # we don't know the amount but luckily the mint will tell us so we use a dummy amount here amounts_dummy = [1] * len(secrets) - # we generate outptus from deterministic secrets and rs + # we generate outputs from deterministic secrets and rs regenerated_outputs, _ = self._construct_outputs(amounts_dummy, secrets, rs) # we ask the mint to reissue the promises proofs = await self.restore_promises( From e471c7468fc4131e1a83157606498879c18792d1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:23:05 +0200 Subject: [PATCH 47/59] revert to normal --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8ca5fd9d..fb3cbfa8 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ black-check: poetry run black . --exclude cashu/nostr --check mypy: - poetry run mypy cashu tests --ignore-missing + poetry run mypy cashu --ignore-missing format: black ruff From 84d89caea1229a64118cf34a10df24dbb96bd551 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:23:14 +0200 Subject: [PATCH 48/59] add comments --- cashu/wallet/wallet.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 9d178b57..9ea5c4ef 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -964,6 +964,14 @@ async def mint( async def add_p2pk_witnesses_to_outputs( self, outputs: List[BlindedMessage] ) -> List[BlindedMessage]: + """Takes a list of outputs and adds a P2PK signatures to each. + + Args: + outputs (List[BlindedMessage]): Outputs to add P2PK signatures to + + Returns: + List[BlindedMessage]: Outputs with P2PK signatures added + """ p2pk_signatures = await self.sign_p2pk_outputs(outputs) for o, s in zip(outputs, p2pk_signatures): o.p2pksigs = [s] @@ -975,8 +983,11 @@ async def add_witnesses_to_outputs( """Adds witnesses to outputs if the inputs (proofs) indicate an appropriate signature flag Args: - proofs (List[Proof]): _description_ - outputs (List[BlindedMessage]): _description_ + proofs (List[Proof]): Inputs to the transaction + outputs (List[BlindedMessage]): Outputs to add witnesses to + + Returns: + List[BlindedMessage]: Outputs with signatures added """ # first we check whether all tokens have serialized secrets as their secret try: @@ -993,9 +1004,6 @@ async def add_witnesses_to_outputs( for p in proofs ] ): - # p2pk_signatures = await self.sign_p2pk_outputs(outputs) - # for o, s in zip(outputs, p2pk_signatures): - # o.p2pksigs = [s] outputs = await self.add_p2pk_witnesses_to_outputs(outputs) return outputs From 6451950c6d0747960d0e6d26b4941ea2c3e4adb1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:24:59 +0200 Subject: [PATCH 49/59] bdhke: generalize hash_e --- cashu/core/crypto/b_dhke.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cashu/core/crypto/b_dhke.py b/cashu/core/crypto/b_dhke.py index 457349b2..e8706239 100644 --- a/cashu/core/crypto/b_dhke.py +++ b/cashu/core/crypto/b_dhke.py @@ -97,12 +97,11 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: return C == Y.mult(a) # type: ignore -def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey) -> bytes: - _R1 = R1.serialize(compressed=False).hex() - _R2 = R2.serialize(compressed=False).hex() - _K = K.serialize(compressed=False).hex() - _C_ = C_.serialize(compressed=False).hex() - e_ = f"{_R1}{_R2}{_K}{_C_}" +def hash_e(*publickeys: PublicKey) -> bytes: + e_ = "" + for p in publickeys: + _p = p.serialize(compressed=False).hex() + e_ += str(_p) e = hashlib.sha256(e_.encode("utf-8")).digest() return e From 9ac144a7257b8c028ac0d6c43d864417727247ea Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:39:38 +0200 Subject: [PATCH 50/59] remove P2PKSecret changes --- cashu/core/base.py | 58 ++++++++++++++++++------------------------ cashu/mint/ledger.py | 24 +++++++---------- cashu/wallet/wallet.py | 10 +++----- 3 files changed, 37 insertions(+), 55 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index aa9032ea..c808abd6 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -55,8 +55,7 @@ def get_tag_all(self, tag_name: str) -> List[str]: all_tags = [] for tag in self.__root__: if tag[0] == tag_name: - for t in tag[1:]: - all_tags.append(t) + all_tags.append(tag[1]) return all_tags @@ -93,12 +92,29 @@ def deserialize(cls, from_proof: str): logger.debug(f"Deserialized Secret: {kind}, {data}, {nonce}, {tags}") return cls(kind=kind, data=data, nonce=nonce, tags=tags) + @property + def locktime(self) -> Union[None, int]: + if self.tags: + locktime = self.tags.get_tag("locktime") + if locktime: + return int(locktime) + return None -class P2PKSecret(Secret): - @classmethod - def from_secret(cls, secret: Secret): - assert secret.kind == SecretKind.P2PK, "Secret is not a P2PK secret" - return cls(**secret.dict()) + @property + def sigflag(self) -> Union[None, str]: + if self.tags: + sigflag = self.tags.get_tag("sigflag") + if sigflag: + return sigflag + return None + + @property + def n_sigs(self) -> Union[None, int]: + if self.tags: + n_sigs = self.tags.get_tag("n_sigs") + if n_sigs: + return int(n_sigs) + return None def get_p2pk_pubkey_from_secret(self) -> List[str]: """Gets the P2PK pubkey from a Secret depending on the locktime @@ -111,8 +127,8 @@ def get_p2pk_pubkey_from_secret(self) -> List[str]: """ pubkeys: List[str] = [self.data] # for now we only support one pubkey # get all additional pubkeys from tags for multisig - if self.tags and self.tags.get_tag("pubkeys"): - pubkeys += self.tags.get_tag_all("pubkeys") + if self.tags and self.tags.get_tag("pubkey"): + pubkeys += self.tags.get_tag_all("pubkey") now = time.time() if self.locktime and self.locktime < now: @@ -127,30 +143,6 @@ def get_p2pk_pubkey_from_secret(self) -> List[str]: return [] return pubkeys - @property - def locktime(self) -> Union[None, int]: - if self.tags: - locktime = self.tags.get_tag("locktime") - if locktime: - return int(locktime) - return None - - @property - def sigflag(self) -> Union[None, str]: - if self.tags: - sigflag = self.tags.get_tag("sigflag") - if sigflag: - return sigflag - return None - - @property - def n_sigs(self) -> Union[None, int]: - if self.tags: - n_sigs = self.tags.get_tag("n_sigs") - if n_sigs: - return int(n_sigs) - return None - class P2SHScript(BaseModel): """ diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 1fb47407..dc9b2093 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -13,7 +13,6 @@ Invoice, MintKeyset, MintKeysets, - P2PKSecret, Proof, Secret, SecretKind, @@ -262,13 +261,12 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: # secret is not a spending condition so we treat is a normal secret return True if secret.kind == SecretKind.P2SH: - p2pk_secret = P2PKSecret.from_secret(secret) # check if locktime is in the past now = time.time() - if p2pk_secret.locktime and p2pk_secret.locktime < now: - logger.trace(f"p2sh locktime ran out ({p2pk_secret.locktime}<{now}).") + if secret.locktime and secret.locktime < now: + logger.trace(f"p2sh locktime ran out ({secret.locktime}<{now}).") return True - logger.trace(f"p2sh locktime still active ({p2pk_secret.locktime}>{now}).") + logger.trace(f"p2sh locktime still active ({secret.locktime}>{now}).") if ( proof.p2shscript is None @@ -293,9 +291,8 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: # P2PK if secret.kind == SecretKind.P2PK: - p2pk_secret = P2PKSecret.from_secret(secret) # check if locktime is in the past - pubkeys = p2pk_secret.get_p2pk_pubkey_from_secret() + pubkeys = secret.get_p2pk_pubkey_from_secret() assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique." logger.trace(f"pubkeys: {pubkeys}") # we will get an empty list if the locktime has passed and no refund pubkey is present @@ -317,7 +314,7 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: # INPUTS: check signatures proof.p2pksigs against pubkey # we expect the signature to be on the pubkey (=message) itself - n_sigs_required = p2pk_secret.n_sigs or 1 + n_sigs_required = secret.n_sigs or 1 assert n_sigs_required > 0, "n_sigs must be positive." # check if enough signatures are present @@ -331,9 +328,9 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: for input_sig in proof.p2pksigs: for pubkey in pubkeys: logger.trace(f"verifying signature {input_sig} by pubkey {pubkey}.") - logger.trace(f"Message: {p2pk_secret.serialize().encode('utf-8')}") + logger.trace(f"Message: {secret.serialize().encode('utf-8')}") if verify_p2pk_signature( - message=p2pk_secret.serialize().encode("utf-8"), + message=secret.serialize().encode("utf-8"), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), signature=bytes.fromhex(input_sig), ): @@ -380,7 +377,7 @@ def _verify_output_spending_conditions( n_sigs = [] for proof in proofs: try: - secret = P2PKSecret.deserialize(proof.secret) + secret = Secret.deserialize(proof.secret) # get all p2pk pubkeys from secrets pubkeys_per_proof.append(secret.get_p2pk_pubkey_from_secret()) # get signature threshold from secrets @@ -413,10 +410,7 @@ def _verify_output_spending_conditions( # now we check if any of the secrets has sigflag==SIG_ALL if not any( - [ - P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL - for p in proofs - ] + [Secret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL for p in proofs] ): # no secret has sigflag==SIG_ALL return True diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 9ea5c4ef..ca04b01d 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -28,7 +28,6 @@ GetMintResponse, Invoice, KeysetsResponse, - P2PKSecret, P2SHScript, PostMeltRequest, PostMintRequest, @@ -999,10 +998,7 @@ async def add_witnesses_to_outputs( # if any of the proofs provided require SIG_ALL, we must provide it if any( - [ - P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL - for p in proofs - ] + [Secret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL for p in proofs] ): outputs = await self.add_p2pk_witnesses_to_outputs(outputs) return outputs @@ -1570,7 +1566,7 @@ async def create_p2pk_lock( tags: Optional[Tags] = None, sig_all: bool = False, n_sigs: int = 1, - ) -> P2PKSecret: + ) -> Secret: logger.debug(f"Provided tags: {tags}") if not tags: tags = Tags() @@ -1583,7 +1579,7 @@ async def create_p2pk_lock( if n_sigs > 1: tags["n_sigs"] = str(n_sigs) logger.debug(f"After tags: {tags}") - return P2PKSecret( + return Secret( kind=SecretKind.P2PK, data=pubkey, tags=tags, From 2fee2499766530cfac2826fdf808460516994a2e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:41:34 +0200 Subject: [PATCH 51/59] revert tests for P2PKSecret --- tests/test_wallet_p2pk.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index c0f066f8..9a0f9b34 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -182,7 +182,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): assert pubkey_wallet1 != pubkey_wallet2 # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet1]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( @@ -202,7 +202,7 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle assert pubkey_wallet1 != pubkey_wallet2 # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet1]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( @@ -224,7 +224,7 @@ async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wal assert pubkey_wallet1 != pubkey_wallet2 # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet1]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock @@ -243,7 +243,7 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal assert pubkey_wallet1 != pubkey_wallet2 # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=3 + pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet1]]), n_sigs=3 ) _, send_proofs = await wallet1.split_to_send( @@ -264,7 +264,7 @@ async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet2]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkey", pubkey_wallet2]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock @@ -287,7 +287,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key( # p2pk test secret_lock = await wallet1.create_p2pk_lock( - pubkey_wallet2, tags=Tags([["pubkeys", wrong_public_key_hex]]), n_sigs=2 + pubkey_wallet2, tags=Tags([["pubkey", wrong_public_key_hex]]), n_sigs=2 ) _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock From 23c2bbc5a50439abb175139f54104f6b33fefd2b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:43:09 +0200 Subject: [PATCH 52/59] revert tests --- tests/test_wallet_p2pk.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index 9a0f9b34..967e250e 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -300,16 +300,14 @@ async def test_p2pk_multisig_with_wrong_first_private_key( def test_tags(): - tags = Tags( - [["key1", "value1"], ["key2", "value2", "value2_1"], ["key2", "value3"]] - ) + tags = Tags([["key1", "value1"], ["key2", "value2"], ["key2", "value3"]]) assert tags.get_tag("key1") == "value1" assert tags["key1"] == "value1" assert tags.get_tag("key2") == "value2" assert tags["key2"] == "value2" assert tags.get_tag("key3") is None assert tags["key3"] is None - assert tags.get_tag_all("key2") == ["value2", "value2_1", "value3"] + assert tags.get_tag_all("key2") == ["value2", "value3"] @pytest.mark.asyncio From e1a7c888ffda4d6db81d38bbd141156732f98980 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:44:01 +0200 Subject: [PATCH 53/59] revert test fully --- tests/test_wallet_p2pk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index 967e250e..50101af9 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -188,9 +188,9 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) - # add signatures of wallet1 + # add signatures of wallet2 send_proofs = await wallet1.add_p2pk_witnesses_to_proofs(send_proofs) - # here we add the signatures of wallet2 + # here we add the signatures of wallet1 await wallet2.redeem(send_proofs) @@ -208,9 +208,9 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle _, send_proofs = await wallet1.split_to_send( wallet1.proofs, 8, secret_lock=secret_lock ) - # add signatures of wallet2 – this is a duplicate signature + # add signatures of wallet2 send_proofs = await wallet2.add_p2pk_witnesses_to_proofs(send_proofs) - # here we add the signatures of wallet2 + # here we add the signatures of wallet1 await assert_err( wallet2.redeem(send_proofs), "Mint Error: p2pk signatures must be unique." ) From 42e447b291f17dadae832d7e4d4eb931f8081584 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:47:19 +0200 Subject: [PATCH 54/59] revert p2pksecret changes --- cashu/wallet/wallet.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index ca04b01d..179780e7 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -963,14 +963,6 @@ async def mint( async def add_p2pk_witnesses_to_outputs( self, outputs: List[BlindedMessage] ) -> List[BlindedMessage]: - """Takes a list of outputs and adds a P2PK signatures to each. - - Args: - outputs (List[BlindedMessage]): Outputs to add P2PK signatures to - - Returns: - List[BlindedMessage]: Outputs with P2PK signatures added - """ p2pk_signatures = await self.sign_p2pk_outputs(outputs) for o, s in zip(outputs, p2pk_signatures): o.p2pksigs = [s] @@ -982,11 +974,8 @@ async def add_witnesses_to_outputs( """Adds witnesses to outputs if the inputs (proofs) indicate an appropriate signature flag Args: - proofs (List[Proof]): Inputs to the transaction - outputs (List[BlindedMessage]): Outputs to add witnesses to - - Returns: - List[BlindedMessage]: Outputs with signatures added + proofs (List[Proof]): _description_ + outputs (List[BlindedMessage]): _description_ """ # first we check whether all tokens have serialized secrets as their secret try: @@ -1000,6 +989,9 @@ async def add_witnesses_to_outputs( if any( [Secret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL for p in proofs] ): + # p2pk_signatures = await self.sign_p2pk_outputs(outputs) + # for o, s in zip(outputs, p2pk_signatures): + # o.p2pksigs = [s] outputs = await self.add_p2pk_witnesses_to_outputs(outputs) return outputs From 023fc810e912f203141dc83931eeab29579bdc1f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Sep 2023 18:22:12 +0200 Subject: [PATCH 55/59] refactor proof invalidation --- cashu/wallet/wallet.py | 400 ++++++++++++++++++++++++++--------------- tests/test_wallet.py | 1 + 2 files changed, 256 insertions(+), 145 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index edf9a5df..de09009f 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -124,121 +124,121 @@ async def _init_s(self): """Dummy function that can be called from outside to use LedgerAPI.s""" return - # ---------- DLEQ PROOFS ---------- - - def verify_proofs_dleq(self, proofs: List[Proof]): - """Verifies DLEQ proofs in proofs.""" - for proof in proofs: - if not proof.dleq: - logger.trace("No DLEQ proof in proof.") - return - logger.trace("Verifying DLEQ proof.") - assert self.keys.public_keys - if not b_dhke.carol_verify_dleq( - secret_msg=proof.secret, - C=PublicKey(bytes.fromhex(proof.C), raw=True), - r=PrivateKey(bytes.fromhex(proof.dleq.r), raw=True), - e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), - s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), - A=self.keys.public_keys[proof.amount], - ): - raise Exception("DLEQ proof invalid.") - else: - logger.debug("DLEQ proof valid.") - - def _construct_proofs( - self, - promises: List[BlindedSignature], - secrets: List[str], - rs: List[PrivateKey], - derivation_paths: List[str], - ) -> List[Proof]: - """Constructs proofs from promises, secrets, rs and derivation paths. - - This method is called after the user has received blind signatures from - the mint. The results are proofs that can be used as ecash. - - Args: - promises (List[BlindedSignature]): blind signatures from mint - secrets (List[str]): secrets that were previously used to create blind messages (that turned into promises) - rs (List[PrivateKey]): blinding factors that were previously used to create blind messages (that turned into promises) - derivation_paths (List[str]): derivation paths that were used to generate secrets and blinding factors - - Returns: - List[Proof]: list of proofs that can be used as ecash - """ - logger.trace("Constructing proofs.") - proofs: List[Proof] = [] - for promise, secret, r, path in zip(promises, secrets, rs, derivation_paths): - logger.trace(f"Creating proof with keyset {self.keyset_id} = {promise.id}") - assert ( - self.keyset_id == promise.id - ), "our keyset id does not match promise id." - - C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) - C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount]) - B_, r = b_dhke.step1_alice(secret, r) # recompute B_ for dleq proofs - - proof = Proof( - id=promise.id, - amount=promise.amount, - C=C.serialize().hex(), - secret=secret, - derivation_path=path, - ) - - # if the mint returned a dleq proof, we add it to the proof - if promise.dleq: - proof.dleq = DLEQWallet( - e=promise.dleq.e, s=promise.dleq.s, r=r.serialize() - ) - - proofs.append(proof) - - logger.trace( - f"Created proof: {proof}, r: {r.serialize()} out of promise {promise}" - ) - - # DLEQ verify - self.verify_proofs_dleq(proofs) - - logger.trace(f"Constructed {len(proofs)} proofs.") - return proofs - - @staticmethod - def _construct_outputs( - amounts: List[int], secrets: List[str], rs: List[PrivateKey] = [] - ) -> Tuple[List[BlindedMessage], List[PrivateKey]]: - """Takes a list of amounts and secrets and returns outputs. - Outputs are blinded messages `outputs` and blinding factors `rs` - - Args: - amounts (List[int]): list of amounts - secrets (List[str]): list of secrets - rs (List[PrivateKey], optional): list of blinding factors. If not given, `rs` are generated in step1_alice. Defaults to []. - - Returns: - List[BlindedMessage]: list of blinded messages that can be sent to the mint - List[PrivateKey]: list of blinding factors that can be used to construct proofs after receiving blind signatures from the mint - - Raises: - AssertionError: if len(amounts) != len(secrets) - """ - assert len(amounts) == len( - secrets - ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}" - outputs: List[BlindedMessage] = [] - - rs_ = [None] * len(amounts) if not rs else rs - rs_return: List[PrivateKey] = [] - for secret, amount, r in zip(secrets, amounts, rs_): - B_, r = b_dhke.step1_alice(secret, r or None) - rs_return.append(r) - output = BlindedMessage(amount=amount, B_=B_.serialize().hex()) - outputs.append(output) - logger.trace(f"Constructing output: {output}, r: {r.serialize()}") - - return outputs, rs_return + # # ---------- DLEQ PROOFS ---------- + + # def verify_proofs_dleq(self, proofs: List[Proof]): + # """Verifies DLEQ proofs in proofs.""" + # for proof in proofs: + # if not proof.dleq: + # logger.trace("No DLEQ proof in proof.") + # return + # logger.trace("Verifying DLEQ proof.") + # assert self.keys.public_keys + # if not b_dhke.carol_verify_dleq( + # secret_msg=proof.secret, + # C=PublicKey(bytes.fromhex(proof.C), raw=True), + # r=PrivateKey(bytes.fromhex(proof.dleq.r), raw=True), + # e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), + # s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), + # A=self.keys.public_keys[proof.amount], + # ): + # raise Exception("DLEQ proof invalid.") + # else: + # logger.debug("DLEQ proof valid.") + + # def _construct_proofs( + # self, + # promises: List[BlindedSignature], + # secrets: List[str], + # rs: List[PrivateKey], + # derivation_paths: List[str], + # ) -> List[Proof]: + # """Constructs proofs from promises, secrets, rs and derivation paths. + + # This method is called after the user has received blind signatures from + # the mint. The results are proofs that can be used as ecash. + + # Args: + # promises (List[BlindedSignature]): blind signatures from mint + # secrets (List[str]): secrets that were previously used to create blind messages (that turned into promises) + # rs (List[PrivateKey]): blinding factors that were previously used to create blind messages (that turned into promises) + # derivation_paths (List[str]): derivation paths that were used to generate secrets and blinding factors + + # Returns: + # List[Proof]: list of proofs that can be used as ecash + # """ + # logger.trace("Constructing proofs.") + # proofs: List[Proof] = [] + # for promise, secret, r, path in zip(promises, secrets, rs, derivation_paths): + # logger.trace(f"Creating proof with keyset {self.keyset_id} = {promise.id}") + # assert ( + # self.keyset_id == promise.id + # ), "our keyset id does not match promise id." + + # C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) + # C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount]) + # B_, r = b_dhke.step1_alice(secret, r) # recompute B_ for dleq proofs + + # proof = Proof( + # id=promise.id, + # amount=promise.amount, + # C=C.serialize().hex(), + # secret=secret, + # derivation_path=path, + # ) + + # # if the mint returned a dleq proof, we add it to the proof + # if promise.dleq: + # proof.dleq = DLEQWallet( + # e=promise.dleq.e, s=promise.dleq.s, r=r.serialize() + # ) + + # proofs.append(proof) + + # logger.trace( + # f"Created proof: {proof}, r: {r.serialize()} out of promise {promise}" + # ) + + # # DLEQ verify + # self.verify_proofs_dleq(proofs) + + # logger.trace(f"Constructed {len(proofs)} proofs.") + # return proofs + + # @staticmethod + # def _construct_outputs( + # amounts: List[int], secrets: List[str], rs: List[PrivateKey] = [] + # ) -> Tuple[List[BlindedMessage], List[PrivateKey]]: + # """Takes a list of amounts and secrets and returns outputs. + # Outputs are blinded messages `outputs` and blinding factors `rs` + + # Args: + # amounts (List[int]): list of amounts + # secrets (List[str]): list of secrets + # rs (List[PrivateKey], optional): list of blinding factors. If not given, `rs` are generated in step1_alice. Defaults to []. + + # Returns: + # List[BlindedMessage]: list of blinded messages that can be sent to the mint + # List[PrivateKey]: list of blinding factors that can be used to construct proofs after receiving blind signatures from the mint + + # Raises: + # AssertionError: if len(amounts) != len(secrets) + # """ + # assert len(amounts) == len( + # secrets + # ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}" + # outputs: List[BlindedMessage] = [] + + # rs_ = [None] * len(amounts) if not rs else rs + # rs_return: List[PrivateKey] = [] + # for secret, amount, r in zip(secrets, amounts, rs_): + # B_, r = b_dhke.step1_alice(secret, r or None) + # rs_return.append(r) + # output = BlindedMessage(amount=amount, B_=B_.serialize().hex()) + # outputs.append(output) + # logger.trace(f"Constructing output: {output}, r: {r.serialize()}") + + # return outputs, rs_return @staticmethod def raise_on_error(resp: Response) -> None: @@ -761,16 +761,12 @@ async def mint( await bump_secret_derivation( db=self.db, keyset_id=self.keyset_id, by=len(amounts) ) - proofs = self._construct_proofs(promises, secrets, rs, derivation_paths) + proofs = await self._construct_proofs(promises, secrets, rs, derivation_paths) - if proofs == []: - raise Exception("received no proofs.") - await self._store_proofs(proofs) if hash: await update_lightning_invoice( db=self.db, hash=hash, paid=True, time_paid=int(time.time()) ) - self.proofs += proofs return proofs async def redeem( @@ -861,18 +857,11 @@ async def split( promises = await super().split(proofs, outputs) # Construct proofs from returned promises (i.e., unblind the signatures) - new_proofs = self._construct_proofs(promises, secrets, rs, derivation_paths) + new_proofs = await self._construct_proofs( + promises, secrets, rs, derivation_paths + ) - # remove used proofs from wallet and add new ones - used_secrets = [p.secret for p in proofs] - self.proofs = list(filter(lambda p: p.secret not in used_secrets, self.proofs)) - # add new proofs to wallet - self.proofs += new_proofs - # store new proofs in database - await self._store_proofs(new_proofs) - # invalidate used proofs in database - for proof in proofs: - await invalidate_proof(proof, db=self.db) + await self.invalidate(proofs) keep_proofs = new_proofs[: len(frst_outputs)] send_proofs = new_proofs[len(frst_outputs) :] @@ -901,7 +890,6 @@ async def pay_lightning( if status.paid: # the payment was successful - await self.invalidate(proofs) invoice_obj = Invoice( amount=-sum_proofs(proofs), pr=invoice, @@ -916,14 +904,15 @@ async def pay_lightning( # handle change and produce proofs if status.change: - change_proofs = self._construct_proofs( + change_proofs = await self._construct_proofs( status.change, secrets[: len(status.change)], rs[: len(status.change)], derivation_paths[: len(status.change)], ) logger.debug(f"Received change: {sum_proofs(change_proofs)} sat") - await self._store_proofs(change_proofs) + + await self.invalidate(proofs) else: raise Exception("could not pay invoice.") @@ -934,10 +923,137 @@ async def check_proof_state(self, proofs): # ---------- TOKEN MECHANICS ---------- + # ---------- DLEQ PROOFS ---------- + + def verify_proofs_dleq(self, proofs: List[Proof]): + """Verifies DLEQ proofs in proofs.""" + for proof in proofs: + if not proof.dleq: + logger.trace("No DLEQ proof in proof.") + return + logger.trace("Verifying DLEQ proof.") + assert self.keys.public_keys + if not b_dhke.carol_verify_dleq( + secret_msg=proof.secret, + C=PublicKey(bytes.fromhex(proof.C), raw=True), + r=PrivateKey(bytes.fromhex(proof.dleq.r), raw=True), + e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), + s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), + A=self.keys.public_keys[proof.amount], + ): + raise Exception("DLEQ proof invalid.") + else: + logger.debug("DLEQ proof valid.") + + async def _construct_proofs( + self, + promises: List[BlindedSignature], + secrets: List[str], + rs: List[PrivateKey], + derivation_paths: List[str], + ) -> List[Proof]: + """Constructs proofs from promises, secrets, rs and derivation paths. + + This method is called after the user has received blind signatures from + the mint. The results are proofs that can be used as ecash. + + Args: + promises (List[BlindedSignature]): blind signatures from mint + secrets (List[str]): secrets that were previously used to create blind messages (that turned into promises) + rs (List[PrivateKey]): blinding factors that were previously used to create blind messages (that turned into promises) + derivation_paths (List[str]): derivation paths that were used to generate secrets and blinding factors + + Returns: + List[Proof]: list of proofs that can be used as ecash + """ + logger.trace("Constructing proofs.") + proofs: List[Proof] = [] + for promise, secret, r, path in zip(promises, secrets, rs, derivation_paths): + logger.trace(f"Creating proof with keyset {self.keyset_id} = {promise.id}") + assert ( + self.keyset_id == promise.id + ), "our keyset id does not match promise id." + + C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) + C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount]) + B_, r = b_dhke.step1_alice(secret, r) # recompute B_ for dleq proofs + + proof = Proof( + id=promise.id, + amount=promise.amount, + C=C.serialize().hex(), + secret=secret, + derivation_path=path, + ) + + # if the mint returned a dleq proof, we add it to the proof + if promise.dleq: + proof.dleq = DLEQWallet( + e=promise.dleq.e, s=promise.dleq.s, r=r.serialize() + ) + + proofs.append(proof) + + logger.trace( + f"Created proof: {proof}, r: {r.serialize()} out of promise {promise}" + ) + + # DLEQ verify + self.verify_proofs_dleq(proofs) + + logger.trace(f"Constructed {len(proofs)} proofs.") + + # add new proofs to wallet + self.proofs += proofs + # store new proofs in database + await self._store_proofs(proofs) + + return proofs + + @staticmethod + def _construct_outputs( + amounts: List[int], secrets: List[str], rs: List[PrivateKey] = [] + ) -> Tuple[List[BlindedMessage], List[PrivateKey]]: + """Takes a list of amounts and secrets and returns outputs. + Outputs are blinded messages `outputs` and blinding factors `rs` + + Args: + amounts (List[int]): list of amounts + secrets (List[str]): list of secrets + rs (List[PrivateKey], optional): list of blinding factors. If not given, `rs` are generated in step1_alice. Defaults to []. + + Returns: + List[BlindedMessage]: list of blinded messages that can be sent to the mint + List[PrivateKey]: list of blinding factors that can be used to construct proofs after receiving blind signatures from the mint + + Raises: + AssertionError: if len(amounts) != len(secrets) + """ + assert len(amounts) == len( + secrets + ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}" + outputs: List[BlindedMessage] = [] + + rs_ = [None] * len(amounts) if not rs else rs + rs_return: List[PrivateKey] = [] + for secret, amount, r in zip(secrets, amounts, rs_): + B_, r = b_dhke.step1_alice(secret, r or None) + rs_return.append(r) + output = BlindedMessage(amount=amount, B_=B_.serialize().hex()) + outputs.append(output) + logger.trace(f"Constructing output: {output}, r: {r.serialize()}") + + return outputs, rs_return + async def _store_proofs(self, proofs): - async with self.db.connect() as conn: - for proof in proofs: - await store_proof(proof, db=self.db, conn=conn) + try: + async with self.db.connect() as conn: + for proof in proofs: + await store_proof(proof, db=self.db, conn=conn) + except Exception as e: + logger.error(f"Could not store proofs in database: {e}") + logger.error(proofs) + raise e @staticmethod def _get_proofs_per_keyset(proofs: List[Proof]): @@ -1172,7 +1288,7 @@ async def invalidate( invalidated_proofs = proofs if invalidated_proofs: - logger.debug( + logger.trace( f"Invalidating {len(invalidated_proofs)} proofs worth" f" {sum_proofs(invalidated_proofs)} sat." ) @@ -1379,14 +1495,8 @@ async def restore_promises( secrets = [secrets[i] for i in matching_indices] rs = [rs[i] for i in matching_indices] # now we can construct the proofs with the secrets and rs - proofs = self._construct_proofs( + proofs = await self._construct_proofs( restored_promises, secrets, rs, derivation_paths ) logger.debug(f"Restored {len(restored_promises)} promises") - await self._store_proofs(proofs) - - # append proofs to proofs in memory so the balance updates - for proof in proofs: - if proof.secret not in [p.secret for p in self.proofs]: - self.proofs.append(proof) return proofs diff --git a/tests/test_wallet.py b/tests/test_wallet.py index f4ec78b6..b4e5cb07 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -172,6 +172,7 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet): @pytest.mark.asyncio async def test_split(wallet1: Wallet): await wallet1.mint(64) + assert wallet1.balance == 64 p1, p2 = await wallet1.split(wallet1.proofs, 20) assert wallet1.balance == 64 assert sum_proofs(p1) == 44 From 35f9a2b946ba7b8020d7bac79461037c4c68fa87 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Sep 2023 18:39:18 +0200 Subject: [PATCH 56/59] store dleq proofs in wallet db --- cashu/core/base.py | 7 +++ cashu/wallet/crud.py | 48 +++++++------- cashu/wallet/migrations.py | 7 +++ cashu/wallet/wallet.py | 126 +------------------------------------ 4 files changed, 40 insertions(+), 148 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index e2c716d8..d0b465e7 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -56,6 +56,13 @@ class Proof(BaseModel): time_reserved: Union[None, str] = "" derivation_path: Union[None, str] = "" # derivation path of the proof + @classmethod + def from_dict(cls, proof_dict: dict): + if proof_dict.get("dleq"): + proof_dict["dleq"] = DLEQWallet(**json.loads(proof_dict["dleq"])) + c = cls(**proof_dict) + return c + def to_dict(self, include_dleq=False): # dictionary without the fields that don't need to be send to Carol if not include_dleq: diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index e229ccf1..fcdfab1a 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -1,3 +1,4 @@ +import json import time from typing import Any, List, Optional, Tuple @@ -9,12 +10,12 @@ async def store_proof( proof: Proof, db: Database, conn: Optional[Connection] = None, -): +) -> None: await (conn or db).execute( """ INSERT INTO proofs - (id, amount, C, secret, time_created, derivation_path) - VALUES (?, ?, ?, ?, ?, ?) + (id, amount, C, secret, time_created, derivation_path, dleq) + VALUES (?, ?, ?, ?, ?, ?, ?) """, ( proof.id, @@ -23,6 +24,7 @@ async def store_proof( str(proof.secret), int(time.time()), proof.derivation_path, + json.dumps(proof.dleq.dict()) if proof.dleq else "", ), ) @@ -30,29 +32,29 @@ async def store_proof( async def get_proofs( db: Database, conn: Optional[Connection] = None, -): +) -> List[Proof]: rows = await (conn or db).fetchall(""" SELECT * from proofs """) - return [Proof(**dict(r)) for r in rows] + return [Proof.from_dict(dict(r)) for r in rows] async def get_reserved_proofs( db: Database, conn: Optional[Connection] = None, -): +) -> List[Proof]: rows = await (conn or db).fetchall(""" SELECT * from proofs WHERE reserved """) - return [Proof(**r) for r in rows] + return [Proof.from_dict(dict(r)) for r in rows] async def invalidate_proof( proof: Proof, db: Database, conn: Optional[Connection] = None, -): +) -> None: await (conn or db).execute( """ DELETE FROM proofs @@ -84,7 +86,7 @@ async def update_proof_reserved( send_id: str = "", db: Optional[Database] = None, conn: Optional[Connection] = None, -): +) -> None: clauses = [] values: List[Any] = [] clauses.append("reserved = ?") @@ -109,7 +111,7 @@ async def secret_used( secret: str, db: Database, conn: Optional[Connection] = None, -): +) -> bool: rows = await (conn or db).fetchone( """ SELECT * from proofs @@ -124,7 +126,7 @@ async def store_p2sh( p2sh: P2SHScript, db: Database, conn: Optional[Connection] = None, -): +) -> None: await (conn or db).execute( """ INSERT INTO p2sh @@ -144,7 +146,7 @@ async def get_unused_locks( address: str = "", db: Optional[Database] = None, conn: Optional[Connection] = None, -): +) -> List[P2SHScript]: clause: List[str] = [] args: List[str] = [] @@ -173,7 +175,7 @@ async def update_p2sh_used( used: bool, db: Optional[Database] = None, conn: Optional[Connection] = None, -): +) -> None: clauses = [] values = [] clauses.append("used = ?") @@ -190,7 +192,7 @@ async def store_keyset( mint_url: str = "", db: Optional[Database] = None, conn: Optional[Connection] = None, -): +) -> None: await (conn or db).execute( # type: ignore """ INSERT INTO keysets @@ -243,7 +245,7 @@ async def store_lightning_invoice( db: Database, invoice: Invoice, conn: Optional[Connection] = None, -): +) -> None: await (conn or db).execute( """ INSERT INTO invoices @@ -266,7 +268,7 @@ async def get_lightning_invoice( db: Database, hash: str = "", conn: Optional[Connection] = None, -): +) -> Invoice: clauses = [] values: List[Any] = [] if hash: @@ -291,7 +293,7 @@ async def get_lightning_invoices( db: Database, paid: Optional[bool] = None, conn: Optional[Connection] = None, -): +) -> List[Invoice]: clauses: List[Any] = [] values: List[Any] = [] @@ -319,7 +321,7 @@ async def update_lightning_invoice( paid: bool, time_paid: Optional[int] = None, conn: Optional[Connection] = None, -): +) -> None: clauses = [] values: List[Any] = [] clauses.append("paid = ?") @@ -344,7 +346,7 @@ async def bump_secret_derivation( by: int = 1, skip: bool = False, conn: Optional[Connection] = None, -): +) -> int: rows = await (conn or db).fetchone( "SELECT counter from keysets WHERE id = ?", (keyset_id,) ) @@ -374,7 +376,7 @@ async def set_secret_derivation( keyset_id: str, counter: int, conn: Optional[Connection] = None, -): +) -> None: await (conn or db).execute( "UPDATE keysets SET counter = ? WHERE id = ?", ( @@ -388,7 +390,7 @@ async def set_nostr_last_check_timestamp( db: Database, timestamp: int, conn: Optional[Connection] = None, -): +) -> None: await (conn or db).execute( "UPDATE nostr SET last = ? WHERE type = ?", (timestamp, "dm"), @@ -398,7 +400,7 @@ async def set_nostr_last_check_timestamp( async def get_nostr_last_check_timestamp( db: Database, conn: Optional[Connection] = None, -): +) -> Optional[int]: row = await (conn or db).fetchone( """ SELECT last from nostr WHERE type = ? @@ -432,7 +434,7 @@ async def store_seed_and_mnemonic( seed: str, mnemonic: str, conn: Optional[Connection] = None, -): +) -> None: await (conn or db).execute( """ INSERT INTO seed diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 061b31c5..94ce47fa 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -173,3 +173,10 @@ async def m009_privatekey_and_determinstic_key_derivation(db: Database): ); """) # await db.execute("INSERT INTO secret_derivation (counter) VALUES (0)") + + +async def m010_add_proofs_dleq(db: Database): + """ + Columns to store DLEQ proofs for proofs. + """ + await db.execute("ALTER TABLE proofs ADD COLUMN dleq TEXT") diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index de09009f..bfbe9efd 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -111,135 +111,11 @@ def __init__(self, url: str, db: Database): self.s = requests.Session() self.db = db - # async def generate_n_secrets( - # self, n: int = 1, skip_bump: bool = False - # ) -> Tuple[List[str], List[PrivateKey], List[str]]: - # return await self.generate_n_secrets(n, skip_bump) - - # async def _generate_secret(self, skip_bump: bool = False) -> str: - # return await self._generate_secret(skip_bump) - @async_set_requests async def _init_s(self): """Dummy function that can be called from outside to use LedgerAPI.s""" return - # # ---------- DLEQ PROOFS ---------- - - # def verify_proofs_dleq(self, proofs: List[Proof]): - # """Verifies DLEQ proofs in proofs.""" - # for proof in proofs: - # if not proof.dleq: - # logger.trace("No DLEQ proof in proof.") - # return - # logger.trace("Verifying DLEQ proof.") - # assert self.keys.public_keys - # if not b_dhke.carol_verify_dleq( - # secret_msg=proof.secret, - # C=PublicKey(bytes.fromhex(proof.C), raw=True), - # r=PrivateKey(bytes.fromhex(proof.dleq.r), raw=True), - # e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), - # s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), - # A=self.keys.public_keys[proof.amount], - # ): - # raise Exception("DLEQ proof invalid.") - # else: - # logger.debug("DLEQ proof valid.") - - # def _construct_proofs( - # self, - # promises: List[BlindedSignature], - # secrets: List[str], - # rs: List[PrivateKey], - # derivation_paths: List[str], - # ) -> List[Proof]: - # """Constructs proofs from promises, secrets, rs and derivation paths. - - # This method is called after the user has received blind signatures from - # the mint. The results are proofs that can be used as ecash. - - # Args: - # promises (List[BlindedSignature]): blind signatures from mint - # secrets (List[str]): secrets that were previously used to create blind messages (that turned into promises) - # rs (List[PrivateKey]): blinding factors that were previously used to create blind messages (that turned into promises) - # derivation_paths (List[str]): derivation paths that were used to generate secrets and blinding factors - - # Returns: - # List[Proof]: list of proofs that can be used as ecash - # """ - # logger.trace("Constructing proofs.") - # proofs: List[Proof] = [] - # for promise, secret, r, path in zip(promises, secrets, rs, derivation_paths): - # logger.trace(f"Creating proof with keyset {self.keyset_id} = {promise.id}") - # assert ( - # self.keyset_id == promise.id - # ), "our keyset id does not match promise id." - - # C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) - # C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount]) - # B_, r = b_dhke.step1_alice(secret, r) # recompute B_ for dleq proofs - - # proof = Proof( - # id=promise.id, - # amount=promise.amount, - # C=C.serialize().hex(), - # secret=secret, - # derivation_path=path, - # ) - - # # if the mint returned a dleq proof, we add it to the proof - # if promise.dleq: - # proof.dleq = DLEQWallet( - # e=promise.dleq.e, s=promise.dleq.s, r=r.serialize() - # ) - - # proofs.append(proof) - - # logger.trace( - # f"Created proof: {proof}, r: {r.serialize()} out of promise {promise}" - # ) - - # # DLEQ verify - # self.verify_proofs_dleq(proofs) - - # logger.trace(f"Constructed {len(proofs)} proofs.") - # return proofs - - # @staticmethod - # def _construct_outputs( - # amounts: List[int], secrets: List[str], rs: List[PrivateKey] = [] - # ) -> Tuple[List[BlindedMessage], List[PrivateKey]]: - # """Takes a list of amounts and secrets and returns outputs. - # Outputs are blinded messages `outputs` and blinding factors `rs` - - # Args: - # amounts (List[int]): list of amounts - # secrets (List[str]): list of secrets - # rs (List[PrivateKey], optional): list of blinding factors. If not given, `rs` are generated in step1_alice. Defaults to []. - - # Returns: - # List[BlindedMessage]: list of blinded messages that can be sent to the mint - # List[PrivateKey]: list of blinding factors that can be used to construct proofs after receiving blind signatures from the mint - - # Raises: - # AssertionError: if len(amounts) != len(secrets) - # """ - # assert len(amounts) == len( - # secrets - # ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}" - # outputs: List[BlindedMessage] = [] - - # rs_ = [None] * len(amounts) if not rs else rs - # rs_return: List[PrivateKey] = [] - # for secret, amount, r in zip(secrets, amounts, rs_): - # B_, r = b_dhke.step1_alice(secret, r or None) - # rs_return.append(r) - # output = BlindedMessage(amount=amount, B_=B_.serialize().hex()) - # outputs.append(output) - # logger.trace(f"Constructing output: {output}, r: {r.serialize()}") - - # return outputs, rs_return - @staticmethod def raise_on_error(resp: Response) -> None: """Raises an exception if the response from the mint contains an error. @@ -1047,7 +923,7 @@ def _construct_outputs( async def _store_proofs(self, proofs): try: - async with self.db.connect() as conn: + async with self.db.connect() as conn: # type: ignore for proof in proofs: await store_proof(proof, db=self.db, conn=conn) except Exception as e: From 659a0e4a138a3b823d54f73eaa3f95325fe1269c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Sep 2023 18:44:11 +0200 Subject: [PATCH 57/59] make mypy happy --- cashu/wallet/api/router.py | 6 +++--- cashu/wallet/cli/cli.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cashu/wallet/api/router.py b/cashu/wallet/api/router.py index d3e60936..1f02ef0f 100644 --- a/cashu/wallet/api/router.py +++ b/cashu/wallet/api/router.py @@ -325,7 +325,7 @@ async def pending( enumerate( groupby( sorted_proofs, - key=itemgetter("send_id"), + key=itemgetter("send_id"), # type: ignore ) ), offset, @@ -334,9 +334,9 @@ async def pending( grouped_proofs = list(value) token = await wallet.serialize_proofs(grouped_proofs) tokenObj = deserialize_token_from_string(token) - mint = [t.mint for t in tokenObj.token][0] + mint = [t.mint for t in tokenObj.token if t.mint][0] reserved_date = datetime.utcfromtimestamp( - int(grouped_proofs[0].time_reserved) + int(grouped_proofs[0].time_reserved) # type: ignore ).strftime("%Y-%m-%d %H:%M:%S") result.update( { diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index fb4d654f..bdbe5167 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -550,7 +550,7 @@ async def pending(ctx: Context, legacy, number: int, offset: int): enumerate( groupby( sorted_proofs, - key=itemgetter("send_id"), + key=itemgetter("send_id"), # type: ignore ) ), offset, From a89c2305d80a2a6046e371a179aa22b751c3e90a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Sep 2023 18:55:27 +0200 Subject: [PATCH 58/59] add minimal mint test --- tests/test_mint.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_mint.py b/tests/test_mint.py index ae9ed4a9..cf95caca 100644 --- a/tests/test_mint.py +++ b/tests/test_mint.py @@ -107,6 +107,12 @@ async def test_generate_promises(ledger: Ledger): promises[0].C_ == "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15" ) + assert promises[0].amount == 8 + + # DLEQ proof present + assert promises[0].dleq + assert promises[0].dleq.s + assert promises[0].dleq.e @pytest.mark.asyncio From b251c2422aac35ff8ccbffa34990140948a01e11 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:02:27 +0200 Subject: [PATCH 59/59] set proof only reserved if it properly serialized --- cashu/wallet/helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cashu/wallet/helpers.py b/cashu/wallet/helpers.py index be3950b4..418a6b75 100644 --- a/cashu/wallet/helpers.py +++ b/cashu/wallet/helpers.py @@ -213,7 +213,6 @@ async def send( "No proof with this amount found. Available amounts:" f" {set([p.amount for p in wallet.proofs])}" ) - await wallet.set_reserved(send_proofs, reserved=True) token = await wallet.serialize_proofs( send_proofs, @@ -221,7 +220,7 @@ async def send( include_dleq=include_dleq, ) print(token) - + await wallet.set_reserved(send_proofs, reserved=True) if legacy: print("") print("Old token format:")