§ 02

§ 02 · Cryptography

XMSS

Signatures made only of hashes, built to outlast quantum computers

XMSS is a way to sign messages using nothing but a hash function. No curves, no hard number puzzles: if the hash function is safe, the signatures are safe.

That matters because of quantum computers. A large enough one could break the elliptic-curve math behind Schnorr signatures, but it would barely dent a good hash function. Pearl adds XMSS as an optional, post-quantum way to lock coins.

Why would hashes survive a quantum computer?

Shor’s algorithm lets a quantum computer solve the discrete-logarithm problem, which is what keeps a private key hidden behind its public key. That would break every elliptic-curve signature.

Hash functions have no such weakness. The best known quantum attack on them, Grover’s algorithm, only takes a square root off the work: a 256-bit hash still needs about 2¹²⁸ quantum steps to invert. So a signature that relies only on hashes stays secure.

XMSS is standardised in RFC 8391. It’s built in three layers.

Layer 1: how can a hash make a signature?

Start with a secret random value and hash it over and over, 15 times. You get a chain of 16 values. Publish only the last one.

To “sign” a number d between 0 and 15, reveal the value d steps along the chain. Anyone can check it: hash it 15 − d more times and see if it lands on the published end.

0515secretrevealedpublicverifier hashes 10 more times
A Winternitz chain with w = 16. Signing the digit 5 reveals the value after 5 hashes. The verifier hashes it 10 more times and must reach the public end.

Hashing forward is easy. Hashing backward is impossible, because hashes are one-way. That asymmetry is the whole signature. This kind of scheme is a one-time signature: once part of a chain is revealed, the key mustn’t sign again.

Layer 2: how do you sign a whole message?

Use many chains, one per digit. This is the Winternitz scheme (WOTS+). With chains of 16 values, each chain signs one hexadecimal digit.

A 32-byte digest has 64 hex digits, so it needs 64 chains. But there’s a catch: anyone holding a signature can hash further along any chain, raising that digit, and sign a different message. Three checksum digits stop that.

That gives 64 + 3 = 67 chains, and a one-time signature of 67 × 32 = 2,144 bytes.

Layer 3: how does one key sign 32 times?

One-time keys are awkward: you’d need a new public key for every signature. XMSS fixes this with a Merkle tree.

Make 32 one-time keys. Hash each one’s public part into a leaf, and build a tree of height 5 over the 32 leaves. The root becomes the long-term public key.

To sign, use the next unused one-time key, and attach its authentication path: the 5 sibling hashes from its leaf to the root. The verifier rebuilds the one-time public key from the signature, climbs the path, and checks the root.

01234567rootused keyauthentication path
Simplified: a tree of height 3 (8 one-time keys). Pearl's tree has height 5, 32 leaves. Signing with leaf 2 needs its authentication path: the highlighted siblings.

So a full XMSS signature is: the leaf’s index, a random value r used to hash the message, the 67-chain one-time signature, and the authentication path.

4+32+67×32+5×32=2,340 bytes4 + 32 + 67 \times 32 + 5 \times 32 = 2{,}340 \text{ bytes}
Signature
index + randomness r + one-time signature + authentication path.

What are Pearl’s exact parameters?

Pearl uses a configuration its code calls XMSS-SHAKE256_5_256, which is not one of RFC 8391’s named sets:

Hash
SHAKE256
32-byte outputs (n = 32)
Winternitz w
16
67 chains (64 + 3)
Tree height
5
one layer (d = 1)
Signatures per key
32
2⁵ leaves
Public key
64 bytes
root + public seed
Signature
2,340 bytes
5 × 468 in a witness

The hash is SHAKE256, from the SHA-3 family. The secret key is 128 bytes plus a 4-byte index that counts the signatures used.

How does Pearl use XMSS?

Through tapscript, Pearl’s script language for Taproot and P2MR outputs:

  • The opcode. OP_CHECKXMSSSIG takes five 468-byte signature pieces and a 64-byte public key from the stack. Stack items are capped at 520 bytes, which is why the 2,340-byte signature is split in five.
  • What is signed. The same tapscript signature digest that Schnorr signatures sign (BIP-342), always with the default sighash type.
  • Failure is final. An invalid XMSS signature makes the whole script fail; it never just pushes “false”. Each check also costs 1,000 units of the input’s script budget.
  • The script leaf. The wallet’s leaf is 40 <64-byte public key> de: push 64 bytes, then OP_CHECKXMSSSIG. 66 bytes in all.

The reference wallet (Oyster) adds such a leaf only when asked: getnewaddress with its pq flag set to true. The XMSS key comes from a separate branch of the wallet’s seed (purpose 222, which is 0xde, the opcode’s byte), stretched with HKDF-SHA256 into a 64-byte private seed and a 32-byte public seed.

The result is a normal-looking Taproot address whose script tree has one XMSS leaf. Here is a real one, made by Oyster from the public BIP-39 test words:

Part Value
XMSS public key (64 bytes) 5e70da755554f576d008eb89bfa9ffa8965e08cfdcfa48aaf85d59499792497a4b9e933bfc2f4695395f43339d799434741e36742ce2f0df31994e54b25ee8f8
Address prl1ph89v46drqp3rd3cfaylqqwnynuu2cgykdgqd60094qxp4jl4sd7ssg6v5j

What are the honest limits today?

XMSS in Pearl is real but early. As of v1.5.2:

  • It’s opt-in. Default addresses, change addresses and everything the desktop wallet creates have no XMSS leaf. They are protected by Schnorr alone.
  • The key path stays open. An XMSS address is a Taproot address, so it can still be spent with an ordinary Schnorr signature, and that’s how the wallet spends it. Someone who could break secp256k1 could use that path too. A P2MR output has no key path, but the wallet doesn’t create P2MR addresses.
  • No wallet signs with it yet. The research found no production code that makes XMSS signatures; only a test calls the signer. The XMSS leaf is an emergency path for now.
  • It’s big. An XMSS script-path spend’s witness is roughly 2,457 bytes, about 615 vbytes, against about 16.5 vbytes for a Schnorr key-path witness (an estimate from the research, not a measured transaction).
Why the opcode number is 0xde

0xde is 222. The wallet derives XMSS keys under BIP-32 purpose 222′, matching the opcode byte, in the same way that Pearl’s normal keys live under purpose 86′ (BIP-86). The opcode is defined in opcode.go (node/txscript/opcode.go:2020-2105 at v1.5.2, on GitHub), and the parameters in xmss.cpp (xmss/src/xmss.cpp:11-31 at v1.5.2, on GitHub):

xmss/src/xmss.cpp C++
// config: XMSS-SHAKE256_5_256 (not in RFC 8391)
static constexpr xmss_params default_params = {
    .func = XMSS_SHAKE256,
    .n = 32,                   // hash input & output length
    .wots_w = 16,              // Winternitz parameter
    .wots_len = 67,            // len1 + len2,
    .full_height = 5,          // allows 32 signatures
    .d = 1,                    // no MT recursion
    .sig_bytes = 2340, // wots_sig_bytes + index_bytes + (height + 1) * n,
    .pk_bytes = 64,    // 2 * n
    // ...
};

The codex

Suggested

Up and down arrows move through the results, Enter opens one, Escape closes. Anywhere on the site, slash or Command K opens this search, and the square brackets [ and ] go to the previous and next page.