7. The hypertree¶
Where we are
Left over from chapter 6: 2^h FORS public keys need
certifying under a single root, but computing the root of a height-63 tree
requires visiting 2^63 leaves.
Goal: the same commitment, computable this century.
Why one tall tree is impossible¶
From chapter 3: a Merkle root
requires every leaf. Height 63 means 2^63 ≈ 9.2 × 10^18 leaves, each a full
WOTS+ key generation of ~500 hash invocations.
That is on the order of 10^21 hash invocations for a single key generation. At
a billion hashes per second it finishes in roughly 300,000 years. Key generation
must take milliseconds.
Certification chains¶
The insight: a Merkle tree is not the only way to bind one key to another. A signature is also a binding. And we already have a signature scheme — XMSS.
So instead of one tree of height 63, stack d trees of height h' = h/d. Each
tree's leaves sign the roots of the trees below.
For SLH-DSA-128s: h = 63, d = 7, h' = 9. Seven layers of height-9 trees.
flowchart TD
ROOT["PK.root — the public key<br/>root of the ONE top-layer tree"]:::pk
ROOT --- T2["layer d-1: one XMSS tree, height h'"]
T2 -->|"a leaf signs"| T1["layer d-2: root of an XMSS tree"]
T1 -->|"a leaf signs"| T0["layer 0: root of an XMSS tree"]
T0 -->|"a leaf signs"| F["the FORS public key"]
F -->|"signs"| MSG["md — your message digest"]
classDef pk fill:#2d7d46,color:#fff
Trust flows down the chain: the verifier trusts PK.root; the top tree's leaf
vouches for a layer-d-2 root; that root's leaf vouches for the next; the
layer-0 leaf vouches for the FORS public key; FORS vouches for the message.
Each link is a real XMSS signature — a WOTS+ signature plus an h'-node
authentication path.
The cost collapse¶
Key generation now needs only the top layer's single tree:
The lower layers are never built at key-generation time. They do not need to be:
they are derived from SK.seed on demand, and any particular lower tree is
only ever built when a signature actually needs it.
Signing builds one tree per layer — the specific tree on the path from the chosen leaf to the root:
Exponential to linear-in-d. That is the whole trick.
| Approach | Keygen leaves | Signing leaves |
|---|---|---|
One tree, h = 63 |
2^63 (infeasible) | 2^63 |
7 trees, h' = 9 |
512 | 3,584 |
Index arithmetic¶
idx_tree and idx_leaf come out of the message digest
(chapter 5). Together they name
a leaf in the bottom layer. Climbing a layer consumes h' bits:
at each layer:
sign with leaf idx_leaf of tree idx_tree
then:
idx_leaf = idx_tree & (2^h' - 1) # low h' bits pick the leaf above
idx_tree = idx_tree >> h' # the rest picks that tree
After d layers idx_tree is zero — there is exactly one tree at the top, and
its root is PK.root. That idx_tree reaches zero after exactly d shifts is
the structural reason h = h' · d must hold, a relation this library
checks at compile time.
Verification¶
Verification does not need PK.root until the very end. Each layer's
xmss_pkFromSig returns a root (chapter 4), and that root becomes
the message verified at the next layer up:
node = FORS public key # reconstructed from the signature
for layer in 0 .. d-1:
node = xmss_pkFromSig(layer's signature, node, idx_leaf, idx_tree)
shift the indices as above
return node == PK.root # the single comparison
One comparison, at the top. Every layer below is a pure reconstruction. This is
why pkFromSig returns a value instead of a boolean — the composability is the
design.
The s/f split, finally explained¶
h is fixed by the security target (roughly, 2^64 signatures need h ≈ 64).
But d is free, and h' = h/d, so choosing d trades signing speed against
signature size.
d |
h' |
Signing cost ∝ d·2^h' |
Signature ∝ (h + d·len)·n |
|
|---|---|---|---|---|
| 128s ("small") | 7 | 9 | 7 × 512 = 3,584 | 7,856 B |
| 128f ("fast") | 22 | 3 | 22 × 8 = 176 | 17,088 B |
- Many shallow layers (
f) — each tree is tiny and cheap to build, so signing is ~20× faster. But every layer contributes a whole WOTS+ signature (len × n≈ 560 bytes), and 22 layers of that dominates the signature size. - Few deep layers (
s) — only 7 WOTS+ signatures to carry, so the signature is 2.2× smaller. But each tree has 512 leaves rather than 8, so signing is much more expensive.
Measured on this project's benchmarks (Apple M3 Pro, ReleaseFast), SHAKE-128s
signing runs around 368 ms against 128f's 19 ms — a factor of ~20, in exchange
for 2.2× less signature. The two variants are not "one is better"; they are two
positions on a genuine curve.
How to choose
Signature travels over a constrained link, or is stored per-record, and
signing is rare → s. Signing happens often, or latency matters, and the
bytes are affordable → f. Both give the same security level.
Two algorithms¶
FIPS 205 §7, implemented in src/hypertree.zig:
| Algorithm | Section | Purpose |
|---|---|---|
ht_sign |
§7.1, Alg 12 | d XMSS signatures up the chain |
ht_verify |
§7.2, Alg 13 | Fold d reconstructions, compare to PK.root |
HT_SIG is d XMSS signatures — d × (len·n + h'·n) = (h + d·len)·n bytes,
the largest term in the total.
Chapter 7 outcome
Stacking d XMSS trees of height h' replaces one height-h tree, turning
exponential key generation into 2^h' and signing into d · 2^h'. Choosing
d is exactly the s/f trade.
Nothing is left broken. Every piece is now in place.