Skip to content

fix(drive): rebalance shielded credit pool subtree keys by access frequency#3607

Merged
QuantumExplorer merged 2 commits intov3.1-devfrom
claude/admiring-booth-97300d
May 6, 2026
Merged

fix(drive): rebalance shielded credit pool subtree keys by access frequency#3607
QuantumExplorer merged 2 commits intov3.1-devfrom
claude/admiring-booth-97300d

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

The eight shielded credit pool subtrees were placed at byte positions 1, 2, 5, 6, 8, 'n', 'o', 'p' (110, 111, 112) — clustered around 0 and the lower-ASCII range. In GroveDB's AVL-balanced parent Merk tree this skewed the tree, putting the highest-traffic subtree (SHIELDED_NOTES_KEY) deep on one side instead of at the root.

What was done?

Rebalanced the eight subtree keys across [0, 255] so the most-queried subtrees sit closest to the parent-tree root. Sorted-position layout:

Byte Depth Constant Why this depth
32 2 SHIELDED_TOTAL_BALANCE_KEY low-frequency reads
64 1 SHIELDED_NULLIFIERS_KEY every spend (membership)
96 2 SHIELDED_ANCHORS_BY_HEIGHT_KEY per-anchor write + most-recent read
128 0 (root) SHIELDED_NOTES_KEY every wallet sync + every state transition
160 2 SHIELDED_RECENT_NULLIFIERS_KEY per-block writes
192 1 SHIELDED_ANCHORS_IN_POOL_KEY every spend (membership)
224 2 SHIELDED_COMPACTED_NULLIFIERS_KEY periodic compaction
240 3 SHIELDED_NULLIFIERS_EXPIRATION_TIME_KEY periodic expiry

`'n'`/`'o'`/`'p'` lose their alphabetic mnemonic — those constants now use numeric bytes to fit the layout. All accesses go through the constants, so the only behavioral change is the on-chain storage layout.

Doc comments referencing the old path bytes ([..., "s", [1]], [..., "s", [6]], etc.) were updated to match.

How Has This Been Tested?

  • `cargo check -p drive -p drive-abci --tests` clean
  • `cargo test -p drive shielded::` — 157 passed, 0 failed
  • `cargo test -p drive-abci --lib shielded` — passing (Plan A version verified; re-running on this commit)
  • `cargo fmt --all`

Breaking Changes

State-structure change. Pre-launch / dev branch (v3.1-dev), so this lands as a direct edit to `create_initial_state_structure_v3` and `transition_to_version_12` (constants only — no byte literals at the call sites). No on-chain migration needed since this is pre-release. Following the precedent of #3605 which retired key 7 the same way.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

🤖 Generated with Claude Code

…quency

Spread the eight shielded-pool subtrees across [0, 255] so GroveDB's
AVL-balanced parent tree puts the highest-traffic subtree (notes) at the
root, with the next-most-queried subtrees one hop below it, and the cold
ones at the leaves.

Layout (depth in the parent Merk tree):

    [128] NOTES                                   ← root
    ├── [64] NULLIFIERS                           ← depth 1 (every spend)
    │     ├── [32]  TOTAL_BALANCE                 ← depth 2
    │     └── [96]  ANCHORS_BY_HEIGHT             ← depth 2
    └── [192] ANCHORS_IN_POOL                     ← depth 1 (every spend)
          ├── [160] RECENT_NULLIFIERS             ← depth 2
          └── [224] COMPACTED_NULLIFIERS          ← depth 2
                └── [240] NULLIFIERS_EXPIRATION   ← depth 3

The 'n'/'o'/'p' alphabetic mnemonic for recent/compacted/expiration is
dropped — those constants now use numeric byte values to fit the
balanced layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Warning

Rate limit exceeded

@QuantumExplorer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 56 minutes and 40 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ce2f17a-c5f2-4587-8191-4e22a4a053f1

📥 Commits

Reviewing files that changed from the base of the PR and between 7b23bc7 and a0bb99c.

📒 Files selected for processing (16)
  • packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs
  • packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs
  • packages/rs-drive/src/drive/initialization/v3/mod.rs
  • packages/rs-drive/src/drive/shielded/estimated_costs.rs
  • packages/rs-drive/src/drive/shielded/has_anchor/mod.rs
  • packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs
  • packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs
  • packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs
  • packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs
  • packages/rs-drive/src/drive/shielded/nullifiers/queries.rs
  • packages/rs-drive/src/drive/shielded/paths.rs
  • packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs
  • packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs
  • packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs
  • packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/admiring-booth-97300d

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added this to the v3.1.0 milestone May 6, 2026
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 6, 2026

✅ Review complete (commit a0bb99c)

GroveDB's AVL rebalancing is order-sensitive: the depth a key ends up
at depends on the order of inserts under the same parent, not just the
final set of keys. Reorder both the genesis init (`v3`) and the v12
upgrade transition to insert level 0 first, then both depth-1 children,
then the depth-2 children, then the depth-3 leaf — so the balanced
shape described in `paths.rs` is what's actually built.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self Reviewed

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.29%. Comparing base (7b23bc7) to head (a0bb99c).
⚠️ Report is 1 commits behind head on v3.1-dev.

Additional details and impacted files
@@            Coverage Diff            @@
##           v3.1-dev    #3607   +/-   ##
=========================================
  Coverage     88.29%   88.29%           
=========================================
  Files          2479     2479           
  Lines        301660   301660           
=========================================
  Hits         266350   266350           
  Misses        35310    35310           
Components Coverage Δ
dpp 87.95% <ø> (ø)
drive 87.37% <100.00%> (ø)
drive-abci 90.25% <100.00%> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 55.66% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@QuantumExplorer QuantumExplorer merged commit ec0a605 into v3.1-dev May 6, 2026
19 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/admiring-booth-97300d branch May 6, 2026 20:17
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Two valid blocking issues. (1) The genesis path's BFS insert ordering is a no-op for tree shape: GroveDB's batch path sorts ops by key into a BTreeMap and Merk's build() recursively picks mid_index = batch.len()/2 of the sorted keys as the root, so with the chosen 8 keys the parent root is byte 160 (RECENT_NULLIFIERS), not 128 (NOTES) as the docs claim. (2) transition_to_version_12() still pre-creates only 4 of the 8 shielded subtrees that genesis creates, leaving upgraded nodes without 4 paths the runtime writes to and producing a different parent-tree shape than genesis even if the missing inserts were added (sequential AVL inserts vs sorted-batch build).

Reviewed commit: a0bb99c

🔴 2 blocking | 🟡 2 suggestion(s)

3 additional findings

🔴 blocking: transition_to_version_12 still creates only 4 of 8 shielded subtrees — diverges from genesis

packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs (lines 611-690)

transition_to_version_12() inserts only SHIELDED_NOTES_KEY (128), SHIELDED_NULLIFIERS_KEY (64), SHIELDED_ANCHORS_IN_POOL_KEY (192), and SHIELDED_TOTAL_BALANCE_KEY (32). The genesis path (initial_state_structure_shielded_pool_operations in packages/rs-drive/src/drive/initialization/v3/mod.rs:79-156) inserts four additional subtrees that the runtime writes to: SHIELDED_ANCHORS_BY_HEIGHT_KEY (96), SHIELDED_RECENT_NULLIFIERS_KEY_U8 (160), SHIELDED_COMPACTED_NULLIFIERS_KEY_U8 (224), and SHIELDED_NULLIFIERS_EXPIRATION_TIME_KEY_U8 (240). After a v11→v12 upgrade these paths are absent, but record_shielded_pool_anchor_if_changed_v0 (writes anchor-by-height, see packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs:96-105), store_nullifiers_for_block_v0 (packages/rs-drive/src/drive/shielded/nullifiers/store_nullifiers/v0/mod.rs:73-80), and the compaction/expiration writers (packages/rs-drive/src/drive/shielded/nullifiers/compact_nullifiers/v0/mod.rs:120-152) all assume those subtrees exist. Compounding this: the upgrade path uses sequential grove_insert_if_not_exists (AVL-style sequential inserts), while genesis applies a single batch (Merk build() from sorted keys). Even if all 8 inserts were added in BFS order to the upgrade path, sequential vs sorted-batch construction produces different tree shapes and root hashes, so genesis-initialized and upgrade-initialized nodes will desync. Add the four missing inserts AND align the construction strategy across both paths, then add a regression test that builds the parent tree both ways and asserts equal root hashes.

🔴 blocking: BFS insert order does not place NOTES at the parent root — genesis batch builds from sorted keys via midpoint split

packages/rs-drive/src/drive/initialization/v3/mod.rs (lines 70-156)

The doc comment claims this BFS ordering is what places SHIELDED_NOTES_KEY at the root via AVL rebalancing, but the genesis batch path makes insertion order irrelevant to the resulting tree shape. Drive::grove_apply_batch routes through BatchStructure::continue_from_ops (grovedb/src/batch/batch_structure.rs:104-198), which collects ops per path into a BTreeMap<KeyInfo, GroveOp> (sorted by key). When the per-path ops are forwarded to merk.apply_unchecked/Walker::apply_to (merk/src/tree/ops.rs:148-229), the parent Merk for [AddressBalances, "s"] is None (the SumTree is being created in the same batch), so apply_to falls through to Self::build (merk/src/tree/ops.rs:235-263). build recursively splits the sorted batch at mid_index = batch.len() / 2. With the 8 sorted child keys [32, 64, 96, 128, 160, 192, 224, 240], mid_index = 4, so the root is batch[4] = 160 (SHIELDED_RECENT_NULLIFIERS_KEY_U8) — not 128 (SHIELDED_NOTES_KEY). Recursive splits then place 96 and 224 at depth 1, 32/128/192/240 at depth 2, and 64 at depth 3. The BFS reorder is a no-op for genesis, and the access-frequency rationale (NOTES at root, spend-path keys at depth 1) does not hold. To genuinely pin NOTES at depth 0 you must either (a) pick a key value that lands at batch.len()/2 after sorting (with 8 keys including NOTES, that's the 5th-smallest position), or (b) avoid the batch path so insertion order matters (sequential grove_insert_if_not_exists).

🟡 suggestion: No test asserts the shielded-pool parent tree shape (root key / per-key depths)

packages/rs-drive/src/drive/shielded/paths.rs (lines 217-280)

The PR's central premise — that the chosen key bytes plus BFS insert order yield a specific Merk shape — is not verified anywhere. The existing tests cover read/write behavior, which works regardless of parent-tree layout. Add a test that initializes a fresh platform via the genesis path, opens the Merk at [AddressBalances, "s"], and asserts (a) the root key, (b) the depth/parent of each of the 8 child keys, and (c) the root hash. Repeat for the v11→v12 upgrade path and assert byte-identical root hashes. Without such a test, future changes to GroveDB's batch-build heuristics or to the key set would silently undo the intended layout.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs`:
- [BLOCKING] lines 611-690: transition_to_version_12 still creates only 4 of 8 shielded subtrees — diverges from genesis
  `transition_to_version_12()` inserts only `SHIELDED_NOTES_KEY (128)`, `SHIELDED_NULLIFIERS_KEY (64)`, `SHIELDED_ANCHORS_IN_POOL_KEY (192)`, and `SHIELDED_TOTAL_BALANCE_KEY (32)`. The genesis path (`initial_state_structure_shielded_pool_operations` in `packages/rs-drive/src/drive/initialization/v3/mod.rs:79-156`) inserts four additional subtrees that the runtime writes to: `SHIELDED_ANCHORS_BY_HEIGHT_KEY (96)`, `SHIELDED_RECENT_NULLIFIERS_KEY_U8 (160)`, `SHIELDED_COMPACTED_NULLIFIERS_KEY_U8 (224)`, and `SHIELDED_NULLIFIERS_EXPIRATION_TIME_KEY_U8 (240)`. After a v11→v12 upgrade these paths are absent, but `record_shielded_pool_anchor_if_changed_v0` (writes anchor-by-height, see `packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs:96-105`), `store_nullifiers_for_block_v0` (`packages/rs-drive/src/drive/shielded/nullifiers/store_nullifiers/v0/mod.rs:73-80`), and the compaction/expiration writers (`packages/rs-drive/src/drive/shielded/nullifiers/compact_nullifiers/v0/mod.rs:120-152`) all assume those subtrees exist. Compounding this: the upgrade path uses sequential `grove_insert_if_not_exists` (AVL-style sequential inserts), while genesis applies a single batch (Merk `build()` from sorted keys). Even if all 8 inserts were added in BFS order to the upgrade path, sequential vs sorted-batch construction produces different tree shapes and root hashes, so genesis-initialized and upgrade-initialized nodes will desync. Add the four missing inserts AND align the construction strategy across both paths, then add a regression test that builds the parent tree both ways and asserts equal root hashes.

In `packages/rs-drive/src/drive/initialization/v3/mod.rs`:
- [BLOCKING] lines 70-156: BFS insert order does not place NOTES at the parent root — genesis batch builds from sorted keys via midpoint split
  The doc comment claims this BFS ordering is what places `SHIELDED_NOTES_KEY` at the root via AVL rebalancing, but the genesis batch path makes insertion order irrelevant to the resulting tree shape. `Drive::grove_apply_batch` routes through `BatchStructure::continue_from_ops` (`grovedb/src/batch/batch_structure.rs:104-198`), which collects ops per path into a `BTreeMap<KeyInfo, GroveOp>` (sorted by key). When the per-path ops are forwarded to `merk.apply_unchecked`/`Walker::apply_to` (`merk/src/tree/ops.rs:148-229`), the parent Merk for `[AddressBalances, "s"]` is `None` (the SumTree is being created in the same batch), so `apply_to` falls through to `Self::build` (`merk/src/tree/ops.rs:235-263`). `build` recursively splits the sorted batch at `mid_index = batch.len() / 2`. With the 8 sorted child keys `[32, 64, 96, 128, 160, 192, 224, 240]`, `mid_index = 4`, so the root is `batch[4] = 160` (`SHIELDED_RECENT_NULLIFIERS_KEY_U8`) — not 128 (`SHIELDED_NOTES_KEY`). Recursive splits then place 96 and 224 at depth 1, 32/128/192/240 at depth 2, and 64 at depth 3. The BFS reorder is a no-op for genesis, and the access-frequency rationale (NOTES at root, spend-path keys at depth 1) does not hold. To genuinely pin NOTES at depth 0 you must either (a) pick a key value that lands at `batch.len()/2` after sorting (with 8 keys including NOTES, that's the 5th-smallest position), or (b) avoid the batch path so insertion order matters (sequential `grove_insert_if_not_exists`).

In `packages/rs-drive/src/drive/shielded/paths.rs`:
- [SUGGESTION] lines 10-28: Parent-tree layout diagram does not match the tree genesis actually builds
  The ASCII diagram shows `[128] NOTES` at the root with `[64] NULLIFIERS` and `[192] ANCHORS_IN_POOL` at depth 1, but the genesis batch produces a tree with `[160] RECENT_NULLIFIERS` at root, `[96]` and `[224]` at depth 1, etc. (see the blocking finding on `initialization/v3/mod.rs`). Either update the diagram to reflect the actual midpoint-split shape, or update the diagram together with a code change that genuinely produces it. Leaving this stale will mislead future readers into making access-pattern decisions based on a layout that does not exist.
- [SUGGESTION] lines 217-280: No test asserts the shielded-pool parent tree shape (root key / per-key depths)
  The PR's central premise — that the chosen key bytes plus BFS insert order yield a specific Merk shape — is not verified anywhere. The existing tests cover read/write behavior, which works regardless of parent-tree layout. Add a test that initializes a fresh platform via the genesis path, opens the Merk at `[AddressBalances, "s"]`, and asserts (a) the root key, (b) the depth/parent of each of the 8 child keys, and (c) the root hash. Repeat for the v11→v12 upgrade path and assert byte-identical root hashes. Without such a test, future changes to GroveDB's batch-build heuristics or to the key set would silently undo the intended layout.

Comment on lines +10 to 28
// The eight subtree keys of the shielded credit pool are placed at evenly-spaced
// byte positions across [0, 255] so that GroveDB's AVL-balanced parent tree
// puts the highest-traffic subtree (`SHIELDED_NOTES_KEY`) at the root, with the
// next-most-queried subtrees one hop below it, and the cold ones at the leaves:
//
// [128] NOTES ← root, every wallet sync
// / \
// [64] NULLIFIERS [192] ANCHORS_IN_POOL
// / \ / \
// [32] TOTAL [96] BY_HEIGHT [160] RECENT [224] COMPACTED
// \
// [240] EXPIRATION
//
// Within a depth tier (children of a given internal node), placement is by
// access frequency: the spend-path subtrees (`NULLIFIERS`, `ANCHORS_IN_POOL`)
// are at depth 1; periodic-write subtrees (`COMPACTED_NULLIFIERS`,
// `EXPIRATION_TIME`) sit at the leaves. Key 7 is the historical
// `SHIELDED_MOST_RECENT_ANCHOR_KEY` slot — see retired-key note below.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Parent-tree layout diagram does not match the tree genesis actually builds

The ASCII diagram shows [128] NOTES at the root with [64] NULLIFIERS and [192] ANCHORS_IN_POOL at depth 1, but the genesis batch produces a tree with [160] RECENT_NULLIFIERS at root, [96] and [224] at depth 1, etc. (see the blocking finding on initialization/v3/mod.rs). Either update the diagram to reflect the actual midpoint-split shape, or update the diagram together with a code change that genuinely produces it. Leaving this stale will mislead future readers into making access-pattern decisions based on a layout that does not exist.

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive/src/drive/shielded/paths.rs`:
- [SUGGESTION] lines 10-28: Parent-tree layout diagram does not match the tree genesis actually builds
  The ASCII diagram shows `[128] NOTES` at the root with `[64] NULLIFIERS` and `[192] ANCHORS_IN_POOL` at depth 1, but the genesis batch produces a tree with `[160] RECENT_NULLIFIERS` at root, `[96]` and `[224]` at depth 1, etc. (see the blocking finding on `initialization/v3/mod.rs`). Either update the diagram to reflect the actual midpoint-split shape, or update the diagram together with a code change that genuinely produces it. Leaving this stale will mislead future readers into making access-pattern decisions based on a layout that does not exist.

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