Skip to content

research: Poseidon2 vs Poseidon — arity gap makes the full swap a net loss - #57

Open
alexandre-mrt wants to merge 1 commit into
mainfrom
research/2026-09-09-poseidon2-merkle-arity
Open

research: Poseidon2 vs Poseidon — arity gap makes the full swap a net loss#57
alexandre-mrt wants to merge 1 commit into
mainfrom
research/2026-09-09-poseidon2-merkle-arity

Conversation

@alexandre-mrt

Copy link
Copy Markdown
Owner

Summary

Nightly research loop, 2026-09-09. Queue item #2 (Poseidon2 vs current Poseidon), after ~40 minutes
retrying queue item #1 (on-chain gas — still blocked, see below).

Hypothesis: swapping Veil's circomlib-Poseidon hash calls for Poseidon2 reduces R1CS constraint
count for at least one of the three circuits (transfer/compliance/withdraw), which should lower
Groth16 prover time since prover cost is dominated by constraint count.

Result: falsified, measured exactly. The full swap makes all three circuits worse:

Circuit Baseline Poseidon2 Δ
transfer.circom 13,611 15,979 +17.4%
compliance.circom 12,743 13,405 +5.2%
withdraw.circom 3,058 5,902 +93.0%

Root cause: Poseidon2's optimized linear layer only supports state widths t ∈ {2,3,4,8,12,16}.
Veil's dominant identity-binding hashes (Poseidon(4), Poseidon(5)) need t=5/t=6, which don't
exist as a published construction, so they pay for a padded t=8 permutation instead of a tight
one — and the extra linear-layer matrix multiplications cost more than the S-box rounds Poseidon2
saves.

The one real, isolated win: the depth-20 Merkle accumulator hasher alone, swapped to Poseidon2's
purpose-built t=2 compression mode, is a genuine -6.3% for that component. Too small to
offset the rest of any circuit that uses it, but a clean, low-risk follow-up on its own (parked).

Verdict: REJECT (full swap) / PARK (Merkle-only partial swap). BASELINE.md is unchanged —
nothing deployed changed; this experiment's circuits live under circuits/experiments/poseidon2/
and are not wired into contracts/, scripts/, or frontend/.

Full writeup: docs/research/2026-09-09-poseidon2-merkle-arity.md

What's in this PR

  • circuits/experiments/poseidon2/ — standalone measurement fixtures: Poseidon2Hash(n) and
    MerkleProof2(depth) templates (built on @taceo/circom-lib's Poseidon2), full mirror copies of
    the three production circuits with the swap applied, isolated per-arity micro-benchmark circuits,
    a permutation-level cross-check between the compiled circuit and its JS witness-builder
    counterpart, and a soundness/negative test for MerkleProof2 (binding + the boolean
    path-index constraint, both required — see report's Threat/privacy model section).
  • scripts/bench/witnesses-poseidon2.mjs + scripts/bench/prove-latency-poseidon2.mjs — reusable
    bench scripts, mirroring the existing witnesses.mjs/prove-latency.mjs pattern.
  • docs/research/2026-09-09-poseidon2-merkle-arity.md — hypothesis, threat/privacy model, approach
    (including what was rejected and why), full raw command output, verdict, where this generalizes,
    and next queue.
  • docs/research/LEDGER.md / EXPERIMENTS.md — appended row, re-ranked queue (Merkle-only PARK
    confirmation added near the top; on-chain gas retry notes updated).
  • circuits/package.json, scripts/package.json — added @taceo/circom-lib and
    @taceo/poseidon2 as dependencies.

Proving-time (wall-clock) measurement was attempted but did not complete within this session's
budget (snarkjs groth16 setup for the largest experiment circuit was still running after 14+
minutes of continuous CPU) — recorded honestly in the report rather than guessed at. The verdict
doesn't depend on it: a circuit with 17–93% more R1CS constraints, same curve and proof system, is
not a plausible proving-time win regardless.

Also reattempted queue item #1 (on-chain gas per entry point): confirmed the public-fullnode
JSON-RPC block is a network-egress-proxy policy (403), not a one-off tool-approval denial as
first read on 2026-07-22. Started a from-source sui CLI build (git access to GitHub works over
the git protocol even though plain HTTPS is blocked) but stopped it deliberately partway through
when it began starving this experiment's own CPU budget. Still blocked; re-ranked in
EXPERIMENTS.md with a note that it needs a dedicated session, not a warm-up to another experiment.

Test plan

  • node --experimental-vm-modules test/transfer.test.mjs — 43/43 pass (unchanged, production circuit untouched)
  • node --experimental-vm-modules test/compliance.test.mjs — 30/30 pass
  • node --experimental-vm-modules test/withdraw.test.mjs — 35/35 pass
  • cd scripts && bun run src/test-converter.ts — 109/109 pass
  • cd scripts && bun run src/test-compliance-utils.ts — 67/67 pass
  • cd frontend && bunx vitest run — 19/19 pass
  • node --experimental-vm-modules circuits/experiments/poseidon2/test/merkle_proof2.test.mjs — 3/3 pass (new negative/soundness test)
  • cd contracts && sui move testNOT RUN, same pre-existing blocker as the 2026-07-22 baseline (no sui CLI reachable; contracts/ untouched by this PR)

No test was loosened, skipped, or given new tolerance. No production circuit, Move module, or
frontend proving code was modified — this PR only adds new, isolated experiment/benchmark code and
documentation.

🤖 Generated with Claude Code

https://claude.ai/code/session_014TjqAWDAUGr7itsmPr5Xzp


Generated by Claude Code

… loss

Measured (real circom --r1cs compiles, cross-validated three independent ways: whole-circuit
deltas sum exactly to per-instance micro-benchmark deltas) that swapping Veil's circomlib Poseidon
calls for Poseidon2 (Horizen Labs' reference construction via @taceo/circom-lib) makes all three
circuits worse, not better: +17.4% transfer.circom, +5.2% compliance.circom, +93.0% withdraw.circom.
Root cause: Poseidon2's optimized linear layer only supports state widths t in {2,3,4,8,12,16},
and Veil's dominant identity-binding hashes (Poseidon(4), Poseidon(5)) fall in the gap, forcing a
padded t=8 permutation instead of a tight one.

The one real win is isolated: the depth-20 Merkle accumulator hasher alone, swapped to Poseidon2's
t=2 compression mode (the pattern @taceo/circom-lib itself uses for Merkle trees), is a genuine
-6.3% for that component — parked as a follow-up (needs its own compiled confirmation).

Verdict: REJECT (full swap) / PARK (Merkle-only partial swap). BASELINE.md unchanged — nothing
deployed changed. Includes a soundness/negative test for the new MerkleProof2 template (binding +
the boolean path-index constraint), a permutation-level cross-check between the circom circuit and
its JS witness-builder counterpart, and a reusable bench script under scripts/bench/.

Also reattempted queue item #1 (on-chain gas): confirmed the fullnode JSON-RPC block is a
network-proxy policy (403), not a one-off tool-approval denial, and started a from-source `sui`
CLI build that was later stopped deliberately to free CPU for this experiment's own zkey
generation — still blocked, re-ranked with that update.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TjqAWDAUGr7itsmPr5Xzp
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