Skip to content
Projects

QuipNetwork/hashsigs-solidity

View on GitHub

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.

How these counts are measured

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

Original

Hash-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 parameter w = 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-layer h = 18 hypertree, 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 by SHRINCSSphincs128sVectors.
    • 128s-q20 (experimental) — as 128s-q18 with 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: a FORS-C few-time signature carried up a hypertree of WOTS-C layers to the public root. Deployed per profile as SPHINCSPlusC256sKeccak, SPHINCSPlusC128sQ18Keccak, and SPHINCSPlusC128sQ20Keccak; 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-C plus an unbalanced XMSS-style authentication path
  • a stateless path
    • fallback / recovery verification path
    • a SPHINCS-style FORS-C + hypertree + WOTS-C structure

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:

Deployment and tooling:

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/&lt;profile&gt;/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 vectors:

Public-Key Shape

The SHRINCS public key (SHRINCS.PublicKey) contains:

  • statefulPublicKey
  • publicKeyCommitment
  • stateless pkSeed
  • stateless hypertreeRoot

The stateless side follows the SPHINCS-style PK = (PK.seed, PK.root) abstraction ([SPHINCSPLUS §5], [FIPS205 §9.1]):

  • pkSeed is the global public seed used by FORS and the hypertree
  • hypertreeRoot is the stateless public root

publicKeyCommitment binds the full hybrid bundle together. The verifier checks that:

  • publicKey.publicKeyCommitment matches the commitment recomputed from
    • statefulPublicKey
    • pkSeed
    • hypertreeRoot
  • 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 0xffffffff from the ERC-7913 and ERC-1271 paths, or false from 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:

  • domainSeparator
  • nonce
  • keyVersion
  • actionType
  • payloadHash

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 == 0
  • domainSeparator == 0
  • actionType == 0
  • payloadHash == 0

Both forms verify:

  • the provided expectedPublicKeyCommitment matches publicKey.publicKeyCommitment
  • the embedded stateful public key
  • compact WOTS-C reconstruction
  • 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 expectedPublicKeyCommitment matches publicKey.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:
    • expectedPublicKeyCommitment
    • currentPublicKey.publicKeyCommitment
    • rotationContext
    • nextStatefulKey.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:
    • expectedPublicKeyCommitment
    • currentPublicKey.publicKeyCommitment
    • rotationContext
    • nextKey.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 ActionContext from wrapper-owned state
    • domainSeparator()
    • nonce
    • keyVersion
  • checks that hash matches 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
  • 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), the SHRINCS stateful envelope.
  • For ABI-valid verify(...) calls, returns 0x024ad318 on success and 0xffffffff on 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_TAG constant identifying its key/envelope format family, and each deployable subclass adds a PROFILE_TAG identifying 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 SHRINCSVerifier address and key bytes
    • calls verifyStateless(...) on that verifier directly
    • shows the non-generic SHRINCS stateless recovery flow separately from the base ERC-7913 interface

These examples are intentionally thin:

  • they do not own nonce, keyVersion, stateful-leaf tracking, or rotation state
  • the soft isAuthorized/isAuthorizedStateless path normalizes verifier reverts (including out-of-gas) to false; requireAuthorized/ requireAuthorizedStateless bubble 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:

  1. Deploy the desired verifier contract
    • for example SHRINCS256sKeccak
  2. Store the verifier key bytes
    • for SHRINCS this is normally the 32-byte publicKeyCommitment
  3. Compute the 32-byte message hash the verifier should authorize
    • typically a domain-separated application digest
  4. Call the verifier
    • directly: IERC7913SignatureVerifier(verifier).verify(key, hash, signature)
    • or through SHRINCSERC7913ConsumerExample.sol, which stores verifier and key and exposes isAuthorized(hash, signature)

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(...):

  1. reads key as the expected bundle commitment
  2. re-tags signature as a stateful SHRINCS envelope in place over calldata, with no copy
  3. passes the calldata-typed structs to the SHRINCS library over the 32-byte hash
  4. calls SHRINCS.verify(...), which enforces the commitment match, bundle shape, WOTS-C reconstruction, and the unbalanced-tree root
  5. returns the ERC-7913 magic value on success, or 0xffffffff on 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:

  • domainSeparator
  • nonce
  • keyVersion
  • actionType
  • payloadHash

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-7913 verify(...) interface
  • SHRINCSStatelessConsumerExample: 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,576
  • HASH_LEN = 32 — the n security parameter [FIPS205 §11]
  • HYPERTREE_HEIGHT = 64 — the h parameter [FIPS205 §7]
  • NUM_HYPERTREE_LAYERS = 8 — the d parameter [FIPS205 §7]
  • FORS_TREE_HEIGHT = 14 — the a parameter [SPHINCSPLUS §5.5]
  • NUM_FORS_TREES = 22 — the k parameter [SPHINCSPLUS §5.5]
  • WOTS_CHAIN_LEN = 16 — the w parameter [WOTSPLUS §3]
  • NUM_WOTS_CHAINS = 64 — the len parameter [WOTSPLUS §3]
  • WOTS_TARGET_SUM_STATEFUL = 480 — the WOTS-C digit-sum target, len * (w - 1) / 2
  • HASH_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^20 is 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 of 2^20

Two verifier rules deserve explicit mention:

  • FORS-C verifies NUM_FORS_TREES - 1 revealed entries, not all NUM_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-C uses the fixed target sum instead of an explicit checksum suffix
    • the reconstructed base-w digits must add up to WOTS_TARGET_SUM_STATEFUL

For the stateful WOTS-C / XMSS path, the code also assumes:

  • STATEFUL_PUBLIC_KEY_BYTES = 68
    • encoded as pkSeed || root || maxSignatures
    • 32 + 32 + 4 bytes
  • WOTS_CHAINS_STATEFUL = 64
    • the stateful compact WOTS-C signature carries 64 chains
  • 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 into SHRINCSParams kept the 256s production build byte-identical to the pre-split verifier (metadata-stripped deployed bytecode compared before/after).
  • 128s-q18 and 128s-q20 (experimental). 16-byte truncated-hash profiles (HASH_LEN = 16, high-aligned via HASH_MASK), single-layer h = 18 hypertree, a = 24, k = 6, 32 stateful WOTS chains. They differ only in the stateless budget: 2^18 for q18, 2^20 for q20. Both compile and pass the structural and profile-invariant test sets, and their stateless paths are verified against Rust-generated signature vectors by SHRINCSSphincs128sVectors. 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:

  • nonce
  • keyVersion

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:

  • domainSeparator
  • nonce
  • keyVersion

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:

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.

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
  • 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:
      • nextStatefulLeafIndex
      • recoveryMode
      • 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
  • 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(...) and rotateFullKey(...) both require StatefulPolicy.RecoveryRotation
    • both also require recoveryMode == true
    • this keeps stateless signatures as recovery authority rather than a normal-operation rotation bypass
  • RecoveryRotation disables the stateful path immediately.

    • selecting StatefulPolicy.RecoveryRotation blocks stateful verification even before enterRecoveryMode()
    • enterRecoveryMode() only arms stateless recovery rotation; it does not change stateful-path availability
  • 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 statelessSignaturesUsed and first consumes one stateless use for the recovery signature itself
    • rotateFullKey(...) replaces the full bundle including the stateless key material
    • it resets statelessSignaturesUsed for 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, and address(this)
    • production wrappers should keep that property even if they change the exact domain-tag scheme

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 payloadHash flow 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 maxSignatures boundary verifies
  • signature exceeding maxSignatures is rejected
  • malformed pkSeed length is rejected
  • malformed stateful public-key length is rejected
  • wrong stateful WOTS-C chain 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 FORS data is rejected
  • tampered hypertree WOTS-C public-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 pkSeed length is rejected
  • malformed hypertreeRoot length is rejected
  • empty hypertree signatures are rejected
  • dropped hypertree layers are rejected
  • dropped FORS entries are rejected
  • short FORS randomizers, secret leaves, auth paths, and auth nodes are rejected
  • malformed hypertree WOTS-C chain 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 RecoveryRotation policy 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 RecoveryRotation policy 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.decode over 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 false through the full verify path

ERC-7913 raw verifier and codec

  • ERC-7913 verify(...) returns 0x024ad318 for a valid stateful raw signature
  • valid signatures from multiple in-budget stateful leaves verify
  • wrong key lengths return 0xffffffff without 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 reverting
  • SHRINCS.statefulEnvelope(...) re-tags canonical and other field-value-equivalent framings in place, and reverts on out-of-bounds offsets or lengths
  • SPHINCSPlusC.toMessage(...) maps the ERC-7913 bytes32 hash to exactly those 32 packed bytes
  • ERC-7913 consumer examples accept verifier || key signers 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). The solidity-standards skill 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/.

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:

  • domainSeparator
  • nonce
  • keyVersion
  • actionType
  • payloadHash

for action flows, and the wrapper-owned rotation context for recovery flows.

The test-only account-aware path is:

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:

  • testExportStatefulActionBundle
    • stateful_vector_abi: ABI encoding of the full exported stateful-action bundle
    • stateful_verify_calldata: calldata for verifyStatefulAction(...)
    • stateful_1271_envelope: ERC-1271 payload for canonical stateful-action validation
  • testExportStatelessActionBundle
    • stateless_vector_abi: ABI encoding of the full exported stateless-action bundle
    • stateless_verify_calldata: calldata for verifyStatelessAction(...)
    • stateless_1271_envelope: ERC-1271 payload for canonical stateless-action validation
  • testExportStatefulOnlyRotationBundle
    • stateful_rotation_vector_abi: ABI encoding of the full exported stateful-only rotation bundle
    • stateful_rotation_calldata: calldata for rotateToFreshKey(...)
  • testExportFullRotationBundle
    • full_rotation_vector_abi: ABI encoding of the full exported full-rotation bundle
    • full_rotation_calldata: calldata for rotateFullKey(...)

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 (profile production)
  • script/DeploySHRINCS256sKeccak.s.sol — 256s verifier (profile production)

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-std is a git submodule at lib/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