X-only Public Keys

Remember from the Pay to Taproot section, the scriptPubKey is:

P2TR scriptPubKeyOP_1 OP_PUSHBYTES_32 <32-byte tweaked public key>

Did you notice the scriptPubKey here only has 32 bytes !!

Q, our tweaked public key, is a point on an elliptic curve.

A point on an elliptic curve should have (x,y)(x, y). So normally we would have 64 bytes: 32 for x and 32 for y.

In Schnorr we use only the x coordinate, for compactness. We store those 32 bytes and throw the rest away.

The question you might be asking is: how can we compute the y coordinate?

The curve gives it back to us

Secp256k1 is defined by:

y2=x3+7(modp)y^2 = x^3 + 7 \pmod p

So if we have xx, we can calculate yy:

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

y=x3+7y = \sqrt{x^3 + 7}

Done.

yy is a square root, so it has two solutions:

  • yy
  • y-y

Two solutions?

Same as 9\sqrt 9 is both +3+3 and 3-3.

And since our curve is on a finite field over pp, there are no negative numbers. Every element xx and yy is an integer in [0,p1][0, p-1].

So when we write y-y we have to reduce it into that range:

ymodp=py-y \bmod p = p - y

The field prime p

pp is the field prime of secp256k1.

p=2256232977p = 2^{256} - 2^{32} - 977

So the two solutions are:

  • yy
  • pyp - y

Now we need to know which of these two is the correct one, so we can reconstruct the point (x,y)(x, y).

How do we know?

The parity trick

One of those two yy values is even and the other is odd. They can't be both even or both odd.

Why? Because pp is an odd number.

p=2256232977(odd)p = 2^{256} - 2^{32} - 977 \quad\text{(odd)}

  • if yy is odd → pyp - y is even (odd − odd = even)
  • if yy is even → pyp - y is odd (odd − even = odd)

So our challenge is just to know whether our yy is odd or even.

The solution is simple. BIP340 has this rule:

an x-only public key always means the point with the even y coordinate

So whenever we read 32 bytes as a public key, we take the point whose y is even.

lift_x (optional)


Three ways to encode a public key

Bitcoin already had two formats before taproot. Worth seeing all three together.

Uncompressed: 65 bytes

0x04 || x || y

Store both coordinates. 32 + 32 = 64 bytes, plus one prefix byte 0x04 meaning "uncompressed". This is the legacy format.

Compressed: 33 bytes

0x02 || x y is even0x03 || x y is odd

We know y is only worth one bit, so throw y away and keep the bit. That is the whole job of the prefix byte. This is what you see in P2PKH, P2WPKH, xpubs.

But look at it: a full byte to carry 1 bit. 7 bits wasted on every key.

x-only: 32 bytes

x

BIP340 turns that bit into a rule instead of data. Nothing left to store.

formatsizehow the y parity is known
uncompressed65 bytesy is written out in full
compressed33 bytesstored in the prefix byte
x-only32 bytesfixed by the rule: always even

An x-only key is just a compressed key with the prefix removed. That is why BIP340 picked "even" and not some other rule: it keeps taproot compatible with every key generation system that already exists.


Don't we lose half the keys ?

Everyone asks this. The answer is no.

Say your private key dd produces a point PP with an even yy. Then we don't have to do anything, we already have what BIP340 requires.

But if dd produces a point with an odd yy, we can't use PP.

Watch what happens if we use ndn - d instead:

nG=O(the point at infinity)n \cdot G = O \quad\text{(the point at infinity)}

What is n?

nn is the order of the group: adding GG to itself nn times brings you back to OO. That is the definition of nn, and it is why private keys live mod nn and not mod pp.

Careful not to mix them: pp is the modulus for the coordinates, nn is the modulus for the private keys.

(nd)G=nGdG=OP=P(n - d)\cdot G = n\cdot G - d\cdot G = O - P = -P

Now:

P=(x,y)=(x,y)-P = -(x, y) = (x, -y)

Reducing into the field:

(x,y)modp=(x,py)(x, -y) \bmod p = (x, p - y)

So the private key ndn - d gives us an even-y point with the same 32 bytes of xx.

We call this negating the private key.

Every xx is still reachable. You never lose a key, you just sometimes flip the private key before signing:

d = d if P has even y n d if P has odd y

What it costs

lift_x needs a square root, which is a full exponentiation. That makes verification around 10% slower than if we had stored the full 64-byte point. BIP340 says this openly and picks compactness anyway.

That is the right call for Bitcoin. A node stores the output forever, but verifies it once.

Size is not the only reason though. Having one unambiguous point per 32 bytes is also what makes batch verification possible: checking thousands of signatures at once. We'll come back to that at the end of the module.


Recap

65 bytes 0x04 || x || y uncompressed33 bytes 0x02/0x03 || x compressed (parity as data)32 bytes x x-only (parity as a rule)
  • A point needs x and y, but y is only worth one bit
  • The curve equation recovers y, up to a sign
  • BIP340 pins the sign by convention: always even
  • Negating the private key gets you there when your key lands on the wrong side

One last thing: it isn't only public keys. A Schnorr signature is R.x || s, 32 + 32 = 64 bytes, and that R is x-only for exactly the same reason.

Which is the next lesson.

Suggest Edits