Skip to content

perf(shielded-pool): replace O(n) root recomputation with incremental frontier#78

Merged
nol4lej merged 1 commit into
mainfrom
perf/shielded-pool-incremental-merkle-frontier
Apr 29, 2026
Merged

perf(shielded-pool): replace O(n) root recomputation with incremental frontier#78
nol4lej merged 1 commit into
mainfrom
perf/shielded-pool-incremental-merkle-frontier

Conversation

@nol4lej
Copy link
Copy Markdown
Member

@nol4lej nol4lej commented Apr 29, 2026

insert_leaf formerly called get_all_leaves() and recomputed the full Poseidon Merkle root from scratch on every insert (O(n) reads, O(n log n) hashes). At 10k commitments this meant ~10k storage reads per shield or private_transfer extrinsic.

Replace with an incremental frontier algorithm: persist a 20-slot array (MerkleTreeFrontier storage item) that stores the last left-sibling at each tree level. Each insert now takes exactly 20 hashes and 1 storage read/write, regardless of tree size. Complexity is O(depth) = O(20) — constant at any scale.

Changes:

  • lib.rs: add MerkleTreeFrontier StorageValue<_, [[u8;32]; 20]>
  • storage.rs: add get_frontier / set_frontier to MerkleRepository
  • merkle.rs: rewrite MerkleTreeService::insert_leaf to use frontier; remove compute_poseidon_merkle_root (now unused); fix old_root in MerkleRootUpdated event (was always [0u8;32], now carries real value)
  • genesis.rs: initialize MerkleTreeFrontier to all-zeros at genesis

Add 5 tests:

  • incremental_root_matches_batch_root_after_single_insert
  • incremental_root_matches_batch_root_after_multiple_inserts
  • incremental_proof_verifies_against_incremental_root
  • merkle_root_updated_event_carries_correct_old_root
  • frontier_survives_storage_round_trip_across_separate_calls

… frontier

insert_leaf formerly called get_all_leaves() and recomputed the full
Poseidon Merkle root from scratch on every insert (O(n) reads,
O(n log n) hashes). At 10k commitments this meant ~10k storage reads
per shield or private_transfer extrinsic.

Replace with an incremental frontier algorithm: persist a 20-slot array
(MerkleTreeFrontier storage item) that stores the last left-sibling at
each tree level. Each insert now takes exactly 20 hashes and 1 storage
read/write, regardless of tree size. Complexity is O(depth) = O(20) —
constant at any scale.

Changes:
- lib.rs: add MerkleTreeFrontier<T> StorageValue<_, [[u8;32]; 20]>
- storage.rs: add get_frontier / set_frontier to MerkleRepository
- merkle.rs: rewrite MerkleTreeService::insert_leaf to use frontier;
  remove compute_poseidon_merkle_root (now unused); fix old_root in
  MerkleRootUpdated event (was always [0u8;32], now carries real value)
- genesis.rs: initialize MerkleTreeFrontier to all-zeros at genesis

Add 5 tests:
- incremental_root_matches_batch_root_after_single_insert
- incremental_root_matches_batch_root_after_multiple_inserts
- incremental_proof_verifies_against_incremental_root
- merkle_root_updated_event_carries_correct_old_root
- frontier_survives_storage_round_trip_across_separate_calls
@nol4lej nol4lej merged commit eea740d into main Apr 29, 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