if x1 == x2 and (y1 + y2) % P == 0: return None
Why identical r implies nonce reuse. In ECDSA on secp256k1 (order n), the signature component r = x(k·G) mod n is a deterministic function of the nonce k alone. Two signatures from the same key can only share r if the same k was used in both — a re-randomized k would land on a different curve point with (overwhelmingly) a different x-coordinate. That single nonce now appears in two equations, and subtracting them eliminates the private key d, leaving k as the only unknown:
s = k⁻¹(z + r·d) mod n
s1 = k⁻¹(z1 + r·d) s2 = k⁻¹(z2 + r·d)
s1 − s2 = k⁻¹(z1 − z2) ⇒ k = (z1 − z2) · (s1 − s2)⁻¹ mod n
d = (s1·k − z1) · r⁻¹ mod n
Edge case s1 == s2. If s1 == s2, then z1 == z2 in any valid signature pair (same message), so (z1−z2) = (s1−s2) = 0 and k is 0/0 — indeterminate. The signatures carry no recoverable information, so the code raises ValueError("signatures unusable") instead of attempting a division by zero. This also guards the inconsistent-but-impossible case of s1 == s2 with z1 != z2.
Recovery math is pure big-integer modular arithmetic (extended Euclid for inversion, no crypto libraries); the EC point multiplication used for verification is an independent, self-contained double-and-add implementation.
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
def modinv(a, m): # extended Euclidean inverse
a %= m; t, newt, r, newr = 0, 1, m, a
while newr:
q = r // newr
t, newt, r, newr = newt, t - q*newt, newr, r - q*newr
if r != 1: return None
return t % m
def recover_nonce(r, s1, s2, z1, z2, n=N):
if s1 % n == s2 % n:
raise ValueError("signatures unusable: s1 == s2 (identical messages)")
return ((z1 - z2) * modinv(s1 - s2, n)) % n
def recover_private_key(r, s1, z1, k, n=N):
return ((s1 * k - z1) * modinv(r, n)) % n
# verification only: recompute r from k*G (pure-python secp256k1)
def ec_add(p1, p2):
if p1 is None: return p2
if p2 is None: return p1
x1, y1 = p1; x2, y2 = p2
if x1 == x2 and (y1 + y2) % P == 0: return None
lam = ((3*x1*x1) * modinv(2*y1, P) if p1 == p2
else (y2 - y1) * modinv(x2 - x1, P)) % P
x3 = (lam*lam - x1 - x2) % P
return (x3, (lam*(x1 - x3) - y1) % P)
def ec_mul(k, point=G):
out, add = None, point
while k:
if k & 1: out = ec_add(out, add)
add = ec_add(add, add); k >>= 1
return out
def verify_r(r, k, n=N): # r' = x(k*G) mod n == r ?
q = ec_mul(k)
return q is not None and q[0] % n == r % n
# pipeline: k = recover_nonce(...); d = recover_private_key(...)
# d_hex = format(d, "064x"); assert verify_r(r, k)
Files: `~/solve_ecdsa_nonce_reuse.py` (solution), `~/cross_check.py` (independent verification). Environment: python 3.14, no crypto libs in the recovery path. - **End-to-end self-test** (`solve_ecdsa_nonce_reuse.py`): generated a random 256-bit key, signed two distinct messages with one shared nonce → identical `r` observed; recovered `d` and `k` exactly; recomputed `r' = x(k·G) mod n` from recovered `k` and it matched the signature's `r` bit-for-bit. - **Independent cross-check** against the `cryptography` library (verification only): my pure-Python `d·G` equals the library's public key; `G` confirmed on-curve and `n·G = ∞`; the recovered key derives an identical public key in the library; recovery works with `(z,s)` pairs swapped (negative differences). - **Edge cases tested:** identical messages → `s1 == s2` → rejected with "signatures unusable" (both the valid identical-message case and the inconsistent `s1==s2, z1!=z2` guard); `k` such that `z1−z2` or `s1−s2` is negative handled by the `% m` normalization in `modinv`. - **Stress:** 200 random trials (random key, random nonce, random distinct hashes) — all 200 recovered the exact private key hex and passed `verify_r`. Sample run output: ``` private key (expected): 68f26eee63f526fad7ec51fa9ced7339d810204e2c177a5da559016b1e3e6807 private key (recovered): 68f26eee63f526fad7ec51fa9ced7339d810204e2c177a5da559016b1e3e6807 r (from signatures): b4bb5b7f305983527db59f78557cdc216fa1c0e0f06640fbb48b8a6b77148edb r (recomputed k*G): b4bb5b7f305983527db59f78557cdc216fa1c0e0f06640fbb48b8a6b77148edb ALL TESTS PASSED / ALL CROSS-CHECKS PASSED / stress: 200/200 ```
{"model": "deepseek-v4-flash", "problem_class": "crypto-ecdsa-reused-nonce-private-key-recovery", "result": "passed", "tests": 209}