Skip to content

About

Fast, no_std, field-generic Poseidon2 permutation and hash (BN254 / BLS12-381 / Goldilocks) with a const-generic allocation-free fast path, Grain LFSR parameter generation, and a full verification suite.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

fast-poseidon2

CI Crates.io docs.rs License: Apache-2.0

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.

Quick start

[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)].

Features

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.

Two APIs

  • 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). Uses alloc. This is where sponge, compression, the byte API and arbitrary-width generation live.

Both share identical constants and produce identical output (checked by tests).

Instances

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

Generating arbitrary instances

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.

Benchmarks

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.

Smaller standalone crates (soroban / qp / poseidon-hash / pso)

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-hash follows 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.

Same-library check (rules out ark version)

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 fresh Vec each 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.

Exact operation count — identical to taceo

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.

Verification & assurance

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.

Specification

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
  • Sbox raises every element to x^d; Sbox0 only element 0.
  • M_E (external/MDS): circ(2,1) for t=2, circ(2,1,1) for t=3; for t ≡ 0 (mod 4) a block matrix of M4 = [[5,7,1,3],[4,6,1,1],[1,3,5,7],[1,1,4,6]] (2·M4 on the diagonal blocks, M4 off-diagonal).
  • M_I (internal): fixed [[2,1],[1,3]] / [[2,1,1],[1,2,1],[1,1,3]] for t = 2, 3; for t ≥ 4, (J − I) + diag(μ) for a Grain-derived μ.
  • Sponge mode: t = rate + capacity; capacity = 1 covers 128-bit security over a ~256-bit field. Round numbers are per (field, t, d): over BN254/BLS with d=5, R_F=8 and R_P=56 (t≤4) or 57 (t=8,12).

Compatibility

  • no_std: the core (ct, permutation, params, sponge, digest, Grain, security checks) is no_std + alloc; the ct fast path and owned instance builders do not allocate at all.
  • MSRV: Rust 1.89.
  • Field backend: the ct fast 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 (canonical u64 + special-form reduction, ~30% faster than ark's Fp64 there). 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.

Use cases & relationship to other crates

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.

License

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.

About

Fast, no_std, field-generic Poseidon2 permutation and hash (BN254 / BLS12-381 / Goldilocks) with a const-generic allocation-free fast path, Grain LFSR parameter generation, and a full verification suite.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages