research: Poseidon2 Merkle hasher — measured REJECT - #42
Open
alexandre-mrt wants to merge 1 commit into
Open
Conversation
Swap circomlib's Poseidon(2) for Poseidon2(t=3) (@taceo/circom-lib, cross-checked against @taceo/poseidon2's JS implementation) as the Merkle node hash in templates/merkle_proof.circom and withdraw.circom's recipHash — the only arity-2 Poseidon call sites, and the highest-multiplicity hash in the protocol (20 calls/proof via the depth-20 tree). Measured result: constraint count and proving time both regress across all three circuits (+9.3%/+9.9%/+2.1% R1CS constraints, +14-19% Node proving time, +6-11% browser proving time), despite non-linear (S-box) constraints dropping slightly as predicted. TACEO's linear-layer templates encode each MDS multiply through several named intermediate signals, and circom emits one R1CS linear constraint per signal — that cost outweighs the S-box savings. 109/109 circuit tests pass (real Groth16 proofs), including a new negative test proving a root computed with the old Poseidon(2) is rejected by the new circuit. Verdict: REJECT. Branch kept for the record; main/BASELINE.md/threat-model unchanged. Full writeup, soundness argument, and leakage analysis in docs/research/2026-08-24-poseidon2-merkle-hasher.md. Ledger and experiment queue updated accordingly. Also fixes scripts/bench/prove-latency.mjs hanging after its last proof (missing process.exit(0), same issue circuits/test/*.mjs already handles).
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-08-24. Queue item #2: does swapping circomlib's
Poseidon(2)forPoseidon2(t=3) as the Merkle-tree node hash (
templates/merkle_proof.circom— 20 calls/proof inboth
transfer.circomandcompliance.circom, the highest-multiplicity hash in the protocol)reduce constraint count and proving time?
Measured answer: no. Constraint count and proving time both regress across all three circuits:
transfer.circomcompliance.circomwithdraw.circomNon-linear (S-box) constraints do drop slightly (~1%), as the Poseidon2 literature predicts — but
@taceo/circom-lib's linear-layer templates encode each MDS multiply through several namedintermediate signals, and circom emits one R1CS linear constraint per signal. That cost dominates
the S-box savings. Full mechanism, raw command output, cross-implementation verification
(circom vs. JS, byte-identical for a fixed input), soundness argument, and leakage analysis in
docs/research/2026-08-24-poseidon2-merkle-hasher.md.Verdict: REJECT. Branch kept for the record —
main,BASELINE.md, anddocs/threat-model.mdare unchanged. Ledger and experiment queue updated; a re-rank note inEXPERIMENTS.mdexplains why on-chain gas (blocked a third time this session, now by an outrightnetwork-proxy policy denial) is demoted, and adds a new low-priority parked item (a hand-fused
linear layer, which could plausibly recover the win this attempt lost) for a future night.
What's in this PR
circuits/templates/poseidon2_hash2.circom— newPoseidon2Hash2template wrapping@taceo/circom-lib'sPoseidon2(t)in circomlib's exactPoseidon(2)sponge convention(capacity-first, digest =
state[0]), so it's a true drop-in.templates/merkle_proof.circom+withdraw.circom(recipHash) rewired to use it — the onlytwo arity-2 Poseidon call sites in the protocol. All other (domain-tagged, arity ≥3) hash sites
are untouched.
scripts/bench/witnesses.mjsand all threecircuits/test/*.test.mjsupdated so their JSreference computations match what the new circuits actually enforce.
transfer.test.mjs): a Merkle root computed with the oldPoseidon(2)is rejected by the new circuit — proof the swap is enforced by R1CS constraints,not a JS-side convention nobody checks.
scripts/bench/prove-latency.mjs(missingprocess.exit(0), samesnarkjs/ffjavascriptopen-handle issuecircuits/test/*.mjsalready handles) — found whilegetting clean benchmark output for this experiment.
docs/research/2026-08-24-poseidon2-merkle-hasher.md,LEDGER.md,EXPERIMENTS.mdupdated.Test plan
node --experimental-vm-modules test/{transfer,compliance,withdraw}.test.mjs— 109/109 pass,real Groth16
fullProve/verifyagainst freshly compiled artifacts (not hash-only mode)cd scripts && bun run src/test-converter.ts— 109/109 pass (unaffected)cd frontend && bun run test— 19/19 pass (vitest, unaffected)sui move test— not run;suiCLI still unavailable in this session (pre-existing,unrelated toolchain gap — no contract code touched by this PR)
node scripts/bench/prove-latency.mjs --runs 10andnode scripts/bench/browser-latency.mjs --runs 8— raw output in the writeup
Poseidon2(3)circuit witness vs.@taceo/poseidon2JS
bn254.t3.permutation, byte-identical for input[0,1,2]Generated by Claude Code