research: Poseidon2 vs Poseidon — arity gap makes the full swap a net loss - #57
Open
alexandre-mrt wants to merge 1 commit into
Open
research: Poseidon2 vs Poseidon — arity gap makes the full swap a net loss#57alexandre-mrt wants to merge 1 commit into
alexandre-mrt wants to merge 1 commit into
Conversation
… 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
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.
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:
transfer.circomcompliance.circomwithdraw.circomRoot 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)) needt=5/t=6, which don'texist as a published construction, so they pay for a padded
t=8permutation instead of a tightone — 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=2compression mode, is a genuine -6.3% for that component. Too small tooffset 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.mdis unchanged —nothing deployed changed; this experiment's circuits live under
circuits/experiments/poseidon2/and are not wired into
contracts/,scripts/, orfrontend/.Full writeup:
docs/research/2026-09-09-poseidon2-merkle-arity.mdWhat's in this PR
circuits/experiments/poseidon2/— standalone measurement fixtures:Poseidon2Hash(n)andMerkleProof2(depth)templates (built on@taceo/circom-lib's Poseidon2), full mirror copies ofthe 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 booleanpath-index constraint, both required — see report's Threat/privacy model section).
scripts/bench/witnesses-poseidon2.mjs+scripts/bench/prove-latency-poseidon2.mjs— reusablebench scripts, mirroring the existing
witnesses.mjs/prove-latency.mjspattern.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 PARKconfirmation added near the top; on-chain gas retry notes updated).
circuits/package.json,scripts/package.json— added@taceo/circom-liband@taceo/poseidon2as dependencies.Proving-time (wall-clock) measurement was attempted but did not complete within this session's
budget (
snarkjs groth16 setupfor 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 asfirst read on 2026-07-22. Started a from-source
suiCLI build (git access to GitHub works overthe 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.mdwith 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 passnode --experimental-vm-modules test/withdraw.test.mjs— 35/35 passcd scripts && bun run src/test-converter.ts— 109/109 passcd scripts && bun run src/test-compliance-utils.ts— 67/67 passcd frontend && bunx vitest run— 19/19 passnode --experimental-vm-modules circuits/experiments/poseidon2/test/merkle_proof2.test.mjs— 3/3 pass (new negative/soundness test)cd contracts && sui move test— NOT RUN, same pre-existing blocker as the 2026-07-22 baseline (nosuiCLI 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