QuipNetwork/hashsigs-solidity
QuipNetwork/hashsigs-solidity has 11,204 GitHub stars and 34 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-07 13:18 UTC. Last push 2026-11-04.
Solidity contracts implementing hash based post quantum signatures
Measured star growth
- in 24h
- 0
- in 7 days
- Update delayed
- in 15 days
- Update delayed
- in 1 month
- Update delayed
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
README.md
OriginalHash-Based Solidity Verifier Implementations
Verifier-oriented Solidity implementations of hash-based signature schemes: the WOTS+ one-time signature scheme and the SHRINCS hybrid stateful/stateless construction.
The production contracts in contracts/ cover on-chain
verification only. Signer-side recovery state, seed restore logic, and
wallet lifecycle management are out of scope; test-only Solidity signer
helpers live under test/helpers/ and are not
deployable.
Implementations
Citation keys follow CODINGSTANDARDS.md §1.
- WOTS+ (contracts/WOTSPlus.sol) — the
Winternitz one-time signature scheme
[WOTSPLUS](A. Hülsing, W-OTS+: Shorter Signatures for Hash-Based Signature Schemes, AFRICACRYPT 2013; https://eprint.iacr.org/2017/965). Keccak-256, 32-byte hashes and messages, Winternitz parameterw = 16, 67 chains (64 message + 3 checksum). Standalone, with no SHRINCS dependency. The public API (verify,verifyWithRandomizationElements,sign,generateKeyPair,generateRandomizationElements,chain, and the public constants) is stable and vector-tested; the signer-side helpers are reference code, documented "do not use on-chain." - SHRINCS (contracts/SHRINCS.sol and its
component libraries) — the hybrid stateful/stateless hash-based
construction by Kudinov and Nick
[SHRINCS], specified in the appendix of Hash-based Signature Schemes for Bitcoin, Cryptology ePrint Archive 2025/2203 (https://eprint.iacr.org/2025/2203). Compiled per profile (see Profiles):256s(default) — keccak-256, 32-byte hashes,h = 64,d = 8,a = 14,k = 22, 64 WOTS-C chains, 2^20 stateless budget. Reviewed and anchored to Rust-generated signature vectors.128s-q18(experimental) — 16-byte truncated hashes, single-layerh = 18hypertree,a = 24,k = 6, 32 WOTS-C chains, 2^18 stateless budget. Anchored to Rust-generated signature vectors; the stateless path is verified against them bySHRINCSSphincs128sVectors.128s-q20(experimental) — as128s-q18with a 2^20 stateless budget; shares the 128s vectors except the profile-bound commitment.
- SPHINCS+C (contracts/SPHINCSPlusC.sol)
— the stateless SPHINCS+C construction
[SPHINCSPLUSC](M. Kudinov, A. Hülsing, E. Ronen, E. Yogev, SPHINCS+C: Compressing SPHINCS+ With (Almost) No Cost, Cryptology ePrint Archive 2022/778, IEEE S&P 2023; https://eprint.iacr.org/2022/778). SHRINCS uses it as the stateless recovery path: aFORS-Cfew-time signature carried up a hypertree ofWOTS-Clayers to the public root. Deployed per profile asSPHINCSPlusC256sKeccak,SPHINCSPlusC128sQ18Keccak, andSPHINCSPlusC128sQ20Keccak; each SHRINCS verifier delegates its stateless path to the matching profile sibling.
What SHRINCS Is
SHRINCS is a two-path signature design:
- a stateful path
- cheap normal-case verification
- compact
WOTS-Cplus an unbalanced XMSS-style authentication path
- a stateless path
- fallback / recovery verification path
- a SPHINCS-style
FORS-C + hypertree + WOTS-Cstructure
Normal operation uses the cheaper stateful path. A restored or degraded
signer falls back to the stateless path. The verifier only checks signatures
and rotation authorizations; it does not track signer state. The ERC-7913
verifiers here cover both paths: each SHRINCS verifier exposes the stateful
verify plus a verifyStateless that delegates to a pinned SPHINCS+C
sibling, and the standalone SPHINCS+C verifiers check the stateless recovery
path directly. Each SHRINCS verifier also exposes verifyAndAttest /
wasVerified: a successful verifyAndAttest records the verification for
the calling account in EIP-1153 transient storage, so downstream contracts
in the same transaction can query it instead of burning another one-time
leaf on a fresh signature.
Repository Layout
Main contracts:
- contracts/SHRINCSVerifier.sol
- abstract ERC-7913 verifier for the hybrid scheme: stateful
verifyplusverifyStatelessdelegating to the pinned SPHINCSPlusC sibling, and the transient attestation surface (verifyAndAttest/wasVerified)
- abstract ERC-7913 verifier for the hybrid scheme: stateful
- contracts/SHRINCS.sol
- pure verification core library (facade over the component libraries): builds the canonical action and rotation hashes and runs the stateful and stateless verify-in-place logic; owns the key and envelope encoders/decoders bridging ERC-7913 opaque bytes to typed structures
- contracts/Hash.sol
- profile-independent hash and bit primitives (address packing, hash masking, base-w digits, bit readers); shared by every suite and never remapped
- contracts/hash/keccak/HashSuite.sol
- the compile-time hash-suite seam: every tagged scheme hash (chain,
leaf, node, digest, finalizer) as one helper each, selected via the
shrincs-hash/remapping axis
- the compile-time hash-suite seam: every tagged scheme hash (chain,
leaf, node, digest, finalizer) as one helper each, selected via the
- contracts/UXMSS.sol
- stateful
WOTS-Creconstruction and unbalanced XMSS-style path verification
- stateful
- contracts/FORSMinusC.sol
FORS-Cdigest extraction and root reconstruction
- contracts/Hypertree.sol
- stateless
WOTS-Cand hypertree layer verification
- stateless
- contracts/WOTSPlusC.sol
- shared
WOTS+Cchain machinery used by the hypertree and UXMSS
- shared
- contracts/SPHINCSPlusCVerifier.sol
- abstract ERC-7913 verifier for the stateless recovery path
- contracts/SPHINCSPlusC.sol
- stateless SPHINCS+C verification core library (
FORS-C+ hypertree +WOTS-Cto the public root)
- stateless SPHINCS+C verification core library (
- contracts/SHRINCS256sKeccak.sol,
contracts/SHRINCS128sQ18Keccak.sol,
contracts/SHRINCS128sQ20Keccak.sol
- deployable per-profile stateful verifiers, each with a
PROFILE_TAG; each pins its SPHINCS+C sibling for the stateless path
- deployable per-profile stateful verifiers, each with a
- contracts/SPHINCSPlusC256sKeccak.sol,
contracts/SPHINCSPlusC128sQ18Keccak.sol,
contracts/SPHINCSPlusC128sQ20Keccak.sol
- deployable per-profile stateless verifiers, each with a
PROFILE_TAG
- deployable per-profile stateless verifiers, each with a
- contracts/profiles/
- per-profile
SHRINCSParamsconstant libraries (see Profiles)
- per-profile
- contracts/interfaces/IERC7913SignatureVerifier.sol
- ERC-7913 verifier interface
- contracts/interfaces/IERC7913TransientAttestation.sol
- transaction-scoped attestation registry interface layered on the
ERC-7913 verifier (
verifyAndAttest/wasVerified), including the shared transient-slot spec consumers derive
- transaction-scoped attestation registry interface layered on the
ERC-7913 verifier (
- contracts/WOTSPlus.sol
- the standalone
WOTS+verifier (see Implementations) - not used by the SHRINCS paths or the ERC-7913 verifiers
- the standalone
- contracts/examples/SHRINCSAccountVerifierExample.sol
- example account wrapper that owns nonce, rotation, and policy state
- contracts/examples/SHRINCSERC7913ConsumerExample.sol
- minimal consumer-side example for the generic
IERC7913SignatureVerifier.verify(...)flow: stores a verifier address plus opaque key bytes and treats the verifier as a pure signature-validity oracle
- minimal consumer-side example for the generic
- contracts/examples/SHRINCSStatelessConsumerExample.sol
- minimal consumer-side example for the SHRINCS-specific
verifyStateless(...)path on the concrete verifier surface
- minimal consumer-side example for the SHRINCS-specific
Deployment and tooling:
- DEPLOYMENTS.md
- deployment registry: salts, predicted addresses, deploy procedure
- script/Create3.sol and
script/DeployBase.s.sol
- CREATE3 factory and shared deploy plumbing
- script/DeploySHRINCS256sKeccak.s.sol
and script/DeploySPHINCSPlusC256sKeccak.s.sol
- production CREATE3 deploy scripts (see Deployment)
- scripts/check-line-length.sh
- 78-char line gate from CODINGSTANDARDS.md
- dev/export-account-vectors.sh
- exports wrapper-ready account vectors as JSON
Architecture:
graph TD
subgraph "Integrator layer (example)"
EX["SHRINCSAccountVerifierExample.sol<br/>(contract — owner, policies, nonce,<br/>keyVersion, q_s budget, stateful-use tracking)"]
end
subgraph "ERC-7913 verifiers"
VF["SHRINCSVerifier.sol (abstract base)<br/>+ SHRINCS256sKeccak / 128sQ18Keccak / 128sQ20Keccak<br/>(stateful verify + verifyStateless)"]
SP["SPHINCSPlusCVerifier.sol (abstract base)<br/>+ SPHINCSPlusC256sKeccak / 128sQ18Keccak / 128sQ20Keccak<br/>(stateless recovery path)"]
end
subgraph "Verification core libraries"
FA["SHRINCS.sol<br/>stateful + stateless verify-in-place,<br/>canonical action + rotation hashes"]
SC["SPHINCSPlusC.sol<br/>stateless FORS-C + hypertree<br/>-> public root"]
end
subgraph "Crypto component libraries"
ST["UXMSS.sol<br/>compact WOTS-C digits +<br/>unbalanced-XMSS auth path<br/>(cheap normal-case path)"]
FO["FORSMinusC.sol<br/>FORS-C digest, grind checks,<br/>k−1 tree-root rebuild -> forsRoot"]
HY["Hypertree.sol<br/>d=8 XMSS layers, stateless WOTS-C,<br/>Merkle paths -> hypertreeRoot"]
WC["WOTSPlusC.sol<br/>shared WOTS+C chain machinery"]
end
subgraph "Foundation"
HS["hash/keccak/HashSuite.sol<br/>tagged scheme hashes<br/>(shrincs-hash/ suite seam)"]
HH["Hash.sol<br/>hash + bit primitives,<br/>address-word packing<br/>(never remapped)"]
PA["profiles/<profile>/SHRINCSParams.sol<br/>(profile-selected constants)"]
end
WP["WOTSPlus.sol<br/>(standalone library —<br/>no SHRINCS dependency)"]
EX --> FA
EX --> SC
EX --> PA
VF --> FA
VF -. stateless delegate .-> SP
SP --> SC
FA --> ST
FA --> SC
FA --> PA
FA --> HS
SC --> FO
SC --> HY
ST --> WC
ST --> HS
ST --> PA
FO --> HH
FO --> HS
FO --> PA
HY --> WC
HY --> HS
HY --> PA
WC --> HS
HS --> HH
HH --> PA
Tests (29 suites, 266 tests as of 2026-07-13, ci profile):
- test/SHRINCSSphincs256sVectors.t.sol
- vector-backed verification and rotation-authorization tests
- test/SHRINCSAccountVerifierExample.t.sol
- wrapper integration and state-transition tests
- test/SHRINCSStatefulPolicyExamples.t.sol
- stateful-use policy tests
- test/SHRINCSCalldataRetag.t.sol
- proves the calldata re-tag decoders agree with
abi.decodeover every vector and fuzzed well-formed envelope, and that malformed envelopes (wild offsets, truncation, aliasing) land in revert orfalsethrough the full verify path
- proves the calldata re-tag decoders agree with
- test/SHRINCSGuardPinning.t.sol
- one adversarial case per retained input guard, asserting malformed
input classes resolve to revert or
false
- one adversarial case per retained input guard, asserting malformed
input classes resolve to revert or
- test/SHRINCSVerifier.t.sol and
test/SHRINCSEnvelope.t.sol
- ERC-7913 raw verifier and envelope encoder/decoder tests
- test/SHRINCSConsumerExamples.t.sol
- consumer-example coverage for the generic ERC-7913 and explicit stateless integration contracts
- test/SHRINCSProfileInvariants.t.sol
- structural invariants and the profile-identity guard for the active
SHRINCSParamsprofile
- structural invariants and the profile-identity guard for the active
- test/SHRINCSMeasurements.t.sol
- gas measurement tests (figures below)
- test/WOTSPlus.t.sol
- standalone
WOTS+tests
- standalone
- signer/keygen, vector-signer, facade, export, and toy-profile suites under test/ exercise the test-only signing helpers
Test vectors:
- test/test_vectors/shrincs_sphincs_256s_keccak.json
- test/test_vectors/shrincs_account_wrapper_vectors.json
- test/test_vectors/wotsplus_keccak256.json
Public-Key Shape
The SHRINCS public key (SHRINCS.PublicKey) contains:
statefulPublicKeypublicKeyCommitment- stateless
pkSeed - stateless
hypertreeRoot
The stateless side follows the SPHINCS-style PK = (PK.seed, PK.root)
abstraction ([SPHINCSPLUS §5], [FIPS205 §9.1]):
pkSeedis the global public seed used by FORS and the hypertreehypertreeRootis the stateless public root
publicKeyCommitment binds the full hybrid bundle together. The verifier
checks that:
publicKey.publicKeyCommitmentmatches the commitment recomputed fromstatefulPublicKeypkSeedhypertreeRoot
- the caller's expected installed commitment matches that declared bundle commitment
This keeps the hybrid stateful/stateless public key coherent while preserving the SPHINCS-style stateless core.
Verification model
SHRINCS verifies signatures in place over calldata. No production verify
path copies the envelope or calls abi.decode, and no path walks the
envelope for canonicity.
Each verifier re-tags the calldata: it reads the ABI head offsets of the
opaque signature argument and points calldata-typed structs at the
signature fields where they already sit. The signature is then verified
directly from those fields. The one copy in the system is the stateless
delegation payload, which the SHRINCS verifier builds by slicing the
re-tagged signature region and forwarding it to its SPHINCS+C sibling.
The re-tag is sound because of two mechanisms:
- solc generates a calldata access check on every typed field read. An offset or length that falls outside the calldata bounds reverts before any field is used.
- a small set of retained input guards (documented in
docs/guard-applicability-review.md) pins the field lengths that the hash construction depends on — the public-key field split, the WOTS key windows, and the shape and count checks that the reconstruction loops read.
Together these turn every malformed envelope into a revert or a false
return. See Revert model for the caller-visible
behavior.
Revert model
The verify paths distinguish two failure classes:
- Malformed envelope bytes revert. Offsets or lengths outside the calldata bounds, and truncated envelopes, fail the solc calldata access check. The call reverts; it does not return a signature-invalid value.
- Well-formed but invalid signatures return the failure value. An
envelope whose bytes are well-formed but whose signature does not verify
returns
0xfffffffffrom the ERC-7913 and ERC-1271 paths, orfalsefrom the library entry points.
Acceptance widened from the earlier canonicity-walk model: any calldata
framing whose in-place field reads reproduce a valid signature's field
values now verifies. The verifier no longer requires the byte-exact
canonical encoding. This is a superset of what abi.decode tolerates:
the re-tagged reads are bounds-checked against calldatasize (the whole
transaction calldata), not the envelope slice, so a read may extend into
adjacent calldata such as the outer ABI zero-padding. Under masked-hash
(128s) profiles the low bytes of each hash node are zero, so a
tail-truncated envelope that abi.decode would reject reads its stripped
tail back from that padding and still verifies. Acceptance never reaches
a wrong-accept: accepted reads always equal a valid signature's exact
field values. One qualification applies to the stateless delegation
path: it slices a bounded signature region before forwarding it to the
SPHINCS+C sibling, so a framing that reproduces those fields only by
reading out into adjacent calldata can revert or return false at the
sibling rather than verify there.
Malleability caveat. Because these field-value-equivalent framings all verify, an external consumer that keys, caches, or deduplicates on the raw signature bytes can see two distinct byte strings that both verify for the same message. Consumers that need a single canonical byte form must re-encode the signature themselves before keying on it. Nothing in this repository keys on raw signature bytes; the wrapper's replay protection binds the message and account state, not the signature encoding.
Verification Flows
flowchart LR
subgraph "Stateful path (normal case)"
A1["action context<br/>(domain, nonce, keyVersion,<br/>actionType, payloadHash)"] --> A2["statefulActionMessageHash"]
A2 --> A3["validate expected<br/>public root"]
A3 --> A4["recompute compact WOTS-C<br/>pk hash from 64 chains<br/>(target-sum 480 check)"]
A4 --> A5["fold unbalanced-XMSS<br/>auth path"]
A5 --> A6{"== stateful root?"}
end
subgraph "Stateless path (fallback / rotation authorization)"
B1["action or rotation<br/>message hash"] --> B2["validate public key +<br/>public root"]
B2 --> B3["FORS-C: digest, omitted final<br/>tree leaf == 0, rebuild 21 tree<br/>roots → fors-pk root"]
B3 --> B4["forsRoot"]
B4 --> B5["8 hypertree layers:<br/>WOTS-C verify + Merkle path,<br/>root chains upward"]
B5 --> B6{"== hypertree root?"}
end
Each verifier has two forms:
- the canonical account-style form shown below, which builds the
canonical message hash from a typed
ActionContext - a raw form (
verifyStatefulUncheckedMessage,verifyStatelessUncheckedMessage) that verifies caller-supplied message bytes directly; the ERC-7913 verifier and the tests use it
1. Stateful verification
SHRINCS.verifyStateful(expectedPublicKeyCommitment, publicKey, actionContext, signature)
verifyStateful(...) computes a canonical hash from ActionContext:
domainSeparatornoncekeyVersionactionTypepayloadHash
payloadHash should be the hash of a typed action payload. This path does
not accept free-form account-operation bytes.
The canonical path rejects invalid contexts:
expectedPublicKeyCommitment == 0domainSeparator == 0actionType == 0payloadHash == 0
Both forms verify:
- the provided
expectedPublicKeyCommitmentmatchespublicKey.publicKeyCommitment - the embedded stateful public key
- compact
WOTS-Creconstruction - the unbalanced XMSS-style authentication path
2. Stateless verification
SHRINCS.verifyStateless(expectedPublicKeyCommitment, publicKey, actionContext, signature)
verifyStateless(...) computes the same canonical ActionContext hash and
applies the same context rejections as the stateful path.
Both forms verify:
- the provided
expectedPublicKeyCommitmentmatchespublicKey.publicKeyCommitment FORS-C- hypertree layer traversal
- stateless
WOTS-C - the final hypertree root against the public key
Design note — hypertree coordinate derivation. SHRINCS derives the
hypertree tree/leaf coordinates sequentially per layer rather than following
the [FIPS205 §8.2] index recurrence. The FORS digest fixes layer 0's
coordinate; each upper layer's leaf index is the low
HYPERTREE_HEIGHT / NUM_HYPERTREE_LAYERS bits of the layer below's tree
index, and its tree index is the remaining high bits.
Hypertree.verifyHypertree derives these coordinates and the test signer
follows the same recurrence, so a signature cannot choose independent
upper-layer addresses. The coordinates are not carried in the signature:
the verifier seeds layer 0 from the FORS digest and derives every upper
layer, making the derived value the sole coordinate input (the pre-T6
carried treeIndex/leafIndex fields were removed with the vector
regeneration). This is a deliberate, documented departure from FIPS 205
(marked Deviates from [FIPS205 §8.2]: in the code); do not change either
side toward the FIPS recurrence without regenerating all vectors.
3. Stateful-key rotation authorization
SHRINCS.rotateStatefulViaStateless(
expectedPublicKeyCommitment,
currentPublicKey,
rotationContext,
recoverySignature,
nextStatefulKey
)
This is a verifier-side authorization helper, not signer recovery logic.
It:
- computes a canonical rotation message hash from:
expectedPublicKeyCommitmentcurrentPublicKey.publicKeyCommitmentrotationContextnextStatefulKey.publicKeyCommitment
- verifies a stateless recovery signature over that canonical hash under the current key
- validates the proposed next stateful public key
- decodes the next stateful key and rejects
maxSignatures == 0 - recomputes the declared next commitment from the next stateful key plus the current stateless seed/root
- rejects mismatches between the declared and recomputed next commitment
- rejects zero
domainSeparator - returns the next public key commitment on success
- returns
bytes32(0)on failure
4. Full SHRINCS-key rotation authorization
SHRINCS.statelessRotate(
expectedPublicKeyCommitment,
currentPublicKey,
rotationContext,
recoverySignature,
nextKey
)
This verifies a stateless recovery signature authorizing a full next SHRINCS key bundle.
It:
- computes a canonical full-rotation message hash from:
expectedPublicKeyCommitmentcurrentPublicKey.publicKeyCommitmentrotationContextnextKey.publicKeyCommitment
- verifies the current stateless recovery signature over that canonical hash
- validates the full next key payload
- decodes the next stateful key and rejects
maxSignatures == 0 - rejects zero
domainSeparator - recomputes the declared next commitment from the full next key payload
- rejects mismatches between the declared and recomputed next commitment
- returns the next public key commitment on success
- returns
bytes32(0)on failure
5. ERC-1271 adapter
The example wrapper exposes:
isValidSignature(bytes32 hash, bytes signature) external view returns (bytes4)
This adapter is limited to canonical account-action envelopes. It is not a generic raw SHRINCS verifier.
Supported envelope modes:
0x01 || abi.encode(publicKey, actionType, payloadHash, statefulSignature)0x02 || abi.encode(publicKey, actionType, payloadHash, statelessSignature)
The adapter:
- verifies the envelope in place over calldata
- the mode byte selects the stateful or stateless decoder
- the decoder re-tags the calldata offsets and reads the signature fields directly; it does not copy or re-materialize the structure
- malformed envelope bytes revert; well-formed bytes that fail
verification return
0xffffffff - any framing whose in-place reads reproduce a valid signature's field values verifies — a superset of decode-equivalent re-encodings (see Verification model)
- rebuilds the current
ActionContextfrom wrapper-owned statedomainSeparator()noncekeyVersion
- checks that
hashmatches the current canonical action hash for that mode - enforces current wrapper policy gates
- stateful leaf policy
- recovery-mode/stateless-budget gating
- verifies the embedded SHRINCS signature without mutating storage
Important semantics:
- ERC-1271 validity here is snapshot-based.
- a signature can be valid now and invalid later after
nonce,keyVersion, policy state, or key state changes
- a signature can be valid now and invalid later after
- known-mode envelopes with malformed bytes revert; well-formed
envelopes that fail verification return
0xffffffff - legacy raw vectors and primitive raw SHRINCS signatures are rejected on this path
- stateless/key-rotation authorizations are not part of this ERC-1271 surface
- minimum gas: verification runs entirely in-contract over calldata,
with no external call. A well-formed envelope that fails verification
returns
0xffffffff; malformed envelope bytes and any execution failure, including out-of-gas, revert. A valid signature is never reported invalid. Callers must forward gas comfortably above the measured figures in Gas measurements.
6. ERC-7913 raw verifier
This repository also includes a standalone ERC-7913 verifier for the raw stateful SHRINCS path.
verify(bytes key, bytes32 hash, bytes signature) → bytes4
- key — the 32-byte SHRINCS
publicKeyCommitment. - hash — the 32-byte message the signature is verified against; the caller constructs it (typically a domain-separated digest).
- signature —
abi.encode(PublicKey, StatefulSignature), theSHRINCSstateful envelope. - For ABI-valid
verify(...)calls, returns0x024ad318on success and0xffffffffon verification failure or a wrong-length key. Malformed envelope bytes revert: the verifier re-tags the calldata in place, and offsets or lengths that fall outside the calldata bounds fail the solc-generated access check rather than being reported as an invalid signature.
The envelope fields are:
PublicKey = (statefulPublicKey, publicKeyCommitment, pkSeed, hypertreeRoot)StatefulSignature = (randomizer, counter, chains, authPath)
The ERC-7913 verifier is intentionally narrow:
- it supports the stateful raw path only
- it checks signature validity only
- it enforces no wrapper policy: nonce, keyVersion, actionType, payloadHash, and leaf-consumption tracking are all the caller's job
- the envelope carries no in-band mode tag or version prefix; the contract
exposes a
VERSION_TAGconstant identifying its key/envelope format family, and each deployable subclass adds aPROFILE_TAGidentifying its compiled parameter set
Minimal consumer-side examples:
- contracts/examples/SHRINCSERC7913ConsumerExample.sol
- stores a verifier address and opaque key bytes
- calls
IERC7913SignatureVerifier.verify(...) - suitable for integrators who already own message construction and only need a yes/no authorization check over an ERC-7913 blob
- contracts/examples/SHRINCSStatelessConsumerExample.sol
- stores a concrete
SHRINCSVerifieraddress and key bytes - calls
verifyStateless(...)on that verifier directly - shows the non-generic SHRINCS stateless recovery flow separately from the base ERC-7913 interface
- stores a concrete
These examples are intentionally thin:
- they do not own nonce,
keyVersion, stateful-leaf tracking, or rotation state - the soft
isAuthorized/isAuthorizedStatelesspath normalizes verifier reverts (including out-of-gas) tofalse;requireAuthorized/requireAuthorizedStatelessbubble them - constructors validate only the 32-byte length of the stored key, not its content
- they model an external consumer of the verifier, not an account wrapper
Concrete consumer flow:
- Deploy the desired verifier contract
- for example
SHRINCS256sKeccak
- for example
- Store the verifier key bytes
- for SHRINCS this is normally the 32-byte
publicKeyCommitment
- for SHRINCS this is normally the 32-byte
- Compute the 32-byte message hash the verifier should authorize
- typically a domain-separated application digest
- Call the verifier
- directly:
IERC7913SignatureVerifier(verifier).verify(key, hash, signature) - or through
SHRINCSERC7913ConsumerExample.sol,
which stores
verifierandkeyand exposesisAuthorized(hash, signature)
- directly:
SHRINCSVerifier itself is an abstract base; the deployable contracts are
the per-profile subclasses (SHRINCS256sKeccak, SHRINCS128sQ18Keccak,
SHRINCS128sQ20Keccak), each compiled under its own build profile.
Verification semantics
At a high level, SHRINCSVerifier.verify(...):
- reads
keyas the expected bundle commitment - re-tags
signatureas a stateful SHRINCS envelope in place over calldata, with no copy - passes the calldata-typed structs to the
SHRINCSlibrary over the 32-bytehash - calls
SHRINCS.verify(...), which enforces the commitment match, bundle shape,WOTS-Creconstruction, and the unbalanced-tree root - returns the ERC-7913 magic value on success, or
0xffffffffon verification failure
A well-formed envelope that fails verification returns 0xffffffff.
Malformed envelope bytes revert: the re-tag reads offsets and lengths
that the solc-generated calldata access check rejects when they fall
outside the calldata bounds.
verify(...) makes no external call: it runs entirely in-contract over
calldata, so any execution failure, including out-of-gas, reverts to the
caller rather than being misreported as an invalid signature.
Security scope
This path is not the same as the canonical account-wrapper flow.
The canonical wrapper binds signatures to:
domainSeparatornoncekeyVersionactionTypepayloadHash
and can also enforce stateful leaf-use policy.
The ERC-7913 raw verifier does none of that. It answers only: does this stateful SHRINCS signature verify for this commitment and these exact 32 message bytes?
That makes it suitable as a low-level verifier surface, but not as a drop-in replacement for the wrapper-owned account flow unless the caller supplies the missing replay protection and policy checks externally.
Interface choice
Use the verifier surfaces for different purposes:
SHRINCSERC7913ConsumerExample: minimal external-app integration over the generic ERC-7913verify(...)interfaceSHRINCSStatelessConsumerExample: minimal external-app integration over the verifier's explicit stateless delegation path- canonical wrapper functions: account authorization with nonce, keyVersion, action, and policy enforcement
- ERC-1271 on the example account: snapshot validation of canonical wrapper-owned signatures
- ERC-7913 raw verifier: low-level cryptographic validity only
For ERC-7913 blobs, callers should treat the verifier contract address as the algorithm/version identifier. Since the envelope has no in-band version tag, opaque blobs are fragile if stored or forwarded across verifier families.
Compile-Time Constants
Each build compiles the verifier for exactly one SHRINCS configuration. Callers do not supply selectors or arbitrary numeric tuples.
The constants live in the profile-selected
contracts/profiles/<profile>/SHRINCSParams.sol library, imported
through the shrincs-profile/ remapping so reference sites stay
profile-agnostic (see Profiles). The default 256s profile
pins these values (citation keys per
CODINGSTANDARDS.md §1):
STATELESS_SIGNATURE_LIMIT = 2^20 = 1,048,576HASH_LEN = 32— thensecurity parameter [FIPS205 §11]HYPERTREE_HEIGHT = 64— thehparameter [FIPS205 §7]NUM_HYPERTREE_LAYERS = 8— thedparameter [FIPS205 §7]FORS_TREE_HEIGHT = 14— theaparameter [SPHINCSPLUS §5.5]NUM_FORS_TREES = 22— thekparameter [SPHINCSPLUS §5.5]WOTS_CHAIN_LEN = 16— thewparameter [WOTSPLUS §3]NUM_WOTS_CHAINS = 64— thelenparameter [WOTSPLUS §3]WOTS_TARGET_SUM_STATEFUL = 480— the WOTS-C digit-sum target,len * (w - 1) / 2HASH_MASK = bytes32-of-ones— high-aligned truncation mask applied at every hash-producing site; a no-op for 256s, a real truncation for the 128s profiles- canonical message hashes bind
HASH_SUITE_KECCAK_256
Important distinction:
- the stateless primitive uses the
256s-style compile-time structure ([FIPS205 §11]:h = 64,d = 8,a = 14,k = 22) STATELESS_SIGNATURE_LIMIT = 2^20is enforced by the wrapper/account layer as the maximum accepted number of stateless signatures under one installed stateless key- the code pairs a
256s-style stateless primitive with a stricter operational cap of2^20
Two verifier rules deserve explicit mention:
FORS-CverifiesNUM_FORS_TREES - 1revealed entries, not allNUM_FORS_TREES- the final FORS tree is omitted by construction
- verification rejects any digest whose omitted final tree would need a nonzero leaf index
WOTS-Cuses the fixed target sum instead of an explicit checksum suffix- the reconstructed base-
wdigits must add up toWOTS_TARGET_SUM_STATEFUL
- the reconstructed base-
For the stateful WOTS-C / XMSS path, the code also assumes:
STATEFUL_PUBLIC_KEY_BYTES = 68- encoded as
pkSeed || root || maxSignatures 32 + 32 + 4bytes
- encoded as
WOTS_CHAINS_STATEFUL = 64- the stateful compact
WOTS-Csignature carries 64 chains
- the stateful compact
WOTS_BASE_STATEFUL = 16- stateful message digits are expanded in base 16
WOTS_TARGET_SUM_STATEFUL = 480- the 64 base-16 digits must sum to 480 for the signature to verify
This is deliberate: the library claims no support for arbitrary future numeric tuples, even ones that are superficially shape-compatible.
Profiles
Three compile-time profiles exist, selected by the shrincs-profile/
Foundry remapping in foundry.toml (each build profile also gets its own
out directory):
256s(default). The SPHINCS+-256s-style parameter set listed above. The split intoSHRINCSParamskept the 256s production build byte-identical to the pre-split verifier (metadata-stripped deployed bytecode compared before/after).128s-q18and128s-q20(experimental). 16-byte truncated-hash profiles (HASH_LEN = 16, high-aligned viaHASH_MASK), single-layerh = 18hypertree,a = 24,k = 6, 32 stateful WOTS chains. They differ only in the stateless budget:2^18for q18,2^20for q20. Both compile and pass the structural and profile-invariant test sets, and their stateless paths are verified against Rust-generated signature vectors bySHRINCSSphincs128sVectors. Neither limit has an approved birthday-bound security analysis. These profiles are retained only for tests, vectors, and research: no production Foundry profile, deployment script, salt, or advertised address is provided.
Build a non-default profile with FOUNDRY_PROFILE:
FOUNDRY_PROFILE=test-128s-q18 forge build
FOUNDRY_PROFILE=test-128s-q18 forge test
test/SHRINCSProfileInvariants.t.sol checks the active profile's
structural invariants and carries a profile-identity guard: a build whose
shrincs-profile/ remapping was shadowed (say, by a top-level
remappings.txt) fails closed. CI builds, lints, and tests all three
profiles and rejects a top-level remappings.txt.
On-Chain Integration State
The SHRINCS library is only responsible for signature verification and
rotation-authorization checking.
It manages none of the surrounding account or protocol state:
- the currently active on-chain SHRINCS public key
- nonces / sequence numbers
- key version / rotation epoch
- recovery policy flags
- pending rotation state
- balances, permissions, or other account logic
A real on-chain verifier or account contract needs an initialization step that stores at least:
currentSHRINCSPublicKey
and usually also:
noncekeyVersion
This is outside the SHRINCS library itself. The library only checks whether the provided signature or rotation authorization is valid for the provided inputs.
The integrating contract should pass its stored currentSHRINCSPublicKey
into the library as expectedPublicKeyCommitment. The library enforces that
the provided publicKey bundle is pinned to that expected key.
For rotation flows, the integrating contract should also supply a
rotationContext carrying at least:
domainSeparatornoncekeyVersion
The library uses that context to build the canonical rotation message hash that must be signed by the stateless recovery path.
Example Wrapper Contract
The library is storage-free by design. A real on-chain verifier or account contract must own the account state and feed that state into the library on every call.
Reference implementation:
- contracts/examples/SHRINCSAccountVerifierExample.sol
- test/SHRINCSAccountVerifierExample.t.sol
- test/SHRINCSStatefulPolicyExamples.t.sol
The example contract is intentionally small. It shows how wrapper-owned state should interact with the library for:
- stateful action verification
- stateless action verification
- ERC-1271 view-only validation of canonical account-action envelopes
- stateless full-key rotation
- stateless usage-limit enforcement
This is not the same integration model as the consumer examples above.
- contracts/examples/SHRINCSERC7913ConsumerExample.sol
and
contracts/examples/SHRINCSStatelessConsumerExample.sol
show an external contract that calls a verifier and stores only verifier
configuration (
verifier,key) - contracts/examples/SHRINCSAccountVerifierExample.sol shows an account-style wrapper that owns freshness, policy, and rotation state and rebuilds canonical wrapper-controlled hashes on-chain
Use the consumer examples when the integrator already has its own message framing and only wants signature validity. Use the account wrapper when the contract itself must enforce replay scoping, key epochs, stateful-leaf policy, and recovery rotation.
Stateful-use policies
The example wrapper also shows account-layer policies for handling stateful
XMSS leaf use. These are wrapper policies, not part of the SHRINCS library
itself.
StatefulPolicy.MonotonicIndex- stores
nextStatefulLeafIndex - accepts only the next expected leaf
- prevents replay/rollback cleanly
- stricter operationally because skipped leaves are not allowed
- brittle if signer state and account state drift apart
- a mismatch can lock out otherwise valid future leaves until the key is rotated or policy is changed
- stores
StatefulPolicy.RecoveryRotation- blocks the stateful path for the entire recovery-rotation policy epoch
- requires an explicit
enterRecoveryMode()call before stateless recovery rotations are accepted - models the "recover, then rotate to a fresh key" workflow
- works best when recovery mode is treated as a bridge to rotation, not as a long-term steady state
- if a system enters recovery-rotation policy and never rotates out, the stateful path loses most of its practical value
StatefulPolicy.LeafBitmap- stores a bitmap of used stateful leaf indices
- rejects reuse of previously used leaves
- allows any unused leaf to be used in any order
- more flexible than monotonic indexing, but storage grows without bound as leaves are consumed
- repeated bitmap writes can become expensive for long-lived accounts
- better fit for small trees or higher-assurance accounts than for high-throughput accounts
The integrator chooses which policy fits the account design. In the example wrapper, policy-changing functions are owner-gated.
Policy caveats
Policy changes are sensitive administrative actions.
- even when owner-gated, switching policy mid-lifecycle can change which future stateful signatures are accepted
- production wrappers should treat policy changes as explicit governance or account-owner operations
- the example wrapper freezes policy changes after the first successful stateful signature in a key epoch
- changing policy after stateful use requires rotating to a fresh key first
Fresh-key rotation must reset stateful tracking state.
- when a new SHRINCS key is installed, stale state such as:
nextStatefulLeafIndexrecoveryMode- active stateful policy mode
- used-leaf bitmap marks from the prior key epoch must not be carried into the new key epoch
- the example wrapper resets this state on fresh-key installation
- when a new SHRINCS key is installed, stale state such as:
Raw verifier paths are lower-level interfaces.
- the example wrapper does not expose raw stateful or raw stateless verification entry points
- raw verification remains available only through lower-level libraries, the ERC-7913 verifier, and test harnesses
- production account flows should prefer the canonical
verifyStatefulAction(...)/verifyStatelessAction(...)style interfaces
Stateless rotation is recovery-only in the example wrapper.
rotateToFreshKey(...)androtateFullKey(...)both requireStatefulPolicy.RecoveryRotation- both also require
recoveryMode == true - this keeps stateless signatures as recovery authority rather than a normal-operation rotation bypass
RecoveryRotationdisables the stateful path immediately.- selecting
StatefulPolicy.RecoveryRotationblocks stateful verification even beforeenterRecoveryMode() enterRecoveryMode()only arms stateless recovery rotation; it does not change stateful-path availability
- selecting
Stateless usage accounting follows the stateless key, not only the bundle epoch.
rotateToFreshKey(...)replaces only the stateful subkey- it preserves the current stateless key material
- it preserves
statelessSignaturesUsedand first consumes one stateless use for the recovery signature itself rotateFullKey(...)replaces the full bundle including the stateless key material- it resets
statelessSignaturesUsedfor the newly installed stateless key after consuming the recovery signature under the old key
The example wrapper binds its signing domain to both contract identity and chain context.
- the domain is derived from a stable tag,
block.chainid, andaddress(this) - production wrappers should keep that property even if they change the exact domain-tag scheme
- the domain is derived from a stable tag,
What the wrapper must handle:
- store
currentSHRINCSPublicKey - store and increment
nonce - store and increment
keyVersion - store and enforce
statelessSignaturesUsed < STATELESS_SIGNATURE_LIMIT - define a stable
domainSeparator - define the typed action payloads whose hash becomes
payloadHash - decide which path is allowed for which operation
- update stored key state only after successful rotation authorization
What the wrapper should not delegate to users:
- choosing
expectedPublicKeyCommitment - choosing the stored
nonce - choosing the stored
keyVersion - choosing an empty
domainSeparator - bypassing the typed
payloadHashflow for normal account operations - changing stateful-use policy unless explicitly authorized
Test Coverage
Current tests cover:
Stateful path
- valid stateful signature verifies
- wrong message is rejected
- wrong public key is rejected
- wrong expected public root is rejected
- zero expected public-key commitment is rejected
- corrupted stateful signature is rejected
- tampered stateful authentication path is rejected
- mismatched stateless root is rejected
- signature at
maxSignaturesboundary verifies - signature exceeding
maxSignaturesis rejected - malformed
pkSeedlength is rejected - malformed stateful public-key length is rejected
- wrong stateful
WOTS-Cchain count is rejected - empty stateful authentication path is rejected
- canonical action hash changes when payload changes
- zeroed account-style action context is rejected
Stateless path
- valid stateless signature verifies
- wrong message is rejected
- tampered
FORSdata is rejected - tampered hypertree
WOTS-Cpublic-key hash is rejected - tampered hypertree authentication path is rejected
- tampered component public-key vector is rejected
- wrong expected public root is rejected
- zero expected public-key commitment is rejected
- malformed
pkSeedlength is rejected - malformed
hypertreeRootlength is rejected - empty hypertree signatures are rejected
- dropped hypertree layers are rejected
- dropped
FORSentries are rejected - short
FORSrandomizers, secret leaves, auth paths, and auth nodes are rejected - malformed hypertree
WOTS-Cchain length is rejected - wrong hypertree authentication path length is rejected
- canonical action hash changes when nonce changes
- zeroed account-style action context is rejected
Rotation authorization helpers
rotateStatefulViaStateless(...)- canonical rotation hash changes when the next stateful key changes
- rejects legacy stateless signatures that were not signed over the canonical rotation hash
- rejects malformed next stateful public key
- rejects next stateful keys with
maxSignatures == 0 - rejects zero
domainSeparator
statelessRotate(...)- canonical rotation hash changes when the next key bundle changes
- rejects legacy stateless signatures that were not signed over the canonical rotation hash
- rejects malformed next key bundles
- rejects next stateful keys with
maxSignatures == 0 - rejects zero
domainSeparator
Example wrapper policies
- wrapper initialization stores the expected owner, key, nonce, key version, and default policy state
- failed wrapper verification and rotation calls preserve account state
- wrapper domain separators differ across contract instances
- owner-gated policy changes are enforced
- non-owner policy changes are rejected
- non-owner recovery-mode toggles are rejected
- entering recovery mode outside
RecoveryRotationpolicy reverts - monotonic-index policy rejects rollback to an earlier expected leaf
- default monotonic-index policy rejects repeated use of the same valid stateful leaf
- monotonic-index policy accepts the expected leaf once and rejects replay
- monotonic-index policy rejects unexpected leaf indices
- policy changes freeze after the first successful stateful use in a key epoch
- recovery-rotation policy blocks stateful use after recovery mode is entered
- under
RecoveryRotation, stateless action verification is rejected until the owner enters recovery mode - full-key rotation requires
RecoveryRotationpolicy and entered recovery mode - recovery-rotation policy rejects legacy rotation authorization and stays in recovery mode
- leaf-bitmap policy marks a leaf as used and rejects reuse
- fresh-key installation clears stale stateful tracking state
- fresh-key installation resets the leaf-bitmap namespace
- fresh-key installation always resets stateless usage
- fresh-key installation unfreezes policy changes for the new key epoch
- ERC-1271 rejects malformed, unknown-mode, and legacy raw-signature envelopes without mutating state
- stateless action verification and rotation reject calls at the stateless usage limit
- stateful-only rotation consumes one stateless recovery use and preserves stateless usage accounting
- stateful-only rotation at the final stateless slot consumes it and rejects the next stateless use
- repeated stateful-only rotation does not mint fresh stateless budget
- full-key rotation consumes one stateless recovery use under the old key and resets stateless usage accounting for the new key
- stateful-only and full-key rotations emit dedicated stateless-usage events
Calldata re-tag and envelope handling
- the calldata re-tag decoders agree with
abi.decodeover every vector and fuzzed well-formed envelope - any framing whose in-place reads reproduce a valid signature's field values verifies — a superset of decode-equivalent re-encodings that, under masked-hash profiles, includes tail truncations read back from ABI padding
- malformed envelope bytes (wild offsets, oversized lengths, in-bounds
aliasing) land in revert or
falsethrough the full verify path
ERC-7913 raw verifier and codec
- ERC-7913
verify(...)returns0x024ad318for a valid stateful raw signature - valid signatures from multiple in-budget stateful leaves verify
- wrong key lengths return
0xffffffffwithout reverting - wrong commitments, tampered hashes, tampered WOTS chains, tampered auth
paths, and mismatched key bundles return
0xffffffff - empty, garbage, truncated, trailing-byte, and malformed SHRINCS envelopes are rejected
- fuzzed ABI-valid
verify(...)inputs return the failure value instead of reverting SHRINCS.decodePublicKeyCommitment(...)accepts exactly 32-byte keys and rejects other lengths without revertingSHRINCS.statefulEnvelope(...)re-tags canonical and other field-value-equivalent framings in place, and reverts on out-of-bounds offsets or lengthsSPHINCSPlusC.toMessage(...)maps the ERC-7913bytes32 hashto exactly those 32 packed bytes- ERC-7913 consumer examples accept
verifier || keysigners and reject no-code, wrong-verifier, short-signer, and non-magic-return cases - 20-byte signer values use the ERC-1271 fallback path rather than ERC-7913 with an empty key
Test-only signer and export helpers
- Solidity keygen rejects zero and excessive stateful budgets
- Solidity keygen is deterministic for a fixed seed and matches Rust output
- test-only stateful signing advances the signing leaf and rejects exhausted keys
- canonical stateful-action signing rejects malformed public-key commitment fields
- staged and high-level stateless vector signing produce verifier-feedable signatures
- account-aware signer helpers produce action and rotation signatures that the example wrapper accepts
- account-vector export produces wrapper-ready action and rotation bundles, including calldata and ERC-1271 envelopes
Toy stateless profile
- toy stateless signing verifies successfully
- toy stateless signing is deterministic
Gas Measurements
Per-profile call gas for the five paths measured by
test/SHRINCSMeasurements.t.sol: the
stateful and stateless canonical wrapper calls, the stateful and
stateless ERC-1271 checks, and the ERC-7913 verifyStateless delegation
to the pinned SPHINCSPlusC sibling. The ERC-1271 figures verify the
envelope in place over calldata; the delegation figure calls
verifyStateless directly. Each cell is the gas the measured .call()
consumed (vm.lastCallGas().gasTotalUsed), excluding setup and calldata
encoding.
| Measurement | default | 128s-q18 | 128s-q20 | 256s-sha2 |
|---|---|---|---|---|
stateful.canonical_wrapper_call_gas |
190,792 | 117,759 | 117,759 | 281,063 |
stateful.erc1271_call_gas |
167,779 | 94,746 | 94,746 | 258,050 |
stateless.canonical_wrapper_call_gas |
1,629,049 | 192,109† | 192,109† | 2,420,861 |
stateless.erc1271_call_gas |
1,606,819 | 191,906† | 191,906† | 2,398,565 |
stateless.verify_stateless_delegation_gas |
1,660,931 | 204,614 | 204,614 | 2,455,228 |
† raw SHRINCS.verifyStatelessUncheckedMessage call (wrapper context-hash derivation excluded); see README.md "Gas Measurements" for detail.
Measured 2026-07-13 at each profile's own solc/via_ir/optimizer
settings (default and 256s-sha2: optimizer runs 200; 128s-q18/q20 share
the same settings). Regenerate this table with scripts/gas-report.sh,
which runs every profile's SHRINCSMeasurements suite and re-emits it.
The default-profile stateless figures fall ~50k from earlier baselines: the T6 wire change drops the carried per-layer tree/leaf coordinates from the stateless signature (the verifier now derives them from the FORS digest), shrinking the envelope calldata and its decode. Because T6 also changes the stateful chain-hash preimage (F-08) and the profile-bound commitment, and because WOTS/FORS chain-walk lengths are data-dependent on the regenerated vectors, these figures are recorded actuals, not equality anchors.
For the stateful rows, the ERC-1271 figure falls below the canonical
wrapper call in every profile: the wrapper builds and validates the
typed ActionContext and canonical hash on-chain, whereas the ERC-1271
path verifies a precomputed hash, and the in-place re-tag verification
costs less than that context machinery.
In-process stateless keygen/signing is compute-infeasible at 128s (full
2^a FORS trees plus the fixed hypertree), so the 128s-q18/q20 stateless
rows measure a different call shape than the other profiles: instead of
the account wrapper, they call the same underlying SHRINCS library
verification through a raw-message test harness
(MeasurementRawStatelessHarness) fed by the profile's Rust-anchored
vector, since the wrapper's context-derived hash cannot match a fixed
vector signature. The delegation row still calls the
production verifyStateless entrypoint directly at every profile, since
that entrypoint already takes the hash as a parameter. A narrower
verify-only figure over the same 128s vector, excluding call overhead,
is in SHRINCSSphincs128sVectors.t.sol (stateless.128s_verify_gas,
~243k).
A default-profile-only data point,
SHRINCSVerifier.t.sol::testGasSnapshotHappyPathVerify (stateful,
adapter-direct verify without the account wrapper), measures 349,162
gas; reproduce with forge test --match-test testGasSnapshotHappyPathVerify -vv.
Reproduce the table with:
scripts/gas-report.sh
or a single profile directly:
FOUNDRY_PROFILE=test-128s-q18 forge test --match-contract SHRINCSMeasurements -vv
These figures require a clean full build per profile, which
scripts/gas-report.sh performs; running the single-profile command above
over an out/ directory a different profile already populated can read
stale harness bytecode and shift a cell (the delegation row drifts by a few
hundred gas from its 1,660,931 clean-build value this way).
The stateful entrypoints make no external call, so their execution
failures (including out-of-gas) simply revert. The stateless delegation
(verifyStateless calling its SPHINCS+C sibling) is the one remaining
external call; an out-of-gas there reverts rather than being misreported
as 0xffffffff (see the ERC-1271 and ERC-7913 sections above). Either
way, forward gas comfortably above these figures.
Development
This project uses Foundry for Solidity build and test work. Coding rules, citation conventions, and the enforcement gates (format, lint, line cap, tests, static analysis, and fuzz/invariant properties) are defined in CODINGSTANDARDS.md §8; GitLab CI runs them all.
Agent tooling
The repository tracks two kinds of files for LLM coding agents:
- AGENTS.md: project instructions that every agent reads.
.agents/skills/: tool-neutral skills in the Agent Skills format (<name>/SKILL.md). Thesolidity-standardsskill walks CODINGSTANDARDS.md for any agent that writes or reviews Solidity here.
Every other agent file is local state and is ignored by .gitignore:
.claude/, CLAUDE.md, .codex/, .beads/, .serena/, and the
bd-generated .agents/skills/beads/. Put project instructions in
AGENTS.md. Put reusable agent workflows in .agents/skills/, never in
a tool-specific directory. Do not commit tool configuration or
task-tracking databases.
Codex, Cursor, Gemini CLI, Copilot, and most other agents read
.agents/skills/ directly. Claude Code reads only .claude/skills/, so
link the shared skill into it once. The link stays local because
.claude/ is ignored:
mkdir -p .claude/skills
ln -s ../../.agents/skills/solidity-standards .claude/skills/
Prerequisites
Install Foundry:
curl -L https://foundry.paradigm.xyz | bash
foundryup
Build
The default foundry.toml profile sets via_ir = true, which the SHRINCS
verifier requires to avoid stack-too-deep compilation failures:
forge build
To build a non-default profile, set FOUNDRY_PROFILE
(FOUNDRY_PROFILE=test-128s-q18 forge build); see Profiles and the
profile sections in foundry.toml.
Test
Run the full verifier test suite:
forge test
Expected result as of 2026-07-13 on the default profile:
266 tests passed, 0 failed.
Using Rust-Generated SHRINCS Vectors
The Solidity verifier tests are intended to consume vectors generated by the Rust SHRINCS signer/keygen implementation. The Solidity code verifies those vectors; it does not generate production signing material.
The vector file expected by this repository is:
test/test_vectors/shrincs_sphincs_256s_keccak.json
From the Rust repository, generate the vector file:
cargo test --test generate_shrincs_vectors -- --ignored --nocapture
The Rust generator writes:
tests/test_vectors/shrincs_sphincs_256s_keccak.json
Copy that file into this Solidity repository:
cp /path/to/hashsigs-rs/tests/test_vectors/shrincs_sphincs_256s_keccak.json \
/path/to/hashsigs-solidity/test/test_vectors/shrincs_sphincs_256s_keccak.json
Then run the SHRINCS-only Solidity tests:
forge test --match-contract SHRINCSSphincs256sVectorsTest -vv
forge test --match-contract SHRINCSAccountVerifierExampleTest -vv
The vector JSON contains the Rust-generated public keys, messages, signatures, and negative/tampered cases. It also includes compatibility calldata fields used by the current Foundry vector decoder.
Test-Only Signer Helpers
This repository also contains test-only Solidity signer helpers under
test/helpers/. These are not part of the production
verifier surface in contracts/.
test/helpers/SHRINCSTestSigner.sol- deterministic Solidity keygen plus stateful signing helpers
- used to mirror Rust signer behavior in tests
test/helpers/SHRINCSStatelessVectorSigner.sol- staged storage-backed stateless signer for exact production-profile vector generation
- splits FORS and hypertree work across multiple calls to avoid EVM memory blowups
test/helpers/SHRINCSStatelessVectorSigningFacade.sol- thin test facade that drives the staged stateless signer through a
simpler
signFromSeed(...)/completeSession(...)API
- thin test facade that drives the staged stateless signer through a
simpler
test/helpers/SHRINCSAccountSigningFacade.sol- test-only account-aware signer facade for canonical wrapper flows
- rebuilds the live wrapper-owned
domainSeparator,nonce, andkeyVersionbefore signing stateful actions, stateless actions, and both rotation messages
test/helpers/SHRINCSAccountVectorExport.sol- packages account-aware signatures into wrapper-feedable bundles
- exports canonical message bytes,
abi.encodeCall(...)payloads, and ERC-1271 envelopes
Use these helpers for tests, debugging, and local vector generation only. They are not deployable wallet or signer contracts.
Account-Aware Signer and Export Flow
The account wrapper signs canonical, wrapper-owned messages, so test signing must bind:
domainSeparatornoncekeyVersionactionTypepayloadHash
for action flows, and the wrapper-owned rotation context for recovery flows.
The test-only account-aware path is:
test/helpers/SHRINCSAccountSigningFacade.sol- creates wrapper-bound stateful action signatures
- creates wrapper-bound stateless action signatures
- creates wrapper-bound stateless recovery signatures for
rotateToFreshKey(...)androtateFullKey(...)
test/helpers/SHRINCSAccountVectorExport.sol- converts those signatures into exportable bundles that can be fed directly into the live wrapper
test/SHRINCSAccountVectorExport.t.sol- emits the bundle bytes and immediately proves the wrapper accepts them
Run the export suite directly:
forge test --match-path test/SHRINCSAccountVectorExport.t.sol -vv
Or generate a JSON artifact from the emitted bundle bytes:
bash dev/export-account-vectors.sh
The default output artifact is:
test/test_vectors/shrincs_account_wrapper_vectors.json
To cross-check these Solidity-exported account vectors against the Rust verifier, copy that JSON into the Rust repository manually. The repositories are separate, so this handoff is intentionally not automated:
cp /path/to/hashsigs-solidity/test/test_vectors/shrincs_account_wrapper_vectors.json \
/path/to/hashsigs-rs/tests/test_vectors/shrincs_account_wrapper_vectors.json
Then run the Rust-side cross-check from hashsigs-rs:
cargo test --test solidity_account_vectors
The export tests emit four wrapper-feedable bundle categories:
testExportStatefulActionBundlestateful_vector_abi: ABI encoding of the full exported stateful-action bundlestateful_verify_calldata: calldata forverifyStatefulAction(...)stateful_1271_envelope: ERC-1271 payload for canonical stateful-action validation
testExportStatelessActionBundlestateless_vector_abi: ABI encoding of the full exported stateless-action bundlestateless_verify_calldata: calldata forverifyStatelessAction(...)stateless_1271_envelope: ERC-1271 payload for canonical stateless-action validation
testExportStatefulOnlyRotationBundlestateful_rotation_vector_abi: ABI encoding of the full exported stateful-only rotation bundlestateful_rotation_calldata: calldata forrotateToFreshKey(...)
testExportFullRotationBundlefull_rotation_vector_abi: ABI encoding of the full exported full-rotation bundlefull_rotation_calldata: calldata forrotateFullKey(...)
These exports are intended for local tooling, debugging, and cross-repo vector work. They are not a production signer interface.
Deployment
The canonical SHRINCS verifiers and their SPHINCSPlusC stateless delegates
deploy through CREATE3 Foundry scripts. A CREATE3 address depends only on
the factory and the guarded salt, not on the init code, so each verifier
profile gets a distinct, chain-invariant address.
Salts are sender-scoped (CreateX's permissioned mode), so only the
canonical deployer can deploy at an advertised address — see
script/CreateXSalt.sol. DEPLOYMENTS.md is the
registry: it holds the salts, profile tags, predicted addresses, the
deploy procedure, the superseded permissionless-salt deployments, and the
historical CREATE2 (verifier) and Hardhat-Ignition (WOTS+) mechanisms
these scripts replace.
Deploy each SPHINCSPlusC delegate before its SHRINCS sibling: CREATE3
fixes the address either way, but SHRINCSVerifier.verifyStateless
reverts on empty code, and each SHRINCS deploy script asserts its sibling
is already deployed at the pinned address.
script/DeploySPHINCSPlusC256sKeccak.s.sol— 256s stateless delegate (profileproduction)script/DeploySHRINCS256sKeccak.s.sol— 256s verifier (profileproduction)
The 128s-q18 and 128s-q20 contracts are deliberately absent from this
list. They are experimental test/vector artifacts and have no production
deployment scripts, production salts, or advertised addresses. CI enforces
this boundary with scripts/check-production-profiles.sh.
Each script asserts its FOUNDRY_PROFILE and refuses to run under the
wrong one. Example:
FOUNDRY_PROFILE=production forge script \
script/DeploySHRINCS256sKeccak.s.sol \
--rpc-url $RPC --private-key $DEPLOYER_PK --broadcast --verify
SHRINCS is testnet-only. The example wrapper
(contracts/examples/SHRINCSAccountVerifierExample.sol) is a reference
integration, not a canonical deployment; deploy it directly with
forge create when you need one.
Notes
forge-stdis a git submodule atlib/forge-std.- the default Foundry profile sets
via_ir = true; the verifier relies on it for clean builds.
License
Copyright (C) 2024-2026 quip.network
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
Explore related projects
codecrafters-io/build-your-own-x
Master programming by recreating your favorite technologies from scratch.
560,890 starspublic-apis/public-apis
A collective list of free APIs
497,126 starsmorluto/rea
Reverse engineer anything with agents, from app behavior down to native binaries.
442,020 starsopenclaw/openclaw
The AI that really does things. Any OS. Any Platform. The lobster way. 🦞
394,169 stars