Last active
July 14, 2025 21:43
-
-
Save jaonoctus/ec40e7b8ecdf77918281f07ef147ac28 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# --- Setup secp256k1 --- | |
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F | |
a_curve, b_curve = 0, 7 | |
E = EllipticCurve(GF(p), [a_curve, b_curve]) | |
G = E( | |
55066263022277343669578718895168534326250603453777594175500187360389116729240, | |
32670510020758816978083085130507043184471273380659243275938904335757337482424, | |
) | |
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 | |
import hashlib | |
from random import randint | |
import os | |
# --- Constants --- | |
DOMAIN_SEPARATOR = b"Secp256k1_HashToCurve_Cashu_" | |
# --- Hash-to-curve implementation --- | |
def hash_to_curve(x_hex: str): | |
"""Implements Y = h'(x)""" | |
x_bytes = bytes.fromhex(x_hex) | |
counter = 0 | |
while True: | |
msg_hash = hashlib.sha256(DOMAIN_SEPARATOR + x_bytes).digest() | |
digest = hashlib.sha256(msg_hash + counter.to_bytes(4, 'little')).digest() | |
x_candidate = int.from_bytes(digest, 'big') % p | |
try: | |
point = E.lift_x(x_candidate) | |
return point | |
except (ValueError, ArithmeticError): | |
counter += 1 | |
# --- Fiat–Shamir challenge hash --- | |
def challenge_hash(*points): | |
h = hashlib.sha256() | |
for P in points: | |
x, y = P.xy() | |
h.update(int(x).to_bytes(32, 'big')) | |
h.update(int(y).to_bytes(32, 'big')) | |
return int.from_bytes(h.digest(), 'big') % n | |
# --- Alice (user) setup --- | |
x_bytes = os.urandom(32) | |
x_hex = x_bytes.hex() | |
r = randint(1, n - 1) | |
R = r * G # R = rG | |
Y = hash_to_curve(x_hex) # Y = h'(x) | |
T = Y + R # T = Y + R | |
# --- Bob (mint) setup --- | |
a = randint(1, n - 1) | |
A = a * G # A = aG | |
Q = a * T # Q = aT | |
# --- DLEQ proof generation --- | |
j = randint(1, n - 1) | |
J1 = j * G # J1 = jG | |
J2 = j * T # J2 = jT | |
e = challenge_hash(J1, J2, A, Q) # e = h(J1 | J2 | A | Q) | |
s = (j + e * a) % n # s = j + ea | |
# --- Alice unblinds the signature --- | |
Z = Q - r * A # Z = Q - rA = aY | |
# --- DLEQ verification --- | |
J1_check = s * G - e * A # J1' = sG - eA | |
J2_check = s * T - e * Q # J2' = sT - eQ | |
e_check = challenge_hash(J1_check, J2_check, A, Q) # DLEQ uses Q | |
dleq_valid = (e_check == e) | |
# --- Token verification (unblinded signature check) --- | |
Y_check = hash_to_curve(x_hex) | |
valid = a * Y_check == Z # Verify Z = aY | |
# --- Output --- | |
print("\n--- Alice (user) setup ---") | |
print("x (secret):", x_hex) | |
print("Y = h'(x) =", Y.xy()) | |
print("R = rG =", R.xy()) | |
print("T = Y + R =", T.xy()) | |
print("\n--- Bob (mint) setup ---") | |
print("A = aG =", A.xy()) | |
print("Q = aT =", Q.xy()) | |
print("\n--- DLEQ proof generation ---") | |
print("J1 = jG =", J1.xy()) | |
print("J2 = jT =", J2.xy()) | |
print("e = h(J1 | J2 | A | Q) =", e) | |
print("s = j + ea =", s) | |
print("\n--- Alice unblinds the signature ---") | |
print("Z = Q - rA = aY =", Z.xy()) | |
print("\n--- DLEQ verification ---") | |
print("J1' = sG - eA =", J1_check.xy()) | |
print("J2' = sT - eQ =", J2_check.xy()) | |
print("DLEQ valid (e_check == e):", dleq_valid) | |
print("\n--- Token verification (unblinded signature check) ---") | |
print("Z == aY (valid token):", valid) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment