Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Wallet/Mint] DLEQ proofs #175

Merged
merged 70 commits into from
Sep 23, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
347de24
produce dleq
callebtc Jan 28, 2023
562ffa5
start working on verification
callebtc Jan 28, 2023
f4b5970
merge
callebtc Mar 25, 2023
f73e0da
wip dleq
callebtc Mar 25, 2023
e0bd8bc
Use C_ instead of C in verify DLEQ! (#176)
moonsettler Apr 26, 2023
65be72b
Fix: invalid public key (#182)
moonsettler Apr 28, 2023
9002824
Update cashu/core/b_dhke.py
callebtc Apr 28, 2023
a53bb0a
Update tests/test_cli.py
callebtc Apr 28, 2023
6e1171b
merge
callebtc Apr 29, 2023
aa535a8
verify all constructed proofs
callebtc Apr 29, 2023
4c8622d
dleq upon receive
callebtc Apr 29, 2023
d9cc699
serialize without dleq
callebtc Apr 29, 2023
64c5070
all tests passing
callebtc Apr 29, 2023
80d40db
make format
callebtc Apr 29, 2023
6d15300
remove print
callebtc Apr 29, 2023
5e5e958
remove debug
callebtc Apr 29, 2023
4348d4f
option to send with dleq
callebtc Apr 29, 2023
e16010d
add tests
callebtc Apr 30, 2023
9a025fe
fix test
callebtc Apr 30, 2023
f676c70
deterministic p in step2_dleq and fix mypy error for hash_to_curve
callebtc Apr 30, 2023
d907d6c
test crypto/hash_e and crypto/step2_bob_dleq
callebtc Apr 30, 2023
bc5f478
rename A to K in b_dhke.py and test_alice_verify_dleq
callebtc Apr 30, 2023
8f3d1c2
rename tests
callebtc Apr 30, 2023
8459105
make format
callebtc Apr 30, 2023
55e406f
store dleq in mint db (and readd balance view)
callebtc Apr 30, 2023
279e8f3
remove `r` from dleq in tests
callebtc Apr 30, 2023
1050c49
add pending output
callebtc Apr 30, 2023
ef99106
merge main
callebtc May 1, 2023
12be7b1
make format
callebtc May 1, 2023
4af5008
Merge branch 'main' into dleq
callebtc May 1, 2023
3fb099c
works with pre-dleq mints
callebtc May 1, 2023
13873b1
fix comments
callebtc May 1, 2023
befdd12
Merge branch 'main' into dleq
callebtc Jul 25, 2023
0495bef
make format
callebtc Jul 25, 2023
8321ca3
fix some tests
callebtc Jul 25, 2023
ab8a1f0
fix last test
callebtc Jul 25, 2023
205cab3
Merge branch 'main' into dleq
callebtc Jul 28, 2023
00fd85f
Merge branch 'main' into dleq
callebtc Jul 28, 2023
5fccea3
test serialize dleq fix
callebtc Jul 28, 2023
4020273
flake
callebtc Jul 28, 2023
43b04b6
flake
callebtc Jul 28, 2023
8b337d9
keyset.id must be str
callebtc Jul 28, 2023
7ab9539
fix test decorators
callebtc Jul 28, 2023
22c5eef
start removing the duplicate fields from the dleq
callebtc Jul 29, 2023
4960635
Merge branch 'main' into dleq
callebtc Jul 29, 2023
068224e
format
callebtc Jul 30, 2023
e4c245b
remove print
callebtc Jul 30, 2023
749d5df
cleanup
callebtc Jul 30, 2023
61b7ff4
Merge branch 'main' into dleq
callebtc Sep 12, 2023
5b5b8fa
add type anotations to dleq functions
callebtc Sep 19, 2023
809c52f
Merge branch 'dleq' of github.com:callebtc/cashu into dleq
callebtc Sep 19, 2023
1a359a2
remove unnecessary fields from BlindedSignature
callebtc Sep 19, 2023
beca698
tests not working yet
callebtc Sep 19, 2023
8aaf0f8
spelling mistakes
callebtc Sep 20, 2023
57f3967
spelling mistakes
callebtc Sep 20, 2023
877c164
fix more spelling mistakes
callebtc Sep 20, 2023
e471c74
revert to normal
callebtc Sep 20, 2023
84d89ca
add comments
callebtc Sep 20, 2023
6451950
bdhke: generalize hash_e
callebtc Sep 20, 2023
9ac144a
remove P2PKSecret changes
callebtc Sep 20, 2023
2fee249
revert tests for P2PKSecret
callebtc Sep 20, 2023
23c2bbc
revert tests
callebtc Sep 20, 2023
e1a7c88
revert test fully
callebtc Sep 20, 2023
42e447b
revert p2pksecret changes
callebtc Sep 20, 2023
d186529
Merge branch 'main' into dleq
callebtc Sep 21, 2023
023fc81
refactor proof invalidation
callebtc Sep 23, 2023
35f9a2b
store dleq proofs in wallet db
callebtc Sep 23, 2023
659a0e4
make mypy happy
callebtc Sep 23, 2023
a89c230
add minimal mint test
callebtc Sep 23, 2023
b251c24
set proof only reserved if it properly serialized
callebtc Sep 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
70 changes: 67 additions & 3 deletions cashu/core/b_dhke.py
Expand Up @@ -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 = 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
Expand Down Expand Up @@ -61,9 +81,12 @@ def step1_alice(
return B_, r


def step2_bob(B_: PublicKey, a: PrivateKey) -> PublicKey:
C_: PublicKey = B_.mult(a)
return C_
def step2_bob(B_: PublicKey, a: PrivateKey):
C_ = B_.mult(a)

# produce dleq proof
e, s = step2_bob_dleq(B_, a)
return C_, e, s
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new return for step2_bob



def step3_alice(C_: PublicKey, r: PrivateKey, A: PublicKey) -> PublicKey:
Expand All @@ -76,6 +99,47 @@ 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()
_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):
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: bytes, s: bytes, A: 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)
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")
callebtc marked this conversation as resolved.
Show resolved Hide resolved
return e == hash_e(R1, R2, A, ck)


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

# # Alice's keys
Expand Down
24 changes: 24 additions & 0 deletions cashu/core/base.py
Expand Up @@ -19,6 +19,18 @@ class P2SHScript(BaseModel):
address: Union[str, None] = None


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

e: str
s: str
B_: str
C_: str
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new DLEQ model

r: str = ""


class Proof(BaseModel):
"""
Value token
Expand All @@ -31,6 +43,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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New entry in Proof

reserved: Union[
None, bool
] = False # whether this proof is reserved for sending, used for coin management in the wallet
Expand All @@ -41,6 +54,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)

Expand Down Expand Up @@ -77,6 +100,7 @@ class BlindedSignature(BaseModel):
id: Union[str, None] = None
amount: int
C_: str # Hex-encoded signature
dleq: DLEQ


class BlindedMessages(BaseModel):
Expand Down
10 changes: 8 additions & 2 deletions cashu/mint/ledger.py
Expand Up @@ -13,6 +13,7 @@
MintKeyset,
MintKeysets,
Proof,
DLEQ,
)
from cashu.core.db import Database
from cashu.core.helpers import fee_reserve, sum_proofs
Expand Down Expand Up @@ -127,11 +128,16 @@ async def _generate_promise(
"""
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.hex(), B_=B_.serialize().hex(), C_=C_.serialize().hex()),
)

def _check_spendable(self, proof: Proof):
"""Checks whether the proof was already spent."""
Expand Down
93 changes: 64 additions & 29 deletions cashu/wallet/wallet.py
Expand Up @@ -109,19 +109,25 @@ async def _init_s(self):
return

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],
callebtc marked this conversation as resolved.
Show resolved Hide resolved
):
"""Returns proofs of promise from promises. Wants secrets and blinding factors rs."""
proofs: List[Proof] = []
for promise, secret, r in zip(promises, secrets, rs):
for promise, secret, r, output in zip(promises, secrets, rs, outputs):
callebtc marked this conversation as resolved.
Show resolved Hide resolved
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount])
proof = Proof(
id=self.keyset_id,
amount=promise.amount,
C=C.serialize().hex(),
secret=secret,
dleq=promise.dleq,
)
proof.dleq.C_ = promise.C_
proofs.append(proof)
return proofs

Expand Down Expand Up @@ -322,7 +328,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)
callebtc marked this conversation as resolved.
Show resolved Hide resolved

@async_set_requests
async def split(self, proofs, amount, scnd_secret: Optional[str] = None):
Expand Down Expand Up @@ -381,10 +387,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) :],
callebtc marked this conversation as resolved.
Show resolved Hide resolved
)

return frst_proofs, scnd_proofs
Expand Down Expand Up @@ -493,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)
callebtc marked this conversation as resolved.
Show resolved Hide resolved
if proofs == []:
raise Exception("received no proofs.")
await self._store_proofs(proofs)
Expand Down Expand Up @@ -557,6 +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
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))
self.proofs += frst_proofs + scnd_proofs
Expand Down Expand Up @@ -637,6 +655,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
Expand Down Expand Up @@ -666,6 +685,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

callebtc marked this conversation as resolved.
Show resolved Hide resolved
async def _serialize_token_base64(self, token: TokenV2):
"""
Takes a TokenV2 and serializes it in urlsafe_base64.
Expand All @@ -692,30 +735,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())
Expand Down Expand Up @@ -807,6 +826,22 @@ 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
Expand Down
1 change: 1 addition & 0 deletions tests/test_cli.py
Expand Up @@ -116,6 +116,7 @@ def test_receive_tokenv1(mint):
print(result.output)


@pytest.mark.skip
callebtc marked this conversation as resolved.
Show resolved Hide resolved
@pytest.mark.asyncio()
def test_nostr_send(mint):
runner = CliRunner()
Expand Down