Repository navigation
feat: poseidon2 implementation and benchmark - #1
Merged
Merged
Conversation
vinhtc27
force-pushed
the
feat/poseidon2
branch
from
September 10, 2026 16:34
32f32ee to
68145c0
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Multi-burn Poseidon2 tests need equivalent validation and masking edge-case coverage before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds Poseidon2 hashing support for BN254 RLN circuits, including circuit variants, constants, tests, benchmarks, build integration, and CI updates.
Changes:
- Implements Poseidon2 permutation, hashing, and Merkle support.
- Adds single- and multi-burn Poseidon2 RLN circuits.
- Adds reference vectors, fixtures, benchmarks, and build/CI coverage.
File summaries
| File | Reviewed changes |
|---|---|
test/rln_single.test.ts |
Baseline circuit benchmarks |
test/rln_poseidon2_single.test.ts |
Single-burn fixtures and benchmarks |
test/rln_poseidon2_multi.test.ts |
Multi-burn fixtures and benchmarks |
test/poseidon2.test.ts |
Poseidon2 reference-vector tests |
test/circuits/poseidon2_arity3.circom |
Arity-3 test wrapper |
test/circuits/poseidon2_arity2.circom |
Arity-2 test wrapper |
test/circuits/poseidon2_arity1.circom |
Arity-1 test wrapper |
scripts/build-circuits.sh |
Builds new circuit variants |
package.json |
Includes new circuits in builds |
circuits/utils.circom |
Poseidon2 Merkle proof support |
circuits/rln_poseidon2_single.circom |
Single-burn Poseidon2 RLN circuit |
circuits/rln_poseidon2_multi.circom |
Multi-burn Poseidon2 RLN circuit |
circuits/poseidon2.circom |
Poseidon2 permutation and hash implementation |
circuits/poseidon2_constants.circom |
Generated Poseidon2 constants |
.github/workflows/test.yml |
Updated CI build and test workflow |
Review details
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
vinhtc27
force-pushed
the
feat/poseidon2
branch
from
September 28, 2026 14:54
625ffe3 to
14ed7fa
Compare
vinhtc27
force-pushed
the
feat/poseidon2
branch
from
September 29, 2026 07:23
bdfddd0 to
eed60af
Compare
vinhtc27
force-pushed
the
feat/poseidon2
branch
from
September 29, 2026 08:05
eed60af to
1aad1d0
Compare
vinhtc27
added a commit
to vacp2p/zerokit
that referenced
this pull request
Sep 30, 2026
# Poseidon2 support in Zerokit - Using circuits from circom-rln's [PR](vacp2p/circom-rln#1) - Update Zerokit-API RFC [PR](logos-co/logos-lips#460) ## Changes: - `utils`: `Poseidon2` permutation (t = 2..4, HorizenLabs constant set) and one-shot hash in the Logos compression layout (`[inputs.., 0]`, capacity zero last). - `rln`: `Poseidon2Hash` hasher and `RLNBuilder` `_poseidon2` constructors; the rest of the API is unchanged. - Resources restructured to `resources/tree_depth_N/<circuit_name>/`, Poseidon2 zkey/graph embedded as defaults, proof roundtrip test. - Benchmarks for both hashes: hash, the three Merkle tree backends, proof pipeline. - FFI and WASM exposure for Poseidon2 and new poseidon2 example for Rust/C/Nim/WASM. - Function `ffi_rln_witness_input_to_partial_witness` renamed to `ffi_rln_partial_witness_input_from_witness` (target-side `from`, matching Rust `From` and WASM `fromWitness`), `RLNPartialWitnessInput` accessors added with FFI/WASM getters, and the per-hash `toProofValues`/`toProofValuesPoseidon2` helpers dropped from `WasmRLNWitnessInput`. ## Hash layout: The one-shot hash packs the state as `[inputs.., 0]` with the capacity zero last, making it bit-exact with [logos-storage/rust-poseidon-bn254-pure](https://github.com/logos-storage/rust-poseidon-bn254-pure) (the LEZ reference implementation) and the official Nomos compression-mode test vectors, which are pinned in the test suite. This intentionally differs from circomlib's Poseidon layout (zero first). ## Poseidon2 Benchmark: - Benchmarks were performed on a 2020 MacBook M1 Pro. The "drafted" column is the first straightforward implementation; the "optimized" column adds per-width kernels and an internal-round fast path for the published `[1, .., 1, 2]` diagonals (the cheap-diagonal evaluation the Poseidon2 paper picks these entries for). Hash values are identical across both. | bench | Poseidon | Poseidon2 (drafted) | Poseidon2 (optimized) | Δ vs Poseidon | |---|---|---|---|---| | `hash` (1 input) | 9.87 µs | 6.64 µs | 4.73 µs | −52% | | `hash` (10 x 1 input) | 111 µs | 66.6 µs | 49.5 µs | −55% | | `hash` (100 x 1 input) | 1.11 ms | 756 µs | 543 µs | −51% | | `hash` (1000 x 1 input) | 11.30 ms | 7.74 ms | 5.79 ms | −49% | | `FullMerkleTree::set` | 368 µs | 180 µs | 112 µs | −70% | | `FullMerkleTree::set_range` (8192 leaves) | 32.3 ms | 16.4 ms | 10.2 ms | −68% | | `FullMerkleTree::update_next` | 385 µs | 195 µs | 128 µs | −67% | | `FullMerkleTree::verify` | 368 µs | 179 µs | 110 µs | −70% | | `FullMerkleTreeProof::compute_root_from` | 367 µs | 177 µs | 110 µs | −70% | | `OptimalMerkleTree::set` | 369 µs | 181 µs | 113 µs | −69% | | `OptimalMerkleTree::set_range` (8192 leaves) | 33.5 ms | 17.4 ms | 12.0 ms | −64% | | `OptimalMerkleTree::update_next` | 390 µs | 200 µs | 131 µs | −66% | | `OptimalMerkleTree::verify` | 369 µs | 178 µs | 109 µs | −70% | | `OptimalMerkleTreeProof::compute_root_from` | 367 µs | 176 µs | 109 µs | −70% | | `PmTree::set` | 543 µs | 450 µs | 243 µs | −55% | | `PmTree::set_range` (8192 leaves) | 54.5 ms | 37.1 ms | 33.5 ms | −39% | | `PmTree::update_next` | 6.14 ms | 5.93 ms | 3.63 ms | −41% | | `PmTree::verify` | 370 µs | 180 µs | 110 µs | −70% | | `PmTreeProof::compute_root_from` | 370 µs | 179 µs | 111 µs | −70% | | `RLN::generate_proof` | 68.4 ms | 70.5 ms | unchanged | on par | | `RLN::generate_partial_proof` | 44.5 ms | 43.9 ms | unchanged | on par | | `RLN::finish_proof` | 28.9 ms | 29.1 ms | unchanged | on par | Native Merkle hashing is about 3x as fast with Poseidon2, while proving time stays the same like in the Circom bench. ## Cross-implementation Benchmark: The [`benchmark_poseidon`](https://github.com/vacp2p/zerokit/tree/benchmark_poseidon/benchmark_poseidon) branch benchmarks five BN254 Poseidon/Poseidon2 implementations, natively and as RISC0 zkVM cycle counts ([RESULTS.md](https://github.com/vacp2p/zerokit/blob/benchmark_poseidon/benchmark_poseidon/RESULTS.md)). Once initialized, zerokit is the fastest implementation of each hash family; runtime Grain constant derivation makes the hashers unsuitable for zkVM guests as-is (938M cycles per proof for Poseidon, 16.9M for Poseidon2). The other subjects: [light-poseidon](https://github.com/Lightprotocol/light-poseidon) trails zerokit's Poseidon by ~25% per hash on both targets; [jf-poseidon2](https://github.com/EspressoSystems/jellyfish) is the Poseidon2 speed reference zerokit now matches; [rust-poseidon-bn254-pure](https://github.com/logos-storage/rust-poseidon-bn254-pure) (32-bit limbs built for rv32) ties the arkworks-based crates per hash in-guest and is 2.8x slower natively, but is the leanest whole guest (no init, single proof segment).
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Poseidon2 circuits impl compatible with zerokit in Circom
Changes:
poseidon2.circom+poseidon2_constants.circom: Poseidon2 permutation and one-shot hash (t = 2..4) in the Logos compression layout ([inputs.., 0], capacity zero last), with machine-generated constants; t = 3 matches the published HorizenLabs instance.rln_poseidon2_single.circom/rln_poseidon2_multi.circom: the RLN circuits with every hash swapped to Poseidon2, everything else unchanged.rln_single,rln_multi,rln_poseidon2_singleandrln_poseidon2_multi.Circuits Benchmark:
Witness generation is ~18% faster, while proving time and constraint counts stay the same for Poseidon2.
Static Analysis:
scripts/(see README): circomspect 0.9.0 on every circuit file, Picus (commit138b151, cvc5 1.4.1) and zkFuzz 2.2.1 on the six main circuits.utils.circom,poseidon2.circomandposeidon2_constants.circomare template libraries without amainand are covered through the circuits that include them.circomspect
6 warnings, all by design and all inherited from the patterns audited at v1.0.0:
RangeCheck,ConditionalRangeCheck)bitCheckoccurs in a single constraint (CS0017)Num2Bits(LIMIT_BIT_SIZE)is instantiated for its side effect: it boundsmessageIdto 16 bits beforeLessThanNum2Bitsinstead ofNum2Bits_strict(CS0010)LIMIT_BIT_SIZE = 16is far below the field size, guarded byassert(LIMIT_BIT_SIZE < 253)addressSquaredoccurs in a single constraint (CS0017)addressso the optimizer cannot drop itNo issues in
poseidon2.circom,poseidon2_constants.circomand the four RLN circuits.circomspect output
Picus
Checks that every output is uniquely determined by the inputs (no second witness for the same public inputs). Every circuit is
properly constrained, decided by the propagation lemmas alone (solver time 0 ms):The 39 signals left unknown in the multi circuits are internal (masked inactive slots); Picus finishes with guarantee, so every output is determined.
Picus output
zkFuzz
Program-mutation fuzzing, 10 generations with populations of 30. No counterexample on any circuit. All six circuits report zero mutation candidates: the only
<--assignments are inside circomlibNum2BitsandIsZero, both pinned by their own constraints, so the search degenerates to random input search and is weaker evidence than Picus.zkFuzz output
LLM (Fable 5.1) with zk-skills
The circuits were also reviewed against the zksecurity circom-auditor checklist (48 attack vectors): no findings.