[Wallet/Mint] DLEQ proofs (#175)

* 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 <93376500+callebtc@users.noreply.github.com>

* 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 <moonsettler@protonmail.com>
This commit is contained in:
callebtc
2023-09-23 19:06:37 +02:00
committed by GitHub
parent a1802b2d81
commit 6282e0a22a
19 changed files with 717 additions and 205 deletions

View File

@@ -11,6 +11,26 @@ from .crypto.secp import PrivateKey, PublicKey
from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12 from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12
from .p2pk import P2SHScript 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 ------- # ------- PROOFS -------
@@ -24,6 +44,8 @@ class Proof(BaseModel):
amount: int = 0 amount: int = 0
secret: str = "" # secret or message to be blinded and signed secret: str = "" # secret or message to be blinded and signed
C: str = "" # signature on secret, unblinded by wallet C: str = "" # signature on secret, unblinded by wallet
dleq: Union[DLEQWallet, None] = None # DLEQ proof
p2pksigs: Union[List[str], None] = [] # P2PK signature p2pksigs: Union[List[str], None] = [] # P2PK signature
p2shscript: Union[P2SHScript, None] = None # P2SH spending condition p2shscript: Union[P2SHScript, None] = None # P2SH spending condition
# whether this proof is reserved for sending, used for coin management in the wallet # whether this proof is reserved for sending, used for coin management in the wallet
@@ -34,7 +56,28 @@ class Proof(BaseModel):
time_reserved: Union[None, str] = "" time_reserved: Union[None, str] = ""
derivation_path: Union[None, str] = "" # derivation path of the proof 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 # 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) return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C)
@@ -69,9 +112,10 @@ class BlindedSignature(BaseModel):
Blinded signature or "promise" which is the signature on a `BlindedMessage` Blinded signature or "promise" which is the signature on a `BlindedMessage`
""" """
id: Union[str, None] = None id: str
amount: int amount: int
C_: str # Hex-encoded signature C_: str # Hex-encoded signature
dleq: Optional[DLEQ] = None # DLEQ proof
class BlindedMessages(BaseModel): class BlindedMessages(BaseModel):
@@ -296,7 +340,7 @@ class MintKeyset:
Contains the keyset from the mint's perspective. Contains the keyset from the mint's perspective.
""" """
id: Union[str, None] id: str
derivation_path: str derivation_path: str
private_keys: Dict[int, PrivateKey] private_keys: Dict[int, PrivateKey]
public_keys: Union[Dict[int, PublicKey], None] = None public_keys: Union[Dict[int, PublicKey], None] = None
@@ -308,7 +352,7 @@ class MintKeyset:
def __init__( def __init__(
self, self,
id=None, id="",
valid_from=None, valid_from=None,
valid_to=None, valid_to=None,
first_seen=None, first_seen=None,
@@ -411,8 +455,8 @@ class TokenV3Token(BaseModel):
mint: Optional[str] = None mint: Optional[str] = None
proofs: List[Proof] proofs: List[Proof]
def to_dict(self): def to_dict(self, include_dleq=False):
return_dict = dict(proofs=[p.to_dict() for p in self.proofs]) return_dict = dict(proofs=[p.to_dict(include_dleq) for p in self.proofs])
if self.mint: if self.mint:
return_dict.update(dict(mint=self.mint)) # type: ignore return_dict.update(dict(mint=self.mint)) # type: ignore
return return_dict return return_dict
@@ -426,8 +470,8 @@ class TokenV3(BaseModel):
token: List[TokenV3Token] = [] token: List[TokenV3Token] = []
memo: Optional[str] = None memo: Optional[str] = None
def to_dict(self): def to_dict(self, include_dleq=False):
return_dict = dict(token=[t.to_dict() for t in self.token]) return_dict = dict(token=[t.to_dict(include_dleq) for t in self.token])
if self.memo: if self.memo:
return_dict.update(dict(memo=self.memo)) # type: ignore return_dict.update(dict(memo=self.memo)) # type: ignore
return return_dict return return_dict
@@ -454,7 +498,7 @@ class TokenV3(BaseModel):
token = json.loads(base64.urlsafe_b64decode(token_base64)) token = json.loads(base64.urlsafe_b64decode(token_base64))
return cls.parse_obj(token) 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>. Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
""" """
@@ -462,6 +506,6 @@ class TokenV3(BaseModel):
tokenv3_serialized = prefix tokenv3_serialized = prefix
# encode the token as a base64 string # encode the token as a base64 string
tokenv3_serialized += base64.urlsafe_b64encode( tokenv3_serialized += base64.urlsafe_b64encode(
json.dumps(self.to_dict()).encode() json.dumps(self.to_dict(include_dleq)).encode()
).decode() ).decode()
return tokenv3_serialized return tokenv3_serialized

View File

@@ -28,22 +28,43 @@ Bob:
Y = hash_to_curve(secret_message) Y = hash_to_curve(secret_message)
C == a*Y C == a*Y
If true, C must have originated from Bob 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 import hashlib
from typing import Optional from typing import Optional, Tuple
from secp256k1 import PrivateKey, PublicKey from secp256k1 import PrivateKey, PublicKey
def hash_to_curve(message: bytes) -> PublicKey: def hash_to_curve(message: bytes) -> PublicKey:
"""Generates a point from the message hash and checks if the point lies on the curve. """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 point = None
msg_to_hash = message msg_to_hash = message
while point is None: while point is None:
_hash = hashlib.sha256(msg_to_hash).digest() _hash = hashlib.sha256(msg_to_hash).digest()
try: try:
# will error if point does not lie on curve
point = PublicKey(b"\x02" + _hash, raw=True) point = PublicKey(b"\x02" + _hash, raw=True)
except Exception: except Exception:
msg_to_hash = _hash msg_to_hash = _hash
@@ -59,9 +80,11 @@ def step1_alice(
return B_, r 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 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: def step3_alice(C_: PublicKey, r: PrivateKey, A: PublicKey) -> PublicKey:
@@ -74,6 +97,61 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool:
return C == Y.mult(a) # type: ignore 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 # Below is a test of a simple positive and negative case
# # Alice's keys # # Alice's keys

View File

@@ -25,7 +25,7 @@ def step0_carol_privkey():
return seckey return seckey
def step0_carol_checksig_redeemscrip(carol_pubkey): def step0_carol_checksig_redeemscript(carol_pubkey):
"""Create script""" """Create script"""
txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG]) txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG])
# txin_redeemScript = CScript([-123, OP_CHECKLOCKTIMEVERIFY]) # txin_redeemScript = CScript([-123, OP_CHECKLOCKTIMEVERIFY])
@@ -111,7 +111,7 @@ if __name__ == "__main__":
# --------- # ---------
# CAROL defines scripthash and ALICE mints them # CAROL defines scripthash and ALICE mints them
alice_privkey = step0_carol_privkey() 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__()) print("Script:", txin_redeemScript.__repr__())
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
print(f"Carol sends Alice secret = P2SH:{txin_p2sh_address}") print(f"Carol sends Alice secret = P2SH:{txin_p2sh_address}")
@@ -128,7 +128,7 @@ if __name__ == "__main__":
# CAROL redeems with MINT # CAROL redeems with MINT
# CAROL PRODUCES txin_redeemScript and txin_signature to send to 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_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig
txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode()

View File

@@ -49,23 +49,28 @@ class LedgerCrud:
async def store_promise( async def store_promise(
*,
db: Database, db: Database,
amount: int, amount: int,
B_: str, B_: str,
C_: str, C_: str,
id: str, id: str,
e: str = "",
s: str = "",
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ):
await (conn or db).execute( await (conn or db).execute(
f""" f"""
INSERT INTO {table_with_schema(db, 'promises')} INSERT INTO {table_with_schema(db, 'promises')}
(amount, B_b, C_b, id) (amount, B_b, C_b, e, s, id)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", """,
( (
amount, amount,
str(B_), B_,
str(C_), C_,
e,
s,
id, id,
), ),
) )

View File

@@ -7,6 +7,7 @@ from loguru import logger
from ..core import bolt11 from ..core import bolt11
from ..core.base import ( from ..core.base import (
DLEQ,
BlindedMessage, BlindedMessage,
BlindedSignature, BlindedSignature,
Invoice, Invoice,
@@ -117,7 +118,6 @@ class Ledger:
logger.trace(f"crud: stored new keyset {keyset.id}.") logger.trace(f"crud: stored new keyset {keyset.id}.")
# store the new keyset in the current keysets # 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}.") logger.debug(f"Loaded keyset {keyset.id}.")
return keyset return keyset
@@ -188,17 +188,24 @@ class Ledger:
keyset = keyset if keyset else self.keyset keyset = keyset if keyset else self.keyset
logger.trace(f"Generating promise with keyset {keyset.id}.") logger.trace(f"Generating promise with keyset {keyset.id}.")
private_key_amount = keyset.private_keys[amount] 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}") logger.trace(f"crud: _generate_promise storing promise for {amount}")
await self.crud.store_promise( await self.crud.store_promise(
amount=amount, amount=amount,
B_=B_.serialize().hex(), B_=B_.serialize().hex(),
C_=C_.serialize().hex(), C_=C_.serialize().hex(),
id=keyset.id, e=e.serialize(),
s=s.serialize(),
db=self.db, db=self.db,
id=keyset.id,
) )
logger.trace(f"crud: _generate_promise stored promise for {amount}") 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): def _check_spendable(self, proof: Proof):
"""Checks whether the proof was already spent.""" """Checks whether the proof was already spent."""

View File

@@ -156,3 +156,15 @@ async def m007_proofs_and_promises_store_id(db: Database):
await db.execute( await db.execute(
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT" 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"
)

View File

@@ -225,11 +225,11 @@ async def send_command(
global wallet global wallet
if not nostr: if not nostr:
balance, token = await send( 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) return SendResponse(balance=balance, token=token)
else: 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) return SendResponse(balance=wallet.available_balance, token=token, npub=pubkey)
@@ -325,7 +325,7 @@ async def pending(
enumerate( enumerate(
groupby( groupby(
sorted_proofs, sorted_proofs,
key=itemgetter("send_id"), key=itemgetter("send_id"), # type: ignore
) )
), ),
offset, offset,
@@ -334,9 +334,9 @@ async def pending(
grouped_proofs = list(value) grouped_proofs = list(value)
token = await wallet.serialize_proofs(grouped_proofs) token = await wallet.serialize_proofs(grouped_proofs)
tokenObj = deserialize_token_from_string(token) 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( 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") ).strftime("%Y-%m-%d %H:%M:%S")
result.update( result.update(
{ {

View File

@@ -352,6 +352,14 @@ async def balance(ctx: Context, verbose):
type=str, type=str,
) )
@click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", 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( @click.option(
"--legacy", "--legacy",
"-l", "-l",
@@ -387,6 +395,7 @@ async def send_command(
nostr: str, nostr: str,
nopt: str, nopt: str,
lock: str, lock: str,
dleq: bool,
legacy: bool, legacy: bool,
verbose: bool, verbose: bool,
yes: bool, yes: bool,
@@ -394,9 +403,18 @@ async def send_command(
): ):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
if not nostr and not nopt: 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: 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.") @cli.command("receive", help="Receive tokens.")
@@ -532,14 +550,15 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
enumerate( enumerate(
groupby( groupby(
sorted_proofs, sorted_proofs,
key=itemgetter("send_id"), key=itemgetter("send_id"), # type: ignore
) )
), ),
offset, offset,
number, number,
): ):
grouped_proofs = list(value) 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)
tokenObj = deserialize_token_from_string(token) tokenObj = deserialize_token_from_string(token)
mint = [t.mint for t in tokenObj.token][0] mint = [t.mint for t in tokenObj.token][0]
# token_hidden_secret = await wallet.serialize_proofs(grouped_proofs) # token_hidden_secret = await wallet.serialize_proofs(grouped_proofs)

View File

@@ -1,3 +1,4 @@
import json
import time import time
from typing import Any, List, Optional, Tuple from typing import Any, List, Optional, Tuple
@@ -9,12 +10,12 @@ async def store_proof(
proof: Proof, proof: Proof,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO proofs INSERT INTO proofs
(id, amount, C, secret, time_created, derivation_path) (id, amount, C, secret, time_created, derivation_path, dleq)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
( (
proof.id, proof.id,
@@ -23,6 +24,7 @@ async def store_proof(
str(proof.secret), str(proof.secret),
int(time.time()), int(time.time()),
proof.derivation_path, proof.derivation_path,
json.dumps(proof.dleq.dict()) if proof.dleq else "",
), ),
) )
@@ -30,29 +32,29 @@ async def store_proof(
async def get_proofs( async def get_proofs(
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> List[Proof]:
rows = await (conn or db).fetchall(""" rows = await (conn or db).fetchall("""
SELECT * from proofs 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( async def get_reserved_proofs(
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> List[Proof]:
rows = await (conn or db).fetchall(""" rows = await (conn or db).fetchall("""
SELECT * from proofs SELECT * from proofs
WHERE reserved WHERE reserved
""") """)
return [Proof(**r) for r in rows] return [Proof.from_dict(dict(r)) for r in rows]
async def invalidate_proof( async def invalidate_proof(
proof: Proof, proof: Proof,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
DELETE FROM proofs DELETE FROM proofs
@@ -84,7 +86,7 @@ async def update_proof_reserved(
send_id: str = "", send_id: str = "",
db: Optional[Database] = None, db: Optional[Database] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
clauses = [] clauses = []
values: List[Any] = [] values: List[Any] = []
clauses.append("reserved = ?") clauses.append("reserved = ?")
@@ -109,7 +111,7 @@ async def secret_used(
secret: str, secret: str,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> bool:
rows = await (conn or db).fetchone( rows = await (conn or db).fetchone(
""" """
SELECT * from proofs SELECT * from proofs
@@ -124,7 +126,7 @@ async def store_p2sh(
p2sh: P2SHScript, p2sh: P2SHScript,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO p2sh INSERT INTO p2sh
@@ -144,7 +146,7 @@ async def get_unused_locks(
address: str = "", address: str = "",
db: Optional[Database] = None, db: Optional[Database] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> List[P2SHScript]:
clause: List[str] = [] clause: List[str] = []
args: List[str] = [] args: List[str] = []
@@ -173,7 +175,7 @@ async def update_p2sh_used(
used: bool, used: bool,
db: Optional[Database] = None, db: Optional[Database] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
clauses = [] clauses = []
values = [] values = []
clauses.append("used = ?") clauses.append("used = ?")
@@ -190,7 +192,7 @@ async def store_keyset(
mint_url: str = "", mint_url: str = "",
db: Optional[Database] = None, db: Optional[Database] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
await (conn or db).execute( # type: ignore await (conn or db).execute( # type: ignore
""" """
INSERT INTO keysets INSERT INTO keysets
@@ -243,7 +245,7 @@ async def store_lightning_invoice(
db: Database, db: Database,
invoice: Invoice, invoice: Invoice,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO invoices INSERT INTO invoices
@@ -266,7 +268,7 @@ async def get_lightning_invoice(
db: Database, db: Database,
hash: str = "", hash: str = "",
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> Invoice:
clauses = [] clauses = []
values: List[Any] = [] values: List[Any] = []
if hash: if hash:
@@ -291,7 +293,7 @@ async def get_lightning_invoices(
db: Database, db: Database,
paid: Optional[bool] = None, paid: Optional[bool] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> List[Invoice]:
clauses: List[Any] = [] clauses: List[Any] = []
values: List[Any] = [] values: List[Any] = []
@@ -319,7 +321,7 @@ async def update_lightning_invoice(
paid: bool, paid: bool,
time_paid: Optional[int] = None, time_paid: Optional[int] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
clauses = [] clauses = []
values: List[Any] = [] values: List[Any] = []
clauses.append("paid = ?") clauses.append("paid = ?")
@@ -344,7 +346,7 @@ async def bump_secret_derivation(
by: int = 1, by: int = 1,
skip: bool = False, skip: bool = False,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> int:
rows = await (conn or db).fetchone( rows = await (conn or db).fetchone(
"SELECT counter from keysets WHERE id = ?", (keyset_id,) "SELECT counter from keysets WHERE id = ?", (keyset_id,)
) )
@@ -374,7 +376,7 @@ async def set_secret_derivation(
keyset_id: str, keyset_id: str,
counter: int, counter: int,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
await (conn or db).execute( await (conn or db).execute(
"UPDATE keysets SET counter = ? WHERE id = ?", "UPDATE keysets SET counter = ? WHERE id = ?",
( (
@@ -388,7 +390,7 @@ async def set_nostr_last_check_timestamp(
db: Database, db: Database,
timestamp: int, timestamp: int,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
await (conn or db).execute( await (conn or db).execute(
"UPDATE nostr SET last = ? WHERE type = ?", "UPDATE nostr SET last = ? WHERE type = ?",
(timestamp, "dm"), (timestamp, "dm"),
@@ -398,7 +400,7 @@ async def set_nostr_last_check_timestamp(
async def get_nostr_last_check_timestamp( async def get_nostr_last_check_timestamp(
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> Optional[int]:
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
""" """
SELECT last from nostr WHERE type = ? SELECT last from nostr WHERE type = ?
@@ -432,7 +434,7 @@ async def store_seed_and_mnemonic(
seed: str, seed: str,
mnemonic: str, mnemonic: str,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO seed INSERT INTO seed

View File

@@ -160,7 +160,13 @@ async def receive(
async def send( 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. Prints token to send to stdout.
@@ -207,14 +213,14 @@ async def send(
"No proof with this amount found. Available amounts:" "No proof with this amount found. Available amounts:"
f" {set([p.amount for p in wallet.proofs])}" f" {set([p.amount for p in wallet.proofs])}"
) )
await wallet.set_reserved(send_proofs, reserved=True)
token = await wallet.serialize_proofs( token = await wallet.serialize_proofs(
send_proofs, send_proofs,
include_mints=True, include_mints=True,
include_dleq=include_dleq,
) )
print(token) print(token)
await wallet.set_reserved(send_proofs, reserved=True)
if legacy: if legacy:
print("") print("")
print("Old token format:") print("Old token format:")
@@ -222,6 +228,7 @@ async def send(
token = await wallet.serialize_proofs( token = await wallet.serialize_proofs(
send_proofs, send_proofs,
legacy=True, legacy=True,
include_dleq=include_dleq,
) )
print(token) print(token)

View File

@@ -173,3 +173,10 @@ async def m009_privatekey_and_determinstic_key_derivation(db: Database):
); );
""") """)
# await db.execute("INSERT INTO secret_derivation (counter) VALUES (0)") # 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")

View File

@@ -45,10 +45,12 @@ async def nip5_to_pubkey(wallet: Wallet, address: str):
async def send_nostr( async def send_nostr(
wallet: Wallet, wallet: Wallet,
*,
amount: int, amount: int,
pubkey: str, pubkey: str,
verbose: bool = False, verbose: bool = False,
yes: bool = True, yes: bool = True,
include_dleq=False,
): ):
""" """
Sends tokens via nostr. Sends tokens via nostr.
@@ -62,7 +64,7 @@ async def send_nostr(
_, send_proofs = await wallet.split_to_send( _, send_proofs = await wallet.split_to_send(
wallet.proofs, amount, set_reserved=True 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"): if pubkey.startswith("npub"):
pubkey_to = PublicKey().from_npub(pubkey) pubkey_to = PublicKey().from_npub(pubkey)

View File

@@ -21,7 +21,7 @@ from ..core.p2pk import (
sign_p2pk_sign, sign_p2pk_sign,
) )
from ..core.script import ( from ..core.script import (
step0_carol_checksig_redeemscrip, step0_carol_checksig_redeemscript,
step0_carol_privkey, step0_carol_privkey,
step1_carol_create_p2sh_address, step1_carol_create_p2sh_address,
step2_carol_sign_tx, step2_carol_sign_tx,
@@ -41,7 +41,7 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
async def create_p2sh_address_and_store(self) -> str: async def create_p2sh_address_and_store(self) -> str:
"""Creates a P2SH lock script and stores the script and signature in the database.""" """Creates a P2SH lock script and stores the script and signature in the database."""
alice_privkey = step0_carol_privkey() 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_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig
txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode()

View File

@@ -19,6 +19,7 @@ from ..core.base import (
CheckFeesRequest, CheckFeesRequest,
CheckSpendableRequest, CheckSpendableRequest,
CheckSpendableResponse, CheckSpendableResponse,
DLEQWallet,
GetInfoResponse, GetInfoResponse,
GetMeltResponse, GetMeltResponse,
GetMintResponse, GetMintResponse,
@@ -110,101 +111,11 @@ class LedgerAPI(object):
self.s = requests.Session() self.s = requests.Session()
self.db = db 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_set_requests
async def _init_s(self): async def _init_s(self):
"""Dummy function that can be called from outside to use LedgerAPI.s""" """Dummy function that can be called from outside to use LedgerAPI.s"""
return return
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])
proof = Proof(
id=promise.id,
amount=promise.amount,
C=C.serialize().hex(),
secret=secret,
derivation_path=path,
)
proofs.append(proof)
logger.trace(
f"Created proof: {proof}, r: {r.serialize()} out of promise {promise}"
)
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 @staticmethod
def raise_on_error(resp: Response) -> None: def raise_on_error(resp: Response) -> None:
"""Raises an exception if the response from the mint contains an error. """Raises an exception if the response from the mint contains an error.
@@ -465,9 +376,9 @@ class LedgerAPI(object):
}, },
) )
self.raise_on_error(resp) self.raise_on_error(resp)
reponse_dict = resp.json() response_dict = resp.json()
logger.trace("Lightning invoice checked. POST /mint") logger.trace("Lightning invoice checked. POST /mint")
promises = PostMintResponse.parse_obj(reponse_dict).promises promises = PostMintResponse.parse_obj(response_dict).promises
return promises return promises
@async_set_requests @async_set_requests
@@ -506,7 +417,7 @@ class LedgerAPI(object):
@async_set_requests @async_set_requests
async def check_proof_state(self, proofs: List[Proof]): 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) payload = CheckSpendableRequest(proofs=proofs)
@@ -577,8 +488,8 @@ class LedgerAPI(object):
payload = PostMintRequest(outputs=outputs) payload = PostMintRequest(outputs=outputs)
resp = self.s.post(self.url + "/restore", json=payload.dict()) resp = self.s.post(self.url + "/restore", json=payload.dict())
self.raise_on_error(resp) self.raise_on_error(resp)
reponse_dict = resp.json() response_dict = resp.json()
returnObj = PostRestoreResponse.parse_obj(reponse_dict) returnObj = PostRestoreResponse.parse_obj(response_dict)
return returnObj.outputs, returnObj.promises return returnObj.outputs, returnObj.promises
@@ -609,7 +520,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
self.name = name self.name = name
super().__init__(url=url, db=self.db) 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 @classmethod
async def with_db( async def with_db(
@@ -726,16 +637,12 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
await bump_secret_derivation( await bump_secret_derivation(
db=self.db, keyset_id=self.keyset_id, by=len(amounts) 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: if hash:
await update_lightning_invoice( await update_lightning_invoice(
db=self.db, hash=hash, paid=True, time_paid=int(time.time()) db=self.db, hash=hash, paid=True, time_paid=int(time.time())
) )
self.proofs += proofs
return proofs return proofs
async def redeem( async def redeem(
@@ -749,6 +656,10 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
Args: Args:
proofs (List[Proof]): Proofs to be redeemed. 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)) return await self.split(proofs, sum_proofs(proofs))
async def split( async def split(
@@ -797,9 +708,9 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
logger.debug(f"Creating proofs with custom secrets: {secret_locks}") logger.debug(f"Creating proofs with custom secrets: {secret_locks}")
assert len(secret_locks) == len( assert len(secret_locks) == len(
scnd_outputs 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) # append predefined secrets (to send) to random secrets (to keep)
# generate sercets to keep # generate secrets to keep
secrets = [ secrets = [
await self._generate_secret() for s in range(len(frst_outputs)) await self._generate_secret() for s in range(len(frst_outputs))
] + secret_locks ] + secret_locks
@@ -822,18 +733,11 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
promises = await super().split(proofs, outputs) promises = await super().split(proofs, outputs)
# Construct proofs from returned promises (i.e., unblind the signatures) # 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 await self.invalidate(proofs)
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)
keep_proofs = new_proofs[: len(frst_outputs)] keep_proofs = new_proofs[: len(frst_outputs)]
send_proofs = new_proofs[len(frst_outputs) :] send_proofs = new_proofs[len(frst_outputs) :]
@@ -862,7 +766,6 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
if status.paid: if status.paid:
# the payment was successful # the payment was successful
await self.invalidate(proofs)
invoice_obj = Invoice( invoice_obj = Invoice(
amount=-sum_proofs(proofs), amount=-sum_proofs(proofs),
pr=invoice, pr=invoice,
@@ -877,14 +780,15 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
# handle change and produce proofs # handle change and produce proofs
if status.change: if status.change:
change_proofs = self._construct_proofs( change_proofs = await self._construct_proofs(
status.change, status.change,
secrets[: len(status.change)], secrets[: len(status.change)],
rs[: len(status.change)], rs[: len(status.change)],
derivation_paths[: len(status.change)], derivation_paths[: len(status.change)],
) )
logger.debug(f"Received change: {sum_proofs(change_proofs)} sat") logger.debug(f"Received change: {sum_proofs(change_proofs)} sat")
await self._store_proofs(change_proofs)
await self.invalidate(proofs)
else: else:
raise Exception("could not pay invoice.") raise Exception("could not pay invoice.")
@@ -895,10 +799,137 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
# ---------- TOKEN MECHANICS ---------- # ---------- 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 def _store_proofs(self, proofs):
async with self.db.connect() as conn: try:
async with self.db.connect() as conn: # type: ignore
for proof in proofs: for proof in proofs:
await store_proof(proof, db=self.db, conn=conn) 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 @staticmethod
def _get_proofs_per_keyset(proofs: List[Proof]): def _get_proofs_per_keyset(proofs: List[Proof]):
@@ -981,7 +1012,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
return token return token
async def serialize_proofs( async def serialize_proofs(
self, proofs: List[Proof], include_mints=True, legacy=False self, proofs: List[Proof], include_mints=True, include_dleq=False, legacy=False
) -> str: ) -> str:
"""Produces sharable token with proofs and mint information. """Produces sharable token with proofs and mint information.
@@ -1007,7 +1038,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
# V3 tokens # V3 tokens
token = await self._make_token(proofs, include_mints) 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) -> TokenV2: async def _make_token_v2(self, proofs: List[Proof], include_mints=True) -> TokenV2:
""" """
@@ -1016,6 +1047,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
""" """
# build token # build token
token = TokenV2(proofs=proofs) token = TokenV2(proofs=proofs)
# add mint information to the token, if requested # add mint information to the token, if requested
if include_mints: if include_mints:
# dummy object to hold information about the mint # dummy object to hold information about the mint
@@ -1132,7 +1164,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
invalidated_proofs = proofs invalidated_proofs = proofs
if invalidated_proofs: if invalidated_proofs:
logger.debug( logger.trace(
f"Invalidating {len(invalidated_proofs)} proofs worth" f"Invalidating {len(invalidated_proofs)} proofs worth"
f" {sum_proofs(invalidated_proofs)} sat." f" {sum_proofs(invalidated_proofs)} sat."
) )
@@ -1235,7 +1267,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
Args: Args:
mnemonic (Optional[str]): The mnemonic to restore the wallet from. If None, the mnemonic is loaded from the db. 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. batch (int, optional): The number of proofs to restore in one batch. Defaults to 25.
""" """
await self._init_private_key(mnemonic) await self._init_private_key(mnemonic)
@@ -1295,7 +1327,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
) )
# we don't know the amount but luckily the mint will tell us so we use a dummy amount here # 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) 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) regenerated_outputs, _ = self._construct_outputs(amounts_dummy, secrets, rs)
# we ask the mint to reissue the promises # we ask the mint to reissue the promises
proofs = await self.restore_promises( proofs = await self.restore_promises(
@@ -1339,14 +1371,8 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
secrets = [secrets[i] for i in matching_indices] secrets = [secrets[i] for i in matching_indices]
rs = [rs[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 # 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 restored_promises, secrets, rs, derivation_paths
) )
logger.debug(f"Restored {len(restored_promises)} promises") 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 return proofs

View File

@@ -3,6 +3,7 @@ import asyncio
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from cashu.core.base import TokenV3
from cashu.core.settings import settings from cashu.core.settings import settings
from cashu.wallet.cli.cli import cli from cashu.wallet.cli.cli import cli
from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet
@@ -123,9 +124,38 @@ def test_send(mint, cli_prefix):
[*cli_prefix, "send", "10"], [*cli_prefix, "send", "10"],
) )
assert result.exception is None assert result.exception is None
print("SEND")
print(result.output) 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"
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)
# 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"
def test_send_without_split(mint, cli_prefix): def test_send_without_split(mint, cli_prefix):
@@ -243,3 +273,14 @@ def test_nostr_send(mint, cli_prefix):
assert result.exception is None assert result.exception is None
print("NOSTR_SEND") print("NOSTR_SEND")
print(result.output) print(result.output)
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

View File

@@ -31,17 +31,79 @@ def test_tokenv3_get_proofs():
assert len(token.get_proofs()) == 2 assert len(token.get_proofs()) == 2
def test_tokenv3_deserialize_serialize_with_dleq():
token_str = (
"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
def test_tokenv3_deserialize_serialize(): def test_tokenv3_deserialize_serialize():
token_str = ( token_str = (
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci" "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJh"
"LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW" "bW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci"
"1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiMDIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0" "LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOG"
"UyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW"
"1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiM"
"DIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0"
"NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19" "NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19"
) )
token = TokenV3.deserialize(token_str) token = TokenV3.deserialize(token_str)
assert token.serialize() == token_str assert token.serialize() == token_str
def test_tokenv3_deserialize_serialize_no_dleq():
token_str = (
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhb"
"W91bnQiOiAyLCAic2VjcmV0IjogIjZmZjFiY2VlOGUzMzk2NGE4ZDNjNGQ5NzYwNzdiZ"
"DI4ZGVkZWJkODYyMDU0MDQzNDY4ZjU5ZDFiZjI1OTQzN2QiLCAiQyI6ICIwM2I3ZDlkMzIzY"
"TAxOWJlNTE4NzRlOGE5OGY1NDViOTg3Y2JmNmU5MWUwMDc1YTFhZjQ3MjY2NDMxOGRlZ"
"TQzZTUiLCAiZGxlcSI6IHsiZSI6ICI1ZjkxMGQ4NTc0M2U0OTI0ZjRiNjlkNzhjMjFjYTc1Z"
"jEzNzg3Zjc3OTE1NWRmMjMzMjJmYTA1YjU5ODdhYzNmIiwgInMiOiAiZTc4Y2U0MzNiZ"
"WNlZTNjNGU1NzM4ZDdjMzRlNDQyZWQ0MmJkMzk0MjI0ZTc3MjE4OGFjMmI5MzZmMjA2Y2QxY"
"SIsICJyIjogIjI3MzM3ODNmOTQ4MWZlYzAxNzdlYmM4ZjBhOTI2OWVjOGFkNzU5MDU2ZT"
"k3MTRiMWEwYTEwMDQ3MmY2Y2Y5YzIifX0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3"
"VudCI6IDgsICJzZWNyZXQiOiAiMmFkNDMyZDRkNTg2MzJiMmRlMzI0ZmQxYmE5OTcyZmE"
"4MDljNmU3ZGE1ZTkyZWVmYjBiNjYxMmQ5M2Q3ZTAwMCIsICJDIjogIjAzMmFmYjgzOWQwMmR"
"mMWNhOGY5ZGZjNTI1NzUxN2Q0MzY4YjdiMTc0MzgzM2JlYWUzZDQzNmExYmQwYmJkYjVk"
"OCIsICJkbGVxIjogeyJlIjogImY0NjM2MzU5YTUzZGQxNGEyNmUyNTMyMDQxZWIxMDE2OTk1"
"ZTg4NzgwODY0OWFlY2VlNTcwZTA5ZTk2NTU3YzIiLCAicyI6ICJmZWYzMGIzMDcwMDJkMW"
"VjNWZiZjg0ZGZhZmRkMGEwOTdkNDJlMDYxNTZiNzdiMTMzMmNjNGZjNGNjYWEyODJmIiwgIn"
"IiOiAiODQ5MjQxNzBlYzc3ZjhjMDNmZDRlZTkyZTA3MjdlMzYyNTliZjRhYTc4NTBjZTc2"
"NDExMDQ0MmNlNmVlM2FjYyJ9fV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzOCJ9XX0="
)
token_str_no_dleq = (
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bn"
"QiOiAyLCAic2VjcmV0IjogIjZmZjFiY2VlOGUzMzk2NGE4ZDNjNGQ5NzYwNzdiZDI4"
"ZGVkZWJkODYyMDU0MDQzNDY4ZjU5ZDFiZjI1OTQzN2QiLCAiQyI6ICIwM2I3ZDlkMzIzYTAxOWJlN"
"TE4NzRlOGE5OGY1NDViOTg3Y2JmNmU5MWUwMDc1YTFhZjQ3MjY2NDMxOGRlZTQzZTU"
"ifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICIyYWQ0MzJkN"
"GQ1ODYzMmIyZGUzMjRmZDFiYTk5NzJmYTgwOWM2ZTdkYTVlOTJlZWZiMGI2NjEyZD"
"kzZDdlMDAwIiwgIkMiOiAiMDMyYWZiODM5ZDAyZGYxY2E4ZjlkZmM1MjU3NTE3ZDQzNjhiN2IxNzQz"
"ODMzYmVhZTNkNDM2YTFiZDBiYmRiNWQ4In1dLCAibWludCI6ICJodHRwOi8vbG9jY"
"Wxob3N0OjMzMzgifV19"
)
token = TokenV3.deserialize(token_str)
assert token.serialize(include_dleq=False) == token_str_no_dleq
def test_tokenv3_deserialize_with_memo(): def test_tokenv3_deserialize_with_memo():
token_str = ( token_str = (
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjV" "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjV"

View File

@@ -1,4 +1,13 @@
from cashu.core.crypto.b_dhke import hash_to_curve, step1_alice, step2_bob, step3_alice from cashu.core.crypto.b_dhke import (
alice_verify_dleq,
carol_verify_dleq,
hash_e,
hash_to_curve,
step1_alice,
step2_bob,
step2_bob_dleq,
step3_alice,
)
from cashu.core.crypto.secp import PrivateKey, PublicKey from cashu.core.crypto.secp import PrivateKey, PublicKey
@@ -38,9 +47,9 @@ def test_hash_to_curve_iteration():
def test_step1(): def test_step1():
"""""" secret_msg = "test_message"
B_, blinding_factor = step1_alice( B_, blinding_factor = step1_alice(
"test_message", secret_msg,
blinding_factor=PrivateKey( blinding_factor=PrivateKey(
privkey=bytes.fromhex( privkey=bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000001" "0000000000000000000000000000000000000000000000000000000000000001"
@@ -73,7 +82,7 @@ def test_step2():
), ),
raw=True, raw=True,
) )
C_ = step2_bob(B_, a) C_, e, s = step2_bob(B_, a)
assert ( assert (
C_.serialize().hex() C_.serialize().hex()
== "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" == "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2"
@@ -107,3 +116,187 @@ def test_step3():
C.serialize().hex() C.serialize().hex()
== "03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd" == "03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd"
) )
def test_dleq_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_dleq_step2_bob_dleq():
B_, _ = step1_alice(
"test_message",
blinding_factor=PrivateKey(
privkey=bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000001"
),
raw=True,
),
)
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.serialize()
== "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9"
)
assert (
s.serialize()
== "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.serialize()
== "df1984d5c22f7e17afe33b8669f02f530f286ae3b00a1978edaf900f4721f65e"
)
assert (
s.serialize()
== "828404170c86f240c50ae0f5fc17bb6b82612d46b355e046d7cd84b0a3c934a0"
)
def test_dleq_alice_verify_dleq():
# e from test_step2_bob_dleq for a=0x1
e = PrivateKey(
bytes.fromhex(
"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9"
),
raw=True,
)
# s from test_step2_bob_dleq for a=0x1
s = PrivateKey(
bytes.fromhex(
"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",
# blinding_factor=bytes.fromhex(
# "0000000000000000000000000000000000000000000000000000000000000001"
# ), # 32 bytes
# )
B_ = PublicKey(
bytes.fromhex(
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2"
),
raw=True,
)
# # C_ is the same as if we did:
# a = PrivateKey(
# privkey=bytes.fromhex(
# "0000000000000000000000000000000000000000000000000000000000000001"
# ),
# raw=True,
# )
# C_, e, s = step2_bob(B_, a)
C_ = PublicKey(
bytes.fromhex(
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2"
),
raw=True,
)
assert alice_verify_dleq(B_, C_, e, s, A)
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(
privkey=bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000001"
),
raw=True,
),
)
C_, e, s = step2_bob(B_, a)
assert alice_verify_dleq(B_, C_, e, s, A)
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(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
assert carol_verify_dleq(secret_msg=secret_msg, C=C, r=r, e=e, s=s, A=A)

View File

@@ -107,6 +107,12 @@ async def test_generate_promises(ledger: Ledger):
promises[0].C_ promises[0].C_
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15" == "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 @pytest.mark.asyncio

View File

@@ -172,6 +172,7 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split(wallet1: Wallet): async def test_split(wallet1: Wallet):
await wallet1.mint(64) await wallet1.mint(64)
assert wallet1.balance == 64
p1, p2 = await wallet1.split(wallet1.proofs, 20) p1, p2 = await wallet1.split(wallet1.proofs, 20)
assert wallet1.balance == 64 assert wallet1.balance == 64
assert sum_proofs(p1) == 44 assert sum_proofs(p1) == 44