Skip to content

Feat/subtree roots - #165

Merged
nol4lej merged 2 commits into
mainfrom
feat/subtree-roots
Oct 10, 2026
Merged

nol4lej merged 2 commits into
mainfrom
feat/subtree-roots

Conversation

@nol4lej

@nol4lej nol4lej commented Oct 10, 2026

Copy link
Copy Markdown
Member

Summary

Asking a node for a commitment's Merkle proof tells it which note is about to be spent.
This PR adds what a wallet needs to build the path itself, following Zcash's
GetSubtreeRoots model:

  • the lower 6 levels come from the 64 leaves of the note's block, which the wallet
    already saw while scanning;
  • the upper 14 levels come from the tree's level-6 subtree roots, which are the same
    download for every wallet.

Pallet (pallet-shielded-pool)

  • get_subtree_roots(tree_id, start, count) -> Option<SubtreeRoots>:
    • returns up to MAX_SUBTREE_ROOTS (4096, a quarter of a full tree's 2^14 blocks)
      level-SUBTREE_LEVEL (6) roots of one tree;
    • includes the root the tree anchors to (the final root once sealed, else the live
      root), all read from one state;
    • returns only blocks that hold leaves; past them every node is the zero hash, which
      the caller knows;
    • returns None for a tree that does not exist yet.
  • Every root but the last block of the active tree is final: a block's leaves never
    change once it is full.
  • A node missing from storage is rebuilt from the block's leaves. This only happens for a
    sealed tree pruned under an earlier, higher cut. With the current cut
    (SealedTreePrunedBelowLevel = 6) the level-6 nodes are kept, so the rebuild is not
    hit. Worst case: 64 leaf reads per root, at most 4096 roots per call.
  • New public types/constants: SubtreeRoots, SUBTREE_LEVEL, MAX_SUBTREE_ROOTS.

Runtime API v4

  • ShieldedPoolRuntimeApi::get_subtree_roots, marked #[api_version(4)]; the runtime
    implements v4.

RPC (fc-rpc-v2)

  • privacy_getSubtreeRoots(tree_id, start, count) returns tree_id, level,
    tree_leaves, sealed, root, start and roots (0x hex, little-endian field
    elements).
  • Runs behind the same ProofGate as the Merkle-proof RPCs: core-based slots and a
    bounded queue.
  • Refuses with an error against a runtime older than API v4. An unknown tree is an
    invalid-params error.

Compatibility

  • Additive: no storage change, no migration, no change to existing calls or RPCs.
  • Clients check for the method; @orbinum/protocol 0.8.0 already ships
    getSubtreeRoots / parseSubtreeRoots / buildLocalPath.
  • Versions: pallet-shielded-pool 0.24.0, pallet-shielded-pool-runtime-api 0.3.0.

Tests

  • runtime_api_impl::tests::subtree_roots:
    • windows and missing trees;
    • the partial block tracks the live root;
    • a pruned node of a sealed tree is rebuilt;
    • the served roots fold to the anchoring root;
    • a wallet-built path equals the node's path.
  • cargo test -p pallet-shielded-pool (412) and -p fc-rpc-v2 (44) pass; clippy with
    -D warnings and fmt are clean.

Live verification

Against a dev node built from this code:

  • Paths built by the client from privacy_getSubtreeRoots plus the scanned block leaves
    were identical to privacy_getMerkleProofByCommitment:
    • 159/159 on a sealed 2^20-leaf tree;
    • 109/109 on an active 1000-leaf tree.
  • @orbinum/protocol end-to-end against the RPC: 8/8.

@nol4lej
nol4lej merged commit eafee5f into main Oct 10, 2026
6 checks passed
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