Skip to content

feat: poseidon2 implementation and benchmark - #1

Merged
vinhtc27 merged 11 commits into
mainfrom
feat/poseidon2
Oct 5, 2026
Merged

vinhtc27 merged 11 commits into
mainfrom
feat/poseidon2

Conversation

@vinhtc27

@vinhtc27 vinhtc27 commented Sep 5, 2026 •

Copy link
Copy Markdown
Collaborator

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.
  • Tests: official Nomos compression-mode vectors and reference vectors from logos-storage/rust-poseidon-bn254-pure for every arity + witness fixtures computed with zerokit's native Poseidon2, reproduced by the circuits.
  • Benchmarks for rln_single, rln_multi, rln_poseidon2_single and rln_poseidon2_multi.
  • CI workflow aligned with zerokit conventions (build all circuits + full suite on node 18).
  • Improve test coverage and document circuit templates.

Circuits Benchmark:

  • Benchmarks were performed on a 2020 MacBook M1 Pro.
circuit constraints witness (avg) full prove (avg)
rln_single 5,820 10.04 ms 640.65 ms
rln_poseidon2_single 5,829 8.25 ms 625.85 ms
rln_multi (4 burns) 7,390 15.15 ms 716.78 ms
rln_poseidon2_multi (4 burns) 7,417 12.35 ms 711.87 ms

Witness generation is ~18% faster, while proving time and constraint counts stay the same for Poseidon2.

Static Analysis:

  • Three tools, each runnable locally through scripts/ (see README): circomspect 0.9.0 on every circuit file, Picus (commit 138b151, cvc5 1.4.1) and zkFuzz 2.2.1 on the six main circuits. utils.circom, poseidon2.circom and poseidon2_constants.circom are template libraries without a main and are covered through the circuits that include them.
  • Timings below are from the same 2020 MacBook M1 Pro.

circomspect

6 warnings, all by design and all inherited from the patterns audited at v1.0.0:

file warning why it is expected
utils.circom (RangeCheck, ConditionalRangeCheck) bitCheck occurs in a single constraint (CS0017) Num2Bits(LIMIT_BIT_SIZE) is instantiated for its side effect: it bounds messageId to 16 bits before LessThan
utils.circom (same two templates) Num2Bits instead of Num2Bits_strict (CS0010) LIMIT_BIT_SIZE = 16 is far below the field size, guarded by assert(LIMIT_BIT_SIZE < 253)
withdraw.circom, withdraw_poseidon2.circom addressSquared occurs in a single constraint (CS0017) the quadratic constraint binds the public address so the optimizer cannot drop it

No issues in poseidon2.circom, poseidon2_constants.circom and the four RLN circuits.

circomspect output
❯ scripts/circomspect.sh
----------------------
CIRCOMSPECT circuits/poseidon2.circom
----------------------
circomspect: analyzing template 'Poseidon2Permutation'
circomspect: analyzing template 'Poseidon2MatmulInternal'
circomspect: analyzing template 'Poseidon2'
circomspect: analyzing template 'Poseidon2MatmulExternal'
circomspect: analyzing template 'Poseidon2SBox'
circomspect: No issues found.
----------------------
CIRCOMSPECT circuits/poseidon2_constants.circom
----------------------
circomspect: analyzing function 'POSEIDON2_EXT_RC'
circomspect: analyzing function 'POSEIDON2_MAT_DIAG_M_1'
circomspect: analyzing function 'POSEIDON2_INT_RC'
circomspect: No issues found.
----------------------
CIRCOMSPECT circuits/rln_multi.circom
----------------------
circomspect: analyzing template 'RLN'
circomspect: No issues found.
----------------------
CIRCOMSPECT circuits/rln_poseidon2_multi.circom
----------------------
circomspect: analyzing template 'RLNPoseidon2Multi'
circomspect: No issues found.
----------------------
CIRCOMSPECT circuits/rln_poseidon2_single.circom
----------------------
circomspect: analyzing template 'RLNPoseidon2'
circomspect: No issues found.
----------------------
CIRCOMSPECT circuits/rln_single.circom
----------------------
circomspect: analyzing template 'RLN'
circomspect: No issues found.
----------------------
CIRCOMSPECT circuits/utils.circom
----------------------
circomspect: analyzing template 'ConditionalRangeCheck'
warning: Intermediate signals should typically occur in at least two separate constraints.
   ┌─ circuits/utils.circom:93:5
   │
93 │     signal bitCheck[LIMIT_BIT_SIZE] <== Num2Bits(LIMIT_BIT_SIZE)(activeMessageId);
   │     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   │     │
   │     The intermediate signal array `bitCheck` is declared here.
   │     The intermediate signals in `bitCheck` are constrained here.
   │
   = For more details, see https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md#under-constrained-signal.

warning: Using `Num2Bits` to convert field elements to bits may lead to aliasing issues.
   ┌─ circuits/utils.circom:93:41
   │
93 │     signal bitCheck[LIMIT_BIT_SIZE] <== Num2Bits(LIMIT_BIT_SIZE)(activeMessageId);
   │                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Circomlib template `Num2Bits` instantiated here.
   │
   = Consider using `Num2Bits_strict` if the input size may be >= than the prime size.
   = For more details, see https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md#non-strict-binary-conversion.

circomspect: analyzing template 'RangeCheck'
warning: Intermediate signals should typically occur in at least two separate constraints.
   ┌─ circuits/utils.circom:75:5
   │
75 │     signal bitCheck[LIMIT_BIT_SIZE] <== Num2Bits(LIMIT_BIT_SIZE)(messageId);
   │     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   │     │
   │     The intermediate signal array `bitCheck` is declared here.
   │     The intermediate signals in `bitCheck` are constrained here.
   │
   = For more details, see https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md#under-constrained-signal.

warning: Using `Num2Bits` to convert field elements to bits may lead to aliasing issues.
   ┌─ circuits/utils.circom:75:41
   │
75 │     signal bitCheck[LIMIT_BIT_SIZE] <== Num2Bits(LIMIT_BIT_SIZE)(messageId);
   │                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Circomlib template `Num2Bits` instantiated here.
   │
   = Consider using `Num2Bits_strict` if the input size may be >= than the prime size.
   = For more details, see https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md#non-strict-binary-conversion.

circomspect: analyzing template 'MerkleTreeInclusionProofPoseidon2'
circomspect: analyzing template 'MerkleTreeInclusionProof'
circomspect: 4 issues found.
----------------------
CIRCOMSPECT circuits/withdraw.circom
----------------------
circomspect: analyzing template 'Withdraw'
warning: Intermediate signals should typically occur in at least two separate constraints.
   ┌─ circuits/withdraw.circom:14:5
   │
14 │     signal addressSquared <== address * address;
   │     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   │     │
   │     The intermediate signal `addressSquared` is declared here.
   │     The intermediate signal `addressSquared` is constrained here.
   │
   = For more details, see https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md#under-constrained-signal.

circomspect: 1 issue found.
----------------------
CIRCOMSPECT circuits/withdraw_poseidon2.circom
----------------------
circomspect: analyzing template 'WithdrawPoseidon2'
warning: Intermediate signals should typically occur in at least two separate constraints.
   ┌─ circuits/withdraw_poseidon2.circom:14:5
   │
14 │     signal addressSquared <== address * address;
   │     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   │     │
   │     The intermediate signal `addressSquared` is declared here.
   │     The intermediate signal `addressSquared` is constrained here.
   │
   = For more details, see https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md#under-constrained-signal.

circomspect: 1 issue found.

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):

circuit non-linear / linear constraints wires known signals time
withdraw 217 / 365 585 585 34 ms
withdraw_poseidon2 217 / 469 689 689 38 ms
rln_single 5,893 / 12,637 18,553 18,553 999 ms
rln_poseidon2_single 5,830 / 16,205 22,058 22,058 1,234 ms
rln_multi 7,481 / 15,846 23,357 23,318 1,314 ms
rln_poseidon2_multi 7,418 / 20,356 27,804 27,765 1,563 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
❯ scripts/picus.sh
----------------------
PICUS withdraw
----------------------
template instances: 71
non-linear constraints: 217
linear constraints: 365
public inputs: 1
public outputs: 1
private inputs: 1
private outputs: 0
wires: 585
labels: 585
Written successfully: build/picus/withdraw/withdraw.r1cs
Written successfully: build/picus/withdraw/withdraw.sym
Everything went okay, circom safe
warning: could not find z3 executable at /root/.local/share/racket/8.10/pkgs/rosette/bin/z3
parsing r1cs...
configuring precondition...
stats: started_algorithm = 1 unit
[binary01 lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
stats: known_size_imm = 585 unit (Number of inferred known signals (so far))
stats: solving_time_cpu = 0 ms (Time spent for solving (cpu))
stats: solving_time_real = 0 ms (Time spent for solving (real))
stats: solving_time_gc = 0 ms (Time spent for solving (gc))
stats: finished_algorithm = 1 unit
stats: algorithm_time_cpu = 34 ms (Time spent for main algorithm (cpu))
stats: algorithm_time_real = 34 ms (Time spent for main algorithm (real))
stats: algorithm_time_gc = 2 ms (Time spent for main algorithm (gc))
stats: known_size = 585 unit (Number of inferred known signals)
stats: finished_with_guarantee = 1 unit
The circuit is properly constrained
Exiting Picus with the code 8
----------------------
PICUS withdraw_poseidon2
----------------------
template instances: 6
non-linear constraints: 217
linear constraints: 469
public inputs: 1
public outputs: 1
private inputs: 1
private outputs: 0
wires: 689
labels: 689
Written successfully: build/picus/withdraw_poseidon2/withdraw_poseidon2.r1cs
Written successfully: build/picus/withdraw_poseidon2/withdraw_poseidon2.sym
Everything went okay, circom safe
warning: could not find z3 executable at /root/.local/share/racket/8.10/pkgs/rosette/bin/z3
parsing r1cs...
configuring precondition...
stats: started_algorithm = 1 unit
[binary01 lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
stats: known_size_imm = 689 unit (Number of inferred known signals (so far))
stats: solving_time_cpu = 0 ms (Time spent for solving (cpu))
stats: solving_time_real = 0 ms (Time spent for solving (real))
stats: solving_time_gc = 0 ms (Time spent for solving (gc))
stats: finished_algorithm = 1 unit
stats: algorithm_time_cpu = 38 ms (Time spent for main algorithm (cpu))
stats: algorithm_time_real = 38 ms (Time spent for main algorithm (real))
stats: algorithm_time_gc = 2 ms (Time spent for main algorithm (gc))
stats: known_size = 689 unit (Number of inferred known signals)
stats: finished_with_guarantee = 1 unit
The circuit is properly constrained
Exiting Picus with the code 8
----------------------
PICUS rln_single
----------------------
template instances: 216
non-linear constraints: 5893
linear constraints: 12637
public inputs: 2
public outputs: 3
private inputs: 43
private outputs: 0
wires: 18553
labels: 18553
Written successfully: build/picus/rln_single/rln_single.r1cs
Written successfully: build/picus/rln_single/rln_single.sym
Everything went okay, circom safe
warning: could not find z3 executable at /root/.local/share/racket/8.10/pkgs/rosette/bin/z3
parsing r1cs...
configuring precondition...
stats: started_algorithm = 1 unit
[binary01 lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
stats: known_size_imm = 18553 unit (Number of inferred known signals (so far))
stats: solving_time_cpu = 0 ms (Time spent for solving (cpu))
stats: solving_time_real = 0 ms (Time spent for solving (real))
stats: solving_time_gc = 0 ms (Time spent for solving (gc))
stats: finished_algorithm = 1 unit
stats: algorithm_time_cpu = 999 ms (Time spent for main algorithm (cpu))
stats: algorithm_time_real = 999 ms (Time spent for main algorithm (real))
stats: algorithm_time_gc = 152 ms (Time spent for main algorithm (gc))
stats: known_size = 18553 unit (Number of inferred known signals)
stats: finished_with_guarantee = 1 unit
The circuit is properly constrained
Exiting Picus with the code 8
----------------------
PICUS rln_multi
----------------------
template instances: 217
non-linear constraints: 7481
linear constraints: 15846
public inputs: 6
public outputs: 9
private inputs: 46
private outputs: 0
wires: 23357
labels: 23357
Written successfully: build/picus/rln_multi/rln_multi.r1cs
Written successfully: build/picus/rln_multi/rln_multi.sym
Everything went okay, circom safe
warning: could not find z3 executable at /root/.local/share/racket/8.10/pkgs/rosette/bin/z3
parsing r1cs...
configuring precondition...
stats: started_algorithm = 1 unit
[binary01 lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
stats: known_size_imm = 23318 unit (Number of inferred known signals (so far))
stats: solving_time_cpu = 0 ms (Time spent for solving (cpu))
stats: solving_time_real = 0 ms (Time spent for solving (real))
stats: solving_time_gc = 0 ms (Time spent for solving (gc))
stats: finished_algorithm = 1 unit
stats: algorithm_time_cpu = 1314 ms (Time spent for main algorithm (cpu))
stats: algorithm_time_real = 1314 ms (Time spent for main algorithm (real))
stats: algorithm_time_gc = 264 ms (Time spent for main algorithm (gc))
stats: known_size = 23318 unit (Number of inferred known signals)
stats: finished_with_guarantee = 1 unit
The circuit is properly constrained
Exiting Picus with the code 8
----------------------
PICUS rln_poseidon2_single
----------------------
template instances: 20
non-linear constraints: 5830
linear constraints: 16205
public inputs: 2
public outputs: 3
private inputs: 43
private outputs: 0
wires: 22058
labels: 22058
Written successfully: build/picus/rln_poseidon2_single/rln_poseidon2_single.r1cs
Written successfully: build/picus/rln_poseidon2_single/rln_poseidon2_single.sym
Everything went okay, circom safe
warning: could not find z3 executable at /root/.local/share/racket/8.10/pkgs/rosette/bin/z3
parsing r1cs...
configuring precondition...
stats: started_algorithm = 1 unit
[binary01 lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
stats: known_size_imm = 22058 unit (Number of inferred known signals (so far))
stats: solving_time_cpu = 0 ms (Time spent for solving (cpu))
stats: solving_time_real = 0 ms (Time spent for solving (real))
stats: solving_time_gc = 0 ms (Time spent for solving (gc))
stats: finished_algorithm = 1 unit
stats: algorithm_time_cpu = 1233 ms (Time spent for main algorithm (cpu))
stats: algorithm_time_real = 1234 ms (Time spent for main algorithm (real))
stats: algorithm_time_gc = 265 ms (Time spent for main algorithm (gc))
stats: known_size = 22058 unit (Number of inferred known signals)
stats: finished_with_guarantee = 1 unit
The circuit is properly constrained
Exiting Picus with the code 8
----------------------
PICUS rln_poseidon2_multi
----------------------
template instances: 21
non-linear constraints: 7418
linear constraints: 20356
public inputs: 6
public outputs: 9
private inputs: 46
private outputs: 0
wires: 27804
labels: 27804
Written successfully: build/picus/rln_poseidon2_multi/rln_poseidon2_multi.r1cs
Written successfully: build/picus/rln_poseidon2_multi/rln_poseidon2_multi.sym
Everything went okay, circom safe
warning: could not find z3 executable at /root/.local/share/racket/8.10/pkgs/rosette/bin/z3
parsing r1cs...
configuring precondition...
stats: started_algorithm = 1 unit
[binary01 lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
[linear lemma] starting propagation
[basis2 lemma] starting propagation
[aboz lemma] starting propagation
[bim lemma] starting propagation
stats: known_size_imm = 27765 unit (Number of inferred known signals (so far))
stats: solving_time_cpu = 0 ms (Time spent for solving (cpu))
stats: solving_time_real = 0 ms (Time spent for solving (real))
stats: solving_time_gc = 0 ms (Time spent for solving (gc))
stats: finished_algorithm = 1 unit
stats: algorithm_time_cpu = 1563 ms (Time spent for main algorithm (cpu))
stats: algorithm_time_real = 1563 ms (Time spent for main algorithm (real))
stats: algorithm_time_gc = 296 ms (Time spent for main algorithm (gc))
stats: known_size = 27765 unit (Number of inferred known signals)
stats: finished_with_guarantee = 1 unit
The circuit is properly constrained
Exiting Picus with the code 8

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 circomlib Num2Bits and IsZero, both pinned by their own constraints, so the search degenerates to random input search and is weaker evidence than Picus.

circuit trace / side constraints time
withdraw 582 / 582 19.2 s
withdraw_poseidon2 697 / 686 13.4 s
rln_single 18,568 / 18,532 458.8 s
rln_poseidon2_single 22,337 / 22,037 369.5 s
rln_multi 23,481 / 23,330 594.5 s
rln_poseidon2_multi 28,258 / 27,777 476.4 s
zkFuzz output
❯ scripts/zkfuzz.sh
----------------------
ZKFUZZ withdraw (10 generations)
----------------------

           zkFuzz
      ZK Circuit Fuzzer

Welcome to the zkFuzz
══════════════════════════════════
🧾 Loading Whitelists...
🧩 Parsing Templates...
⚙️ Parsing Function...
🛒 Gathering Trace/Side Constraints...
══════════════════════════════════
══════════════════════════════════
🩺 Scanning TCCT Instances...
[2026-09-29T07:11:22Z INFO  zkfuzz]
    🧬 Mutation Settings:
        ├─ Program Population Size                    : 30
        ├─ Input Population Size                      : 30
        ├─ Maximum Number of Generations              : 10
        ├─ Input Initialization Method                : random
        ├─ Fitness Function                           : error
        ├─ Trace Mutation Rate                        : 0.3
        ├─ Trace Crossover Rate                       : 0.5
        ├─ Operator Mutation Rate                     : 0.1
        ├─ Runimte Mutation Rate                      : 0.3
        ├─ Maximum Number of Mutated Points           : 10
        ├─ Input Generation Interval                  : 1
        ├─ Input Generation Maximum Iteration         : 30
        ├─ Input Generation Crossover Rate            : 0.66
        ├─ Input Generation Mutation Rate             : 0.5
        └─ Input Generation Singlepoint Mutation Rate : 0.5
[2026-09-29T07:11:22Z INFO  zkfuzz::mutator::mutation_test]
    ⚖️ Constraints Summary:
        ├─ #Trace Constraints : 582
        ├─ #Side Constraints  : 582
        ├─ #Input Variables   : 2
        └─ #Mutation Candidate: 0
🎲 Random Seed: 13520740047155316464
🧬 Generation: 9/10 (-21888242871839275222246405745257275088548364400416034343698204186575808495617)
 └─ No solution found after 10 generations
╔═══════════════════════════════════════════════════════════════╗
║                        zkFuzz Report                        ║
╚═══════════════════════════════════════════════════════════════╝
📊 Execution Summary:
 ├─ Prime Number      : 21888242871839275222246405745257275088548364400416034343698204186575808495617
 ├─ Compression Rate  : 100.00% (582/582)
 ├─ Verification      : 🆗 No Counter Example Found
 └─ Execution Time    : 19.161524666s
════════════════════════════════════════════════════════════════
Everything went okay
----------------------
ZKFUZZ withdraw_poseidon2 (10 generations)
----------------------

           zkFuzz
      ZK Circuit Fuzzer

Welcome to the zkFuzz
══════════════════════════════════
🧾 Loading Whitelists...
🧩 Parsing Templates...
⚙️ Parsing Function...
🛒 Gathering Trace/Side Constraints...
══════════════════════════════════
══════════════════════════════════
🩺 Scanning TCCT Instances...
[2026-09-29T16:30:58Z INFO  zkfuzz]
    🧬 Mutation Settings:
        ├─ Program Population Size                    : 30
        ├─ Input Population Size                      : 30
        ├─ Maximum Number of Generations              : 10
        ├─ Input Initialization Method                : random
        ├─ Fitness Function                           : error
        ├─ Trace Mutation Rate                        : 0.3
        ├─ Trace Crossover Rate                       : 0.5
        ├─ Operator Mutation Rate                     : 0.1
        ├─ Runimte Mutation Rate                      : 0.3
        ├─ Maximum Number of Mutated Points           : 10
        ├─ Input Generation Interval                  : 1
        ├─ Input Generation Maximum Iteration         : 30
        ├─ Input Generation Crossover Rate            : 0.66
        ├─ Input Generation Mutation Rate             : 0.5
        └─ Input Generation Singlepoint Mutation Rate : 0.5
[2026-09-29T16:30:58Z INFO  zkfuzz::mutator::mutation_test]
    ⚖️ Constraints Summary:
        ├─ #Trace Constraints : 697
        ├─ #Side Constraints  : 686
        ├─ #Input Variables   : 2
        └─ #Mutation Candidate: 0
🎲 Random Seed: 676321017243594270
🧬 Generation: 9/10 (-21888242871839275222246405745257275088548364400416034343698204186575808495617)
 └─ No solution found after 10 generations
╔═══════════════════════════════════════════════════════════════╗
║                        zkFuzz Report                        ║
╚═══════════════════════════════════════════════════════════════╝
📊 Execution Summary:
 ├─ Prime Number      : 21888242871839275222246405745257275088548364400416034343698204186575808495617
 ├─ Compression Rate  : 99.85% (686/687)
 ├─ Verification      : 🆗 No Counter Example Found
 └─ Execution Time    : 13.41731825s
════════════════════════════════════════════════════════════════
Everything went okay
----------------------
ZKFUZZ rln_single (10 generations)
----------------------

           zkFuzz
      ZK Circuit Fuzzer

Welcome to the zkFuzz
══════════════════════════════════
🧾 Loading Whitelists...
🧩 Parsing Templates...
⚙️ Parsing Function...
🛒 Gathering Trace/Side Constraints...
══════════════════════════════════
══════════════════════════════════
🩺 Scanning TCCT Instances...
[2026-09-29T07:11:42Z INFO  zkfuzz]
    🧬 Mutation Settings:
        ├─ Program Population Size                    : 30
        ├─ Input Population Size                      : 30
        ├─ Maximum Number of Generations              : 10
        ├─ Input Initialization Method                : random
        ├─ Fitness Function                           : error
        ├─ Trace Mutation Rate                        : 0.3
        ├─ Trace Crossover Rate                       : 0.5
        ├─ Operator Mutation Rate                     : 0.1
        ├─ Runimte Mutation Rate                      : 0.3
        ├─ Maximum Number of Mutated Points           : 10
        ├─ Input Generation Interval                  : 1
        ├─ Input Generation Maximum Iteration         : 30
        ├─ Input Generation Crossover Rate            : 0.66
        ├─ Input Generation Mutation Rate             : 0.5
        └─ Input Generation Singlepoint Mutation Rate : 0.5
[2026-09-29T07:11:42Z INFO  zkfuzz::mutator::mutation_test]
    ⚖️ Constraints Summary:
        ├─ #Trace Constraints : 18568
        ├─ #Side Constraints  : 18532
        ├─ #Input Variables   : 45
        └─ #Mutation Candidate: 0
🎲 Random Seed: 12928762443862615417
🧬 Generation: 9/10 (-14737)
 └─ No solution found after 10 generations
╔═══════════════════════════════════════════════════════════════╗
║                        zkFuzz Report                        ║
╚═══════════════════════════════════════════════════════════════╝
📊 Execution Summary:
 ├─ Prime Number      : 21888242871839275222246405745257275088548364400416034343698204186575808495617
 ├─ Compression Rate  : 99.81% (18532/18567)
 ├─ Verification      : 🆗 No Counter Example Found
 └─ Execution Time    : 458.816593625s
════════════════════════════════════════════════════════════════
Everything went okay
----------------------
ZKFUZZ rln_multi (10 generations)
----------------------

           zkFuzz
      ZK Circuit Fuzzer

Welcome to the zkFuzz
══════════════════════════════════
🧾 Loading Whitelists...
🧩 Parsing Templates...
⚙️ Parsing Function...
🛒 Gathering Trace/Side Constraints...
══════════════════════════════════
══════════════════════════════════
🩺 Scanning TCCT Instances...
[2026-09-29T07:19:21Z INFO  zkfuzz]
    🧬 Mutation Settings:
        ├─ Program Population Size                    : 30
        ├─ Input Population Size                      : 30
        ├─ Maximum Number of Generations              : 10
        ├─ Input Initialization Method                : random
        ├─ Fitness Function                           : error
        ├─ Trace Mutation Rate                        : 0.3
        ├─ Trace Crossover Rate                       : 0.5
        ├─ Operator Mutation Rate                     : 0.1
        ├─ Runimte Mutation Rate                      : 0.3
        ├─ Maximum Number of Mutated Points           : 10
        ├─ Input Generation Interval                  : 1
        ├─ Input Generation Maximum Iteration         : 30
        ├─ Input Generation Crossover Rate            : 0.66
        ├─ Input Generation Mutation Rate             : 0.5
        └─ Input Generation Singlepoint Mutation Rate : 0.5
[2026-09-29T07:19:21Z INFO  zkfuzz::mutator::mutation_test]
    ⚖️ Constraints Summary:
        ├─ #Trace Constraints : 23481
        ├─ #Side Constraints  : 23330
        ├─ #Input Variables   : 52
        └─ #Mutation Candidate: 0
🎲 Random Seed: 14776266381620809145
🧬 Generation: 9/10 (-21888242871839275222246405745257275088548364400416034343698204186575808479615)
 └─ No solution found after 10 generations
╔═══════════════════════════════════════════════════════════════╗
║                        zkFuzz Report                        ║
╚═══════════════════════════════════════════════════════════════╝
📊 Execution Summary:
 ├─ Prime Number      : 21888242871839275222246405745257275088548364400416034343698204186575808495617
 ├─ Compression Rate  : 99.39% (23330/23474)
 ├─ Verification      : 🆗 No Counter Example Found
 └─ Execution Time    : 594.501981291s
════════════════════════════════════════════════════════════════
Everything went okay
----------------------
ZKFUZZ rln_poseidon2_single (10 generations)
----------------------

           zkFuzz
      ZK Circuit Fuzzer

Welcome to the zkFuzz
══════════════════════════════════
🧾 Loading Whitelists...
🧩 Parsing Templates...
⚙️ Parsing Function...
🛒 Gathering Trace/Side Constraints...
══════════════════════════════════
══════════════════════════════════
🩺 Scanning TCCT Instances...
[2026-09-29T16:31:11Z INFO  zkfuzz]
    🧬 Mutation Settings:
        ├─ Program Population Size                    : 30
        ├─ Input Population Size                      : 30
        ├─ Maximum Number of Generations              : 10
        ├─ Input Initialization Method                : random
        ├─ Fitness Function                           : error
        ├─ Trace Mutation Rate                        : 0.3
        ├─ Trace Crossover Rate                       : 0.5
        ├─ Operator Mutation Rate                     : 0.1
        ├─ Runimte Mutation Rate                      : 0.3
        ├─ Maximum Number of Mutated Points           : 10
        ├─ Input Generation Interval                  : 1
        ├─ Input Generation Maximum Iteration         : 30
        ├─ Input Generation Crossover Rate            : 0.66
        ├─ Input Generation Mutation Rate             : 0.5
        └─ Input Generation Singlepoint Mutation Rate : 0.5
[2026-09-29T16:31:11Z INFO  zkfuzz::mutator::mutation_test]
    ⚖️ Constraints Summary:
        ├─ #Trace Constraints : 22337
        ├─ #Side Constraints  : 22037
        ├─ #Input Variables   : 45
        └─ #Mutation Candidate: 0
🎲 Random Seed: 4276762615745071864
🧬 Generation: 9/10 (-15626)
 └─ No solution found after 10 generations
╔═══════════════════════════════════════════════════════════════╗
║                        zkFuzz Report                        ║
╚═══════════════════════════════════════════════════════════════╝
📊 Execution Summary:
 ├─ Prime Number      : 21888242871839275222246405745257275088548364400416034343698204186575808495617
 ├─ Compression Rate  : 99.84% (22037/22072)
 ├─ Verification      : 🆗 No Counter Example Found
 └─ Execution Time    : 369.47924775s
════════════════════════════════════════════════════════════════
Everything went okay
----------------------
ZKFUZZ rln_poseidon2_multi (10 generations)
----------------------

           zkFuzz
      ZK Circuit Fuzzer

Welcome to the zkFuzz
══════════════════════════════════
🧾 Loading Whitelists...
🧩 Parsing Templates...
⚙️ Parsing Function...
🛒 Gathering Trace/Side Constraints...
══════════════════════════════════
══════════════════════════════════
🩺 Scanning TCCT Instances...
[2026-09-29T16:37:21Z INFO  zkfuzz]
    🧬 Mutation Settings:
        ├─ Program Population Size                    : 30
        ├─ Input Population Size                      : 30
        ├─ Maximum Number of Generations              : 10
        ├─ Input Initialization Method                : random
        ├─ Fitness Function                           : error
        ├─ Trace Mutation Rate                        : 0.3
        ├─ Trace Crossover Rate                       : 0.5
        ├─ Operator Mutation Rate                     : 0.1
        ├─ Runimte Mutation Rate                      : 0.3
        ├─ Maximum Number of Mutated Points           : 10
        ├─ Input Generation Interval                  : 1
        ├─ Input Generation Maximum Iteration         : 30
        ├─ Input Generation Crossover Rate            : 0.66
        ├─ Input Generation Mutation Rate             : 0.5
        └─ Input Generation Singlepoint Mutation Rate : 0.5
[2026-09-29T16:37:21Z INFO  zkfuzz::mutator::mutation_test]
    ⚖️ Constraints Summary:
        ├─ #Trace Constraints : 28258
        ├─ #Side Constraints  : 27777
        ├─ #Input Variables   : 52
        └─ #Mutation Candidate: 0
🎲 Random Seed: 10213515019717298256
🧬 Generation: 9/10 (-31290)
 └─ No solution found after 10 generations
╔═══════════════════════════════════════════════════════════════╗
║                        zkFuzz Report                        ║
╚═══════════════════════════════════════════════════════════════╝
📊 Execution Summary:
 ├─ Prime Number      : 21888242871839275222246405745257275088548364400416034343698204186575808495617
 ├─ Compression Rate  : 99.48% (27777/27921)
 ├─ Verification      : 🆗 No Counter Example Found
 └─ Execution Time    : 476.352384916s
════════════════════════════════════════════════════════════════
Everything went okay

LLM (Fable 5.1) with zk-skills

The circuits were also reviewed against the zksecurity circom-auditor checklist (48 attack vectors): no findings.

@vinhtc27 vinhtc27 changed the title Feat/poseidon2 feat: poseidon2 Sep 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread test/rln_poseidon2_multi.test.ts
@vinhtc27 vinhtc27 changed the title feat: poseidon2 feat: poseidon2 implementation and benchmark Sep 11, 2026
@vinhtc27
vinhtc27 requested a balanced review from Copilot September 12, 2026 01:58

This comment was marked as resolved.

@vinhtc27
vinhtc27 requested a review from bkomuves September 14, 2026 09:19
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).
@vinhtc27
vinhtc27 requested a balanced review from Copilot September 30, 2026 11:35

This comment was marked as off-topic.

@vinhtc27
vinhtc27 merged commit 2cef30b into main Oct 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants