Skip to content

Feat/merkle proof scaling - #113

Open
nol4lej wants to merge 7 commits into
mainfrom
feat/merkle-proof-scaling
Open

Feat/merkle proof scaling#113
nol4lej wants to merge 7 commits into
mainfrom
feat/merkle-proof-scaling

Conversation

@nol4lej

@nol4lej nol4lej commented Jul 30, 2026

Copy link
Copy Markdown
Member

Why

Serving a Merkle proof was O(n): get_merkle_path loaded every leaf from storage and rebuilt all 20 levels per request (~90k reads + ~90k Poseidon hashes at current testnet size), and the RPC hard-rejected trees above MAX_RPC_LEAVES = 100_000 — the spend path (transfer/unshield) would have gone down at ~10% of tree capacity. Implements Options A + B of MERKLE_PROOF_SCALING_PLAN.md.

What

Pallet (pallet-shielded-pool 0.10.1 → 0.11.0, STORAGE_VERSION 0 → 1):

  • New MerkleNodes: StorageNMap<(tree_id, level, index), Hash> — the insert_leaf frontier walk already computed every node on the insertion path and discarded them; they are now persisted (levels 1..=19; level 0 is MerkleLeaves, level 20 is PoseidonRoot).
  • get_merkle_path rewritten to 20 point reads, zero hashing. Missing siblings resolve to the canonical zero hash. get_all_leaves removed.
  • MigrateToV1: one-shot, idempotent backfill of MerkleNodes from existing leaves, wired into the runtime Executive. try-runtime pre/post_upgrade hooks verify the backfilled top nodes derive PoseidonRoot.
  • The key is (tree_id, level, index) — forest-ready per MULTI_TREE_FOREST_PLAN.md, tree_id = 0 on the current single tree, so the future multi-tree migration needs no storage remapping.

RPC (fc-rpc-v2):

  • privacy_getMerkleProof / privacy_getMerkleProofByCommitment now route through the runtime API: O(1) index lookup via CommitmentToLeafIndex (kills the linear leaf scan) and O(depth) path reads. Root and path resolve at the same block hash — atomicity preserved.
  • MAX_RPC_LEAVES cap and its error removed. Response shape unchanged — no client (ts-sdk / app / explorer) changes required.

Runtime:

  • spec_version 3 → 4 (consensus-affecting: new storage writes + migration). transaction_version unchanged — no extrinsic signature changes.
  • Implements the TryRuntime API (was missing); build.rs skips the Poseidon host-function feature on try-runtime builds so the stock try-runtime CLI can execute the wasm.
  • New template/runtime/RUNTIME_VERSIONS.md logging RuntimeVersion history.

Testing

  • Pallet suite: 278 tests green, including new property test (stored-node path === recomputed reference path for every leaf), boundary tests (first/last leaf, single-leaf tree), top-nodes-derive-root, and migration tests (backfill correctness, idempotency, empty tree).
  • try-runtime rehearsal against a live testnet snapshot (~block 202k, ~90k leaves): migration idempotent, full state decodes, all pallet try-state checks pass, post_upgrade root check passes. Measured weight: ~3s ref_time with native host functions — the upgrade block runs overweight once (acceptable on a 6s-slot solochain; move to MBM if deployed above ~150k leaves).
  • Weights re-benchmarked on reference hardware (Hetzner 16 vCPU/32 GB, EPYC-Genoa, --steps 50 --repeat 20): leaf-inserting extrinsics now charge the node writes (shield 13 → 32 writes; batch inserts dedupe shared ancestors). Known pre-existing gap: the unshield bench exercises the no-change-note path.

Deploy notes

  • Release order: runtime upgrade + node binary together (the RPC changed).
  • Old SDK/app remain fully compatible — server-side change only.

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.

1 participant