Skip to content

research: merkle-zero-hash-pruning - #49

Open
alexandre-mrt wants to merge 3 commits into
mainfrom
claude/intelligent-cannon-y775h1
Open

research: merkle-zero-hash-pruning#49
alexandre-mrt wants to merge 3 commits into
mainfrom
claude/intelligent-cannon-y775h1

Conversation

@alexandre-mrt

Copy link
Copy Markdown
Owner

Summary

Nightly research loop, 2026-08-31. Full report: docs/research/2026-08-31-merkle-zero-hash-pruning.md.

  • Hypothesis: Veil's two off-chain depth-20 Merkle builders (scripts/src/compliance-utils.ts, frontend/src/lib/merkle-tree.ts) rebuild the entire 2^depth-wide tree on every call — O(2^depth) Poseidon calls regardless of real leaf count. Pruning the always-zero padding (fill from precomputed per-level zero-subtree hashes instead of re-hashing) should cut this to O(n + depth), bit-identical output.
  • What changed: both builders now do exactly that. Zero API changes for compliance-utils.ts (same signature, same fully-padded output shape every existing caller relies on); frontend/lib/merkle-tree.ts's private internals only, same public surface.
  • Numbers (depth 20, the deployed circuit depth): naive build = 101,358 ms, constant regardless of n. Pruned build = 43.8 ms at n=1 (today's actual real-world usage in both files) up to 108,763 ms at full occupancy (correctly no speedup left — no padding to skip). ~2,315x at the real usage pattern. Also ran depth 24 (~16.8M capacity) to show the technique removes the "off-chain rebuild is impractical at greater depth" objection for a future anonymity-set increase.
  • Correctness: pruned output verified bit-identical to a naive full-width reference by three independent checks — a dedicated benchmark's built-in gate, 34 new scripts unit tests (every layer, 15 values of n including boundary cases), and a real ~76s naive-reference cross-check in a new frontend vitest suite (merkle-tree.ts had zero test coverage before this).
  • Side findings: confirmed EXPERIMENTS.md item 12 (chained circuits npm test hang) is already fixed on main (PRs fix(circuits): look up compliance artifacts in build-compliance/ #16/fix(circuits): exit test runners explicitly after the last proof #17); reconfirmed item 1 (on-chain gas / sui CLI) is still blocked by network policy, now via a second independent method (JSON-RPC 403, crates.io content 403, git-protocol clones do work).
  • Verdict: KEEP. BASELINE.md, LEDGER.md, EXPERIMENTS.md, and docs/threat-model.md (RR5) updated accordingly.

No circuit, Move module, or proof format changed — roots and proofs are identical to before, so no soundness/leakage argument applies (see the report's Threat/privacy model section for the full reasoning on why this is an availability/UX fix, not a cryptographic one).

Test plan

  • cd scripts && bun run src/test-compliance-utils.ts — 101/101 pass (34 new)
  • cd scripts && bun run src/test-converter.ts — 109/109 pass
  • cd scripts && bun run src/fuzz-tests.ts — 6/6 properties, 500 cases each
  • cd frontend && bunx vitest run — 23/23 pass (4 new)
  • cd circuits && npm test — 65/65 pass (hash-only mode; no circuit touched, no compiled artifacts available in this sandbox)
  • cd contracts && sui move test — NOT RUN, sui CLI unavailable in this sandbox (pre-existing, unrelated blocker — see report)
  • bun run scripts/bench/merkle-scale.mjs — raw benchmark output in the report, correctness gate passes before any timing is reported

Co-authored by an autonomous nightly research routine (Claude Sonnet 5) — see the linked report for full methodology and raw command output.


Generated by Claude Code

claude added 2 commits August 31, 2026 07:41
Both off-chain Merkle tree builders (scripts/src/compliance-utils.ts,
frontend/src/lib/merkle-tree.ts) rebuilt the entire 2^depth-wide tree on
every call, hashing every zero-padding pair for real - O(2^depth) Poseidon
calls regardless of leaf count. seed-credential-tree.ts already warned this
takes ~60s for a single-leaf tree; the same cost sits inside every real
transfer proof via useProofGeneration.ts.

Both now only hash the real leaf prefix and fill the always-zero remainder
from precomputed per-level zero-subtree hashes - O(n + depth) instead,
verified bit-identical to the old output by three independent checks (a
dedicated benchmark, 34 new scripts unit tests, and a ~76s naive-reference
cross-check in the frontend suite). Measured: naive n=1 build = 101,358ms
(constant regardless of n); pruned = 43.8ms at n=1 up to 108,763ms at full
occupancy (correctly no speedup left when there's no padding to skip).

Also confirms EXPERIMENTS.md item 12 (chained circuits npm test hang) is
already fixed on main (PRs #16/#17), and reconfirms item 1 (on-chain gas)
is still blocked by network policy via a second, independent method.

Full report, depth-24 numbers, and threat/privacy framing:
docs/research/2026-08-31-merkle-zero-hash-pruning.md

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016m6rsxgsUNWCbZd4x9SibV
Both bun-using CI jobs pinned oven-sh/setup-bun@735343b,
labelled v2.0.1 - but that commit doesn't exist in oven-sh/setup-bun at all
(GitHub Actions: "unable to find version"). The real v2.0.1 tag points to
4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5; the broken hash appears to be a
near-miss for v2.0.2's real SHA (735343b667d3e6f658f44d0eca948eb6282f2b76),
off by a handful of trailing characters.

This has been breaking both the "Proof converter + compliance utils" and
"Frontend" CI jobs since the commit that introduced them (e37ecd6) - every
CI run on main since then has failed at the action-resolution step, before
any real test runs. Unrelated to this PR's diff; ported here so this PR's
own CI can go green rather than waiting on main to pick up the same fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016m6rsxgsUNWCbZd4x9SibV

Copy link
Copy Markdown
Owner Author

CI failed on the first push, but not because of this PR's diff: both oven-sh/setup-bun-using jobs ("Proof converter + compliance utils", "Frontend (vitest + biome + tsc)") failed at the action-resolution step with Unable to resolve action oven-sh/setup-bun@735343b667d3e6f658f44e0a462d63b48e3324cb, unable to find version.

That SHA doesn't exist in oven-sh/setup-bun — checking the real tag history, v2.0.1 is actually 4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5; the broken hash looks like a near-miss for v2.0.2's real SHA (735343b667d3e6f658f44d0eca948eb6282f2b76), off by a handful of trailing characters. It's been in .github/workflows/ci.yml since the commit that introduced these two jobs (e37ecd6) — every CI run on main since then has failed the same way, before any real test executes. Not caused by this PR.

Ported the fix here (4dc78ea) rather than waiting on main to pick it up, since it blocks this PR going green either way. Worth landing the same 2-line fix on main directly too, since it's currently breaking CI for every PR in the repo, not just this one.


Generated by Claude Code

…ree test

circomlibjs ships no type declarations. Every other direct import of it in
this package goes through src/lib/dynamicRequire.ts, which already carries
a @ts-expect-error for exactly this. The new test file imported it directly
instead, which bunx tsc --noEmit correctly flagged in CI (TS7016) - not
caught locally since I'd typechecked before adding this file and only
re-ran vitest after. Same suppression, same reasoning, matching the
existing convention.

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