Skip to content

lfglabs-dev/SPHINCS-

 
 

Repository files navigation

SPHINCS- Post-Quantum Ethereum verifiers


WARNING: RESEARCH PROTOTYPE - NOT FOR PRODUCTION USE

This codebase is a scheme exploration for lightweight variants of SPHINCS+ (called SPHINCs-). It has not been audited, contains no security guarantees, and is not safe to use with real funds. Cryptographic parameters, key derivation, and contract logic have not been reviewed by any third party. Use on testnets only.


Welcome to SPHINCs-, a family of EVM-optimised variants of SLH-DSA and the recently proposed SLH-DSA-SHA2-128-24. They all achieve low gas cost for pure on-chain signature verification without any precompile which makes them useful today without any ethereum hardfork. The key modifications is substituting SHAKE256 with the native keccak256 opcode and a significant reduction of the signature budget. NIST initially standardized a scheme with a 2^64 signature budget which is a huge number. Ethereum's on chain data shows that among 64,294,251 Ethereum mainnet addresses that sent at least one transaction in 2025 99.99% had less than 3000 transactions annually. The variants described in the repo give a trade-off between signing budget per key pair, verifier cost in gas and signer keygen and signing keccak calls (hardware wallet friendliness).

One can simply build a smart account using any of these verifiers, they are stateless and they maintain 128bits up their specified limits. For a efficient design that works on constrained device you can use the JARDÍN account design (A combination of SPHINCs- with a smaller compact path) are available for this. The SPHINCS+ registration path, the FORS compact path, all the JARDIN contracts and signers lives in a separate repo: nconsigny/JARDIN.


Variants

There are different ways to construct the SPHINCS signature scheme. Existing litterature shows various ways to optimise for signature size or verify cost.

Variant Family h d a k w l swn Sig sign_h Verify Frame 4337 sec_10 sec_14 sec_18 sec_20
C7 WOTS+C / FORS+C 24 2 16 8 8 43 151 3,704 B 4.3 M 127 K 210 K 318 K 128 128 128 128
C11 WOTS+C / FORS+C 16 2 11 13 8 43 203 3,976 B 292 K 116 K 202 K 308 K 128 128 104.5 86.1
C13 WOTS+C / FORS+C 22 2 19 7 8 43 208 3,688 B ~10 M 105 K 188 K 293 K 128 128 128 128
C12 vanilla SPHINCs+ 20 5 7 20 8 45 - 6,512 B 36.6 K 276 K - - 128 127.8 109.1 95.4
SLH-DSA-SHA2-128-24 vanilla SPHINCs+ 22 1 24 6 4 68 - 3,856 B ~1.07 B ~142 K - - 128 128 128 128
SLH-DSA-Keccak-128-24 vanilla SPHINCs+ 22 1 24 6 4 68 - 3,856 B ~1.07 B ~94 K - - 128 128 128 128
  • Family: the SPHINCS+ construction style (vanilla SPHINCs+ SPX, WOTS +). WOTS+C / FORS+C is the C-series compact construction with counter-grinding (ePrint 2025/2203). Plain SLH-DSA / SPHINCS+ is the standard FIPS 205 construction with no counter grinding — C12 and the two SLH-DSA-128-24 entries are the same algorithm at different parameter sets, with the SHA-2 row using the FIPS 22-byte ADRSc + SHA-256 hash.
  • sign_h: hash-function calls during keygen + one signature, zero-memory signer (no inter-sign caching — the relevant case for a hardware wallet). A high number means a lot of work for the hardware. C12 the lightest is ~40 sec to sign on secure element.
  • swn: small-Winternitz-number counter bits used by the WOTS+C / FORS+C grinding. Plain SPX and SLH-DSA don't counter-grind.
  • sec_N: security bits at 2^N signatures per key. SLH-DSA-*-128-24 is flat 128-bit up to the 2²⁴ hard cap, undefined beyond.
  • Verify (pure): Foundry gasleft() measurement of the assembly block.
  • Frame: total EIP-8141 frame-tx gas (ethrex). C12 / SLH-DSA-128-24 are not yet wired to frame accounts in this repo.
  • 4337: total ERC-4337 handleOps tx gas (Sepolia). The 4337 wiring for C7 / C11 lives in SphincsAccount + SphincsAccountFactory; no SLH-DSA or C12 account exists here yet.
  • : C13 uses the FIPS 205 §11.2.2 uncompressed 32-byte ADRS layout with keccak256, not JARDIN's. First verifier on the FIPS address layout — see the Address layout subsection below.

C13 vs the rest of the family — what changes

C13's parameter choice (h=22 d=2 a=19 k=7 w=8) was built around three goals: smallest signature, cheapest verify at full 128-bit security across the signature-count window, FIPS-aligned address layout. The numbers above bear out the design:

Property C7 C11 C13 C12 SLH-DSA-SHA2-128-24 SLH-DSA-Keccak-128-24
Sig size 3,704 B 3,976 B 3,688 B ← smallest 6,512 B 3,856 B 3,856 B
Pure-asm verify 127 K 116 K 105 K ← cheapest at sec_20=128 276 K 142 K 94 K
Frame tx total (ethrex) 210 K 202 K 188 K
4337 handleOps total (Sepolia) 318 K 308 K 293 K
Signature-count cap 2²⁴ 2¹⁶ 2²² 2²⁰ (h=20, d=5) 2²⁴ 2²⁴
Security at the cap 128 bit 86 bit 128 bit 95 bit 128 bit 128 bit
Hash-call cost / sign (cold) 4.3 M 292 K ~10 M 36.6 K ~1.07 B ~1.07 B
ADRS layout JARDIN JARDIN FIPS uncompressed JARDIN FIPS ADRSc JARDIN

Reading the table:

  • vs C7: C13 holds full 128-bit security up to its 2²² cap (vs C7's 2²⁴), but verifies in 105 K vs 127 K (~17 % cheaper) and signs roughly half the hashes. Trade-off: half the signature-count budget per key. Good fit when keys rotate often or the per-key budget is bounded by policy.
  • vs C11: Same security at sec_14 (128 bit), but C11 collapses to 86 bit at sec_20. C13 holds 128 bit all the way to sec_20. Cost: ~30× higher signer hash count (C13's a=19 FORS trees vs C11's a=11), but cheapest verify in the C-series.
  • vs C12 (plain SPHINCS+ without counter grinding): C13 verifies ~2.6× cheaper for a comparable security envelope, with a sig that's ~44 % smaller. C12 wins decisively on signer cost (it has no counter-grinding step at all), so C12 stays the right pick for tightly-constrained signers (e.g. secure-element keys). C13 is the right pick when verify gas matters more than signer time.
  • vs SLH-DSA-SHA2-128-24 (NIST-compliant standalone): C13 is roughly same sig size (-4 %) and ~25 % cheaper to verify, with ~100× lower signer cost. The trade is that SLH-DSA is the FIPS 205 NIST-blessed parameter set; C13 is a research parameter choice in the ePrint 2025/2203 family. C13's ADRS layout now matches FIPS to keep cross-impl porting clean.
  • Smallest signature of any variant in the repo. The combination k=7, a=19 with the 4 B count tail and 11-level subtree gives the leanest payload.

The takeaway: C13 is the cheapest verifier in the repo at 128-bit security up to a 2²² sig cap, and the smallest signature. The cost is sign-time, which is ~30× C11 and ~2× C7. For an Ethereum smart account that signs occasionally and is verified by everyone, that asymmetry is the right shape.

C11 and C12 are light enough to run on a hardware wallet, 390s and 47.5s signature times on a ST33K1M5 secure element (Ledger nano S+). C12 has the lowest hardware signer cost of all (36 K hashes - plain SPX with d=5 hypertree skips most tree-hash work) at the price of a 6,512-byte sig. SLH-DSA-SHA2-128-24 is the FIPS-aligned alternative: much larger signer cost even on a desktop-class signer that caches the XMSS tree (~200 M hashes / sig, dominated by FORS — which can't be cached because the leaf-index to FORS-tree-address mapping changes with every message), and ~1.07 B / sig on a zero-memory signer that has to rebuild the 2²²-leaf XMSS for every auth path. Constant 128-bit security up to the 2²⁴ cap. The Keccak twin trades bit-exact NIST compliance for ~34 % cheaper on-chain verification (but not a very interesting trade-off as it keeps the same signer cost).

Stateless SPHINCs- Architecture

Shared hash kernel

Two distinct ADRS layouts live in this repo. The keccak-family verifiers used to all share JARDIN's, but C13 onward uses the FIPS 205 uncompressed layout instead. Target end state: just two layouts — FIPS uncompressed 32 B for keccak/SHAKE-family hashes, and FIPS ADRSc 22 B for SHA-2 — both straight out of FIPS 205. JARDIN remains for the older C-series and the keccak SLH-DSA twin until they're migrated.

Layout Variants ADRS bytes Hash F/H/T input
JARDIN 32 B C7, C11, C12, SLH-DSA-Keccak-128-24 layer4 ‖ tree8 ‖ type4 ‖ kp4 ‖ ci4 ‖ cp4 ‖ ha4 keccak256 seed32 ‖ adrs32 ‖ payload
FIPS uncompressed 32 B C13 (first user) layer4 ‖ tree12 ‖ type4 ‖ word1·4 ‖ word2·4 ‖ word3·4 keccak256 seed32 ‖ adrs32 ‖ payload
FIPS ADRSc 22 B SLH-DSA-SHA2-128-24 layer1 ‖ tree8 ‖ type1 ‖ 12 B type-dependent SHA-256 (precompile 0x02) PK.seed(16) ‖ zeros(48) ‖ ADRSc(22) ‖ payload

Address layout

FIPS 205 §4.2 / §11.2.2 uncompressed 32-byte ADRS (the SHAKE-instantiation form):

bytes  0.. 4  layer address       (uint32)
bytes  4..16  tree address        (96 bits big-endian; top 4 B = 0 in current instances)
bytes 16..20  type                (uint32)
bytes 20..24  word1 (type-dependent)
bytes 24..28  word2 (type-dependent)
bytes 28..32  word3 (type-dependent)
type name word1 word2 word3
0 WOTS_HASH key_pair_address chain_address hash_address
1 WOTS_PK key_pair_address 0 0
2 TREE 0 tree_height tree_index
3 FORS_TREE key_pair_address tree_height tree_index
4 FORS_ROOTS key_pair_address 0 0

JARDIN 32-byte ADRS (used by C7/C11/C12/SLH-DSA-Keccak today): same 32-byte width, but with an 8-byte tree field (FIPS gives it 12) and four type-dependent words (FIPS uses three). The freed-up byte budget went to ci (chain_index) being a dedicated WOTS-only slot, while in FIPS chain_address and tree_height share word2 — same bytes, type-dependent meaning. JARDIN's 4th word (ha) is unused for every type in practice; the structural divergence from FIPS is the 8 vs 12 byte tree field.

Why C13 moved to FIPS uncompressed. "Reduce differences between families": FIPS-aligning the ADRS makes the keccak verifier port cleanly from a FIPS reference implementation, and pares the repo's address-layout inventory toward just two layouts (above). The hash stays keccak256 — switching to SHA-256 would double on-chain gas (precompile staticcall vs native opcode) and would only be relevant if we needed full SLH-DSA-SHA2 family alignment, which we don't.

SLH-DSA-128-24 family, two wire-level layouts:

  • SHA-2 variant — FIPS 205 §11.2.1 bit-exact: ADRSc (22 B), SHA-256 via precompile, nested Hmsg = MGF1-SHA-256(R ‖ seed ‖ SHA-256(R ‖ seed ‖ root ‖ M), m=21), byte-wise LSB-first digest-to-indices (same convention as the sphincs/sphincsplus reference and PQClean).
  • Keccak variant — JARDIN twin: 32-byte JARDIN ADRS (layer4 ‖ tree8 ‖ type4 ‖ kp4 ‖ ci4 ‖ cp4 ‖ ha4), keccak256 primitive, F / H / T input = seed32 ‖ adrs32 ‖ payload, one-shot Hmsg = keccak(seed ‖ root ‖ R ‖ msg ‖ 0xFF..FB) (no MGF1), LSB-first digest-to-indices on the 256-bit keccak output interpreted as a single big-endian integer.

Shared Verifier Model

The SPHINCS- verifier is deployed once and shared by all accounts. Follows the ZKnox/Kohaku pattern.

SPHINCs-Asm (deployed once, stateless, pure)
    ↑ verify(pkSeed, pkRoot, message, sig) → bool
    │
    ├── SphincsAccount (4337)       ← keys in storage, rotatable
    └── FrameAccount (EIP-8141)     ← keys in storage, rotatable

ERC-4337 Hybrid Account

The account stores keys as immutables and passes them to the shared verifier on each UserOp.

EntryPoint.handleOps()
    └── SphincsAccount._validateSignature()
            ├── ECDSA.recover(userOpHash, ecdsaSig) == owner
            └── sharedVerifier.verify(pkSeed, pkRoot, userOpHash, sphincsSig) == true

EIP-8141 Frame Transaction (Pure PQ)

The frame account has keys baked into its bytecode - no storage, no calldata overhead for keys. It receives sigHash + raw_sig, builds the full ABI call to the shared verifier internally, and calls APPROVE on success.

Frame Transaction (type 0x06)
    ├── Frame 0 (VERIFY): frame account builds verify(pkSeed, pkRoot, sigHash, sig)
    │     from embedded keys + calldata → STATICCALLs shared verifier → APPROVE
    └── Frame 1 (SENDER): ETH transfer / contract call

No ECDSA - pure post-quantum. Keys are stored in EVM storage (not bytecode) to support future key rotation via rotateKeys() - costs ~4K gas per verify but keeps the same account address across key changes.

Key Derivation

BIP-39 Path (Rust WASM signer)

BIP-39 mnemonic (12 or 24 words)
    │
    ├──▶ HMAC-SHA512("sphincs-c6-v1", seed) → pkSeed, sk_seed (quantum-safe)
    └──▶ BIP-32 m/44'/60'/0'/0/0 → ECDSA address (independent)

SPHINCs- and ECDSA are derived through independent paths - compromising one does not compromise the other.

Signers

Signer Language Targets BIP-39
script/signer.py Python C-series (C7 / C9 / C11) No
signer-wasm/ Rust/WASM C-series Yes
script/slh_dsa_sha2_128_24_signer.py Python (slow; ~hours at NIST params) SLH-DSA-SHA2-128-24 No
script/slh_dsa_keccak_128_24_signer.py Python (slow; ~hours at NIST params) SLH-DSA-Keccak-128-24 No
signers/sphincsplus-128-24/ C (forked from sphincs/sphincsplus ref) SLH-DSA-SHA2-128-24 No, seeds fed in
signers/jardin-keccak-128-24/ C (sphincsplus fork + custom keccak + 32-B ADRS) SLH-DSA-Keccak-128-24 No
# Rust WASM C-series signer
cd signer-wasm && wasm-pack build --release --target web
cargo test --release -- --ignored

# SLH-DSA-128-24 fast C signers (~11 min per NIST-params sign on pure C, no SHA-NI)
(cd signers/sphincsplus-128-24  && make)
(cd signers/jardin-keccak-128-24 && make)
# Python wrapper with disk cache; Forge FFI tests use these:
python3 script/slh_dsa_sha2_128_24_fast_signer.py   <master_sk_hex> <message_hex>
python3 script/slh_dsa_keccak_128_24_fast_signer.py <master_sk_hex> <message_hex>

Deployed Contracts & Transactions

EntryPoint v0.9: 0x433709009B8330FDa32311DF1C2AFA402eD8D009 (Sepolia)

Sepolia (ERC-4337 Hybrid, C-series)

Variant Verifier Account Gas Tx
C9 0x18F005... 0xA94111... 300 K 0x8366513b...
C11 0xC25ef5... 0x3C3b0c... 308 K 0x9fba169c...
C13 0xce176d... 0xcef985... (factory 0xcaf5d2...) 293 K 0xbbf06456...

C13 on Sepolia uses the FIPS 205 §11.2.2 uncompressed 32-byte ADRS (keccak256 hash). First verifier in the repo on the FIPS address layout. Standalone verify tx-level gas: 188,278. Full hybrid-4337 handleOps UserOp gas: 292,727 (ECDSA recovery + C13 verify + 0.00001 ETH self-transfer through SphincsAccount.execute). See script/.c13_addresses.json for the canonical address record.

Sepolia (SLH-DSA-128-24 standalone verifiers, no account wired yet)

Variant Verifier Deploy tx Sample verify tx Verify-tx gas
SLH-DSA-SHA2-128-24 0x9Fe417... 0x09be3c59... 0x00fa6b37... 225,642
SLH-DSA-Keccak-128-24 0x2Ac9Ec... 0x253aa6dc... 0x90d785a1... 177,910

Verify-tx gas is the full top-level tx cost including 21 K tx base + ~63 K for the 3,872-B calldata payload; pure assembly execution is ~142 K (SHA-2) / ~94 K (Keccak). Keccak wins by ~21 % tx-level and ~34 % assembly-level because every F / H / T is a single native keccak256 opcode, while the SHA-2 variant pays a staticcall(0x02) dispatch per hash × ~280 hashes.

ethrex Testnet (EIP-8141 Frame Tx - Pure PQ)

Older demo devnet at demo.eip-8141.ethrex.xyz (chain 1729) — C9 / C10 / C11 frame accounts:

Variant Verifier Frame Account Gas Verify Tx
C9 0xc0F115... 0xc96304... 195K 117K 0x393588ec...
C10 0xD0141E... 0x07882A... 203K 122K 0x0a2571f8...
C11 0x315575... 0x5bf5b1... 202K 122K 0x053428f5...

Current eip-8141.ethrex.xyz devnet (chain 3151908, Osaka fork, frame opcodes enabled from genesis; RPCs rpc1/rpc2/rpc3.eip-8141.ethrex.xyz):

Variant Verifier Frame Account Frame Gas Sample Frame Tx
C13 0x659415... 0xae3bA3... 188 K 0xabf3ce2f...

EIP-8141 type-0x06 frame tx, two frames: [VERIFY(frame_account, sigHash‖c13_sig, flags=approve sender+payer), SENDER(0x...dead, value=0.00001 ETH)]. The VERIFY frame's runtime staticcalls the shared C13 verifier and APPROVE(0,0,3)s on success. Sender by the frame account itself — no protocol-level signature needed. Reproduce with script/send_frame_tx_c13.py. Note: ethrex's on-chain FrameTransaction RLP layout differs from the draft EIP-8141 markdown by omitting the signatures field — the script handles this; the deviation is documented inline.

Setup

forge build
pip install eth-account eth-abi requests pycryptodome
cd signer-wasm && cargo build --release

Usage

# ── C-series (stateless hybrid + frame accounts) ───────────────────────────
# Deploy shared C-series verifier + SphincsAccountFactory (once):
forge script legacy/script/DeploySepolia.s.sol --rpc-url sepolia --broadcast

# Create account + send hybrid ERC-4337 UserOp (C7 / C11):
python3 legacy/script/send_userop.py create \
  --factory <factory> --ecdsa-key $PRIVATE_KEY --variant c7
python3 legacy/script/send_userop.py send \
  --account <account> --ecdsa-key $PRIVATE_KEY \
  --to <recipient> --value 0.001 --variant c7

# ── SLH-DSA-128-24 (standalone verifiers, no account) ──────────────────────
# Deploy both SLH-DSA verifiers to Sepolia:
forge script script/DeploySlhDsa128_24Sepolia.s.sol --rpc-url sepolia --broadcast

# Build the fast C signer (used by Forge FFI tests, ~11 min per NIST-params sign):
(cd signers/sphincsplus-128-24  && make)  # SHA-2 variant
(cd signers/jardin-keccak-128-24 && make) # Keccak variant

# Run the Forge end-to-end verify tests (first run triggers a real sign; later
# runs hit the disk cache at signers/*/\.cache/):
forge test --match-contract SLH_DSA_SHA2_128_24_Test   -vv
forge test --match-contract SLH_DSA_Keccak_128_24_Test -vv

Tests

forge test
cd signer-wasm && cargo test --release -- --ignored

Formal Verification (Lean 4 / Verity)

Verified Kernel

The three on-chain verifiers in this repository (SPHINCs-C13Asm.sol, SPHINCs-C12Asm.sol, and SLH-DSA-SHA2-128-24verifier.sol) have been formally verified in Verity, a Lean 4 framework for proving Solidity correct.

The proof establishes that each verifier's model runs exactly the check sequence the SPHINCS- algorithm prescribes and reaches the same verdict (accept, reject, or revert) and, for the +C variants, that it accepts only when the grinding is present: the WOTS digit sum hits its target and the forced FORS index is zero. This holds modulo a single named model/bytecode bridge axiom, and the whole result reduces to a small, explicit trust surface (hash collision resistance, the named EVM primitives, and that bridge axiom).

A hand-held walkthrough of what SPHINCS- is, what a correct verifier must check, and how the proof is structured is available here:

https://lfglabs.dev/research/sphincs-minus-verifier

About

SPHINCS- variants are optimised versions of SLH-DSA (SPHINCS+). They optimise for signer load and cheap EVM verification.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • C 29.8%
  • Python 20.9%
  • Solidity 14.8%
  • Lean 14.3%
  • JavaScript 8.4%
  • Yul 5.7%
  • Other 6.1%