From 15f41c80af01656eeffa33a3ce0b78d831c54fc5 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 7 May 2026 02:15:18 +0700 Subject: [PATCH 1/2] fix(drive): rebalance shielded credit pool subtree keys by access frequency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../v0/mod.rs | 8 +- .../state_transitions/test_helpers.rs | 2 +- .../shielded/most_recent_anchor/v0/mod.rs | 2 +- .../src/drive/shielded/estimated_costs.rs | 8 +- .../src/drive/shielded/has_anchor/mod.rs | 2 +- .../src/drive/shielded/has_anchor/v0/mod.rs | 2 +- .../src/drive/shielded/has_nullifier/mod.rs | 2 +- .../drive/shielded/has_nullifier/v0/mod.rs | 2 +- .../src/drive/shielded/notes_count/v0/mod.rs | 2 +- .../src/drive/shielded/nullifiers/queries.rs | 45 ++++++----- packages/rs-drive/src/drive/shielded/paths.rs | 76 +++++++++++++------ .../drive/shielded/prune_anchors/v0/mod.rs | 2 +- .../shielded/read_total_balance/v0/mod.rs | 2 +- .../record_anchor_if_changed/v0/mod.rs | 10 +-- .../v0/mod.rs | 2 +- 15 files changed, 104 insertions(+), 63 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index d4a45a70472..7cf684eda45 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -626,7 +626,7 @@ impl Platform { )?; // Notes tree (CommitmentTree = CountTree items + Sinsemilla Frontier): - // [AddressBalances, "s"] / [1] + // [AddressBalances, "s"] / [128] let shielded_pool_path = shielded_credit_pool_path(); self.drive.grove_insert_if_not_exists( (&shielded_pool_path).into(), @@ -637,7 +637,7 @@ impl Platform { &platform_version.drive, )?; - // Nullifiers tree (ProvableCountTree): [AddressBalances, "s"] / [2] + // Nullifiers tree (ProvableCountTree): [AddressBalances, "s"] / [64] self.drive.grove_insert_if_not_exists( (&shielded_pool_path).into(), &[SHIELDED_NULLIFIERS_KEY], @@ -647,7 +647,7 @@ impl Platform { &platform_version.drive, )?; - // Total balance SumItem(0): [AddressBalances, "s"] / [5] + // Total balance SumItem(0): [AddressBalances, "s"] / [32] self.drive.grove_insert_if_not_exists( (&shielded_pool_path).into(), &[SHIELDED_TOTAL_BALANCE_KEY], @@ -657,7 +657,7 @@ impl Platform { &platform_version.drive, )?; - // Anchors tree (NormalTree) inside pool: [AddressBalances, "s"] / [6] + // Anchors tree (NormalTree) inside pool: [AddressBalances, "s"] / [192] // Stores block_height_be → anchor_bytes self.drive.grove_insert_if_not_exists( (&shielded_pool_path).into(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs index ac89889853f..1b9aae61479 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs @@ -462,7 +462,7 @@ pub fn process_transition( } /// Insert a fake anchor into the shielded anchors tree via GroveDB. -/// Anchors are stored as anchor_bytes → block_height_be in [AddressBalances, "s", [6]]. +/// Anchors are stored as anchor_bytes → block_height_be in [AddressBalances, "s", [192]]. pub fn insert_anchor_into_state(platform: &TempPlatform, anchor: &[u8; 32]) { let platform_version = PlatformVersion::latest(); let grove_version = &platform_version.drive.grove_version; diff --git a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs index 29b7ef80c74..b32842aa08f 100644 --- a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs @@ -18,7 +18,7 @@ use drive::util::grove_operations::GroveDBToUse; impl Platform { /// Answer `getMostRecentShieldedAnchor` by reading the latest /// entry from the anchors-by-height index - /// (`[..., "s", [8]]`) — a `limit 1` reverse query. The anchor + /// (`[..., "s", [96]]`) — a `limit 1` reverse query. The anchor /// is the value at the highest block-height key. /// /// Returns `[0; 32]` (mapped to `None` by the response decoder diff --git a/packages/rs-drive/src/drive/shielded/estimated_costs.rs b/packages/rs-drive/src/drive/shielded/estimated_costs.rs index 581c87f4abe..70266760595 100644 --- a/packages/rs-drive/src/drive/shielded/estimated_costs.rs +++ b/packages/rs-drive/src/drive/shielded/estimated_costs.rs @@ -105,7 +105,7 @@ impl Drive { }, ); - // Notes tree: [AddressBalances, "s", 1] + // Notes tree: [AddressBalances, "s", 128] // CommitmentTree - stores notes (cmx||encrypted_note items + Sinsemilla frontier) estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_path(shielded_credit_pool_notes_path()), @@ -116,7 +116,7 @@ impl Drive { }, ); - // Nullifiers tree: [AddressBalances, "s", 2] + // Nullifiers tree: [AddressBalances, "s", 64] // ProvableCountTree - stores spent nullifiers (32-byte key -> empty item) estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_path(shielded_credit_pool_nullifiers_path()), @@ -127,7 +127,7 @@ impl Drive { }, ); - // Anchors tree: [AddressBalances, "s", 6] + // Anchors tree: [AddressBalances, "s", 192] // NormalTree - stores anchor_bytes -> block_height_be estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_path(shielded_credit_pool_anchors_path()), @@ -138,7 +138,7 @@ impl Drive { }, ); - // Anchors-by-height tree: [AddressBalances, "s", 8] + // Anchors-by-height tree: [AddressBalances, "s", 96] // NormalTree - stores block_height_be -> anchor_bytes (reverse index for pruning) estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_path(shielded_credit_pool_anchors_by_height_path()), diff --git a/packages/rs-drive/src/drive/shielded/has_anchor/mod.rs b/packages/rs-drive/src/drive/shielded/has_anchor/mod.rs index b02ba7e5d20..60d70072a89 100644 --- a/packages/rs-drive/src/drive/shielded/has_anchor/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_anchor/mod.rs @@ -11,7 +11,7 @@ impl Drive { /// Checks whether a shielded pool anchor exists in the anchors tree. /// /// Anchors are stored as `anchor_bytes -> block_height_be` in - /// `[AddressBalances, "s", [6]]`. Uses O(1) key lookup. + /// `[AddressBalances, "s", [192]]`. Uses O(1) key lookup. /// /// # Parameters /// - `anchor`: The 32-byte anchor to look up diff --git a/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs b/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs index 2cfeb3fcfc1..d9da382780a 100644 --- a/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs @@ -10,7 +10,7 @@ impl Drive { /// Version 0 implementation of checking whether a shielded anchor exists. /// /// Performs an O(1) key lookup in the anchors tree at - /// `[AddressBalances, "s", [6]]`. + /// `[AddressBalances, "s", [192]]`. pub(in crate::drive) fn has_shielded_anchor_v0( &self, anchor: &[u8; 32], diff --git a/packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs b/packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs index ba950b2ce4d..b89085bc288 100644 --- a/packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs @@ -11,7 +11,7 @@ impl Drive { /// Checks whether a nullifier has already been spent in the shielded pool. /// /// Nullifiers are stored in the nullifiers tree at - /// `[AddressBalances, "s", [2]]`. Uses O(1) key lookup. + /// `[AddressBalances, "s", [64]]`. Uses O(1) key lookup. /// /// # Parameters /// - `nullifier`: The 32-byte nullifier to look up diff --git a/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs b/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs index 90fdb41e7e2..dc48e5c4602 100644 --- a/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs @@ -10,7 +10,7 @@ impl Drive { /// Version 0 implementation of checking whether a nullifier exists. /// /// Performs an O(1) key lookup in the nullifiers tree at - /// `[AddressBalances, "s", [2]]`. + /// `[AddressBalances, "s", [64]]`. pub(in crate::drive) fn has_nullifier_v0( &self, nullifier: &[u8; 32], diff --git a/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs b/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs index 63a56806501..bb2b6ffb4ab 100644 --- a/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs @@ -9,7 +9,7 @@ impl Drive { /// Version 0 implementation of counting shielded pool notes. /// /// Returns the total number of items in the CommitmentTree at - /// `[AddressBalances, "s", [1]]`. + /// `[AddressBalances, "s", [128]]`. pub(in crate::drive) fn shielded_pool_notes_count_v0( &self, transaction: TransactionArg, diff --git a/packages/rs-drive/src/drive/shielded/nullifiers/queries.rs b/packages/rs-drive/src/drive/shielded/nullifiers/queries.rs index 951d9214e2a..d7c955653e5 100644 --- a/packages/rs-drive/src/drive/shielded/nullifiers/queries.rs +++ b/packages/rs-drive/src/drive/shielded/nullifiers/queries.rs @@ -1,25 +1,34 @@ use crate::drive::shielded::paths::SHIELDED_CREDIT_POOL_KEY; use crate::drive::RootTree; -/// The subtree key for per-block nullifiers storage (CountSumTree) -pub const SHIELDED_RECENT_NULLIFIERS_KEY: &[u8; 1] = b"n"; +// Byte positions chosen to balance the parent shielded-pool Merk tree — +// see the layout diagram at the top of `crate::drive::shielded::paths`. -/// The subtree key for per-block nullifiers storage as u8 -pub const SHIELDED_RECENT_NULLIFIERS_KEY_U8: u8 = b'n'; +/// The subtree key for per-block nullifiers storage (CountSumTree). +/// +/// Depth 2 in the parent tree (right subtree of `SHIELDED_NOTES_KEY`). +pub const SHIELDED_RECENT_NULLIFIERS_KEY: &[u8; 1] = &[160]; -/// The subtree key for compacted nullifiers storage -pub const SHIELDED_COMPACTED_NULLIFIERS_KEY: &[u8; 1] = b"o"; +/// The subtree key for per-block nullifiers storage as u8. +pub const SHIELDED_RECENT_NULLIFIERS_KEY_U8: u8 = 160; -/// The subtree key for compacted nullifiers storage as u8 -pub const SHIELDED_COMPACTED_NULLIFIERS_KEY_U8: u8 = b'o'; +/// The subtree key for compacted nullifiers storage. +/// +/// Depth 2 in the parent tree. +pub const SHIELDED_COMPACTED_NULLIFIERS_KEY: &[u8; 1] = &[224]; -/// The subtree key for nullifiers expiration time storage -pub const SHIELDED_NULLIFIERS_EXPIRATION_TIME_KEY: &[u8; 1] = b"p"; +/// The subtree key for compacted nullifiers storage as u8. +pub const SHIELDED_COMPACTED_NULLIFIERS_KEY_U8: u8 = 224; -/// The subtree key for nullifiers expiration time storage as u8 -pub const SHIELDED_NULLIFIERS_EXPIRATION_TIME_KEY_U8: u8 = b'p'; +/// The subtree key for nullifiers expiration time storage. +/// +/// Deepest leaf in the parent tree — only touched by periodic expiry sweeps. +pub const SHIELDED_NULLIFIERS_EXPIRATION_TIME_KEY: &[u8; 1] = &[240]; -/// Path to per-block nullifiers: [AddressBalances, "s", "n"] +/// The subtree key for nullifiers expiration time storage as u8. +pub const SHIELDED_NULLIFIERS_EXPIRATION_TIME_KEY_U8: u8 = 240; + +/// Path to per-block nullifiers: [AddressBalances, "s", [160]] pub fn shielded_recent_nullifiers_path() -> [&'static [u8]; 3] { [ Into::<&[u8; 1]>::into(RootTree::AddressBalances), @@ -28,7 +37,7 @@ pub fn shielded_recent_nullifiers_path() -> [&'static [u8]; 3] { ] } -/// Path to per-block nullifiers as vec: [AddressBalances, "s", "n"] +/// Path to per-block nullifiers as vec: [AddressBalances, "s", [160]] pub fn shielded_recent_nullifiers_path_vec() -> Vec> { vec![ vec![RootTree::AddressBalances as u8], @@ -37,7 +46,7 @@ pub fn shielded_recent_nullifiers_path_vec() -> Vec> { ] } -/// Path to compacted nullifiers: [AddressBalances, "s", "o"] +/// Path to compacted nullifiers: [AddressBalances, "s", [224]] pub fn shielded_compacted_nullifiers_path() -> [&'static [u8]; 3] { [ Into::<&[u8; 1]>::into(RootTree::AddressBalances), @@ -46,7 +55,7 @@ pub fn shielded_compacted_nullifiers_path() -> [&'static [u8]; 3] { ] } -/// Path to compacted nullifiers as vec: [AddressBalances, "s", "o"] +/// Path to compacted nullifiers as vec: [AddressBalances, "s", [224]] pub fn shielded_compacted_nullifiers_path_vec() -> Vec> { vec![ vec![RootTree::AddressBalances as u8], @@ -55,7 +64,7 @@ pub fn shielded_compacted_nullifiers_path_vec() -> Vec> { ] } -/// Path to nullifiers expiration time: [AddressBalances, "s", "p"] +/// Path to nullifiers expiration time: [AddressBalances, "s", [240]] pub fn shielded_nullifiers_expiration_time_path() -> [&'static [u8]; 3] { [ Into::<&[u8; 1]>::into(RootTree::AddressBalances), @@ -64,7 +73,7 @@ pub fn shielded_nullifiers_expiration_time_path() -> [&'static [u8]; 3] { ] } -/// Path to nullifiers expiration time as vec: [AddressBalances, "s", "p"] +/// Path to nullifiers expiration time as vec: [AddressBalances, "s", [240]] pub fn shielded_nullifiers_expiration_time_path_vec() -> Vec> { vec![ vec![RootTree::AddressBalances as u8], diff --git a/packages/rs-drive/src/drive/shielded/paths.rs b/packages/rs-drive/src/drive/shielded/paths.rs index 2863ba92870..0f02c083c42 100644 --- a/packages/rs-drive/src/drive/shielded/paths.rs +++ b/packages/rs-drive/src/drive/shielded/paths.rs @@ -7,32 +7,64 @@ pub const SHIELDED_CREDIT_POOL_KEY: &[u8; 1] = b"s"; /// The subtree key for the shielded credit pool as a u8 pub const SHIELDED_CREDIT_POOL_KEY_U8: u8 = b's'; -/// Key for the notes tree (CommitmentTree) inside a shielded pool -pub const SHIELDED_NOTES_KEY: u8 = 1; +// 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. -/// Key for the nullifiers tree inside a shielded pool -pub const SHIELDED_NULLIFIERS_KEY: u8 = 2; - -/// Key for the total balance sum item inside a shielded pool -pub const SHIELDED_TOTAL_BALANCE_KEY: u8 = 5; +/// Key for the total balance sum item inside a shielded pool. +/// +/// Depth 2 in the parent tree (left subtree of `SHIELDED_NULLIFIERS_KEY`). +pub const SHIELDED_TOTAL_BALANCE_KEY: u8 = 32; -/// Key for the anchors tree inside a shielded pool (anchor_bytes → block_height_be). -/// Used by `validate_anchor_exists` for O(1) membership checks at spend time. -pub const SHIELDED_ANCHORS_IN_POOL_KEY: u8 = 6; +/// Key for the nullifiers tree inside a shielded pool. +/// +/// Depth 1 in the parent tree — checked on every spend for membership. +pub const SHIELDED_NULLIFIERS_KEY: u8 = 64; // Key 7 was previously `SHIELDED_MOST_RECENT_ANCHOR_KEY`, a redundant // `Item([u8;32])` slot mirroring the latest entry in // `SHIELDED_ANCHORS_BY_HEIGHT_KEY`. It was removed because the duplicated // state could (and did) drift out of sync with the anchors tree under prune, // leaving the validator's lookup table empty while the pool was still live. -// The most-recent anchor is now derived from `[8]` via a `limit 1` reverse -// query — see `Drive::query_most_recent_shielded_anchor`. +// The most-recent anchor is now derived from `SHIELDED_ANCHORS_BY_HEIGHT_KEY` +// (`[96]`) via a `limit 1` reverse query — see +// `Drive::query_most_recent_shielded_anchor`. /// Key for the anchors-by-height tree inside a shielded pool (block_height_be → anchor_bytes). /// Reverse index of `SHIELDED_ANCHORS_IN_POOL_KEY`, used both for pruning old /// anchors by height range and as the canonical source of the most-recent /// anchor (read via `limit 1` reverse query). -pub const SHIELDED_ANCHORS_BY_HEIGHT_KEY: u8 = 8; +/// +/// Depth 2 in the parent tree. +pub const SHIELDED_ANCHORS_BY_HEIGHT_KEY: u8 = 96; + +/// Key for the notes tree (CommitmentTree) inside a shielded pool. +/// +/// Placed at byte 128 — the median of the eight pool subtrees, putting it at +/// the root of the parent Merk tree because every wallet sync and every +/// shield/transfer/spend touches this subtree. +pub const SHIELDED_NOTES_KEY: u8 = 128; + +/// Key for the anchors tree inside a shielded pool (anchor_bytes → block_height_be). +/// Used by `validate_anchor_exists` for O(1) membership checks at spend time. +/// +/// Depth 1 in the parent tree — checked on every spend. +pub const SHIELDED_ANCHORS_IN_POOL_KEY: u8 = 192; /// Chunk power for the notes CommitmentTree (2^11 = 2048 items per chunk) pub const SHIELDED_NOTES_CHUNK_POWER: u8 = 11; @@ -53,7 +85,7 @@ pub fn shielded_credit_pool_path_vec() -> Vec> { ] } -/// Path to the notes tree: [AddressBalances, "s", [1]] +/// Path to the notes tree: [AddressBalances, "s", [128]] pub fn shielded_credit_pool_notes_path() -> [&'static [u8]; 3] { [ Into::<&[u8; 1]>::into(RootTree::AddressBalances), @@ -62,7 +94,7 @@ pub fn shielded_credit_pool_notes_path() -> [&'static [u8]; 3] { ] } -/// Path to the notes tree as a vec: [AddressBalances, "s", [1]] +/// Path to the notes tree as a vec: [AddressBalances, "s", [128]] pub fn shielded_credit_pool_notes_path_vec() -> Vec> { vec![ vec![RootTree::AddressBalances as u8], @@ -71,7 +103,7 @@ pub fn shielded_credit_pool_notes_path_vec() -> Vec> { ] } -/// Path to the nullifiers tree: [AddressBalances, "s", [2]] +/// Path to the nullifiers tree: [AddressBalances, "s", [64]] pub fn shielded_credit_pool_nullifiers_path() -> [&'static [u8]; 3] { [ Into::<&[u8; 1]>::into(RootTree::AddressBalances), @@ -80,7 +112,7 @@ pub fn shielded_credit_pool_nullifiers_path() -> [&'static [u8]; 3] { ] } -/// Path to the nullifiers tree as a vec: [AddressBalances, "s", [2]] +/// Path to the nullifiers tree as a vec: [AddressBalances, "s", [64]] pub fn shielded_credit_pool_nullifiers_path_vec() -> Vec> { vec![ vec![RootTree::AddressBalances as u8], @@ -89,7 +121,7 @@ pub fn shielded_credit_pool_nullifiers_path_vec() -> Vec> { ] } -/// Path to the anchors tree: [AddressBalances, "s", [6]] +/// Path to the anchors tree: [AddressBalances, "s", [192]] pub fn shielded_credit_pool_anchors_path() -> [&'static [u8]; 3] { [ Into::<&[u8; 1]>::into(RootTree::AddressBalances), @@ -98,7 +130,7 @@ pub fn shielded_credit_pool_anchors_path() -> [&'static [u8]; 3] { ] } -/// Path to the anchors tree as a vec: [AddressBalances, "s", [6]] +/// Path to the anchors tree as a vec: [AddressBalances, "s", [192]] pub fn shielded_credit_pool_anchors_path_vec() -> Vec> { vec![ vec![RootTree::AddressBalances as u8], @@ -107,7 +139,7 @@ pub fn shielded_credit_pool_anchors_path_vec() -> Vec> { ] } -/// Path to the anchors-by-height tree: [AddressBalances, "s", [8]] +/// Path to the anchors-by-height tree: [AddressBalances, "s", [96]] pub fn shielded_credit_pool_anchors_by_height_path() -> [&'static [u8]; 3] { [ Into::<&[u8; 1]>::into(RootTree::AddressBalances), @@ -116,7 +148,7 @@ pub fn shielded_credit_pool_anchors_by_height_path() -> [&'static [u8]; 3] { ] } -/// Path to the anchors-by-height tree as a vec: [AddressBalances, "s", [8]] +/// Path to the anchors-by-height tree as a vec: [AddressBalances, "s", [96]] pub fn shielded_credit_pool_anchors_by_height_path_vec() -> Vec> { vec![ vec![RootTree::AddressBalances as u8], @@ -160,7 +192,7 @@ pub fn shielded_latest_recorded_anchor_path_query() -> PathQuery { /// Resolves the nullifiers path based on pool type. /// /// Pool types: -/// - 0: Main credit shielded pool → `[AddressBalances, "s", [2]]` +/// - 0: Main credit shielded pool → `[AddressBalances, "s", [64]]` /// - 1: Main token shielded pool (not yet implemented) /// - 2: Individual token shielded pool (not yet implemented, requires pool_identifier) pub fn nullifiers_path_for_pool( diff --git a/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs b/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs index ac0b72bdc37..a12e03ba720 100644 --- a/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs @@ -19,7 +19,7 @@ impl Drive { /// window), the entry with the highest block_height is preserved. /// /// Why: `validate_anchor_exists` reads the primary anchors tree - /// (`[..., "s", [6]]`) when checking spend bundles. The + /// (`[..., "s", [192]]`) when checking spend bundles. The /// most-recent anchor — exposed via /// `query_most_recent_shielded_anchor` — is derived from the /// highest entry in the anchors-by-height index, so any chain diff --git a/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs b/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs index dcde1d80c76..51506755068 100644 --- a/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs @@ -10,7 +10,7 @@ use grovedb::TransactionArg; impl Drive { /// Version 0 implementation of reading the shielded pool total balance. /// - /// Reads the total balance from `[AddressBalances, "s", [5]]`. + /// Reads the total balance from `[AddressBalances, "s", [32]]`. /// Returns 0 if the key does not exist yet. pub(in crate::drive) fn read_shielded_pool_total_balance_v0( &self, diff --git a/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs b/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs index 8288c80a679..e7ea0699b35 100644 --- a/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs @@ -13,11 +13,11 @@ impl Drive { /// Version 0 implementation of recording the shielded pool anchor. /// /// Reads the current Sinsemilla anchor from the CommitmentTree at - /// `[AddressBalances, "s", [1]]`. If it differs from the most-recent + /// `[AddressBalances, "s", [128]]`. If it differs from the most-recent /// anchor — derived as the latest entry in the anchors-by-height - /// index `[..., "s", [8]]` (a `limit 1` reverse query) — inserts: - /// - `anchor_bytes → block_height_be` into the anchors tree `[..., [6]]` - /// - `block_height_be → anchor_bytes` into the anchors-by-height tree `[..., [8]]` + /// index `[..., "s", [96]]` (a `limit 1` reverse query) — inserts: + /// - `anchor_bytes → block_height_be` into the anchors tree `[..., [192]]` + /// - `block_height_be → anchor_bytes` into the anchors-by-height tree `[..., [96]]` /// /// There is intentionally no separate "most recent anchor" item: /// the anchors-by-height index is the canonical log, and the @@ -110,7 +110,7 @@ impl Drive { } /// Read the latest recorded shielded-pool anchor from - /// `SHIELDED_ANCHORS_BY_HEIGHT_KEY` (`[..., "s", [8]]`) via a + /// `SHIELDED_ANCHORS_BY_HEIGHT_KEY` (`[..., "s", [96]]`) via a /// `limit 1` reverse query. Returns `None` if the index is empty /// (pool has never recorded an anchor — chain is at genesis or /// no shielded ops yet). diff --git a/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs index 5f3ca168bc5..efebc103079 100644 --- a/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs @@ -10,7 +10,7 @@ use platform_version::version::PlatformVersion; impl Drive { /// Verify a `getMostRecentShieldedAnchor` proof. /// - /// Replays the canonical "latest entry in `[..., "s", [8]]`, + /// Replays the canonical "latest entry in `[..., "s", [96]]`, /// `limit 1` reverse" path query that the drive-abci handler /// runs (see `shielded_latest_recorded_anchor_path_query`) and /// extracts the anchor bytes from the highest-block-height entry. From a0bb99c1413ef0987b66b48443f5c3380431f6a4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 7 May 2026 02:52:17 +0700 Subject: [PATCH 2/2] fix(drive): insert shielded subtrees in BFS order for AVL balance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../v0/mod.rs | 27 ++++++++----- .../src/drive/initialization/v3/mod.rs | 40 ++++++++++++------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 7cf684eda45..e8302729601 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -625,7 +625,14 @@ impl Platform { &platform_version.drive, )?; - // Notes tree (CommitmentTree = CountTree items + Sinsemilla Frontier): + // The four child inserts below are ordered breadth-first to match the + // intended balanced shape of the parent Merk tree (see the layout + // diagram in `drive::drive::shielded::paths`): root first, then both + // depth-1 children, then the depth-2 leaf. AVL rebalancing is + // order-sensitive, so this ordering is what actually places + // `SHIELDED_NOTES_KEY` at the root and the spend-path keys at depth 1. + + // Level 0 (root): notes tree (CommitmentTree = CountTree items + Sinsemilla Frontier) // [AddressBalances, "s"] / [128] let shielded_pool_path = shielded_credit_pool_path(); self.drive.grove_insert_if_not_exists( @@ -637,7 +644,8 @@ impl Platform { &platform_version.drive, )?; - // Nullifiers tree (ProvableCountTree): [AddressBalances, "s"] / [64] + // Level 1 (left): nullifiers tree (ProvableCountTree) + // [AddressBalances, "s"] / [64] self.drive.grove_insert_if_not_exists( (&shielded_pool_path).into(), &[SHIELDED_NULLIFIERS_KEY], @@ -647,22 +655,23 @@ impl Platform { &platform_version.drive, )?; - // Total balance SumItem(0): [AddressBalances, "s"] / [32] + // Level 1 (right): anchors tree (NormalTree) — anchor_bytes → block_height_be + // [AddressBalances, "s"] / [192] self.drive.grove_insert_if_not_exists( (&shielded_pool_path).into(), - &[SHIELDED_TOTAL_BALANCE_KEY], - Element::new_sum_item(0), + &[SHIELDED_ANCHORS_IN_POOL_KEY], + Element::empty_tree(), Some(transaction), None, &platform_version.drive, )?; - // Anchors tree (NormalTree) inside pool: [AddressBalances, "s"] / [192] - // Stores block_height_be → anchor_bytes + // Level 2: total balance SumItem(0) + // [AddressBalances, "s"] / [32] self.drive.grove_insert_if_not_exists( (&shielded_pool_path).into(), - &[SHIELDED_ANCHORS_IN_POOL_KEY], - Element::empty_tree(), + &[SHIELDED_TOTAL_BALANCE_KEY], + Element::new_sum_item(0), Some(transaction), None, &platform_version.drive, diff --git a/packages/rs-drive/src/drive/initialization/v3/mod.rs b/packages/rs-drive/src/drive/initialization/v3/mod.rs index 7eb5c780271..1863b9fb473 100644 --- a/packages/rs-drive/src/drive/initialization/v3/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v3/mod.rs @@ -68,47 +68,57 @@ impl Drive { } /// Adds shielded pool batch operations for initialization. + /// + /// The eight subtree inserts are ordered breadth-first to match the + /// intended balanced shape of the parent Merk tree (see the layout + /// diagram in `crate::drive::shielded::paths`): root first, then both + /// depth-1 children, then the four depth-2 children, then the depth-3 + /// leaf. AVL rebalancing is order-sensitive, so this ordering is what + /// actually places `SHIELDED_NOTES_KEY` at the root and the spend-path + /// keys at depth 1. pub(in crate::drive::initialization) fn initial_state_structure_shielded_pool_operations( &self, batch: &mut GroveDbOpBatch, ) -> Result<(), Error> { - // 1. Shielded credit pool SumTree under AddressBalances + // Parent: shielded credit pool SumTree under AddressBalances. Must be + // inserted before any of its children so the subtree exists. batch.add_insert( vec![vec![RootTree::AddressBalances as u8]], vec![SHIELDED_CREDIT_POOL_KEY_U8], Element::empty_sum_tree(), ); - // 2. Notes tree (CommitmentTree = CountTree items + Sinsemilla Frontier) + // Level 0 (root): notes tree (CommitmentTree = CountTree items + Sinsemilla Frontier) batch.add_insert( shielded_credit_pool_path_vec(), vec![SHIELDED_NOTES_KEY], Element::empty_commitment_tree(SHIELDED_NOTES_CHUNK_POWER)?, ); - // 3. Nullifiers tree (ProvableCountTree) + // Level 1 (left): nullifiers tree (ProvableCountTree) — checked on every spend. batch.add_insert( shielded_credit_pool_path_vec(), vec![SHIELDED_NULLIFIERS_KEY], Element::empty_provable_count_tree(), ); - // 4. Total balance SumItem(0) + // Level 1 (right): anchors tree (NormalTree) — checked on every spend. + // Stores anchor_bytes → block_height_be. batch.add_insert( shielded_credit_pool_path_vec(), - vec![SHIELDED_TOTAL_BALANCE_KEY], - Element::new_sum_item(0), + vec![SHIELDED_ANCHORS_IN_POOL_KEY], + Element::empty_tree(), ); - // 5. Anchors tree (NormalTree) inside pool: anchor_bytes → block_height_be + // Level 2: total balance SumItem(0). batch.add_insert( shielded_credit_pool_path_vec(), - vec![SHIELDED_ANCHORS_IN_POOL_KEY], - Element::empty_tree(), + vec![SHIELDED_TOTAL_BALANCE_KEY], + Element::new_sum_item(0), ); - // 5b. Anchors-by-height tree (NormalTree): block_height_be → anchor_bytes. - // Reverse index for pruning old anchors by height range. Also the + // Level 2: anchors-by-height tree (NormalTree) — block_height_be → anchor_bytes. + // Reverse index for pruning old anchors by height range and the // canonical source of the most-recent anchor (read via `limit 1` // reverse query) — there is no separate "most recent" slot; key 7 // was retired because the duplicate state could desync from the @@ -119,7 +129,7 @@ impl Drive { Element::empty_tree(), ); - // 6. Per-block nullifiers CountSumTree under shielded credit pool. + // Level 2: per-block recent-nullifiers CountSumTree. // Each item is an ItemWithSumItem (serialized Vec<[u8;32]> + nullifier count as sum). batch.add_insert( shielded_credit_pool_path_vec(), @@ -127,15 +137,15 @@ impl Drive { Element::empty_count_sum_tree(), ); - // 7. Compacted nullifiers NormalTree under shielded credit pool. - // Key: (start_block, end_block) as 16 bytes, Value: serialized Vec<[u8;32]> + // Level 2: compacted nullifiers NormalTree. + // Key: (start_block, end_block) as 16 bytes, Value: serialized Vec<[u8;32]>. batch.add_insert( shielded_credit_pool_path_vec(), vec![SHIELDED_COMPACTED_NULLIFIERS_KEY_U8], Element::empty_tree(), ); - // 8. Nullifiers expiration time NormalTree under shielded credit pool. + // Level 3: nullifiers-expiration-time NormalTree (deepest leaf). batch.add_insert( shielded_credit_pool_path_vec(), vec![SHIELDED_NULLIFIERS_EXPIRATION_TIME_KEY_U8],