Poseidon, by hand and by code
Sections
A SHA-256 of “abc” inside a SNARK takes about 24,000 R1CS constraints. The same input through Poseidon — properly parameterised — takes about 250.
Two orders of magnitude. That ratio is the entire reason ZERA’s unified shielded pool ships with consumer-grade UX in 2026. It’s also the reason every modern ZK system you can name uses Poseidon, Rescue, or one of their cousins instead of something the cryptographic community has been beating on for twenty years.
This post is the long answer to why.
The problem with hashing inside a SNARK
A zero-knowledge SNARK proves you know a witness such that for some arithmetic circuit over a prime field . Every operation in becomes a constraint, and proof time scales roughly linearly with the number of constraints.
The trouble with SHA-256 is that it was designed for CPU efficiency, not arithmetic-circuit efficiency. Its building blocks — XOR, AND, bitwise rotation — are cheap on a CPU and catastrophically expensive in . A single XOR over 32-bit words requires unpacking each word into 32 individual binary constraints, doing the XOR bit-by-bit, then packing back. SHA-256 has 64 rounds of mixing, and every round does several of these.
The constraint cost looks roughly like:
where bits and the per-operation constants run between 30 and 100. You end up north of 25k constraints for a 64-byte input — and that’s just the hash. A real circuit has dozens of these per spend.
This is the gap that hash-friendly arithmetisation closes.
Poseidon’s design: only field operations, all the way down
Grassi, Khovratovich, Rechberger, Roy, and Schofnegger (2021) had a different idea: design the hash natively in . No bits. No bytes. Just field elements all the way down.
Poseidon is a permutation-based sponge. The state is field elements — typically for hashing two-to-one (input input output) and for absorbing three field elements at once. The permutation alternates two kinds of rounds:
- Full rounds apply an S-box to every state element, then mix.
- Partial rounds apply an S-box to one state element, then mix.
The S-box is the simplest possible non-linear function over a prime field:
with chosen as the smallest exponent for which (so the map is a bijection). For BN254 — the curve underlying most production ZK pairings, including the one ZERA’s SDK uses — is divisible by 2 and 3, so is the smallest legal exponent. Poseidon over BN254 ships with .
The full permutation is:
flowchart LR
S[State t elems] --> AC1[+ round constants]
AC1 --> SB1[S-box: x^5 on all elems]
SB1 --> M1[MDS matrix mix]
M1 --> N{round full or partial?}
N -->|full| AC2[+ round constants]
N -->|partial| AC3[+ round constants]
AC2 --> SB2[S-box on all]
AC3 --> SB3[S-box on first elem only]
SB2 --> M2[MDS mix]
SB3 --> M2
M2 --> O[output state] Three primitives, repeated times: add round constants ⊕ S-box ⊕ MDS matrix multiplication.
That’s the whole algorithm.
Counting the constraints
This is where the order-of-magnitude advantage shows up.
Each S-box is . In R1CS that’s three multiplication constraints (one for , one for , one for ). The MDS matrix is a fixed matrix of constants applied to the state — that’s free in R1CS because constant multiplications fold into linear combinations and don’t generate constraints.
So per round:
Recommended parameters for BN254 with (hashing two field elements) are full rounds and partial rounds. Total constraint count:
Two hundred and forty-three constraints. For a hash of two field elements (~64 bytes of payload). SHA-256 was 24,000+ for a similar payload. That ratio — about 100× — is the entire ball game.
| Option | Cost | Latency | Blast radius | Notes |
|---|---|---|---|---|
| SHA-256 (in-circuit) | ~24,000 constraints / 64-byte input | Fast on CPU; brutal in SNARKs | Standard; battle-tested | Designed for hardware, not for finite fields |
| Poseidon-128, t=3, α=5 (BN254) | ~243 constraints / 2 field elements | Slow on CPU vs SHA; fast in SNARKs | Younger primitive; growing analysis | Designed for SNARKs first; the standard since 2020 |
| Rescue-Prime | ~150 constraints / 2 field elements | Slightly fewer constraints than Poseidon | Less peer review than Poseidon | Closer to a research curiosity in 2026 |
| Anemoi | ~120 constraints / 2 field elements | Newest; lowest constraint count | Very young; minimal cryptanalysis | Promising but I would not bet a production pool on it yet |
The blast-radius column is doing real work. Poseidon’s the one I’m comfortable shipping in zera-sdk right now. Rescue and Anemoi are interesting but the cryptanalysis hasn’t caught up to the deployment.
A 30-line Poseidon you can run in the browser
Here’s a complete, working Poseidon-128 over BN254, written in TypeScript with bigint arithmetic. It’s not optimised — production code uses Montgomery form, precomputes S-box squares, and uses constant-time field arithmetic — but it’s correct and small enough to read in one sitting.
The thing that’s striking when you write this out is how little there is. A SHA-256 implementation is hundreds of lines of bit-twiddling. Poseidon is essentially: add a constant, raise to the fifth power, multiply by a fixed matrix, repeat.
Why specifically
The S-box choice is the most-questioned part of Poseidon. Why not ? Or ?
Two constraints:
-
Bijection. is a permutation of if and only if . For BN254, , so all share a factor with and produce non-bijective maps. The smallest that works is 5.
-
Algebraic degree. The whole point of the S-box is to introduce algebraic non-linearity that defeats interpolation attacks. Higher → more non-linearity → fewer rounds needed. So you want small enough to be cheap, large enough to need few rounds.
For curves where (like BLS12-381), the choice flips to and the round count drops because each S-box is more powerful. The trade-off is: cheaper per-round but more rounds.
The choice of for the prime field of BN254 is dictated by the requirement that the S-box must be a permutation: it must hold that . The next constraint — the one that determines round counts — is the algebraic degree.
What I would change in a v2
Three things, if I were re-designing Poseidon for 2027:
-
Drop the partial-rounds split. The original design has 8 full + 57 partial rounds; the partial rounds save a lot of constraints but make security analysis harder. Poseidon2 (Grassi, Khovratovich, Roy 2023) keeps a similar structure with cleaner analysis. I’d ship Poseidon2 by default in a fresh deployment.
-
Make the MDS matrix circulant. A circulant MDS — where each row is a rotation of the previous — has identical security properties but lets you exploit FFT-friendly arithmetic. Worth it on the prover side.
-
Standardise the parameter file format. Every implementation rolls its own format for round constants. The Circomlib JSON format works, but a CBOR or Cap’n Proto schema would let implementations cross-check parameters in a way that’s currently per-vendor. I keep the Circomlib JSON in zera-sdk because compatibility, not because it’s the right choice.
Where this goes in production
Inside zera-sdk the Poseidon implementation is crates/zera-sdk-core/src/hash/poseidon.rs. It’s about 200 lines of safe Rust, written against the ff crate for field arithmetic, with the round constants loaded from a JSON file extracted from Circomlib for cross-implementation parity.
Further reading
- Poseidon: A New Hash Function for Zero-Knowledge Proof Systems — Grassi, Khovratovich, Rechberger, Roy, Schofnegger (USENIX Security 2021) — the original
- Poseidon2: A Faster Version of the Poseidon Hash Function — Grassi, Khovratovich, Roy (2023) — what I’d ship in a v2
- Anemoi: Exploiting the Link between Arithmetisation-Oriented and CCZ-Equivalent Symmetric Designs — Bouvier et al. (2022) — the next-gen contender
Dax911/zera-sdk— production Rust implementation- Pedersen commitments, in production — sister piece on what we’re hashing to (commitments)
- Nullifiers without the witchcraft — what we use Poseidon to derive (single-use nullifiers)
- Privacy’s broadband moment — why Poseidon is part of the four-curve crossing in 2026