Skip to content

Commit

Permalink
[Wallet/Mint] DLEQ proofs (#175)
Browse files Browse the repository at this point in the history
* produce dleq

* start working on verification

* wip dleq

* 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

* 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 <[email protected]>

* Update cashu/core/b_dhke.py

* Update tests/test_cli.py

* verify all constructed proofs

* dleq upon receive

* serialize without dleq

* all tests passing

* make format

* remove print

* remove debug

* option to send with dleq

* add tests

* fix test

* deterministic p in step2_dleq and fix mypy error for hash_to_curve

* test crypto/hash_e and crypto/step2_bob_dleq

* rename A to K in b_dhke.py and test_alice_verify_dleq

* rename tests

* make format

* store dleq in mint db (and readd balance view)

* remove `r` from dleq in tests

* add pending output

* make format

* works with pre-dleq mints

* fix comments

* make format

* fix some tests

* fix last test

* test serialize dleq fix

* flake

* flake

* keyset.id must be str

* fix test decorators

* start removing the duplicate fields from the dleq

* format

* remove print

* cleanup

* add type anotations to dleq functions

* remove unnecessary fields from BlindedSignature

* tests not working yet

* spelling mistakes

* spelling mistakes

* fix more spelling mistakes

* revert to normal

* add comments

* bdhke: generalize hash_e

* remove P2PKSecret changes

* revert tests for P2PKSecret

* revert tests

* revert test fully

* revert p2pksecret changes

* refactor proof invalidation

* store dleq proofs in wallet db

* make mypy happy

---------

Co-authored-by: moonsettler <[email protected]>
  • Loading branch information
callebtc and moonsettler authored Sep 23, 2023
1 parent a1802b2 commit 6282e0a
Show file tree
Hide file tree
Showing 19 changed files with 717 additions and 205 deletions.
64 changes: 54 additions & 10 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@
from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12
from .p2pk import P2SHScript


class DLEQ(BaseModel):
"""
Discrete Log Equality (DLEQ) Proof
"""

e: str
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


# ------- PROOFS -------


Expand All @@ -24,6 +44,8 @@ class Proof(BaseModel):
amount: int = 0
secret: str = "" # secret or message to be blinded and signed
C: str = "" # signature on secret, unblinded by wallet
dleq: Union[DLEQWallet, None] = None # DLEQ proof

p2pksigs: Union[List[str], None] = [] # P2PK signature
p2shscript: Union[P2SHScript, None] = None # P2SH spending condition
# whether this proof is reserved for sending, used for coin management in the wallet
Expand All @@ -34,7 +56,28 @@ class Proof(BaseModel):
time_reserved: Union[None, str] = ""
derivation_path: Union[None, str] = "" # derivation path of the proof

def to_dict(self):
@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:
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,
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)

Expand Down Expand Up @@ -69,9 +112,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: Optional[DLEQ] = None # DLEQ proof


class BlindedMessages(BaseModel):
Expand Down Expand Up @@ -296,7 +340,7 @@ class MintKeyset:
Contains the keyset from the mint's perspective.
"""

id: Union[str, None]
id: str
derivation_path: str
private_keys: Dict[int, PrivateKey]
public_keys: Union[Dict[int, PublicKey], None] = None
Expand All @@ -308,7 +352,7 @@ class MintKeyset:

def __init__(
self,
id=None,
id="",
valid_from=None,
valid_to=None,
first_seen=None,
Expand Down Expand Up @@ -411,8 +455,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=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
return return_dict
Expand All @@ -426,8 +470,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=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
return return_dict
Expand All @@ -454,14 +498,14 @@ def deserialize(cls, tokenv3_serialized: str) -> "TokenV3":
token = json.loads(base64.urlsafe_b64decode(token_base64))
return cls.parse_obj(token)

def serialize(self) -> str:
def serialize(self, include_dleq=False) -> str:
"""
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
"""
prefix = "cashuA"
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
86 changes: 82 additions & 4 deletions cashu/core/crypto/b_dhke.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,43 @@
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 = 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'
"""

import hashlib
from typing import Optional
from typing import Optional, Tuple

from secp256k1 import PrivateKey, PublicKey


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:
# will error if point does not lie on curve
point = PublicKey(b"\x02" + _hash, raw=True)
except Exception:
msg_to_hash = _hash
Expand All @@ -59,9 +80,11 @@ def step1_alice(
return B_, r


def step2_bob(B_: PublicKey, a: PrivateKey) -> PublicKey:
def step2_bob(B_: PublicKey, a: PrivateKey) -> Tuple[PublicKey, PrivateKey, PrivateKey]:
C_: PublicKey = B_.mult(a) # type: ignore
return C_
# produce dleq proof
e, s = step2_bob_dleq(B_, a)
return C_, e, s


def step3_alice(C_: PublicKey, r: PrivateKey, A: PublicKey) -> PublicKey:
Expand All @@ -74,6 +97,61 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool:
return C == Y.mult(a) # type: ignore


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


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)
else:
# normally, we generate a random p
p = PrivateKey()

R1 = p.pubkey # R1 = pG
assert R1
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
spk = PrivateKey(s, raw=True)
epk = PrivateKey(e, raw=True)
return epk, spk


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
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,
) -> 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
return alice_verify_dleq(B_, C_, e, s, A)


# Below is a test of a simple positive and negative case

# # Alice's keys
Expand Down
6 changes: 3 additions & 3 deletions cashu/core/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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}")
Expand All @@ -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()
Expand Down
13 changes: 9 additions & 4 deletions cashu/mint/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,28 @@ async def update_lightning_invoice(*args, **kwags):


async def store_promise(
*,
db: Database,
amount: int,
B_: str,
C_: str,
id: 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, id)
VALUES (?, ?, ?, ?)
(amount, B_b, C_b, e, s, id)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
amount,
str(B_),
str(C_),
B_,
C_,
e,
s,
id,
),
)
Expand Down
17 changes: 12 additions & 5 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from ..core import bolt11
from ..core.base import (
DLEQ,
BlindedMessage,
BlindedSignature,
Invoice,
Expand Down Expand Up @@ -117,8 +118,7 @@ async def load_keyset(self, derivation_path, autosave=True):
logger.trace(f"crud: stored new keyset {keyset.id}.")

# store the new keyset in the current keysets
if keyset.id:
self.keysets.keysets[keyset.id] = keyset
self.keysets.keysets[keyset.id] = keyset
logger.debug(f"Loaded keyset {keyset.id}.")
return keyset

Expand Down Expand Up @@ -188,17 +188,24 @@ async def _generate_promise(
keyset = keyset if keyset else self.keyset
logger.trace(f"Generating promise with keyset {keyset.id}.")
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)
logger.trace(f"crud: _generate_promise storing promise for {amount}")
await self.crud.store_promise(
amount=amount,
B_=B_.serialize().hex(),
C_=C_.serialize().hex(),
id=keyset.id,
e=e.serialize(),
s=s.serialize(),
db=self.db,
id=keyset.id,
)
logger.trace(f"crud: _generate_promise stored promise for {amount}")
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.serialize(), s=s.serialize()),
)

def _check_spendable(self, proof: Proof):
"""Checks whether the proof was already spent."""
Expand Down
12 changes: 12 additions & 0 deletions cashu/mint/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,15 @@ async def m007_proofs_and_promises_store_id(db: Database):
await db.execute(
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT"
)


async def m008_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"
)
Loading

0 comments on commit 6282e0a

Please sign in to comment.