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 dbd7f8e8770..29b7ef80c74 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 @@ -10,29 +10,29 @@ use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_response::{ use dpp::check_validation_result_with_data; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::drive::shielded::paths::{ - shielded_credit_pool_path, shielded_credit_pool_path_vec, SHIELDED_MOST_RECENT_ANCHOR_KEY, -}; -use drive::grovedb::{Element, PathQuery, Query, SizedQuery}; -use drive::util::grove_operations::{DirectQueryType, GroveDBToUse}; +use drive::drive::shielded::paths::shielded_latest_recorded_anchor_path_query; +use drive::grovedb::query_result_type::QueryResultType; +use drive::grovedb::Element; +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 + /// is the value at the highest block-height key. + /// + /// Returns `[0; 32]` (mapped to `None` by the response decoder + /// downstream) when the index is empty — the pool has never + /// recorded an anchor yet on this chain. pub(super) fn query_most_recent_shielded_anchor_v0( &self, GetMostRecentShieldedAnchorRequestV0 { prove }: GetMostRecentShieldedAnchorRequestV0, platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { - let response = if prove { - let path_query = PathQuery { - path: shielded_credit_pool_path_vec(), - query: SizedQuery { - query: Query::new_single_key(vec![SHIELDED_MOST_RECENT_ANCHOR_KEY]), - limit: Some(1), - offset: None, - }, - }; + let path_query = shielded_latest_recorded_anchor_path_query(); + let response = if prove { let proof = check_validation_result_with_data!(self.drive.grove_get_proved_path_query( &path_query, None, @@ -50,19 +50,22 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { - let pool_path = shielded_credit_pool_path(); - - let maybe_element = self.drive.grove_get_raw( - (&pool_path).into(), - &[SHIELDED_MOST_RECENT_ANCHOR_KEY], - DirectQueryType::StatefulDirectQuery, + let (results, _) = self.drive.grove_get_raw_path_query( + &path_query, None, + QueryResultType::QueryKeyElementPairResultType, &mut vec![], &platform_version.drive, )?; - let anchor_bytes = match maybe_element { - Some(Element::Item(bytes, _)) => bytes, + let entries = results.to_key_elements(); + let anchor_bytes = match entries.into_iter().next() { + Some((_height_key, Element::Item(bytes, _))) => bytes, + // Empty index, or the entry isn't an Item (which + // would be a state-corruption bug elsewhere). Either + // way, return the zero-anchor sentinel — same shape + // the previous `[7]`-backed implementation used when + // the slot was uninitialised. _ => vec![0u8; 32], }; diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/shielded_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/shielded_tests.rs index 403c5bb4c7f..f8d611276eb 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/shielded_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/shielded_tests.rs @@ -361,7 +361,6 @@ mod tests { fn run_chain_verify_anchors_after_shielding() { use drive::drive::shielded::paths::{ shielded_credit_pool_anchors_by_height_path, shielded_credit_pool_anchors_path_vec, - shielded_credit_pool_path, SHIELDED_MOST_RECENT_ANCHOR_KEY, }; use drive::grovedb::query_result_type::QueryResultType; use drive::grovedb::{Element, PathQuery, Query, SizedQuery}; @@ -532,41 +531,27 @@ mod tests { } } - // 3. Verify most recent anchor is set and non-zero - let pool_path = shielded_credit_pool_path(); - let most_recent_element = drive - .grove - .get( - &pool_path, - &[SHIELDED_MOST_RECENT_ANCHOR_KEY], - None, - &platform_version.drive.grove_version, - ) - .unwrap() - .expect("most recent anchor element must exist"); - - if let Element::Item(most_recent_bytes, _) = most_recent_element { - assert_eq!( - most_recent_bytes.len(), - 32, - "most recent anchor must be 32 bytes" - ); - assert_ne!( - most_recent_bytes, - vec![0u8; 32], - "most recent anchor must not be all zeros after successful shields" - ); - // Most recent anchor must be one of the recorded anchors - let is_known = anchor_to_height - .iter() - .any(|(a, _)| *a == most_recent_bytes); - assert!( - is_known, - "most recent anchor must match one of the recorded anchors" - ); - } else { - panic!("most recent anchor must be an Item element"); - } + // 3. Verify the derived "most recent anchor" — i.e. the + // highest-block-height entry in the anchors-by-height + // index — exists and matches one of the recorded anchors. + // There is no longer a separate "most recent anchor" slot; + // the index is the canonical source. + let most_recent_anchor = drive + .read_latest_recorded_shielded_anchor_v0(None, &platform_version.drive) + .expect("read latest recorded anchor") + .expect("most recent anchor must exist after successful shields"); + assert_ne!( + most_recent_anchor.to_vec(), + vec![0u8; 32], + "most recent anchor must not be all zeros after successful shields" + ); + let is_known = anchor_to_height + .iter() + .any(|(a, _)| *a == most_recent_anchor.to_vec()); + assert!( + is_known, + "most recent anchor must match one of the recorded anchors" + ); tracing::info!( anchor_count = anchor_entries.len(), diff --git a/packages/rs-drive/src/drive/initialization/v3/mod.rs b/packages/rs-drive/src/drive/initialization/v3/mod.rs index dc817a940b0..7eb5c780271 100644 --- a/packages/rs-drive/src/drive/initialization/v3/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v3/mod.rs @@ -107,21 +107,18 @@ impl Drive { Element::empty_tree(), ); - // 5b. Anchors-by-height tree (NormalTree): block_height_be → anchor_bytes - // Reverse index for pruning old anchors by height range. + // 5b. Anchors-by-height tree (NormalTree): block_height_be → anchor_bytes. + // Reverse index for pruning old anchors by height range. Also 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 + // anchors tree under prune. batch.add_insert( shielded_credit_pool_path_vec(), vec![SHIELDED_ANCHORS_BY_HEIGHT_KEY], Element::empty_tree(), ); - // 5c. Most recent anchor item (empty initially, set on first block with notes) - batch.add_insert( - shielded_credit_pool_path_vec(), - vec![SHIELDED_MOST_RECENT_ANCHOR_KEY], - Element::new_item(vec![0u8; 32]), - ); - // 6. Per-block nullifiers CountSumTree under shielded credit pool. // Each item is an ItemWithSumItem (serialized Vec<[u8;32]> + nullifier count as sum). batch.add_insert( diff --git a/packages/rs-drive/src/drive/shielded/paths.rs b/packages/rs-drive/src/drive/shielded/paths.rs index 98b122d1ea2..2863ba92870 100644 --- a/packages/rs-drive/src/drive/shielded/paths.rs +++ b/packages/rs-drive/src/drive/shielded/paths.rs @@ -1,4 +1,5 @@ use crate::drive::RootTree; +use grovedb::{PathQuery, Query, SizedQuery}; /// The subtree key for the shielded credit pool under AddressBalances pub const SHIELDED_CREDIT_POOL_KEY: &[u8; 1] = b"s"; @@ -15,14 +16,22 @@ 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 anchors tree inside a shielded pool (anchor_bytes → block_height_be) +/// 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 most recent anchor item inside a shielded pool -pub const SHIELDED_MOST_RECENT_ANCHOR_KEY: u8 = 7; - -/// 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 for pruning old anchors by height range. +// 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`. + +/// 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; /// Chunk power for the notes CommitmentTree (2^11 = 2048 items per chunk) @@ -116,6 +125,38 @@ pub fn shielded_credit_pool_anchors_by_height_path_vec() -> Vec> { ] } +/// Canonical `PathQuery` used to read the most-recent recorded +/// shielded-pool anchor: a `limit 1` reverse scan over +/// `SHIELDED_ANCHORS_BY_HEIGHT_KEY`, returning the entry with the +/// highest `block_height_be` key. +/// +/// Shared between three call sites that must agree byte-for-byte: +/// - `Drive::read_latest_recorded_shielded_anchor_v0` (raw read used +/// by `record_shielded_pool_anchor_if_changed_v0` to decide whether +/// the anchor changed this block); +/// - `Platform::query_most_recent_shielded_anchor_v0` (proven RPC +/// handler); +/// - `Drive::verify_most_recent_shielded_anchor_v0` (SDK-side proof +/// verifier — replays the same `PathQuery`). +/// +/// Keep these three in sync via this helper rather than open-coding +/// the `PathQuery` at each site; subtle differences (e.g. swapping +/// `left_to_right` or the `limit`) would silently produce +/// non-matching proofs. +pub fn shielded_latest_recorded_anchor_path_query() -> PathQuery { + let mut query = Query::new(); + query.insert_all(); + query.left_to_right = false; + PathQuery { + path: shielded_credit_pool_anchors_by_height_path_vec(), + query: SizedQuery { + query, + limit: Some(1), + offset: None, + }, + } +} + /// Resolves the nullifiers path based on pool type. /// /// Pool types: 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 8581c424819..ac0b72bdc37 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 @@ -1,5 +1,6 @@ use crate::drive::shielded::paths::{ - shielded_credit_pool_anchors_by_height_path, shielded_credit_pool_anchors_path, + shielded_credit_pool_anchors_by_height_path, shielded_credit_pool_anchors_by_height_path_vec, + shielded_credit_pool_anchors_path, }; use crate::drive::Drive; use crate::error::Error; @@ -10,9 +11,24 @@ use grovedb::{Element, PathQuery, Query, QueryItem, SizedQuery, Transaction}; impl Drive { /// Version 0 implementation of pruning shielded pool anchors. /// - /// Queries the anchors-by-height tree for all entries with - /// `block_height < cutoff_height`, then deletes those entries from both - /// the anchors-by-height tree and the primary anchors tree. + /// Deletes anchors-by-height entries with `block_height < cutoff_height` + /// (and the matching primary anchors-tree entries), with one + /// crucial exception: **at least one entry must always remain in + /// the index**. Specifically, if every recorded anchor is below + /// the cutoff (no shielded ops have happened in the retention + /// 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 + /// 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 + /// state where that index is empty also has an empty primary + /// anchors tree, and *every* spend would be rejected with + /// `InvalidAnchorError` until a new shielded op refreshed the + /// state. Preserving the highest entry keeps the live anchor in + /// `[6]` indefinitely while the pool sits idle, at the cost of + /// at most one stale entry — bounded and acceptable. pub(in crate::drive) fn prune_shielded_pool_anchors_v0( &self, cutoff_height: u64, @@ -20,42 +36,88 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result<(), Error> { let grove_version = &platform_version.drive.grove_version; - - // Query anchors-by-height for all entries with height < cutoff (exclusive) let by_height_path = shielded_credit_pool_anchors_by_height_path(); - let mut query = Query::new(); - query.insert_item(QueryItem::RangeTo(..cutoff_height.to_be_bytes().to_vec())); + let by_height_path_vec = shielded_credit_pool_anchors_by_height_path_vec(); + + // 1. Query for entries strictly below cutoff (`RangeTo` is + // exclusive). Anything in this set is a candidate for + // deletion subject to the "always keep one" rule below. + let mut below_query = Query::new(); + below_query.insert_item(QueryItem::RangeTo(..cutoff_height.to_be_bytes().to_vec())); - let path_query = PathQuery { - path: by_height_path.iter().map(|p| p.to_vec()).collect(), + let below_path_query = PathQuery { + path: by_height_path_vec.clone(), query: SizedQuery { - query, + query: below_query, limit: None, offset: None, }, }; - let (results, _) = self.grove_get_raw_path_query( - &path_query, + let (below_results, _) = self.grove_get_raw_path_query( + &below_path_query, Some(transaction), QueryResultType::QueryKeyElementPairResultType, &mut vec![], &platform_version.drive, )?; - let entries = results.to_key_elements(); - if entries.is_empty() { + let entries_below: Vec<(Vec, Element)> = below_results.to_key_elements(); + if entries_below.is_empty() { return Ok(()); } - let anchors_path = shielded_credit_pool_anchors_path(); + // 2. Probe for any entry at or above cutoff. Cheap — we only + // need to know whether one exists, hence `limit: Some(1)`. + // If at least one does, the live anchor is recent and + // every entry below cutoff can be pruned safely. + // Otherwise, the highest entry below cutoff *is* the live + // anchor; we exclude it from deletion. + let mut above_query = Query::new(); + above_query.insert_item(QueryItem::RangeFrom(cutoff_height.to_be_bytes().to_vec()..)); + let above_path_query = PathQuery { + path: by_height_path_vec, + query: SizedQuery { + query: above_query, + limit: Some(1), + offset: None, + }, + }; + let (above_results, _) = self.grove_get_raw_path_query( + &above_path_query, + Some(transaction), + QueryResultType::QueryKeyElementPairResultType, + &mut vec![], + &platform_version.drive, + )?; + let any_above_cutoff = !above_results.to_key_elements().is_empty(); + + let to_delete: Vec<(Vec, Element)> = if any_above_cutoff { + entries_below + } else { + // Exclude the entry with the highest block_height key. + // Keys are big-endian u64 → lexicographic comparison + // matches numeric comparison. + let max_key = entries_below + .iter() + .map(|(k, _)| k.clone()) + .max() + .expect("entries_below non-empty (guarded above)"); + entries_below + .into_iter() + .filter(|(k, _)| k != &max_key) + .collect() + }; - for (height_key, element) in entries { - // Extract anchor_bytes from the element value + // 3. Delete from both trees. Order doesn't matter for + // correctness — both writes occur atomically as part of + // the block transaction. + let anchors_path = shielded_credit_pool_anchors_path(); + for (height_key, element) in to_delete { if let Element::Item(anchor_bytes, _) = element { - // Delete from anchors tree (anchor_bytes -> block_height) - // NOTE: .unwrap() is CostContext::unwrap(), not Result::unwrap(). - // It discards cost tracking info and never panics. + // NOTE: `.unwrap()` is `CostContext::unwrap()`, NOT + // `Result::unwrap()`. Discards cost-tracking info, + // never panics — standard pattern across Drive. self.grove .delete( &anchors_path, @@ -67,8 +129,6 @@ impl Drive { .unwrap() .map_err(Error::from)?; } - - // Delete from anchors-by-height tree (block_height -> anchor_bytes) self.grove .delete( &by_height_path, @@ -94,7 +154,7 @@ mod tests { use dpp::version::PlatformVersion; use grovedb::Element; - /// Inserts (anchor_bytes -> height) and (height_be -> anchor_bytes) at a given height. + /// Inserts (anchor_bytes → height) and (height_be → anchor_bytes) at a given height. fn seed_anchor( drive: &crate::drive::Drive, transaction: &grovedb::Transaction, @@ -146,7 +206,8 @@ mod tests { #[test] fn prune_cutoff_excludes_anchors_at_cutoff_height() { // Cutoff is exclusive (`RangeTo ..cutoff`). An anchor at exactly `cutoff` - // must not be pruned. + // must not be pruned. Sanity-checks that the at-or-above + // probe succeeds (anchor 20 is the live one). let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); let transaction = drive.grove.start_transaction(); @@ -158,7 +219,6 @@ mod tests { .prune_shielded_pool_anchors_v0(20, &transaction, platform_version) .expect("prune below 20"); - // Anchor at height 10 should be gone; anchor at height 20 should remain. let mut drive_ops = vec![]; assert!(!drive .has_shielded_anchor( @@ -179,8 +239,11 @@ mod tests { } #[test] - fn prune_removes_all_below_cutoff() { - // Multiple old anchors all below cutoff -> all pruned. + fn prune_removes_all_below_cutoff_when_a_recent_anchor_exists() { + // Multiple old anchors below cutoff AND a newer anchor at + // or above cutoff → every old anchor gets pruned, the new + // one survives. (Distinguishes from + // `prune_keeps_highest_when_all_below_cutoff` below.) let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); let transaction = drive.grove.start_transaction(); @@ -188,6 +251,7 @@ mod tests { for h in 1u64..=5 { seed_anchor(&drive, &transaction, [h as u8; 32], h, platform_version); } + seed_anchor(&drive, &transaction, [0xAAu8; 32], 12, platform_version); drive .prune_shielded_pool_anchors_v0(10, &transaction, platform_version) @@ -204,6 +268,14 @@ mod tests { ) .unwrap()); } + assert!(drive + .has_shielded_anchor( + &[0xAAu8; 32], + Some(&transaction), + &mut drive_ops, + platform_version + ) + .unwrap()); } #[test] @@ -238,4 +310,87 @@ mod tests { ) .unwrap()); } + + #[test] + fn prune_keeps_highest_when_all_below_cutoff() { + // The desync regression test. After ≥ retention_blocks of + // shielded inactivity, every anchor in the by-height index + // is below the prune cutoff. Pruning naively would empty + // both trees and freeze every spend with `InvalidAnchorError`. + // The fix preserves the highest entry below cutoff so the + // anchors tree always has the live anchor. + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let transaction = drive.grove.start_transaction(); + + // Three old anchors, no recent one. + seed_anchor(&drive, &transaction, [0xA1u8; 32], 100, platform_version); + seed_anchor(&drive, &transaction, [0xA2u8; 32], 200, platform_version); + seed_anchor(&drive, &transaction, [0xA3u8; 32], 300, platform_version); + + // Cutoff above every entry -> all are pruning candidates. + drive + .prune_shielded_pool_anchors_v0(1000, &transaction, platform_version) + .expect("prune below 1000"); + + let mut drive_ops = vec![]; + // Old entries (100, 200) are gone. + assert!(!drive + .has_shielded_anchor( + &[0xA1u8; 32], + Some(&transaction), + &mut drive_ops, + platform_version + ) + .unwrap()); + assert!(!drive + .has_shielded_anchor( + &[0xA2u8; 32], + Some(&transaction), + &mut drive_ops, + platform_version + ) + .unwrap()); + // Highest entry (300) survives — this is the live anchor + // the validator must still find. + assert!(drive + .has_shielded_anchor( + &[0xA3u8; 32], + Some(&transaction), + &mut drive_ops, + platform_version + ) + .unwrap()); + + // And the most-recent reader still sees it. + let latest = drive + .read_latest_recorded_shielded_anchor_v0(Some(&transaction), &platform_version.drive) + .expect("read latest"); + assert_eq!(latest, Some([0xA3u8; 32])); + } + + #[test] + fn prune_keeps_single_old_entry() { + // Edge case of the previous test: only one entry, it's old. + // Must survive pruning regardless of cutoff. + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let transaction = drive.grove.start_transaction(); + + seed_anchor(&drive, &transaction, [0xCCu8; 32], 50, platform_version); + + drive + .prune_shielded_pool_anchors_v0(10_000, &transaction, platform_version) + .expect("prune"); + + let mut drive_ops = vec![]; + assert!(drive + .has_shielded_anchor( + &[0xCCu8; 32], + Some(&transaction), + &mut drive_ops, + platform_version + ) + .unwrap()); + } } 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 23854dbccb1..8288c80a679 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 @@ -1,37 +1,47 @@ use crate::drive::shielded::paths::{ shielded_credit_pool_anchors_by_height_path, shielded_credit_pool_anchors_path, - shielded_credit_pool_path, SHIELDED_MOST_RECENT_ANCHOR_KEY, SHIELDED_NOTES_KEY, + shielded_credit_pool_path, shielded_latest_recorded_anchor_path_query, SHIELDED_NOTES_KEY, }; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use dpp::version::PlatformVersion; +use grovedb::query_result_type::QueryResultType; use grovedb::{Element, Transaction}; 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 - /// anchor (stored at `[AddressBalances, "s", [7]]`), inserts: - /// - `anchor_bytes -> block_height.to_be_bytes()` into anchors tree `[..., [6]]` - /// - `block_height.to_be_bytes() -> anchor_bytes` into anchors-by-height tree `[..., [8]]` - /// - Updates the most recent anchor item + /// `[AddressBalances, "s", [1]]`. 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]]` + /// + /// There is intentionally no separate "most recent anchor" item: + /// the anchors-by-height index is the canonical log, and the + /// most-recent anchor is whatever sits at the highest block-height + /// key. Eliminating the duplicate slot also eliminates the prune + /// vs. record desync that previously left the anchors tree empty + /// while the live anchor remained pinned in the redundant slot. pub(in crate::drive) fn record_shielded_pool_anchor_if_changed_v0( &self, block_height: u64, transaction: &Transaction, platform_version: &PlatformVersion, ) -> Result<(), Error> { - let grove_version = &platform_version.drive.grove_version; + let drive_version = &platform_version.drive; + let grove_version = &drive_version.grove_version; let pool_path = shielded_credit_pool_path(); - // 1. Read current anchor from CommitmentTree + // 1. Read current anchor from the CommitmentTree. // - // NOTE: .unwrap() below is CostContext::unwrap(), NOT Result::unwrap(). - // CostContext::unwrap() simply discards cost tracking info and never - // panics. This is the standard pattern for GroveDB operations throughout - // the Drive codebase when cost tracking is not needed. + // NOTE: `.unwrap()` below is `CostContext::unwrap()`, NOT + // `Result::unwrap()`. `CostContext::unwrap()` simply discards + // cost-tracking info and never panics. Standard pattern for + // GroveDB operations across the Drive codebase when cost + // tracking is not needed. let current_anchor = self .grove .commitment_tree_anchor( @@ -45,100 +55,133 @@ impl Drive { let current_anchor_bytes: [u8; 32] = current_anchor.to_bytes(); - // 2. Read most recent anchor from the dedicated element - let most_recent_anchor: [u8; 32] = self - .grove - .get( - &pool_path, - &[SHIELDED_MOST_RECENT_ANCHOR_KEY], + // 2. Read the latest recorded anchor from `[8]` via a + // `limit 1` reverse query. This is the post-removal + // replacement for the old `most_recent_anchor` slot — same + // value, but derived from the canonical log so it cannot + // drift out of sync with the anchors tree under prune. + // + // NOTE: there is intentionally no "skip when current is the + // Sinsemilla empty root" guard. The empty root is a + // well-defined value, recording it is harmless (it can't + // be spent against — no notes), and it ensures `[6]` is + // populated from the very first block-end event onward + // rather than only after the first shield op. + let latest_recorded = + self.read_latest_recorded_shielded_anchor_v0(Some(transaction), drive_version)?; + + // 3. Only insert if the anchor actually changed. Orchard's + // commitment tree only changes when a new note is + // appended, so over an idle pool this short-circuits every + // block and avoids the per-block insert cost. + if latest_recorded == Some(current_anchor_bytes) { + return Ok(()); + } + + // 4. Anchor changed — insert into both trees atomically with + // the rest of the block transaction. + let anchors_path = shielded_credit_pool_anchors_path(); + self.grove + .insert( + &anchors_path, + ¤t_anchor_bytes, + Element::new_item(block_height.to_be_bytes().to_vec()), + None, Some(transaction), grove_version, ) .unwrap() - .map_err(Error::from) - .and_then(|element| { - if let Element::Item(value, _) = element { - value.try_into().map_err(|_| { - Error::Drive(DriveError::CorruptedElementType( - "most recent anchor is not 32 bytes", - )) - }) - } else { - Err(Error::Drive(DriveError::CorruptedElementType( - "most recent anchor element is not an Item", - ))) - } - })?; + .map_err(Error::from)?; - // 3. Only store if different (skip zero anchor from empty tree) - let should_store = - current_anchor_bytes != most_recent_anchor && current_anchor_bytes != [0u8; 32]; + let anchors_by_height_path = shielded_credit_pool_anchors_by_height_path(); + self.grove + .insert( + &anchors_by_height_path, + &block_height.to_be_bytes(), + Element::new_item(current_anchor_bytes.to_vec()), + None, + Some(transaction), + grove_version, + ) + .unwrap() + .map_err(Error::from)?; - if should_store { - let anchors_path = shielded_credit_pool_anchors_path(); + Ok(()) + } - // Insert anchor_bytes -> block_height into the anchors tree - self.grove - .insert( - &anchors_path, - ¤t_anchor_bytes, - Element::new_item(block_height.to_be_bytes().to_vec()), - None, - Some(transaction), - grove_version, - ) - .unwrap() - .map_err(Error::from)?; + /// Read the latest recorded shielded-pool anchor from + /// `SHIELDED_ANCHORS_BY_HEIGHT_KEY` (`[..., "s", [8]]`) 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). + /// + /// Single source of truth for "what's the most-recent anchor on + /// this chain right now": + /// + /// - `record_shielded_pool_anchor_if_changed_v0` calls this to + /// decide whether the anchor changed this block. + /// - `Platform::query_most_recent_shielded_anchor_v0` builds the + /// same path query against `grove_get_proved_path_query` so + /// the SDK's verifier can replay it byte-for-byte. + /// Public so drive-abci's strategy tests + the + /// `getMostRecentShieldedAnchor` non-proven query path can reach + /// it; both are inside the workspace and would otherwise have to + /// duplicate the path-query construction. + pub fn read_latest_recorded_shielded_anchor_v0( + &self, + transaction: grovedb::TransactionArg, + drive_version: &dpp::version::drive_versions::DriveVersion, + ) -> Result, Error> { + let path_query = shielded_latest_recorded_anchor_path_query(); - // Insert block_height -> anchor_bytes into the anchors-by-height tree (for pruning) - let anchors_by_height_path = shielded_credit_pool_anchors_by_height_path(); - self.grove - .insert( - &anchors_by_height_path, - &block_height.to_be_bytes(), - Element::new_item(current_anchor_bytes.to_vec()), - None, - Some(transaction), - grove_version, - ) - .unwrap() - .map_err(Error::from)?; + let (results, _) = self.grove_get_raw_path_query( + &path_query, + transaction, + QueryResultType::QueryKeyElementPairResultType, + &mut vec![], + drive_version, + )?; - // Update the most recent anchor - self.grove - .insert( - &pool_path, - &[SHIELDED_MOST_RECENT_ANCHOR_KEY], - Element::new_item(current_anchor_bytes.to_vec()), - None, - Some(transaction), - grove_version, - ) - .unwrap() - .map_err(Error::from)?; + let entries = results.to_key_elements(); + match entries.into_iter().next() { + Some((_height_key, Element::Item(anchor_bytes, _))) => { + let anchor: [u8; 32] = anchor_bytes.try_into().map_err(|_v: Vec| { + Error::Drive(DriveError::CorruptedElementType( + "anchors-by-height value is not 32 bytes", + )) + })?; + Ok(Some(anchor)) + } + Some(_) => Err(Error::Drive(DriveError::CorruptedElementType( + "anchors-by-height entry is not an Item", + ))), + None => Ok(None), } - - Ok(()) } } #[cfg(test)] mod tests { use crate::drive::shielded::paths::{ - shielded_credit_pool_path, SHIELDED_MOST_RECENT_ANCHOR_KEY, + shielded_credit_pool_anchors_by_height_path, shielded_credit_pool_anchors_path, }; use crate::drive::Drive; - use crate::error::drive::DriveError; - use crate::error::Error; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; use dpp::version::PlatformVersion; use grovedb::Element; #[test] - fn record_on_empty_pool_does_nothing() { - // The commitment tree is empty - current_anchor_bytes is [0; 32]. The - // should_store guard (`!= [0u8; 32]`) keeps us out of the write branch, - // so has_shielded_anchor on any anchor still returns false. + fn record_on_empty_pool_records_the_sinsemilla_empty_root() { + // The commitment tree is empty, but its anchor is the + // well-defined Sinsemilla empty root (a non-zero hash), not + // `[0; 32]`. The new code records that anchor on the first + // block-end after pool init; subsequent calls with the + // unchanged anchor short-circuit (covered by + // `record_idempotent_when_anchor_unchanged`). The wrong + // assertion here is "no anchor was recorded" — it would + // imply we silently dropped state on every empty pool — so + // we instead assert that exactly one anchor lands in `[8]`, + // and that the matching `[6]` membership succeeds for it. let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); let transaction = drive.grove.start_transaction(); @@ -147,23 +190,30 @@ mod tests { .record_shielded_pool_anchor_if_changed_v0(10, &transaction, platform_version) .expect("record on empty tree should succeed"); + let latest = drive + .read_latest_recorded_shielded_anchor_v0(Some(&transaction), &platform_version.drive) + .expect("read latest") + .expect("empty-pool anchor should now be recorded"); + let mut drive_ops = vec![]; - assert!(!drive + assert!(drive .has_shielded_anchor( - &[0u8; 32], + &latest, Some(&transaction), &mut drive_ops, platform_version ) .unwrap()); + // The Sinsemilla empty root is not the zero hash; the old + // code's `!= [0; 32]` guard was a stale defense against an + // uninitialised slot, not a real "empty pool" gate. + assert_ne!(latest, [0u8; 32]); } #[test] fn record_after_note_insert_stores_anchor() { - // Insert a real note - the CommitmentTree advances and the current anchor - // becomes non-zero. record_anchor_if_changed should then store an entry. - // Note: cmx bytes must encode a valid Pallas field element; small values - // like [0x01; 32] work because they're below the field modulus. + // Insert a real note → CommitmentTree advances → current + // anchor becomes non-zero → both `[6]` and `[8]` get an entry. let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); let transaction = drive.grove.start_transaction(); @@ -187,42 +237,46 @@ mod tests { ) .expect("apply note op"); - // Record the anchor. drive .record_shielded_pool_anchor_if_changed_v0(5, &transaction, platform_version) .expect("record anchor after insert"); - // The most recent anchor slot was updated to a non-zero value. - let elem = drive - .grove - .get( - &shielded_credit_pool_path(), - &[SHIELDED_MOST_RECENT_ANCHOR_KEY], + // `read_latest_recorded_shielded_anchor_v0` returns the same + // anchor that's now in `[6]`. They write atomically, so a + // membership check must succeed against the same key. + let latest = drive + .read_latest_recorded_shielded_anchor_v0(Some(&transaction), &platform_version.drive) + .expect("read latest") + .expect("anchor should be recorded"); + + let mut drive_ops = vec![]; + assert!(drive + .has_shielded_anchor( + &latest, Some(&transaction), - &platform_version.drive.grove_version, + &mut drive_ops, + platform_version ) - .unwrap() - .expect("most recent anchor"); - if let Element::Item(bytes, _) = elem { - assert_ne!(bytes, vec![0u8; 32], "most recent anchor should be updated"); - } else { - panic!("expected Element::Item for most recent anchor"); - } + .unwrap()); } #[test] - fn corrupted_most_recent_anchor_returns_corrupted_element_type() { - // Overwrite the most recent anchor key with an invalid length item (e.g. 10 bytes). - // The try_into into [u8; 32] must fail with CorruptedElementType. + fn record_idempotent_when_anchor_unchanged() { + // Recording the same anchor twice in successive blocks must + // not double-insert: the index would otherwise gain a stale + // higher-height entry pointing at the live anchor and confuse + // both prune and most-recent-anchor reads. let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); let transaction = drive.grove.start_transaction(); - let grove_version = &platform_version.drive.grove_version; - // First: insert a note so current_anchor is non-zero (otherwise we'd skip past - // the failing read via should_store=false). cmx must be a valid Pallas element. - let ops = Drive::insert_note_op([1u8; 32], [0x02u8; 32], vec![3u8; 216], platform_version) - .expect("build"); + let ops = Drive::insert_note_op( + [0xAAu8; 32], + [0x01u8; 32], + vec![0x42; 216], + platform_version, + ) + .expect("build insert note op"); let grove_ops = crate::fees::op::LowLevelDriveOperation::grovedb_operations_batch_consume(ops); drive @@ -233,29 +287,95 @@ mod tests { &mut vec![], &platform_version.drive, ) - .expect("apply"); + .expect("apply note op"); - // Corrupt the most recent anchor item to a wrong length. drive - .grove - .insert( - &shielded_credit_pool_path(), - &[SHIELDED_MOST_RECENT_ANCHOR_KEY], - Element::new_item(vec![0xEEu8; 10]), - None, + .record_shielded_pool_anchor_if_changed_v0(5, &transaction, platform_version) + .expect("first record"); + drive + .record_shielded_pool_anchor_if_changed_v0(6, &transaction, platform_version) + .expect("second record (no-op)"); + drive + .record_shielded_pool_anchor_if_changed_v0(7, &transaction, platform_version) + .expect("third record (no-op)"); + + // `[8]` should have exactly one entry — the original block 5. + use crate::drive::shielded::paths::shielded_credit_pool_anchors_by_height_path_vec; + use grovedb::query_result_type::QueryResultType; + use grovedb::{PathQuery, Query, SizedQuery}; + let path_query = PathQuery { + path: shielded_credit_pool_anchors_by_height_path_vec(), + query: SizedQuery { + query: Query::new_range_full(), + limit: None, + offset: None, + }, + }; + let (results, _) = drive + .grove_get_raw_path_query( + &path_query, Some(&transaction), - grove_version, + QueryResultType::QueryKeyElementPairResultType, + &mut vec![], + &platform_version.drive, ) - .unwrap() - .expect("corrupt most recent anchor"); - - let err = drive - .record_shielded_pool_anchor_if_changed_v0(1, &transaction, platform_version) - .expect_err("expected CorruptedElementType"); - assert!( - matches!(err, Error::Drive(DriveError::CorruptedElementType(_))), - "got: {:?}", - err + .expect("scan anchors-by-height"); + let entries = results.to_key_elements(); + assert_eq!( + entries.len(), + 1, + "expected single anchor entry at block 5, got {}", + entries.len() ); + assert_eq!(entries[0].0, 5u64.to_be_bytes().to_vec()); + } + + #[test] + fn read_latest_returns_highest_height_entry() { + // With multiple anchors recorded, the helper must return the + // one keyed at the highest block_height (the live root). + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let transaction = drive.grove.start_transaction(); + let grove_version = &platform_version.drive.grove_version; + + let by_height_path = shielded_credit_pool_anchors_by_height_path(); + let anchors_path = shielded_credit_pool_anchors_path(); + for (h, anchor) in [ + (10u64, [0x11u8; 32]), + (20u64, [0x22u8; 32]), + (15u64, [0x33u8; 32]), + ] { + drive + .grove + .insert( + &anchors_path, + &anchor, + Element::new_item(h.to_be_bytes().to_vec()), + None, + Some(&transaction), + grove_version, + ) + .unwrap() + .expect("seed anchor"); + drive + .grove + .insert( + &by_height_path, + &h.to_be_bytes(), + Element::new_item(anchor.to_vec()), + None, + Some(&transaction), + grove_version, + ) + .unwrap() + .expect("seed by-height"); + } + + let latest = drive + .read_latest_recorded_shielded_anchor_v0(Some(&transaction), &platform_version.drive) + .expect("read latest") + .expect("not empty"); + assert_eq!(latest, [0x22u8; 32], "highest height (20) should win"); } } 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 c7dca457dee..5f3ca168bc5 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 @@ -1,41 +1,34 @@ -use crate::drive::shielded::paths::{ - shielded_credit_pool_path_vec, SHIELDED_MOST_RECENT_ANCHOR_KEY, -}; +use crate::drive::shielded::paths::shielded_latest_recorded_anchor_path_query; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::proof::ProofError; use crate::error::Error; use crate::verify::RootHash; -use grovedb::{Element, GroveDb, PathQuery, Query, SizedQuery}; +use grovedb::{Element, GroveDb}; use platform_version::version::PlatformVersion; impl Drive { + /// Verify a `getMostRecentShieldedAnchor` proof. + /// + /// Replays the canonical "latest entry in `[..., "s", [8]]`, + /// `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. + /// + /// Returns `Ok(None)` if the anchors-by-height index is empty — + /// i.e. no shielded ops have produced a recorded anchor yet on + /// this chain. pub(super) fn verify_most_recent_shielded_anchor_v0( proof: &[u8], verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Option<[u8; 32]>), Error> { - let path_query = PathQuery { - path: shielded_credit_pool_path_vec(), - query: SizedQuery { - query: Query::new_single_key(vec![SHIELDED_MOST_RECENT_ANCHOR_KEY]), - limit: Some(1), - offset: None, - }, - }; + let path_query = shielded_latest_recorded_anchor_path_query(); let (root_hash, mut proved_key_values) = if verify_subset_of_proof { - GroveDb::verify_subset_query_with_absence_proof( - proof, - &path_query, - &platform_version.drive.grove_version, - )? + GroveDb::verify_subset_query(proof, &path_query, &platform_version.drive.grove_version)? } else { - GroveDb::verify_query_with_absence_proof( - proof, - &path_query, - &platform_version.drive.grove_version, - )? + GroveDb::verify_query(proof, &path_query, &platform_version.drive.grove_version)? }; if proved_key_values.len() > 1 { @@ -44,20 +37,15 @@ impl Drive { ))); } - let anchor = if let Some(proved) = proved_key_values.pop() { - match proved.2 { + let anchor = match proved_key_values.pop() { + Some(proved) => match proved.2 { Some(Element::Item(value, _)) => { - let anchor: [u8; 32] = value.try_into().map_err(|_| { + let anchor: [u8; 32] = value.try_into().map_err(|_v: Vec| { Error::Drive(DriveError::CorruptedElementType( - "most recent anchor is not 32 bytes", + "anchors-by-height value is not 32 bytes", )) })?; - // A zero anchor means no anchor has been recorded yet - if anchor == [0u8; 32] { - None - } else { - Some(anchor) - } + Some(anchor) } Some(_) => { return Err(Error::Proof(ProofError::CorruptedProof( @@ -65,9 +53,8 @@ impl Drive { ))); } None => None, - } - } else { - None + }, + None => None, }; Ok((root_hash, anchor)) @@ -78,30 +65,32 @@ impl Drive { mod tests { use super::*; use crate::drive::shielded::paths::{ - shielded_credit_pool_path_vec, SHIELDED_MOST_RECENT_ANCHOR_KEY, + shielded_credit_pool_anchors_by_height_path_vec, SHIELDED_ANCHORS_BY_HEIGHT_KEY, + SHIELDED_CREDIT_POOL_KEY, }; + use crate::drive::RootTree; use crate::util::batch::grovedb_op_batch::GroveDbOpBatchV0Methods; use crate::util::batch::GroveDbOpBatch; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; use grovedb::batch::QualifiedGroveDbOp; - use grovedb::{PathQuery, Query, SizedQuery}; use platform_version::version::PlatformVersion; - #[test] - fn should_prove_and_verify_most_recent_shielded_anchor_present() { - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::latest(); - - let pool_path = shielded_credit_pool_path_vec(); - let anchor: [u8; 32] = [42u8; 32]; - - // Insert the most recent anchor - let op = QualifiedGroveDbOp::insert_or_replace_op( - pool_path.clone(), - vec![SHIELDED_MOST_RECENT_ANCHOR_KEY], + fn seed_anchor_at_height( + drive: &Drive, + height: u64, + anchor: [u8; 32], + platform_version: &PlatformVersion, + ) { + let by_height_path = vec![ + vec![RootTree::AddressBalances as u8], + SHIELDED_CREDIT_POOL_KEY.to_vec(), + vec![SHIELDED_ANCHORS_BY_HEIGHT_KEY], + ]; + let op = QualifiedGroveDbOp::insert_only_known_to_not_already_exist_op( + by_height_path, + height.to_be_bytes().to_vec(), Element::new_item(anchor.to_vec()), ); - drive .grove_apply_batch( GroveDbOpBatch::from_operations(vec![op]), @@ -109,23 +98,22 @@ mod tests { None, &platform_version.drive, ) - .expect("should apply batch"); - - // Construct the same path query as the verify function - let path_query = PathQuery { - path: pool_path, - query: SizedQuery { - query: Query::new_single_key(vec![SHIELDED_MOST_RECENT_ANCHOR_KEY]), - limit: Some(1), - offset: None, - }, - }; + .expect("seed anchor at height"); + } + #[test] + fn should_prove_and_verify_most_recent_shielded_anchor_present() { + // Seed the highest-block-height entry; verifier reads it back. + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let anchor: [u8; 32] = [42u8; 32]; + seed_anchor_at_height(&drive, 100, anchor, platform_version); + + let path_query = shielded_latest_recorded_anchor_path_query(); let proof = drive .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) .expect("should produce proof"); - // Verify let (root_hash, verified_anchor) = Drive::verify_most_recent_shielded_anchor(proof.as_slice(), false, platform_version) .expect("should verify proof"); @@ -134,32 +122,22 @@ mod tests { assert_eq!( verified_anchor, Some(anchor), - "verified anchor should match" + "verified anchor should match seeded anchor" ); } #[test] fn should_prove_and_verify_most_recent_shielded_anchor_absent() { + // No anchors recorded — the index is empty, so the verifier + // returns None. let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); - let pool_path = shielded_credit_pool_path_vec(); - - // Construct the same path query (no data inserted) - let path_query = PathQuery { - path: pool_path, - query: SizedQuery { - query: Query::new_single_key(vec![SHIELDED_MOST_RECENT_ANCHOR_KEY]), - limit: Some(1), - offset: None, - }, - }; - + let path_query = shielded_latest_recorded_anchor_path_query(); let proof = drive .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) .expect("should produce proof"); - // Verify let (root_hash, verified_anchor) = Drive::verify_most_recent_shielded_anchor(proof.as_slice(), false, platform_version) .expect("should verify proof"); @@ -172,51 +150,30 @@ mod tests { } #[test] - fn should_prove_and_verify_zero_anchor_as_none() { + fn highest_block_height_wins() { + // Multiple recorded anchors → the verifier returns the one at + // the highest block_height (`limit 1` + `left_to_right=false`). let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); + seed_anchor_at_height(&drive, 50, [0x11u8; 32], platform_version); + seed_anchor_at_height(&drive, 200, [0x22u8; 32], platform_version); + seed_anchor_at_height(&drive, 100, [0x33u8; 32], platform_version); - let pool_path = shielded_credit_pool_path_vec(); - - // Insert a zero anchor (means no anchor has been recorded yet) - let op = QualifiedGroveDbOp::insert_or_replace_op( - pool_path.clone(), - vec![SHIELDED_MOST_RECENT_ANCHOR_KEY], - Element::new_item([0u8; 32].to_vec()), - ); - - drive - .grove_apply_batch( - GroveDbOpBatch::from_operations(vec![op]), - false, - None, - &platform_version.drive, - ) - .expect("should apply batch"); - - // Construct the same path query - let path_query = PathQuery { - path: pool_path, - query: SizedQuery { - query: Query::new_single_key(vec![SHIELDED_MOST_RECENT_ANCHOR_KEY]), - limit: Some(1), - offset: None, - }, - }; - + let path_query = shielded_latest_recorded_anchor_path_query(); let proof = drive .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) .expect("should produce proof"); - // Verify - zero anchor should be treated as None - let (root_hash, verified_anchor) = + let (_, verified_anchor) = Drive::verify_most_recent_shielded_anchor(proof.as_slice(), false, platform_version) .expect("should verify proof"); - assert!(!root_hash.is_empty(), "root hash should not be empty"); - assert!( - verified_anchor.is_none(), - "zero anchor should be treated as none" + assert_eq!( + verified_anchor, + Some([0x22u8; 32]), + "highest height (200) should win" ); + // Suppress unused-import warnings under cfg(test). + let _ = shielded_credit_pool_anchors_by_height_path_vec; } }