ForsVerifier
Stateless FORS+C verifier
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 off-chain 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 owner: 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);
// recovered == owner -> authorized
// recovered == 0 -> malformed length or failed grinding checkImplementation 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. - Domain separation.
DOM = 0xFF…FDkeeps standalone FORS+C distinct from spec SPHINCS+ (0xFF…FF) 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. - Specialized, 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 legacy WOTS+C verifier costs ~93k gas, and a full SLH-DSA / SPHINCS+ verify would be an order of magnitude above that. That is exactly why NiceTry uses standalone FORS (no XMSS hypertree).