A fast, no_std, field-generic Poseidon2 permutation and hash — faithful to
the Horizen Labs reference and the paper "Poseidon2: A Faster Version of the
Poseidon Hash Function" (eprint 2023/323). Ships KAT-verified BN254,
BLS12-381 and Goldilocks instances, generates parameters for any width/field
via the Grain LFSR, and comes with a const-generic, allocation-free fast path
that is at parity with the fastest Poseidon2 crate published. Correctness is held
down by KATs, differential property tests, fuzzing, Miri, Kani proofs and
mutation testing. See Benchmarks and
Verification.
[dependencies]
fast-poseidon2 = "0.1"use ark_bn254::Fr;
use fast_poseidon2::{instances::bn254, Endianness};
// Const-generic fast path: no allocation, [F; T] on the stack. Build once, reuse.
let perm = bn254::ct::t3(); // t = 3, d = 5, R_F = 8, R_P = 56
let out: [Fr; 3] = perm.permutation(&[a, b, c]);
// Flexible path: one type for every width + generated instances.
let hash = bn254::poseidon2_t3().compress(&[left, right])?; // 2-to-1 Merkle
let digest = bn254::poseidon2_t3().hash(&inputs, /*rate=*/ 2, /*out=*/ 1)?; // sponge
// Byte-oriented, endianness-aware — the surface a hashing syscall would wrap.
let bytes = bn254::poseidon2_t3().hashv_bytes(Endianness::BigEndian, &[&a_be, &b_be])?;The permutation core is #![no_std] with no allocation (construction
included); #![forbid(unsafe_code)].
| Feature | Default | Description |
|---|---|---|
std |
✓ | cached (OnceLock) instance getters and security::round_numbers |
bn254 |
✓ | BN254 (alt_bn128) scalar-field instances, t = 2,3,4,8,12 |
bls12-381 |
✓ | BLS12-381 scalar-field instances, t = 2,3,4,8,12 |
goldilocks |
✓ | Goldilocks field (2⁶⁴−2³²+1) + instances t = 8,12,16,20 |
Disable default features to use the field-generic core with your own ark-ff
field and no baked curves. On x86_64 the crate automatically enables
ark-ff/asm (hand-written MULX/ADCX/ADOX Montgomery) for the field multiply.
ct— const-generic fast path. [ct::Poseidon2Ct<F, T, RF, RP, D>] fixes width, rounds and S-box degree at compile time; the state is[F; T]on the stack, round-constant tables are contiguous arrays, and the loops fully unroll. Zero allocation,no_std. This is the hot path.Poseidon2— flexible path. Runtime-configured width, one type for every instance (and every generated instance). Usesalloc. This is where sponge, compression, the byte API and arbitrary-width generation live.
Both share identical constants and produce identical output (checked by tests).
Shipped, cached, and KAT/round-trip verified:
| Field | Widths (t) |
d |
Provenance |
|---|---|---|---|
| BN254 | 2, 3, 4, 8, 12 | 5 | t=3 reference-verbatim; others generated (Grain) |
| BLS12-381 | 2, 3, 4, 8, 12 | 5 | t=2,3,4,8 reference-verbatim; t=12 generated |
| Goldilocks | 8, 12, 16, 20 | 7 | generated, matches the reference t=12 KAT |
The Grain LFSR is seeded purely from the public tuple (field, sbox, field_size, t, R_F, R_P), so any instance is reproducible bit-for-bit — including widths and
fields nobody published:
use ark_bn254::Fr;
use fast_poseidon2::{Poseidon2, Poseidon2Params, security};
let (rf, rp) = security::round_numbers(254, 8, 5, 128, true); // paper's formula
let params = Poseidon2Params::<Fr>::generate(8, 5, rf, rp)?; // constants + secure diagonal
let perm = Poseidon2::new(params);generate draws round constants and, for t ≥ 4, a Grain-drawn internal
diagonal re-sampled until it passes a minimal-polynomial security check. The
procedure is proven faithful: it reproduces Horizen's published BLS12-381 t=8
(constants and diagonal) bit-for-bit.
Head-to-head against every other Poseidon2 crate on crates.io
(taceo-poseidon2, zkhash, p3-poseidon2) plus the Poseidon1 hasher
light-poseidon. criterion, Apple-Silicon (aarch64), all-portable code (ark's
x86 asm is inactive here). Harness: bench-compare/; ours is
the ct fast path. Every entry uses independent iterations (fresh input each
time) so the CPU pipelines across iterations equally — reusing one buffer in
place would serialise the in-place crates and skew the comparison. Numbers swing
±3–5% with load; treat sub-5% as ties.
BN254 permutation, µs (taceo is BN254-only; zkhash ships only t=3):
| width | ours | taceo | zkhash | vs zkhash |
|---|---|---|---|---|
t=2 |
3.87 | 3.79 | — | — |
t=3 |
4.31 | 4.21 | 4.93 | −13% |
t=4 |
7.32 | 7.24 | — | — |
t=8 |
12.46 | 11.94 | — | — |
t=12 |
17.07 | 16.68 | — | — |
Goldilocks permutation, µs — ours here uses the specialised
GoldilocksFast backend (u64 + Plonky2-style lazy
special-form reduction), not ark:
| width | ours | zkhash | vs zkhash |
|---|---|---|---|
t=8 |
1.04 | 1.61 | −35% |
t=12 |
1.36 | 2.07 | −34% |
t=16 |
1.75 | 2.56 | −32% |
t=20 |
2.10 | 3.06 | −31% |
BLS12-381 permutation, µs (taceo has none):
| width | ours | zkhash | vs zkhash |
|---|---|---|---|
t=2 |
3.51 | 4.27 | −18% |
t=3 |
3.95 | 4.76 | −17% |
t=4 |
6.93 | 7.77 | −11% |
t=8 |
11.45 | 13.08 | −12% |
t=12 |
15.88 | — | — |
BN254 2-to-1 hash, µs (mixes algorithms):
| crate | time | |
|---|---|---|
fast-poseidon2 compress (Poseidon2) |
4.23 | |
light-poseidon (Poseidon1) |
17.46 | what sol_poseidon wraps — ~4.1× slower |
Summary: faster than zkhash on every field and width (BN254 −13%, BLS
−11…−18%, Goldilocks −31…−35%) — even though zkhash allocates per call and we do
not. Within ~2–4% of taceo on BN254 (the other const-generic crate;
marginally ahead), and we uniquely also ship BLS, Goldilocks and arbitrary-width
generation. ~4.1× faster than Poseidon1.
The four other standalone Poseidon2 crates on crates.io, each compared on the
closest matching operation (ours = the specialised GoldilocksFast backend for
Goldilocks):
| comparison | ours | competitor | result |
|---|---|---|---|
Goldilocks t=12 permutation vs poseidon-hash |
1.36 | 1.22 | poseidon-hash ~11% faster |
BN254 t=4 permutation vs pso-poseidon (bb/noir params, ark 0.6) |
7.33 | 7.52 | ours ~2% faster |
Goldilocks 32-byte hash vs qp-poseidon-core |
2.76 | 1.72 | qp faster (different sponge) |
Honest reading: after adding the specialised GoldilocksFast backend we
beat zkhash by ~30–35% on Goldilocks (table above). Our field arithmetic
(lazy Plonky2-style add/mul, reduction to [0, 2^64) with canonicalisation
deferred to output) is now identical to poseidon-hash's. The remaining
~11% is not arithmetic — it is the external MDS matrix convention:
- We reproduce the HorizenLabs reference bit-for-bit, whose 4×4 block is
M4 = [[5,7,1,3],[4,6,1,1],[1,3,5,7],[1,1,4,6]]. Coefficients up to 7 need two doublings (×4) per row — ~14 add/double ops per 4-element window. poseidon-hashfollows the Plonky3 convention,M4 = circ(2,3,1,1)(coefficients ≤ 3) — ~11 ops per window, and a different round-constant set.
That ~3-ops-per-window difference, applied over 3 windows × 9 external layers, is
the ~11%. It is inherent to matching Horizen's constants and KATs — the two
conventions are incompatible instances, not a faster/slower implementation of the
same one. Closing it would mean shipping a separate Plonky3-convention instance
(different KATs), not tuning the current one. qp-poseidon-core leads on
byte-hashing (a different sponge). On BN254 t=4 we edge pso-poseidon.
soroban-poseidon is excluded: it is a Stellar/Soroban contract binding that
routes through host functions and needs a soroban_sdk::Env, so benchmarking it
natively would measure host emulation, not a comparable primitive.
zkhash links ark-ff 0.4; we use ark-ff 0.5. To separate algorithm from
library version, bench-compare runs our exact algorithm on ark-ff 0.4 and
on zkhash's own field types:
| instance | ours (ark-ff 0.4) | zkhash (ark-ff 0.4) | delta |
|---|---|---|---|
BN254 t=3 |
4.25 | 5.06 | −16% |
BLS12-381 t=8 |
11.49 | 13.01 | −12% |
Goldilocks t=12 |
1.79 | 2.13 | −16% |
On the identical library and field types we are faster across the board — so the win is genuinely our const-generic, allocation-free code, not the ark version.
Methodology note (a bug we found and fixed). An earlier version of this harness reused one state buffer in place across iterations for the in-place crates while
zkhash's API returns a freshVeceach call. That created a loop-carried dependency that serialised our iterations and made us look ~20% slower on BLS. Switching every entry to independent iterations removed the artifact and flipped the result. Lesson: benchmark permutations with independent inputs, or you measure latency-chaining, not throughput.
p3-poseidon2 could not be built into the harness (0.6.1 needs an unstable rustc
feature current nightly dropped); it targets small STARK fields with SIMD kernels,
uses its own non-interoperable constants, and is coupled to Plonky3 traits.
The ct path executes the same sequence of field operations as
taceo-poseidon2 — verified line-for-line (same M4, same external/internal
matmuls, same in-place x^d S-box, same round schedule). The counts are fixed by
the algorithm; both crates hit them exactly. (Squarings are counted apart from
general multiplies because ark implements square() as a distinct, cheaper
routine. The t = 2, 3 internal layers use no multiplications.)
| instance | S-boxes | squarings | general muls | diag muls | field-mults | lin. adds | doublings |
|---|---|---|---|---|---|---|---|
BN254/BLS t=2 |
72 | 144 | 72 | 0 | 216 | 267 | 56 |
BN254/BLS t=3 |
80 | 160 | 80 | 0 | 240 | 405 | 56 |
BN254/BLS t=4 |
88 | 176 | 88 | 224 | 488 | 552 | 54 |
BN254/BLS t=8 |
121 | 242 | 121 | 456 | 819 | 1228 | 108 |
BN254/BLS t=12 |
153 | 306 | 153 | 684 | 1143 | 1860 | 162 |
Goldilocks t=8 |
86 | 172 | 172 | 176 | 520 | 668 | 108 |
Goldilocks t=12 |
118 | 236 | 236 | 264 | 736 | 1020 | 162 |
Goldilocks t=16 |
150 | 300 | 300 | 352 | 952 | 1372 | 216 |
Goldilocks t=20 |
182 | 364 | 364 | 440 | 1168 | 1724 | 270 |
Worked example — BN254 t=3: t·R_F + R_P = 3·8 + 56 = 80 S-boxes; each
x^5 = 2 squarings + 1 multiply → 160 squarings + 80 multiplies; the t=3
internal layer adds 0 multiplies → exactly 240 field multiplications, 405
additions, 56 doublings. taceo-poseidon2 performs the identical 240 / 405 / 56.
Reproduce: cargo bench --manifest-path bench-compare/Cargo.toml.
Correctness and memory safety are treated as load-bearing. Full detail in VERIFICATION.md and docs/.
| Technique | Result |
|---|---|
| Known-answer tests | BN254 t=3; BLS12-381 t=2,3,4; Goldilocks t=12 — exact vs reference |
| Generator reproduction | Grain constants + minpoly diagonal match Horizen bit-for-bit |
| Differential proptest | cheap-matrix vs independent dense-matrix reference — 14/14 |
| cargo-fuzz | differential 253k runs + byte-API 1.65M runs — 0 crashes |
| Miri | no UB over the deterministic suite |
| Kani | 4/4 bounded proofs of the structural skeleton |
| cargo-mutants | catch rate drove closing real test gaps; survivors equivalent |
| Verus | unbounded proofs of the integer specs (scaffold) |
| Coverage | 90.3% lines (with --ignored) |
The 254/255-bit field arithmetic is out of reach for a model checker, so its correctness rests on KATs + differential proptest/fuzz against an independent implementation; Kani/Verus prove the surrounding integer/index/shape logic. This split is explicit and standard for field-arithmetic crates.
For width t, degree d, full rounds R_F (even) and partial rounds R_P:
x <- M_E · x # initial external linear layer
R_F/2 × : x <- M_E · Sbox(x + RC_r) # full (external) rounds
R_P × : x <- M_I · Sbox0(x + rc_r) # partial (internal) rounds
R_F/2 × : x <- M_E · Sbox(x + RC_r) # full (external) rounds
Sboxraises every element tox^d;Sbox0only element 0.M_E(external/MDS):circ(2,1)fort=2,circ(2,1,1)fort=3; fort ≡ 0 (mod 4)a block matrix ofM4 = [[5,7,1,3],[4,6,1,1],[1,3,5,7],[1,1,4,6]](2·M4on the diagonal blocks,M4off-diagonal).M_I(internal): fixed[[2,1],[1,3]]/[[2,1,1],[1,2,1],[1,1,3]]fort = 2, 3; fort ≥ 4,(J − I) + diag(μ)for a Grain-derivedμ.- Sponge mode:
t = rate + capacity;capacity = 1covers 128-bit security over a ~256-bit field. Round numbers are per(field, t, d): over BN254/BLS withd=5,R_F=8andR_P=56(t≤4) or57(t=8,12).
no_std: the core (ct, permutation, params, sponge, digest, Grain, security checks) isno_std + alloc; thectfast path and owned instance builders do not allocate at all.- MSRV: Rust 1.89.
- Field backend: the
ctfast path is generic over a small [field::Field] trait, so each field uses its best arithmetic — ark-ff for BN254/BLS12-381 (where a hand-rolled Montgomery came out slower than ark's tuned one), and the hand-specialised [gold::GoldilocksFast] for Goldilocks (canonicalu64+ special-form reduction, ~30% faster than ark'sFp64there). Both are differential-tested against ark and the reference KATs. A future SIMD/fiat-crypto backend can drop in the same way. See docs/benchmarks.md.
Poseidon2 is an arithmetization-friendly hash for ZK proof systems, Merkle trees,
commitments, nullifiers, and Fiat–Shamir transcripts. This crate is a general
primitive, not tied to any chain. It can back a host-native hashing syscall (the
role light-poseidon plays for Solana's Poseidon1 sol_poseidon — but Poseidon2,
and field-generic), a prover/relayer, or an off-chain indexer equally. Versus the
alternatives: zkhash is the research reference (widest primitive zoo, no
verification suite); taceo-poseidon2 is BN254-only, permutation-only;
p3-poseidon2 is for small STARK fields inside Plonky3 with non-interoperable
constants. This crate is the field-generic, arbitrary-width, verified option.
Copyright 2026 Calisto Abel Mathias.
Licensed under the Apache License, Version 2.0. Per Section 4(d)
of the License, the NOTICE file must be included in any
redistribution or derivative work — so attribution to the author travels with the
code wherever it is used.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you shall be licensed as Apache-2.0, without any additional terms or conditions.