Command Palette

Search for a command to run...

Blog

curves~behind.bitcoin

a hands-on tour of secp256k1, ecdsa and schnorr signatures

Bitcoin isn't "secured by passwords". It's secured by math.
And at the center of that math is one of the most elegant structures in cryptography:
an elliptic curve.

  • What even is a field or a curve?
  • How does 256-bit randomness become a public key that nobody can reverse?
  • How are signatures used to verify transactions?

If you're more hands-on you can open this notebook which has code to follow along.

Why elliptic curves?

Cryptography needs mathematical structures where certain operations are easy in one direction but practically impossible to reverse. Before elliptic curves, systems like RSA used the difficulty of factoring large numbers. But there's a catch: as computers get faster, you need exponentially larger keys. A 256-bit RSA key is trivially breakable today.

Elliptic curves give us something special: a group where the discrete logarithm problem is much harder per bit of key size. A 256-bit elliptic curve key provides roughly the same security as a 3072-bit RSA key. This means:

  • Smaller keys (32 bytes vs 384 bytes)
  • Faster operations
  • Smaller signatures
  • Less bandwidth and storage

The "magic" is that while you can efficiently compute P=dGP = dG (multiply a point by a scalar), there's no known shortcut to recover dd from PP and GG. The group structure scrambles the point through billions of additions in a way that appears random, and unlike integer logarithms, there's no equivalent of a "slide rule" that lets you work backwards.

From continuous geometry to discrete arithmetic

Bitcoin uses an elliptic curve known as secp256k1.

On a continuous plane (real numbers), it looks like a smooth curve:

y2=x3+7y^2 = x^3 + 7

Elliptic curve y^2 = x^3 + 7 plotted over real numbers

We observe that in an elliptic curve, there are two y-values for a given x. This observation will come in handy later.

Bitcoin curves are modular and discrete

In Bitcoin, we don’t use real numbers at all.
Instead, all numbers live inside a finite field — integers modulo a large prime pp.

A finite field Fp\mathbb{F}_p (where pp is prime) is the set:

{0,1,2,,p1}\{0, 1, 2, \ldots, p-1\}

where addition, subtraction, multiplication, and division are all done modulo pp.

Why must pp be prime?

Because when pp is prime, every non-zero element in Fp\mathbb{F}_p has a multiplicative inverse:

x0,  x1(modp)\forall x \neq 0,\ \exists \space x^{-1} \pmod p

That means division always works.
This is what makes Fp\mathbb{F}_p a proper field — and elliptic curve formulas rely heavily on division.

The curve equation modulo pp

Bitcoin’s curve is not the real-number curve that we drew above y2=x3+7y^2 = x^3 + 7.
It is the modular version:

y2x3+7(modp)y^2 \equiv x^3 + 7 \pmod p

That single change — doing everything mod pp — completely changes the geometry:

  • you only get a finite set of valid integer points (x,y)(x, y)
  • there is no smooth curve anymore
  • arithmetic wraps around at pp
  • there are no fractional coordinates

So what does the curve look like mod pp?

Instead of a continuous curve, you get a cloud of discrete points:

Elliptic curve points plotted over a finite field (discrete modular points)

At this point, the curve has boiled down into a finite set of points — and now we can define algebra on them. Even though it looks random, this set of points forms a group with a special addition rule. That group structure is what Bitcoin uses for public keys and signatures. Your public key is in fact, one of those points!

Terminologies: Curve, Point, and Scalar

To build Bitcoin signatures from scratch, we’ll use a few core objects and operations. All of this is discussed in the context of secp256k1.

1. Prime field modulus: pp

All coordinate math is defined over finite field Fp\mathbb{F}_p:

  • valid coordinates are integers in the range 0x,y<p0 \le x,y < p
  • arithmetic “wraps around” mod pp

p=2256232977p = 2^{256} - 2^{32} - 977
The same can be represented in:

Hex = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
Dec = 115792089237316195423570985008687907853269984665640564039457584007908834671663

2. Any point: P=(x,y)P = (x,y)

A point is a pair (x,y)(x,y) that satisfies the curve equation mod pp. Therefore, it's valid if and only if:

y2x3+7(modp)y^2 \equiv x^3 + 7 \pmod p

3. Point at infinity: O\mathcal{O}

There’s a special point called the point at infinity, written O\mathcal{O}. It behaves like the identity element of addition:

P+O=PP + \mathcal{O} = P

4. Point addition: P+QP + Q

Elliptic curve points can be “added” using a special algebraic rule. This addition has important properties:

  • it stays on the curve (closure)
  • it is associative
  • there is an identity element O\mathcal{O}

This is what makes elliptic curve points form a group. The algebraic process graphically is shown below:

P + Q
Addition of two points on the curve
2P
Doubling a point on the curve

5. Point negation / inverse: P-P

Every point P=(x,y)P=(x,y) has an inverse:

P=(x,ymodp)-P = (x, -y \bmod p), such that P+(P)=OP + (-P) = \mathcal{O}

6. A scalar: kk

A scalar is just an integer used to repeatedly add a point.

We use scalars in two places:

  • mod pp for field arithmetic (coordinates and slopes)
  • mod nn for group scalars (private keys and signatures)

7. Scalar multiplication: kPkP

Scalar multiplication means:

kP=P+P++P(k times)kP = P + P + \cdots + P \quad (k \text{ times})

This is the core operation behind Bitcoin keys:

  • easy to compute forward
  • hard to undo (discrete log problem)

8. Generator point: GG

The generator point GG is a fixed point on the curve chosen by the standard. It generates the subgroup used in Bitcoin.

Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240
Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424

In practice, private key is a scalar dd while public key is the point P=dGP = dG.

9. Group order: nn

If you add GG to itself nn times, you get the identity point.

nG=OnG = \mathcal{O}

The number nn is called the order of the subgroup generated by GG.

Hex = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
Dec = 115792089237316195423570985008687907852837564279074904382605163141518161494337

Bitcoin requires scalars to be in this range, 1dn11 \le d \le n-1 and most signature math happens modulo nn.

Generating a Public/Private Keypair

Private Key: Any scalar 1dn11 \le d \le n-1
Corresponding public key: P=dGP = dG, a point on the secp256k1 curve.

d = secrets.randbelow(n)
P = d * G

This operation is easy to compute, but hard to reverse. Mathematically, given PP and GG, it's infeasible to compute dd, where P=dGP = dG.

SEC1 Public Key Serialization (used in ECDSA)

The points are generated modulo pp which is in 256 bits or 32 bytes. Therefore, x or y coordinate values will be of 32 bytes.

Uncompressed Serialization (65 bytes)

04 || x(32 bytes) || y(32 bytes)

Compressed Serialization (33 bytes)

For a given x, there can be ONLY two values of y, because y=±x3+7y = \pm\sqrt{x^3+7}

(03|02) || x(32 bytes)
 
03 = odd y
02 = even y

BIP340 X-Only Public Key Serialization (used in Schnorr/Taproot)

BIP340 enforces a canonical rule:

The public key point is always interpreted as the point with even yy

Thereby, if public key P=dGP = dG has an odd y value (P.yP.y is odd), then the private key is negated to d=ndd' = n - d, giving P=dG=PP' = d'G = -P which has even yy.

The serialization here will only be the x value, so 32 bytes.

x(32 bytes)

Elliptic Curve Digital Signature Algorithm (ECDSA)

ECDSA is the signature scheme Bitcoin used from day one (and it’s still widely used today). It lets you prove:

I know the private key dd corresponding to public key P=dGP=dG,
without revealing dd.

ECDSA Signing Algorithm

The signer has:

  • Private Key dd
  • Public Key P=dGP = dG
  • Message Hash zz (For Bitcoin, this is the transaction sighash)

The signature is the pair (r,s)(r, s)

Step 1: Generate a secret nonce kk

k[1,n1]k \in [1, n-1] - kk must be unpredictable - kk must never repeat - kk must remain secret

In production Bitcoin, this is done deterministically using RFC6979 k=f(d,z)k = f(d, z)

Step 2: Compute the nonce point RR

R=kGR = kG

Take its x-coordinate: r=Rxmodnr = R_x \bmod n

Step 3: Compute the signature scalar ss

The signing formula is:

s=k1(z+rd)modns = k^{-1}(z + rd) \bmod n

The final ECDSA signature is: (r,s)(r, s)

def ecdsa_sign(z: int, d: int, G: Point) -> Tuple[int, int]:
    """
    Sign a message hash interpreted as integer z.
    Returns (r, s) as integers in [1, n-1].
    """
    n = G.curve.n
 
    if n is None:
        raise ValueError("Curve must have order n")
    if not (1 <= d < n):
        raise ValueError("Invalid private key d")
 
    z = z % n # message should also be mod n
 
    while True:
        # Random Nonce is fine for learning but not production
        # because what if nonce reused because of randomness?
        # k = secrets.randbelow(n)
 
        # RFC6979 Deterministic Nonce
        k = rfc6979_generate_nonce(d, z, n)
 
        R = k * G
        if R.is_infinity(): continue
 
        r = R.x % n
        if r == 0: continue
 
        k_inv = Scalar(k, n).inv().value
        s = (k_inv * (z + r * d)) % n
        if s == 0: continue
 
        # The signature is NOT a point, just a pair of values
        return (r, s)

ECDSA Verification Algorithm

The verifier has:

  • signature (r,s)(r,s)
  • message hash zz
  • public key PP

They want to verify the signature without knowing dd.

Step 1: Range checks

  • r[1,n1]r \in [1, n-1]
  • s[1,n1]s \in [1, n-1]

Otherwise reject

Step 2: Compute inverse of ss

w=s1modnw = s^{-1} \bmod n

Step 3: Compute two scalars

u1=zwmodnu_1 = zw \bmod n u2=rwmodnu_2 = rw \bmod n

Step 4: Reconstruct the point XX

X=u1G+u2PX = u_1G + u_2P

Reject if X=OX = \mathcal{O}.

Step 5: Compare x-coordinate

v=Xxmodnv = X_x \bmod n

The signature is valid iff v=rv = r

def ecdsa_verify(z: int, sig: Tuple[int, int], P: Point, G: Point) -> bool:
    """
    Verify ECDSA signature (r,s) for message hash integer z against public key P.
    """
    r, s = sig
    n = G.curve.n
    if n is None:
        raise ValueError("Curve must have order n")
 
    if P.is_infinity() or (P.curve != G.curve):
        return False
    if not (1 <= r < n and 1 <= s < n):
        return False
 
    z = z % n # message should be mod n
 
    s_inv = Scalar(s, n).inv().value
 
    u1 = (z * s_inv) % n
    u2 = (r * s_inv) % n
 
    X = (u1 * G) + (u2 * P)
    if X.is_infinity():
        return False
 
    return (X.x % n) == r

ECDSA Problems

Bitcoin, historically, had to deal with two classes of problems.

1. Nonce reuse leaks the private key

s=k1(z+rd)modns=k^{-1}(z+rd) \bmod n
sk=z+rdmodn\Rightarrow sk = z+rd \bmod n

Now, suppose you sign s1s_1 and s2s_2 with the same nonce kk.

s1k=z1+rdmodns_1k = z_1 + rd \bmod n
s2k=z2+rdmodns_2k = z_2 + rd \bmod n

Subtract:

(s1s2)k=(z1z2)modn(s_1-s_2)k = (z_1-z_2) \bmod n
    k=(z1z2)(s1s2)1modn\implies k = (z_1-z_2)(s_1-s_2)^{-1} \bmod n

And once kk is known,

Private Key d=(skz)r1modnd = (sk - z)r^{-1} \bmod n

Why Deterministic Nonces?

If kk is generated randomly, even a tiny flaw in the RNG (biased bits, predictable state, or VM snapshot reuse) can leak the private key. RFC6979 removes this risk entirely by deriving kk deterministically from the private key and message: k=f(d,z)k = f(d, z).

This means:

  • Same (d,z)(d, z) always produces the same kk — no randomness needed
  • Different messages produce different kk values — no reuse
  • The nonce is unpredictable to attackers who don't know dd

2. Signature Malleability

If (r,s)(r,s) is a valid ECDSA signature, then (r,ns)(r, n-s) is also valid.

r, s = sig = ecdsa_sign(z, d, G)
 
assert ecdsa_verify(z, (r, s), P, G) # expected to pass
assert ecdsa_verify(z, (r, n - s), P, G) # NO ERROR!

Reasons Bitcoin moved away from ECDSA (towards Schnorr / Taproot)

1. Transaction ID Malleability: (pre- SegWit)

Before SegWit, the transaction ID was computed as: txid=sha256d(serialized-transaction)txid = sha256d(\text{serialized-transaction})

and the serialization included scriptSig, which contains the signature bytes. So any mutation to signature bytes could change the txid even though the spend remained valid.

a) DER encoding malleability (non-canonical encodings)
  • The same mathematical ECDSA signature (r, s) could be encoded in multiple valid (but non-canonical) ways in DER
  • This allowed third parties to mutate signature bytes, and therby mutate txid

It was fixed by BIP66 (Strict DER encoding) (consensus rule).

b) s-value malleability (high-s / low-s)

For any valid signature (r,s), this is also valid: (r, n - s)

This produces a different signature that still verifies, again allowing txid mutation pre-SegWit.

Mitigated by enforcing low-S signatures as policy/standardness (often discussed in context of BIP62), and later enforced more strictly for SegWit spends.

c) SegWit solved it structurally

SegWit moved signatures into the witness, so:

  • txid no longer commits to signatures
  • malleability no longer breaks txid-based chaining

Fixed by SegWit (BIP141) at the protocol level.

2. Nonce Fragility / Leaking Private Key (discussed above)

3. No clean key aggregation

ECDSA's multiplicative structure (s=k1(z+rd)s = k^{-1}(z + rd)) doesn't compose nicely. If Alice and Bob want to create a joint signature, they can't simply add their individual signatures together.

Schnorr's linear structure (s=k+eds = k + ed) is fundamentally different. If Alice signs with s1=k1+ed1s_1 = k_1 + e \cdot d_1 and Bob signs with s2=k2+ed2s_2 = k_2 + e \cdot d_2, then s1+s2=(k1+k2)+e(d1+d2)s_1 + s_2 = (k_1 + k_2) + e(d_1 + d_2) is a valid signature for the aggregated key (d1+d2)G=P1+P2(d_1 + d_2)G = P_1 + P_2.

This linearity enables protocols like MuSig2, where multiple parties can create a single signature that's indistinguishable from a regular signature — enabling efficient multisig, atomic swaps, and more.

4. Additional malleability vectors

Beyond the (r,ns)(r, n-s) malleability discussed above, ECDSA has other subtle malleability issues:

  • The same (r,s)(r, s) values can sometimes be valid for different messages if rr values collide after mod nn reduction (extremely rare but theoretically possible)
  • Implementation bugs around DER encoding created additional attack surface

Schnorr's design eliminates these by construction: the even-yy requirement and the way the challenge hash commits to the full message remove ambiguity.

5. No efficient batch verification

When a Bitcoin node receives a block with hundreds of transactions, it must verify hundreds of signatures. With ECDSA, each verification requires separate elliptic curve operations.

Schnorr verification is a linear equation: check that sG=R+ePsG = R + eP, or equivalently, sGePR=OsG - eP - R = \mathcal{O}.

For multiple signatures, you can combine these checks: i(siGeiPiRi)=O\sum_i (s_i G - e_i P_i - R_i) = \mathcal{O}

With random coefficients (to prevent cancellation attacks), this becomes a single multi-scalar multiplication — which is much faster than nn separate verifications. Batch verification can speed up block validation by 2-3x.


Schnorr Signatures (BIP340)

Schnorr signatures are what Bitcoin uses for Taproot outputs. They are cleaner, more structured, and more composable.

Schnorr signatures look like: (rx,s)(r_x, s)

where:

  • rxr_x is the x-coordinate of nonce point RR
  • ss is a scalar modulo nn

Schnorr Signing Algorithm

The signer has:

  • Private Key dd
  • Public Key P=dGP = dG
  • Message mm (32 bytes)

Step 1: Compute public key and enforce even-y

P=dGP = dG

If PyP_y is odd, replace dndd \leftarrow n-d and PPP \leftarrow -P so that PyP_y is always even.

Step 2: Generate nonce kk deterministically

BIP340 uses tagged hashes with optional aux randomness: k=f(d,m,aux)k = f(d, m, aux)

Step 3: Compute nonce point RR

R=kGR = kG

If RyR_y is odd, replace knkk \leftarrow n-k and RRR \leftarrow -R so that RyR_y is always even.

Step 4: Compute challenge ee

e=H(rx,px,m)modne = H(r_x, p_x, m) \bmod n

Step 5: Compute signature scalar ss

s=k+edmodns = k + ed \bmod n

The Schnorr signature is (rx,s)(r_x, s), serialized as

rx(32 bytes) || s(32 bytes)

Schnorr Verification Algorithm

The verifier has:

  • x-only public key pxp_x
  • signature (rx,s)(r_x, s)
  • message mm

Step 1: Range checks

  • rx[0,p1]r_x \in [0, p-1]
  • s[0,n1]s \in [0, n-1]

Step 2: Lift x-only public key

Compute P=lift_x(px)P = lift\_x(p_x): solve y2x3+7(modp)y^2 \equiv x^3 + 7 \pmod p and choose the even yy.

Step 3: Compute challenge

e=H(rx,px,m)modne = H(r_x, p_x, m) \bmod n

Step 4: Reconstruct nonce point

R=sGePR' = sG - eP

Reject if R=OR' = \mathcal{O}.

Step 5: Final checks

Signature is valid iff RyR'_y is even and Rx=rxR'_x = r_x.

You can take a look at the implementation of Schnorr Signatures in the notebook.

Tagged Hashes in BIP340

BIP340 uses tagged hashes for domain separation:

  • BIP0340/aux
  • BIP0340/nonce
  • BIP0340/challenge

Htag(x)=sha256(sha256(tag)sha256(tag)x)H_{tag}(x) = sha256(sha256(tag) \| sha256(tag) \| x)

Schnorr vs ECDSA: No Malleability

In ECDSA, (r,s)(r, s) and (r,ns)(r, n-s) both verify.

In BIP340 Schnorr, signatures are canonicalized using the even-yy requirement on RR, so the "flip ss" trick does not produce a second valid signature.

A Note on Quantum Computing

Everything in this post assumes classical computers. A sufficiently powerful quantum computer running Shor's algorithm could solve the discrete log problem in polynomial time, breaking ECDSA and Schnorr alike.

How worried should we be?

  • Current state: As of 2026, the largest quantum computers have a few thousand qubits. Breaking secp256k1 would require millions of stable, error-corrected qubits.
  • Timeline estimates: Experts disagree wildly — some say 10 years, others say 30+, some say never at scale.
  • Bitcoin's exposure: Only public keys that have been revealed (by spending) are vulnerable. Addresses that have never spent are protected by the hash function layer (RIPEMD160 + SHA256), which quantum computers don't break as efficiently.
  • Mitigation: Post-quantum signature schemes exist (lattice-based, hash-based) and could be added to Bitcoin via soft fork if quantum computers become a real threat.

The cryptographic community is actively researching post-quantum alternatives, but for now, elliptic curves remain secure against all known attacks.

Takeaways

The security of Bitcoin rests on a beautiful mathematical foundation: the difficulty of reversing scalar multiplication on elliptic curves. From a 256-bit random number (your private key), we derive a public key that's trivial to compute but impossible to reverse. Signatures prove you know the private key without revealing it, using the clever trick of committing to randomness before being challenged.

ECDSA served Bitcoin well for over a decade, but its multiplicative structure created subtle problems: fragile nonces, signature malleability, and no path to aggregation. Schnorr's linear design (s=k+eds = k + ed) elegantly solves these issues while enabling new capabilities like efficient multisig and batch verification.

Key concepts:

  • Fp\mathbb{F}_p for coordinates/slopes, Zn\mathbb{Z}_n for private keys/signature scalars
  • Modular inverse exists for all non-zero values in both fields
  • A point has a group inverse P-P, not a modular inverse
  • Recovering dd from P=dGP=dG is the Elliptic Curve Discrete Log Problem (infeasible)
  • ECDSA nonce failure is catastrophic (leaks private key)
  • Schnorr signatures are smaller, canonical, and better for aggregation
  • Tagged hashes provide domain separation
  • aux is signer-only randomness to harden nonce generation