Challenge
Verifier → prover: a fresh random nonce the prover must MAC with the join code.
The nonce is a fixed-width field, and a wrong width is REJECTED, never reshaped (#1820)
nonce is always exactly NONCE_BYTES bytes — enforced here, not merely documented. The check lives in the constructor deliberately: kotlinx-serialization invokes it, so the invariant holds on every path, encode and decode alike, and a new consumer cannot forget it.
It is load-bearing because the prover MACs this value verbatim (HMAC-SHA256(code, nonce)) and hands the tag back. Leaving the width to the sender let a peer decide how many bytes of the MAC input existed — a zero-length nonce erased it entirely, so the tag degenerated to HMAC(code, ""), carrying no per-attempt freshness at all. A width is a quantity and could be clamped; a nonce is not: a wrong-width nonce is proof of a malformed or forged challenge, and padding or truncating it to NONCE_BYTES would launder that proof into a valid-looking challenge. The frame is dropped instead — decode returns null, which TokenGatedSeam already treats as "do not answer".
Enforcing the width does not make the nonce unpredictable: it is still chosen wholly by the sender, so a peer can pick a fixed 16-byte value and precompute against it (#1865). See the "Entropy ↔ TTL" note on LogTapJoinToken.