From 6dfa0fbd96e9d6e31f706863b0c6381b4a794d29 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 7 May 2026 00:22:35 +0700 Subject: [PATCH] fix(drive,drive-abci): retire SHIELDED_MOST_RECENT_ANCHOR_KEY; derive most-recent from [8] and never empty it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shielded pool kept the live anchor in two places: * `[..., "s", [6]]` (`SHIELDED_ANCHORS_IN_POOL_KEY`) — the lookup table validate_anchor_exists reads, written and pruned together with `[8]`. * `[..., "s", [7]]` (`SHIELDED_MOST_RECENT_ANCHOR_KEY`) — a redundant single-slot mirror of the latest anchor, written by record_shielded_pool_anchor_if_changed and never touched by prune. After ≥ retention_blocks (1000) of shielded inactivity, prune removed the only entry from `[6]`/`[8]`, but `[7]` retained its value because record was guarded by "anchor changed since last record" and the anchor hadn't changed for 1000 blocks. The state ended up with: * getMostRecentShieldedAnchor → live anchor (from `[7]`) * getShieldedAnchors → empty (from `[6]`) * validate_anchor_exists → false → every spend rejected with InvalidAnchorError until a new shield op refreshed `[6]`. Two changes that together remove the failure mode: 1. Drop `[7]` entirely. The most-recent anchor is now derived from a `limit 1` reverse query against `[8]` — single source of truth, can't desync. - paths.rs: remove the constant; document the retirement. - initialization/v3: drop the `[7]` init insert. - record_anchor_if_changed/v0: read latest from `[8]`, compare, insert `[6]`+`[8]` when changed. Drop the stale `!= [0; 32]` guard (which was a defense against `[7]`'s uninitialised state, not a real "empty pool" gate — the Sinsemilla empty root is non-zero). - query/shielded/most_recent_anchor/v0 + verify equivalent: replay the same `limit 1` reverse path query so proofs match. 2. prune_shielded_pool_anchors_v0 now preserves the highest `[8]` entry whenever pruning would otherwise empty the index (i.e. every entry is below cutoff). The retention invariant relaxes to "keep at most one stale anchor when the pool sits idle past the retention window" — bounded and acceptable, vs. the old behavior of "empty the lookup table and freeze every spend." Probes for any entry ≥ cutoff with `limit 1` to skip the special case when the live anchor is already recent. A new `Drive::read_latest_recorded_shielded_anchor_v0` helper encapsulates the reverse query so record/query/verify all share the canonical `PathQuery` (in `shielded_latest_recorded_anchor_path_query`). The strategy test that previously asserted on `[7]` now reads the helper instead. Five new unit tests cover the regression: - `record_on_empty_pool_records_the_sinsemilla_empty_root` - `record_idempotent_when_anchor_unchanged` - `read_latest_returns_highest_height_entry` - `prune_keeps_highest_when_all_below_cutoff` (the desync case) - `prune_keeps_single_old_entry` 3043 drive lib tests + 22 verify-shielded + 54 drive-abci shielded query + 15 shielded-common tests pass; clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shielded/most_recent_anchor/v0/mod.rs | 47 ++- .../test_cases/shielded_tests.rs | 57 +-- .../src/drive/initialization/v3/mod.rs | 15 +- packages/rs-drive/src/drive/shielded/paths.rs | 53 ++- .../drive/shielded/prune_anchors/v0/mod.rs | 211 ++++++++-- .../record_anchor_if_changed/v0/mod.rs | 382 ++++++++++++------ .../v0/mod.rs | 179 ++++---- 7 files changed, 601 insertions(+), 343 deletions(-) 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; } }