diff --git a/grovedb-query/src/merge.rs b/grovedb-query/src/merge.rs index 86c9d1003..2b15b2289 100644 --- a/grovedb-query/src/merge.rs +++ b/grovedb-query/src/merge.rs @@ -16,16 +16,49 @@ impl SubqueryBranch { (None, Some(subquery)) => Some(subquery), (Some(subquery), Some(other_subquery)) => { let mut merged_subquery = subquery.clone(); - merged_subquery.merge_with(*other_subquery); + merged_subquery.merge_with_unchecked(*other_subquery); Some(merged_subquery) } } } + /// Whether either side of a branch merge carries a read mode + /// anywhere in its subquery. Branch merges combine subqueries by + /// field, which cannot express "both of these are axis reads" — so + /// the public entry points refuse rather than drop the mode. + fn branch_merge_carries_read_mode(&self, other: &Self) -> bool { + let carries = |branch: &Self| { + branch + .subquery + .as_deref() + .is_some_and(|subquery| subquery.has_read_mode_anywhere()) + }; + carries(self) || carries(other) + } + /// Merges two subquery branches, combining their subquery paths and /// subqueries. When paths differ, creates conditional subqueries to /// preserve both branches. - pub fn merge(&self, other: &Self) -> Self { + /// + /// Errors if either side carries a [`ReadMode`](crate::ReadMode) + /// anywhere: merging combines subqueries field by field, and the + /// mode is not one of the combined fields, so a merge would answer + /// key selection where the caller asked for an axis or sum-budget + /// read. + pub fn merge(&self, other: &Self) -> Result { + if self.branch_merge_carries_read_mode(other) { + return Err(crate::error::Error::NotSupported( + "can not merge subquery branches carrying read modes (axis / sum-budget reads)" + .to_string(), + )); + } + Ok(self.merge_unchecked(other)) + } + + /// The merge body, private: every public path checks read modes + /// first, and the recursive internals only ever see branches whose + /// descendants passed that check. + fn merge_unchecked(&self, other: &Self) -> Self { match (&self.subquery_path, &other.subquery_path) { (None, None) => { // they both just have subqueries without paths @@ -81,7 +114,7 @@ impl SubqueryBranch { Some(left_path_leftovers) }; merged_query.insert_key(left_top_key.clone()); - merged_query.merge_conditional_boxed_subquery( + merged_query.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(left_top_key), SubqueryBranch { subquery_path: maybe_left_path_leftovers, @@ -96,7 +129,7 @@ impl SubqueryBranch { }; merged_query.insert_key(right_top_key.clone()); - merged_query.merge_conditional_boxed_subquery( + merged_query.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(right_top_key), SubqueryBranch { subquery_path: maybe_right_path_leftovers, @@ -121,7 +154,7 @@ impl SubqueryBranch { merged_query.insert_key(first_key.clone()); // our subquery stays the same as we didn't change level // add a conditional subquery for other - merged_query.merge_conditional_boxed_subquery( + merged_query.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(first_key), SubqueryBranch { subquery_path: maybe_left_path_leftovers, @@ -146,7 +179,7 @@ impl SubqueryBranch { merged_query.insert_key(other_first.clone()); // our subquery stays the same as we didn't change level // add a conditional subquery for other - merged_query.merge_conditional_boxed_subquery( + merged_query.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(other_first), SubqueryBranch { subquery_path: maybe_right_path_leftovers, @@ -184,7 +217,7 @@ impl SubqueryBranch { }; // our subquery stays the same as we didn't change level // add a conditional subquery for other - merged_subquery.merge_conditional_boxed_subquery( + merged_subquery.merge_conditional_boxed_subquery_unchecked( // there are no conditional subquery branches yes QueryItem::Key(our_top_key), SubqueryBranch { @@ -220,7 +253,7 @@ impl SubqueryBranch { }; // their subquery stays the same as we didn't change level // add a conditional subquery for other - merged_subquery.merge_conditional_boxed_subquery( + merged_subquery.merge_conditional_boxed_subquery_unchecked( // there are no conditional subquery branches yes QueryItem::Key(their_top_key), SubqueryBranch { @@ -245,7 +278,7 @@ impl Query { ) { if let Some(current_subquery) = self.default_subquery_branch.subquery.as_mut() { if let Some(other_subquery) = other_default_branch_subquery { - current_subquery.merge_with(*other_subquery); + current_subquery.merge_with_unchecked(*other_subquery); } } else { // None existed yet @@ -258,7 +291,36 @@ impl Query { /// or subqueried to the subquery_path/subquery if a subquery is /// present. Merging involves creating conditional subqueries in the /// subqueries subqueries and paths. - pub fn merge_default_subquery_branch(&mut self, other_default_subquery_branch: SubqueryBranch) { + /// + /// Errors if either side carries a [`ReadMode`](crate::ReadMode) + /// anywhere — see [`SubqueryBranch::merge`] for why a branch merge + /// cannot carry one through. + pub fn merge_default_subquery_branch( + &mut self, + other_default_subquery_branch: SubqueryBranch, + ) -> Result<(), crate::error::Error> { + let carries = |branch: &SubqueryBranch| { + branch + .subquery + .as_deref() + .is_some_and(|subquery| subquery.has_read_mode_anywhere()) + }; + if carries(&self.default_subquery_branch) || carries(&other_default_subquery_branch) { + return Err(crate::error::Error::NotSupported( + "can not merge a default subquery branch carrying a read mode (axis / \ + sum-budget read)" + .to_string(), + )); + } + self.merge_default_subquery_branch_unchecked(other_default_subquery_branch); + Ok(()) + } + + /// The merge body, private: see [`SubqueryBranch::merge_unchecked`]. + fn merge_default_subquery_branch_unchecked( + &mut self, + other_default_subquery_branch: SubqueryBranch, + ) { match ( &self.default_subquery_branch.subquery_path, &other_default_subquery_branch.subquery_path, @@ -307,7 +369,7 @@ impl Query { } else { Some(left_path_leftovers) }; - self.merge_conditional_boxed_subquery( + self.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(left_top_key), SubqueryBranch { subquery_path: maybe_left_path_leftovers, @@ -321,7 +383,7 @@ impl Query { Some(right_path_leftovers) }; - self.merge_conditional_boxed_subquery( + self.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(right_top_key), SubqueryBranch { subquery_path: maybe_right_path_leftovers, @@ -343,7 +405,7 @@ impl Query { // our subquery stays the same as we didn't change level // add a conditional subquery for other - self.merge_conditional_boxed_subquery( + self.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(first_key), SubqueryBranch { subquery_path: maybe_left_path_leftovers, @@ -362,7 +424,7 @@ impl Query { }; // our subquery stays the same as we didn't change level // add a conditional subquery for other - self.merge_conditional_boxed_subquery( + self.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(other_first), SubqueryBranch { subquery_path: maybe_right_path_leftovers, @@ -397,7 +459,7 @@ impl Query { }; // our subquery stays the same as we didn't change level // add a conditional subquery for other - self.merge_conditional_boxed_subquery( + self.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(our_top_key), SubqueryBranch { subquery_path: maybe_our_subquery_path, @@ -422,7 +484,7 @@ impl Query { }; // our subquery stays the same as we didn't change level // add a conditional subquery for other - self.merge_conditional_boxed_subquery( + self.merge_conditional_boxed_subquery_unchecked( QueryItem::Key(their_top_key), SubqueryBranch { subquery_path: maybe_their_subquery_path, @@ -463,7 +525,10 @@ impl Query { // No old items were using the default, or no old items existed. Just // apply the incoming default directly. for item in items { - self.merge_conditional_boxed_subquery(item, default_subquery_branch.clone()); + self.merge_conditional_boxed_subquery_unchecked( + item, + default_subquery_branch.clone(), + ); } return; } @@ -476,9 +541,15 @@ impl Query { let existing_default = self.default_subquery_branch.clone(); for item in in_both { if existing_default.subquery.is_some() || existing_default.subquery_path.is_some() { - self.merge_conditional_boxed_subquery(item.clone(), existing_default.clone()); + self.merge_conditional_boxed_subquery_unchecked( + item.clone(), + existing_default.clone(), + ); } - self.merge_conditional_boxed_subquery(item, default_subquery_branch.clone()); + self.merge_conditional_boxed_subquery_unchecked( + item, + default_subquery_branch.clone(), + ); } } @@ -486,14 +557,70 @@ impl Query { // items, just get the incoming default. if let Some(theirs_only) = intersection.theirs { for item in theirs_only { - self.merge_conditional_boxed_subquery(item, default_subquery_branch.clone()); + self.merge_conditional_boxed_subquery_unchecked( + item, + default_subquery_branch.clone(), + ); } } } /// Merges multiple queries into a single query. Items are unioned and /// conditional subquery branches are merged where they intersect. - pub fn merge_multiple(mut queries: Vec) -> Self { + /// + /// Errors if any input carries a [`ReadMode`](crate::ReadMode) at + /// any nesting level — axis and sum-budget reads have no defined + /// merge semantics, and merging them silently as key selection + /// would change what the query means. + /// + /// The **direction** (`left_to_right`) of every query after the + /// first is discarded — the merged query keeps the first query's + /// direction, at every nesting level. Use + /// [`Self::merge_multiple_directional`] to require agreement + /// instead of silently keeping the first. + pub fn merge_multiple(queries: Vec) -> Result { + for query in &queries { + if query.has_read_mode_anywhere() { + return Err(crate::error::Error::NotSupported( + "cannot merge queries carrying read modes (axis / sum-budget reads)" + .to_string(), + )); + } + } + Ok(Self::merge_multiple_unchecked(queries)) + } + + /// [`Self::merge_multiple`] with **direction agreement**: every + /// input's top-level `left_to_right` must match, and the merged + /// query carries it, instead of silently keeping the first query's + /// direction. (Directions at deeper nesting levels still follow the + /// first-branch-wins behavior of the underlying merge.) + pub fn merge_multiple_directional(queries: Vec) -> Result { + if let Some(first) = queries.first() { + let direction = first.left_to_right; + if queries.iter().any(|query| query.left_to_right != direction) { + return Err(crate::error::Error::NotSupported( + "cannot merge queries with conflicting directions (left_to_right differs)" + .to_string(), + )); + } + } + Self::merge_multiple(queries) + } + + /// The merge body, private. The `_unchecked` family is the whole + /// recursive machinery; the invariant that keeps it sound is that + /// **every** public entry point rejects read modes before calling + /// in — the whole-query merges (`merge_multiple`, + /// `merge_multiple_directional`, `merge_with`) and the branch + /// merges alike (`SubqueryBranch::merge`, + /// `merge_default_subquery_branch`, + /// `merge_conditional_boxed_subquery`, + /// `merge_conditional_subquery_branches_with_new_at_query_item`). + /// A new public wrapper that skips the check reintroduces the + /// silent-drop bug, because these bodies destructure `read_mode` + /// away by design. + fn merge_multiple_unchecked(mut queries: Vec) -> Self { if queries.is_empty() { return Query::new(); } @@ -507,10 +634,7 @@ impl Query { conditional_subquery_branches, left_to_right: _, add_parent_tree_on_subquery, - // Read modes do not merge: PathQuery::merge rejects - // read-mode queries before reaching this, and these - // merge functions become fallible (rejecting read-mode - // conflicts explicitly) in a follow-up. + // Checked by the public wrappers; read-mode-free here. read_mode: _, } = query; // Preserve add_parent_tree_on_subquery if any query requests it @@ -530,7 +654,7 @@ impl Query { for (conditional_item, conditional_subquery_branch) in conditional_subquery_branches { - merged_query.merge_conditional_boxed_subquery( + merged_query.merge_conditional_boxed_subquery_unchecked( conditional_item.clone(), conditional_subquery_branch, ); @@ -553,15 +677,30 @@ impl Query { /// Merges another query into this one, combining items and conditional /// subquery branches. - pub fn merge_with(&mut self, other: Query) { + /// + /// Errors if either side carries a [`ReadMode`](crate::ReadMode) at + /// any nesting level — see [`Self::merge_multiple`]. `other`'s + /// direction is discarded (this query's is kept), matching the + /// long-standing merge behavior. + pub fn merge_with(&mut self, other: Query) -> Result<(), crate::error::Error> { + if self.has_read_mode_anywhere() || other.has_read_mode_anywhere() { + return Err(crate::error::Error::NotSupported( + "cannot merge queries carrying read modes (axis / sum-budget reads)".to_string(), + )); + } + self.merge_with_unchecked(other); + Ok(()) + } + + /// The merge-with body, private: see [`Self::merge_multiple_unchecked`]. + fn merge_with_unchecked(&mut self, other: Query) { let Query { mut items, default_subquery_branch, conditional_subquery_branches, left_to_right: _, add_parent_tree_on_subquery, - // See merge_multiple: read modes do not merge; gated - // upstream and made explicitly fallible in a follow-up. + // Checked by the public wrappers; read-mode-free here. read_mode: _, } = other; // Preserve add_parent_tree_on_subquery if either query requests it @@ -576,7 +715,7 @@ impl Query { if let Some(conditional_subquery_branches) = conditional_subquery_branches { for (conditional_item, conditional_subquery_branch) in conditional_subquery_branches { - self.merge_conditional_boxed_subquery( + self.merge_conditional_boxed_subquery_unchecked( conditional_item.clone(), conditional_subquery_branch, ); @@ -600,16 +739,51 @@ impl Query { /// subquery and subquery_path if the item matches for the key. If /// multiple conditional subquery items match, then the first one that /// matches is used (in order that they were added). + /// + /// Errors if EITHER side carries a [`ReadMode`](crate::ReadMode) + /// anywhere — the receiving query's existing conditional branches + /// as well as the incoming one. A one-sided check is not enough: + /// merging an overlapping item splits both sides' branches and + /// recombines them through + /// [`merge_with_unchecked`](Query::merge_with_unchecked), so an + /// existing read mode can end up governing a different key range + /// than the caller established. See [`SubqueryBranch::merge`] for + /// why a branch merge cannot carry one through. pub fn merge_conditional_boxed_subquery( &mut self, query_item_merging_in: QueryItem, subquery_branch_merging_in: SubqueryBranch, + ) -> Result<(), crate::error::Error> { + if self.has_read_mode_anywhere() + || subquery_branch_merging_in + .subquery + .as_deref() + .is_some_and(|subquery| subquery.has_read_mode_anywhere()) + { + return Err(crate::error::Error::NotSupported( + "can not merge a conditional subquery branch carrying a read mode (axis / \ + sum-budget read)" + .to_string(), + )); + } + self.merge_conditional_boxed_subquery_unchecked( + query_item_merging_in, + subquery_branch_merging_in, + ); + Ok(()) + } + + /// The merge body, private: see [`SubqueryBranch::merge_unchecked`]. + fn merge_conditional_boxed_subquery_unchecked( + &mut self, + query_item_merging_in: QueryItem, + subquery_branch_merging_in: SubqueryBranch, ) { if subquery_branch_merging_in.subquery.is_some() || subquery_branch_merging_in.subquery_path.is_some() { self.conditional_subquery_branches = Some( - Self::merge_conditional_subquery_branches_with_new_at_query_item( + Self::merge_conditional_subquery_branches_with_new_at_query_item_unchecked( self.conditional_subquery_branches.take(), query_item_merging_in, subquery_branch_merging_in, @@ -622,10 +796,52 @@ impl Query { /// subquery and subquery_path if the item matches for the key. If /// multiple conditional subquery items match, then the first one that /// matches is used (in order that they were added). + /// + /// Errors if EITHER the existing branch map or the incoming branch + /// carries a [`ReadMode`](crate::ReadMode) anywhere — see + /// [`Query::merge_conditional_boxed_subquery`] for why the existing + /// side has to be inspected too. pub fn merge_conditional_subquery_branches_with_new_at_query_item( conditional_subquery_branches: Option>, query_item_merging_in: QueryItem, subquery_branch_merging_in: SubqueryBranch, + ) -> Result, crate::error::Error> { + let existing_carries = conditional_subquery_branches + .as_ref() + .is_some_and(|branches| { + branches.values().any(|branch| { + branch + .subquery + .as_deref() + .is_some_and(|subquery| subquery.has_read_mode_anywhere()) + }) + }); + if existing_carries + || subquery_branch_merging_in + .subquery + .as_deref() + .is_some_and(|subquery| subquery.has_read_mode_anywhere()) + { + return Err(crate::error::Error::NotSupported( + "can not merge a conditional subquery branch carrying a read mode (axis / \ + sum-budget read)" + .to_string(), + )); + } + Ok( + Self::merge_conditional_subquery_branches_with_new_at_query_item_unchecked( + conditional_subquery_branches, + query_item_merging_in, + subquery_branch_merging_in, + ), + ) + } + + /// The merge body, private: see [`SubqueryBranch::merge_unchecked`]. + fn merge_conditional_subquery_branches_with_new_at_query_item_unchecked( + conditional_subquery_branches: Option>, + query_item_merging_in: QueryItem, + subquery_branch_merging_in: SubqueryBranch, ) -> IndexMap { let mut merged_items: IndexMap = IndexMap::new(); // first we need to check if there are already conditional subquery branches @@ -667,7 +883,7 @@ impl Query { } // merge the overlapping subquery branches let merged_subquery_branch = - subquery_branch.merge(&subquery_branch_merging_in); + subquery_branch.merge_unchecked(&subquery_branch_merging_in); merged_items.insert(in_both, merged_subquery_branch); match (ours_left, ours_right, theirs_left, theirs_right) { diff --git a/grovedb-query/tests/merge_coverage.rs b/grovedb-query/tests/merge_coverage.rs index ba1e7db69..8bb826005 100644 --- a/grovedb-query/tests/merge_coverage.rs +++ b/grovedb-query/tests/merge_coverage.rs @@ -1,6 +1,8 @@ use indexmap::IndexMap; -use grovedb_query::{Query, QueryItem, SubqueryBranch}; +use grovedb_query::{ + AxisQuery, IndexAxis, Query, QueryItem, ReadMode, SubqueryBranch, SumBudgetRead, +}; fn k(v: u8) -> Vec { vec![v] @@ -20,7 +22,7 @@ fn merge_branch_both_paths_none_both_subqueries_none() { subquery_path: None, subquery: None, }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, None); assert_eq!(merged.subquery, None); } @@ -36,7 +38,7 @@ fn merge_branch_both_paths_none_self_has_subquery() { subquery_path: None, subquery: None, }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, None); assert_eq!(merged.subquery, Some(Box::new(sq))); } @@ -52,7 +54,7 @@ fn merge_branch_both_paths_none_other_has_subquery() { subquery_path: None, subquery: Some(Box::new(sq.clone())), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, None); assert_eq!(merged.subquery, Some(Box::new(sq))); } @@ -69,7 +71,7 @@ fn merge_branch_both_paths_none_both_have_subqueries() { subquery_path: None, subquery: Some(Box::new(sq_b)), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, None); let mq = merged.subquery.expect("merged subquery should exist"); // Both keys should be present in the merged query items @@ -92,7 +94,7 @@ fn merge_branch_same_paths_merges_subqueries() { subquery_path: Some(path.clone()), subquery: Some(Box::new(Query::new_single_key(k(2)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, Some(path)); let mq = merged.subquery.unwrap(); assert!(mq.items.contains(&QueryItem::Key(k(1)))); @@ -114,7 +116,7 @@ fn merge_branch_divergent_paths_both_have_leftovers() { subquery_path: Some(vec![k(10), k(30)]), subquery: Some(Box::new(Query::new_single_key(k(2)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, Some(vec![k(10)])); let mq = merged.subquery.unwrap(); // Should have conditional subquery branches for keys 20 and 30 @@ -135,7 +137,7 @@ fn merge_branch_divergent_paths_multi_segment_leftovers() { subquery_path: Some(vec![k(10), k(30)]), subquery: Some(Box::new(Query::new_single_key(k(2)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, Some(vec![k(10)])); let mq = merged.subquery.unwrap(); let conds = mq.conditional_subquery_branches.as_ref().unwrap(); @@ -158,7 +160,7 @@ fn merge_branch_divergent_paths_no_common_prefix() { subquery_path: Some(vec![k(2)]), subquery: Some(Box::new(Query::new_single_key(k(20)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); // No common path assert_eq!(merged.subquery_path, None); let mq = merged.subquery.unwrap(); @@ -182,7 +184,7 @@ fn merge_branch_our_path_longer_right_empty_leftovers() { subquery_path: Some(vec![k(10), k(20)]), subquery: Some(Box::new(Query::new_single_key(k(2)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, Some(vec![k(10), k(20)])); let mq = merged.subquery.unwrap(); let conds = mq.conditional_subquery_branches.as_ref().unwrap(); @@ -200,7 +202,7 @@ fn merge_branch_their_path_longer_left_empty_leftovers() { subquery_path: Some(vec![k(10), k(20), k(40)]), subquery: Some(Box::new(Query::new_single_key(k(2)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, Some(vec![k(10), k(20)])); let mq = merged.subquery.unwrap(); let conds = mq.conditional_subquery_branches.as_ref().unwrap(); @@ -221,7 +223,7 @@ fn merge_branch_ours_has_path_theirs_none() { subquery_path: None, subquery: Some(Box::new(Query::new_single_key(k(20)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); // Path should be None (dropped to the shorter level) assert_eq!(merged.subquery_path, None); let mq = merged.subquery.unwrap(); @@ -241,7 +243,7 @@ fn merge_branch_ours_none_theirs_has_path() { subquery_path: Some(vec![k(7), k(8)]), subquery: Some(Box::new(Query::new_single_key(k(20)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, None); let mq = merged.subquery.unwrap(); let conds = mq.conditional_subquery_branches.as_ref().unwrap(); @@ -260,7 +262,7 @@ fn merge_branch_ours_has_single_segment_path_theirs_none() { subquery_path: None, subquery: Some(Box::new(Query::new_single_key(k(20)))), }; - let merged = a.merge(&b); + let merged = a.merge(&b).expect("no read modes involved"); assert_eq!(merged.subquery_path, None); let mq = merged.subquery.unwrap(); let conds = mq.conditional_subquery_branches.as_ref().unwrap(); @@ -462,7 +464,7 @@ fn merge_multiple_two_queries_with_items_and_conditionals() { Some(Query::new_single_key(k(50))), ); - let merged = Query::merge_multiple(vec![q1, q2]); + let merged = Query::merge_multiple(vec![q1, q2]).expect("merge must succeed"); // All four keys should be present assert!(merged.items.contains(&QueryItem::Key(k(1)))); assert!(merged.items.contains(&QueryItem::Key(k(2)))); @@ -483,7 +485,7 @@ fn merge_multiple_three_queries() { let mut q3 = Query::new_single_key(k(3)); q3.set_subquery_path(vec![k(30)]); - let merged = Query::merge_multiple(vec![q1, q2, q3]); + let merged = Query::merge_multiple(vec![q1, q2, q3]).expect("merge must succeed"); assert_eq!(merged.items.len(), 3); assert!(merged.items.contains(&QueryItem::Key(k(1)))); assert!(merged.items.contains(&QueryItem::Key(k(2)))); @@ -521,7 +523,8 @@ fn merge_conditional_branches_none_existing_direct_insert() { subquery_path: Some(vec![k(10)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); assert_eq!(merged.len(), 1); assert!(merged.contains_key(&QueryItem::Key(k(5)))); } @@ -545,7 +548,8 @@ fn merge_conditional_branches_exact_overlap_no_leftovers() { subquery_path: Some(vec![k(20)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); // Exact overlap — single merged entry assert_eq!(merged.len(), 1); assert!(merged.contains_key(&QueryItem::Range(k(1)..k(5)))); @@ -570,7 +574,8 @@ fn merge_conditional_branches_ours_extends_left_only() { subquery_path: Some(vec![k(20)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); assert_eq!(merged.len(), 2); assert!(merged.contains_key(&QueryItem::Range(k(1)..k(3)))); assert!(merged.contains_key(&QueryItem::Range(k(3)..k(5)))); @@ -595,7 +600,8 @@ fn merge_conditional_branches_ours_extends_right_only() { subquery_path: Some(vec![k(20)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); assert_eq!(merged.len(), 2); assert!(merged.contains_key(&QueryItem::Range(k(1)..k(3)))); } @@ -619,7 +625,8 @@ fn merge_conditional_branches_ours_contains_theirs() { subquery_path: Some(vec![k(20)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); // Should have left piece, intersection, and right piece assert!(merged.len() >= 2); assert!(merged.contains_key(&QueryItem::Key(k(5)))); @@ -644,7 +651,8 @@ fn merge_conditional_branches_theirs_contains_ours() { subquery_path: Some(vec![k(20)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); // Should have left piece from theirs, intersection at Key(5), right piece from theirs assert!(merged.len() >= 2); assert!(merged.contains_key(&QueryItem::Key(k(5)))); @@ -669,7 +677,8 @@ fn merge_conditional_branches_theirs_extends_right_only() { subquery_path: Some(vec![k(20)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); assert_eq!(merged.len(), 2); assert!(merged.contains_key(&QueryItem::Range(k(3)..k(5)))); assert!(merged.contains_key(&QueryItem::Range(k(5)..k(7)))); @@ -694,7 +703,8 @@ fn merge_conditional_branches_theirs_extends_left_only() { subquery_path: Some(vec![k(20)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); assert_eq!(merged.len(), 2); assert!(merged.contains_key(&QueryItem::Range(k(3)..k(5)))); assert!(merged.contains_key(&QueryItem::Range(k(1)..k(3)))); @@ -719,7 +729,8 @@ fn merge_conditional_branches_no_overlap_appends() { subquery_path: Some(vec![k(20)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); assert_eq!(merged.len(), 2); assert!(merged.contains_key(&QueryItem::Range(k(1)..k(3)))); assert!(merged.contains_key(&QueryItem::Range(k(5)..k(7)))); @@ -751,7 +762,8 @@ fn merge_conditional_branches_incoming_spans_multiple_existing() { subquery_path: Some(vec![k(30)]), subquery: None, }, - ); + ) + .expect("no read modes involved"); // Should have entries for: the overlap with first range, the overlap with // second range, and the leftover pieces from the incoming range assert!(merged.len() >= 3); @@ -769,7 +781,7 @@ fn merge_multiple_preserves_add_parent_tree_on_subquery() { let mut q2 = Query::new_single_key(k(2)); q2.add_parent_tree_on_subquery = true; - let merged = Query::merge_multiple(vec![q1, q2]); + let merged = Query::merge_multiple(vec![q1, q2]).expect("merge must succeed"); assert!( merged.add_parent_tree_on_subquery, "merge_multiple should preserve add_parent_tree_on_subquery when any query sets it" @@ -784,7 +796,7 @@ fn merge_with_preserves_add_parent_tree_on_subquery() { let mut q2 = Query::new_single_key(k(2)); q2.add_parent_tree_on_subquery = true; - q1.merge_with(q2); + q1.merge_with(q2).expect("merge must succeed"); assert!( q1.add_parent_tree_on_subquery, "merge_with should preserve add_parent_tree_on_subquery when other query sets it" @@ -809,7 +821,7 @@ fn merge_with_both_have_conditional_branches() { Some(Query::new_single_key(k(200))), ); - q1.merge_with(q2); + q1.merge_with(q2).expect("merge must succeed"); assert!(q1.items.contains(&QueryItem::Key(k(1)))); assert!(q1.items.contains(&QueryItem::Key(k(2)))); let conds = q1.conditional_subquery_branches.as_ref().unwrap(); @@ -835,7 +847,7 @@ fn merge_with_overlapping_conditional_items_intersect() { Some(Query::new_single_key(k(200))), ); - q1.merge_with(q2); + q1.merge_with(q2).expect("merge must succeed"); // Items should be merged (ranges unioned) assert!(!q1.items.is_empty()); // Conditional branches should exist covering the overlapping and @@ -870,7 +882,7 @@ fn merge_multiple_overlapping_items_get_both_defaults() { subquery: Some(Box::new(Query::new_single_key(vec![b'b']))), }; - let merged = Query::merge_multiple(vec![query_a, query_b]); + let merged = Query::merge_multiple(vec![query_a, query_b]).expect("merge must succeed"); // All three keys should be present assert!(merged.items.contains(&QueryItem::Key(k(1)))); @@ -940,7 +952,7 @@ fn merge_multiple_non_overlapping_items_get_own_defaults() { subquery: Some(Box::new(Query::new_single_key(vec![b'b']))), }; - let merged = Query::merge_multiple(vec![query_a, query_b]); + let merged = Query::merge_multiple(vec![query_a, query_b]).expect("merge must succeed"); // Key(1): only in A → uses default "a" (no conditional needed) // Key(2): only in B → conditional "b" @@ -986,7 +998,7 @@ fn merge_with_overlapping_items_get_both_defaults() { subquery: Some(Box::new(Query::new_single_key(vec![b'b']))), }; - query_a.merge_with(query_b); + query_a.merge_with(query_b).expect("merge must succeed"); let conds = query_a .conditional_subquery_branches @@ -1040,7 +1052,7 @@ fn merge_multiple_three_queries_pairwise_overlaps() { subquery: Some(Box::new(Query::new_single_key(vec![b'c']))), }; - let merged = Query::merge_multiple(vec![qa, qb, qc]); + let merged = Query::merge_multiple(vec![qa, qb, qc]).expect("merge must succeed"); assert_eq!(merged.items.len(), 5); @@ -1136,7 +1148,7 @@ fn merge_multiple_overlapping_ranges_get_correct_defaults() { subquery: Some(Box::new(Query::new_single_key(vec![b'b']))), }; - let merged = Query::merge_multiple(vec![query_a, query_b]); + let merged = Query::merge_multiple(vec![query_a, query_b]).expect("merge must succeed"); let conds = merged .conditional_subquery_branches @@ -1197,7 +1209,7 @@ fn merge_multiple_existing_conditional_not_polluted_by_default() { subquery: Some(Box::new(Query::new_single_key(vec![b'b']))), }; - let merged = Query::merge_multiple(vec![query_a, query_b]); + let merged = Query::merge_multiple(vec![query_a, query_b]).expect("merge must succeed"); let conds = merged .conditional_subquery_branches @@ -1251,7 +1263,7 @@ fn merge_with_existing_conditional_not_polluted_by_default() { subquery: Some(Box::new(Query::new_single_key(vec![b'b']))), }; - query_a.merge_with(query_b); + query_a.merge_with(query_b).expect("merge must succeed"); let conds = query_a .conditional_subquery_branches @@ -1302,7 +1314,7 @@ fn merge_multiple_incoming_conditional_overrides_old_default() { Some(Query::new_single_key(vec![b'x'])), ); - let merged = Query::merge_multiple(vec![query_a, query_b]); + let merged = Query::merge_multiple(vec![query_a, query_b]).expect("merge must succeed"); let conds = merged .conditional_subquery_branches @@ -1362,7 +1374,7 @@ fn merge_multiple_one_empty_default_no_panic() { subquery: Some(Box::new(Query::new_single_key(vec![b'b']))), }; - let merged = Query::merge_multiple(vec![query_a, query_b]); + let merged = Query::merge_multiple(vec![query_a, query_b]).expect("merge must succeed"); // Key(2) overlaps, but A has empty default → Key(2) should just get B's default let conds = merged.conditional_subquery_branches.as_ref().unwrap(); @@ -1465,3 +1477,206 @@ fn query_item_merge_range_inclusive_with_range() { let merged = a.merge(&b); assert_eq!(merged, QueryItem::Range(k(1)..k(8))); } + +// ─────────────────────────────────────────────────────────────────────── +// Read modes in branch merges +// +// Branch merges combine subqueries field by field, and `read_mode` is +// not one of the combined fields — an unchecked merge silently drops +// it, answering key selection where the caller asked for an axis or +// sum-budget read. Every public branch-merge entry point must refuse +// instead. Both mode kinds are covered: they are separate enum +// variants, and a check that pattern-matched only one would pass a +// Axis-only suite while dropping sum-budget reads. +// ─────────────────────────────────────────────────────────────────────── + +fn axis_subquery() -> Query { + let mut query = Query::new_single_key(k(1)); + query.read_mode = Some(Box::new(ReadMode::Axis(AxisQuery::top_k( + IndexAxis::Sum, + 1, + 0, + true, + )))); + query +} + +fn sum_budget_subquery() -> Query { + let mut query = Query::new_single_key(k(1)); + query.read_mode = Some(Box::new(ReadMode::SumBudget(SumBudgetRead { + sum_limit: 100, + match_limit: None, + }))); + query +} + +fn branch_with(subquery: Query) -> SubqueryBranch { + SubqueryBranch { + subquery_path: None, + subquery: Some(Box::new(subquery)), + } +} + +fn plain_branch() -> SubqueryBranch { + branch_with(Query::new_single_key(k(2))) +} + +#[test] +fn subquery_branch_merge_refuses_read_modes_on_either_side() { + for mode in [axis_subquery(), sum_budget_subquery()] { + let moded = branch_with(mode); + + // Incoming side carries the mode. + match plain_branch().merge(&moded) { + Err(grovedb_query::error::Error::NotSupported(message)) => { + assert!(message.contains("read mode"), "got: {message}") + } + other => panic!("merging in a read-mode branch must be refused, got {other:?}"), + } + // Receiving side carries it — dropping either side is equally + // wrong, so the check is not one-sided. + match moded.merge(&plain_branch()) { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => panic!("merging onto a read-mode branch must be refused, got {other:?}"), + } + } + + // Two plain branches still merge, so the guard is not blanket. + plain_branch() + .merge(&plain_branch()) + .expect("plain branches still merge"); +} + +#[test] +fn subquery_branch_merge_refuses_nested_read_modes() { + // The mode is one level down, inside the branch subquery's own + // default branch — `has_read_mode_anywhere` must find it there too. + let mut nested = Query::new_single_key(k(3)); + nested.set_subquery(axis_subquery()); + match plain_branch().merge(&branch_with(nested)) { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => panic!("a nested read mode must be refused, got {other:?}"), + } +} + +#[test] +fn default_subquery_branch_merge_refuses_read_modes() { + for mode in [axis_subquery(), sum_budget_subquery()] { + // Incoming default branch carries the mode. + let mut target = Query::new_single_key(k(9)); + target.default_subquery_branch = plain_branch(); + match target.merge_default_subquery_branch(branch_with(mode.clone())) { + Err(grovedb_query::error::Error::NotSupported(message)) => { + assert!(message.contains("read mode"), "got: {message}") + } + other => panic!("merging in a read-mode default branch must be refused, got {other:?}"), + } + + // Existing default branch carries it. + let mut target = Query::new_single_key(k(9)); + target.default_subquery_branch = branch_with(mode); + match target.merge_default_subquery_branch(plain_branch()) { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => { + panic!("merging onto a read-mode default branch must be refused, got {other:?}") + } + } + } + + let mut target = Query::new_single_key(k(9)); + target.default_subquery_branch = plain_branch(); + target + .merge_default_subquery_branch(plain_branch()) + .expect("plain default branches still merge"); +} + +#[test] +fn conditional_subquery_branch_merges_refuse_read_modes() { + for mode in [axis_subquery(), sum_budget_subquery()] { + let mut target = Query::new_single_key(k(9)); + match target + .merge_conditional_boxed_subquery(QueryItem::Key(k(1)), branch_with(mode.clone())) + { + Err(grovedb_query::error::Error::NotSupported(message)) => { + assert!(message.contains("read mode"), "got: {message}") + } + other => panic!("a read-mode conditional branch must be refused, got {other:?}"), + } + + match Query::merge_conditional_subquery_branches_with_new_at_query_item( + None, + QueryItem::Key(k(1)), + branch_with(mode), + ) { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => panic!("a read-mode conditional branch must be refused, got {other:?}"), + } + } + + let mut target = Query::new_single_key(k(9)); + target + .merge_conditional_boxed_subquery(QueryItem::Key(k(1)), plain_branch()) + .expect("plain conditional branches still merge"); +} + +#[test] +fn conditional_merges_refuse_read_modes_on_the_existing_side_too() { + // Review finding: the guard only inspected the INCOMING branch. An + // overlapping merge splits both sides' branches and recombines them + // through the unchecked machinery, so an existing read mode can end + // up governing a different key range than the caller established. + // The boundary has to be total, matching the two-sided checks on + // `SubqueryBranch::merge` and `merge_default_subquery_branch`. + for mode in [axis_subquery(), sum_budget_subquery()] { + // Same key. + let mut target = Query::new_single_key(k(9)); + target.add_conditional_subquery(QueryItem::Key(k(1)), None, Some(mode.clone())); + match target.merge_conditional_boxed_subquery(QueryItem::Key(k(1)), plain_branch()) { + Err(grovedb_query::error::Error::NotSupported(message)) => { + assert!(message.contains("read mode"), "got: {message}") + } + other => panic!("an existing read-mode conditional must be refused, got {other:?}"), + } + + // Overlapping range — the shape that actually reaches + // `merge_subquery` -> `merge_with_unchecked`. + let mut target = Query::new_single_key(k(9)); + target.add_conditional_subquery(QueryItem::Range(k(1)..k(5)), None, Some(mode.clone())); + match target.merge_conditional_boxed_subquery(QueryItem::Range(k(3)..k(8)), plain_branch()) + { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => panic!("an overlapping read-mode conditional must be refused, got {other:?}"), + } + + // The associated-fn form takes the existing map as an argument, + // so it has to inspect that map rather than only the incoming + // branch. + let mut existing = IndexMap::new(); + existing.insert( + QueryItem::Key(k(1)), + SubqueryBranch { + subquery_path: None, + subquery: Some(Box::new(mode)), + }, + ); + match Query::merge_conditional_subquery_branches_with_new_at_query_item( + Some(existing), + QueryItem::Key(k(2)), + plain_branch(), + ) { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => panic!("an existing read-mode branch map must be refused, got {other:?}"), + } + } + + // Plain on both sides still merges, so the guard is not blanket. + let mut target = Query::new_single_key(k(9)); + target.add_conditional_subquery( + QueryItem::Key(k(1)), + None, + Some(Query::new_single_key(k(7))), + ); + target + .merge_conditional_boxed_subquery(QueryItem::Key(k(2)), plain_branch()) + .expect("plain conditional merges still work"); +} diff --git a/grovedb-query/tests/query_terminal_and_merge.rs b/grovedb-query/tests/query_terminal_and_merge.rs index b1bf66361..8898d168a 100644 --- a/grovedb-query/tests/query_terminal_and_merge.rs +++ b/grovedb-query/tests/query_terminal_and_merge.rs @@ -118,7 +118,7 @@ fn merge_apis_cover_default_and_conditional_paths() { other.insert_key(k(2)); other.set_subquery_path(vec![k(9)]); - base.merge_with(other); + base.merge_with(other).expect("merge must succeed"); assert_eq!(base.items.len(), 2); assert!(base.items.contains(&QueryItem::Key(k(1)))); assert!(base.items.contains(&QueryItem::Key(k(2)))); @@ -129,7 +129,7 @@ fn merge_apis_cover_default_and_conditional_paths() { .expect("conditional branch should be created"); assert!(conditional.contains_key(&QueryItem::Key(k(2)))); - let merged_empty = Query::merge_multiple(vec![]); + let merged_empty = Query::merge_multiple(vec![]).expect("merge must succeed"); assert!(merged_empty.items.is_empty()); let mut left = Query::new_single_key(k(10)); @@ -172,7 +172,8 @@ fn merge_conditional_subquery_branches_splits_intersections() { subquery_path: Some(vec![p(b"a")]), subquery: None, }, - ); + ) + .expect("no read modes involved"); assert_eq!(merged.len(), 3); assert!(merged.contains_key(&QueryItem::Range(k(1)..k(3)))); diff --git a/grovedb-version/src/version/grovedb_versions.rs b/grovedb-version/src/version/grovedb_versions.rs index ec276d02f..8107d0067 100644 --- a/grovedb-version/src/version/grovedb_versions.rs +++ b/grovedb-version/src/version/grovedb_versions.rs @@ -55,6 +55,25 @@ pub struct GroveDBPathQueryMethodVersions { pub unified_read_mode: FeatureVersion, } +/// Method versions for the standalone indexed-axis query family — the +/// per-axis reads and the echo-based proof envelopes +/// (`prove/verify_indexed_axis_*` and their per-axis wrappers). These +/// entry points predate this struct and shipped unversioned; the slots +/// exist so the first future divergence bumps a number instead of +/// forking behavior silently. The embedded (V1-envelope) axis shapes +/// are gated separately via +/// `GroveDBOperationsProofVersions::axis_descent_in_v1_envelope`. +#[derive(Clone, Debug, Default)] +pub struct GroveDBOperationsIndexedAxisVersions { + /// The trusted per-axis reads (`indexed_{count,sum,avg}_*`). + pub read: FeatureVersion, + /// The standalone single-path envelope provers + /// (`prove_indexed_axis_{top_k,top_k_paginated,query,rank_of_key,range_aggregate}`). + pub prove_single_path: FeatureVersion, + /// The matching standalone verifiers. + pub verify_single_path: FeatureVersion, +} + #[derive(Clone, Debug, Default)] pub struct GroveDBApplyBatchVersions { pub apply_batch_structure: FeatureVersion, @@ -119,6 +138,7 @@ pub struct GroveDBOperationsVersions { pub delete_up_tree: GroveDBOperationsDeleteUpTreeVersions, pub query: GroveDBOperationsQueryVersions, pub proof: GroveDBOperationsProofVersions, + pub indexed_axis: GroveDBOperationsIndexedAxisVersions, pub average_case: GroveDBOperationsAverageCaseVersions, pub worst_case: GroveDBOperationsWorstCaseVersions, } diff --git a/grovedb-version/src/version/v1.rs b/grovedb-version/src/version/v1.rs index 01cb8f295..eb5a0a9a9 100644 --- a/grovedb-version/src/version/v1.rs +++ b/grovedb-version/src/version/v1.rs @@ -4,8 +4,8 @@ use crate::version::{ GroveDBApplyBatchVersions, GroveDBElementMethodVersions, GroveDBOperationsAverageCaseVersions, GroveDBOperationsDeleteUpTreeVersions, GroveDBOperationsDeleteVersions, GroveDBOperationsGetVersions, - GroveDBOperationsInsertVersions, GroveDBOperationsProofVersions, - GroveDBOperationsQueryVersions, GroveDBOperationsVersions, + GroveDBOperationsIndexedAxisVersions, GroveDBOperationsInsertVersions, + GroveDBOperationsProofVersions, GroveDBOperationsQueryVersions, GroveDBOperationsVersions, GroveDBOperationsWorstCaseVersions, GroveDBPathQueryMethodVersions, GroveDBQueryLimits, GroveDBReplicationVersions, GroveDBVersions, }, @@ -150,6 +150,11 @@ pub const GROVE_V1: GroveVersion = GroveVersion { follow_element: 0, run_path_query: 0, }, + indexed_axis: GroveDBOperationsIndexedAxisVersions { + read: 0, + prove_single_path: 0, + verify_single_path: 0, + }, proof: GroveDBOperationsProofVersions { prove_query: 0, prove_query_many: 0, diff --git a/grovedb-version/src/version/v2.rs b/grovedb-version/src/version/v2.rs index 893ab59c8..8c92dac9e 100644 --- a/grovedb-version/src/version/v2.rs +++ b/grovedb-version/src/version/v2.rs @@ -4,8 +4,8 @@ use crate::version::{ GroveDBApplyBatchVersions, GroveDBElementMethodVersions, GroveDBOperationsAverageCaseVersions, GroveDBOperationsDeleteUpTreeVersions, GroveDBOperationsDeleteVersions, GroveDBOperationsGetVersions, - GroveDBOperationsInsertVersions, GroveDBOperationsProofVersions, - GroveDBOperationsQueryVersions, GroveDBOperationsVersions, + GroveDBOperationsIndexedAxisVersions, GroveDBOperationsInsertVersions, + GroveDBOperationsProofVersions, GroveDBOperationsQueryVersions, GroveDBOperationsVersions, GroveDBOperationsWorstCaseVersions, GroveDBPathQueryMethodVersions, GroveDBQueryLimits, GroveDBReplicationVersions, GroveDBVersions, }, @@ -150,6 +150,11 @@ pub const GROVE_V2: GroveVersion = GroveVersion { follow_element: 0, run_path_query: 0, }, + indexed_axis: GroveDBOperationsIndexedAxisVersions { + read: 0, + prove_single_path: 0, + verify_single_path: 0, + }, proof: GroveDBOperationsProofVersions { prove_query: 0, prove_query_many: 0, diff --git a/grovedb-version/src/version/v3.rs b/grovedb-version/src/version/v3.rs index 9874dbf86..220c619df 100644 --- a/grovedb-version/src/version/v3.rs +++ b/grovedb-version/src/version/v3.rs @@ -4,8 +4,8 @@ use crate::version::{ GroveDBApplyBatchVersions, GroveDBElementMethodVersions, GroveDBOperationsAverageCaseVersions, GroveDBOperationsDeleteUpTreeVersions, GroveDBOperationsDeleteVersions, GroveDBOperationsGetVersions, - GroveDBOperationsInsertVersions, GroveDBOperationsProofVersions, - GroveDBOperationsQueryVersions, GroveDBOperationsVersions, + GroveDBOperationsIndexedAxisVersions, GroveDBOperationsInsertVersions, + GroveDBOperationsProofVersions, GroveDBOperationsQueryVersions, GroveDBOperationsVersions, GroveDBOperationsWorstCaseVersions, GroveDBPathQueryMethodVersions, GroveDBQueryLimits, GroveDBReplicationVersions, GroveDBVersions, }, @@ -154,6 +154,11 @@ pub const GROVE_V3: GroveVersion = GroveVersion { follow_element: 0, run_path_query: 0, }, + indexed_axis: GroveDBOperationsIndexedAxisVersions { + read: 0, + prove_single_path: 0, + verify_single_path: 0, + }, proof: GroveDBOperationsProofVersions { prove_query: 0, prove_query_many: 0, diff --git a/grovedb-version/src/version/v4.rs b/grovedb-version/src/version/v4.rs index 9af6e579d..45caf8a22 100644 --- a/grovedb-version/src/version/v4.rs +++ b/grovedb-version/src/version/v4.rs @@ -47,6 +47,13 @@ //! proved elements. V1..V3 refuse the shape on both sides. Gated because //! it adds an acceptance rule to the live V1 envelope. //! +//! - `path_query_methods.merge: 1` — `PathQuery::merge` requires every input +//! to agree on `left_to_right` (typed error on conflict) and propagates the +//! shared direction to the merged root. V1..V3 keep the long-standing +//! silent behavior (input directions dropped; sub-level merges take the +//! synthesized default). Gated because merged queries feed proofs and both +//! sides must re-derive the identical merged query. +//! //! - `path_query_methods.unified_read_mode: 1` — `PathQuery` read modes //! (axis-ordered and sum-budget reads carried in `Query::read_mode`) are //! served by the unified dispatch (`run_path_query`, and the unified @@ -77,8 +84,8 @@ use crate::version::{ GroveDBApplyBatchVersions, GroveDBElementMethodVersions, GroveDBOperationsAverageCaseVersions, GroveDBOperationsDeleteUpTreeVersions, GroveDBOperationsDeleteVersions, GroveDBOperationsGetVersions, - GroveDBOperationsInsertVersions, GroveDBOperationsProofVersions, - GroveDBOperationsQueryVersions, GroveDBOperationsVersions, + GroveDBOperationsIndexedAxisVersions, GroveDBOperationsInsertVersions, + GroveDBOperationsProofVersions, GroveDBOperationsQueryVersions, GroveDBOperationsVersions, GroveDBOperationsWorstCaseVersions, GroveDBPathQueryMethodVersions, GroveDBQueryLimits, GroveDBReplicationVersions, GroveDBVersions, }, @@ -227,6 +234,11 @@ pub const GROVE_V4: GroveVersion = GroveVersion { follow_element: 0, run_path_query: 0, }, + indexed_axis: GroveDBOperationsIndexedAxisVersions { + read: 0, + prove_single_path: 0, + verify_single_path: 0, + }, proof: GroveDBOperationsProofVersions { prove_query: 0, prove_query_many: 0, @@ -281,7 +293,7 @@ pub const GROVE_V4: GroveVersion = GroveVersion { aggregate_sum_path_query_methods: GroveDBAggregateSumPathQueryMethodVersions { merge: 0 }, path_query_methods: GroveDBPathQueryMethodVersions { terminal_keys: 0, - merge: 0, + merge: 1, // direction-aware merge: agreement required and propagated (V4+) query_items_at_path: 0, should_add_parent_tree_at_path: 0, unified_read_mode: 1, diff --git a/grovedb/src/operations/indexed_tree.rs b/grovedb/src/operations/indexed_tree.rs index 1da2cc91c..9261867c1 100644 --- a/grovedb/src/operations/indexed_tree.rs +++ b/grovedb/src/operations/indexed_tree.rs @@ -1260,6 +1260,10 @@ impl GroveDb { where B: AsRef<[u8]> + 'b, { + grovedb_version::check_grovedb_v0_with_cost!( + "indexed_axis_top_k_generic", + grove_version.grovedb_versions.operations.indexed_axis.read + ); let mut cost = OperationCost::default(); let tx = TxRef::new(&self.db, transaction); let tx_ref = tx.as_ref(); @@ -1308,6 +1312,10 @@ impl GroveDb { where B: AsRef<[u8]> + 'b, { + grovedb_version::check_grovedb_v0_with_cost!( + "indexed_axis_top_k_paginated_generic", + grove_version.grovedb_versions.operations.indexed_axis.read + ); let mut cost = OperationCost::default(); let tx = TxRef::new(&self.db, transaction); let tx_ref = tx.as_ref(); @@ -1378,6 +1386,10 @@ impl GroveDb { where B: AsRef<[u8]> + 'b, { + grovedb_version::check_grovedb_v0_with_cost!( + "indexed_axis_range_generic", + grove_version.grovedb_versions.operations.indexed_axis.read + ); let mut cost = OperationCost::default(); let tx = TxRef::new(&self.db, transaction); let tx_ref = tx.as_ref(); @@ -1513,6 +1525,14 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + // The version gate has to precede the degenerate-range + // fast path below: returning the empty answer first would + // leave inverted bounds outside the version contract that + // every other input to this entry point is held to. + grovedb_version::check_grovedb_v0_with_cost!( + "indexed_count_range", + grove_version.grovedb_versions.operations.indexed_axis.read + ); let cost = OperationCost::default(); if lo_count > hi_count { return Ok(Vec::new()).wrap_with_cost(cost); @@ -1583,6 +1603,10 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + grovedb_version::check_grovedb_v0_with_cost!( + "indexed_count_range_aggregate", + grove_version.grovedb_versions.operations.indexed_axis.read + ); use grovedb_merk::proofs::query::QueryItem as MerkQueryItemForRange; let mut cost = OperationCost::default(); @@ -1707,6 +1731,14 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + // The version gate has to precede the degenerate-range + // fast path below: returning the empty answer first would + // leave inverted bounds outside the version contract that + // every other input to this entry point is held to. + grovedb_version::check_grovedb_v0_with_cost!( + "indexed_sum_range", + grove_version.grovedb_versions.operations.indexed_axis.read + ); let cost = OperationCost::default(); if lo_sum > hi_sum { return Ok(Vec::new()).wrap_with_cost(cost); @@ -1769,6 +1801,10 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + grovedb_version::check_grovedb_v0_with_cost!( + "indexed_sum_range_aggregate", + grove_version.grovedb_versions.operations.indexed_axis.read + ); use grovedb_merk::proofs::query::QueryItem as MerkQueryItemForRange; let mut cost = OperationCost::default(); @@ -1901,6 +1937,14 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + // The version gate has to precede the degenerate-range + // fast path below: returning the empty answer first would + // leave inverted bounds outside the version contract that + // every other input to this entry point is held to. + grovedb_version::check_grovedb_v0_with_cost!( + "indexed_avg_range", + grove_version.grovedb_versions.operations.indexed_axis.read + ); let cost = OperationCost::default(); if lo_avg > hi_avg { return Ok(Vec::new()).wrap_with_cost(cost); diff --git a/grovedb/src/operations/proof/indexed_axis/axis_api.rs b/grovedb/src/operations/proof/indexed_axis/axis_api.rs index 3fad8e904..23f695db6 100644 --- a/grovedb/src/operations/proof/indexed_axis/axis_api.rs +++ b/grovedb/src/operations/proof/indexed_axis/axis_api.rs @@ -9,7 +9,6 @@ use grovedb_element::indexed::IndexAxis; use grovedb_merk::proofs::Query as MerkQuery; #[cfg(feature = "minimal")] use grovedb_path::SubtreePath; -#[cfg(feature = "minimal")] use grovedb_version::version::GroveVersion; #[cfg(feature = "minimal")] @@ -127,6 +126,7 @@ impl GroveDb { path: &[&[u8]], expected_k: u16, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_top_k( proof_bytes, @@ -134,6 +134,7 @@ impl GroveDb { IndexAxis::Count, expected_k, expected_descending, + grove_version, ) } @@ -144,6 +145,7 @@ impl GroveDb { expected_k: u16, expected_offset: u64, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_top_k_paginated( proof_bytes, @@ -152,6 +154,7 @@ impl GroveDb { expected_k, expected_offset, expected_descending, + grove_version, ) } @@ -161,6 +164,7 @@ impl GroveDb { path: &[&[u8]], secondary_query: MerkQuery, expected_limit: Option, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_query( proof_bytes, @@ -168,6 +172,7 @@ impl GroveDb { IndexAxis::Count, secondary_query, expected_limit, + grove_version, ) } @@ -177,6 +182,7 @@ impl GroveDb { path: &[&[u8]], expected_lo_count: u64, expected_hi_count: u64, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_range_aggregate( proof_bytes, @@ -184,6 +190,7 @@ impl GroveDb { IndexAxis::Count, expected_lo_count as i128, expected_hi_count as i128, + grove_version, ) } @@ -297,6 +304,7 @@ impl GroveDb { path: &[&[u8]], expected_k: u16, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_top_k( proof_bytes, @@ -304,6 +312,7 @@ impl GroveDb { IndexAxis::Sum, expected_k, expected_descending, + grove_version, ) } @@ -314,6 +323,7 @@ impl GroveDb { expected_k: u16, expected_offset: u64, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_top_k_paginated( proof_bytes, @@ -322,6 +332,7 @@ impl GroveDb { expected_k, expected_offset, expected_descending, + grove_version, ) } @@ -331,6 +342,7 @@ impl GroveDb { path: &[&[u8]], secondary_query: MerkQuery, expected_limit: Option, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_query( proof_bytes, @@ -338,6 +350,7 @@ impl GroveDb { IndexAxis::Sum, secondary_query, expected_limit, + grove_version, ) } @@ -347,6 +360,7 @@ impl GroveDb { path: &[&[u8]], expected_lo_sum: i64, expected_hi_sum: i64, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_range_aggregate( proof_bytes, @@ -354,6 +368,7 @@ impl GroveDb { IndexAxis::Sum, expected_lo_sum as i128, expected_hi_sum as i128, + grove_version, ) } @@ -441,6 +456,7 @@ impl GroveDb { path: &[&[u8]], expected_k: u16, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_top_k( proof_bytes, @@ -448,6 +464,7 @@ impl GroveDb { IndexAxis::Avg, expected_k, expected_descending, + grove_version, ) } @@ -458,6 +475,7 @@ impl GroveDb { expected_k: u16, expected_offset: u64, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_top_k_paginated( proof_bytes, @@ -466,6 +484,7 @@ impl GroveDb { expected_k, expected_offset, expected_descending, + grove_version, ) } @@ -475,6 +494,7 @@ impl GroveDb { path: &[&[u8]], secondary_query: MerkQuery, expected_limit: Option, + grove_version: &GroveVersion, ) -> Result { Self::verify_indexed_axis_query( proof_bytes, @@ -482,6 +502,7 @@ impl GroveDb { IndexAxis::Avg, secondary_query, expected_limit, + grove_version, ) } } diff --git a/grovedb/src/operations/proof/indexed_axis/generate.rs b/grovedb/src/operations/proof/indexed_axis/generate.rs index ff08ea4a4..6024a2f9c 100644 --- a/grovedb/src/operations/proof/indexed_axis/generate.rs +++ b/grovedb/src/operations/proof/indexed_axis/generate.rs @@ -339,6 +339,14 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + grovedb_version::check_grovedb_v0_with_cost!( + "prove_indexed_axis_top_k", + grove_version + .grovedb_versions + .operations + .indexed_axis + .prove_single_path + ); let mut full_range = MerkQuery::new(); full_range.insert_all(); full_range.left_to_right = !descending; @@ -362,6 +370,14 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + grovedb_version::check_grovedb_v0_with_cost!( + "prove_indexed_axis_query", + grove_version + .grovedb_versions + .operations + .indexed_axis + .prove_single_path + ); let mut cost = OperationCost::default(); let path: SubtreePath = path.into(); let batch = StorageBatch::new(); @@ -427,6 +443,14 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + grovedb_version::check_grovedb_v0_with_cost!( + "prove_indexed_axis_top_k_paginated", + grove_version + .grovedb_versions + .operations + .indexed_axis + .prove_single_path + ); let mut cost = OperationCost::default(); let path: SubtreePath = path.into(); let batch = StorageBatch::new(); @@ -601,6 +625,14 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + grovedb_version::check_grovedb_v0_with_cost!( + "prove_indexed_axis_rank_of_key", + grove_version + .grovedb_versions + .operations + .indexed_axis + .prove_single_path + ); let mut cost = OperationCost::default(); let path: SubtreePath = path.into(); @@ -674,6 +706,14 @@ impl GroveDb { B: AsRef<[u8]> + 'b, P: Into>, { + grovedb_version::check_grovedb_v0_with_cost!( + "prove_indexed_axis_range_aggregate", + grove_version + .grovedb_versions + .operations + .indexed_axis + .prove_single_path + ); let mut cost = OperationCost::default(); match axis { IndexAxis::Count | IndexAxis::Sum => {} diff --git a/grovedb/src/operations/proof/indexed_axis/verify.rs b/grovedb/src/operations/proof/indexed_axis/verify.rs index d7aadaa6c..07439a1c5 100644 --- a/grovedb/src/operations/proof/indexed_axis/verify.rs +++ b/grovedb/src/operations/proof/indexed_axis/verify.rs @@ -22,6 +22,7 @@ use grovedb_merk::{ tree::{axes_digest, combine_hash, combine_hash_three, value_hash, CryptoHash}, }; use grovedb_query::QueryItem as MerkQueryItem; +use grovedb_version::{check_grovedb_v0, version::GroveVersion}; use crate::{Error, GroveDb}; @@ -304,7 +305,16 @@ impl GroveDb { expected_axis: IndexAxis, expected_k: u16, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { + check_grovedb_v0!( + "verify_indexed_axis_top_k", + grove_version + .grovedb_versions + .operations + .indexed_axis + .verify_single_path + ); let envelope = decode_range_envelope(proof_bytes)?; if envelope.axis_tag != expected_axis.tag() { return Err(Error::CorruptedData(format!( @@ -345,7 +355,16 @@ impl GroveDb { expected_axis: IndexAxis, secondary_query: MerkQuery, expected_limit: Option, + grove_version: &GroveVersion, ) -> Result { + check_grovedb_v0!( + "verify_indexed_axis_query", + grove_version + .grovedb_versions + .operations + .indexed_axis + .verify_single_path + ); let envelope = decode_range_envelope(proof_bytes)?; if envelope.axis_tag != expected_axis.tag() { return Err(Error::CorruptedData(format!( @@ -381,7 +400,16 @@ impl GroveDb { expected_k: u16, expected_offset: u64, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { + check_grovedb_v0!( + "verify_indexed_axis_top_k_paginated", + grove_version + .grovedb_versions + .operations + .indexed_axis + .verify_single_path + ); let config = bincode::config::standard().with_limit::<{ 16 * 1024 * 1024 }>(); let (envelope, consumed): (IndexedAxisPaginatedProof, _) = bincode::decode_from_slice(proof_bytes, config).map_err(|e| { @@ -444,6 +472,7 @@ impl GroveDb { item_key: &[u8], expected_rank: u64, expected_descending: bool, + grove_version: &GroveVersion, ) -> Result { let result = Self::verify_indexed_axis_top_k_paginated( proof_bytes, @@ -452,6 +481,7 @@ impl GroveDb { 1, expected_rank, expected_descending, + grove_version, )?; if result.skipped != expected_rank { return Err(Error::CorruptedData(format!( @@ -490,7 +520,16 @@ impl GroveDb { expected_axis: IndexAxis, expected_lo: i128, expected_hi: i128, + grove_version: &GroveVersion, ) -> Result { + check_grovedb_v0!( + "verify_indexed_axis_range_aggregate", + grove_version + .grovedb_versions + .operations + .indexed_axis + .verify_single_path + ); let config = bincode::config::standard().with_limit::<{ 16 * 1024 * 1024 }>(); let (envelope, consumed): (IndexedAxisAggregateProof, _) = bincode::decode_from_slice(proof_bytes, config).map_err(|e| { diff --git a/grovedb/src/query/mod.rs b/grovedb/src/query/mod.rs index 36e50a5df..027c5f222 100644 --- a/grovedb/src/query/mod.rs +++ b/grovedb/src/query/mod.rs @@ -938,10 +938,16 @@ impl PathQuery { mut path_queries: Vec<&PathQuery>, grove_version: &GroveVersion, ) -> Result { - check_grovedb_v0!( - "merge", - grove_version.grovedb_versions.path_query_methods.merge - ); + let merge_version = grove_version.grovedb_versions.path_query_methods.merge; + if merge_version > 1 { + return Err(Error::VersionError( + grovedb_version::error::GroveVersionError::UnknownVersionMismatch { + method: "merge".to_string(), + known_versions: vec![0, 1], + received: merge_version, + }, + )); + } if path_queries.is_empty() { return Err(Error::InvalidInput( "merge function requires at least 1 path query", @@ -965,6 +971,27 @@ impl PathQuery { return Ok(path_queries.remove(0).clone()); } + // Direction handling, version-gated. `merge` slot 0 (V1..V3) + // keeps the long-standing behavior: input directions are + // silently dropped (sub-level inputs end up under a synthesized + // root whose direction is the default). Slot 1 (V4+) requires + // every input to agree and propagates the shared direction to + // the merged root. Merged queries feed proofs and the verifier + // re-runs the same merge with the same grove version, so both + // sides stay in agreement at every version. + let shared_direction = path_queries[0].query.query.left_to_right; + if merge_version >= 1 + && path_queries + .iter() + .any(|path_query| path_query.query.query.left_to_right != shared_direction) + { + return Err(Error::NotSupported( + "can not merge path queries with conflicting directions (left_to_right \ + differs); align the directions before merging" + .to_string(), + )); + } + let (common_path, next_index) = PathQuery::get_common_path(&path_queries); let mut queries_for_common_path_this_level: Vec = vec![]; @@ -1003,7 +1030,19 @@ impl PathQuery { }) })?; - let mut merged_query = Query::merge_multiple(queries_for_common_path_this_level); + // Version-gated direction handling. The `merge` slot's `0` + // (V1..V3) keeps the long-standing silent first-wins behavior; + // `1` (V4+) requires every merged query to agree on + // `left_to_right` and propagates it, erroring on conflict — + // merged queries feed proofs, and the verifier re-runs the same + // merge with the same grove version, so both sides stay in + // agreement at every version. + let mut merged_query = match merge_version { + 0 => Query::merge_multiple(queries_for_common_path_this_level) + .map_err(|e| Error::NotSupported(e.to_string()))?, + _ => Query::merge_multiple_directional(queries_for_common_path_this_level) + .map_err(|e| Error::NotSupported(e.to_string()))?, + }; // add conditional subqueries for sub_path_query in queries_for_common_path_sub_level { let SubqueryBranch { @@ -1023,7 +1062,21 @@ impl PathQuery { subquery_path: rest_of_path, subquery, }; - merged_query.merge_conditional_boxed_subquery(QueryItem::Key(key), subquery_branch); + // The read-mode gate at the top of `merge` already rejected + // any input carrying one, so this cannot fire today — + // propagate rather than discard, so a future path that + // reaches here with a read mode surfaces it instead of + // silently dropping the mode. + merged_query + .merge_conditional_boxed_subquery(QueryItem::Key(key), subquery_branch) + .map_err(|e| Error::NotSupported(e.to_string()))?; + } + + // V4+: the agreed direction travels to the merged root (it + // would otherwise be lost whenever the inputs land at a sub + // level under a synthesized root query). + if merge_version >= 1 { + merged_query.left_to_right = shared_direction; } Ok(PathQuery::new_unsized(common_path, merged_query)) diff --git a/grovedb/src/tests/axis_descent_proof_tests.rs b/grovedb/src/tests/axis_descent_proof_tests.rs index 4c77b2f82..49bd4d2ce 100644 --- a/grovedb/src/tests/axis_descent_proof_tests.rs +++ b/grovedb/src/tests/axis_descent_proof_tests.rs @@ -189,9 +189,14 @@ mod tests { .prove_indexed_sum_top_k([TEST_LEAF, b"psit"].as_ref(), 3, true, None, grove_version) .unwrap() .expect("standalone prove"); - let standalone = - GroveDb::verify_indexed_sum_top_k(&standalone_bytes, &[TEST_LEAF, b"psit"], 3, true) - .expect("standalone verify"); + let standalone = GroveDb::verify_indexed_sum_top_k( + &standalone_bytes, + &[TEST_LEAF, b"psit"], + 3, + true, + grove_version, + ) + .expect("standalone verify"); assert_eq!(standalone.root_hash, verified_root); assert_eq!( entries_as_sum(&standalone.entries), @@ -961,6 +966,7 @@ mod tests { b"alice", standalone_rank, true, + grove_version, ) .expect("standalone avg rank verify"); assert_eq!(rank, standalone_rank); diff --git a/grovedb/src/tests/coverage_round7_tests.rs b/grovedb/src/tests/coverage_round7_tests.rs index edb2797a3..332a7ed25 100644 --- a/grovedb/src/tests/coverage_round7_tests.rs +++ b/grovedb/src/tests/coverage_round7_tests.rs @@ -470,7 +470,7 @@ mod tests { envelope.ancestor_attestations.clear(); let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("ancestor_attestations")), "expected ancestor_attestations length mismatch, got {:?}", @@ -497,7 +497,7 @@ mod tests { // other_axes_root_hashes must be empty in that case. Inject one. envelope.other_axes_root_hashes = vec![(1, [0u8; 32])]; let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("other_axes_root_hashes")), "expected non-PCPSIT-with-other-axes rejection, got {:?}", @@ -532,7 +532,7 @@ mod tests { .other_axes_root_hashes .push((IndexAxis::Count.tag(), [0u8; 32])); let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("duplicate") || msg.contains("unsorted")), "expected duplicate-tag rejection, got {:?}", @@ -558,7 +558,7 @@ mod tests { bincode::decode_from_slice(&proof_bytes, config).expect("decode"); envelope.primary_root_hash[0] ^= 0xFF; let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!( matches!(result, Err(Error::CorruptedData(_))), "expected primary-hash tamper rejection, got {:?}", @@ -685,8 +685,14 @@ mod tests { .expect("prove"); // verify_indexed_axis_range_aggregate with Avg must reject // before doing any envelope arithmetic. - let result = - GroveDb::verify_indexed_axis_range_aggregate(&proof_bytes, path, IndexAxis::Avg, 0, 10); + let result = GroveDb::verify_indexed_axis_range_aggregate( + &proof_bytes, + path, + IndexAxis::Avg, + 0, + 10, + grove_version, + ); // Either axis-mismatch (envelope tag=count, expected=avg) or // not-supported-for-avg. The first one fires. assert!(matches!( @@ -708,7 +714,8 @@ mod tests { .unwrap() .expect("prove"); // Expect k=7 instead of 1. - let result = GroveDb::verify_indexed_count_top_k(&proof_bytes, path, 7, true); + let result = + GroveDb::verify_indexed_count_top_k(&proof_bytes, path, 7, true, grove_version); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("limit")), "expected limit mismatch, got {:?}", @@ -729,7 +736,8 @@ mod tests { .unwrap() .expect("prove"); // Expected hi=99 but envelope carries 10. - let result = GroveDb::verify_indexed_count_range_aggregate(&proof_bytes, path, 0, 99); + let result = + GroveDb::verify_indexed_count_range_aggregate(&proof_bytes, path, 0, 99, grove_version); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("hi")), "expected hi mismatch, got {:?}", @@ -755,7 +763,14 @@ mod tests { .unwrap() .expect("prove"); // Expected k=5 vs envelope 2. - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof_bytes, path, 5, 0, true); + let result = GroveDb::verify_indexed_count_top_k_paginated( + &proof_bytes, + path, + 5, + 0, + true, + grove_version, + ); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("k") || msg.contains("limit")), "expected k mismatch, got {:?}", @@ -785,7 +800,8 @@ mod tests { // enforces that layer_proofs.len() != path.len() OR layer_proofs // is empty. The "empty" arm runs. let path: &[&[u8]] = &[]; - let result = GroveDb::verify_indexed_count_top_k(&bytes, path, 1, true); + let result = + GroveDb::verify_indexed_count_top_k(&bytes, path, 1, true, GroveVersion::latest()); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -807,7 +823,14 @@ mod tests { let config = bincode::config::standard().with_limit::<{ 16 * 1024 * 1024 }>(); let bytes = bincode::encode_to_vec(&envelope, config).expect("encode"); let path: &[&[u8]] = &[]; - let result = GroveDb::verify_indexed_count_top_k_paginated(&bytes, path, 1, 0, true); + let result = GroveDb::verify_indexed_count_top_k_paginated( + &bytes, + path, + 1, + 0, + true, + GroveVersion::latest(), + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -828,7 +851,13 @@ mod tests { let config = bincode::config::standard().with_limit::<{ 16 * 1024 * 1024 }>(); let bytes = bincode::encode_to_vec(&envelope, config).expect("encode"); let path: &[&[u8]] = &[]; - let result = GroveDb::verify_indexed_count_range_aggregate(&bytes, path, 0, 10); + let result = GroveDb::verify_indexed_count_range_aggregate( + &bytes, + path, + 0, + 10, + GroveVersion::latest(), + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -850,7 +879,7 @@ mod tests { // Insert a bogus extra layer so len(layer_proofs) != len(path). envelope.layer_proofs.push(vec![0u8; 16]); let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("layers")), "expected layer-count mismatch, got {:?}", @@ -874,7 +903,14 @@ mod tests { bincode::decode_from_slice(&proof_bytes, config).expect("decode"); envelope.layer_proofs.push(vec![0u8; 16]); let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&tampered, path, 1, 0, true); + let result = GroveDb::verify_indexed_count_top_k_paginated( + &tampered, + path, + 1, + 0, + true, + grove_version, + ); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("layers")), "expected layer-count mismatch, got {:?}", @@ -898,7 +934,8 @@ mod tests { bincode::decode_from_slice(&proof_bytes, config).expect("decode"); envelope.layer_proofs.push(vec![0u8; 16]); let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_range_aggregate(&tampered, path, 0, 10); + let result = + GroveDb::verify_indexed_count_range_aggregate(&tampered, path, 0, 10, grove_version); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("layers")), "expected layer-count mismatch, got {:?}", @@ -920,8 +957,14 @@ mod tests { .unwrap() .expect("prove"); // Call with axis=Sum on a count-axis envelope. - let result = - GroveDb::verify_indexed_axis_range_aggregate(&proof_bytes, path, IndexAxis::Sum, 0, 10); + let result = GroveDb::verify_indexed_axis_range_aggregate( + &proof_bytes, + path, + IndexAxis::Sum, + 0, + 10, + grove_version, + ); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("axis")), "expected axis mismatch, got {:?}", @@ -947,6 +990,7 @@ mod tests { 1, 0, true, + grove_version, ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -963,7 +1007,14 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 1, 0, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof_bytes, path, 1, 0, false); + let result = GroveDb::verify_indexed_count_top_k_paginated( + &proof_bytes, + path, + 1, + 0, + false, + grove_version, + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -981,8 +1032,14 @@ mod tests { .unwrap() .expect("prove"); // Call verify_indexed_axis_query with axis=Sum. - let result = - GroveDb::verify_indexed_axis_query(&proof_bytes, path, IndexAxis::Sum, q, Some(5)); + let result = GroveDb::verify_indexed_axis_query( + &proof_bytes, + path, + IndexAxis::Sum, + q, + Some(5), + grove_version, + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1005,7 +1062,8 @@ mod tests { let mut bad_q = MerkQuery::new(); bad_q.insert_all(); bad_q.left_to_right = false; - let result = GroveDb::verify_indexed_count_query(&proof_bytes, path, bad_q, Some(5)); + let result = + GroveDb::verify_indexed_count_query(&proof_bytes, path, bad_q, Some(5), grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1022,7 +1080,8 @@ mod tests { .prove_indexed_count_query(path, q.clone(), Some(5), None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_query(&proof_bytes, path, q, Some(99)); + let result = + GroveDb::verify_indexed_count_query(&proof_bytes, path, q, Some(99), grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1037,7 +1096,13 @@ mod tests { .prove_indexed_count_range_aggregate(path, 5, 20, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_range_aggregate(&proof_bytes, path, 99, 20); + let result = GroveDb::verify_indexed_count_range_aggregate( + &proof_bytes, + path, + 99, + 20, + grove_version, + ); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("lo")), "expected lo mismatch, got {:?}", @@ -1097,7 +1162,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_top_k(&proof_bytes, path, 2, true).expect("verify"); + GroveDb::verify_indexed_count_top_k(&proof_bytes, path, 2, true, grove_version) + .expect("verify"); let entries = match &result.entries { AxisEntries::Count(v) => v.as_slice(), other => panic!("expected count entries, got {:?}", other), @@ -1147,7 +1213,7 @@ mod tests { ]); } let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1174,7 +1240,7 @@ mod tests { *att = AncestorAttestation::SingleSecondary([3u8; 32]); } let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1185,7 +1251,14 @@ mod tests { #[test] fn verify_indexed_axis_paginated_rejects_truncated_buffer() { let path: &[&[u8]] = &[TEST_LEAF, b"x"]; - let result = GroveDb::verify_indexed_count_top_k_paginated(&[0u8; 4], path, 1, 0, true); + let result = GroveDb::verify_indexed_count_top_k_paginated( + &[0u8; 4], + path, + 1, + 0, + true, + GroveVersion::latest(), + ); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("decoding")), "expected decoding error, got {:?}", @@ -1196,7 +1269,13 @@ mod tests { #[test] fn verify_indexed_axis_aggregate_rejects_truncated_buffer() { let path: &[&[u8]] = &[TEST_LEAF, b"x"]; - let result = GroveDb::verify_indexed_count_range_aggregate(&[0u8; 4], path, 0, 10); + let result = GroveDb::verify_indexed_count_range_aggregate( + &[0u8; 4], + path, + 0, + 10, + GroveVersion::latest(), + ); assert!( matches!(&result, Err(Error::CorruptedData(msg)) if msg.contains("decoding")), "expected decoding error, got {:?}", @@ -1207,7 +1286,8 @@ mod tests { #[test] fn verify_indexed_axis_range_rejects_truncated_buffer() { let path: &[&[u8]] = &[TEST_LEAF, b"x"]; - let result = GroveDb::verify_indexed_count_top_k(&[0u8; 4], path, 1, true); + let result = + GroveDb::verify_indexed_count_top_k(&[0u8; 4], path, 1, true, GroveVersion::latest()); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1233,7 +1313,7 @@ mod tests { *b ^= 0xFF; } let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1286,7 +1366,7 @@ mod tests { "expected at least one SingleSecondary attestation" ); let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&tampered, path, 1, true, grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1314,7 +1394,8 @@ mod tests { bincode::decode_from_slice(&proof_bytes, config).expect("decode"); envelope.primary_root_hash[10] ^= 0x55; let tampered = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let result = GroveDb::verify_indexed_count_range_aggregate(&tampered, path, 0, 10); + let result = + GroveDb::verify_indexed_count_range_aggregate(&tampered, path, 0, 10, grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1805,7 +1886,8 @@ mod tests { .prove_indexed_count_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); let entries = match &result.entries { AxisEntries::Count(v) => v.as_slice(), _ => panic!("expected count"), @@ -1831,7 +1913,8 @@ mod tests { .prove_indexed_sum_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); let entries = match &result.entries { AxisEntries::Sum(v) => v.as_slice(), _ => panic!("expected sum"), @@ -1858,7 +1941,8 @@ mod tests { .prove_indexed_avg_top_k(path, 2, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_avg_top_k(&proof, path, 2, true).expect("verify"); + let result = GroveDb::verify_indexed_avg_top_k(&proof, path, 2, true, grove_version) + .expect("verify"); let entries = match &result.entries { AxisEntries::Avg(v) => v.as_slice(), _ => panic!("expected avg"), @@ -1879,7 +1963,8 @@ mod tests { .prove_indexed_count_top_k(path, 10, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 10, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 10, true, grove_version) + .expect("verify"); let entries = match &result.entries { AxisEntries::Count(v) => v.as_slice(), _ => panic!("expected count"), @@ -1899,8 +1984,9 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 2, 100, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 2, 100, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 2, 100, true, grove_version) + .expect("verify"); let entries = match &result.entries { AxisEntries::Sum(v) => v.as_slice(), _ => panic!("expected sum"), @@ -2033,8 +2119,9 @@ mod tests { .prove_indexed_count_range_aggregate(path, 1, 10, None, grove_version) .unwrap() .expect("prove count agg"); - let result = GroveDb::verify_indexed_count_range_aggregate(&proof, path, 1, 10) - .expect("verify count agg"); + let result = + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 1, 10, grove_version) + .expect("verify count agg"); // Each entry contributes count=1, so range [1,10] returns 3. assert_eq!(result.aggregate, 3); assert_eq!(result.axis, IndexAxis::Count); @@ -2057,8 +2144,9 @@ mod tests { .prove_indexed_sum_range_aggregate(path, -100, 100, None, grove_version) .unwrap() .expect("prove sum agg"); - let result = GroveDb::verify_indexed_sum_range_aggregate(&proof, path, -100, 100) - .expect("verify sum agg"); + let result = + GroveDb::verify_indexed_sum_range_aggregate(&proof, path, -100, 100, grove_version) + .expect("verify sum agg"); // Sum = 5 + 10 - 3 = 12. assert_eq!(result.aggregate, 12); assert_eq!(result.axis, IndexAxis::Sum); @@ -2080,8 +2168,9 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 2, 1, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 1, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 1, true, grove_version) + .expect("verify"); let entries = match &result.entries { AxisEntries::Count(v) => v.as_slice(), _ => panic!("expected count"), @@ -2111,7 +2200,8 @@ mod tests { .prove_indexed_sum_query(path, q.clone(), Some(2), None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_query(&proof, path, q, Some(2)).expect("verify"); + let result = GroveDb::verify_indexed_sum_query(&proof, path, q, Some(2), grove_version) + .expect("verify"); let entries = match &result.entries { AxisEntries::Sum(v) => v.as_slice(), _ => panic!("expected sum"), diff --git a/grovedb/src/tests/indexed_axis_nested_and_bounds_tests.rs b/grovedb/src/tests/indexed_axis_nested_and_bounds_tests.rs index 7d85bdaf1..e9c6b0d76 100644 --- a/grovedb/src/tests/indexed_axis_nested_and_bounds_tests.rs +++ b/grovedb/src/tests/indexed_axis_nested_and_bounds_tests.rs @@ -129,8 +129,15 @@ mod tests { envelope.ancestor_attestations ); - let result = GroveDb::verify_indexed_axis_top_k(&proof, path, IndexAxis::Count, 5, true) - .expect("verify through an indexed ancestor"); + let result = GroveDb::verify_indexed_axis_top_k( + &proof, + path, + IndexAxis::Count, + 5, + true, + GroveVersion::latest(), + ) + .expect("verify through an indexed ancestor"); assert_eq!( result.root_hash, db.root_hash(None, gv).unwrap().expect("root hash"), @@ -217,8 +224,15 @@ mod tests { envelope.ancestor_attestations ); - let result = GroveDb::verify_indexed_axis_top_k(&proof, path, IndexAxis::Sum, 5, true) - .expect("verify through a sum-axis indexed ancestor"); + let result = GroveDb::verify_indexed_axis_top_k( + &proof, + path, + IndexAxis::Sum, + 5, + true, + GroveVersion::latest(), + ) + .expect("verify through a sum-axis indexed ancestor"); assert_eq!( result.root_hash, db.root_hash(None, gv).unwrap().expect("root hash"), @@ -310,8 +324,15 @@ mod tests { other => panic!("expected [NotIndexed, MultiAxis], got {other:?}"), } - let result = GroveDb::verify_indexed_axis_top_k(&proof, path, IndexAxis::Count, 5, true) - .expect("verify through a multi-axis ancestor"); + let result = GroveDb::verify_indexed_axis_top_k( + &proof, + path, + IndexAxis::Count, + 5, + true, + GroveVersion::latest(), + ) + .expect("verify through a multi-axis ancestor"); assert_eq!( result.root_hash, db.root_hash(None, gv).unwrap().expect("root hash"), @@ -382,8 +403,15 @@ mod tests { } let forged = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let err = GroveDb::verify_indexed_axis_top_k(&forged, path, IndexAxis::Count, 5, true) - .expect_err("a truncated axes list must not verify"); + let err = GroveDb::verify_indexed_axis_top_k( + &forged, + path, + IndexAxis::Count, + 5, + true, + GroveVersion::latest(), + ) + .expect_err("a truncated axes list must not verify"); match err { Error::CorruptedData(message) => assert!( message.contains("chain mismatch") @@ -438,9 +466,15 @@ mod tests { .prove_indexed_axis_range_aggregate(path, IndexAxis::Count, -5, 10, None, gv) .unwrap() .expect("prove with a below-domain lower bound"); - let result = - GroveDb::verify_indexed_axis_range_aggregate(&proof, path, IndexAxis::Count, -5, 10) - .expect("verify with the same bounds"); + let result = GroveDb::verify_indexed_axis_range_aggregate( + &proof, + path, + IndexAxis::Count, + -5, + 10, + GroveVersion::latest(), + ) + .expect("verify with the same bounds"); assert_eq!( result.aggregate, 3, "[-5, 10] must cover the same entries as [0, 10]" @@ -457,9 +491,16 @@ mod tests { .unwrap() .expect("prove [0, 10]"); assert_eq!( - GroveDb::verify_indexed_axis_range_aggregate(&clamped, path, IndexAxis::Count, 0, 10) - .expect("verify [0, 10]") - .aggregate, + GroveDb::verify_indexed_axis_range_aggregate( + &clamped, + path, + IndexAxis::Count, + 0, + 10, + GroveVersion::latest() + ) + .expect("verify [0, 10]") + .aggregate, result.aggregate, ); @@ -471,9 +512,16 @@ mod tests { .unwrap() .expect("prove a wholly out-of-domain range"); assert_eq!( - GroveDb::verify_indexed_axis_range_aggregate(&below, path, IndexAxis::Count, -20, -1) - .expect("verify") - .aggregate, + GroveDb::verify_indexed_axis_range_aggregate( + &below, + path, + IndexAxis::Count, + -20, + -1, + GroveVersion::latest() + ) + .expect("verify") + .aggregate, 0, "an entirely below-domain range must commit 0, not the count at 0" ); @@ -519,8 +567,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 10, offset, false, None, gv) .unwrap() .expect("offset far past the end must be provable via count commitments"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 10, offset, false) - .expect("verify offset-past-end page"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 10, + offset, + false, + GroveVersion::latest(), + ) + .expect("verify offset-past-end page"); assert_eq!( result.skipped, 1, "the whole 1-entry walk is attested as skipped" @@ -574,8 +629,15 @@ mod tests { .prove_indexed_avg_top_k(path, 5, true, None, gv) .unwrap() .expect("prove avg top_k"); - GroveDb::verify_indexed_axis_top_k(&proof, path, IndexAxis::Avg, 5, true) - .expect("the honest avg proof verifies"); + GroveDb::verify_indexed_axis_top_k( + &proof, + path, + IndexAxis::Avg, + 5, + true, + GroveVersion::latest(), + ) + .expect("the honest avg proof verifies"); let config = bincode::config::standard(); let (mut envelope, _): (IndexedAxisRangeProof, _) = @@ -587,8 +649,15 @@ mod tests { envelope.target_is_pcpsit = false; let forged = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let err = GroveDb::verify_indexed_axis_top_k(&forged, path, IndexAxis::Avg, 5, true) - .expect_err("avg on a claimed single-axis target must be refused"); + let err = GroveDb::verify_indexed_axis_top_k( + &forged, + path, + IndexAxis::Avg, + 5, + true, + GroveVersion::latest(), + ) + .expect_err("avg on a claimed single-axis target must be refused"); match err { Error::CorruptedData(message) => assert!( message.contains( @@ -634,9 +703,15 @@ mod tests { envelope.axis_tag = IndexAxis::Avg.tag(); let forged = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let err = - GroveDb::verify_indexed_axis_range_aggregate(&forged, path, IndexAxis::Avg, 0, 10) - .expect_err("an avg-tagged aggregate must be refused"); + let err = GroveDb::verify_indexed_axis_range_aggregate( + &forged, + path, + IndexAxis::Avg, + 0, + 10, + GroveVersion::latest(), + ) + .expect_err("an avg-tagged aggregate must be refused"); assert!( matches!(err, Error::NotSupported(ref m) if m == "indexed-axis aggregate proofs are not defined for the Avg axis"), @@ -666,8 +741,15 @@ mod tests { envelope.secondary_proof[last] ^= 0xff; let forged = bincode::encode_to_vec(&envelope, config).expect("re-encode"); - let err = GroveDb::verify_indexed_axis_top_k(&forged, path, IndexAxis::Count, 3, true) - .expect_err("a mangled secondary proof must not verify"); + let err = GroveDb::verify_indexed_axis_top_k( + &forged, + path, + IndexAxis::Count, + 3, + true, + GroveVersion::latest(), + ) + .expect_err("a mangled secondary proof must not verify"); match err { Error::CorruptedData(message) => assert!( message.contains("secondary proof failed to verify"), diff --git a/grovedb/src/tests/indexed_axis_offset_proof_tests.rs b/grovedb/src/tests/indexed_axis_offset_proof_tests.rs index 933fc1df7..7c8d5a33c 100644 --- a/grovedb/src/tests/indexed_axis_offset_proof_tests.rs +++ b/grovedb/src/tests/indexed_axis_offset_proof_tests.rs @@ -133,16 +133,28 @@ mod tests { .prove_indexed_sum_top_k(path, 3, descending, None, gv) .unwrap() .expect("prove top-k"); - let top_k_result = GroveDb::verify_indexed_sum_top_k(&top_k, path, 3, descending) - .expect("verify top-k"); + let top_k_result = GroveDb::verify_indexed_sum_top_k( + &top_k, + path, + 3, + descending, + GroveVersion::latest(), + ) + .expect("verify top-k"); let paginated = db .prove_indexed_sum_top_k_paginated(path, 3, 0, descending, None, gv) .unwrap() .expect("prove paginated offset 0"); - let paginated_result = - GroveDb::verify_indexed_sum_top_k_paginated(&paginated, path, 3, 0, descending) - .expect("verify paginated offset 0"); + let paginated_result = GroveDb::verify_indexed_sum_top_k_paginated( + &paginated, + path, + 3, + 0, + descending, + GroveVersion::latest(), + ) + .expect("verify paginated offset 0"); assert_eq!(paginated_result.skipped, 0); assert_eq!( @@ -171,8 +183,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, 3, true, None, gv) .unwrap() .expect("prove"); - let result = - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 3, + true, + GroveVersion::latest(), + ) + .expect("verify"); assert_eq!(result.skipped, 3); assert_eq!( entries_as_sum(&result.entries), @@ -189,8 +208,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 2, 4, false, None, gv) .unwrap() .expect("prove"); - let result = - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 2, 4, false).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 2, + 4, + false, + GroveVersion::latest(), + ) + .expect("verify"); assert_eq!(result.skipped, 4); assert_eq!( entries_as_sum(&result.entries), @@ -216,8 +242,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 5, 8, true, None, gv) .unwrap() .expect("prove"); - let result = - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 5, 8, true).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 5, + 8, + true, + GroveVersion::latest(), + ) + .expect("verify"); assert_eq!(result.skipped, 8); assert_eq!( entries_as_sum(&result.entries), @@ -247,9 +280,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, offset, descending, None, gv) .unwrap() .expect("prove offset past end"); - let result = - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, offset, descending) - .expect("verify offset past end"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + offset, + descending, + GroveVersion::latest(), + ) + .expect("verify offset past end"); assert_eq!( result.skipped, 10, "the attested skipped count is the total population" @@ -268,8 +307,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, 10, false, None, gv) .unwrap() .expect("prove offset == population"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 10, false) - .expect("verify offset == population"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 10, + false, + GroveVersion::latest(), + ) + .expect("verify offset == population"); assert_eq!(result.skipped, 10); assert!(result.entries.is_empty()); } @@ -287,8 +333,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, 5, false, None, gv) .unwrap() .expect("prove on empty"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 5, false) - .expect("verify on empty"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 5, + false, + GroveVersion::latest(), + ) + .expect("verify on empty"); assert_eq!(result.skipped, 0); assert!(result.entries.is_empty()); assert_eq!(result.root_hash, root_hash(&db, gv)); @@ -310,8 +363,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 1, 3, true, None, gv) .unwrap() .expect("prove 4th biggest"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 1, 3, true) - .expect("verify 4th biggest"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 1, + 3, + true, + GroveVersion::latest(), + ) + .expect("verify 4th biggest"); assert_eq!(result.skipped, 3); assert_eq!( entries_as_sum(&result.entries), @@ -343,6 +403,7 @@ mod tests { 1, rank_zero_based as u64, true, + GroveVersion::latest(), ) .expect("verify rank window"); assert_eq!(result.skipped, rank_zero_based as u64); @@ -387,8 +448,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, 4, false, None, gv) .unwrap() .expect("prove mid-tie ascending"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 4, false) - .expect("verify mid-tie ascending"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 4, + false, + GroveVersion::latest(), + ) + .expect("verify mid-tie ascending"); assert_eq!(result.skipped, 4); assert_eq!( entries_as_sum(&result.entries), @@ -406,8 +474,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, 3, true, None, gv) .unwrap() .expect("prove mid-tie descending"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 3, true) - .expect("verify mid-tie descending"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 3, + true, + GroveVersion::latest(), + ) + .expect("verify mid-tie descending"); assert_eq!(result.skipped, 3); assert_eq!( entries_as_sum(&result.entries), @@ -453,8 +528,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, 1, false, None, gv) .unwrap() .expect("prove"); - let result = - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 1, false).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 1, + false, + GroveVersion::latest(), + ) + .expect("verify"); assert_eq!(result.skipped, 1); assert_eq!( entries_as_sum(&result.entries), @@ -471,8 +553,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, 99, true, None, gv) .unwrap() .expect("prove past end"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 99, true) - .expect("verify past end"); + let result = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 99, + true, + GroveVersion::latest(), + ) + .expect("verify past end"); assert_eq!(result.skipped, 6, "total population attested"); assert!(result.entries.is_empty()); } @@ -497,8 +586,15 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 3, 3, true, None, gv) .unwrap() .expect("prove"); - let baseline = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 3, true) - .expect("baseline verifies"); + let baseline = GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 3, + true, + GroveVersion::latest(), + ) + .expect("baseline verifies"); assert_eq!(baseline.root_hash, expected_root); let baseline_entries = entries_as_sum(&baseline.entries).to_vec(); @@ -507,7 +603,14 @@ mod tests { for bit in 0..8u8 { let mut mutated = proof.clone(); mutated[byte_idx] ^= 1 << bit; - match GroveDb::verify_indexed_sum_top_k_paginated(&mutated, path, 3, 3, true) { + match GroveDb::verify_indexed_sum_top_k_paginated( + &mutated, + path, + 3, + 3, + true, + GroveVersion::latest(), + ) { Err(_) => {} Ok(result) => { if result.root_hash == expected_root @@ -553,14 +656,30 @@ mod tests { let truncated = &proof[..proof.len() - 1]; assert!( - GroveDb::verify_indexed_sum_top_k_paginated(truncated, path, 2, 2, false).is_err(), + GroveDb::verify_indexed_sum_top_k_paginated( + truncated, + path, + 2, + 2, + false, + GroveVersion::latest() + ) + .is_err(), "truncated proof must not verify" ); let mut extended = proof.clone(); extended.extend_from_slice(b"garbage"); assert!( - GroveDb::verify_indexed_sum_top_k_paginated(&extended, path, 2, 2, false).is_err(), + GroveDb::verify_indexed_sum_top_k_paginated( + &extended, + path, + 2, + 2, + false, + GroveVersion::latest() + ) + .is_err(), "garbage-extended proof must not verify" ); } @@ -580,15 +699,39 @@ mod tests { .expect("prove"); assert!( - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 4, 2, true).is_err(), + GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 4, + 2, + true, + GroveVersion::latest() + ) + .is_err(), "wrong k must be rejected" ); assert!( - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 3, true).is_err(), + GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 3, + true, + GroveVersion::latest() + ) + .is_err(), "wrong offset must be rejected" ); assert!( - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 3, 2, false).is_err(), + GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 3, + 2, + false, + GroveVersion::latest() + ) + .is_err(), "wrong direction must be rejected" ); } @@ -630,6 +773,7 @@ mod tests { key, expected_rank, descending, + GroveVersion::latest(), ) .expect("verify rank of key"); assert_eq!(result.root_hash, expected_root); @@ -663,8 +807,16 @@ mod tests { .unwrap() .expect("prove"); assert_eq!(rank, 2); - GroveDb::verify_indexed_axis_rank_of_key(&proof, path, IndexAxis::Sum, b"t_b", 2, false) - .expect("verify ascending mid-tie rank"); + GroveDb::verify_indexed_axis_rank_of_key( + &proof, + path, + IndexAxis::Sum, + b"t_b", + 2, + false, + GroveVersion::latest(), + ) + .expect("verify ascending mid-tie rank"); // Descending walk: hi t_c t_b t_a lo → t_b has rank 2 there too // (symmetric fixture), t_c has rank 1. @@ -673,8 +825,16 @@ mod tests { .unwrap() .expect("prove"); assert_eq!(rank, 1); - GroveDb::verify_indexed_axis_rank_of_key(&proof, path, IndexAxis::Sum, b"t_c", 1, true) - .expect("verify descending mid-tie rank"); + GroveDb::verify_indexed_axis_rank_of_key( + &proof, + path, + IndexAxis::Sum, + b"t_c", + 1, + true, + GroveVersion::latest(), + ) + .expect("verify descending mid-tie rank"); } /// A rank proof only verifies for the exact (key, rank) pair it was @@ -694,18 +854,42 @@ mod tests { assert_eq!(rank, 3); assert!( - GroveDb::verify_indexed_axis_rank_of_key(&proof, path, IndexAxis::Sum, b"g", 4, true) - .is_err(), + GroveDb::verify_indexed_axis_rank_of_key( + &proof, + path, + IndexAxis::Sum, + b"g", + 4, + true, + GroveVersion::latest() + ) + .is_err(), "a different rank claim must be rejected (offset echo mismatch)" ); assert!( - GroveDb::verify_indexed_axis_rank_of_key(&proof, path, IndexAxis::Sum, b"h", 3, true) - .is_err(), + GroveDb::verify_indexed_axis_rank_of_key( + &proof, + path, + IndexAxis::Sum, + b"h", + 3, + true, + GroveVersion::latest() + ) + .is_err(), "a different key claim must be rejected (the entry at rank 3 is g)" ); assert!( - GroveDb::verify_indexed_axis_rank_of_key(&proof, path, IndexAxis::Sum, b"g", 3, false) - .is_err(), + GroveDb::verify_indexed_axis_rank_of_key( + &proof, + path, + IndexAxis::Sum, + b"g", + 3, + false, + GroveVersion::latest() + ) + .is_err(), "a different direction must be rejected" ); } @@ -776,11 +960,26 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 1, 99, false, None, gv) .unwrap() .expect("prove offset past end"); - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 1, 99, false) - .expect("paginated shape verifies"); + GroveDb::verify_indexed_sum_top_k_paginated( + &proof, + path, + 1, + 99, + false, + GroveVersion::latest(), + ) + .expect("paginated shape verifies"); assert!( - GroveDb::verify_indexed_axis_rank_of_key(&proof, path, IndexAxis::Sum, b"a", 99, false) - .is_err(), + GroveDb::verify_indexed_axis_rank_of_key( + &proof, + path, + IndexAxis::Sum, + b"a", + 99, + false, + GroveVersion::latest() + ) + .is_err(), "a rank claim past the population must be rejected (skipped < rank)" ); @@ -791,8 +990,16 @@ mod tests { .unwrap() .expect("prove offset == population"); assert!( - GroveDb::verify_indexed_axis_rank_of_key(&proof, path, IndexAxis::Sum, b"a", 10, false) - .is_err(), + GroveDb::verify_indexed_axis_rank_of_key( + &proof, + path, + IndexAxis::Sum, + b"a", + 10, + false, + GroveVersion::latest() + ) + .is_err(), "a rank claim equal to the population must be rejected (no yielded entry)" ); } @@ -812,10 +1019,12 @@ mod tests { .prove_indexed_sum_top_k(path, 3, true, None, gv) .unwrap() .expect("prove top-k"); - GroveDb::verify_indexed_sum_top_k(&top_k, path, 3, true).expect("clean top-k verifies"); + GroveDb::verify_indexed_sum_top_k(&top_k, path, 3, true, GroveVersion::latest()) + .expect("clean top-k verifies"); top_k.push(0); assert!( - GroveDb::verify_indexed_sum_top_k(&top_k, path, 3, true).is_err(), + GroveDb::verify_indexed_sum_top_k(&top_k, path, 3, true, GroveVersion::latest()) + .is_err(), "trailing byte after the range envelope must be rejected" ); @@ -823,11 +1032,24 @@ mod tests { .prove_indexed_sum_range_aggregate(path, 0, 100, None, gv) .unwrap() .expect("prove aggregate"); - GroveDb::verify_indexed_sum_range_aggregate(&aggregate, path, 0, 100) - .expect("clean aggregate verifies"); + GroveDb::verify_indexed_sum_range_aggregate( + &aggregate, + path, + 0, + 100, + GroveVersion::latest(), + ) + .expect("clean aggregate verifies"); aggregate.push(0); assert!( - GroveDb::verify_indexed_sum_range_aggregate(&aggregate, path, 0, 100).is_err(), + GroveDb::verify_indexed_sum_range_aggregate( + &aggregate, + path, + 0, + 100, + GroveVersion::latest() + ) + .is_err(), "trailing byte after the aggregate envelope must be rejected" ); } diff --git a/grovedb/src/tests/indexed_axis_proof_tests.rs b/grovedb/src/tests/indexed_axis_proof_tests.rs index ee7a103a6..d2fa45717 100644 --- a/grovedb/src/tests/indexed_axis_proof_tests.rs +++ b/grovedb/src/tests/indexed_axis_proof_tests.rs @@ -149,7 +149,8 @@ mod tests { .prove_indexed_count_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); let entries = entries_as_count(&result.entries); assert_eq!( entries, @@ -176,7 +177,8 @@ mod tests { .prove_indexed_count_top_k(path, 3, false, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, false).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, false, grove_version) + .expect("verify"); let entries = entries_as_count(&result.entries); assert_eq!( entries, @@ -209,8 +211,9 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 2, 2, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 2, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 2, true, grove_version) + .expect("verify"); // Descending paged after skipping 2: c(3 was top-3? no — desc top-2 = f(6), e(5); // after skip-2 of f,e → d(4), c(3). let entries = entries_as_count(&result.entries); @@ -234,7 +237,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 5, 15).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 5, 15, grove_version) + .expect("verify"); // b(5) + c(10) — both in [5,15]; a(1) outside, d(20) outside. assert_eq!(result.aggregate, 2); assert_eq!(result.axis, IndexAxis::Count); @@ -254,8 +258,8 @@ mod tests { .prove_indexed_count_query(path, q.clone(), Some(10), None, grove_version) .unwrap() .expect("prove"); - let result = - GroveDb::verify_indexed_count_query(&proof, path, q, Some(10)).expect("verify"); + let result = GroveDb::verify_indexed_count_query(&proof, path, q, Some(10), grove_version) + .expect("verify"); let entries = entries_as_count(&result.entries); // Ascending all: 1,5,10. assert_eq!( @@ -286,7 +290,8 @@ mod tests { .prove_indexed_sum_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); let entries = entries_as_sum(&result.entries); assert_eq!( entries, @@ -313,7 +318,8 @@ mod tests { .prove_indexed_sum_top_k(path, 4, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 4, true).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 4, true, grove_version) + .expect("verify"); let entries = entries_as_sum(&result.entries); assert_eq!( entries, @@ -351,7 +357,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 2, 2, true).expect("verify"); + GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 2, 2, true, grove_version) + .expect("verify"); // Descending after skip-2: 6,5 skipped → d(4), c(3) returned. let entries = entries_as_sum(&result.entries); assert_eq!(entries, &[(4i64, b"d".to_vec()), (3, b"c".to_vec())]); @@ -373,7 +380,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 0, 25).expect("verify"); + GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 0, 25, grove_version) + .expect("verify"); // In [0,25]: b(5) + c(20) = 25. assert_eq!(result.aggregate, 25); assert_eq!(result.axis, IndexAxis::Sum); @@ -425,7 +433,8 @@ mod tests { .prove_indexed_count_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); // Each ItemWithSumItem insert contributes count = 1, so all // count_values are 1 — secondary keys differ only by original // key suffix, and the result list is in descending lex order @@ -457,7 +466,8 @@ mod tests { .prove_indexed_sum_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); let entries = entries_as_sum(&result.entries); assert_eq!( entries, @@ -491,7 +501,8 @@ mod tests { .prove_indexed_avg_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_avg_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_avg_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); let entries = entries_as_avg(&result.entries); assert_eq!( entries, @@ -519,7 +530,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 1, 1).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 1, 1, grove_version) + .expect("verify"); // All 3 entries have count_value=1, so [1,1] captures all 3. assert_eq!(result.aggregate, 3); } @@ -540,7 +552,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 0, 25).expect("verify"); + GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 0, 25, grove_version) + .expect("verify"); // In [0,25]: b(10) + c(20) = 30. assert_eq!(result.aggregate, 30); } @@ -560,8 +573,9 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 2, 1, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 1, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 1, true, grove_version) + .expect("verify"); // count_value=1 for all 4 entries (ItemWithSumItem). Descending // order by (count=1 ‖ original_key) → desc lex: d, c, b, a. // Skip-1 then take-2 → c, b. @@ -589,7 +603,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_avg_top_k_paginated(&proof, path, 2, 1, true).expect("verify"); + GroveDb::verify_indexed_avg_top_k_paginated(&proof, path, 2, 1, true, grove_version) + .expect("verify"); assert_eq!(result.skipped, 1); let entries = entries_as_avg(&result.entries); assert_eq!(entries.len(), 2); @@ -711,7 +726,7 @@ mod tests { // Tamper near the end of the secondary proof region. let i = proof.len() - 10; proof[i] ^= 0xFF; - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 2, true); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 2, true, grove_version); assert!(result.is_err(), "tampered proof should not verify"); } @@ -727,7 +742,8 @@ mod tests { .expect("prove"); let i = proof.len() / 2; proof[i] ^= 0xFF; - let result = GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 0, 10); + let result = + GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 0, 10, grove_version); assert!(result.is_err(), "tampered proof should not verify"); } @@ -742,7 +758,14 @@ mod tests { .unwrap() .expect("prove"); // Verifying with Sum axis must fail (the envelope tag is Count). - let result = GroveDb::verify_indexed_axis_top_k(&proof, path, IndexAxis::Sum, 1, true); + let result = GroveDb::verify_indexed_axis_top_k( + &proof, + path, + IndexAxis::Sum, + 1, + true, + grove_version, + ); assert!(result.is_err()); } @@ -756,7 +779,7 @@ mod tests { .prove_indexed_count_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 1, true); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 1, true, grove_version); assert!(result.is_err()); } @@ -770,7 +793,7 @@ mod tests { .prove_indexed_count_top_k(path, 1, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 1, false); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 1, false, grove_version); assert!(result.is_err()); } @@ -784,7 +807,8 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 1, 0, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 1, 1, true); + let result = + GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 1, 1, true, grove_version); assert!(result.is_err()); } @@ -830,7 +854,8 @@ mod tests { .prove_indexed_count_top_k(path, 2, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 2, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 2, true, grove_version) + .expect("verify"); let entries = entries_as_count(&result.entries); assert_eq!(entries, &[(9u64, b"b".to_vec()), (4, b"a".to_vec())]); assert_eq!(result.root_hash, root_hash(&db, grove_version)); @@ -851,7 +876,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 10, 5).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 10, 5, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 0); } @@ -866,7 +892,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 10, 5).expect("verify"); + GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 10, 5, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 0); } @@ -932,7 +959,7 @@ mod tests { // proof region after the brief header). let i = 12.min(proof.len() - 1); proof[i] ^= 0xFF; - let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 2, true); + let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 2, true, grove_version); assert!(result.is_err(), "tampered layer proof should not verify"); } @@ -949,7 +976,8 @@ mod tests { // Tamper near the middle of the proof. let i = proof.len() / 2; proof[i] ^= 0xFF; - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 1, 1, true); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 1, 1, true, grove_version); assert!( result.is_err(), "tampered paginated proof should not verify" @@ -968,7 +996,8 @@ mod tests { .expect("prove"); let i = proof.len() - 5; proof[i] ^= 0xFF; - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 1, 1, true); + let result = + GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 1, 1, true, grove_version); assert!( result.is_err(), "tampered sum paginated proof should not verify" @@ -992,7 +1021,7 @@ mod tests { .expect("prove"); let i = proof.len() - 8; proof[i] ^= 0xFF; - let result = GroveDb::verify_indexed_avg_top_k(&proof, path, 2, true); + let result = GroveDb::verify_indexed_avg_top_k(&proof, path, 2, true, grove_version); assert!( result.is_err(), "tampered avg top-k proof should not verify" @@ -1016,7 +1045,8 @@ mod tests { .expect("prove"); let i = proof.len() / 3; proof[i] ^= 0xFF; - let result = GroveDb::verify_indexed_avg_top_k_paginated(&proof, path, 1, 1, true); + let result = + GroveDb::verify_indexed_avg_top_k_paginated(&proof, path, 1, 1, true, grove_version); assert!( result.is_err(), "tampered avg paginated proof should not verify" @@ -1036,7 +1066,8 @@ mod tests { // Tamper at multiple sites: front and back. let i = proof.len() - 4; proof[i] ^= 0xFF; - let result = GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 10); + let result = + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 10, grove_version); assert!( result.is_err(), "tampered count aggregate proof should not verify" @@ -1054,7 +1085,8 @@ mod tests { .unwrap() .expect("prove"); // Wrong expected lo on verify. - let result = GroveDb::verify_indexed_count_range_aggregate(&proof, path, 1, 10); + let result = + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 1, 10, grove_version); assert!(result.is_err(), "lo mismatch should be rejected"); } @@ -1068,7 +1100,8 @@ mod tests { .prove_indexed_count_range_aggregate(path, 0, 10, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 11); + let result = + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 11, grove_version); assert!(result.is_err(), "hi mismatch should be rejected"); } @@ -1087,8 +1120,14 @@ mod tests { // Tamper the envelope to claim axis=Avg by reading and forging // bytes is brittle; instead just call verify_indexed_axis_range_aggregate // expecting axis=Avg — the axis-tag mismatch fires first. - let result = - GroveDb::verify_indexed_axis_range_aggregate(&proof, path, IndexAxis::Avg, 0, 10); + let result = GroveDb::verify_indexed_axis_range_aggregate( + &proof, + path, + IndexAxis::Avg, + 0, + 10, + grove_version, + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1103,8 +1142,15 @@ mod tests { .unwrap() .expect("prove"); // Envelope tag is Count, verify under Sum. - let result = - GroveDb::verify_indexed_axis_top_k_paginated(&proof, path, IndexAxis::Sum, 1, 0, true); + let result = GroveDb::verify_indexed_axis_top_k_paginated( + &proof, + path, + IndexAxis::Sum, + 1, + 0, + true, + grove_version, + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1119,7 +1165,8 @@ mod tests { .unwrap() .expect("prove"); // Wrong k on verify. - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 3, 0, true); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 3, 0, true, grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1134,7 +1181,8 @@ mod tests { .unwrap() .expect("prove"); // Verify with wrong direction. - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 1, 0, false); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 1, 0, false, grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1151,7 +1199,7 @@ mod tests { .unwrap() .expect("prove"); // Verify with wrong expected_limit. - let result = GroveDb::verify_indexed_count_query(&proof, path, q, Some(5)); + let result = GroveDb::verify_indexed_count_query(&proof, path, q, Some(5), grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1171,7 +1219,8 @@ mod tests { let mut q_desc = MerkQuery::new(); q_desc.insert_all(); q_desc.left_to_right = false; - let result = GroveDb::verify_indexed_count_query(&proof, path, q_desc, Some(2)); + let result = + GroveDb::verify_indexed_count_query(&proof, path, q_desc, Some(2), grove_version); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1180,11 +1229,25 @@ mod tests { // Pure garbage bytes — bincode decode fails. let garbage = vec![0xFFu8; 4]; let path: &[&[u8]] = &[TEST_LEAF, b"pcit"]; - let r1 = GroveDb::verify_indexed_count_top_k(&garbage, path, 1, true); + let r1 = + GroveDb::verify_indexed_count_top_k(&garbage, path, 1, true, GroveVersion::latest()); assert!(matches!(r1, Err(Error::CorruptedData(_)))); - let r2 = GroveDb::verify_indexed_count_top_k_paginated(&garbage, path, 1, 0, true); + let r2 = GroveDb::verify_indexed_count_top_k_paginated( + &garbage, + path, + 1, + 0, + true, + GroveVersion::latest(), + ); assert!(matches!(r2, Err(Error::CorruptedData(_)))); - let r3 = GroveDb::verify_indexed_count_range_aggregate(&garbage, path, 0, 10); + let r3 = GroveDb::verify_indexed_count_range_aggregate( + &garbage, + path, + 0, + 10, + GroveVersion::latest(), + ); assert!(matches!(r3, Err(Error::CorruptedData(_)))); } @@ -1200,7 +1263,8 @@ mod tests { .prove_indexed_count_top_k(path, 0, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 0, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 0, true, grove_version) + .expect("verify"); assert!(result.entries.is_empty()); assert_eq!(result.entries.len(), 0); } @@ -1215,7 +1279,8 @@ mod tests { .prove_indexed_count_top_k(path, 100, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 100, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 100, true, grove_version) + .expect("verify"); assert_eq!(result.entries.len(), 2); } @@ -1229,8 +1294,9 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 3, 0, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 3, 0, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 3, 0, true, grove_version) + .expect("verify"); assert_eq!(result.skipped, 0); assert_eq!(result.entries.len(), 3); } @@ -1245,8 +1311,15 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 2, 100, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 100, true) - .expect("verify"); + let result = GroveDb::verify_indexed_count_top_k_paginated( + &proof, + path, + 2, + 100, + true, + grove_version, + ) + .expect("verify"); // Only 2 entries total; offset 100 → empty result. assert_eq!(result.entries.len(), 0); } @@ -1261,8 +1334,9 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 2, 100, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 2, 100, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 2, 100, true, grove_version) + .expect("verify"); // 2 entries in proof, skip should be min(100, 2) = 2 → empty. assert_eq!(result.skipped, 2); assert_eq!(result.entries.len(), 0); @@ -1280,8 +1354,14 @@ mod tests { .prove_indexed_count_range_aggregate(path, 100, u64::MAX, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_range_aggregate(&proof, path, 100, u64::MAX) - .expect("verify"); + let result = GroveDb::verify_indexed_count_range_aggregate( + &proof, + path, + 100, + u64::MAX, + grove_version, + ) + .expect("verify"); // c(100) and b(1000) in range; a(1) excluded. assert_eq!(result.aggregate, 2); } @@ -1296,8 +1376,9 @@ mod tests { .prove_indexed_count_range_aggregate(path, 0, u64::MAX, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, u64::MAX) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, u64::MAX, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 3); } @@ -1312,7 +1393,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 100).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 100, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 0); } @@ -1327,7 +1409,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 1, i64::MAX).expect("verify"); + GroveDb::verify_indexed_sum_range_aggregate(&proof, path, 1, i64::MAX, grove_version) + .expect("verify"); // Only a(i64::MAX) is in [1, i64::MAX]. assert_eq!(result.aggregate, i64::MAX as i128); } @@ -1347,7 +1430,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_sum_range_aggregate(&proof, path, -100, -1).expect("verify"); + GroveDb::verify_indexed_sum_range_aggregate(&proof, path, -100, -1, grove_version) + .expect("verify"); // -10 + -5 = -15 assert_eq!(result.aggregate, -15); } @@ -1370,9 +1454,15 @@ mod tests { ) .unwrap() .expect("prove"); - let result = - GroveDb::verify_indexed_axis_range_aggregate(&proof, path, IndexAxis::Count, -50, -10) - .expect("verify"); + let result = GroveDb::verify_indexed_axis_range_aggregate( + &proof, + path, + IndexAxis::Count, + -50, + -10, + grove_version, + ) + .expect("verify"); assert_eq!(result.aggregate, 0); } @@ -1440,22 +1530,24 @@ mod tests { .prove_indexed_count_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove count"); - let r_c = - GroveDb::verify_indexed_count_top_k(&proof_c, path, 3, true).expect("verify count"); + let r_c = GroveDb::verify_indexed_count_top_k(&proof_c, path, 3, true, grove_version) + .expect("verify count"); assert_eq!(r_c.root_hash, root); let proof_s = db .prove_indexed_sum_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove sum"); - let r_s = GroveDb::verify_indexed_sum_top_k(&proof_s, path, 3, true).expect("verify sum"); + let r_s = GroveDb::verify_indexed_sum_top_k(&proof_s, path, 3, true, grove_version) + .expect("verify sum"); assert_eq!(r_s.root_hash, root); let proof_a = db .prove_indexed_avg_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove avg"); - let r_a = GroveDb::verify_indexed_avg_top_k(&proof_a, path, 3, true).expect("verify avg"); + let r_a = GroveDb::verify_indexed_avg_top_k(&proof_a, path, 3, true, grove_version) + .expect("verify avg"); assert_eq!(r_a.root_hash, root); } @@ -1642,7 +1734,8 @@ mod tests { .prove_indexed_count_query(path, q.clone(), Some(3), None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_query(&proof, path, q, Some(3)).expect("verify"); + let result = GroveDb::verify_indexed_count_query(&proof, path, q, Some(3), grove_version) + .expect("verify"); let entries = entries_as_count(&result.entries); assert_eq!( entries, @@ -1667,7 +1760,8 @@ mod tests { .prove_indexed_sum_query(path, q.clone(), Some(3), None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_query(&proof, path, q, Some(3)).expect("verify"); + let result = GroveDb::verify_indexed_sum_query(&proof, path, q, Some(3), grove_version) + .expect("verify"); let entries = entries_as_sum(&result.entries); assert_eq!( entries, @@ -1695,7 +1789,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_avg_top_k_paginated(&proof, path, 1, 0, false).expect("verify"); + GroveDb::verify_indexed_avg_top_k_paginated(&proof, path, 1, 0, false, grove_version) + .expect("verify"); assert_eq!(result.skipped, 0); assert_eq!(result.entries.len(), 1); } @@ -1720,7 +1815,8 @@ mod tests { .prove_indexed_avg_query(path, q.clone(), None, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_avg_query(&proof, path, q, None).expect("verify"); + let result = GroveDb::verify_indexed_avg_query(&proof, path, q, None, grove_version) + .expect("verify"); assert_eq!(result.entries.len(), 2); assert_eq!(result.root_hash, root_hash(&db, grove_version)); } @@ -1775,7 +1871,8 @@ mod tests { .prove_indexed_sum_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_sum_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); let entries = entries_as_sum(&result.entries); assert_eq!( entries, @@ -1843,7 +1940,7 @@ mod tests { .expect("prove"); // Lie about path: extend by one extra segment. let bad: &[&[u8]] = &[TEST_LEAF, b"pcit", b"extra"]; - let r = GroveDb::verify_indexed_count_top_k(&proof, bad, 1, true); + let r = GroveDb::verify_indexed_count_top_k(&proof, bad, 1, true, grove_version); assert!(matches!(r, Err(Error::CorruptedData(s)) if s.contains("layers"))); } @@ -1859,7 +1956,7 @@ mod tests { .expect("prove"); // Drop a path segment. let bad: &[&[u8]] = &[TEST_LEAF]; - let r = GroveDb::verify_indexed_sum_top_k(&proof, bad, 1, true); + let r = GroveDb::verify_indexed_sum_top_k(&proof, bad, 1, true, grove_version); assert!(matches!(r, Err(Error::CorruptedData(s)) if s.contains("layers"))); } @@ -1874,7 +1971,8 @@ mod tests { .unwrap() .expect("prove"); let bad: &[&[u8]] = &[TEST_LEAF]; - let r = GroveDb::verify_indexed_count_top_k_paginated(&proof, bad, 1, 0, true); + let r = + GroveDb::verify_indexed_count_top_k_paginated(&proof, bad, 1, 0, true, grove_version); assert!(matches!(r, Err(Error::CorruptedData(s)) if s.contains("layers"))); } @@ -1889,7 +1987,7 @@ mod tests { .unwrap() .expect("prove"); let bad: &[&[u8]] = &[TEST_LEAF]; - let r = GroveDb::verify_indexed_sum_top_k_paginated(&proof, bad, 1, 0, true); + let r = GroveDb::verify_indexed_sum_top_k_paginated(&proof, bad, 1, 0, true, grove_version); assert!(matches!(r, Err(Error::CorruptedData(s)) if s.contains("layers"))); } @@ -1904,7 +2002,7 @@ mod tests { .unwrap() .expect("prove"); let bad: &[&[u8]] = &[TEST_LEAF]; - let r = GroveDb::verify_indexed_count_range_aggregate(&proof, bad, 0, 10); + let r = GroveDb::verify_indexed_count_range_aggregate(&proof, bad, 0, 10, grove_version); assert!(matches!(r, Err(Error::CorruptedData(s)) if s.contains("layers"))); } @@ -1919,7 +2017,7 @@ mod tests { .unwrap() .expect("prove"); let bad: &[&[u8]] = &[TEST_LEAF, b"psit", b"extra"]; - let r = GroveDb::verify_indexed_sum_range_aggregate(&proof, bad, -10, 10); + let r = GroveDb::verify_indexed_sum_range_aggregate(&proof, bad, -10, 10, grove_version); assert!(matches!(r, Err(Error::CorruptedData(s)) if s.contains("layers"))); } @@ -1939,7 +2037,14 @@ mod tests { .unwrap() .expect("prove"); // Verify under sum axis — axis tag mismatch should be reported. - let result = GroveDb::verify_indexed_axis_query(&proof, path, IndexAxis::Sum, q, None); + let result = GroveDb::verify_indexed_axis_query( + &proof, + path, + IndexAxis::Sum, + q, + None, + grove_version, + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -1956,7 +2061,14 @@ mod tests { .prove_indexed_count_top_k(path, 1, true, None, grove_version) .unwrap() .expect("prove"); - let r = GroveDb::verify_indexed_axis_top_k(&proof, path, IndexAxis::Avg, 1, true); + let r = GroveDb::verify_indexed_axis_top_k( + &proof, + path, + IndexAxis::Avg, + 1, + true, + grove_version, + ); assert!(matches!(r, Err(Error::CorruptedData(s)) if s.contains("axis mismatch"))); } @@ -1974,8 +2086,9 @@ mod tests { .prove_indexed_sum_top_k_paginated(path, 1, 1000, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 1, 1000, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_sum_top_k_paginated(&proof, path, 1, 1000, true, grove_version) + .expect("verify"); assert!(result.entries.is_empty()); // skipped is clamped to total_returned which is 2 (whole secondary) // when combined_limit = offset+k = 1001 covers everything. @@ -2011,8 +2124,15 @@ mod tests { .unwrap() .expect("prove count top_k"); // Honest verification under the Count axis succeeds. - GroveDb::verify_indexed_axis_top_k(&proof, path, IndexAxis::Count, 3, true) - .expect("honest count verify"); + GroveDb::verify_indexed_axis_top_k( + &proof, + path, + IndexAxis::Count, + 3, + true, + GroveVersion::latest(), + ) + .expect("honest count verify"); // Relabel: decode the envelope, flip axis_tag to Sum, keep the // PCIT element bytes and the count-secondary proof, re-encode. @@ -2024,7 +2144,14 @@ mod tests { env.axis_tag = IndexAxis::Sum.tag(); let forged = bincode::encode_to_vec(&env, config).expect("re-encode forged envelope"); - let res = GroveDb::verify_indexed_axis_top_k(&forged, path, IndexAxis::Sum, 3, true); + let res = GroveDb::verify_indexed_axis_top_k( + &forged, + path, + IndexAxis::Sum, + 3, + true, + GroveVersion::latest(), + ); assert!( matches!( res, @@ -2073,9 +2200,15 @@ mod tests { .prove_indexed_axis_range_aggregate(path, IndexAxis::Count, lo, hi, None, v) .unwrap() .expect("prove out-of-domain count aggregate"); - let result = - GroveDb::verify_indexed_axis_range_aggregate(&proof, path, IndexAxis::Count, lo, hi) - .expect("verify out-of-domain count aggregate"); + let result = GroveDb::verify_indexed_axis_range_aggregate( + &proof, + path, + IndexAxis::Count, + lo, + hi, + GroveVersion::latest(), + ) + .expect("verify out-of-domain count aggregate"); assert_eq!( result.aggregate, 0, "count range entirely above u64::MAX must aggregate to 0, not count the \ @@ -2097,9 +2230,15 @@ mod tests { .prove_indexed_axis_range_aggregate(path, IndexAxis::Sum, lo, hi, None, v) .unwrap() .expect("prove out-of-domain sum aggregate"); - let result = - GroveDb::verify_indexed_axis_range_aggregate(&proof, path, IndexAxis::Sum, lo, hi) - .expect("verify out-of-domain sum aggregate"); + let result = GroveDb::verify_indexed_axis_range_aggregate( + &proof, + path, + IndexAxis::Sum, + lo, + hi, + GroveVersion::latest(), + ) + .expect("verify out-of-domain sum aggregate"); assert_eq!( result.aggregate, 0, "sum range entirely above i64::MAX must aggregate to 0, not sum the boundary entry" @@ -2120,9 +2259,15 @@ mod tests { .prove_indexed_axis_range_aggregate(path, IndexAxis::Sum, lo, hi, None, v) .unwrap() .expect("prove below-domain sum aggregate"); - let result = - GroveDb::verify_indexed_axis_range_aggregate(&proof, path, IndexAxis::Sum, lo, hi) - .expect("verify below-domain sum aggregate"); + let result = GroveDb::verify_indexed_axis_range_aggregate( + &proof, + path, + IndexAxis::Sum, + lo, + hi, + GroveVersion::latest(), + ) + .expect("verify below-domain sum aggregate"); assert_eq!( result.aggregate, 0, "sum range entirely below i64::MIN must aggregate to 0, not sum the boundary entry" @@ -2203,7 +2348,8 @@ mod tests { .prove_indexed_count_top_k(path, 3, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 3, true, grove_version) + .expect("verify"); assert_eq!(entries_as_count(&result.entries).len(), 3); assert_eq!(result.root_hash, root_hash(&db, grove_version)); } @@ -2218,7 +2364,8 @@ mod tests { .prove_indexed_count_top_k(path, 1, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 1, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 1, true, grove_version) + .expect("verify"); assert_eq!(entries_as_count(&result.entries), &[(99u64, b"b".to_vec())]); } @@ -2226,13 +2373,20 @@ mod tests { #[test] fn ported_verify_top_k_rejects_truncated_proof() { - let result = GroveDb::verify_indexed_count_top_k(&[0x00, 0x01], &[b"x"], 1, true); + let result = GroveDb::verify_indexed_count_top_k( + &[0x00, 0x01], + &[b"x"], + 1, + true, + GroveVersion::latest(), + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } #[test] fn ported_verify_top_k_rejects_empty_bytes() { - let result = GroveDb::verify_indexed_count_top_k(&[], &[b"x"], 1, true); + let result = + GroveDb::verify_indexed_count_top_k(&[], &[b"x"], 1, true, GroveVersion::latest()); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -2247,7 +2401,8 @@ mod tests { .unwrap() .expect("prove"); let shorter: &[&[u8]] = &[TEST_LEAF]; - let err = GroveDb::verify_indexed_count_top_k(&proof, shorter, 1, true).unwrap_err(); + let err = GroveDb::verify_indexed_count_top_k(&proof, shorter, 1, true, grove_version) + .unwrap_err(); assert!(matches!(err, Error::CorruptedData(_))); } @@ -2284,7 +2439,7 @@ mod tests { .expect("prove"); let mid = proof.len() / 2; proof[mid] ^= 0xff; - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 2, true); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 2, true, grove_version); assert!(result.is_err()); } @@ -2318,7 +2473,7 @@ mod tests { .unwrap() .expect("prove cidx_a"); let path_b: &[&[u8]] = &[TEST_LEAF, b"cidx_b"]; - let result = GroveDb::verify_indexed_count_top_k(&proof, path_b, 2, true); + let result = GroveDb::verify_indexed_count_top_k(&proof, path_b, 2, true, grove_version); assert!(result.is_err(), "wrong path must fail"); } @@ -2369,7 +2524,8 @@ mod tests { .prove_indexed_count_top_k(path, 10, true, None, grove_version) .unwrap() .expect("prove deeper"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 10, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 10, true, grove_version) + .expect("verify"); assert_eq!(entries_as_count(&result.entries).len(), 3); assert_eq!(result.root_hash, root_hash(&db, grove_version)); } @@ -2391,8 +2547,9 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 2, 1, false, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 1, false) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 1, false, grove_version) + .expect("verify"); assert_eq!( entries_as_count(&result.entries), &[(2u64, b"b".to_vec()), (3u64, b"c".to_vec())] @@ -2419,10 +2576,17 @@ mod tests { .unwrap() .expect("paginated"); let top_result = - GroveDb::verify_indexed_count_top_k(&top_proof, path, 3, true).expect("verify"); - let pag_result = - GroveDb::verify_indexed_count_top_k_paginated(&pag_proof, path, 3, 0, true) + GroveDb::verify_indexed_count_top_k(&top_proof, path, 3, true, grove_version) .expect("verify"); + let pag_result = GroveDb::verify_indexed_count_top_k_paginated( + &pag_proof, + path, + 3, + 0, + true, + grove_version, + ) + .expect("verify"); assert_eq!( entries_as_count(&top_result.entries), entries_as_count(&pag_result.entries) @@ -2449,8 +2613,9 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 3, 0, true, None, grove_version) .unwrap() .expect("prove on empty"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 3, 0, true) - .expect("verify on empty"); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 3, 0, true, grove_version) + .expect("verify on empty"); assert!(entries_as_count(&result.entries).is_empty()); assert_eq!(result.root_hash, root_hash(&db, grove_version)); } @@ -2469,8 +2634,9 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 3, 6, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 3, 6, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 3, 6, true, grove_version) + .expect("verify"); let got = entries_as_count(&result.entries); assert_eq!(got.len(), 3); assert_eq!(got[0].0, 3); @@ -2513,8 +2679,9 @@ mod tests { .prove_indexed_count_top_k_paginated(path, 2, 1, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 1, true) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_top_k_paginated(&proof, path, 2, 1, true, grove_version) + .expect("verify"); // Descending: d(4), c(3), b(2), a(1). Skip 1 (d), take 2: c, b. assert_eq!( entries_as_count(&result.entries), @@ -2538,7 +2705,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 5, 5).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 5, 5, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 2); } @@ -2554,7 +2722,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 100, 200).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 100, 200, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 0); } @@ -2592,8 +2761,9 @@ mod tests { .prove_indexed_count_range_aggregate(path, 0, u64::MAX, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, u64::MAX) - .expect("verify"); + let result = + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, u64::MAX, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 3); assert_eq!(result.root_hash, root_hash(&db, grove_version)); } @@ -2614,7 +2784,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 100).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 100, grove_version) + .expect("verify"); // [0, 100]: a(1), b(50), c(100) = 3. assert_eq!(result.aggregate, 3); assert_eq!(result.root_hash, root_hash(&db, grove_version)); @@ -2632,7 +2803,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 0).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 0, 0, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 2, "two entries with count=0"); } @@ -2647,9 +2819,14 @@ mod tests { .prove_indexed_count_range_aggregate(path, u64::MAX, u64::MAX, None, grove_version) .unwrap() .expect("prove"); - let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, u64::MAX, u64::MAX) - .expect("verify"); + let result = GroveDb::verify_indexed_count_range_aggregate( + &proof, + path, + u64::MAX, + u64::MAX, + grove_version, + ) + .expect("verify"); assert_eq!(result.aggregate, 0); } @@ -2668,7 +2845,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 42, 42).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 42, 42, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 4); } @@ -2693,7 +2871,8 @@ mod tests { .prove_indexed_count_query(path, q.clone(), None, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_query(&proof, path, q, None).expect("verify"); + let result = GroveDb::verify_indexed_count_query(&proof, path, q, None, grove_version) + .expect("verify"); // count_value 5 (b) and 10 (c) are in [5,11). assert_eq!(entries_as_count(&result.entries).len(), 2); } @@ -2711,7 +2890,8 @@ mod tests { .prove_indexed_count_query(path, q.clone(), None, None, grove_version) .unwrap() .expect("prove"); - let err = GroveDb::verify_indexed_count_query(&proof, path, q, Some(0)).unwrap_err(); + let err = GroveDb::verify_indexed_count_query(&proof, path, q, Some(0), grove_version) + .unwrap_err(); assert!(matches!(err, Error::CorruptedData(_))); } @@ -2719,7 +2899,13 @@ mod tests { fn ported_verify_query_rejects_corrupt_bytes() { let mut q = MerkQuery::new(); q.insert_all(); - let result = GroveDb::verify_indexed_count_query(&[0xff; 5], &[b"x"], q, None); + let result = GroveDb::verify_indexed_count_query( + &[0xff; 5], + &[b"x"], + q, + None, + GroveVersion::latest(), + ); assert!(matches!(result, Err(Error::CorruptedData(_)))); } @@ -2763,7 +2949,8 @@ mod tests { .prove_indexed_count_query(path, q.clone(), None, None, grove_version) .unwrap() .expect("prove single key"); - let result = GroveDb::verify_indexed_count_query(&proof, path, q, None).expect("verify"); + let result = GroveDb::verify_indexed_count_query(&proof, path, q, None, grove_version) + .expect("verify"); let got = entries_as_count(&result.entries); assert_eq!(got.len(), 1); assert_eq!(got[0], (5u64, b"alice".to_vec())); @@ -2807,7 +2994,8 @@ mod tests { .prove_indexed_count_query(path, q.clone(), None, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_query(&proof, path, q, None).expect("verify"); + let result = GroveDb::verify_indexed_count_query(&proof, path, q, None, grove_version) + .expect("verify"); let got = entries_as_count(&result.entries); assert_eq!(got.len(), 1); assert_eq!(got[0].0, 2); @@ -2828,7 +3016,8 @@ mod tests { .prove_indexed_count_top_k(path, 5, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 5, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 5, true, grove_version) + .expect("verify"); let got = entries_as_count(&result.entries); assert_eq!(got.len(), 2); // c(3) and a(1) remain. @@ -2862,7 +3051,8 @@ mod tests { .unwrap() .expect("prove"); let proven = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 3, 9).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 3, 9, grove_version) + .expect("verify"); assert_eq!(unproven as i128, proven.aggregate); } @@ -2880,7 +3070,8 @@ mod tests { .prove_indexed_count_top_k(path, 10, true, None, grove_version) .unwrap() .expect("prove"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 10, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 10, true, grove_version) + .expect("verify"); let got = entries_as_count(&result.entries); assert_eq!(got.len(), 10); assert_eq!(got[0].0, 29); @@ -2933,7 +3124,8 @@ mod tests { .prove_indexed_count_top_k(path, 4, true, None, grove_version) .unwrap() .expect("prove triple-nested"); - let result = GroveDb::verify_indexed_count_top_k(&proof, path, 4, true).expect("verify"); + let result = GroveDb::verify_indexed_count_top_k(&proof, path, 4, true, grove_version) + .expect("verify"); let got = entries_as_count(&result.entries); assert_eq!(got.len(), 4); assert_eq!(got[0].0, 7); @@ -2984,7 +3176,8 @@ mod tests { .unwrap() .expect("prove"); let result = - GroveDb::verify_indexed_count_range_aggregate(&proof, path, 15, 25).expect("verify"); + GroveDb::verify_indexed_count_range_aggregate(&proof, path, 15, 25, grove_version) + .expect("verify"); assert_eq!(result.aggregate, 1, "only b(20) in [15,25]"); assert_eq!(result.root_hash, root_hash(&db, grove_version)); } diff --git a/grovedb/src/tests/merge_versioning_tests.rs b/grovedb/src/tests/merge_versioning_tests.rs new file mode 100644 index 000000000..f0e684f2f --- /dev/null +++ b/grovedb/src/tests/merge_versioning_tests.rs @@ -0,0 +1,498 @@ +//! Version-gated merge semantics and the indexed-axis version wiring. +//! +//! `path_query_methods.merge = 1` (GROVE_V4) makes `PathQuery::merge` +//! direction-aware: every merged query must agree on `left_to_right` +//! and the merged query carries it; a conflict is a typed error. Below +//! V4 the long-standing silent first-wins behavior is preserved — +//! merged queries feed proofs, and the verifier re-runs the same merge +//! with the same grove version, so both sides agree at every version. + +#[cfg(test)] +mod tests { + use grovedb_merk::proofs::{query::query_item::QueryItem, Query}; + use grovedb_version::version::{GroveVersion, GROVE_VERSIONS}; + + use crate::{Error, PathQuery}; + + fn directional_query(path_key: &[u8], left_to_right: bool) -> PathQuery { + let mut query = Query::new_with_direction(left_to_right); + query.insert_item(QueryItem::RangeFull(..)); + PathQuery::new_unsized(vec![path_key.to_vec()], query) + } + + #[test] + fn v4_merge_requires_direction_agreement_and_propagates_it() { + let v4 = GroveVersion::latest(); + assert_eq!(v4.protocol_version, 4); + + // Agreement: the shared direction survives the merge. + for direction in [true, false] { + let a = directional_query(b"a", direction); + let b = directional_query(b"b", direction); + let merged = PathQuery::merge(vec![&a, &b], v4).expect("agreeing merge succeeds"); + assert_eq!( + merged.query.query.left_to_right, direction, + "merged direction must be the shared one" + ); + } + + // Conflict: typed rejection instead of silently keeping the + // first query's direction. + let ascending = directional_query(b"a", true); + let descending = directional_query(b"b", false); + match PathQuery::merge(vec![&ascending, &descending], v4) { + Err(Error::NotSupported(message)) => { + assert!(message.contains("direction"), "got: {message}") + } + other => panic!("conflicting directions must be rejected at V4, got {other:?}"), + } + } + + #[test] + fn pre_v4_merge_keeps_the_silent_first_wins_behavior() { + let v3 = &GROVE_VERSIONS[2]; + assert_eq!(v3.protocol_version, 3); + + let ascending = directional_query(b"a", true); + let descending = directional_query(b"b", false); + let merged = PathQuery::merge(vec![&descending, &ascending], v3) + .expect("pre-V4 merge tolerates direction conflicts"); + // Historic quirk being preserved: for sub-level inputs the + // merged root is a synthesized query whose direction is the + // DEFAULT — the inputs' directions are silently dropped + // entirely. (V4 requires agreement and propagates instead.) + assert!( + merged.query.query.left_to_right, + "pre-V4 the merged root keeps the synthesized default direction" + ); + } + + #[test] + fn query_level_merges_reject_read_modes() { + use grovedb_merk::proofs::query::{AxisQuery, IndexAxis, ReadMode}; + + let mut axis_query = Query::new(); + axis_query.read_mode = Some(Box::new(ReadMode::Axis(AxisQuery::top_k( + IndexAxis::Sum, + 1, + 0, + true, + )))); + let plain = Query::new_single_key(b"k".to_vec()); + + // Direct Query-level API (rs-drive-facing): read modes on + // either side, at any nesting level, are rejected instead of + // silently merged as key selection. + match Query::merge_multiple(vec![plain.clone(), axis_query.clone()]) { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => panic!("merge_multiple must reject read modes, got {other:?}"), + } + let mut target = plain.clone(); + match target.merge_with(axis_query.clone()) { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => panic!("merge_with must reject read modes, got {other:?}"), + } + // Nested: a read mode hidden in a subquery branch is caught too. + let mut nested = Query::new_single_key(b"outer".to_vec()); + nested.set_subquery(axis_query); + match Query::merge_multiple(vec![plain, nested]) { + Err(grovedb_query::error::Error::NotSupported(_)) => {} + other => panic!("merge_multiple must reject nested read modes, got {other:?}"), + } + } + + #[test] + fn indexed_axis_version_slots_are_wired() { + // The slots exist so the first future divergence bumps a number + // instead of forking silently; all-zero today. An unknown slot + // value must be rejected by every gated entry point. + let mut doctored = GroveVersion::latest().clone(); + doctored + .grovedb_versions + .operations + .indexed_axis + .verify_single_path = 9; + let result = crate::GroveDb::verify_indexed_axis_top_k( + &[0u8; 4], + &[b"any".as_slice()], + grovedb_merk::proofs::query::IndexAxis::Count, + 1, + true, + &doctored, + ); + match result { + Err(Error::VersionError(_)) => {} + other => panic!("unknown verify_single_path version must be rejected, got {other:?}"), + } + } + + // ----------------------------------------------------------------- + // Every gated entry point, not just one per slot + // + // The slots only do their job if EVERY entry point behind them + // checks. One unchecked function is a silent fork the day a slot + // diverges, so each is exercised against an unknown value. + // ----------------------------------------------------------------- + + /// A grove version with one indexed-axis slot set to an + /// unrecognized value. + fn doctored(slot: fn(&mut grovedb_version::version::GroveVersion)) -> GroveVersion { + let mut version = GroveVersion::latest().clone(); + slot(&mut version); + version + } + + /// A PSIT at `[TEST_LEAF, b"psit"]` with a couple of entries, built + /// at the real version so the fixture itself is not gated. + fn psit_db(grove_version: &GroveVersion) -> (crate::tests::TempGroveDb, Vec>) { + use crate::{ + tests::{make_test_grovedb, TEST_LEAF}, + Element, + }; + let db = make_test_grovedb(grove_version); + db.insert( + [TEST_LEAF].as_ref(), + b"psit", + Element::empty_provable_sum_indexed_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("create PSIT"); + for (key, sum) in [(b"a", 5i64), (b"b", 9)] { + db.insert_into_provable_sum_indexed_tree( + [TEST_LEAF, b"psit"].as_ref(), + key, + Element::new_sum_item(sum), + None, + grove_version, + ) + .unwrap() + .expect("insert PSIT entry"); + } + (db, vec![TEST_LEAF.to_vec(), b"psit".to_vec()]) + } + + #[test] + fn every_trusted_read_rejects_an_unknown_read_version() { + use grovedb_merk::proofs::query::IndexAxis; + + let real = GroveVersion::latest(); + let (db, _) = psit_db(real); + let bad = doctored(|v| v.grovedb_versions.operations.indexed_axis.read = 9); + let path = [crate::tests::TEST_LEAF, b"psit"]; + + macro_rules! assert_version_rejected { + ($label:expr, $call:expr) => { + match $call.unwrap() { + Err(Error::VersionError(_)) => {} + other => panic!( + "{} must reject an unknown read version, got {:?}", + $label, other + ), + } + }; + } + + assert_version_rejected!( + "indexed_sum_top_k", + db.indexed_sum_top_k(path.as_ref(), 1, true, None, &bad) + ); + assert_version_rejected!( + "indexed_sum_top_k_paginated", + db.indexed_sum_top_k_paginated(path.as_ref(), 1, 0, true, None, &bad) + ); + assert_version_rejected!( + "indexed_sum_range", + db.indexed_sum_range(path.as_ref(), 0, 100, true, 10, None, &bad) + ); + assert_version_rejected!( + "indexed_sum_range_aggregate", + db.indexed_sum_range_aggregate(path.as_ref(), 0, 100, None, &bad) + ); + assert_version_rejected!( + "indexed_count_range_aggregate", + db.indexed_count_range_aggregate(path.as_ref(), 0, 100, None, &bad) + ); + // Sanity: the same calls succeed at the real version, so the + // rejections above are the gate firing and not a broken fixture. + db.indexed_sum_top_k(path.as_ref(), 1, true, None, real) + .unwrap() + .expect("ungated read works"); + let _ = IndexAxis::Sum; + } + + #[test] + fn every_prover_rejects_an_unknown_prove_version() { + use grovedb_merk::proofs::{query::IndexAxis, Query as MerkQuery}; + + let real = GroveVersion::latest(); + let (db, _) = psit_db(real); + let bad = doctored(|v| v.grovedb_versions.operations.indexed_axis.prove_single_path = 9); + let path = [crate::tests::TEST_LEAF, b"psit"]; + + macro_rules! assert_version_rejected { + ($label:expr, $call:expr) => { + match $call.unwrap() { + Err(Error::VersionError(_)) => {} + other => panic!( + "{} must reject an unknown prove version, got {:?}", + $label, other + ), + } + }; + } + + assert_version_rejected!( + "prove_indexed_sum_top_k", + db.prove_indexed_sum_top_k(path.as_ref(), 1, true, None, &bad) + ); + assert_version_rejected!( + "prove_indexed_sum_top_k_paginated", + db.prove_indexed_sum_top_k_paginated(path.as_ref(), 1, 0, true, None, &bad) + ); + assert_version_rejected!( + "prove_indexed_axis_query", + db.prove_indexed_axis_query( + path.as_ref(), + IndexAxis::Sum, + MerkQuery::new(), + Some(1), + None, + &bad, + ) + ); + assert_version_rejected!( + "prove_indexed_axis_rank_of_key", + db.prove_indexed_axis_rank_of_key( + path.as_ref(), + IndexAxis::Sum, + b"a", + true, + None, + &bad + ) + ); + assert_version_rejected!( + "prove_indexed_count_range_aggregate", + db.prove_indexed_count_range_aggregate(path.as_ref(), 0, 100, None, &bad) + ); + } + + #[test] + fn every_verifier_rejects_an_unknown_verify_version() { + use grovedb_merk::proofs::{query::IndexAxis, Query as MerkQuery}; + + let bad = doctored(|v| { + v.grovedb_versions + .operations + .indexed_axis + .verify_single_path = 9 + }); + let path = [b"any".as_slice()]; + // The gate fires before any decoding, so garbage bytes are + // enough — and prove it, by asserting the error is the VERSION + // error rather than a decode failure. + let garbage = [0u8; 4]; + + macro_rules! assert_version_rejected { + ($label:expr, $call:expr) => { + match $call { + Err(Error::VersionError(_)) => {} + other => panic!( + "{} must reject an unknown verify version, got {:?}", + $label, other + ), + } + }; + } + + assert_version_rejected!( + "verify_indexed_axis_top_k", + crate::GroveDb::verify_indexed_axis_top_k( + &garbage, + &path, + IndexAxis::Count, + 1, + true, + &bad, + ) + ); + assert_version_rejected!( + "verify_indexed_axis_top_k_paginated", + crate::GroveDb::verify_indexed_axis_top_k_paginated( + &garbage, + &path, + IndexAxis::Count, + 1, + 0, + true, + &bad, + ) + ); + assert_version_rejected!( + "verify_indexed_axis_query", + crate::GroveDb::verify_indexed_axis_query( + &garbage, + &path, + IndexAxis::Count, + MerkQuery::new(), + Some(1), + &bad, + ) + ); + assert_version_rejected!( + "verify_indexed_axis_range_aggregate", + crate::GroveDb::verify_indexed_axis_range_aggregate( + &garbage, + &path, + IndexAxis::Count, + 0, + 100, + &bad, + ) + ); + } + + // ----------------------------------------------------------------- + // PathQuery::merge's own version handling + // ----------------------------------------------------------------- + + #[test] + fn merge_rejects_an_unknown_merge_version() { + let mut bad = GroveVersion::latest().clone(); + bad.grovedb_versions.path_query_methods.merge = 9; + let a = directional_query(b"a", true); + let b = directional_query(b"b", true); + match PathQuery::merge(vec![&a, &b], &bad) { + Err(Error::VersionError(_)) => {} + other => panic!("unknown merge version must be rejected, got {other:?}"), + } + } + + #[test] + fn merge_refuses_limits_and_offsets_at_every_merge_version() { + use crate::SizedQuery; + + // Both refusals predate the version gate and must survive it — + // a merged limit/offset would silently mean something different + // than either input asked for. + for version in [&GROVE_VERSIONS[2], GroveVersion::latest()] { + let plain = directional_query(b"a", true); + + let mut query = Query::new(); + query.insert_item(QueryItem::RangeFull(..)); + let with_offset = PathQuery::new( + vec![b"b".to_vec()], + SizedQuery::new(query.clone(), None, Some(1)), + ); + match PathQuery::merge(vec![&plain, &with_offset], version) { + Err(Error::NotSupported(message)) => { + assert!(message.contains("offset"), "got: {message}") + } + other => panic!("merging an offset must be refused, got {other:?}"), + } + + let with_limit = + PathQuery::new(vec![b"b".to_vec()], SizedQuery::new(query, Some(2), None)); + match PathQuery::merge(vec![&plain, &with_limit], version) { + Err(Error::NotSupported(message)) => { + assert!(message.contains("limit"), "got: {message}") + } + other => panic!("merging a limit must be refused, got {other:?}"), + } + } + } + + #[test] + fn merge_surfaces_read_mode_conflicts_at_both_merge_versions() { + use grovedb_merk::proofs::query::{AxisQuery, IndexAxis, ReadMode}; + + // `PathQuery::merge` refuses read modes up front, before the + // version-gated Query-level merge is reached — merging would + // drop the mode and mangle an axis or sum-budget read into key + // selection. The refusal must not depend on the merge version: + // the verifier re-runs the same merge at the same version, so a + // version where this slipped through would fork the two sides. + // (`Query::merge_multiple` refuses read modes too; that is + // defense in depth for direct rs-drive-facing callers, exercised + // by `query_level_merges_reject_read_modes`.) + for version in [&GROVE_VERSIONS[2], GroveVersion::latest()] { + let mut axis = Query::new(); + axis.read_mode = Some(Box::new(ReadMode::Axis(AxisQuery::top_k( + IndexAxis::Sum, + 1, + 0, + true, + )))); + let axis_pq = PathQuery::new_unsized(vec![b"a".to_vec()], axis); + let plain = directional_query(b"a", true); + + match PathQuery::merge(vec![&axis_pq, &plain], version) { + Err(Error::NotSupported(_)) => {} + other => panic!( + "merging a read-mode query at merge version {} must be refused, got {:?}", + version.grovedb_versions.path_query_methods.merge, other + ), + } + } + } + + #[test] + fn inverted_ranges_are_still_held_to_the_version_contract() { + // Review finding: the count/sum/avg range wrappers answered + // `Ok([])` for `lo > hi` BEFORE delegating to the gated generic, + // so a degenerate range slipped the version check entirely — + // part of each entry point sat outside the contract every other + // input to it is held to. + let real = GroveVersion::latest(); + let (db, _) = psit_db(real); + let bad = doctored(|v| v.grovedb_versions.operations.indexed_axis.read = 9); + let path = [crate::tests::TEST_LEAF, b"psit"]; + + macro_rules! assert_version_rejected { + ($label:expr, $call:expr) => { + match $call.unwrap() { + Err(Error::VersionError(_)) => {} + other => panic!( + "{} must reject an unknown read version even for an inverted range, \ + got {:?}", + $label, other + ), + } + }; + } + + assert_version_rejected!( + "indexed_sum_range", + db.indexed_sum_range(path.as_ref(), 100, 0, true, 10, None, &bad) + ); + assert_version_rejected!( + "indexed_count_range", + db.indexed_count_range(path.as_ref(), 100, 0, true, 10, None, &bad) + ); + assert_version_rejected!( + "indexed_avg_range", + db.indexed_avg_range(path.as_ref(), 100, 0, true, 10, None, &bad) + ); + // The aggregate readers already gated before their fast path; + // pin that so a future refactor cannot reintroduce the same gap. + assert_version_rejected!( + "indexed_sum_range_aggregate", + db.indexed_sum_range_aggregate(path.as_ref(), 100, 0, None, &bad) + ); + assert_version_rejected!( + "indexed_count_range_aggregate", + db.indexed_count_range_aggregate(path.as_ref(), 100, 0, None, &bad) + ); + + // At the real version an inverted range still answers empty + // rather than erroring — the gate moved, the semantics did not. + let empty = db + .indexed_sum_range(path.as_ref(), 100, 0, true, 10, None, real) + .unwrap() + .expect("inverted range is still a valid, empty answer"); + assert!(empty.is_empty()); + } +} diff --git a/grovedb/src/tests/mod.rs b/grovedb/src/tests/mod.rs index e1bd945ba..50aa09f0d 100644 --- a/grovedb/src/tests/mod.rs +++ b/grovedb/src/tests/mod.rs @@ -61,6 +61,7 @@ mod indexed_axis_proof_tests; mod indexed_tree_secondary_drift_tests; mod indexed_tree_security_regression_tests; mod is_empty_tree_tests; +mod merge_versioning_tests; mod misc_coverage_tests; mod mmr_tree_tests; mod non_counted_tests;