ForsVerifier
src/Verifiers/ForsVerifier.sol is the onchain FORS+C verifier. It is stateless and pure: it takes a signature blob and a digest and returns the address that must have signed it. It holds no storage, knows nothing about accounts, and is shared by every account.
Interface
contract ForsVerifier is ISignatureVerifier {
uint256 public constant N = 16; // hash truncation (bytes)
uint256 public constant K = 26; // FORS trees (25 real under +C)
uint256 public constant A = 5; // tree height (32 leaves per tree)
uint256 public constant SIG_LEN = 2448;
function recover(bytes calldata sig, bytes32 digest) external pure returns (address);
}The four parameters are ABI-readable so offchain tooling can confirm the deployed set. recover is the only entry point.
recover(sig, digest)
Returns the signer address, or address(0) on failure. It performs no comparison against any stored key: that is the account's job. The steps:
- Reject if
sig.length != SIG_LEN(2,448). - Read
R,pkSeed,counter(each a 16-byte value in the top half of a 32-byte word) and recomputedVal = keccak256(pkSeed ‖ R ‖ digest ‖ DOM ‖ counter). - +C grinding check: if the K-th
A-bit field ofdValis nonzero, returnaddress(0). - For each of the 25 real trees, extract the leaf index
md[t] = (dVal >> 5·t) & 31, recompute the leaf hash from the revealedsk, and climb the 5-node auth path to a root. - Compress the 25 roots into
pkRootwith oneTcall. - Return
last20(keccak256(pad32(pkSeed) ‖ pad32(pkRoot))).
address recovered = VERIFIER.recover(userOp.signature, userOpHash);
// authState[recovered] == ACTIVE -> authorised, key is burned and nextOwner activated
// recovered == 0 -> malformed length or failed grinding check
// anything else -> not an active key, validation failsImplementation notes
- Single primitive. Every hash is truncated
keccak256(N = 16); the five roles (PRF/F/H/T/Hmsg) are separated by the 32-byte ADRS mixed into each input. See FORS+C → Overview. - FIPS 205 address layout. Since July 2026 the ADRS follows the FIPS 205 §4.2 field layout, with the type word at bytes 16..20 (
3 << 96forFORS_TREE,4 << 96forFORS_ROOTS) and the tree/leaf indices in the low words. This is the same layout theSphincsVerifieruses. Signers written against the earlier layout (type at<< 128) produce signatures that recover the wrong address. The exact formulas are in the Signer spec. - Domain separation.
DOM = 0xFF…FDkeeps standalone FORS+C distinct from spec SPHINCS+ (0xFF…FF, also used by the SPHINCS backup verifier) and the SLH-DSA-Keccak family (0xFF…FE). - Hand-tuned assembly. The verifier body is memory-safe inline assembly, with the inner Merkle climb unrolled for
A = 5and a fixed scratch layout after the roots buffer. This is what brings the cost down to ~34.1k gas warm / ~38.1k cold. - Specialised, by design. Compile-time guards (
FORS_A_UNROLL_GUARD, the scratch-alignment guards) fail the build ifAor the memory layout changes without updating the unrolled block. RetuningK/Ais described in Parameters → Retuning.
Gas
| Call | Gas (measured at K=26, A=5) |
|---|---|
| warm | ~34.1k |
| cold | ~38.1k |
For comparison, the SPHINCS backup verifier costs ~105k, a NIST-parameter SLH-DSA-128s verify about 361k, and the archived WOTS+C verifier ~93k. Standalone FORS (no XMSS hypertree) is exactly what makes per-transaction verification this cheap.