mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
[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:
@@ -11,6 +11,26 @@ from .crypto.secp import PrivateKey, PublicKey
|
||||
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 -------
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
@@ -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
|
||||
@@ -308,7 +352,7 @@ class MintKeyset:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id=None,
|
||||
id="",
|
||||
valid_from=None,
|
||||
valid_to=None,
|
||||
first_seen=None,
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -454,7 +498,7 @@ class TokenV3(BaseModel):
|
||||
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>.
|
||||
"""
|
||||
@@ -462,6 +506,6 @@ class TokenV3(BaseModel):
|
||||
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
|
||||
|
||||
@@ -28,22 +28,43 @@ Bob:
|
||||
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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@ def step0_carol_privkey():
|
||||
return seckey
|
||||
|
||||
|
||||
def step0_carol_checksig_redeemscrip(carol_pubkey):
|
||||
def step0_carol_checksig_redeemscript(carol_pubkey):
|
||||
"""Create script"""
|
||||
txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG])
|
||||
# txin_redeemScript = CScript([-123, OP_CHECKLOCKTIMEVERIFY])
|
||||
@@ -111,7 +111,7 @@ if __name__ == "__main__":
|
||||
# ---------
|
||||
# CAROL defines scripthash and ALICE mints them
|
||||
alice_privkey = step0_carol_privkey()
|
||||
txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub)
|
||||
txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub)
|
||||
print("Script:", txin_redeemScript.__repr__())
|
||||
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
|
||||
print(f"Carol sends Alice secret = P2SH:{txin_p2sh_address}")
|
||||
@@ -128,7 +128,7 @@ if __name__ == "__main__":
|
||||
# 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()
|
||||
|
||||
@@ -49,23 +49,28 @@ class LedgerCrud:
|
||||
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from loguru import logger
|
||||
|
||||
from ..core import bolt11
|
||||
from ..core.base import (
|
||||
DLEQ,
|
||||
BlindedMessage,
|
||||
BlindedSignature,
|
||||
Invoice,
|
||||
@@ -117,8 +118,7 @@ class Ledger:
|
||||
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
|
||||
|
||||
@@ -188,17 +188,24 @@ class Ledger:
|
||||
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."""
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -225,11 +225,11 @@ async def send_command(
|
||||
global wallet
|
||||
if not nostr:
|
||||
balance, token = await send(
|
||||
wallet, amount, lock, legacy=False, split=not nosplit
|
||||
wallet, amount=amount, lock=lock, legacy=False, split=not nosplit
|
||||
)
|
||||
return SendResponse(balance=balance, token=token)
|
||||
else:
|
||||
token, pubkey = await send_nostr(wallet, amount, nostr)
|
||||
token, pubkey = await send_nostr(wallet, amount=amount, pubkey=nostr)
|
||||
return SendResponse(balance=wallet.available_balance, token=token, npub=pubkey)
|
||||
|
||||
|
||||
@@ -325,7 +325,7 @@ async def pending(
|
||||
enumerate(
|
||||
groupby(
|
||||
sorted_proofs,
|
||||
key=itemgetter("send_id"),
|
||||
key=itemgetter("send_id"), # type: ignore
|
||||
)
|
||||
),
|
||||
offset,
|
||||
@@ -334,9 +334,9 @@ async def pending(
|
||||
grouped_proofs = list(value)
|
||||
token = await wallet.serialize_proofs(grouped_proofs)
|
||||
tokenObj = deserialize_token_from_string(token)
|
||||
mint = [t.mint for t in tokenObj.token][0]
|
||||
mint = [t.mint for t in tokenObj.token if t.mint][0]
|
||||
reserved_date = datetime.utcfromtimestamp(
|
||||
int(grouped_proofs[0].time_reserved)
|
||||
int(grouped_proofs[0].time_reserved) # type: ignore
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
result.update(
|
||||
{
|
||||
|
||||
@@ -352,6 +352,14 @@ async def balance(ctx: Context, verbose):
|
||||
type=str,
|
||||
)
|
||||
@click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", type=str)
|
||||
@click.option(
|
||||
"--dleq",
|
||||
"-d",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Send with DLEQ proof.",
|
||||
type=bool,
|
||||
)
|
||||
@click.option(
|
||||
"--legacy",
|
||||
"-l",
|
||||
@@ -387,6 +395,7 @@ async def send_command(
|
||||
nostr: str,
|
||||
nopt: str,
|
||||
lock: str,
|
||||
dleq: bool,
|
||||
legacy: bool,
|
||||
verbose: bool,
|
||||
yes: bool,
|
||||
@@ -394,9 +403,18 @@ async def send_command(
|
||||
):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
if not nostr and not nopt:
|
||||
await send(wallet, amount, lock, legacy, split=not nosplit)
|
||||
await send(
|
||||
wallet,
|
||||
amount=amount,
|
||||
lock=lock,
|
||||
legacy=legacy,
|
||||
split=not nosplit,
|
||||
include_dleq=dleq,
|
||||
)
|
||||
else:
|
||||
await send_nostr(wallet, amount, nostr or nopt, verbose, yes)
|
||||
await send_nostr(
|
||||
wallet, amount=amount, pubkey=nostr or nopt, verbose=verbose, yes=yes
|
||||
)
|
||||
|
||||
|
||||
@cli.command("receive", help="Receive tokens.")
|
||||
@@ -532,14 +550,15 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
|
||||
enumerate(
|
||||
groupby(
|
||||
sorted_proofs,
|
||||
key=itemgetter("send_id"),
|
||||
key=itemgetter("send_id"), # type: ignore
|
||||
)
|
||||
),
|
||||
offset,
|
||||
number,
|
||||
):
|
||||
grouped_proofs = list(value)
|
||||
token = await wallet.serialize_proofs(grouped_proofs)
|
||||
# TODO: we can't return DLEQ because we don't store it
|
||||
token = await wallet.serialize_proofs(grouped_proofs, include_dleq=False)
|
||||
tokenObj = deserialize_token_from_string(token)
|
||||
mint = [t.mint for t in tokenObj.token][0]
|
||||
# token_hidden_secret = await wallet.serialize_proofs(grouped_proofs)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
@@ -9,12 +10,12 @@ async def store_proof(
|
||||
proof: Proof,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO proofs
|
||||
(id, amount, C, secret, time_created, derivation_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(id, amount, C, secret, time_created, derivation_path, dleq)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.id,
|
||||
@@ -23,6 +24,7 @@ async def store_proof(
|
||||
str(proof.secret),
|
||||
int(time.time()),
|
||||
proof.derivation_path,
|
||||
json.dumps(proof.dleq.dict()) if proof.dleq else "",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -30,29 +32,29 @@ async def store_proof(
|
||||
async def get_proofs(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> List[Proof]:
|
||||
rows = await (conn or db).fetchall("""
|
||||
SELECT * from proofs
|
||||
""")
|
||||
return [Proof(**dict(r)) for r in rows]
|
||||
return [Proof.from_dict(dict(r)) for r in rows]
|
||||
|
||||
|
||||
async def get_reserved_proofs(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> List[Proof]:
|
||||
rows = await (conn or db).fetchall("""
|
||||
SELECT * from proofs
|
||||
WHERE reserved
|
||||
""")
|
||||
return [Proof(**r) for r in rows]
|
||||
return [Proof.from_dict(dict(r)) for r in rows]
|
||||
|
||||
|
||||
async def invalidate_proof(
|
||||
proof: Proof,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
DELETE FROM proofs
|
||||
@@ -84,7 +86,7 @@ async def update_proof_reserved(
|
||||
send_id: str = "",
|
||||
db: Optional[Database] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
clauses.append("reserved = ?")
|
||||
@@ -109,7 +111,7 @@ async def secret_used(
|
||||
secret: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> bool:
|
||||
rows = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * from proofs
|
||||
@@ -124,7 +126,7 @@ async def store_p2sh(
|
||||
p2sh: P2SHScript,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO p2sh
|
||||
@@ -144,7 +146,7 @@ async def get_unused_locks(
|
||||
address: str = "",
|
||||
db: Optional[Database] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> List[P2SHScript]:
|
||||
clause: List[str] = []
|
||||
args: List[str] = []
|
||||
|
||||
@@ -173,7 +175,7 @@ async def update_p2sh_used(
|
||||
used: bool,
|
||||
db: Optional[Database] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
clauses = []
|
||||
values = []
|
||||
clauses.append("used = ?")
|
||||
@@ -190,7 +192,7 @@ async def store_keyset(
|
||||
mint_url: str = "",
|
||||
db: Optional[Database] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute( # type: ignore
|
||||
"""
|
||||
INSERT INTO keysets
|
||||
@@ -243,7 +245,7 @@ async def store_lightning_invoice(
|
||||
db: Database,
|
||||
invoice: Invoice,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO invoices
|
||||
@@ -266,7 +268,7 @@ async def get_lightning_invoice(
|
||||
db: Database,
|
||||
hash: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> Invoice:
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
if hash:
|
||||
@@ -291,7 +293,7 @@ async def get_lightning_invoices(
|
||||
db: Database,
|
||||
paid: Optional[bool] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> List[Invoice]:
|
||||
clauses: List[Any] = []
|
||||
values: List[Any] = []
|
||||
|
||||
@@ -319,7 +321,7 @@ async def update_lightning_invoice(
|
||||
paid: bool,
|
||||
time_paid: Optional[int] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
clauses.append("paid = ?")
|
||||
@@ -344,7 +346,7 @@ async def bump_secret_derivation(
|
||||
by: int = 1,
|
||||
skip: bool = False,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> int:
|
||||
rows = await (conn or db).fetchone(
|
||||
"SELECT counter from keysets WHERE id = ?", (keyset_id,)
|
||||
)
|
||||
@@ -374,7 +376,7 @@ async def set_secret_derivation(
|
||||
keyset_id: str,
|
||||
counter: int,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"UPDATE keysets SET counter = ? WHERE id = ?",
|
||||
(
|
||||
@@ -388,7 +390,7 @@ async def set_nostr_last_check_timestamp(
|
||||
db: Database,
|
||||
timestamp: int,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"UPDATE nostr SET last = ? WHERE type = ?",
|
||||
(timestamp, "dm"),
|
||||
@@ -398,7 +400,7 @@ async def set_nostr_last_check_timestamp(
|
||||
async def get_nostr_last_check_timestamp(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> Optional[int]:
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT last from nostr WHERE type = ?
|
||||
@@ -432,7 +434,7 @@ async def store_seed_and_mnemonic(
|
||||
seed: str,
|
||||
mnemonic: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO seed
|
||||
|
||||
@@ -160,7 +160,13 @@ async def receive(
|
||||
|
||||
|
||||
async def send(
|
||||
wallet: Wallet, amount: int, lock: str, legacy: bool, split: bool = True
|
||||
wallet: Wallet,
|
||||
*,
|
||||
amount: int,
|
||||
lock: str,
|
||||
legacy: bool,
|
||||
split: bool = True,
|
||||
include_dleq: bool = False,
|
||||
):
|
||||
"""
|
||||
Prints token to send to stdout.
|
||||
@@ -207,14 +213,14 @@ async def send(
|
||||
"No proof with this amount found. Available amounts:"
|
||||
f" {set([p.amount for p in wallet.proofs])}"
|
||||
)
|
||||
await wallet.set_reserved(send_proofs, reserved=True)
|
||||
|
||||
token = await wallet.serialize_proofs(
|
||||
send_proofs,
|
||||
include_mints=True,
|
||||
include_dleq=include_dleq,
|
||||
)
|
||||
print(token)
|
||||
|
||||
await wallet.set_reserved(send_proofs, reserved=True)
|
||||
if legacy:
|
||||
print("")
|
||||
print("Old token format:")
|
||||
@@ -222,6 +228,7 @@ async def send(
|
||||
token = await wallet.serialize_proofs(
|
||||
send_proofs,
|
||||
legacy=True,
|
||||
include_dleq=include_dleq,
|
||||
)
|
||||
print(token)
|
||||
|
||||
|
||||
@@ -173,3 +173,10 @@ async def m009_privatekey_and_determinstic_key_derivation(db: Database):
|
||||
);
|
||||
""")
|
||||
# await db.execute("INSERT INTO secret_derivation (counter) VALUES (0)")
|
||||
|
||||
|
||||
async def m010_add_proofs_dleq(db: Database):
|
||||
"""
|
||||
Columns to store DLEQ proofs for proofs.
|
||||
"""
|
||||
await db.execute("ALTER TABLE proofs ADD COLUMN dleq TEXT")
|
||||
|
||||
@@ -45,10 +45,12 @@ async def nip5_to_pubkey(wallet: Wallet, address: str):
|
||||
|
||||
async def send_nostr(
|
||||
wallet: Wallet,
|
||||
*,
|
||||
amount: int,
|
||||
pubkey: str,
|
||||
verbose: bool = False,
|
||||
yes: bool = True,
|
||||
include_dleq=False,
|
||||
):
|
||||
"""
|
||||
Sends tokens via nostr.
|
||||
@@ -62,7 +64,7 @@ async def send_nostr(
|
||||
_, send_proofs = await wallet.split_to_send(
|
||||
wallet.proofs, amount, set_reserved=True
|
||||
)
|
||||
token = await wallet.serialize_proofs(send_proofs)
|
||||
token = await wallet.serialize_proofs(send_proofs, include_dleq=include_dleq)
|
||||
|
||||
if pubkey.startswith("npub"):
|
||||
pubkey_to = PublicKey().from_npub(pubkey)
|
||||
|
||||
@@ -21,7 +21,7 @@ from ..core.p2pk import (
|
||||
sign_p2pk_sign,
|
||||
)
|
||||
from ..core.script import (
|
||||
step0_carol_checksig_redeemscrip,
|
||||
step0_carol_checksig_redeemscript,
|
||||
step0_carol_privkey,
|
||||
step1_carol_create_p2sh_address,
|
||||
step2_carol_sign_tx,
|
||||
@@ -41,7 +41,7 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
async def create_p2sh_address_and_store(self) -> str:
|
||||
"""Creates a P2SH lock script and stores the script and signature in the database."""
|
||||
alice_privkey = step0_carol_privkey()
|
||||
txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub)
|
||||
txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub)
|
||||
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
|
||||
txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig
|
||||
txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode()
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..core.base import (
|
||||
CheckFeesRequest,
|
||||
CheckSpendableRequest,
|
||||
CheckSpendableResponse,
|
||||
DLEQWallet,
|
||||
GetInfoResponse,
|
||||
GetMeltResponse,
|
||||
GetMintResponse,
|
||||
@@ -110,101 +111,11 @@ class LedgerAPI(object):
|
||||
self.s = requests.Session()
|
||||
self.db = db
|
||||
|
||||
# async def generate_n_secrets(
|
||||
# self, n: int = 1, skip_bump: bool = False
|
||||
# ) -> Tuple[List[str], List[PrivateKey], List[str]]:
|
||||
# return await self.generate_n_secrets(n, skip_bump)
|
||||
|
||||
# async def _generate_secret(self, skip_bump: bool = False) -> str:
|
||||
# return await self._generate_secret(skip_bump)
|
||||
|
||||
@async_set_requests
|
||||
async def _init_s(self):
|
||||
"""Dummy function that can be called from outside to use LedgerAPI.s"""
|
||||
return
|
||||
|
||||
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
|
||||
def raise_on_error(resp: Response) -> None:
|
||||
"""Raises an exception if the response from the mint contains an error.
|
||||
@@ -465,9 +376,9 @@ class LedgerAPI(object):
|
||||
},
|
||||
)
|
||||
self.raise_on_error(resp)
|
||||
reponse_dict = resp.json()
|
||||
response_dict = resp.json()
|
||||
logger.trace("Lightning invoice checked. POST /mint")
|
||||
promises = PostMintResponse.parse_obj(reponse_dict).promises
|
||||
promises = PostMintResponse.parse_obj(response_dict).promises
|
||||
return promises
|
||||
|
||||
@async_set_requests
|
||||
@@ -506,7 +417,7 @@ class LedgerAPI(object):
|
||||
@async_set_requests
|
||||
async def check_proof_state(self, proofs: List[Proof]):
|
||||
"""
|
||||
Cheks whether the secrets in proofs are already spent or not and returns a list of booleans.
|
||||
Checks whether the secrets in proofs are already spent or not and returns a list of booleans.
|
||||
"""
|
||||
payload = CheckSpendableRequest(proofs=proofs)
|
||||
|
||||
@@ -577,8 +488,8 @@ class LedgerAPI(object):
|
||||
payload = PostMintRequest(outputs=outputs)
|
||||
resp = self.s.post(self.url + "/restore", json=payload.dict())
|
||||
self.raise_on_error(resp)
|
||||
reponse_dict = resp.json()
|
||||
returnObj = PostRestoreResponse.parse_obj(reponse_dict)
|
||||
response_dict = resp.json()
|
||||
returnObj = PostRestoreResponse.parse_obj(response_dict)
|
||||
return returnObj.outputs, returnObj.promises
|
||||
|
||||
|
||||
@@ -609,7 +520,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
self.name = name
|
||||
|
||||
super().__init__(url=url, db=self.db)
|
||||
logger.debug(f"Wallet initalized with mint URL {url}")
|
||||
logger.debug(f"Wallet initialized with mint URL {url}")
|
||||
|
||||
@classmethod
|
||||
async def with_db(
|
||||
@@ -726,16 +637,12 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
await bump_secret_derivation(
|
||||
db=self.db, keyset_id=self.keyset_id, by=len(amounts)
|
||||
)
|
||||
proofs = self._construct_proofs(promises, secrets, rs, derivation_paths)
|
||||
proofs = await self._construct_proofs(promises, secrets, rs, derivation_paths)
|
||||
|
||||
if proofs == []:
|
||||
raise Exception("received no proofs.")
|
||||
await self._store_proofs(proofs)
|
||||
if hash:
|
||||
await update_lightning_invoice(
|
||||
db=self.db, hash=hash, paid=True, time_paid=int(time.time())
|
||||
)
|
||||
self.proofs += proofs
|
||||
return proofs
|
||||
|
||||
async def redeem(
|
||||
@@ -749,6 +656,10 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
Args:
|
||||
proofs (List[Proof]): Proofs to be redeemed.
|
||||
"""
|
||||
# verify DLEQ of incoming proofs
|
||||
logger.debug("Verifying DLEQ of incoming proofs.")
|
||||
self.verify_proofs_dleq(proofs)
|
||||
logger.debug("DLEQ verified.")
|
||||
return await self.split(proofs, sum_proofs(proofs))
|
||||
|
||||
async def split(
|
||||
@@ -797,9 +708,9 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
logger.debug(f"Creating proofs with custom secrets: {secret_locks}")
|
||||
assert len(secret_locks) == len(
|
||||
scnd_outputs
|
||||
), "number of secret_locks does not match number of ouptus."
|
||||
), "number of secret_locks does not match number of outputs."
|
||||
# append predefined secrets (to send) to random secrets (to keep)
|
||||
# generate sercets to keep
|
||||
# generate secrets to keep
|
||||
secrets = [
|
||||
await self._generate_secret() for s in range(len(frst_outputs))
|
||||
] + secret_locks
|
||||
@@ -822,18 +733,11 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
promises = await super().split(proofs, outputs)
|
||||
|
||||
# Construct proofs from returned promises (i.e., unblind the signatures)
|
||||
new_proofs = self._construct_proofs(promises, secrets, rs, derivation_paths)
|
||||
new_proofs = await self._construct_proofs(
|
||||
promises, secrets, rs, derivation_paths
|
||||
)
|
||||
|
||||
# remove used proofs from wallet and add new ones
|
||||
used_secrets = [p.secret for p in proofs]
|
||||
self.proofs = list(filter(lambda p: p.secret not in used_secrets, self.proofs))
|
||||
# add new proofs to wallet
|
||||
self.proofs += new_proofs
|
||||
# store new proofs in database
|
||||
await self._store_proofs(new_proofs)
|
||||
# invalidate used proofs in database
|
||||
for proof in proofs:
|
||||
await invalidate_proof(proof, db=self.db)
|
||||
await self.invalidate(proofs)
|
||||
|
||||
keep_proofs = new_proofs[: len(frst_outputs)]
|
||||
send_proofs = new_proofs[len(frst_outputs) :]
|
||||
@@ -862,7 +766,6 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
|
||||
if status.paid:
|
||||
# the payment was successful
|
||||
await self.invalidate(proofs)
|
||||
invoice_obj = Invoice(
|
||||
amount=-sum_proofs(proofs),
|
||||
pr=invoice,
|
||||
@@ -877,14 +780,15 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
|
||||
# handle change and produce proofs
|
||||
if status.change:
|
||||
change_proofs = self._construct_proofs(
|
||||
change_proofs = await self._construct_proofs(
|
||||
status.change,
|
||||
secrets[: len(status.change)],
|
||||
rs[: len(status.change)],
|
||||
derivation_paths[: len(status.change)],
|
||||
)
|
||||
logger.debug(f"Received change: {sum_proofs(change_proofs)} sat")
|
||||
await self._store_proofs(change_proofs)
|
||||
|
||||
await self.invalidate(proofs)
|
||||
|
||||
else:
|
||||
raise Exception("could not pay invoice.")
|
||||
@@ -895,10 +799,137 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
|
||||
# ---------- TOKEN MECHANICS ----------
|
||||
|
||||
# ---------- DLEQ PROOFS ----------
|
||||
|
||||
def verify_proofs_dleq(self, proofs: List[Proof]):
|
||||
"""Verifies DLEQ proofs in proofs."""
|
||||
for proof in proofs:
|
||||
if not proof.dleq:
|
||||
logger.trace("No DLEQ proof in proof.")
|
||||
return
|
||||
logger.trace("Verifying DLEQ proof.")
|
||||
assert self.keys.public_keys
|
||||
if not b_dhke.carol_verify_dleq(
|
||||
secret_msg=proof.secret,
|
||||
C=PublicKey(bytes.fromhex(proof.C), raw=True),
|
||||
r=PrivateKey(bytes.fromhex(proof.dleq.r), raw=True),
|
||||
e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True),
|
||||
s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True),
|
||||
A=self.keys.public_keys[proof.amount],
|
||||
):
|
||||
raise Exception("DLEQ proof invalid.")
|
||||
else:
|
||||
logger.debug("DLEQ proof valid.")
|
||||
|
||||
async def _construct_proofs(
|
||||
self,
|
||||
promises: List[BlindedSignature],
|
||||
secrets: List[str],
|
||||
rs: List[PrivateKey],
|
||||
derivation_paths: List[str],
|
||||
) -> List[Proof]:
|
||||
"""Constructs proofs from promises, secrets, rs and derivation paths.
|
||||
|
||||
This method is called after the user has received blind signatures from
|
||||
the mint. The results are proofs that can be used as ecash.
|
||||
|
||||
Args:
|
||||
promises (List[BlindedSignature]): blind signatures from mint
|
||||
secrets (List[str]): secrets that were previously used to create blind messages (that turned into promises)
|
||||
rs (List[PrivateKey]): blinding factors that were previously used to create blind messages (that turned into promises)
|
||||
derivation_paths (List[str]): derivation paths that were used to generate secrets and blinding factors
|
||||
|
||||
Returns:
|
||||
List[Proof]: list of proofs that can be used as ecash
|
||||
"""
|
||||
logger.trace("Constructing proofs.")
|
||||
proofs: List[Proof] = []
|
||||
for promise, secret, r, path in zip(promises, secrets, rs, derivation_paths):
|
||||
logger.trace(f"Creating proof with keyset {self.keyset_id} = {promise.id}")
|
||||
assert (
|
||||
self.keyset_id == promise.id
|
||||
), "our keyset id does not match promise id."
|
||||
|
||||
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
|
||||
C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount])
|
||||
B_, r = b_dhke.step1_alice(secret, r) # recompute B_ for dleq proofs
|
||||
|
||||
proof = Proof(
|
||||
id=promise.id,
|
||||
amount=promise.amount,
|
||||
C=C.serialize().hex(),
|
||||
secret=secret,
|
||||
derivation_path=path,
|
||||
)
|
||||
|
||||
# if the mint returned a dleq proof, we add it to the proof
|
||||
if promise.dleq:
|
||||
proof.dleq = DLEQWallet(
|
||||
e=promise.dleq.e, s=promise.dleq.s, r=r.serialize()
|
||||
)
|
||||
|
||||
proofs.append(proof)
|
||||
|
||||
logger.trace(
|
||||
f"Created proof: {proof}, r: {r.serialize()} out of promise {promise}"
|
||||
)
|
||||
|
||||
# DLEQ verify
|
||||
self.verify_proofs_dleq(proofs)
|
||||
|
||||
logger.trace(f"Constructed {len(proofs)} proofs.")
|
||||
|
||||
# add new proofs to wallet
|
||||
self.proofs += proofs
|
||||
# store new proofs in database
|
||||
await self._store_proofs(proofs)
|
||||
|
||||
return proofs
|
||||
|
||||
@staticmethod
|
||||
def _construct_outputs(
|
||||
amounts: List[int], secrets: List[str], rs: List[PrivateKey] = []
|
||||
) -> Tuple[List[BlindedMessage], List[PrivateKey]]:
|
||||
"""Takes a list of amounts and secrets and returns outputs.
|
||||
Outputs are blinded messages `outputs` and blinding factors `rs`
|
||||
|
||||
Args:
|
||||
amounts (List[int]): list of amounts
|
||||
secrets (List[str]): list of secrets
|
||||
rs (List[PrivateKey], optional): list of blinding factors. If not given, `rs` are generated in step1_alice. Defaults to [].
|
||||
|
||||
Returns:
|
||||
List[BlindedMessage]: list of blinded messages that can be sent to the mint
|
||||
List[PrivateKey]: list of blinding factors that can be used to construct proofs after receiving blind signatures from the mint
|
||||
|
||||
Raises:
|
||||
AssertionError: if len(amounts) != len(secrets)
|
||||
"""
|
||||
assert len(amounts) == len(
|
||||
secrets
|
||||
), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}"
|
||||
outputs: List[BlindedMessage] = []
|
||||
|
||||
rs_ = [None] * len(amounts) if not rs else rs
|
||||
rs_return: List[PrivateKey] = []
|
||||
for secret, amount, r in zip(secrets, amounts, rs_):
|
||||
B_, r = b_dhke.step1_alice(secret, r or None)
|
||||
rs_return.append(r)
|
||||
output = BlindedMessage(amount=amount, B_=B_.serialize().hex())
|
||||
outputs.append(output)
|
||||
logger.trace(f"Constructing output: {output}, r: {r.serialize()}")
|
||||
|
||||
return outputs, rs_return
|
||||
|
||||
async def _store_proofs(self, proofs):
|
||||
async with self.db.connect() as conn:
|
||||
for proof in proofs:
|
||||
await store_proof(proof, db=self.db, conn=conn)
|
||||
try:
|
||||
async with self.db.connect() as conn: # type: ignore
|
||||
for proof in proofs:
|
||||
await store_proof(proof, db=self.db, conn=conn)
|
||||
except Exception as e:
|
||||
logger.error(f"Could not store proofs in database: {e}")
|
||||
logger.error(proofs)
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def _get_proofs_per_keyset(proofs: List[Proof]):
|
||||
@@ -981,7 +1012,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
return token
|
||||
|
||||
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:
|
||||
"""Produces sharable token with proofs and mint information.
|
||||
|
||||
@@ -1007,7 +1038,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
|
||||
# V3 tokens
|
||||
token = await self._make_token(proofs, include_mints)
|
||||
return token.serialize()
|
||||
return token.serialize(include_dleq)
|
||||
|
||||
async def _make_token_v2(self, proofs: List[Proof], include_mints=True) -> TokenV2:
|
||||
"""
|
||||
@@ -1016,6 +1047,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
"""
|
||||
# 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
|
||||
@@ -1132,7 +1164,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
invalidated_proofs = proofs
|
||||
|
||||
if invalidated_proofs:
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"Invalidating {len(invalidated_proofs)} proofs worth"
|
||||
f" {sum_proofs(invalidated_proofs)} sat."
|
||||
)
|
||||
@@ -1235,7 +1267,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
|
||||
Args:
|
||||
mnemonic (Optional[str]): The mnemonic to restore the wallet from. If None, the mnemonic is loaded from the db.
|
||||
to (int, optional): The number of consecutive empty reponses to stop restoring. Defaults to 2.
|
||||
to (int, optional): The number of consecutive empty responses to stop restoring. Defaults to 2.
|
||||
batch (int, optional): The number of proofs to restore in one batch. Defaults to 25.
|
||||
"""
|
||||
await self._init_private_key(mnemonic)
|
||||
@@ -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
|
||||
amounts_dummy = [1] * len(secrets)
|
||||
# we generate outptus from deterministic secrets and rs
|
||||
# we generate outputs from deterministic secrets and rs
|
||||
regenerated_outputs, _ = self._construct_outputs(amounts_dummy, secrets, rs)
|
||||
# we ask the mint to reissue the promises
|
||||
proofs = await self.restore_promises(
|
||||
@@ -1339,14 +1371,8 @@ class Wallet(LedgerAPI, WalletP2PK, WalletSecrets):
|
||||
secrets = [secrets[i] for i in matching_indices]
|
||||
rs = [rs[i] for i in matching_indices]
|
||||
# now we can construct the proofs with the secrets and rs
|
||||
proofs = self._construct_proofs(
|
||||
proofs = await self._construct_proofs(
|
||||
restored_promises, secrets, rs, derivation_paths
|
||||
)
|
||||
logger.debug(f"Restored {len(restored_promises)} promises")
|
||||
await self._store_proofs(proofs)
|
||||
|
||||
# append proofs to proofs in memory so the balance updates
|
||||
for proof in proofs:
|
||||
if proof.secret not in [p.secret for p in self.proofs]:
|
||||
self.proofs.append(proof)
|
||||
return proofs
|
||||
|
||||
@@ -3,6 +3,7 @@ import asyncio
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from cashu.core.base import TokenV3
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.cli.cli import cli
|
||||
from cashu.wallet.wallet import Wallet
|
||||
@@ -123,9 +124,38 @@ def test_send(mint, cli_prefix):
|
||||
[*cli_prefix, "send", "10"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("SEND")
|
||||
print(result.output)
|
||||
assert "cashuA" in result.output, "output does not have a token"
|
||||
token_str = result.output.split("\n")[0]
|
||||
assert "cashuA" in token_str, "output does not have a token"
|
||||
token = TokenV3.deserialize(token_str)
|
||||
assert token.token[0].proofs[0].dleq is None, "dleq included"
|
||||
|
||||
|
||||
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):
|
||||
@@ -243,3 +273,14 @@ def test_nostr_send(mint, cli_prefix):
|
||||
assert result.exception is None
|
||||
print("NOSTR_SEND")
|
||||
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
|
||||
|
||||
@@ -31,17 +31,79 @@ def test_tokenv3_get_proofs():
|
||||
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():
|
||||
token_str = (
|
||||
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci"
|
||||
"LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW"
|
||||
"1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiMDIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0"
|
||||
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJh"
|
||||
"bW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci"
|
||||
"LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOG"
|
||||
"UyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW"
|
||||
"1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiM"
|
||||
"DIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0"
|
||||
"NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19"
|
||||
)
|
||||
token = TokenV3.deserialize(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():
|
||||
token_str = (
|
||||
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjV"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -38,9 +47,9 @@ def test_hash_to_curve_iteration():
|
||||
|
||||
|
||||
def test_step1():
|
||||
""""""
|
||||
secret_msg = "test_message"
|
||||
B_, blinding_factor = step1_alice(
|
||||
"test_message",
|
||||
secret_msg,
|
||||
blinding_factor=PrivateKey(
|
||||
privkey=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
@@ -73,7 +82,7 @@ def test_step2():
|
||||
),
|
||||
raw=True,
|
||||
)
|
||||
C_ = step2_bob(B_, a)
|
||||
C_, e, s = step2_bob(B_, a)
|
||||
assert (
|
||||
C_.serialize().hex()
|
||||
== "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2"
|
||||
@@ -107,3 +116,187 @@ def test_step3():
|
||||
C.serialize().hex()
|
||||
== "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)
|
||||
|
||||
@@ -107,6 +107,12 @@ async def test_generate_promises(ledger: Ledger):
|
||||
promises[0].C_
|
||||
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
|
||||
)
|
||||
assert promises[0].amount == 8
|
||||
|
||||
# DLEQ proof present
|
||||
assert promises[0].dleq
|
||||
assert promises[0].dleq.s
|
||||
assert promises[0].dleq.e
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -172,6 +172,7 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_split(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
assert wallet1.balance == 64
|
||||
p1, p2 = await wallet1.split(wallet1.proofs, 20)
|
||||
assert wallet1.balance == 64
|
||||
assert sum_proofs(p1) == 44
|
||||
|
||||
Reference in New Issue
Block a user