How do you prove a coin is yours without showing the key that unlocks it?
A
The trick lives on a curve. Two operations: one easy in one direction and impossible in the other; one algebraic identity that only the secret-holder can produce but anyone can check. The widget below runs the entire dance with numbers small enough to verify by hand.
What needs to be true
Three properties at once: (a) only you can produce a valid signature for a given message, (b) anyone can check it, (c) checking it does not reveal what made it producible. No password scheme satisfies all three — passwords must be shown to be checked. We need a one-way function: easy in the producing direction, impossible in the recovering direction, with a clean algebraic identity tying the two together.
The one-way function — scalar multiplication on a curve
Take an
Computing from takes about additions (double-and-add). Recovering from is the
# Toy curve E: y² = x³ + 7 over F_17.
# Same shape (a=0, b=7) as secp256k1, microscopic prime.
P, A, B = 17, 0, 7
INF = None # the point at infinity is the group identity
def add(P_, Q_):
if P_ is INF: return Q_
if Q_ is INF: return P_
x1, y1 = P_; x2, y2 = Q_
if x1 == x2 and (y1 + y2) % P == 0:
return INF
if P_ == Q_:
lam = (3 * x1 * x1 + A) * pow(2 * y1, -1, P) % P
else:
lam = (y2 - y1) * pow(x2 - x1, -1, P) % P
x3 = (lam * lam - x1 - x2) % P
y3 = (lam * (x1 - x3) - y1) % P
return (x3, y3)
def scalar_mul(k, P_):
R = INF
while k > 0:
if k & 1: R = add(R, P_)
P_ = add(P_, P_)
k >>= 1
return R
G = (1, 5) # generator
N = 9 # order of G — 9G = INF
scalar_mul(2, G) # → (2, 10)
scalar_mul(3, G) # → (5, 9)
scalar_mul(9, G) # → None (= O)Keys — the move that anyone can do, no one can undo
Pick a random integer and keep it secret — that is your
In the widget, drag the private key d slider and watch Q hop around the curve while G stays fixed. Every in lands on a different point of the subgroup. The puzzle: given the position of Q on the plot, can you tell which produced it without trying all eight? On the toy curve you can; on secp256k1 you cannot.
Sign — commit to the message, hide the key
The
In the widget, the signer panel shows the equations live. Drag d, h, or k and the signature recomputes. Note the line labelled verifier · only knows h, (r, s), Q: the boundary between what leaves the signing device and what stays in it.
# Sign a message hash with private key d, nonce k.
def sign(d, h, k):
R = scalar_mul(k, G)
r = R[0] % N
s = (h + r * d) * pow(k, -1, N) % N
if r == 0 or s == 0:
raise ValueError("retry with another k")
return r, s
# Toy: d = 5 (private), h = 3 (message hash), k = 7 (nonce)
sign(5, 3, 7) # → (2, 7)Verify — the identity that holds iff you knew the key
The verifier is given , , and — nothing else. Compute , , then . Accept the signature iff . Watch the algebra:
V = u₁·G + u₂·Q = (h/s)·G + (r/s)·dG = ((h + r·d)/s)·G ← group up by G = ((h + r·d) · k / (h + r·d))·G ← s = (h + r·d)/k = k·G = R ← so V.x = R.x = r ✓
The line hides from anyone who only sees ; the same line, plugged into , makes exactly equal . The verifier never recovers and yet arrives at the same point the signer used. That coincidence — produced only by knowing , checked without — is the signature of the entire scheme, in both senses of the word.
# Verify uses only public information: h, (r, s), Q.
def verify(h, r, s, Q):
s_inv = pow(s, -1, N)
u1 = h * s_inv % N
u2 = r * s_inv % N
V = add(scalar_mul(u1, G), scalar_mul(u2, Q))
return V is not INF and V[0] % N == r
Q = scalar_mul(5, G) # public key matching d = 5
verify(3, 2, 7, Q) # → True
# The algebra: V = u1·G + u2·Q
# = (h/s)G + (r/s)·dG
# = ((h + r·d)/s)·G
# = ((h + r·d) · k / (h + r·d))·G # because s = (h+r·d)/k
# = k·G = R. So V.x mod N = r.
# Verifier never sees d. The identity holds iff signer knew d.secp256k1 — the same algorithm, bigger numbers
Bitcoin runs the same group law and the same ECDSA equations on
The math is sound; the implementation is where it dies. ECDSA requires a fresh, uniformly random nonce for every signature. Reuse across two different messages and an attacker recovers with eighth-grade algebra: — a one-line wallet drainer. Sony’s PlayStation 3 firmware (2010) reused ; their master signing key fell out in days. Bitcoin wallets have lost coins to non-uniform RNG drift the same way. The discrete-log gap is unbreakable by mathematics; the nonce is breakable by code.
Sign with d. Verify without d. A signature is a number you can only produce by knowing the key, and anyone can check by walking around it. Same key authorizes and refuses to be revealed.
On with , compute 3G using the chord-and-tangent group law: first 2G by doubling, then add G. Verify your answer against the widget.
With d = 3, h = 4, k = 2, produce the ECDSA signature . You may use from exercise 1. Confirm in the widget by setting d, h, k to these values.
You receive h = 4, signature (r, s) = (2, 5), and public key . Run the verifier equations: compute u₁, u₂, then V = u₁G + u₂Q, and check V.x mod 9 = r.
A forger doesn’t know d, but tries to invent a signature for some message hash and a target public key Q. Explain — without proof, just the structure — why almost any guess fails the verify check. What would the forger have to know to succeed?
Cryptography textbooks usually open ECDSA with the verification equation — — already on the page, asking the reader to accept it. Lemma walks the door the other way: why would the verifier ever arrive at without knowing ? The answer rebuilds the algebra from the one-way street (multiply easy, divide hard) up. The textbook hides the asymmetry; the asymmetry is the point.