Skip to content

Chapter 17: SLH-DSA (FIPS 205) from scratch

Chapter 16 built FORS and hypertrees using bare SHA-256 calls. Every hash in that construction, whether it generated a secret leaf, compressed a Merkle node, or derived a WOTS+ chain value, called the same sha_n() function with no structural separation between contexts. A hash used to derive a FORS leaf at position (j,i)(j, i) was indistinguishable from a hash used at a different position or in a different subtree, because the only difference was the data fed in. FIPS 205 closes this gap with two additions (National Institute of Standards and Technology, 2024). First, a 32-byte address structure (ADRS) encodes the exact position of every address-dependent hash call. Second, tweakable hash functions take the ADRS as a tweak so that identical inputs at different positions produce different outputs.

The result is SLH-DSA: a stateless, hash-only digital signature scheme standardized by NIST in August 2024. The construction is the same FORS-at-the-bottom-of-a-hypertree architecture from Chapter 16, with domain separation added at every level. The security argument reduces entirely to properties of the hash function, in the targeted multi-target forms Chapter 18 states rather than plain collision resistance, with no lattice or number-theoretic assumptions.

The diagram below shows the components of an SLH-DSA signature. The message enters at the top, passes through Hmsg\mathbf{H}_{msg} to produce the FORS digest and tree/leaf indices, then FORS signs the digest at the selected position. The FORS public key is authenticated upward through dd hypertree layers, each contributing a WOTS+ signature and a Merkle authentication path. ADRS annotations show which type is active at each level.

SLH-DSA signature components. A vertical flow diagram showing the SLH-DSA signing process. The message at the top flows through H_msg to produce a digest. The digest splits into idx_tree, idx_leaf, and FORS message md. FORS signs md producing SIG_FORS and PK_FORS. PK_FORS enters the hypertree where d layers of WOTS+ sign it upward. The signature components R, SIG_FORS, and SIG_HT are collected on the right. Message M H_msg(R, PK.seed, PK.root, M) idx_tree idx_leaf md FORS sign(md) k trees, depth a FORS_TREE / FORS_PRF SIG_FORS PK_FORS HT layer 0: WOTS+ sign PK_FORS XMSS tree at idx_tree, leaf idx_leaf WOTS_HASH HT layers 1..d-1: sign roots upward TREE SIG_HT PK.root (verify) R (n bytes) SIG = R || SIG_FORS || SIG_HT
Figure 17.1. SLH-DSA signature components. H_msg produces the FORS digest, the tree index, and the leaf index. FORS signs the digest. The FORS public key is authenticated through d hypertree layers, each contributing a WOTS+ signature and a Merkle path.

The following code computes the public-key root of a toy SLH-DSA construction: it builds the WOTS+ public keys at each leaf of the top-layer XMSS tree and hashes them upward to PK.root. Later sections build the WOTS+, FORS, and hypertree signing components. The full keygen, sign, and verify pipeline lives in the companion package at solutions/ch17-slh-dsa/. WOTS+, FORS, XMSS, and the hypertree are SLH-DSA components, not FIPS 205-approved standalone signature schemes (National Institute of Standards and Technology, 2024). The parameters are not from FIPS 205. They are small enough that the root computation runs in under a second, and a full toy signature would be about 2 KB. The structure is identical to the real thing: ADRS-tagged tweakable hashes, WOTS+ with domain-separated chains, FORS with the F-function leaf separation, and a hypertree assembling the pieces.

import hashlib, struct
# --- Toy parameters (NOT FIPS 205) ---
n = 16 # hash output bytes
h, d = 9, 3 # total height, layers -> hp = 3
hp = h // d # subtree height
a, k = 3, 3 # FORS: t = 2^a = 8 leaves, k trees
w = 16 # Winternitz parameter
# --- ADRS (32-byte mutable address) ---
def new_adrs():
return bytearray(32)
def set_layer(ad, v):
struct.pack_into(">I", ad, 0, v)
def set_tree(ad, v):
ad[4:16] = v.to_bytes(12, "big")
def set_type_clear(ad, v):
struct.pack_into(">I", ad, 16, v)
ad[20:32] = b"\x00" * 12
def set_kp(ad, v):
struct.pack_into(">I", ad, 20, v)
def get_kp(ad):
return struct.unpack_from(">I", ad, 20)[0]
def set_chain(ad, v):
struct.pack_into(">I", ad, 24, v)
def set_hash_addr(ad, v):
struct.pack_into(">I", ad, 28, v)
def set_height(ad, v):
struct.pack_into(">I", ad, 24, v)
def set_idx(ad, v):
struct.pack_into(">I", ad, 28, v)
def compress(ad):
return bytes([ad[3]]) + bytes(ad[8:16]) + bytes([ad[19]]) + bytes(ad[20:32])
# --- Tweakable hashes (SHA-256 for n=16) ---
def tw_F(pk_s, ad, m):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + m).digest()[:n]
def tw_H(pk_s, ad, m1, m2):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + m1 + m2).digest()[:n]
def tw_T(pk_s, ad, m):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + m).digest()[:n]
def tw_PRF(pk_s, sk_s, ad):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + sk_s).digest()[:n]
# --- Keygen: compute top XMSS tree root ---
import math
lg_w = int(math.log2(w))
ell_1 = math.ceil(8 * n / lg_w)
mc = ell_1 * (w - 1)
ell_2 = math.ceil((math.floor(math.log2(mc)) + 1) / lg_w)
ell = ell_1 + ell_2
sk_seed = bytes.fromhex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")
sk_prf = bytes.fromhex("01020304050607080910111213141516")
pk_seed = bytes.fromhex("f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6")
def wots_pk(sk_s, pk_s, ad):
"""Compressed WOTS+ public key (n bytes)."""
kp = get_kp(ad)
sk_ad = bytearray(ad); set_type_clear(sk_ad, 5); set_kp(sk_ad, kp)
w_ad = bytearray(ad); set_type_clear(w_ad, 0); set_kp(w_ad, kp)
tmp = b""
for i in range(ell):
set_chain(sk_ad, i)
val = tw_PRF(pk_s, sk_s, sk_ad)
set_chain(w_ad, i)
for j in range(w - 1):
set_hash_addr(w_ad, j)
val = tw_F(pk_s, w_ad, val)
tmp += val
pk_ad = bytearray(ad); set_type_clear(pk_ad, 1); set_kp(pk_ad, kp)
return tw_T(pk_s, pk_ad, tmp)
def xmss_node(sk_s, i, z, pk_s, ad):
if z == 0:
set_type_clear(ad, 0); set_kp(ad, i)
return wots_pk(sk_s, pk_s, ad)
left = xmss_node(sk_s, 2*i, z-1, pk_s, bytearray(ad))
right = xmss_node(sk_s, 2*i+1, z-1, pk_s, bytearray(ad))
set_type_clear(ad, 2); set_height(ad, z); set_idx(ad, i)
return tw_H(pk_s, ad, left, right)
ad = new_adrs(); set_layer(ad, d - 1)
pk_root = xmss_node(sk_seed, 0, hp, pk_seed, ad)
pk = pk_seed + pk_root
print(f"Public key: {pk.hex()[:32]}... ({len(pk)} bytes)")
# ==> Public key: f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6... (32 bytes)

Every Python block this chapter prints is also a standalone file in the companion repository, under chapter-code/ch17/, one file per block. Appendix C covers the clone and the environment they run on.

The public key is 32 bytes: PK.seed (16 bytes) concatenated with PK.root (16 bytes). The keygen computed the root of the top-layer XMSS tree by recursively building WOTS+ public keys at each leaf and hashing them upward.

FIPS 205 approves twelve parameter sets: six SHA2 sets and six SHAKE sets. They collapse into six structural profiles across two axes: NIST security category (1, 3, 5) and signature-size profile (s for small, f for fast). The s sets minimize signature size at the cost of slower signing. The f sets minimize signing time at the cost of larger signatures and more verification work (National Institute of Standards and Technology, 2024). For blockchain use, the two numbers that matter are signature bytes per transaction (block throughput) and verification cost per node (Ethereum gas).

The four corner cases of the FIPS 205 size table:

Parameter setSignaturePublic keyCategory
SLH-DSA-128s7,856 B32 B1
SLH-DSA-128f17,088 B32 B1
SLH-DSA-256s29,792 B64 B5
SLH-DSA-256f49,856 B64 B5

Transaction signing is one-shot per spend, so the s sets are the natural default: the smaller signature reduces per-block pressure and the lower per-verification hash count reduces the Ethereum gas envelope. The f sets win only when signer-side latency dominates, and they cost both larger transactions and more verification work. The signature-byte budget anchors back to the secp256k1 baseline from Chapter 4, where a 64-byte Schnorr signature pairs with a 32-byte x-only public key under BIP-340 / BIP-341 (Wuille, Nick, & Ruffing, 2020; Wuille, Nick, & Towns, 2020). Chapter 37 develops the per-block migration tax this byte-budget gap produces, alongside the draft BIP-360 proposal (Pay-to-Merkle-Root), a soft-fork draft that targets long-exposure quantum resistance for Bitcoin outputs rather than a full post-quantum signature (Beast et al., 2024).

Four of SLH-DSA’s six hash-function roles, PRF, F, H, and T\mathbf{T}_\ell, take a 32-byte address among their inputs. The ADRS encodes the exact position of the value being computed within the overall tree structure: which hypertree layer, which subtree, which WOTS+ key, which chain, and which step within the chain (National Institute of Standards and Technology, 2024). The two message-hashing roles, PRFmsg\mathbf{PRF}_{msg} and Hmsg\mathbf{H}_{msg}, take no address; the tweakable hash functions section below returns to all six.

The 32-byte layout (all values big-endian) follows FIPS 205 Section 4.2:

BytesFieldMeaning
0 to 3layer addresswhich hypertree layer
4 to 15tree addresswhich subtree within that layer
16 to 19typewhich of the seven contexts below
20 to 23keypair addresswhich WOTS+ or FORS key, or padding
24 to 27chain addressor tree height, under TREE and FORS_TREE
28 to 31hash addressor tree index, under TREE and FORS_TREE

The seven type values are WOTS_HASH 0, WOTS_PK 1, TREE 2, FORS_TREE 3, FORS_ROOTS 4, WOTS_PRF 5, and FORS_PRF 6.

The diagram below shows the 32-byte ADRS with byte offsets and the type-dependent interpretation of bytes 20 through 31. Three common types are shown: WOTS_HASH (chain and hash addressing), TREE (height and index), and FORS_TREE (keypair, height, and index).

ADRS field layout. A horizontal bar showing the 32-byte ADRS structure. Bytes 0 to 3 are the layer address. Bytes 4 to 15 are the tree address (12 bytes). Bytes 16 to 19 are the type field. Bytes 20 to 31 change interpretation based on type. Three rows below show the type-dependent fields for WOTS_HASH, TREE, and FORS_TREE. ADRS (32 bytes, big-endian) layer 0-3 tree address (12 bytes) 4-15 type 16-19 context-dependent (12 bytes) 20-31 Type-dependent fields (bytes 20-31): WOTS_HASH (0): keypair 20-23 chain 24-27 hash addr 28-31 TREE (2): padding (0) 20-23 tree height 24-27 tree index 28-31 FORS_TREE (3): keypair 20-23 tree height 24-27 tree index 28-31
Figure 17.2. ADRS 32-byte field layout. Bytes 0 to 3 are the layer address, 4 to 15 the tree address, 16 to 19 the type, and 20 to 31 context-dependent. The three rows below show the type-dependent interpretation for WOTS_HASH, TREE, and FORS_TREE.

Setting the type field zeros bytes 20 through 31 (the context-dependent fields). This prevents stale values from a previous hash call from leaking into a new context.

import hashlib, struct
def new_adrs():
return bytearray(32)
def set_type_clear(ad, v):
struct.pack_into(">I", ad, 16, v)
ad[20:32] = b"\x00" * 12
def set_kp(ad, v):
struct.pack_into(">I", ad, 20, v)
ad = new_adrs()
set_kp(ad, 42)
print(f"Before set_type: kp = {struct.unpack_from('>I', ad, 20)[0]}")
set_type_clear(ad, 2) # TREE
print(f"After set_type: kp = {struct.unpack_from('>I', ad, 20)[0]}")
# ==> Before set_type: kp = 42
# ==> After set_type: kp = 0

For the SHA2 parameter sets, FIPS 205 compresses the 32-byte ADRS to 22 bytes; the SHAKE sets keep the full 32-byte address. The compressed form keeps byte 3 (layer LSB), bytes 8 through 15 (tree address), byte 19 (type LSB), and bytes 20 through 31 (context-dependent fields). In the SHA2 constructions, PK.seed and the zero padding fill the first hash-compression block; the compressed ADRS and the function input begin the next (National Institute of Standards and Technology, 2024).

SLH-DSA defines six hash-function roles. Four of them, PRF, F, H, and T\mathbf{T}_\ell, take ADRS as a tweak; PRFmsg\mathbf{PRF}_{msg} and Hmsg\mathbf{H}_{msg} bind the randomizer, message, and public-key material instead. FIPS 205 does not use the word “tweakable”. The tweakable-hash formalism for F, H, and T\mathbf{T}_\ell comes from the SPHINCS+ framework (Aumasson et al., 2020). For the SHA2 parameter sets with n=16n = 16 (security category 1), all six use SHA-256. For n{24,32}n \in \{24, 32\} (categories 3 and 5), F and PRF use SHA-256 while H, T\mathbf{T}_\ell, Hmsg\mathbf{H}_{msg}, and PRFmsg\mathbf{PRF}_{msg} switch to SHA-512 (National Institute of Standards and Technology, 2024).

The construction for F at n=16n = 16 is:

F(PK.seed,ADRS,M1)=Truncn(SHA-256(PK.seedtoByte(0,64n)ADRScM1))\mathbf{F}(\text{PK.seed}, \text{ADRS}, M_1) = \text{Trunc}_n(\text{SHA-256}(\text{PK.seed} \| \text{toByte}(0, 64-n) \| \text{ADRS}^c \| M_1))

PK.seed and the zero padding together fill exactly 64 bytes (one SHA-256 block). The compressed ADRS and the input M1M_1 form the second block. Because the ADRS encodes the chain position, identical inputs at different positions hash differently.

The SHA2 H function for n24n \geq 24 switches to SHA-512:

H(PK.seed,ADRS,M1,M2)=Truncn(SHA-512(PK.seedtoByte(0,128n)ADRScM1M2))\begin{aligned} \mathbf{H}(\text{PK.seed}, \text{ADRS}, M_1, M_2) = {} & \text{Trunc}_n(\text{SHA-512}(\text{PK.seed} \\ & {} \| \text{toByte}(0, 128-n) \| \text{ADRS}^c \| M_1 \| M_2)) \end{aligned}

This split is not an efficiency choice. FIPS 205 Appendix A records two separate changes. SHA-256 was replaced by SHA-512 in Hmsg\mathbf{H}_{msg}, PRFmsg\mathbf{PRF}_{msg}, H, and T\mathbf{T}_\ell for the category 3 and 5 SHA2 sets, after weaknesses were found in reaching category-5 security with SHA-256. Separately, R and PK.seed were added to the MGF1 input in the SHA2 Hmsg\mathbf{H}_{msg} construction, to mitigate multi-target long-message second-preimage attacks. The cryptanalysis section returns to why the 256-bit chaining value is the binding constraint (National Institute of Standards and Technology, 2024).

Chapter 15 built WOTS+ with a simplified domain separation: seed || chain_index || step_index || value. SLH-DSA replaces this with the full ADRS machinery. Secret values come from PRF with type WOTS_PRF. Chain iterations use F with type WOTS_HASH. Public key compression uses T\mathbf{T}_\ell with type WOTS_PK.

import hashlib, struct, math
n, w = 16, 16
lg_w = int(math.log2(w))
ell_1 = math.ceil(8 * n / lg_w)
mc = ell_1 * (w - 1)
ell_2 = math.ceil((math.floor(math.log2(mc)) + 1) / lg_w)
ell = ell_1 + ell_2
def compress(ad):
return bytes([ad[3]]) + bytes(ad[8:16]) + bytes([ad[19]]) + bytes(ad[20:32])
def tw_F(pk_s, ad, m):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + m).digest()[:n]
def tw_T(pk_s, ad, m):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + m).digest()[:n]
def tw_PRF(pk_s, sk_s, ad):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + sk_s).digest()[:n]
def set_type_clear(ad, v):
struct.pack_into(">I", ad, 16, v)
ad[20:32] = b"\x00" * 12
def set_kp(ad, v):
struct.pack_into(">I", ad, 20, v)
def get_kp(ad):
return struct.unpack_from(">I", ad, 20)[0]
def set_chain(ad, v):
struct.pack_into(">I", ad, 24, v)
def set_hash_addr(ad, v):
struct.pack_into(">I", ad, 28, v)
pk_seed = bytes.fromhex("f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6")
sk_seed = bytes.fromhex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")
ad = bytearray(32)
set_kp(ad, 0)
# Generate one WOTS+ secret and chain it to the endpoint
sk_ad = bytearray(ad); set_type_clear(sk_ad, 5); set_kp(sk_ad, 0)
set_chain(sk_ad, 0)
secret = tw_PRF(pk_seed, sk_seed, sk_ad)
w_ad = bytearray(ad); set_type_clear(w_ad, 0); set_kp(w_ad, 0)
set_chain(w_ad, 0)
val = secret
for j in range(w - 1):
set_hash_addr(w_ad, j)
val = tw_F(pk_seed, w_ad, val)
print(f"WOTS+ chain 0: secret {secret.hex()[:16]}... -> endpoint {val.hex()[:16]}...")
print(f"Chain length: {w - 1} F calls, ell = {ell} chains, sig = {ell * n} bytes")
# ==> WOTS+ chain 0: secret 36c81ce3666e22c6... -> endpoint 19f71540b3d7e140...
# ==> Chain length: 15 F calls, ell = 35 chains, sig = 560 bytes

Each WOTS+ key consists of =35\ell = 35 chains (1=32\ell_1 = 32 message chains plus 2=3\ell_2 = 3 checksum chains). The signature is 35 chain values of 16 bytes each: 560 bytes. This matches Chapter 15’s WOTS+ construction, but every hash call now carries an ADRS that encodes (layer, tree, keypair, chain, step).

Chapter 16 hashed each secret into its leaf with a bare sha_n. SLH-DSA derives the secret with PRF (with type FORS_PRF) and hashes it with F(PK.seed, ADRS, secret), so the leaf is bound to its position as well as to its secret. The public leaf is the F output. The signature still reveals the raw secret, but only for the FORS leaves the digest selects. An adversary who sees only the public leaf node must invert F to recover the secret (National Institute of Standards and Technology, 2024).

import hashlib, struct
n = 16
def compress(ad):
return bytes([ad[3]]) + bytes(ad[8:16]) + bytes([ad[19]]) + bytes(ad[20:32])
def tw_F(pk_s, ad, m):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + m).digest()[:n]
def tw_PRF(pk_s, sk_s, ad):
return hashlib.sha256(pk_s + b"\x00"*(64-n) + compress(ad) + sk_s).digest()[:n]
def set_type_clear(ad, v):
struct.pack_into(">I", ad, 16, v)
ad[20:32] = b"\x00" * 12
def set_kp(ad, v):
struct.pack_into(">I", ad, 20, v)
def set_idx(ad, v):
struct.pack_into(">I", ad, 28, v)
def set_height(ad, v):
struct.pack_into(">I", ad, 24, v)
pk_seed = bytes.fromhex("f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6")
sk_seed = bytes.fromhex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")
ad = bytearray(32)
# Generate FORS secret at tree 0, leaf 5
sk_ad = bytearray(ad); set_type_clear(sk_ad, 6); set_kp(sk_ad, 0) # FORS_PRF
set_idx(sk_ad, 5)
secret = tw_PRF(pk_seed, sk_seed, sk_ad)
# Hash through F to get the leaf node (F-function separation)
leaf_ad = bytearray(ad); set_type_clear(leaf_ad, 3); set_kp(leaf_ad, 0) # FORS_TREE
set_height(leaf_ad, 0); set_idx(leaf_ad, 5)
leaf_node = tw_F(pk_seed, leaf_ad, secret)
print(f"FORS secret: {secret.hex()[:16]}...")
print(f"FORS leaf: {leaf_node.hex()[:16]}...")
print(f"Different: {secret != leaf_node}")
# ==> FORS secret: ba9ea0e47b175e85...
# ==> FORS leaf: 7663b64f96a03367...
# ==> Different: True

The secret and the leaf node are different values. The signature reveals the secret (so the verifier can recompute the leaf by applying F), but an adversary who sees only the leaf node in the public tree cannot recover the secret without inverting F, which requires inverting SHA-256.

That is the first of two things SLH-DSA adds to Chapter 16’s FORS. The second sits above the trees. Chapter 16 formed the FORS public key by concatenating the kk roots and hashing them. FIPS 205 compresses them with a single Tk\mathbf{T}_k call under an address whose type is FORS_ROOTS (Algorithm 17, lines 21 to 24). That address carries the same keypair address as the FORS_TREE addresses below it, with zeros in the last two words (National Institute of Standards and Technology, 2024). It is neither a plain concatenate-and-hash nor a further Merkle tree over the roots, and the dedicated type is what keeps that one call from colliding with the T\mathbf{T}_\ell call that compresses a WOTS+ public key.

The hypertree stacks dd layers of XMSS trees. Each XMSS tree has height h=h/dh' = h/d. At the bottom layer (layer 0), WOTS+ keys sign FORS public keys. At each upper layer, WOTS+ keys sign the roots of subtrees from the layer below. The root of the top-layer tree (layer d1d - 1) is PK.root.

Algorithm 9 of FIPS 205 (xmss_node) computes tree nodes recursively. Leaf nodes (height 0) are compressed WOTS+ public keys. Internal nodes combine two children via H with TREE-type ADRS (National Institute of Standards and Technology, 2024).

Signing a message MM under SLH-DSA follows five steps (FIPS 205 Algorithm 19):

  • Compute the randomizer R=PRFmsg(SK.prf,opt_rand,M)R = \mathbf{PRF}_{msg}(\text{SK.prf}, opt\_rand, M)
  • Compute the message digest =Hmsg(R,PK.seed,PK.root,M)= \mathbf{H}_{msg}(R, \text{PK.seed}, \text{PK.root}, M)
  • Extract (idxtree,idxleaf,md)(idx_{tree}, idx_{leaf}, md) from the digest: mdmd selects FORS indices, idxtreeidx_{tree} selects the bottom-layer subtree, idxleafidx_{leaf} selects the WOTS+ key within that subtree
  • Sign mdmd with FORS at position (idxtree,idxleaf)(idx_{tree}, idx_{leaf}), producing SIG_FORS and PK_FORS
  • Sign PK_FORS through dd hypertree layers, producing SIG_HT

The signature is RSIGFORSSIGHTR \| \text{SIG}_{FORS} \| \text{SIG}_{HT}.

Algorithm 19 is slh_sign_internal. FIPS 205 Section 10 wraps it in two application-facing interfaces, both of which prefix a domain separator and a caller-supplied context string before the internal signer sees the message. The pure interface, slh_sign(M, ctx, SK), signs

M=toByte(0,1)toByte(ctx,1)ctxMM' = \text{toByte}(0, 1) \, \| \, \text{toByte}(|ctx|, 1) \, \| \, ctx \, \| \, M

The pre-hash interface, hash_slh_sign(M, ctx, PH, SK), signs

M=toByte(1,1)toByte(ctx,1)ctxOIDPH(M)M' = \text{toByte}(1, 1) \, \| \, \text{toByte}(|ctx|, 1) \, \| \, ctx \, \| \, OID \, \| \, PH(M)

where OIDOID identifies the pre-hash function PHPH. The context string is at most 255 bytes and empty by default. Verification reconstructs the same MM' before calling slh_verify_internal (National Institute of Standards and Technology, 2024).

FIPS 205 parameter sets and signature sizes

Section titled “FIPS 205 parameter sets and signature sizes”

The signature size formula has three components:

  • RR: nn bytes (the randomizer)
  • SIGFORS\text{SIG}_{FORS}: k(1+a)nk(1 + a) \cdot n bytes (kk trees, each contributing one leaf value and aa authentication-path nodes)
  • SIGHT\text{SIG}_{HT}: d(+h)nd(\ell + h') \cdot n bytes (dd layers, each contributing a WOTS+ signature of n\ell \cdot n bytes and an authentication path of hnh' \cdot n bytes)
import math
def sig_size(name, n, h, d, a, k, w):
hp = h // d
lg_w = int(math.log2(w))
ell_1 = math.ceil(8 * n / lg_w)
mc = ell_1 * (w - 1)
ell_2 = math.ceil((math.floor(math.log2(mc)) + 1) / lg_w)
ell = ell_1 + ell_2
r_bytes = n
fors_bytes = k * (1 + a) * n
ht_bytes = d * (ell + hp) * n
total = r_bytes + fors_bytes + ht_bytes
print(f"{name:24s} n={n:2d} sig={total:6,d} B "
f"(R={r_bytes}, FORS={fors_bytes:,d}, HT={ht_bytes:,d})")
sig_size("SLH-DSA-SHA2-128s", 16, 63, 7, 12, 14, 16)
sig_size("SLH-DSA-SHA2-128f", 16, 66, 22, 6, 33, 16)
sig_size("SLH-DSA-SHA2-192s", 24, 63, 7, 14, 17, 16)
sig_size("SLH-DSA-SHA2-192f", 24, 66, 22, 8, 33, 16)
sig_size("SLH-DSA-SHA2-256s", 32, 64, 8, 14, 22, 16)
sig_size("SLH-DSA-SHA2-256f", 32, 68, 17, 9, 35, 16)
# ==> SLH-DSA-SHA2-128s n=16 sig= 7,856 B (R=16, FORS=2,912, HT=4,928)
# ==> SLH-DSA-SHA2-128f n=16 sig=17,088 B (R=16, FORS=3,696, HT=13,376)
# ==> SLH-DSA-SHA2-192s n=24 sig=16,224 B (R=24, FORS=6,120, HT=10,080)
# ==> SLH-DSA-SHA2-192f n=24 sig=35,664 B (R=24, FORS=7,128, HT=28,512)
# ==> SLH-DSA-SHA2-256s n=32 sig=29,792 B (R=32, FORS=10,560, HT=19,200)
# ==> SLH-DSA-SHA2-256f n=32 sig=49,856 B (R=32, FORS=11,200, HT=38,624)

The “s” variants (small signatures) use deeper subtrees (h=8h' = 8 or 99) and fewer hypertree layers (d=7d = 7 or 88), which concentrates more WOTS+ keys in each layer and reduces the total number of WOTS+ signatures in SIG_HT. The “f” variants (fast signing) use shallow subtrees (h=3h' = 3 or 44) and many layers (d=17d = 17 or 2222), which makes each subtree cheaper to build but multiplies the number of WOTS+ signatures. SLH-DSA-SHA2-128s produces 7,856-byte signatures; SLH-DSA-SHA2-128f produces 17,088-byte signatures. Signing is faster with the “f” variant because each subtree has only 23=82^3 = 8 leaves instead of 29=5122^9 = 512 (National Institute of Standards and Technology, 2024).

Public keys are 2n2n bytes for all parameter sets: PK.seed (nn bytes) and PK.root (nn bytes). Secret keys are 4n4n bytes: SK.seed, SK.prf, PK.seed, and PK.root.

Without ADRS domain separation, an adversary who finds a preimage for any hash call in the tree can forge a signature, and every hash call across a key’s lifetime joins one large multi-target pool. ADRS does not take the analysis out of the multi-target setting; it makes the target domain explicit, which is what keeps the generic bound independent of the target count. Inverting the hash at one ADRS-tagged position does not help at another, because the tweaked inputs differ (Aumasson et al., 2020). The FIPS 205 parameter sets are chosen so the EUF-CMA claim holds for up to 2642^{64} signatures per key, accounting for the relevant multi-target and many-signature effects (National Institute of Standards and Technology, 2024).

The “128” in SLH-DSA-SHA2-128s is the length in bits of the security parameter nn. FIPS 205 Section 11 defines the number in a parameter-set name as exactly that, so n=16n = 16 bytes. The claimed NIST security category is assigned separately in Table 2, and for both 128 sets it is category 1, not 128 (National Institute of Standards and Technology, 2024). The standard is explicit that the two are different kinds of quantity: security strength under this approach “is not described by a single number, such as ‘128 bits of security’”, but as a claim that breaking the parameter set costs at least as much as breaking a block cipher with a prescribed key size (National Institute of Standards and Technology, 2024). Nor is 8n8n the width of every digest inside the scheme. Hmsg\mathbf{H}_{msg} produces mm bytes, 30 for this parameter set, and the SHA-2 constructions truncate and expand around it (National Institute of Standards and Technology, 2024). Chapter 18 carries the multi-target arithmetic and the residual proof loss that the parameter search absorbs.

Grover’s algorithm (Chapter 1) reduces the cost of brute-force search from O(2n)O(2^n) to O(2n/2)O(2^{n/2}). For preimage resistance, this reduces the effective security of an nn-bit hash from nn bits to roughly n/2n/2 bits against a quantum adversary. SLH-DSA-SHA2-128s uses n=128n = 128 bits (16 bytes), so quantum preimage attacks require approximately 2642^{64} quantum hash evaluations. This is consistent with the intuition behind NIST security category 1, which is defined by the computational resources needed to break AES-128, not by a bit count. The full SLH-DSA security claim is a parameter-set claim, not the statement that n/2=64n/2 = 64 (National Institute of Standards and Technology, 2024).

For collision resistance, the birthday bound gives 2n/22^{n/2} classical security, reduced to 2n/32^{n/3} queries by the Brassard-Hoyer-Tapp (BHT) algorithm from Chapter 3, in the query model with about 2n/32^{n/3} entries of quantum-accessible memory (Brassard et al., 1998). SLH-DSA’s security does not depend on collision resistance of the hash function alone. The tweakable construction and ADRS domain separation yield a tighter multi-target security reduction than the standard collision-resistance bound. Chapter 18 develops the per-signature vs per-FORS-instance vs lifetime target-count taxonomy and the multi-target preimage arithmetic for each FIPS 205 parameter set.

Applications that need message-bound signatures (a signature that cannot be valid for two different messages under the same key) need an extra step. FIPS 205 Section 11 notes that the key owner could find an Hmsg\mathbf{H}_{msg} collision and reuse one signature for two messages. Such applications should either choose a parameter set with enough Hmsg\mathbf{H}_{msg} collision margin or apply a message-binding transformation such as BUFF (National Institute of Standards and Technology, 2024). Chapter 18 also analyzes fault injection against WOTS+ chain computation, timing side channels, and the grafting attack on SLH-DSA’s deterministic randomizer: an adversary who can fault the hypertree computation on a deterministically-signed message can exploit WOTS+ key reuse to forge signatures with far fewer queries than generic preimage search.

Hash function split for categories 3 and 5

Section titled “Hash function split for categories 3 and 5”

The SHA-256/SHA-512 split is specified in Section 11.2.2 of FIPS 205 and motivated in Appendix A. The H function processes two nn-byte inputs, which span multiple SHA-256 compression-function blocks. The 256-bit chaining value of SHA-256 limits the effective multi-target second-preimage resistance in the tweakable hash construction. For n=32n = 32 (category 5, targeting 256-bit classical security), this chaining-value width is insufficient. SHA-512 with its 512-bit chaining value provides the needed margin. F and PRF still use SHA-256 because they have single nn-byte inputs and the preimage resistance of SHA-256 is adequate for all categories (National Institute of Standards and Technology, 2024).

  • Lamport + Merkle (Chapter 14): 8,192-byte Lamport one-time signature. The complete Merkle signature Chapter 14 verifies also carries the 16,384-byte Lamport public key and the authentication path, 25,216 bytes at d=20d = 20. Stateful. Simplest construction, largest signatures. One secret per bit of the message hash.
  • WOTS+ + XMSS (Chapter 15): roughly 2,500-byte signature at h=10h = 10, n=32n = 32. Stateful. Roughly 4x compression over Lamport via w=16w = 16 hash chains. Leaf counter must never be reused.
  • FORS (Chapter 16): 960-byte signature at k=6k = 6, t=16t = 16, n=32n = 32. Few-time. No counter, but limited to a bounded number of signatures per key: a forgery needs every one of a target’s kk indices already revealed in its tree, the occupancy form of Chapter 16, and that probability grows with each signature.
  • SLH-DSA-SHA2-128s (FIPS 205): 7,856-byte signature. Stateless. Hmsg\mathbf{H}_{msg} derives a pseudorandom (idx_tree, idx_leaf) pair from the randomizer, the public key material, and the message, over 2h=2632^h = 2^{63} possible positions. Position reuse does happen: after qq signatures the chance of at least one repeat is about q(q1)/264q(q-1) / 2^{64}, small for low-volume keys but not for arbitrarily high-volume ones. FORS is few-time, so occasional position reuse does not collapse the scheme, but repeated reuse of one position accumulates revealed FORS leaves. The EUF-CMA claim rests on the FIPS 205 parameterization and the 2642^{64}-signatures-per-key design target, not on positions never colliding (Chapter 16).
  • SLH-DSA-SHA2-128f (FIPS 205): 17,088-byte signature. Stateless. Faster signing (shallow subtrees) at the cost of larger signatures.
  • ML-DSA (FIPS 204): 2,420 to 4,627-byte signature depending on parameter set (National Institute of Standards and Technology, 2024a). Stateless. Relies on lattice assumptions (Module-LWE for key recovery, Module-SIS for unforgeability) in addition to hash properties. Smaller signatures but a fundamentally different security basis.

SLH-DSA’s large signatures are the cost of statelessness with hash-only security. Applications that cannot guarantee state synchronization across replicas (TLS servers, cloud HSMs, backup-restored keys) accept the size penalty. Applications with a single, tightly controlled signer (firmware signing, certificate roots) may prefer XMSS’s 2,500-byte signatures and the state management burden (Hülsing et al., 2018; National Institute of Standards and Technology, 2024b).

Every number above is a size or a speed, which is what an operator picks a parameter set on. None of them is a security margin. Chapter 18 closes Part III by supplying the other half. It counts the hash targets one FORS instance’s position space holds, ktk \cdot t FORS leaves plus dd \cdot \ell WOTS+ chain values, under the counterfactual that ADRS domain separation is absent, and works that multi-target preimage arithmetic through each FIPS 205 parameter set. The model is position-scoped: it is not a count of what one adversary collects per signature, and not the key-level proof term. It also takes Chapter 16’s exact occupancy form and finds the reuse thresholds for each parameter set, separating reuse of one fixed FORS instance from key-level security. It then steps outside the EUF-CMA model for the fault and grafting attacks, which FIPS 205 treats as implementation concerns rather than properties of the construction.

  1. Compressed ADRS computation. Construct an ADRS with layer address 3, tree address 100, type WOTS_HASH (0), keypair address 7, chain address 12, and hash address 5. Compute the 22-byte compressed form by extracting ADRS[3], ADRS[8:16], ADRS[19], and ADRS[20:32]. Verify that the compressed form is 22 bytes.

  2. Signature size derivation. Compute the SLH-DSA signature size for SLH-DSA-SHA2-192s (n=24n = 24, h=63h = 63, d=7d = 7, a=14a = 14, k=17k = 17, w=16w = 16) from first principles. Show the WOTS+ parameter derivation (1\ell_1, 2\ell_2, \ell), the FORS component, the hypertree component, and the total.

  3. Type-zeroing rationale. Suppose set_type did NOT zero bytes 20 through 31. Construct a scenario where stale chain, hash, or tree-index fields survive a type change. Explain how this produces non-canonical address encodings that can cause implementation divergence or unintended domain separation between hash calls that should be independent.

  4. Parameter tradeoff. Starting from the toy parameters (n=16n = 16, h=9h = 9, d=3d = 3, a=3a = 3, k=3k = 3, w=16w = 16), modify hh, dd, or ww to reduce the hypertree signature component. Compute the new component size and explain the cost: fewer signing positions, more hypertree layers, or longer and slower WOTS+ chains.

  5. When to choose s vs f. An application signs 100 messages per second on a server with 8 replicas. Each signature must be verified by mobile clients over a cellular connection. Argue whether SLH-DSA-SHA2-128s or SLH-DSA-SHA2-128f is the better choice, considering signing speed, verification speed, and bandwidth.

Worked solutions and editorial notes for these exercises are in Appendix D, Chapter 17. A separate track, for rebuilding rather than reading. The package exercises/ch17-slh-dsa arrives with the ADRS field accessors and the WOTS+ public-key routine already implemented, because the chapter prints them, and stubs what it does not: the tweakable hashes at their SHAKE and SHA-512 branches, the chain function at an arbitrary start index, FORS, the hypertree, and the three top-level SLH-DSA routines. Run PQC_IMPL=exercises pytest tests/ch17 to grade your version against the suite that proves the reference one.

Aumasson, J.-P., Bernstein, D. J., Beullens, W., Dobraunig, C., Eichlseder, M., Fluhrer, S., Gazdag, S.-L., Hülsing, A., Kampanakis, P., Kölbl, S., Lange, T., Lauridsen, M. M., Mendel, F., Niederhagen, R., Rechberger, C., Rijneveld, J., Schwabe, P., & Westerbaan, B. (2020). SPHINCS+: Submission to the NIST Post-Quantum Cryptography Standardization Process. NIST PQC Round 3 submission; SPHINCS+ specification v3, 1 October 2020. https://sphincs.org/data/sphincs+-round3-specification.pdf
Beast, H., Heilman, E., & Foxen Duke, I. (2024). BIP-360: Pay-to-Merkle-Root (P2MR). Bitcoin Improvement Proposal (Draft). https://github.com/bitcoin/bips/blob/master/bip-0360.mediawiki
Brassard, G., Høyer, P., & Tapp, A. (1998). Quantum Cryptanalysis of Hash and Claw-Free Functions. LATIN ’98: Theoretical Informatics, 1380, 163–169. https://doi.org/10.1007/bfb0054319
Hülsing, A., Butin, D., Gazdag, S., Rijneveld, J., & Mohaisen, A. (2018). XMSS: eXtended Merkle Signature Scheme. IETF RFC 8391. https://doi.org/10.17487/rfc8391
Moody, D., & Dang, Q. (2026). NIST SP 800-230 ipd: Additional SLH-DSA Parameter Sets for Limited-Signature Use Cases. National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-230.ipd
National Institute of Standards and Technology. (2024a). FIPS 204: Module-Lattice-Based Digital Signature Standard. Federal Information Processing Standards Publication. https://doi.org/10.6028/NIST.FIPS.204
National Institute of Standards and Technology. (2024b). FIPS 205: Stateless Hash-Based Digital Signature Standard. Federal Information Processing Standards Publication. https://doi.org/10.6028/NIST.FIPS.205
Wuille, P., Nick, J., & Ruffing, T. (2020). BIP-340: Schnorr Signatures for secp256k1. Bitcoin Improvement Proposal. https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
Wuille, P., Nick, J., & Towns, A. (2020). BIP-341: Taproot: SegWit version 1 spending rules. Bitcoin Improvement Proposal. https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki

Last updated: