FORS+C Parameters
Current selection
The active set is defined at the top of src/Verifiers/ForsVerifier.sol:
uint256 constant FORS_N = 16; // hash truncation / node size (bytes)
uint256 constant FORS_K = 26; // FORS trees (only K-1 = 25 are real under +C)
uint256 constant FORS_A = 5; // tree height -> 2^5 = 32 leaves per tree| Property | Value |
|---|---|
| Hash primitive | truncated Keccak-256 (N = 16 bytes) |
Trees K | 26 (25 real, 1 dropped by +C) |
Tree height A | 5 (32 leaves per tree) |
| Signature size | 2,448 bytes |
| Onchain public key | 20-byte address |
| Verifier gas | ~34.1k warm / ~38.1k cold (measured) |
| Signer work | ~2,400 Keccak calls / signature |
q=1 security | 128-bit classical (NIST Level 1) |
| Domain byte | 0xFD |
Signature layout
A FORS+C signature is exactly FORS_SIG_LEN = 2,448 bytes:
offset length field
0 16 R (per-signature randomizer)
16 16 pkSeed (public per-keypair seed)
32 2400 tree openings for t = 0..24 (25 × 96 B)
2432 16 counter (+C grinding nonce, big-endian uint128)Each of the 25 tree openings is 96 bytes:
offset within tree length field
0 16 sk for the selected leaf
16 16 auth[0] (leaf-level sibling)
32 16 auth[1]
48 16 auth[2]
64 16 auth[3]
80 16 auth[4] (root-level sibling)Derived constants (all recomputed from K, A, N):
TREE_LEN = 16 + A·16 = 96
SECTION_LEN = (K-1) · TREE_LEN = 2,400
SIG_LEN = 16 + 16 + 2,400 + 16 = 2,448The K-th tree's auth path is omitted entirely: the verifier knows its index is 0 from the grinding constraint and never opens it.
Security and key reuse
After q signatures under one key, each tree has up to q revealed leaves out of 2^A. Forging requires a message whose K hash-derived indices all land on revealed leaves, so the work is roughly:
security(q) ≈ min( n , K · (A − log₂ q) ) bits, classicalcapped at n = 128 (the hash truncation). For K = 26, A = 5:
q (signatures per key) | Classical security |
|---|---|
| 1 | 128 bits (NIST Level 1, capped) |
| 2 | 104 bits |
| 3 | 89 bits |
| 4 | 78 bits |
| 5 | 70 bits |
q = 1 sits at the 128-bit ceiling (K · A = 130 just clears the cap). Degradation is steep: reuse beyond q = 2 drops below Level 1. That is acceptable because the design target is one signature per key, enforced by atomic rotation.
Recommended reuse budget
The few-time property exists to make failure modes (dropped UserOp, bundler replacement, a burn-before-sign desync) recoverable rather than catastrophic. Wallets MUST track per-key reuse and enforce a bounded budget:
q ≤ 2: normal-operation envelope (≥ 104 bits). Replacement-transaction flows fall here.q ≤ 5: maximum under degraded security (≥ 70 bits) for emergency reuse.q > 5: the wallet MUST refuse to sign with that key.
This is the central reason FORS+C replaced WOTS+C: under WOTS+C, the same failure modes leak enough chain information for immediate classical forgery.
The two-forest cache
To keep signing interactive, the wallet keeps two full FORS forests ready at all times, where a "forest" means all signer material for one keypair (skSeed, pkSeed, cached tree nodes, pkRoot, owner address):
current: forest that authorizes the next UserOp
next: forest whose address is appended as nextOwnerAfter releasing a signature it starts building next + 1 in the background, so the user never waits at a confirmation screen. The invariant is that address(S_{i+1}) must be known before signing UserOp i, because nextOwner is part of the signed userOpHash; the full forest for S_{i+1} only needs to be ready by UserOp i+1.
Memory (public tree nodes, N = 16), under +C with one tree omitted:
| Set | Per forest | Two forests |
|---|---|---|
K=26, A=5 | ~25 KB | ~50 KB |
Leaf secrets are derived on demand from skSeed rather than cached, to avoid persisting more secret material than necessary. The cache is a UX optimization only: the contract cares solely that the current owner verifies the UserOp and that the appended nextOwner is nonzero. See docs/fors-two-forest-cache.md for the full state machine.
Hardware-wallet feasibility
The K=26, A=5 set was chosen specifically to keep signing viable on constrained devices: dropping from A=10 to A=5 (1,024 → 32 leaves per tree) cuts signer work by roughly 17×, to ~2,400 Keccak calls per signature, paid for by steeper q-degradation that the rotation-per-UserOp model can absorb.
On a phone or desktop CPU this workload is negligible. On secure-element hardware with software Keccak it is noticeably slower but still workable; if interactive secure-element UX becomes a priority, the path is a SHA-256 variant of the verifier (separate contract, new domain byte) to leverage hardware hash acceleration.
Retuning
To change the set, edit FORS_K and FORS_A at the top of ForsVerifier.sol. All derived constants (signature layout, loop bounds, bit masks, shift counts) recompute automatically from those two values.
Larger K · A (e.g. K=14, A=13, K·A ≥ 180) keeps high security across many reuses, at the cost of much larger/slower signatures. Prefer it only if the threat model expands to "many signatures per key." A full parameter sweep and rationale lives in docs/fors-parameters.md.