Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use dpp::check_validation_result_with_data;
use dpp::validation::ValidationResult;
use dpp::version::PlatformVersion;
use drive::drive::shielded::paths::shielded_latest_recorded_anchor_path_query;
use drive::error::drive::DriveError;
use drive::grovedb::query_result_type::QueryResultType;
use drive::grovedb::Element;
use drive::util::grove_operations::GroveDBToUse;
Expand Down Expand Up @@ -59,14 +60,45 @@ impl<C> Platform<C> {
)?;

let entries = results.to_key_elements();
// EMPTY-INDEX CONVENTION (see also
// `Drive::verify_most_recent_shielded_anchor_v0`):
//
// The `Anchor`/`Proof` `oneof` in the gRPC response has
// no third "absent" variant, so the proven and non-proven
// branches encode "no anchor recorded yet" differently:
//
// * Non-proven (this branch) — return
// `Anchor(vec![0u8; 32])`. Callers treat the all-zero
// anchor as "absent." Carries over from the
// pre-`[7]`-removal behaviour where the slot was
// initialised to `[0; 32]`; kept for wire-format
// compatibility.
// * Proven — return the GroveDB proof. The SDK runs
// `verify_most_recent_shielded_anchor_v0` against
// the same `limit 1` reverse `PathQuery`, which
// returns `Ok(None)` when the index is empty.
//
// If a future protocol revision extends the response
// with an explicit absent variant, drop the
// `vec![0u8; 32]` sentinel here in lock-step with both
// the verifier and the SDK decoder.
// Split the three states explicitly. A non-`Item`
// element under `SHIELDED_ANCHORS_BY_HEIGHT_KEY` is a
// state-corruption signal — the prove-mode verifier
// (`verify_most_recent_shielded_anchor_v0`) and the
// raw-read helper
// (`Drive::read_latest_recorded_shielded_anchor_v0`)
// both surface this as `CorruptedProof` /
// `CorruptedElementType`, so the non-prove path must
// not silently fold it into the empty-index sentinel.
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],
Some(_) => {
return Err(Error::Drive(drive::error::Error::Drive(
DriveError::CorruptedElementType("anchors-by-height entry is not an Item"),
)));
}
None => vec![0u8; 32],
};
Comment thread
QuantumExplorer marked this conversation as resolved.

GetMostRecentShieldedAnchorResponseV0 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,20 +534,48 @@ mod tests {
// 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")
// There is no longer a separate "most recent anchor"
// slot; the index is the canonical source.
//
// Read directly via the same path query the production
// handler uses, rather than reaching across crates for
// the internal `read_latest_recorded_shielded_anchor_v0`
// helper — its visibility is `pub(in crate::drive)` and
// a strategy test has no reason to push that to the
// public Drive surface.
use drive::drive::shielded::paths::shielded_latest_recorded_anchor_path_query;
let path_query = shielded_latest_recorded_anchor_path_query();
let (results, _) = drive
.grove_get_raw_path_query(
&path_query,
None,
QueryResultType::QueryKeyElementPairResultType,
&mut vec![],
&platform_version.drive,
)
.expect("query latest recorded shielded anchor");
let mut entries = results.to_key_elements();
let (_, most_recent_element) = entries
.pop()
.expect("most recent anchor must exist after successful shields");
let most_recent_anchor = if let Element::Item(bytes, _) = most_recent_element {
bytes
} else {
panic!("expected Item element in anchors-by-height tree");
};
assert_eq!(
most_recent_anchor.len(),
32,
"most recent anchor must be 32 bytes"
);
assert_ne!(
most_recent_anchor.to_vec(),
most_recent_anchor,
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());
.any(|(a, _)| *a == most_recent_anchor);
assert!(
is_known,
"most recent anchor must match one of the recorded anchors"
Expand Down
14 changes: 10 additions & 4 deletions packages/rs-drive/src/drive/shielded/estimated_costs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,19 @@ impl Drive {
// SumTree containing: notes (CommitmentTree), permanent nullifiers (ProvableCountTree),
// total balance (SumItem), anchors (NormalTree), anchors-by-height (NormalTree),
// recent nullifiers (CountSumTree), compacted nullifiers (NormalTree),
// expiration time (NormalTree), most recent anchor (Item)
// 9 elements total (7 subtrees + 2 items) → balanced Merk depth = ceil(log2(9)) = 4
// expiration time (NormalTree).
// 8 elements total (7 subtrees + 1 item) → balanced Merk depth = ceil(log2(8)) = 3.
//
// The retired `SHIELDED_MOST_RECENT_ANCHOR_KEY = 7` slot used
// to add a second `Item` entry; the most-recent anchor is
// now derived from the highest-block-height entry in the
// anchors-by-height subtree, so the pool layer is one item
// smaller than before.
estimated_costs_only_with_layer_info.insert(
KeyInfoPath::from_known_path(shielded_credit_pool_path()),
EstimatedLayerInformation {
tree_type: TreeType::SumTree,
estimated_layer_count: EstimatedLevel(4, false),
estimated_layer_count: EstimatedLevel(3, false),
estimated_layer_sizes: Mix {
subtrees_size: Some((
1,
Expand All @@ -99,7 +105,7 @@ impl Drive {
None,
7, // 7 subtrees: notes, permanent nullifiers, anchors, anchors-by-height, recent nullifiers, compacted nullifiers, expiration time
)),
items_size: Some((1, 32, None, 2)), // 2 items: total balance (SumItem), most recent anchor (Item)
items_size: Some((1, 8, None, 1)), // 1 item: total balance (SumItem, i64 = 8 bytes)
references_size: None,
},
},
Expand Down
22 changes: 12 additions & 10 deletions packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,19 @@ impl Drive {
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()
// `entries_below` came from a `Query::new()` (default
// `left_to_right = true`) over a `RangeTo` against
// `SHIELDED_ANCHORS_BY_HEIGHT_KEY`, whose keys are
// big-endian u64 — so GroveDB returns the entries in
// ascending numeric order and the live anchor is
// always the last element. Pop it off and discard;
// the remaining vec is exactly the prunable set, no
// extra scan or per-key clone.
let mut candidates = entries_below;
candidates
.pop()
.expect("entries_below non-empty (guarded above)");
entries_below
.into_iter()
.filter(|(k, _)| k != &max_key)
.collect()
candidates
};

// 3. Delete from both trees. Order doesn't matter for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,14 @@ impl Drive {
/// (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(
/// Used by `record_shielded_pool_anchor_if_changed_v0` to decide
/// whether the anchor changed this block. The drive-abci handler
/// (`Platform::query_most_recent_shielded_anchor_v0`) and SDK
/// verifier (`Drive::verify_most_recent_shielded_anchor_v0`)
/// share the same `PathQuery` shape via
/// `shielded_latest_recorded_anchor_path_query`, but operate on
/// proofs rather than this raw helper.
pub(in crate::drive) fn read_latest_recorded_shielded_anchor_v0(
&self,
transaction: grovedb::TransactionArg,
drive_version: &dpp::version::drive_versions::DriveVersion,
Expand Down
Loading