diff --git a/grovedb-query/src/aggregate_count.rs b/grovedb-query/src/aggregate_count.rs new file mode 100644 index 000000000..986978a70 --- /dev/null +++ b/grovedb-query/src/aggregate_count.rs @@ -0,0 +1,877 @@ +//! `AggregateCountOnRange` Query helpers and validation. +//! +//! This module owns the Query-level construction, detection, and +//! validation of `AggregateCountOnRange` queries. They come in two +//! shapes: +//! +//! - **Leaf** — a query whose single item is `AggregateCountOnRange(_)`. +//! Produces a single `u64` count over the inner range. +//! +//! - **Carrier** — a query whose items are `Key(_)` / `Range*(_)` and +//! whose `default_subquery_branch.subquery` resolves (after walking +//! the optional `subquery_path`) to a valid leaf +//! `AggregateCountOnRange`. Produces one `u64` per matched outer +//! key — the natural per-outer-key extension of the leaf shape. +//! +//! All aggregate-count validation lives in this file so the much larger +//! `Query` core in `query.rs` stays focused on the general-purpose +//! query plumbing. Forthcoming aggregate variants (sum, average) will +//! live in sibling modules (`aggregate_sum`, `aggregate_average`, …) +//! with parallel naming. + +use crate::{error::Error, query::Query, query_item::QueryItem}; + +impl Query { + /// Creates an aggregate-count-on-range query that counts the elements + /// matched by `range`. The resulting query has `AggregateCountOnRange(range)` + /// as its sole item, no subquery branches, and `left_to_right = true` + /// (counting is direction-agnostic). + /// + /// `range` must be a true range variant (`Range`, `RangeInclusive`, + /// `RangeFrom`, `RangeTo`, `RangeToInclusive`, `RangeAfter`, `RangeAfterTo`, + /// or `RangeAfterToInclusive`). Passing `Key`, `RangeFull`, or another + /// `AggregateCountOnRange` is allowed at construction time but will be + /// rejected by [`Self::validate_aggregate_count_on_range`]. + pub fn new_aggregate_count_on_range(range: QueryItem) -> Self { + Self { + items: vec![QueryItem::AggregateCountOnRange(Box::new(range))], + left_to_right: true, + ..Self::default() + } + } + + /// If this query contains an `AggregateCountOnRange` item *anywhere* in + /// its `items` vec, returns a reference to the first such item (whether + /// the surrounding query is well-formed or not). Returns `None` only + /// when no item is an `AggregateCountOnRange`. + /// + /// This is intentionally a **detection-only** helper: malformed queries + /// like `items: [Key(...), AggregateCountOnRange(...)]` still report + /// `Some(...)` here so callers don't accidentally route them through + /// the regular-query path. Use + /// [`Self::validate_aggregate_count_on_range`] when you also need to + /// enforce the well-formedness rules (single item, allowed inner kind, + /// no subqueries, etc.). + pub fn aggregate_count_on_range(&self) -> Option<&QueryItem> { + self.items + .iter() + .find(|item| item.is_aggregate_count_on_range()) + } + + /// Returns `true` if any item in this query — including items inside + /// nested subquery branches — is an `AggregateCountOnRange`. + /// + /// `AggregateCountOnRange` is a *terminal* item: a well-formed query + /// either contains exactly one `AggregateCountOnRange` at the top + /// level and nothing else (leaf shape) or contains + /// `Key`/`Range*` items at the top level with an aggregate-count nested in the + /// `default_subquery_branch.subquery` (carrier shape). This recursive + /// detector exists so the prover can validate up front: if any aggregate-count + /// is present anywhere, the query as a whole must satisfy + /// [`Self::validate_aggregate_count_on_range`] — otherwise a malformed + /// shape could slip past a top-level-only check and be silently routed + /// through the regular-proof path. + pub fn has_aggregate_count_on_range_anywhere(&self) -> bool { + if self.aggregate_count_on_range().is_some() { + return true; + } + if let Some(sub) = self.default_subquery_branch.subquery.as_deref() + && sub.has_aggregate_count_on_range_anywhere() + { + return true; + } + if let Some(branches) = &self.conditional_subquery_branches { + for branch in branches.values() { + if let Some(sub) = branch.subquery.as_deref() + && sub.has_aggregate_count_on_range_anywhere() + { + return true; + } + } + } + false + } + + /// Validates the Query-level constraints that apply when an + /// `AggregateCountOnRange` is present. On success, returns a reference + /// to the inner range `QueryItem` describing the keys being counted + /// (the same item regardless of whether the surrounding query is the + /// leaf shape or the carrier shape). + /// + /// Top-level dispatcher: classifies the query as either + /// - **leaf** (the query owns an `AggregateCountOnRange` item directly — + /// the original single-`u64` shape), or + /// - **carrier** (the query is an outer fan-out of `Key`/`Range` items + /// whose `default_subquery_branch.subquery` resolves to a leaf + /// `AggregateCountOnRange` — the per-outer-key shape) + /// + /// and forwards to the corresponding rule set. See + /// [`Self::validate_leaf_aggregate_count_on_range`] and + /// [`Self::validate_carrier_aggregate_count_on_range`] for the precise + /// rules in each case. + /// + /// `SizedQuery::limit` / `SizedQuery::offset` checks live at the + /// `PathQuery` / `SizedQuery` layer. + pub fn validate_aggregate_count_on_range(&self) -> Result<&QueryItem, Error> { + if self.aggregate_count_on_range().is_some() { + // Owns an aggregate-count at this level → leaf shape. + self.validate_leaf_aggregate_count_on_range() + } else if self.has_aggregate_count_on_range_anywhere() { + // Doesn't own an aggregate-count but a nested subquery does → carrier shape. + self.validate_carrier_aggregate_count_on_range() + } else { + Err(Error::InvalidOperation( + "validate_aggregate_count_on_range called on a query without an \ + AggregateCountOnRange item", + )) + } + } + + /// Validates the leaf shape: a query whose single item is + /// `AggregateCountOnRange(_)` and whose surroundings carry no subquery + /// branches. Returns a reference to the inner range `QueryItem`. + /// + /// Rules enforced (matching the constraints documented in the GroveDB + /// book chapter "Aggregate Count Queries"): + /// + /// 1. The query must contain exactly one item. + /// 2. That item must be `AggregateCountOnRange(_)`. + /// 3. The inner item must not be `Key` (use `has_raw` / `get_raw` for + /// existence tests). + /// 4. The inner item must not be `RangeFull` (read the parent + /// `Element::ProvableCountTree` / `Element::ProvableCountSumTree` + /// bytes directly for the unconditional total). + /// 5. The inner item must not itself be `AggregateCountOnRange`. + /// 6. `default_subquery_branch.subquery` and + /// `default_subquery_branch.subquery_path` must both be `None`. + /// 7. `conditional_subquery_branches` must be `None` or empty. + pub fn validate_leaf_aggregate_count_on_range(&self) -> Result<&QueryItem, Error> { + if self.items.len() != 1 { + return Err(Error::InvalidOperation( + "AggregateCountOnRange must be the only item in the query", + )); + } + let inner = match &self.items[0] { + QueryItem::AggregateCountOnRange(inner) => inner.as_ref(), + _ => { + return Err(Error::InvalidOperation( + "validate_aggregate_count_on_range called on a query without an \ + AggregateCountOnRange item", + )); + } + }; + match inner { + QueryItem::Key(_) => { + return Err(Error::InvalidOperation( + "AggregateCountOnRange may not wrap Key — use has_raw / get_raw for \ + existence tests", + )); + } + QueryItem::RangeFull(_) => { + return Err(Error::InvalidOperation( + "AggregateCountOnRange may not wrap RangeFull — read the parent \ + ProvableCountTree element for the unconditional total", + )); + } + QueryItem::AggregateCountOnRange(_) => { + return Err(Error::InvalidOperation( + "AggregateCountOnRange may not wrap another AggregateCountOnRange", + )); + } + _ => {} + } + if self.default_subquery_branch.subquery.is_some() + || self.default_subquery_branch.subquery_path.is_some() + { + return Err(Error::InvalidOperation( + "AggregateCountOnRange queries may not carry a default subquery branch", + )); + } + if let Some(branches) = &self.conditional_subquery_branches + && !branches.is_empty() + { + return Err(Error::InvalidOperation( + "AggregateCountOnRange queries may not carry conditional subquery branches", + )); + } + Ok(inner) + } + + /// Validates the carrier shape: an outer query whose items are + /// `Key`/`Range`-like (NOT `AggregateCountOnRange`), and whose + /// `default_subquery_branch.subquery` resolves to a valid leaf `AggregateCountOnRange` + /// query (possibly after walking a `subquery_path`). + /// + /// Returns a reference to the leaf's inner range `QueryItem` — the + /// same kind of value [`Self::validate_leaf_aggregate_count_on_range`] + /// returns for a leaf-shape query. + /// + /// Rules enforced: + /// 1. Items must be non-empty. + /// 2. Each item must be `Key(_)` or a `Range*(_)` variant — explicitly + /// NOT `AggregateCountOnRange` (those route through the leaf + /// validator) and NOT `RangeFull` (use a leaf `AggregateCountOnRange` on the parent + /// instead). + /// 3. `default_subquery_branch.subquery` must be `Some(_)`. Its target + /// query must itself validate as a leaf `AggregateCountOnRange` query. + /// 4. `default_subquery_branch.subquery_path` may be `Some(_)` + /// (typically names the path from each outer-key match to the leaf + /// subtree). When set, every element must be a non-empty key. + /// 5. `conditional_subquery_branches` must be `None` or empty + /// (out of scope for the initial implementation). + pub fn validate_carrier_aggregate_count_on_range(&self) -> Result<&QueryItem, Error> { + if self.items.is_empty() { + return Err(Error::InvalidOperation( + "carrier AggregateCountOnRange query must have at least one outer item", + )); + } + for item in &self.items { + match item { + QueryItem::Key(_) + | QueryItem::Range(_) + | QueryItem::RangeInclusive(_) + | QueryItem::RangeFrom(_) + | QueryItem::RangeTo(_) + | QueryItem::RangeToInclusive(_) + | QueryItem::RangeAfter(_) + | QueryItem::RangeAfterTo(_) + | QueryItem::RangeAfterToInclusive(_) => {} + QueryItem::RangeFull(_) => { + return Err(Error::InvalidOperation( + "carrier AggregateCountOnRange query may not have a RangeFull outer item", + )); + } + QueryItem::AggregateCountOnRange(_) => { + return Err(Error::InvalidOperation( + "carrier AggregateCountOnRange query may not own an \ + AggregateCountOnRange item — use the leaf shape instead", + )); + } + } + } + let subquery = match self.default_subquery_branch.subquery.as_deref() { + Some(sub) => sub, + None => { + return Err(Error::InvalidOperation( + "carrier AggregateCountOnRange query must set \ + default_subquery_branch.subquery to a leaf `AggregateCountOnRange` query", + )); + } + }; + if let Some(path) = &self.default_subquery_branch.subquery_path + && path.iter().any(|k| k.is_empty()) + { + return Err(Error::InvalidOperation( + "carrier AggregateCountOnRange query's subquery_path must contain non-empty keys", + )); + } + if let Some(branches) = &self.conditional_subquery_branches + && !branches.is_empty() + { + return Err(Error::InvalidOperation( + "carrier AggregateCountOnRange query may not carry conditional subquery \ + branches (out of scope for this feature)", + )); + } + // The subquery must validate as a leaf `AggregateCountOnRange` (which is what the + // proof descent will ultimately consume). + subquery.validate_leaf_aggregate_count_on_range() + } +} + +#[cfg(test)] +mod tests { + use indexmap::IndexMap; + + use crate::{query_item::QueryItem, Query, SubqueryBranch}; + + // ---------- Leaf aggregate-count validation tests ---------- + // + // These hit each numbered rule in + // `Query::validate_leaf_aggregate_count_on_range` independently. The + // happy path is also covered to ensure the success arm returns the + // inner range. + + fn make_aggregate_count_query(inner: QueryItem) -> Query { + Query::new_aggregate_count_on_range(inner) + } + + #[test] + fn validate_aggregate_count_happy_path_returns_inner() { + let q = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + let inner = q + .validate_aggregate_count_on_range() + .expect("happy path should validate"); + match inner { + QueryItem::Range(r) => { + assert_eq!(r.start, b"a".to_vec()); + assert_eq!(r.end, b"z".to_vec()); + } + _ => panic!("expected inner Range"), + } + } + + #[test] + fn validate_aggregate_count_rejects_extra_items() { + let mut q = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + q.items.push(QueryItem::Key(b"extra".to_vec())); + let err = q + .validate_aggregate_count_on_range() + .expect_err("two-item query must fail"); + assert!(matches!(err, crate::error::Error::InvalidOperation(_))); + } + + #[test] + fn validate_aggregate_count_rejects_non_aggregate_count_only_item() { + // A query with one item that isn't AggregateCountOnRange triggers the + // "validate called on a query without an AggregateCountOnRange item" + // branch. + let q = Query::new_single_query_item(QueryItem::Key(b"k".to_vec())); + let err = q + .validate_aggregate_count_on_range() + .expect_err("non-aggregate-count-only item must fail"); + assert!(matches!(err, crate::error::Error::InvalidOperation(_))); + } + + #[test] + fn validate_aggregate_count_rejects_inner_key() { + let q = make_aggregate_count_query(QueryItem::Key(b"k".to_vec())); + let err = q + .validate_aggregate_count_on_range() + .expect_err("inner Key must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => assert!(msg.contains("Key")), + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_aggregate_count_rejects_inner_range_full() { + let q = make_aggregate_count_query(QueryItem::RangeFull(std::ops::RangeFull)); + let err = q + .validate_aggregate_count_on_range() + .expect_err("inner RangeFull must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => assert!(msg.contains("RangeFull")), + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_aggregate_count_rejects_nested_aggregate_count() { + // AggregateCountOnRange wrapping another AggregateCountOnRange. + let inner_aggregate_count = QueryItem::AggregateCountOnRange(Box::new(QueryItem::Range( + b"a".to_vec()..b"z".to_vec(), + ))); + let q = make_aggregate_count_query(inner_aggregate_count); + let err = q + .validate_aggregate_count_on_range() + .expect_err("nested `AggregateCountOnRange` must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => { + assert!(msg.contains("AggregateCountOnRange")) + } + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_aggregate_count_rejects_default_subquery_branch() { + let mut q = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + q.default_subquery_branch = SubqueryBranch { + subquery_path: None, + subquery: Some(Box::new(Query::new())), + }; + let err = q + .validate_aggregate_count_on_range() + .expect_err("default subquery branch must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => assert!(msg.contains("subquery")), + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_aggregate_count_rejects_default_subquery_path() { + let mut q = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + q.default_subquery_branch = SubqueryBranch { + subquery_path: Some(vec![b"x".to_vec()]), + subquery: None, + }; + let err = q + .validate_aggregate_count_on_range() + .expect_err("subquery_path must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => assert!(msg.contains("subquery")), + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_aggregate_count_rejects_conditional_subquery_branches() { + let mut q = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + let mut branches = IndexMap::new(); + branches.insert( + QueryItem::Key(b"k".to_vec()), + SubqueryBranch { + subquery_path: None, + subquery: Some(Box::new(Query::new())), + }, + ); + q.conditional_subquery_branches = Some(branches); + let err = q + .validate_aggregate_count_on_range() + .expect_err("conditional branches must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => { + assert!(msg.contains("conditional")); + } + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_aggregate_count_accepts_empty_conditional_branches_map() { + // An empty `Some(IndexMap::new())` is treated as "no branches" by the + // validator (the rule enforces non-empty rejection only). + let mut q = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + q.conditional_subquery_branches = Some(IndexMap::new()); + let inner = q + .validate_aggregate_count_on_range() + .expect("empty conditional map must validate"); + assert!(matches!(inner, QueryItem::Range(_))); + } + + #[test] + fn aggregate_count_on_range_helper_detects_aggregate_count_anywhere_in_items() { + // Well-formed shape — single aggregate-count item. + let q = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + assert!(q.aggregate_count_on_range().is_some()); + + // Two items including aggregate-count → still detected, so the routing layer + // can hand the malformed query to validate_aggregate_count_on_range + // for a precise error rather than silently treating it as a regular + // query. + let mut q2 = q.clone(); + q2.items.push(QueryItem::Key(b"x".to_vec())); + assert!( + q2.aggregate_count_on_range().is_some(), + "aggregate-count + extra item must still be detected as aggregate-count-bearing" + ); + + // aggregate-count not at index 0 — also detected. + let mut q3 = Query::new_single_query_item(QueryItem::Key(b"x".to_vec())); + q3.items.push(QueryItem::AggregateCountOnRange(Box::new( + QueryItem::Range(b"a".to_vec()..b"z".to_vec()), + ))); + assert!(q3.aggregate_count_on_range().is_some()); + + // No aggregate-count anywhere → None. + let q4 = Query::new_single_query_item(QueryItem::Key(b"x".to_vec())); + assert!(q4.aggregate_count_on_range().is_none()); + + // Empty items → None. + let q5 = Query::new(); + assert!(q5.aggregate_count_on_range().is_none()); + } + + #[test] + fn has_aggregate_count_on_range_anywhere_walks_subqueries() { + // No aggregate-count anywhere → false. + let plain = Query::new_single_query_item(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + assert!(!plain.has_aggregate_count_on_range_anywhere()); + + // Top-level aggregate-count → true (covered by `aggregate_count_on_range` too). + let top = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + assert!(top.has_aggregate_count_on_range_anywhere()); + + // aggregate-count hidden inside `default_subquery_branch.subquery` — the + // top-level-only `aggregate_count_on_range` would miss it, but the + // recursive helper finds it. This is the surface that the + // prove_query entry-point gate uses to refuse to run any + // aggregate-count-bearing query that isn't a canonical leaf-or-carrier shape. + let inner_aggregate_count = + make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + let mut hidden = + Query::new_single_query_item(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + hidden.set_subquery(inner_aggregate_count); + assert!(hidden.aggregate_count_on_range().is_none()); + assert!( + hidden.has_aggregate_count_on_range_anywhere(), + "aggregate-count hidden in default subquery branch must be detected" + ); + + // aggregate-count hidden in a conditional subquery branch. + let inner_aggregate_count2 = + make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + let mut conditional = + Query::new_single_query_item(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + conditional.add_conditional_subquery( + QueryItem::Key(b"k".to_vec()), + None, + Some(inner_aggregate_count2), + ); + assert!( + conditional.has_aggregate_count_on_range_anywhere(), + "aggregate-count hidden in conditional subquery branch must be detected" + ); + } + + // ---------- Carrier aggregate-count validation tests ---------- + // + // The carrier shape is an outer query with `Key`/`Range*` items whose + // `default_subquery_branch.subquery` resolves to a leaf `AggregateCountOnRange` query. + // It is the multi-outer-key extension of the leaf shape, returning one + // count per outer key. These tests verify the + // `validate_carrier_aggregate_count_on_range` rules and the dispatcher + // behavior of the top-level `validate_aggregate_count_on_range`. + + fn make_leaf_aggregate_count_subquery() -> Query { + make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())) + } + + #[test] + fn validate_carrier_aggregate_count_happy_path_keys_outer_with_subquery_path() { + let mut carrier = Query::new(); + carrier.items.push(QueryItem::Key(b"brand_000".to_vec())); + carrier.items.push(QueryItem::Key(b"brand_001".to_vec())); + carrier.set_subquery_path(vec![b"color".to_vec()]); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + // Top-level dispatcher accepts the carrier and returns the leaf's + // inner range. + let inner = carrier + .validate_aggregate_count_on_range() + .expect("carrier should validate"); + assert!(matches!(inner, QueryItem::Range(_))); + // And the dedicated carrier validator agrees. + carrier + .validate_carrier_aggregate_count_on_range() + .expect("carrier validator should accept"); + // Leaf validator must reject (carrier-level items aren't aggregate-count). + assert!(carrier.validate_leaf_aggregate_count_on_range().is_err()); + } + + #[test] + fn validate_carrier_aggregate_count_happy_path_no_subquery_path() { + // subquery_path is optional — the leaf `AggregateCountOnRange` may be directly under + // each outer match. + let mut carrier = Query::new(); + carrier.items.push(QueryItem::Key(b"a".to_vec())); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + carrier + .validate_aggregate_count_on_range() + .expect("carrier without subquery_path should validate"); + } + + #[test] + fn validate_carrier_aggregate_count_rejects_aggregate_count_at_both_levels() { + // Carrier itself owns an aggregate-count AND its subquery is also an aggregate-count. + // The top-level dispatcher routes to the LEAF validator first + // (because aggregate_count_on_range() returns Some at carrier + // level), so the leaf's "single item" rule catches the + // aggregate-count-in-subquery shape via the items-len check or the + // no-subquery rule. Either way the error fires. + let mut q = make_aggregate_count_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + q.set_subquery(make_leaf_aggregate_count_subquery()); + let err = q + .validate_aggregate_count_on_range() + .expect_err("aggregate-count at both levels must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => { + assert!( + msg.contains("AggregateCountOnRange") || msg.contains("subquery"), + "unexpected message: {msg}" + ); + } + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_carrier_aggregate_count_rejects_range_full_outer() { + let mut carrier = Query::new(); + carrier + .items + .push(QueryItem::RangeFull(std::ops::RangeFull)); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + let err = carrier + .validate_aggregate_count_on_range() + .expect_err("RangeFull outer must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => { + assert!(msg.contains("RangeFull"), "unexpected message: {msg}"); + } + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_carrier_aggregate_count_rejects_aggregate_count_outer_item() { + // Both a Key and an AggregateCountOnRange item at the carrier + // level. The leaf validator's items-len check fires first (since + // there's an aggregate-count item in items, aggregate_count_on_range() + // returns Some, and len != 1). + let mut carrier = Query::new(); + carrier.items.push(QueryItem::Key(b"k".to_vec())); + carrier + .items + .push(QueryItem::AggregateCountOnRange(Box::new( + QueryItem::Range(b"a".to_vec()..b"z".to_vec()), + ))); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + let err = carrier + .validate_aggregate_count_on_range() + .expect_err("aggregate-count + Key outer items must fail"); + assert!(matches!(err, crate::error::Error::InvalidOperation(_))); + } + + #[test] + fn validate_carrier_aggregate_count_rejects_carrier_with_missing_subquery() { + // Outer items present but no subquery → not a carrier (and not a + // leaf), so the top-level dispatcher routes to the + // "not an aggregate-count query" error. + let mut carrier = Query::new(); + carrier.items.push(QueryItem::Key(b"k".to_vec())); + let err = carrier + .validate_aggregate_count_on_range() + .expect_err("carrier without subquery must fail"); + assert!(matches!(err, crate::error::Error::InvalidOperation(_))); + } + + #[test] + fn validate_carrier_aggregate_count_rejects_non_aggregate_count_subquery() { + // Outer Keys + subquery that is NOT an aggregate-count (just a regular range + // query) → not a valid carrier aggregate-count. The top-level dispatcher + // sees `has_aggregate_count_on_range_anywhere() == false`, so it + // surfaces the "not an aggregate-count query" error rather than the carrier + // validator's "subquery must validate as leaf `AggregateCountOnRange`" error. + let mut carrier = Query::new(); + carrier.items.push(QueryItem::Key(b"k".to_vec())); + let regular_sub = + Query::new_single_query_item(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + carrier.set_subquery(regular_sub); + let err = carrier + .validate_aggregate_count_on_range() + .expect_err("non-aggregate-count subquery must fail"); + assert!(matches!(err, crate::error::Error::InvalidOperation(_))); + } + + #[test] + fn validate_carrier_aggregate_count_rejects_conditional_branches() { + let mut carrier = Query::new(); + carrier.items.push(QueryItem::Key(b"k".to_vec())); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + carrier.add_conditional_subquery( + QueryItem::Key(b"k".to_vec()), + None, + Some(make_leaf_aggregate_count_subquery()), + ); + let err = carrier + .validate_aggregate_count_on_range() + .expect_err("carrier conditional branches must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => { + assert!(msg.contains("conditional"), "unexpected message: {msg}") + } + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_carrier_aggregate_count_rejects_empty_outer_items() { + // Empty items + leaf `AggregateCountOnRange` subquery → not a valid carrier. + // (Empty outer means no outer key to iterate; doesn't make sense.) + let mut carrier = Query::new(); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + let err = carrier + .validate_carrier_aggregate_count_on_range() + .expect_err("empty outer items must fail"); + assert!(matches!(err, crate::error::Error::InvalidOperation(_))); + } + + #[test] + fn validate_carrier_aggregate_count_rejects_nested_carrier() { + // Out of scope: a "Range × Range × AggregateCountOnRange" + // shape — i.e. an outer carrier whose subquery is itself + // another carrier (with its own Range outer items + leaf + // AggregateCountOnRange subquery). This is the + // `IN × IN`-on-prefix case the spec explicitly defers. + // + // The carrier validator delegates to the *leaf* validator for + // the subquery, so a carrier-of-carrier subquery fails the + // leaf rules ("must contain exactly one item" / + // "validate called on a query without an AggregateCountOnRange + // item") — exactly what we want. + let mut inner_carrier = Query::new(); + inner_carrier + .items + .push(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + inner_carrier.set_subquery(make_leaf_aggregate_count_subquery()); + + let mut outer_carrier = Query::new(); + outer_carrier + .items + .push(QueryItem::Range(b"A".to_vec()..b"Z".to_vec())); + outer_carrier.set_subquery(inner_carrier); + + let err = outer_carrier + .validate_aggregate_count_on_range() + .expect_err("nested carrier (Range x Range x ACOR) must be rejected"); + assert!(matches!(err, crate::error::Error::InvalidOperation(_))); + } + + #[test] + fn validate_carrier_aggregate_count_rejects_carrier_subquery_with_invalid_inner() { + // The carrier validator delegates to the leaf validator for the + // subquery, so a malformed leaf `AggregateCountOnRange` (e.g. wrapping `Key`) is + // surfaced via the carrier path. Pin the exact rejection message + // so a refactor that re-routes the rejection through a different + // arm doesn't silently accept the malformed shape. + let mut carrier = Query::new(); + carrier.items.push(QueryItem::Key(b"k".to_vec())); + carrier.set_subquery(make_aggregate_count_query(QueryItem::Key(b"k".to_vec()))); + let err = carrier + .validate_aggregate_count_on_range() + .expect_err("malformed inner Key in subquery aggregate-count must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => assert!( + msg.contains("may not wrap Key"), + "unexpected message: {msg}" + ), + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_carrier_aggregate_count_rejects_empty_subquery_path_element() { + // A carrier's subquery_path may not contain empty keys — those + // would point at "no key" in the intermediate descent, which the + // merk single-key prover can't satisfy. + let mut carrier = Query::new(); + carrier.items.push(QueryItem::Key(b"k".to_vec())); + carrier.set_subquery_path(vec![b"".to_vec()]); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + let err = carrier + .validate_aggregate_count_on_range() + .expect_err("empty subquery_path key must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => { + assert!(msg.contains("non-empty keys"), "unexpected message: {msg}") + } + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_carrier_aggregate_count_accepts_range_outer_items() { + // A carrier may use Range outer items (the spec leaves room for + // this). Verify the validator agrees for every Range* variant + // the rule whitelists. + for outer in [ + QueryItem::Range(b"a".to_vec()..b"z".to_vec()), + QueryItem::RangeInclusive(b"a".to_vec()..=b"z".to_vec()), + QueryItem::RangeFrom(b"a".to_vec()..), + QueryItem::RangeTo(..b"z".to_vec()), + QueryItem::RangeToInclusive(..=b"z".to_vec()), + QueryItem::RangeAfter(b"a".to_vec()..), + QueryItem::RangeAfterTo(b"a".to_vec()..b"z".to_vec()), + QueryItem::RangeAfterToInclusive(b"a".to_vec()..=b"z".to_vec()), + ] { + let mut carrier = Query::new(); + carrier.items.push(outer); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + carrier + .validate_aggregate_count_on_range() + .expect("carrier with Range* outer should validate"); + } + } + + #[test] + fn validate_aggregate_count_dispatcher_rejects_non_aggregate_count_query() { + // The top-level dispatcher returns the "not an aggregate-count" error when + // neither shape matches. + let q = Query::new_single_query_item(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + let err = q + .validate_aggregate_count_on_range() + .expect_err("non-aggregate-count query must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => assert!(msg.contains( + "validate_aggregate_count_on_range called on a query \ + without an AggregateCountOnRange item" + )), + _ => panic!("expected InvalidOperation"), + } + } + + // ---------- Direct carrier-validator branch coverage ---------- + // + // The top-level dispatcher classifies first and routes to either + // `validate_leaf_aggregate_count_on_range` or + // `validate_carrier_aggregate_count_on_range`. Some carrier rules + // (no subquery, aggregate-count outer item) get masked by classification — + // calling the carrier validator directly is the only way to exercise + // them. These tests pin those branches. + + #[test] + fn validate_carrier_aggregate_count_direct_rejects_missing_subquery() { + // Carrier-shaped items but no subquery — the carrier + // validator's "subquery must be Some" branch fires. + let mut carrier = Query::new(); + carrier.insert_key(b"k".to_vec()); + let err = carrier + .validate_carrier_aggregate_count_on_range() + .expect_err("missing subquery must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => { + assert!(msg.contains("must set"), "unexpected message: {msg}") + } + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_carrier_aggregate_count_direct_rejects_aggregate_count_outer_item() { + // aggregate-count appears in outer items + a leaf subquery is set. The + // top-level dispatcher routes to the leaf validator (because + // `aggregate_count_on_range()` returns Some at the carrier + // level); calling the carrier validator directly is the only + // way to hit the carrier-side rule. + let mut carrier = Query::new(); + carrier + .items + .push(QueryItem::AggregateCountOnRange(Box::new( + QueryItem::Range(b"a".to_vec()..b"z".to_vec()), + ))); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + let err = carrier + .validate_carrier_aggregate_count_on_range() + .expect_err("aggregate-count outer item via direct carrier validator must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => assert!( + msg.contains("may not own an") || msg.contains("AggregateCountOnRange"), + "unexpected message: {msg}" + ), + _ => panic!("expected InvalidOperation"), + } + } + + #[test] + fn validate_carrier_aggregate_count_direct_rejects_range_full_outer() { + // RangeFull outer + leaf subquery — exercise the carrier + // validator's `RangeFull` arm directly. + let mut carrier = Query::new(); + carrier + .items + .push(QueryItem::RangeFull(std::ops::RangeFull)); + carrier.set_subquery(make_leaf_aggregate_count_subquery()); + let err = carrier + .validate_carrier_aggregate_count_on_range() + .expect_err("RangeFull outer via direct carrier validator must fail"); + match err { + crate::error::Error::InvalidOperation(msg) => { + assert!(msg.contains("RangeFull"), "unexpected message: {msg}") + } + _ => panic!("expected InvalidOperation"), + } + } +} diff --git a/grovedb-query/src/lib.rs b/grovedb-query/src/lib.rs index 3bc3f834d..69605f03f 100644 --- a/grovedb-query/src/lib.rs +++ b/grovedb-query/src/lib.rs @@ -9,6 +9,11 @@ /// Error types for query operations. pub mod error; +/// `AggregateCountOnRange` (ACOR) construction and validation. The +/// shape, detection, and leaf/carrier rule sets live here; the proof +/// generation and verification live in the `grovedb` crate. +mod aggregate_count; + /// Aggregate sum query for sum-up-to style queries. pub mod aggregate_sum_query; diff --git a/grovedb-query/src/query.rs b/grovedb-query/src/query.rs index 8f1c3fc00..2ab979994 100644 --- a/grovedb-query/src/query.rs +++ b/grovedb-query/src/query.rs @@ -303,149 +303,6 @@ impl Query { } } - /// Creates an aggregate-count-on-range query that counts the elements - /// matched by `range`. The resulting query has `AggregateCountOnRange(range)` - /// as its sole item, no subquery branches, and `left_to_right = true` - /// (counting is direction-agnostic). - /// - /// `range` must be a true range variant (`Range`, `RangeInclusive`, - /// `RangeFrom`, `RangeTo`, `RangeToInclusive`, `RangeAfter`, `RangeAfterTo`, - /// or `RangeAfterToInclusive`). Passing `Key`, `RangeFull`, or another - /// `AggregateCountOnRange` is allowed at construction time but will be - /// rejected by [`validate_aggregate_count_on_range`]. - pub fn new_aggregate_count_on_range(range: QueryItem) -> Self { - Self { - items: vec![QueryItem::AggregateCountOnRange(Box::new(range))], - left_to_right: true, - ..Self::default() - } - } - - /// If this query contains an `AggregateCountOnRange` item *anywhere* in - /// its `items` vec, returns a reference to the first such item (whether - /// the surrounding query is well-formed or not). Returns `None` only - /// when no item is an `AggregateCountOnRange`. - /// - /// This is intentionally a **detection-only** helper: malformed queries - /// like `items: [Key(...), AggregateCountOnRange(...)]` still report - /// `Some(...)` here so callers don't accidentally route them through - /// the regular-query path. Use - /// [`Self::validate_aggregate_count_on_range`] when you also need to - /// enforce the well-formedness rules (single item, allowed inner kind, - /// no subqueries, etc.). - pub fn aggregate_count_on_range(&self) -> Option<&QueryItem> { - self.items - .iter() - .find(|item| item.is_aggregate_count_on_range()) - } - - /// Returns `true` if any item in this query — including items inside - /// nested subquery branches — is an `AggregateCountOnRange`. - /// - /// `AggregateCountOnRange` is a *terminal* item: the canonical - /// well-formed query contains exactly one `AggregateCountOnRange` at - /// the top level and nothing else. This recursive detector exists so - /// the prover can validate up front: if any ACOR is present anywhere, - /// the query as a whole must satisfy - /// [`Self::validate_aggregate_count_on_range`] — otherwise a malformed - /// shape (e.g. ACOR hidden inside `default_subquery_branch.subquery`) - /// could slip past a top-level-only check and be silently routed - /// through the regular-proof path. - pub fn has_aggregate_count_on_range_anywhere(&self) -> bool { - if self.aggregate_count_on_range().is_some() { - return true; - } - if let Some(sub) = self.default_subquery_branch.subquery.as_deref() - && sub.has_aggregate_count_on_range_anywhere() - { - return true; - } - if let Some(branches) = &self.conditional_subquery_branches { - for branch in branches.values() { - if let Some(sub) = branch.subquery.as_deref() - && sub.has_aggregate_count_on_range_anywhere() - { - return true; - } - } - } - false - } - - /// Validates the Query-level constraints that apply when an - /// `AggregateCountOnRange` is present. On success, returns a reference - /// to the inner `QueryItem` describing the range to count. - /// - /// Rules enforced (matching the constraints documented in the GroveDB - /// book chapter "Aggregate Count Queries"): - /// - /// 1. The query must contain exactly one item. - /// 2. That item must be `AggregateCountOnRange(_)`. - /// 3. The inner item must not be `Key` (use `has_raw` / `get_raw` for - /// existence tests). - /// 4. The inner item must not be `RangeFull` (read the parent - /// `Element::ProvableCountTree` / `Element::ProvableCountSumTree` - /// bytes directly for the unconditional total). - /// 5. The inner item must not itself be `AggregateCountOnRange`. - /// 6. `default_subquery_branch.subquery` and - /// `default_subquery_branch.subquery_path` must both be `None`. - /// 7. `conditional_subquery_branches` must be `None` or empty. - /// - /// `SizedQuery::limit` / `SizedQuery::offset` checks live at the - /// `PathQuery` / `SizedQuery` layer (see - /// [`SizedQuery::validate_aggregate_count_on_range`]). - pub fn validate_aggregate_count_on_range(&self) -> Result<&QueryItem, Error> { - if self.items.len() != 1 { - return Err(Error::InvalidOperation( - "AggregateCountOnRange must be the only item in the query", - )); - } - let inner = match &self.items[0] { - QueryItem::AggregateCountOnRange(inner) => inner.as_ref(), - _ => { - return Err(Error::InvalidOperation( - "validate_aggregate_count_on_range called on a query without an \ - AggregateCountOnRange item", - )); - } - }; - match inner { - QueryItem::Key(_) => { - return Err(Error::InvalidOperation( - "AggregateCountOnRange may not wrap Key — use has_raw / get_raw for \ - existence tests", - )); - } - QueryItem::RangeFull(_) => { - return Err(Error::InvalidOperation( - "AggregateCountOnRange may not wrap RangeFull — read the parent \ - ProvableCountTree element for the unconditional total", - )); - } - QueryItem::AggregateCountOnRange(_) => { - return Err(Error::InvalidOperation( - "AggregateCountOnRange may not wrap another AggregateCountOnRange", - )); - } - _ => {} - } - if self.default_subquery_branch.subquery.is_some() - || self.default_subquery_branch.subquery_path.is_some() - { - return Err(Error::InvalidOperation( - "AggregateCountOnRange queries may not carry a default subquery branch", - )); - } - if let Some(branches) = &self.conditional_subquery_branches - && !branches.is_empty() - { - return Err(Error::InvalidOperation( - "AggregateCountOnRange queries may not carry conditional subquery branches", - )); - } - Ok(inner) - } - /// Returns `true` if the given key would trigger a subquery (either via /// the default subquery branch or a matching conditional branch). pub fn has_subquery_on_key(&self, key: &[u8], in_path: bool) -> bool { @@ -1050,233 +907,4 @@ mod tests { "innermost query should have no further subquery" ); } - - // ---------- AggregateCountOnRange validation tests ---------- - // - // These hit each numbered rule in `Query::validate_aggregate_count_on_range` - // independently. The happy path is also covered to ensure the success - // arm returns the inner range. - - fn make_acor_query(inner: QueryItem) -> Query { - Query::new_aggregate_count_on_range(inner) - } - - #[test] - fn validate_acor_happy_path_returns_inner() { - let q = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - let inner = q - .validate_aggregate_count_on_range() - .expect("happy path should validate"); - match inner { - QueryItem::Range(r) => { - assert_eq!(r.start, b"a".to_vec()); - assert_eq!(r.end, b"z".to_vec()); - } - _ => panic!("expected inner Range"), - } - } - - #[test] - fn validate_acor_rejects_extra_items() { - let mut q = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - q.items.push(QueryItem::Key(b"extra".to_vec())); - let err = q - .validate_aggregate_count_on_range() - .expect_err("two-item query must fail"); - assert!(matches!(err, crate::error::Error::InvalidOperation(_))); - } - - #[test] - fn validate_acor_rejects_non_acor_only_item() { - // A query with one item that isn't AggregateCountOnRange triggers the - // "validate called on a query without an AggregateCountOnRange item" - // branch. - let q = Query::new_single_query_item(QueryItem::Key(b"k".to_vec())); - let err = q - .validate_aggregate_count_on_range() - .expect_err("non-ACOR-only item must fail"); - assert!(matches!(err, crate::error::Error::InvalidOperation(_))); - } - - #[test] - fn validate_acor_rejects_inner_key() { - let q = make_acor_query(QueryItem::Key(b"k".to_vec())); - let err = q - .validate_aggregate_count_on_range() - .expect_err("inner Key must fail"); - match err { - crate::error::Error::InvalidOperation(msg) => assert!(msg.contains("Key")), - _ => panic!("expected InvalidOperation"), - } - } - - #[test] - fn validate_acor_rejects_inner_range_full() { - let q = make_acor_query(QueryItem::RangeFull(std::ops::RangeFull)); - let err = q - .validate_aggregate_count_on_range() - .expect_err("inner RangeFull must fail"); - match err { - crate::error::Error::InvalidOperation(msg) => assert!(msg.contains("RangeFull")), - _ => panic!("expected InvalidOperation"), - } - } - - #[test] - fn validate_acor_rejects_nested_acor() { - // AggregateCountOnRange wrapping another AggregateCountOnRange. - let inner_acor = QueryItem::AggregateCountOnRange(Box::new(QueryItem::Range( - b"a".to_vec()..b"z".to_vec(), - ))); - let q = make_acor_query(inner_acor); - let err = q - .validate_aggregate_count_on_range() - .expect_err("nested ACOR must fail"); - match err { - crate::error::Error::InvalidOperation(msg) => { - assert!(msg.contains("AggregateCountOnRange")) - } - _ => panic!("expected InvalidOperation"), - } - } - - #[test] - fn validate_acor_rejects_default_subquery_branch() { - let mut q = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - q.default_subquery_branch = SubqueryBranch { - subquery_path: None, - subquery: Some(Box::new(Query::new())), - }; - let err = q - .validate_aggregate_count_on_range() - .expect_err("default subquery branch must fail"); - match err { - crate::error::Error::InvalidOperation(msg) => assert!(msg.contains("subquery")), - _ => panic!("expected InvalidOperation"), - } - } - - #[test] - fn validate_acor_rejects_default_subquery_path() { - let mut q = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - q.default_subquery_branch = SubqueryBranch { - subquery_path: Some(vec![b"x".to_vec()]), - subquery: None, - }; - let err = q - .validate_aggregate_count_on_range() - .expect_err("subquery_path must fail"); - match err { - crate::error::Error::InvalidOperation(msg) => assert!(msg.contains("subquery")), - _ => panic!("expected InvalidOperation"), - } - } - - #[test] - fn validate_acor_rejects_conditional_subquery_branches() { - let mut q = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - let mut branches = IndexMap::new(); - branches.insert( - QueryItem::Key(b"k".to_vec()), - SubqueryBranch { - subquery_path: None, - subquery: Some(Box::new(Query::new())), - }, - ); - q.conditional_subquery_branches = Some(branches); - let err = q - .validate_aggregate_count_on_range() - .expect_err("conditional branches must fail"); - match err { - crate::error::Error::InvalidOperation(msg) => { - assert!(msg.contains("conditional")); - } - _ => panic!("expected InvalidOperation"), - } - } - - #[test] - fn validate_acor_accepts_empty_conditional_branches_map() { - // An empty `Some(IndexMap::new())` is treated as "no branches" by the - // validator (the rule enforces non-empty rejection only). - let mut q = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - q.conditional_subquery_branches = Some(IndexMap::new()); - let inner = q - .validate_aggregate_count_on_range() - .expect("empty conditional map must validate"); - assert!(matches!(inner, QueryItem::Range(_))); - } - - #[test] - fn aggregate_count_on_range_helper_detects_acor_anywhere_in_items() { - // Well-formed shape — single ACOR item. - let q = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - assert!(q.aggregate_count_on_range().is_some()); - - // Two items including ACOR → still detected, so the routing layer - // can hand the malformed query to validate_aggregate_count_on_range - // for a precise error rather than silently treating it as a regular - // query. - let mut q2 = q.clone(); - q2.items.push(QueryItem::Key(b"x".to_vec())); - assert!( - q2.aggregate_count_on_range().is_some(), - "ACOR + extra item must still be detected as ACOR-bearing" - ); - - // ACOR not at index 0 — also detected. - let mut q3 = Query::new_single_query_item(QueryItem::Key(b"x".to_vec())); - q3.items.push(QueryItem::AggregateCountOnRange(Box::new( - QueryItem::Range(b"a".to_vec()..b"z".to_vec()), - ))); - assert!(q3.aggregate_count_on_range().is_some()); - - // No ACOR anywhere → None. - let q4 = Query::new_single_query_item(QueryItem::Key(b"x".to_vec())); - assert!(q4.aggregate_count_on_range().is_none()); - - // Empty items → None. - let q5 = Query::new(); - assert!(q5.aggregate_count_on_range().is_none()); - } - - #[test] - fn has_aggregate_count_on_range_anywhere_walks_subqueries() { - // No ACOR anywhere → false. - let plain = Query::new_single_query_item(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - assert!(!plain.has_aggregate_count_on_range_anywhere()); - - // Top-level ACOR → true (covered by `aggregate_count_on_range` too). - let top = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - assert!(top.has_aggregate_count_on_range_anywhere()); - - // ACOR hidden inside `default_subquery_branch.subquery` — the - // top-level-only `aggregate_count_on_range` would miss it, but the - // recursive helper finds it. This is the surface that the - // prove_query entry-point gate uses to refuse to run any - // ACOR-bearing query that isn't the canonical single-ACOR shape. - let inner_acor = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - let mut hidden = - Query::new_single_query_item(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - hidden.set_subquery(inner_acor); - assert!(hidden.aggregate_count_on_range().is_none()); - assert!( - hidden.has_aggregate_count_on_range_anywhere(), - "ACOR hidden in default subquery branch must be detected" - ); - - // ACOR hidden in a conditional subquery branch. - let inner_acor2 = make_acor_query(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - let mut conditional = - Query::new_single_query_item(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); - conditional.add_conditional_subquery( - QueryItem::Key(b"k".to_vec()), - None, - Some(inner_acor2), - ); - assert!( - conditional.has_aggregate_count_on_range_anywhere(), - "ACOR hidden in conditional subquery branch must be detected" - ); - } } diff --git a/grovedb/src/operations/get/query.rs b/grovedb/src/operations/get/query.rs index bbbe7d371..b5d87d043 100644 --- a/grovedb/src/operations/get/query.rs +++ b/grovedb/src/operations/get/query.rs @@ -668,10 +668,16 @@ where { /// skips proof generation, serialization, and verification entirely. /// /// `path_query` must satisfy - /// [`PathQuery::validate_aggregate_count_on_range`] — a single - /// `AggregateCountOnRange(_)` item, no subqueries, no pagination, and an - /// inner range that isn't `Key`, `RangeFull`, or another - /// `AggregateCountOnRange`. Any other shape is rejected up front with + /// [`PathQuery::validate_leaf_aggregate_count_on_range`] — strictly the + /// **leaf** shape: a single `AggregateCountOnRange(_)` item, no + /// subqueries, no pagination, and an inner range that isn't `Key`, + /// `RangeFull`, or another `AggregateCountOnRange`. Carrier-shape + /// queries (outer `Keys` + `AggregateCountOnRange` subquery) are + /// rejected here because this entry point returns one `u64` and has + /// no way to surface per-outer-key counts; use + /// [`Self::prove_query`] + + /// [`Self::verify_aggregate_count_query_per_key`](GroveDb::verify_aggregate_count_query_per_key) + /// for those. Any other shape is rejected up front with /// `Error::InvalidQuery` before any merk reads happen. /// /// The subtree at `path_query.path` must be a `ProvableCountTree` or @@ -701,12 +707,14 @@ where { let mut cost = OperationCost::default(); - // Up-front shape validation: same gate the prover and verifier use. - // Catches malformed ACOR queries (illegal inner range, ACOR-hidden-in- - // subquery, pagination, etc.) before any storage reads. + // Up-front shape validation. Strictly the leaf shape — this + // entry point returns a single `u64` and has no way to surface + // per-outer-key carrier results. Catches malformed leaf + // aggregate-count queries (illegal inner range, pagination, + // etc.) AND carrier-shape queries before any storage reads. let inner_range = cost_return_on_error_no_add!( cost, - path_query.validate_aggregate_count_on_range().cloned() + path_query.validate_leaf_aggregate_count_on_range().cloned() ); let tx = TxRef::new(&self.db, transaction); @@ -735,6 +743,159 @@ where { Ok(count).wrap_with_cost(cost) } + /// Executes an `AggregateCountOnRange` query in either the **leaf** or + /// **carrier** shape without generating a proof, returning one + /// `(outer_key, count)` pair per matched outer key. + /// + /// This is the no-proof counterpart of + /// [`GroveDb::verify_aggregate_count_query_per_key`]: it performs the + /// same merk-level boundary walks the per-key verifier reconstructs + /// from a proof but skips proof generation, encoding, decoding, and + /// chain verification entirely. + /// + /// For a **leaf** query the returned vector contains exactly one + /// entry whose key is an empty byte string and whose count is the + /// same `u64` [`Self::query_aggregate_count`] would have returned. + /// This matches the per-key verifier's leaf behavior, so callers + /// that always handle `Vec<(Vec, u64)>` don't need to branch on + /// the shape. + /// + /// For a **carrier** query the outer items must be `Key(_)` / + /// `Range*(_)` and the `default_subquery_branch.subquery` must + /// validate as a leaf `AggregateCountOnRange`. The optional + /// `subquery_path` is followed exactly (single-key step per element) + /// before the count walk. The returned vector has one entry per + /// matched outer key in query-direction order (ascending lex when + /// `left_to_right = true`, descending otherwise). Outer-key + /// candidates that don't exist contribute no entry; outer-key + /// candidates whose leaf subtree is empty contribute `(key, 0)`. + /// + /// `path_query` must satisfy + /// [`PathQuery::validate_aggregate_count_on_range`] in either + /// shape. Pagination is rejected. Each leaf subtree the walk + /// terminates in must be a `ProvableCountTree` or + /// `ProvableCountSumTree` — the merk-level walk rejects any other + /// tree type. + /// + /// The returned counts are **not** independently verifiable — + /// callers are trusting their own merk read path. For verifiable + /// counts, use [`Self::prove_query`] + + /// [`GroveDb::verify_aggregate_count_query_per_key`]. + pub fn query_aggregate_count_per_key( + &self, + path_query: &PathQuery, + transaction: TransactionArg, + grove_version: &GroveVersion, + ) -> CostResult, u64)>, Error> { + check_grovedb_v0_with_cost!( + "query_aggregate_count_per_key", + grove_version + .grovedb_versions + .operations + .query + .query_aggregate_count_on_range + ); + + let mut cost = OperationCost::default(); + + // Up-front shape validation: accept both leaf and carrier shapes. + // We classify by what the top-level query owns: a direct + // `AggregateCountOnRange` item means leaf; otherwise the + // dispatcher already confirmed a valid carrier subquery exists. + let inner_range = cost_return_on_error_no_add!( + cost, + path_query.validate_aggregate_count_on_range().cloned() + ); + + if path_query.query.query.aggregate_count_on_range().is_some() { + // Leaf shape: delegate to the existing single-`u64` entry + // point and wrap as a one-entry vector with an empty key. + let count = cost_return_on_error!( + &mut cost, + self.query_aggregate_count(path_query, transaction, grove_version) + ); + return Ok(vec![(Vec::new(), count)]).wrap_with_cost(cost); + } + + // Carrier shape: enumerate matched outer keys at the carrier + // subtree, then per match navigate `subquery_path` and run the + // merk-level count walk on the leaf. + let q = &path_query.query.query; + let outer_items = q.items.clone(); + let subquery_path = q + .default_subquery_branch + .subquery_path + .clone() + .unwrap_or_default(); + let left_to_right = q.left_to_right; + + // Build a "shallow" path query that enumerates the carrier's + // outer items at `path_query.path` without descending into the + // subquery — we want just the matched outer keys, not the + // (unproven) results of the leaf aggregate-count. + let mut shallow_query = grovedb_query::Query::new_with_direction(left_to_right); + shallow_query.items = outer_items; + let shallow_pq = PathQuery::new_unsized(path_query.path.clone(), shallow_query); + + let (matched, _skipped) = cost_return_on_error!( + &mut cost, + self.query_raw( + &shallow_pq, + true, // allow_cache + false, // decrease_limit_on_range_with_no_sub_elements + true, // error_if_intermediate_path_tree_not_present + QueryResultType::QueryKeyElementPairResultType, + transaction, + grove_version, + ) + ); + + let key_elements = matched.to_key_elements(); + let mut results: Vec<(Vec, u64)> = Vec::with_capacity(key_elements.len()); + let tx = TxRef::new(&self.db, transaction); + + for (key, element) in key_elements { + // Refuse non-tree matches: aggregate-count requires + // descending into the matched element to find the leaf + // count subtree. + if !element.is_any_tree() { + return Err(Error::InvalidQuery( + "carrier aggregate-count matched a non-tree element; outer items must \ + resolve to tree elements", + )) + .wrap_with_cost(cost); + } + + // Build the path to the leaf count subtree: + // `path_query.path / outer_key / subquery_path...`. + let mut leaf_path_owned: Vec> = path_query.path.clone(); + leaf_path_owned.push(key.clone()); + leaf_path_owned.extend(subquery_path.iter().cloned()); + let leaf_path: Vec<&[u8]> = leaf_path_owned.iter().map(|p| p.as_slice()).collect(); + + let leaf_subtree = cost_return_on_error!( + &mut cost, + self.open_transactional_merk_at_path( + SubtreePath::from(leaf_path.as_slice()), + tx.as_ref(), + None, + grove_version, + ) + ); + + let count = cost_return_on_error!( + &mut cost, + leaf_subtree + .count_aggregate_on_range(&inner_range, grove_version) + .map_err(Error::MerkError) + ); + + results.push((key, count)); + } + + Ok(results).wrap_with_cost(cost) + } + /// Retrieves SumItem values that match a regular [`PathQuery`], returning /// a `Vec` of the raw sum values and the number of skipped elements. /// diff --git a/grovedb/src/operations/proof/aggregate_count.rs b/grovedb/src/operations/proof/aggregate_count.rs deleted file mode 100644 index 6920c78c5..000000000 --- a/grovedb/src/operations/proof/aggregate_count.rs +++ /dev/null @@ -1,367 +0,0 @@ -//! GroveDB-side prove/verify glue for `AggregateCountOnRange` queries. -//! -//! The merk-level pieces live in `grovedb_merk::proofs::query::aggregate_count` -//! (proof generation in `Merk::prove_aggregate_count_on_range`, proof -//! verification in `verify_aggregate_count_on_range_proof`). This module -//! adds the GroveDB-level *envelope* handling: a verifier that walks the -//! multi-layer `GroveDBProof` chain (parent merk → ... → leaf merk), -//! verifies the path-element existence proofs at each non-leaf layer, and -//! delegates to the merk-level count verifier at the leaf. -//! -//! The proof generator side is wired directly into -//! [`GroveDb::prove_subqueries`] / [`GroveDb::prove_subqueries_v1`] — see -//! the "Aggregate-count short-circuit" branches there. - -use grovedb_merk::{ - proofs::{ - query::{aggregate_count::verify_aggregate_count_on_range_proof, QueryProofVerify}, - Query as MerkQuery, - }, - tree::{combine_hash, value_hash}, - CryptoHash, -}; -use grovedb_version::{check_grovedb_v0, version::GroveVersion}; - -use crate::{ - operations::proof::{ - GroveDBProof, GroveDBProofV0, GroveDBProofV1, LayerProof, MerkOnlyLayerProof, ProofBytes, - }, - Element, Error, GroveDb, PathQuery, -}; - -impl GroveDb { - /// Verify a serialized `prove_query` proof against an - /// `AggregateCountOnRange` `PathQuery`, returning the GroveDB root hash - /// and the verified count. - /// - /// `path_query` must satisfy - /// [`PathQuery::validate_aggregate_count_on_range`] — a single - /// `AggregateCountOnRange(_)` item, no subqueries, no pagination, and an - /// inner range that isn't `Key`, `RangeFull`, or another - /// `AggregateCountOnRange`. Any other shape is rejected up front with - /// `Error::InvalidQuery` before any bytes are decoded. - /// - /// Returns: - /// - `root_hash` — the reconstructed GroveDB root hash. The caller is - /// responsible for comparing this against their trusted root hash. - /// - `count` — the number of keys in the inner range that were committed - /// by the proof. - /// - /// Cryptographic guarantees: - /// - At each non-leaf layer, a regular single-key merk proof - /// demonstrates that the next path element exists with the recorded - /// value bytes; the verifier checks the chain - /// `combine_hash(H(value), lower_hash) == parent_proof_hash` so a - /// forged path is impossible without a root-hash mismatch. - /// - At the leaf layer, the count is committed by `HashWithCount`'s - /// `node_hash_with_count(kv_hash, left, right, count)` recomputation — - /// tampering with the count produces a different reconstructed merk - /// root, and the chain check above then fails. - pub fn verify_aggregate_count_query( - proof: &[u8], - path_query: &PathQuery, - grove_version: &GroveVersion, - ) -> Result<(CryptoHash, u64), Error> { - check_grovedb_v0!( - "verify_aggregate_count_query", - grove_version - .grovedb_versions - .operations - .proof - .verify_query_with_options - ); - - let inner_range = path_query.validate_aggregate_count_on_range()?.clone(); - - // Decode the GroveDBProof envelope using the same config the prover - // uses on the way out (matches `prove_query`). - let config = bincode::config::standard() - .with_big_endian() - .with_limit::<{ 256 * 1024 * 1024 }>(); - let grovedb_proof: GroveDBProof = bincode::decode_from_slice(proof, config) - .map_err(|e| Error::CorruptedData(format!("unable to decode proof: {}", e)))? - .0; - - let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); - - match grovedb_proof { - GroveDBProof::V0(GroveDBProofV0 { root_layer, .. }) => verify_v0_layer( - &root_layer, - path_query, - &path_keys, - 0, - &inner_range, - grove_version, - ), - GroveDBProof::V1(GroveDBProofV1 { root_layer }) => verify_v1_layer( - &root_layer, - path_query, - &path_keys, - 0, - &inner_range, - grove_version, - ), - } - } -} - -/// Walk a V0 (`MerkOnlyLayerProof`) envelope. At each non-leaf depth we -/// verify the single-key existence proof for `path[depth]` and descend into -/// the matching lower layer; at the leaf depth we delegate to the merk -/// count verifier. -fn verify_v0_layer( - layer: &MerkOnlyLayerProof, - path_query: &PathQuery, - path_keys: &[&[u8]], - depth: usize, - inner_range: &grovedb_merk::proofs::query::QueryItem, - grove_version: &GroveVersion, -) -> Result<(CryptoHash, u64), Error> { - if depth == path_keys.len() { - // Leaf layer: count proof. - return verify_count_leaf(&layer.merk_proof, inner_range, path_query); - } - - // Non-leaf: build a single-key merk query and verify. - let next_key = path_keys[depth].to_vec(); - let (proven_value_bytes, parent_root_hash, parent_proof_hash) = - verify_single_key_layer_proof_v0(&layer.merk_proof, &next_key, path_query)?; - - // Descend. - let lower_layer = layer.lower_layers.get(&next_key).ok_or_else(|| { - Error::InvalidProof( - path_query.clone(), - format!( - "aggregate-count proof missing lower layer for path key {}", - hex::encode(&next_key) - ), - ) - })?; - let (lower_hash, count) = verify_v0_layer( - lower_layer, - path_query, - path_keys, - depth + 1, - inner_range, - grove_version, - )?; - - // Chain check: combine_hash(H(tree_value), lower_hash) must equal the - // value_hash recorded by the parent merk for this tree element. - enforce_lower_chain( - path_query, - &next_key, - &proven_value_bytes, - &lower_hash, - &parent_proof_hash, - grove_version, - )?; - - Ok((parent_root_hash, count)) -} - -/// Walk a V1 (`LayerProof`) envelope. Mirrors `verify_v0_layer`; the V1 -/// envelope wraps merk proof bytes in `ProofBytes::Merk(_)` and we reject -/// any other tree-specific proof variant for count queries (they're not -/// applicable to provable count trees). -fn verify_v1_layer( - layer: &LayerProof, - path_query: &PathQuery, - path_keys: &[&[u8]], - depth: usize, - inner_range: &grovedb_merk::proofs::query::QueryItem, - grove_version: &GroveVersion, -) -> Result<(CryptoHash, u64), Error> { - let merk_bytes = match &layer.merk_proof { - ProofBytes::Merk(b) => b.as_slice(), - other => { - return Err(Error::InvalidProof( - path_query.clone(), - format!( - "aggregate-count proof has unexpected non-merk leaf bytes: {:?}", - std::mem::discriminant(other) - ), - )); - } - }; - - if depth == path_keys.len() { - return verify_count_leaf(merk_bytes, inner_range, path_query); - } - - let next_key = path_keys[depth].to_vec(); - let (proven_value_bytes, parent_root_hash, parent_proof_hash) = - verify_single_key_layer_proof_v0(merk_bytes, &next_key, path_query)?; - - let lower_layer = layer.lower_layers.get(&next_key).ok_or_else(|| { - Error::InvalidProof( - path_query.clone(), - format!( - "aggregate-count proof missing lower layer for path key {}", - hex::encode(&next_key) - ), - ) - })?; - let (lower_hash, count) = verify_v1_layer( - lower_layer, - path_query, - path_keys, - depth + 1, - inner_range, - grove_version, - )?; - - enforce_lower_chain( - path_query, - &next_key, - &proven_value_bytes, - &lower_hash, - &parent_proof_hash, - grove_version, - )?; - - Ok((parent_root_hash, count)) -} - -/// Verify the leaf layer: bytes are the encoded count-proof Op stream; -/// the inner range is the same one the prover counted over. -fn verify_count_leaf( - leaf_bytes: &[u8], - inner_range: &grovedb_merk::proofs::query::QueryItem, - path_query: &PathQuery, -) -> Result<(CryptoHash, u64), Error> { - let (root_hash, count) = verify_aggregate_count_on_range_proof(leaf_bytes, inner_range) - .unwrap() - .map_err(|e| { - Error::InvalidProof( - path_query.clone(), - format!("aggregate-count leaf proof failed to verify: {}", e), - ) - })?; - Ok((root_hash, count)) -} - -/// Verify a non-leaf layer that should contain a single-key proof for -/// `target_key`. Returns `(proven_value_bytes, this_layer_root_hash, -/// proof_hash_recorded_for_target)`. -/// -/// The "proof_hash" is the value_hash committed by the merk proof for the -/// target key — this is the hash the verifier will compare against -/// `combine_hash(H(child_tree_value), lower_layer_root_hash)` to enforce -/// the chain. -fn verify_single_key_layer_proof_v0( - merk_bytes: &[u8], - target_key: &[u8], - path_query: &PathQuery, -) -> Result<(Vec, CryptoHash, CryptoHash), Error> { - let level_query = MerkQuery { - items: vec![grovedb_merk::proofs::query::QueryItem::Key( - target_key.to_vec(), - )], - left_to_right: true, - ..Default::default() - }; - - let (root_hash, merk_result) = level_query - .execute_proof(merk_bytes, None, true, 0) - .unwrap() - .map_err(|e| { - Error::InvalidProof( - path_query.clone(), - format!( - "non-leaf single-key proof for {} failed to verify: {}", - hex::encode(target_key), - e - ), - ) - })?; - - // Find the result row for our target key and pull the value + proof_hash. - let proved = merk_result - .result_set - .iter() - .find(|p| p.key == target_key) - .ok_or_else(|| { - Error::InvalidProof( - path_query.clone(), - format!( - "non-leaf proof did not contain the expected key {}", - hex::encode(target_key) - ), - ) - })?; - - let value_bytes = proved.value.clone().ok_or_else(|| { - Error::InvalidProof( - path_query.clone(), - format!( - "non-leaf proof for key {} returned no value bytes", - hex::encode(target_key) - ), - ) - })?; - - Ok((value_bytes, root_hash, proved.proof)) -} - -/// Enforce the layer-chain hash equality: the parent merk's recorded -/// value_hash for the tree element must equal `combine_hash(H(value), -/// lower_layer_root_hash)`. This is what makes the count cryptographically -/// bound to the GroveDB root hash — the leaf count proof's reconstructed -/// `lower_hash` must agree with the parent's commitment, transitively up to -/// the root. -/// -/// Intermediate path elements may be any tree type — the GroveDB grove can -/// route through Normal/Sum/Count/etc. trees on the way down to the -/// provable-count leaf. The leaf-level tree-type check is enforced by the -/// merk prover (`Merk::prove_aggregate_count_on_range`); here we only -/// require that each non-leaf element on the path *is* some non-empty tree, -/// since only trees have a lower layer to chain into. -fn enforce_lower_chain( - path_query: &PathQuery, - target_key: &[u8], - proven_value_bytes: &[u8], - lower_hash: &CryptoHash, - parent_proof_hash: &CryptoHash, - grove_version: &GroveVersion, -) -> Result<(), Error> { - let element = Element::deserialize(proven_value_bytes, grove_version) - .map_err(|e| { - Error::InvalidProof( - path_query.clone(), - format!( - "non-leaf proof's element at key {} failed to deserialize: {}", - hex::encode(target_key), - e - ), - ) - })? - .into_underlying(); - if !element.is_any_tree() { - return Err(Error::InvalidProof( - path_query.clone(), - format!( - "aggregate-count proof's path element at key {} is not a tree element \ - (got {:?}); count queries can only descend through tree elements", - hex::encode(target_key), - std::mem::discriminant(&element) - ), - )); - } - - let value_h = value_hash(proven_value_bytes).value().to_owned(); - let combined = combine_hash(&value_h, lower_hash).value().to_owned(); - if combined != *parent_proof_hash { - return Err(Error::InvalidProof( - path_query.clone(), - format!( - "aggregate-count proof chain mismatch at key {}: parent recorded value_hash \ - {} but combine_hash(H(value), lower_root) is {}", - hex::encode(target_key), - hex::encode(parent_proof_hash), - hex::encode(combined) - ), - )); - } - Ok(()) -} diff --git a/grovedb/src/operations/proof/aggregate_count/classification.rs b/grovedb/src/operations/proof/aggregate_count/classification.rs new file mode 100644 index 000000000..40c314beb --- /dev/null +++ b/grovedb/src/operations/proof/aggregate_count/classification.rs @@ -0,0 +1,77 @@ +//! Classification of an `AggregateCountOnRange` `PathQuery` into either +//! the **leaf** shape (single `AggregateCountOnRange(_)` item) or the +//! **carrier** shape (outer `Key`/`Range*` items routing to a leaf +//! aggregate-count subquery). +//! +//! The classification is consumed by the per-key traversal in +//! [`super::per_key`] to decide whether to terminate the path walk at a +//! single count proof or to fan out across the carrier's matched outer +//! keys. +//! +//! Forthcoming aggregate variants (sum, average) will define their own +//! parallel classification types (`AggregateSumClassification`, +//! `AggregateAverageClassification`, …) in sibling modules — the +//! leaf-vs-carrier shape is a property of any aggregate-on-range query, +//! but each variant carries its own kind of inner descriptor. + +use grovedb_query::QueryItem; + +use crate::{Error, PathQuery}; + +/// Classification of an `AggregateCountOnRange` `PathQuery`. Encodes +/// either the leaf-only inner range (no carrier descent) or the +/// carrier outer items + leaf inner range + optional `subquery_path` +/// that the verifier must follow per outer key. +pub(super) struct AggregateCountClassification { + /// The inner range that the leaf merk count proof must satisfy. + pub(super) leaf_inner_range: QueryItem, + /// Carrier outer items. `None` for leaf-only queries. + pub(super) carrier_outer_items: Option>, + /// Carrier subquery_path (the keys between each outer match and the + /// leaf merk). Empty `Vec` if no subquery_path was set. `None` for + /// leaf-only queries. + pub(super) carrier_subquery_path: Option>>, + /// Whether the outer query is left-to-right. Affects which results + /// the merk_proof returns when the outer items are ranges. Always + /// `true` for leaf-only. + pub(super) carrier_left_to_right: bool, +} + +/// Classify an `AggregateCountOnRange` path query and validate it at +/// the PathQuery level — `SizedQuery::limit` / `offset` (which +/// aggregate-count explicitly forbids) are enforced for both shapes. +pub(super) fn classify_aggregate_count_path_query( + path_query: &PathQuery, +) -> Result { + let leaf_inner = path_query.validate_aggregate_count_on_range()?.clone(); + let q = &path_query.query.query; + if q.aggregate_count_on_range().is_some() { + // Leaf shape: top-level `AggregateCountOnRange` item. The + // top-level `validate_aggregate_count_on_range` dispatcher above + // routed through the leaf validator, so we already know + // `leaf_inner` is the inner range of the top-level + // `AggregateCountOnRange` item. + return Ok(AggregateCountClassification { + leaf_inner_range: leaf_inner, + carrier_outer_items: None, + carrier_subquery_path: None, + carrier_left_to_right: true, + }); + } + // Carrier shape: validation above routed through the carrier + // validator, so `leaf_inner` is the *subquery's* inner range. We + // just need to extract the outer items and the optional + // subquery_path. + let outer_items = q.items.clone(); + let subquery_path = q + .default_subquery_branch + .subquery_path + .clone() + .unwrap_or_default(); + Ok(AggregateCountClassification { + leaf_inner_range: leaf_inner, + carrier_outer_items: Some(outer_items), + carrier_subquery_path: Some(subquery_path), + carrier_left_to_right: q.left_to_right, + }) +} diff --git a/grovedb/src/operations/proof/aggregate_count/helpers.rs b/grovedb/src/operations/proof/aggregate_count/helpers.rs new file mode 100644 index 000000000..d22ab6e45 --- /dev/null +++ b/grovedb/src/operations/proof/aggregate_count/helpers.rs @@ -0,0 +1,284 @@ +//! Shared helpers used by both the leaf-chain walker and the per-key +//! carrier walker. +//! +//! - [`decode_grovedb_proof`] — parse the bincode envelope. +//! - [`verify_count_leaf`] — delegate to the merk-level count verifier. +//! - [`expect_merk_bytes`] — unwrap a `ProofBytes::Merk(_)` or reject. +//! - [`verify_single_key_layer_proof_v0`] — verify a non-leaf merk +//! proof for one expected key and recover its value bytes + chain +//! commitment hash. +//! - [`OuterMatch`] + [`execute_carrier_layer_proof`] — verify the +//! carrier's multi-key merk proof, collect one `OuterMatch` per +//! matched outer key. +//! - [`enforce_lower_chain`] — `combine_hash(H(value), lower_root) == +//! parent_value_hash`, the binding that ties each layer's count to +//! the GroveDB root hash. + +use grovedb_merk::{ + proofs::{ + query::{aggregate_count::verify_aggregate_count_on_range_proof, QueryProofVerify}, + Query as MerkQuery, + }, + tree::{combine_hash, value_hash}, + CryptoHash, +}; +use grovedb_query::QueryItem; +use grovedb_version::version::GroveVersion; + +use crate::{ + operations::proof::{GroveDBProof, ProofBytes}, + Element, Error, PathQuery, +}; + +/// Decode a serialized `GroveDBProof` envelope using the same bincode +/// configuration the prover writes out. +/// +/// Decoding is canonical: trailing bytes beyond the encoded envelope +/// are rejected. Without this check the same `(RootHash, count)` could +/// be reconstructed from many different proof byte-strings (a proof and +/// the same proof with arbitrary suffix bytes), which is harmless for +/// the chain-bound correctness guarantee but breaks any +/// equality-by-bytes assumption a caller might rely on (caching, +/// deduplication, hashing the proof itself). +pub(super) fn decode_grovedb_proof(proof: &[u8]) -> Result { + let config = bincode::config::standard() + .with_big_endian() + .with_limit::<{ 256 * 1024 * 1024 }>(); + let (decoded, consumed) = bincode::decode_from_slice(proof, config) + .map_err(|e| Error::CorruptedData(format!("unable to decode proof: {}", e)))?; + if consumed != proof.len() { + return Err(Error::CorruptedData(format!( + "aggregate-count proof has {} trailing bytes after the encoded envelope", + proof.len() - consumed + ))); + } + Ok(decoded) +} + +/// Verify the leaf layer: bytes are the encoded count-proof Op stream; +/// the inner range is the same one the prover counted over. +pub(super) fn verify_count_leaf( + leaf_bytes: &[u8], + inner_range: &QueryItem, + path_query: &PathQuery, +) -> Result<(CryptoHash, u64), Error> { + let (root_hash, count) = verify_aggregate_count_on_range_proof(leaf_bytes, inner_range) + .unwrap() + .map_err(|e| { + Error::InvalidProof( + path_query.clone(), + format!("aggregate-count leaf proof failed to verify: {}", e), + ) + })?; + Ok((root_hash, count)) +} + +/// Unwrap a `ProofBytes::Merk(_)` or reject the proof — aggregate-count +/// envelopes are always merk-flavored at every layer. +pub(super) fn expect_merk_bytes<'a>( + proof_bytes: &'a ProofBytes, + path_query: &PathQuery, +) -> Result<&'a [u8], Error> { + match proof_bytes { + ProofBytes::Merk(b) => Ok(b.as_slice()), + other => Err(Error::InvalidProof( + path_query.clone(), + format!( + "aggregate-count proof has unexpected non-merk layer bytes: {:?}", + std::mem::discriminant(other) + ), + )), + } +} + +/// Verify a non-leaf layer that should contain a single-key proof for +/// `target_key`. Returns `(proven_value_bytes, this_layer_root_hash, +/// proof_hash_recorded_for_target)`. +/// +/// The "proof_hash" is the value_hash committed by the merk proof for the +/// target key — this is the hash the verifier will compare against +/// `combine_hash(H(child_tree_value), lower_layer_root_hash)` to enforce +/// the chain. +pub(super) fn verify_single_key_layer_proof_v0( + merk_bytes: &[u8], + target_key: &[u8], + path_query: &PathQuery, +) -> Result<(Vec, CryptoHash, CryptoHash), Error> { + let level_query = MerkQuery { + items: vec![grovedb_merk::proofs::query::QueryItem::Key( + target_key.to_vec(), + )], + left_to_right: true, + ..Default::default() + }; + + let (root_hash, merk_result) = level_query + .execute_proof(merk_bytes, None, true, 0) + .unwrap() + .map_err(|e| { + Error::InvalidProof( + path_query.clone(), + format!( + "non-leaf single-key proof for {} failed to verify: {}", + hex::encode(target_key), + e + ), + ) + })?; + + let proved = merk_result + .result_set + .iter() + .find(|p| p.key == target_key) + .ok_or_else(|| { + Error::InvalidProof( + path_query.clone(), + format!( + "non-leaf proof did not contain the expected key {}", + hex::encode(target_key) + ), + ) + })?; + + let value_bytes = proved.value.clone().ok_or_else(|| { + Error::InvalidProof( + path_query.clone(), + format!( + "non-leaf proof for key {} returned no value bytes", + hex::encode(target_key) + ), + ) + })?; + + Ok((value_bytes, root_hash, proved.proof)) +} + +/// One matched outer key in the carrier layer's multi-key merk proof. +pub(super) struct OuterMatch { + /// The matched outer key bytes. + pub(super) outer_key: Vec, + /// The serialized tree element bytes for the matched outer key (a + /// non-empty tree element of some flavor). + pub(super) value_bytes: Vec, + /// The value_hash the parent merk committed for this outer key — the + /// hash that must equal `combine_hash(H(value), lower_layer_root)`. + pub(super) commitment_hash: CryptoHash, +} + +/// Execute the carrier-layer multi-key merk proof for `outer_items`, +/// returning `(carrier_merk_root_hash, matched_outer_keys)`. Each +/// `OuterMatch` carries the value bytes and the parent-recorded value_hash +/// that the chain check will validate. +pub(super) fn execute_carrier_layer_proof( + merk_bytes: &[u8], + outer_items: &[QueryItem], + left_to_right: bool, + path_query: &PathQuery, +) -> Result<(CryptoHash, Vec), Error> { + // The grovedb_query::QueryItem and grovedb_merk::proofs::query::QueryItem + // types are identical (the merk crate re-exports the grovedb-query one). + let level_query = MerkQuery { + items: outer_items.to_vec(), + left_to_right, + ..Default::default() + }; + + // Walk direction must match the prover's; otherwise the merk + // walker stops at the first out-of-order boundary and only the + // last key in the proof is returned. + let (root_hash, merk_result) = level_query + .execute_proof(merk_bytes, None, left_to_right, 0) + .unwrap() + .map_err(|e| { + Error::InvalidProof( + path_query.clone(), + format!( + "carrier aggregate-count multi-key proof failed to verify: {}", + e + ), + ) + })?; + + let mut matched = Vec::with_capacity(merk_result.result_set.len()); + for proved in &merk_result.result_set { + let value = proved.value.clone().ok_or_else(|| { + Error::InvalidProof( + path_query.clone(), + format!( + "carrier aggregate-count proof returned a result row without value bytes \ + for key {}", + hex::encode(&proved.key) + ), + ) + })?; + matched.push(OuterMatch { + outer_key: proved.key.clone(), + value_bytes: value, + commitment_hash: proved.proof, + }); + } + + Ok((root_hash, matched)) +} + +/// Enforce the layer-chain hash equality: the parent merk's recorded +/// value_hash for the tree element must equal `combine_hash(H(value), +/// lower_layer_root_hash)`. This is what makes the count cryptographically +/// bound to the GroveDB root hash — the leaf count proof's reconstructed +/// `lower_hash` must agree with the parent's commitment, transitively up to +/// the root. +/// +/// Intermediate path elements may be any tree type — the GroveDB grove can +/// route through Normal/Sum/Count/etc. trees on the way down to the +/// provable-count leaf. The leaf-level tree-type check is enforced by the +/// merk prover (`Merk::prove_aggregate_count_on_range`); here we only +/// require that each non-leaf element on the path *is* some non-empty tree, +/// since only trees have a lower layer to chain into. +pub(super) fn enforce_lower_chain( + path_query: &PathQuery, + target_key: &[u8], + proven_value_bytes: &[u8], + lower_hash: &CryptoHash, + parent_proof_hash: &CryptoHash, + grove_version: &GroveVersion, +) -> Result<(), Error> { + let element = Element::deserialize(proven_value_bytes, grove_version) + .map_err(|e| { + Error::InvalidProof( + path_query.clone(), + format!( + "non-leaf proof's element at key {} failed to deserialize: {}", + hex::encode(target_key), + e + ), + ) + })? + .into_underlying(); + if !element.is_any_tree() { + return Err(Error::InvalidProof( + path_query.clone(), + format!( + "aggregate-count proof's path element at key {} is not a tree element \ + (got {:?}); count queries can only descend through tree elements", + hex::encode(target_key), + std::mem::discriminant(&element) + ), + )); + } + + let value_h = value_hash(proven_value_bytes).value().to_owned(); + let combined = combine_hash(&value_h, lower_hash).value().to_owned(); + if combined != *parent_proof_hash { + return Err(Error::InvalidProof( + path_query.clone(), + format!( + "aggregate-count proof chain mismatch at key {}: parent recorded value_hash \ + {} but combine_hash(H(value), lower_root) is {}", + hex::encode(target_key), + hex::encode(parent_proof_hash), + hex::encode(combined) + ), + )); + } + Ok(()) +} diff --git a/grovedb/src/operations/proof/aggregate_count/leaf_chain.rs b/grovedb/src/operations/proof/aggregate_count/leaf_chain.rs new file mode 100644 index 000000000..7057142ca --- /dev/null +++ b/grovedb/src/operations/proof/aggregate_count/leaf_chain.rs @@ -0,0 +1,77 @@ +//! Leaf-chain walker: descends `path_query.path` via single-key existence +//! proofs and delegates to the merk-level count verifier at the leaf +//! merk. Used by the legacy single-`u64` entry point +//! [`crate::GroveDb::verify_aggregate_count_query`]. +//! +//! The carrier-shape per-key walker in [`super::per_key`] reuses the +//! same single-key descent helpers (`verify_single_key_layer_proof_v0`, +//! `enforce_lower_chain`) for path-prefix layers and is structurally +//! parallel, but emits a `Vec<(outer_key, count)>` instead of one `u64`. + +use grovedb_merk::CryptoHash; +use grovedb_query::QueryItem; +use grovedb_version::version::GroveVersion; + +use crate::{ + operations::proof::{ + aggregate_count::helpers::{ + enforce_lower_chain, expect_merk_bytes, verify_count_leaf, + verify_single_key_layer_proof_v0, + }, + LayerProof, + }, + Error, PathQuery, +}; + +/// Walk `path_query.path` layer by layer through `layer.lower_layers`, +/// verifying a single-key existence proof at each non-leaf depth and +/// delegating to [`verify_count_leaf`] at the leaf. At each non-leaf +/// step, the chain check `combine_hash(H(value), lower_root) == +/// parent_value_hash` ties the layer's count to the GroveDB root hash. +pub(super) fn verify_v1_leaf_chain( + layer: &LayerProof, + path_query: &PathQuery, + path_keys: &[&[u8]], + depth: usize, + inner_range: &QueryItem, + grove_version: &GroveVersion, +) -> Result<(CryptoHash, u64), Error> { + let merk_bytes = expect_merk_bytes(&layer.merk_proof, path_query)?; + + if depth == path_keys.len() { + return verify_count_leaf(merk_bytes, inner_range, path_query); + } + + let next_key = path_keys[depth].to_vec(); + let (proven_value_bytes, parent_root_hash, parent_proof_hash) = + verify_single_key_layer_proof_v0(merk_bytes, &next_key, path_query)?; + + let lower_layer = layer.lower_layers.get(&next_key).ok_or_else(|| { + Error::InvalidProof( + path_query.clone(), + format!( + "aggregate-count proof missing lower layer for path key {}", + hex::encode(&next_key) + ), + ) + })?; + let (lower_hash, count) = verify_v1_leaf_chain( + lower_layer, + path_query, + path_keys, + depth + 1, + inner_range, + grove_version, + )?; + + enforce_lower_chain( + path_query, + &next_key, + &proven_value_bytes, + &lower_hash, + &parent_proof_hash, + grove_version, + )?; + + Ok((parent_root_hash, count)) +} diff --git a/grovedb/src/operations/proof/aggregate_count/mod.rs b/grovedb/src/operations/proof/aggregate_count/mod.rs new file mode 100644 index 000000000..c163e4e7b --- /dev/null +++ b/grovedb/src/operations/proof/aggregate_count/mod.rs @@ -0,0 +1,227 @@ +//! GroveDB-side prove/verify glue for `AggregateCountOnRange` queries. +//! +//! The merk-level pieces live in `grovedb_merk::proofs::query::aggregate_count` +//! (proof generation in `Merk::prove_aggregate_count_on_range`, proof +//! verification in `verify_aggregate_count_on_range_proof`). This module +//! adds the GroveDB-level *envelope* handling: a verifier that walks the +//! multi-layer `GroveDBProof` chain (parent merk → ... → leaf merk), +//! verifies the path-element existence proofs at each non-leaf layer, and +//! delegates to the merk-level count verifier at the leaf. +//! +//! The proof generator side is wired directly into +//! [`GroveDb::prove_subqueries`] / [`GroveDb::prove_subqueries_v1`] — see +//! the "Aggregate-count short-circuit" branches there. +//! +//! ## Two shapes +//! +//! `AggregateCountOnRange` queries come in two flavors: +//! +//! - **Leaf** — a single `AggregateCountOnRange(_)` item at the top level +//! of the inner `Query`. The proof descends `path_query.path` via +//! single-key existence checks and produces a single `u64` at the leaf +//! merk. Surfaced through [`GroveDb::verify_aggregate_count_query`]. +//! +//! - **Carrier** — an outer query whose items are `Key(_)` / `Range*(_)` +//! (one IN-style fan-out dimension) and whose +//! `default_subquery_branch.subquery` resolves to a leaf +//! `AggregateCountOnRange`. The proof descends `path_query.path` via +//! single-key checks, then at the carrier merk it produces a multi-key +//! proof over the outer items; each matched outer key recurses through +//! the `subquery_path` (if any) to a leaf merk that produces its own +//! count. The verifier returns one `(outer_key, count)` pair per +//! matched outer key. Surfaced through +//! [`GroveDb::verify_aggregate_count_query_per_key`]. +//! +//! The same leaf/carrier shape will apply to forthcoming aggregate +//! variants (sum, average) — each will get its own sibling module under +//! `grovedb/src/operations/proof/` with parallel naming. +//! +//! ## Module layout +//! +//! - [`classification`] — `AggregateCountClassification` struct and the +//! `classify_aggregate_count_path_query` function that distinguishes +//! leaf vs. carrier shape. +//! - [`leaf_chain`] — the recursive walker used by the legacy +//! single-`u64` entry point. +//! - [`per_key`] — the carrier-shape walker that drives both shapes +//! through the new `(outer_key, count)` entry point. +//! - [`helpers`] — shared utilities (envelope decode, single-key +//! layer verification, chain enforcement, multi-key outer proof +//! execution). + +mod classification; +mod helpers; +mod leaf_chain; +mod per_key; + +use grovedb_merk::CryptoHash; +use grovedb_version::{check_grovedb_v0, version::GroveVersion}; + +use crate::{ + operations::proof::{GroveDBProof, GroveDBProofV1, LayerProof}, + Error, GroveDb, PathQuery, +}; + +impl GroveDb { + /// Verify a serialized `prove_query` proof against a leaf + /// `AggregateCountOnRange` `PathQuery`, returning the GroveDB root hash + /// and the verified count. + /// + /// `path_query` must satisfy + /// [`PathQuery::validate_aggregate_count_on_range`] and additionally must + /// be the **leaf** shape — a single `AggregateCountOnRange(_)` item, no + /// subqueries, no pagination, and an inner range that isn't `Key`, + /// `RangeFull`, or another `AggregateCountOnRange`. Carrier-shape + /// aggregate-count queries (outer `Keys` + `AggregateCountOnRange` + /// subquery) must use + /// [`GroveDb::verify_aggregate_count_query_per_key`] instead. + /// + /// `AggregateCountOnRange` requires **V1 proof envelopes** + /// (`GroveDBProofV1`). V0 (`GroveDBProofV0` / `MerkOnlyLayerProof`) + /// envelopes predate the aggregate-count feature and are only + /// produced by grove versions older than the one used by Dash + /// Platform v12; this entry point rejects them with + /// `Error::InvalidProof`. + /// + /// Returns: + /// - `root_hash` — the reconstructed GroveDB root hash. The caller is + /// responsible for comparing this against their trusted root hash. + /// - `count` — the number of keys in the inner range that were committed + /// by the proof. + /// + /// Cryptographic guarantees: + /// - At each non-leaf layer, a regular single-key merk proof + /// demonstrates that the next path element exists with the recorded + /// value bytes; the verifier checks the chain + /// `combine_hash(H(value), lower_hash) == parent_proof_hash` so a + /// forged path is impossible without a root-hash mismatch. + /// - At the leaf layer, the count is committed by `HashWithCount`'s + /// `node_hash_with_count(kv_hash, left, right, count)` recomputation — + /// tampering with the count produces a different reconstructed merk + /// root, and the chain check above then fails. + pub fn verify_aggregate_count_query( + proof: &[u8], + path_query: &PathQuery, + grove_version: &GroveVersion, + ) -> Result<(CryptoHash, u64), Error> { + check_grovedb_v0!( + "verify_aggregate_count_query", + grove_version + .grovedb_versions + .operations + .proof + .verify_query_with_options + ); + + // Validate at the PathQuery level so SizedQuery::limit / offset + // (which aggregate-count explicitly forbids) are enforced + // alongside the inner-Query shape rules. + let inner_range = path_query.validate_leaf_aggregate_count_on_range()?.clone(); + + let grovedb_proof = helpers::decode_grovedb_proof(proof)?; + let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); + + let root_layer = require_v1_envelope(&grovedb_proof, path_query)?; + let (root_hash, count) = leaf_chain::verify_v1_leaf_chain( + root_layer, + path_query, + &path_keys, + 0, + &inner_range, + grove_version, + )?; + Ok((root_hash, count)) + } + + /// Verify a serialized `prove_query` proof against an + /// `AggregateCountOnRange` `PathQuery` in either the leaf or carrier + /// shape, returning one `(outer_key, count)` pair per matched outer + /// key. + /// + /// For a **leaf** aggregate-count query the returned vector contains + /// exactly one entry whose key is an empty byte string and whose + /// count is the same `u64` + /// [`GroveDb::verify_aggregate_count_query`] would have returned. + /// This makes carrier and leaf consumers symmetric: callers that + /// always process a `Vec<(Vec, u64)>` don't need to branch on + /// the shape. + /// + /// For a **carrier** aggregate-count query the outer items must be + /// `Key(_)` / `Range*(_)`, the `default_subquery_branch.subquery` + /// must validate as a leaf `AggregateCountOnRange`, and the optional + /// `subquery_path` is followed exactly (single-key descent per + /// element) before the count proof. The returned vector has one + /// entry per matched outer key in **query-direction order**: when + /// the carrier's `left_to_right` is `true` (the default, matching + /// the merk prover's natural walk) entries come back in ascending + /// lexicographic key order; when `left_to_right` is `false` they + /// come back in descending order, mirroring the merk proof's own + /// emission order. Outer-key candidates that the prover proved as + /// absent contribute no entry. + /// + /// Like [`GroveDb::verify_aggregate_count_query`], this entry point + /// requires **V1 proof envelopes**. V0 envelopes predate the + /// aggregate-count feature and are rejected with + /// `Error::InvalidProof`. + /// + /// Cryptographic guarantees: + /// - Every layer is committed via the same `combine_hash(H(value), + /// lower_hash) == parent_proof_hash` chain check used by the leaf + /// verifier, so a forged path through the carrier or + /// `subquery_path` produces a root-hash mismatch. + /// - Each per-outer-key count is committed by the leaf + /// `HashWithCount` / `KVDigestCount` recomputation; + /// counts can't be tampered with independently. + pub fn verify_aggregate_count_query_per_key( + proof: &[u8], + path_query: &PathQuery, + grove_version: &GroveVersion, + ) -> Result<(CryptoHash, Vec<(Vec, u64)>), Error> { + check_grovedb_v0!( + "verify_aggregate_count_query_per_key", + grove_version + .grovedb_versions + .operations + .proof + .verify_query_with_options + ); + + // Classify the query and extract the leaf inner range plus the + // optional carrier subquery_path. For leaf queries the carrier + // descent below is skipped (carrier_outer_items is None). + let classification = classification::classify_aggregate_count_path_query(path_query)?; + + let grovedb_proof = helpers::decode_grovedb_proof(proof)?; + let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); + + let root_layer = require_v1_envelope(&grovedb_proof, path_query)?; + per_key::verify_v1_with_classification( + root_layer, + path_query, + &path_keys, + &classification, + grove_version, + ) + } +} + +/// Extract the V1 root layer from a `GroveDBProof` envelope, or refuse +/// the proof. `AggregateCountOnRange` (both leaf and carrier) requires +/// V1 envelopes — the V0 (`MerkOnlyLayerProof`) envelope predates the +/// aggregate-count feature and is only emitted by grove versions older +/// than the one used by Dash Platform v12, so it cannot legitimately +/// contain an aggregate-count proof. +fn require_v1_envelope<'a>( + proof: &'a GroveDBProof, + path_query: &PathQuery, +) -> Result<&'a LayerProof, Error> { + match proof { + GroveDBProof::V1(GroveDBProofV1 { root_layer }) => Ok(root_layer), + GroveDBProof::V0(_) => Err(Error::InvalidProof( + path_query.clone(), + "AggregateCountOnRange proofs require V1 proof envelopes; V0 envelopes predate \ + this feature and cannot legitimately carry an aggregate-count proof" + .to_string(), + )), + } +} diff --git a/grovedb/src/operations/proof/aggregate_count/per_key.rs b/grovedb/src/operations/proof/aggregate_count/per_key.rs new file mode 100644 index 000000000..4594c54e7 --- /dev/null +++ b/grovedb/src/operations/proof/aggregate_count/per_key.rs @@ -0,0 +1,230 @@ +//! Per-key carrier walker: dispatches leaf vs. carrier shape based on +//! the [`AggregateCountClassification`], walks the path-prefix layers +//! with single-key descents, then either emits a single +//! `(empty_key, u64)` entry (leaf shape) or executes the carrier's +//! multi-key merk proof and recurses through `subquery_path` per +//! matched outer key (carrier shape). +//! +//! V0 (`MerkOnlyLayerProof`) envelopes are rejected at the entry-point +//! gate in [`super::mod`] before they reach this walker — V0 predates +//! the aggregate-count feature and cannot legitimately carry one. + +use grovedb_merk::CryptoHash; +use grovedb_query::QueryItem; +use grovedb_version::version::GroveVersion; + +use crate::{ + operations::proof::{ + aggregate_count::{ + classification::AggregateCountClassification, + helpers::{ + enforce_lower_chain, execute_carrier_layer_proof, expect_merk_bytes, + verify_count_leaf, verify_single_key_layer_proof_v0, OuterMatch, + }, + }, + LayerProof, + }, + Error, PathQuery, +}; + +/// Entry point for the per-key carrier walker. Wraps the recursive +/// [`verify_v1_per_key`] with `depth = 0`. +pub(super) fn verify_v1_with_classification( + layer: &LayerProof, + path_query: &PathQuery, + path_keys: &[&[u8]], + classification: &AggregateCountClassification, + grove_version: &GroveVersion, +) -> Result<(CryptoHash, Vec<(Vec, u64)>), Error> { + verify_v1_per_key( + layer, + path_query, + path_keys, + 0, + classification, + grove_version, + ) +} + +/// Recursive worker for the per-key walk. While `depth < path_keys.len()` +/// it performs a single-key descent (same as the leaf chain walker); +/// once it reaches the carrier merk it dispatches on the classification +/// shape: leaf collapses to a one-entry result vector, carrier executes +/// the multi-key proof and fans out via [`verify_v1_carrier_layer`]. +fn verify_v1_per_key( + layer: &LayerProof, + path_query: &PathQuery, + path_keys: &[&[u8]], + depth: usize, + classification: &AggregateCountClassification, + grove_version: &GroveVersion, +) -> Result<(CryptoHash, Vec<(Vec, u64)>), Error> { + let merk_bytes = expect_merk_bytes(&layer.merk_proof, path_query)?; + + if depth < path_keys.len() { + let next_key = path_keys[depth].to_vec(); + let (proven_value_bytes, parent_root_hash, parent_proof_hash) = + verify_single_key_layer_proof_v0(merk_bytes, &next_key, path_query)?; + let lower_layer = layer.lower_layers.get(&next_key).ok_or_else(|| { + Error::InvalidProof( + path_query.clone(), + format!( + "aggregate-count proof missing lower layer for path key {}", + hex::encode(&next_key) + ), + ) + })?; + let (lower_hash, results) = verify_v1_per_key( + lower_layer, + path_query, + path_keys, + depth + 1, + classification, + grove_version, + )?; + enforce_lower_chain( + path_query, + &next_key, + &proven_value_bytes, + &lower_hash, + &parent_proof_hash, + grove_version, + )?; + return Ok((parent_root_hash, results)); + } + + match &classification.carrier_outer_items { + None => { + let (root, count) = + verify_count_leaf(merk_bytes, &classification.leaf_inner_range, path_query)?; + Ok((root, vec![(Vec::new(), count)])) + } + Some(outer_items) => verify_v1_carrier_layer( + layer, + merk_bytes, + path_query, + outer_items, + classification, + grove_version, + ), + } +} + +/// Execute the carrier's multi-key outer merk proof, then for each +/// matched outer key descend the `subquery_path` (if any) and the +/// leaf count proof, enforcing the chain at each step. Returns one +/// `(outer_key, count)` entry per match in query-direction order. +fn verify_v1_carrier_layer( + layer: &LayerProof, + merk_bytes: &[u8], + path_query: &PathQuery, + outer_items: &[QueryItem], + classification: &AggregateCountClassification, + grove_version: &GroveVersion, +) -> Result<(CryptoHash, Vec<(Vec, u64)>), Error> { + let (carrier_root, matched) = execute_carrier_layer_proof( + merk_bytes, + outer_items, + classification.carrier_left_to_right, + path_query, + )?; + + // Invariant from `classify_aggregate_count_path_query`: whenever + // `carrier_outer_items` is `Some`, `carrier_subquery_path` is also + // `Some` (possibly empty). Surface a verification failure rather + // than aborting if the invariant ever drifts. + let subquery_path = classification + .carrier_subquery_path + .as_ref() + .ok_or_else(|| { + Error::InvalidProof( + path_query.clone(), + "carrier aggregate-count classification missing subquery_path".to_string(), + ) + })?; + + let mut results = Vec::with_capacity(matched.len()); + for OuterMatch { + outer_key, + value_bytes, + commitment_hash, + } in matched + { + let lower_layer = layer.lower_layers.get(&outer_key).ok_or_else(|| { + Error::InvalidProof( + path_query.clone(), + format!( + "carrier aggregate-count proof missing lower layer for outer key {}", + hex::encode(&outer_key) + ), + ) + })?; + + let (lower_root, count) = verify_v1_subquery_path( + lower_layer, + path_query, + subquery_path, + 0, + &classification.leaf_inner_range, + grove_version, + )?; + + enforce_lower_chain( + path_query, + &outer_key, + &value_bytes, + &lower_root, + &commitment_hash, + grove_version, + )?; + results.push((outer_key, count)); + } + + Ok((carrier_root, results)) +} + +/// Walk the carrier's `subquery_path` (zero or more intermediate +/// single-key layers between an outer match and the leaf merk), +/// terminating in the merk-level count verifier. +fn verify_v1_subquery_path( + layer: &LayerProof, + path_query: &PathQuery, + subquery_path: &[Vec], + depth: usize, + inner_range: &QueryItem, + grove_version: &GroveVersion, +) -> Result<(CryptoHash, u64), Error> { + let merk_bytes = expect_merk_bytes(&layer.merk_proof, path_query)?; + if depth == subquery_path.len() { + return verify_count_leaf(merk_bytes, inner_range, path_query); + } + let next_key = subquery_path[depth].clone(); + let (proven_value_bytes, parent_root_hash, parent_proof_hash) = + verify_single_key_layer_proof_v0(merk_bytes, &next_key, path_query)?; + let lower_layer = layer.lower_layers.get(&next_key).ok_or_else(|| { + Error::InvalidProof( + path_query.clone(), + format!( + "carrier aggregate-count proof missing subquery_path layer for key {}", + hex::encode(&next_key) + ), + ) + })?; + let (lower_hash, count) = verify_v1_subquery_path( + lower_layer, + path_query, + subquery_path, + depth + 1, + inner_range, + grove_version, + )?; + enforce_lower_chain( + path_query, + &next_key, + &proven_value_bytes, + &lower_hash, + &parent_proof_hash, + grove_version, + )?; + Ok((parent_root_hash, count)) +} diff --git a/grovedb/src/operations/proof/generate.rs b/grovedb/src/operations/proof/generate.rs index 6f136e466..935af6772 100644 --- a/grovedb/src/operations/proof/generate.rs +++ b/grovedb/src/operations/proof/generate.rs @@ -116,21 +116,36 @@ impl GroveDb { // the path doesn't exist. Without this gate, `prove_query` would // happily return a regular path/absence proof for an invalid // aggregate-count request. - if path_query + let is_acor_query = path_query .query .query - .has_aggregate_count_on_range_anywhere() - && let Err(e) = path_query.validate_aggregate_count_on_range() - { + .has_aggregate_count_on_range_anywhere(); + if is_acor_query && let Err(e) = path_query.validate_aggregate_count_on_range() { return Err(e).wrap_with_cost(OperationCost::default()); } - match grove_version + let prove_version = grove_version .grovedb_versions .operations .proof - .prove_query_non_serialized - { + .prove_query_non_serialized; + + // AggregateCountOnRange requires V1 proof envelopes. The legacy + // V0 (`MerkOnlyLayerProof`) envelope predates ACOR and is only + // produced by grove versions that pre-date Dash Platform v12; + // refusing the combination here keeps callers from accidentally + // emitting a V0 ACOR proof that the verifier would (correctly) + // reject. + if is_acor_query && prove_version == 0 { + return Err(Error::NotSupported( + "AggregateCountOnRange proofs require V1 proof envelopes; upgrade the grove \ + version producing the proof" + .to_string(), + )) + .wrap_with_cost(OperationCost::default()); + } + + match prove_version { 0 => self.prove_query_non_serialized_v0(path_query, prove_options, grove_version), 1 => self.prove_query_non_serialized_v1(path_query, prove_options, grove_version), version => Err(Error::VersionError( @@ -1090,6 +1105,17 @@ impl GroveDb { .wrap_with_cost(cost); } + // Whether the surrounding query is an aggregate-count carrier: + // empty trees that match a `subquery_path` step still need a + // lower-layer descent so the aggregate-count short-circuit can + // emit an empty count proof (verifier reads it as count = 0). + // For non-aggregate-count queries, empty trees keep their + // existing "terminal result" semantics. + let is_aggregate_count_query = path_query + .query + .query + .has_aggregate_count_on_range_anywhere(); + let mut merk_proof = cost_return_on_error!( &mut cost, self.generate_merk_proof( @@ -1407,6 +1433,39 @@ impl GroveDb { } has_a_result_at_level |= true; } + // Empty count trees under an aggregate-count + // carrier still need a lower-layer descent — + // the recursion hits the ACOR short-circuit on + // the empty merk and emits an empty count proof + // (verifier reads it as count = 0). + Ok(Element::ProvableCountTree(None, ..)) + | Ok(Element::ProvableCountSumTree(None, ..)) + if !done_with_results + && is_aggregate_count_query + && query.has_subquery_or_matching_in_path_on_key(key) => + { + let mut lower_path = path.clone(); + lower_path.push(key.as_slice()); + + let previous_limit = *overall_limit; + + let layer_proof = cost_return_on_error!( + &mut cost, + self.prove_subqueries_v1( + lower_path, + path_query, + overall_limit, + prove_options, + current_depth + 1, + grove_version, + ) + ); + + if previous_limit != *overall_limit { + has_a_result_at_level |= true; + } + lower_layers.insert(key.clone(), layer_proof); + } // Empty trees and CommitmentTree without subquery Ok(Element::Tree(None, _)) | Ok(Element::SumTree(None, ..)) diff --git a/grovedb/src/query/mod.rs b/grovedb/src/query/mod.rs index 2c1c0c585..40af8e37a 100644 --- a/grovedb/src/query/mod.rs +++ b/grovedb/src/query/mod.rs @@ -116,14 +116,37 @@ impl SizedQuery { } /// Validates that this sized query is a well-formed - /// `AggregateCountOnRange` query. On success, returns a reference to the - /// inner range item (the `QueryItem` wrapped by `AggregateCountOnRange`). + /// `AggregateCountOnRange` query in either the **leaf** or **carrier** + /// shape. On success, returns a reference to the leaf inner range item + /// (the `QueryItem` wrapped by the underlying `AggregateCountOnRange`, + /// whether at this level for leaf queries or inside the + /// `default_subquery_branch.subquery` for carrier queries). /// /// This is the `SizedQuery`-level entry point: it forwards to /// [`Query::validate_aggregate_count_on_range`] and additionally rejects /// any non-`None` `limit` or `offset` (counting is an aggregate over the /// full match set — pagination would silently change the answer). pub fn validate_aggregate_count_on_range(&self) -> Result<&QueryItem, Error> { + self.check_aggregate_count_size_constraints()?; + self.query + .validate_aggregate_count_on_range() + .map_err(query_validation_error_to_static_str) + .map_err(Error::InvalidQuery) + } + + /// Strict variant of [`Self::validate_aggregate_count_on_range`] that + /// only accepts the **leaf** shape (single `AggregateCountOnRange(_)` + /// item, no subqueries). Used by entry points that produce a single + /// `u64` and need to reject the carrier shape up front. + pub fn validate_leaf_aggregate_count_on_range(&self) -> Result<&QueryItem, Error> { + self.check_aggregate_count_size_constraints()?; + self.query + .validate_leaf_aggregate_count_on_range() + .map_err(query_validation_error_to_static_str) + .map_err(Error::InvalidQuery) + } + + fn check_aggregate_count_size_constraints(&self) -> Result<(), Error> { if self.limit.is_some() { return Err(Error::InvalidQuery( "AggregateCountOnRange queries may not set SizedQuery::limit", @@ -134,10 +157,7 @@ impl SizedQuery { "AggregateCountOnRange queries may not set SizedQuery::offset", )); } - self.query - .validate_aggregate_count_on_range() - .map_err(query_validation_error_to_static_str) - .map_err(Error::InvalidQuery) + Ok(()) } } @@ -146,7 +166,7 @@ impl SizedQuery { /// `grovedb_query::error::Error::InvalidOperation(&'static str)`, so this is /// just a projection of that variant; any other error variant (which would /// indicate an unrelated bug) is forwarded as a generic catch-all label. -fn query_validation_error_to_static_str(e: grovedb_query::error::Error) -> &'static str { +pub(crate) fn query_validation_error_to_static_str(e: grovedb_query::error::Error) -> &'static str { match e { grovedb_query::error::Error::InvalidOperation(msg) => msg, _ => "AggregateCountOnRange query validation failed", @@ -190,14 +210,21 @@ impl PathQuery { } /// Validates that this `PathQuery` is a well-formed - /// `AggregateCountOnRange` query. On success, returns a reference to the - /// inner range item. + /// `AggregateCountOnRange` query in either the leaf or carrier shape. + /// On success, returns a reference to the leaf inner range item. /// /// Forwards to [`SizedQuery::validate_aggregate_count_on_range`]. pub fn validate_aggregate_count_on_range(&self) -> Result<&QueryItem, Error> { self.query.validate_aggregate_count_on_range() } + /// Strict variant of [`Self::validate_aggregate_count_on_range`] that + /// only accepts the **leaf** shape (single `AggregateCountOnRange(_)` + /// item, no subqueries). + pub fn validate_leaf_aggregate_count_on_range(&self) -> Result<&QueryItem, Error> { + self.query.validate_leaf_aggregate_count_on_range() + } + /// Returns `true` if this `PathQuery`'s underlying query carries an /// `AggregateCountOnRange` item (whether well-formed or not). Use /// [`Self::validate_aggregate_count_on_range`] when you also need diff --git a/grovedb/src/tests/aggregate_count_query_tests.rs b/grovedb/src/tests/aggregate_count_query_tests.rs index b812d740e..e4568ebc7 100644 --- a/grovedb/src/tests/aggregate_count_query_tests.rs +++ b/grovedb/src/tests/aggregate_count_query_tests.rs @@ -735,16 +735,16 @@ mod tests { #[test] fn aggregate_count_with_missing_path_and_invalid_inner_is_rejected_at_entry() { // Codex finding: validation only fires inside `prove_subqueries` when - // the recursion reaches the ACOR-bearing leaf level. If the path + // the recursion reaches the aggregate-count-bearing leaf level. If the path // doesn't exist (e.g. "missing" key under TEST_LEAF), the recursive - // prover never sees the ACOR item and the malformed query is allowed + // prover never sees the aggregate-count item and the malformed query is allowed // to return a regular path/absence proof. Fix: validate at the // `prove_query` entry point, before any recursive dispatch. let v = GroveVersion::latest(); let db = make_test_grovedb(v); let path_query = PathQuery::new_aggregate_count_on_range( vec![TEST_LEAF.to_vec(), b"missing".to_vec()], - // QueryItem::Key as the inner range is invalid for ACOR. + // QueryItem::Key as the inner range is invalid for aggregate-count. QueryItem::Key(b"k".to_vec()), ); let prove_result = db.grove_db.prove_query(&path_query, None, v).unwrap(); @@ -752,33 +752,35 @@ mod tests { Err(crate::Error::InvalidQuery(msg)) => { assert!( msg.contains("AggregateCountOnRange may not wrap Key"), - "expected ACOR-Key rejection, got: {msg}" + "expected `AggregateCountOnRange`-Key rejection, got: {msg}" ); } other => panic!( - "malformed ACOR with non-existent path must be rejected at entry, got {:?}", + "malformed aggregate-count with non-existent path must be rejected at entry, got {:?}", other.map(|b| b.len()) ), } } #[test] - fn aggregate_count_hidden_in_subquery_branch_is_rejected_at_entry() { - // Codex's broader concern: an `AggregateCountOnRange` smuggled - // inside a `default_subquery_branch.subquery` is also invalid (ACOR - // is terminal — it cannot be reached via a normal subquery path) - // and must be rejected up front. The recursive detector - // `has_aggregate_count_on_range_anywhere` finds the hidden ACOR; - // top-level `validate_aggregate_count_on_range` then rejects - // because the surrounding query isn't the canonical single-ACOR - // shape. + fn aggregate_count_hidden_in_subquery_branch_with_invalid_inner_is_rejected_at_entry() { + // After the carrier aggregate-count feature landed, an `AggregateCountOnRange` + // smuggled inside a `default_subquery_branch.subquery` is **valid** + // when the surrounding query satisfies the carrier rules — that is + // the whole point of the carrier shape. + // + // What this test still guards is the *other* malformed case: a + // carrier whose subquery is itself a malformed leaf `AggregateCountOnRange` (here, an + // aggregate-count wrapping `Key` — leaf rule 3). The carrier validator + // delegates to `validate_leaf_aggregate_count_on_range`, which + // surfaces the malformed-inner error, and the prove-entry gate + // refuses to run the query. let v = GroveVersion::latest(); let db = make_test_grovedb(v); - let inner_acor = QueryItem::AggregateCountOnRange(Box::new(QueryItem::Range( - b"a".to_vec()..b"z".to_vec(), - ))); + let bad_inner_aggregate_count = + QueryItem::AggregateCountOnRange(Box::new(QueryItem::Key(b"k".to_vec()))); let mut sub_query = grovedb_merk::proofs::Query::new(); - sub_query.insert_item(inner_acor); + sub_query.insert_item(bad_inner_aggregate_count); let mut top_query = grovedb_merk::proofs::Query::new(); top_query.insert_range_inclusive(b"a".to_vec()..=b"z".to_vec()); top_query.set_subquery(sub_query); @@ -787,11 +789,21 @@ mod tests { SizedQuery::new(top_query, None, None), ); let prove_result = db.grove_db.prove_query(&path_query, None, v).unwrap(); - assert!( - matches!(prove_result, Err(crate::Error::InvalidQuery(_))), - "ACOR hidden in subquery branch must be rejected at entry, got {:?}", - prove_result.map(|b| b.len()) - ); + // Pin the specific reason (the leaf validator's "wrap Key" + // rejection delegated through the carrier validator) so a + // future refactor that re-routes the rejection through a + // different but still-`InvalidQuery` arm doesn't silently + // accept the malformed shape. + match prove_result { + Err(crate::Error::InvalidQuery(msg)) => assert!( + msg.contains("AggregateCountOnRange may not wrap Key"), + "expected malformed-inner-Key rejection, got: {msg}" + ), + other => panic!( + "carrier aggregate-count with malformed leaf-inner Key must be rejected at entry, got {:?}", + other.map(|b| b.len()) + ), + } } #[test] @@ -824,26 +836,31 @@ mod tests { } #[test] - fn provable_count_tree_works_on_grove_v2_envelope() { - // GROVE_V2 dispatches to the V0 prove_query_non_serialized path, which - // produces a `MerkOnlyLayerProof` envelope rather than V1's - // `LayerProof`. Verify the same prove → verify cycle works through that - // envelope. + fn aggregate_count_rejects_grove_v2_envelope() { + // GROVE_V2 dispatches to the V0 prove_query_non_serialized path, + // which produces a `MerkOnlyLayerProof` envelope. aggregate-count was added + // after V0 envelopes were superseded by V1 (in the grove version + // used by Dash Platform v12+), so V0+aggregate-count is impossible in any + // deployed Platform release. The prover rejects the combination + // up front to keep callers from emitting a V0 aggregate-count proof that + // the verifier would (correctly) refuse. let v: &GroveVersion = &GROVE_V2; - let (db, root) = setup_15_key_provable_count_tree(v); + let (db, _root) = setup_15_key_provable_count_tree(v); let path_query = PathQuery::new_aggregate_count_on_range( vec![TEST_LEAF.to_vec(), b"ct".to_vec()], QueryItem::RangeInclusive(b"c".to_vec()..=b"l".to_vec()), ); - let proof = db - .grove_db - .prove_query(&path_query, None, v) - .unwrap() - .expect("prove_query (v0 envelope) should succeed"); - let (got_root, got_count) = GroveDb::verify_aggregate_count_query(&proof, &path_query, v) - .expect("verify should succeed against v0 envelope"); - assert_eq!(got_root, root); - assert_eq!(got_count, 10); + let prove_result = db.grove_db.prove_query(&path_query, None, v).unwrap(); + match prove_result { + Err(crate::Error::NotSupported(msg)) => assert!( + msg.contains("V1 proof envelopes"), + "unexpected message: {msg}" + ), + other => panic!( + "expected NotSupported for V0+aggregate-count, got {:?}", + other.map(|b| b.len()) + ), + } } #[test] @@ -1382,6 +1399,46 @@ mod tests { ); } + #[test] + fn no_proof_rejects_carrier_shape() { + // `query_aggregate_count` returns a single `u64` and has no way + // to surface per-outer-key carrier counts. Calling it with a + // carrier-shape path query must be rejected up front by the + // leaf-only validator, BEFORE any storage reads happen — even + // though the dispatcher-level `validate_aggregate_count_on_range` + // would have accepted the same query. + use grovedb_query::Query; + let v = GroveVersion::latest(); + let (db, _) = setup_15_key_provable_count_tree(v); + + let mut carrier = Query::new(); + carrier.insert_key(b"brand_000".to_vec()); + carrier.set_subquery_path(vec![b"color".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range(QueryItem::Range( + b"a".to_vec()..b"z".to_vec(), + ))); + let path_query = PathQuery::new( + vec![TEST_LEAF.to_vec(), b"ct".to_vec()], + SizedQuery::new(carrier, None, None), + ); + + // Sanity: the dispatcher-level validator accepts this as a + // valid carrier, so the rejection below is specifically + // because `query_aggregate_count` tightens to leaf-only. + assert!(path_query.validate_aggregate_count_on_range().is_ok()); + + let err = db + .grove_db + .query_aggregate_count(&path_query, None, v) + .unwrap() + .expect_err("carrier shape must be rejected at the no-proof entry"); + assert!( + matches!(err, crate::Error::InvalidQuery(_)), + "expected InvalidQuery, got {:?}", + err + ); + } + #[test] fn no_proof_rejects_invalid_inner_range() { // Same shape check the prover/verifier use: Key inner is invalid for @@ -1545,4 +1602,1312 @@ mod tests { .expect("query_aggregate_count on empty tree should succeed"); assert_eq!(count, 0, "empty tree must return 0"); } + + // ---------- No-proof per-key entry point ---------- + // + // `query_aggregate_count_per_key` is the no-proof counterpart of + // `verify_aggregate_count_query_per_key`: same surface shape + // (`Vec<(Vec, u64)>`), accepts both leaf and carrier path + // queries, but skips proof generation and verification entirely. + + #[test] + fn no_proof_per_key_leaf_matches_single_count() { + // Leaf-shape path query → returns a one-entry vec with an + // empty key and the same count `query_aggregate_count` + // returns (the per-key entry's leaf-symmetry contract). + let v = GroveVersion::latest(); + let (db, _) = setup_15_key_provable_count_tree(v); + let path_query = PathQuery::new_aggregate_count_on_range( + vec![TEST_LEAF.to_vec(), b"ct".to_vec()], + QueryItem::RangeInclusive(b"c".to_vec()..=b"l".to_vec()), + ); + + let single = db + .grove_db + .query_aggregate_count(&path_query, None, v) + .unwrap() + .expect("legacy single-u64 entry should succeed"); + let per_key = db + .grove_db + .query_aggregate_count_per_key(&path_query, None, v) + .unwrap() + .expect("per-key entry should succeed"); + + assert_eq!(single, 10); + assert_eq!(per_key.len(), 1); + assert_eq!(per_key[0].0, Vec::::new()); + assert_eq!(per_key[0].1, single); + } + + #[test] + fn no_proof_per_key_carrier_returns_per_outer_count() { + // Carrier shape → one (brand, count) entry per matched outer + // key, mirroring `verify_aggregate_count_query_per_key`'s + // contract. + let v = GroveVersion::latest(); + let (db, _root) = setup_brand_color_carrier_tree(v, &[b"brand_000", b"brand_001"], 1_000); + let path_query = carrier_count_path_query( + &[b"brand_000", b"brand_001"], + QueryItem::RangeAfter(b"color_00499".to_vec()..), + ); + let results = db + .grove_db + .query_aggregate_count_per_key(&path_query, None, v) + .unwrap() + .expect("no-proof carrier query should succeed"); + assert_eq!(results.len(), 2); + assert_eq!(results[0].0, b"brand_000".to_vec()); + assert_eq!(results[1].0, b"brand_001".to_vec()); + assert_eq!(results[0].1, 500); + assert_eq!(results[1].1, 500); + } + + #[test] + fn no_proof_per_key_skips_absent_outer_keys() { + // Absent outer keys contribute no entry — same as the proof + // path's behavior. + let v = GroveVersion::latest(); + let (db, _root) = setup_brand_color_carrier_tree(v, &[b"brand_000"], 100); + let path_query = carrier_count_path_query( + &[b"brand_000", b"brand_missing"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + let results = db + .grove_db + .query_aggregate_count_per_key(&path_query, None, v) + .unwrap() + .expect("no-proof carrier query should succeed"); + assert_eq!(results.len(), 1, "absent key contributes no entry"); + assert_eq!(results[0].0, b"brand_000".to_vec()); + assert_eq!(results[0].1, 50); + } + + #[test] + fn no_proof_per_key_empty_leaf_returns_zero() { + // Outer key exists, subquery_path resolves cleanly, but the + // leaf count tree is empty. Match the proof path: emit + // `(key, 0)` rather than skipping or erroring. + use grovedb_query::Query; + let v = GroveVersion::latest(); + let db = make_test_grovedb(v); + db.insert( + [TEST_LEAF].as_ref(), + b"byBrand", + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert byBrand"); + db.insert( + [TEST_LEAF, b"byBrand"].as_ref(), + b"brand_000", + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert brand"); + db.insert( + [TEST_LEAF, b"byBrand", b"brand_000"].as_ref(), + b"color", + Element::empty_provable_count_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert empty color"); + + let mut carrier = Query::new(); + carrier.insert_key(b"brand_000".to_vec()); + carrier.set_subquery_path(vec![b"color".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range(QueryItem::Range( + b"a".to_vec()..b"z".to_vec(), + ))); + let path_query = PathQuery::new( + vec![TEST_LEAF.to_vec(), b"byBrand".to_vec()], + SizedQuery::new(carrier, None, None), + ); + let results = db + .grove_db + .query_aggregate_count_per_key(&path_query, None, v) + .unwrap() + .expect("no-proof carrier with empty leaf should succeed"); + assert_eq!(results.len(), 1); + assert_eq!(results[0].0, b"brand_000".to_vec()); + assert_eq!(results[0].1, 0); + } + + #[test] + fn no_proof_per_key_matches_proof_path_per_key() { + // Cross-check: for a non-trivial carrier query, the no-proof + // result must agree element-for-element with the proof-based + // `verify_aggregate_count_query_per_key`. + let v = GroveVersion::latest(); + let (db, _root) = + setup_brand_color_carrier_tree(v, &[b"brand_000", b"brand_001", b"brand_002"], 100); + let path_query = carrier_count_path_query( + &[b"brand_000", b"brand_001", b"brand_002"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + let no_proof = db + .grove_db + .query_aggregate_count_per_key(&path_query, None, v) + .unwrap() + .expect("no-proof should succeed"); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + let (_root, proved) = GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("verify should succeed"); + assert_eq!(no_proof, proved); + } + + #[test] + fn no_proof_per_key_rejects_non_aggregate_count_query() { + // Same validation gate as the proof per-key entry: non-ACOR + // path queries are rejected up front with `InvalidQuery`. + let v = GroveVersion::latest(); + let path_query = PathQuery::new_single_query_item( + vec![TEST_LEAF.to_vec()], + QueryItem::Key(b"k".to_vec()), + ); + let db = make_test_grovedb(v); + let err = db + .grove_db + .query_aggregate_count_per_key(&path_query, None, v) + .unwrap() + .expect_err("non-aggregate-count path query must be rejected"); + assert!(matches!(err, crate::Error::InvalidQuery(_))); + } + + // ---------- Carrier aggregate-count end-to-end tests ---------- + // + // A "carrier" aggregate-count query is an outer fan-out — the outer query items + // are `Key`/`Range*` and the `default_subquery_branch.subquery` + // resolves (after walking the optional `subquery_path`) to a leaf + // aggregate-count. The verifier returns one `(outer_key, u64)` pair per matched + // outer key. These tests exercise the full prove → encode → decode → + // verify pipeline. + + /// Build a 3-deep tree shaped like the Dash Platform GROUP BY use + /// case: `TEST_LEAF / "byBrand" / / "color" / + /// `. + /// + /// Each brand subtree has a `color` child that is a + /// `ProvableCountTree` populated with `colors_per_brand` keys of the + /// form `color_`. + fn setup_brand_color_carrier_tree( + grove_version: &GroveVersion, + brands: &[&[u8]], + colors_per_brand: u32, + ) -> (crate::tests::TempGroveDb, [u8; 32]) { + let db = make_test_grovedb(grove_version); + db.insert( + [TEST_LEAF].as_ref(), + b"byBrand", + Element::empty_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("insert byBrand"); + for brand in brands { + db.insert( + [TEST_LEAF, b"byBrand"].as_ref(), + brand, + Element::empty_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("insert brand"); + db.insert( + [TEST_LEAF, b"byBrand", brand].as_ref(), + b"color", + Element::empty_provable_count_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("insert color subtree"); + for i in 0..colors_per_brand { + let key = format!("color_{:05}", i); + db.insert( + [TEST_LEAF, b"byBrand", brand, b"color"].as_ref(), + key.as_bytes(), + Element::new_item(key.as_bytes().to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("insert color leaf"); + } + } + let root = db + .grove_db + .root_hash(None, grove_version) + .unwrap() + .expect("root_hash"); + (db, root) + } + + /// Build a carrier aggregate-count `PathQuery` rooted at + /// `[TEST_LEAF, "byBrand"]`, fanning out across `outer_keys` and + /// counting elements in each brand's `color` subtree matching the + /// inner range. + fn carrier_count_path_query(outer_keys: &[&[u8]], inner_range: QueryItem) -> PathQuery { + use grovedb_query::Query; + + let mut carrier = Query::new(); + for k in outer_keys { + // Use `insert_key` (not `items.push`) so items end up in + // sorted-ascending order — the merk multi-key walker + // expects that invariant. + carrier.insert_key(k.to_vec()); + } + carrier.set_subquery_path(vec![b"color".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range(inner_range)); + + PathQuery::new( + vec![TEST_LEAF.to_vec(), b"byBrand".to_vec()], + SizedQuery::new(carrier, None, None), + ) + } + + #[test] + fn carrier_two_outer_keys_succeeds() { + // Carrier with two outer brand keys, range on the color subtree. + // Expected: two (key, count) pairs in query-direction order with + // the correct per-brand aggregate. The carrier defaults to + // `left_to_right=true`, so output is ascending lex. + let v = GroveVersion::latest(); + let (db, expected_root) = + setup_brand_color_carrier_tree(v, &[b"brand_000", b"brand_001"], 1_000); + // Pick a range that drops the lower 500 elements (`color_00000` + // through `color_00499`). + let path_query = carrier_count_path_query( + &[b"brand_000", b"brand_001"], + QueryItem::RangeAfter(b"color_00499".to_vec()..), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query (carrier aggregate-count) should succeed"); + let (got_root, results) = + GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("verify carrier aggregate-count should succeed"); + assert_eq!(got_root, expected_root, "root must match GroveDB root"); + assert_eq!(results.len(), 2, "expected one result per outer key"); + assert_eq!(results[0].0, b"brand_000".to_vec()); + assert_eq!(results[1].0, b"brand_001".to_vec()); + // Each brand has 1 000 colors; range_after `color_00499` leaves + // the upper 500 (`color_00500` .. `color_00999`). + assert_eq!(results[0].1, 500); + assert_eq!(results[1].1, 500); + } + + #[test] + fn carrier_with_unknown_outer_key_returns_present_keys_only() { + // Spec acceptance criterion 2: an outer-key match that doesn't + // exist contributes no entry to the result vector (it's an + // absence, not an error). The prover doesn't emit a lower layer + // for keys that don't exist in the carrier subtree, so the + // verifier sees only the matched keys. + let v = GroveVersion::latest(); + let (db, expected_root) = setup_brand_color_carrier_tree(v, &[b"brand_000"], 1_000); + // Ask for two brands — one present, one absent. + let path_query = carrier_count_path_query( + &[b"brand_000", b"brand_999_missing"], + QueryItem::RangeAfter(b"color_00499".to_vec()..), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + let (got_root, results) = + GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("verify should succeed"); + assert_eq!(got_root, expected_root); + // Only the present brand contributes a result row. + assert_eq!( + results.len(), + 1, + "absent outer keys must not contribute an entry" + ); + assert_eq!(results[0].0, b"brand_000".to_vec()); + assert_eq!(results[0].1, 500); + } + + #[test] + fn rejects_nested_carrier_range_range_aggregate_count() { + // Out of scope: a "Range × Range × AggregateCountOnRange" + // shape — an outer carrier whose subquery is *itself* another + // carrier. This is the `IN × IN`-on-prefix case the spec + // explicitly defers. The carrier validator delegates to the + // leaf validator for the subquery, which rejects because the + // inner carrier has its own outer items (not a single + // `AggregateCountOnRange`). Both the static validator and the + // prover's entry-point gate must refuse. + use grovedb_query::Query; + + // inner_carrier: Range outer + leaf aggregate-count subquery. + let mut inner_carrier = Query::new(); + inner_carrier + .items + .push(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + inner_carrier.set_subquery_path(vec![b"leaf".to_vec()]); + inner_carrier.set_subquery(Query::new_aggregate_count_on_range(QueryItem::Range( + b"a".to_vec()..b"z".to_vec(), + ))); + + // outer_carrier: Range outer + inner_carrier as subquery. + let mut outer_carrier = Query::new(); + outer_carrier + .items + .push(QueryItem::Range(b"A".to_vec()..b"Z".to_vec())); + outer_carrier.set_subquery_path(vec![b"middle".to_vec()]); + outer_carrier.set_subquery(inner_carrier); + + let pq = PathQuery::new( + vec![TEST_LEAF.to_vec()], + SizedQuery::new(outer_carrier, None, None), + ); + let v = GroveVersion::latest(); + + // Static validator rejects. + assert!( + pq.validate_aggregate_count_on_range().is_err(), + "nested carrier (Range x Range x ACOR) must fail validation" + ); + + // Prover entry-point gate also rejects. + let prove_result = make_test_grovedb(v).grove_db.prove_query(&pq, None, v); + match prove_result.value() { + Err(crate::Error::InvalidQuery(_)) => {} + other => panic!("expected InvalidQuery, got {:?}", other), + } + } + + #[test] + fn rejects_aggregate_count_at_both_levels() { + // Try to build a query where the carrier ITSELF has an aggregate-count item + // AND its subquery is also an aggregate-count. The validator must reject up + // front at prove time. + use grovedb_query::Query; + + let mut q = + Query::new_aggregate_count_on_range(QueryItem::Range(b"a".to_vec()..b"z".to_vec())); + q.set_subquery(Query::new_aggregate_count_on_range(QueryItem::Range( + b"a".to_vec()..b"z".to_vec(), + ))); + let pq = PathQuery::new(vec![TEST_LEAF.to_vec()], SizedQuery::new(q, None, None)); + let v = GroveVersion::latest(); + // Validation catches it. + assert!( + pq.validate_aggregate_count_on_range().is_err(), + "aggregate-count + subquery aggregate-count must fail validation" + ); + // The prove_query entry-point gate must also reject it. + let prove_result = make_test_grovedb(v).grove_db.prove_query(&pq, None, v); + match prove_result.value() { + Err(crate::Error::InvalidQuery(_)) => {} + other => panic!("expected InvalidQuery, got {:?}", other), + } + } + + #[test] + fn leaf_unchanged_under_per_key_verifier() { + // The leaf shape — a single-`AggregateCountOnRange` query — produces exactly the + // same proof bytes it did before this feature. Verifying it via + // the new per-key entry point returns a one-entry Vec with an + // empty key and the same count `verify_aggregate_count_query` + // returns. This is the leaf-symmetry contract. + let v = GroveVersion::latest(); + let (db, expected_root) = setup_15_key_provable_count_tree(v); + let path_query = PathQuery::new_aggregate_count_on_range( + vec![TEST_LEAF.to_vec(), b"ct".to_vec()], + QueryItem::RangeInclusive(b"c".to_vec()..=b"l".to_vec()), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + // Existing single-u64 entry point still works. + let (root_one, count_one) = GroveDb::verify_aggregate_count_query(&proof, &path_query, v) + .expect("legacy leaf verifier must still accept legacy leaf proof"); + // New per-key entry point also accepts leaf and returns a + // one-entry Vec with an empty key. + let (root_many, results) = + GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("per-key verifier must accept leaf proofs"); + assert_eq!(root_one, expected_root); + assert_eq!(root_one, root_many); + assert_eq!(count_one, 10); + assert_eq!(results.len(), 1); + assert_eq!(results[0].0, Vec::::new()); + assert_eq!(results[0].1, count_one); + } + + #[test] + fn carrier_with_range_outer_succeeds() { + // The carrier supports a Range outer item (the per-spec + // "decide-or-defer" case). With an outer `RangeAfter`, the + // matched outer keys come back in lex-asc order and each + // contributes its own count. + use grovedb_query::Query; + let v = GroveVersion::latest(); + let (db, expected_root) = + setup_brand_color_carrier_tree(v, &[b"brand_000", b"brand_001", b"brand_002"], 100); + + let mut carrier = Query::new(); + // Take everything strictly after brand_000 → brand_001, brand_002. + carrier + .items + .push(QueryItem::RangeAfter(b"brand_000".to_vec()..)); + carrier.set_subquery_path(vec![b"color".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range(QueryItem::RangeAfter( + b"color_00049".to_vec().., + ))); + let path_query = PathQuery::new( + vec![TEST_LEAF.to_vec(), b"byBrand".to_vec()], + SizedQuery::new(carrier, None, None), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query (carrier with Range outer) should succeed"); + let (got_root, results) = + GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("verify carrier with Range outer should succeed"); + assert_eq!(got_root, expected_root); + assert_eq!(results.len(), 2); + assert_eq!(results[0].0, b"brand_001".to_vec()); + assert_eq!(results[1].0, b"brand_002".to_vec()); + for (_, count) in results { + // 100 colors per brand; > color_00049 leaves 50. + assert_eq!(count, 50); + } + } + + #[test] + fn carrier_right_to_left_returns_descending_order() { + // Flip the carrier's `left_to_right` flag — output must come + // back in descending lex order, mirroring the merk walker's + // reversed emission. + use grovedb_query::Query; + let v = GroveVersion::latest(); + let (db, expected_root) = + setup_brand_color_carrier_tree(v, &[b"brand_000", b"brand_001", b"brand_002"], 100); + let mut carrier = Query::new_with_direction(false); + carrier.insert_key(b"brand_000".to_vec()); + carrier.insert_key(b"brand_001".to_vec()); + carrier.insert_key(b"brand_002".to_vec()); + carrier.set_subquery_path(vec![b"color".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range(QueryItem::RangeAfter( + b"color_00049".to_vec().., + ))); + let path_query = PathQuery::new( + vec![TEST_LEAF.to_vec(), b"byBrand".to_vec()], + SizedQuery::new(carrier, None, None), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query (carrier aggregate-count, right-to-left) should succeed"); + let (got_root, results) = + GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("verify carrier aggregate-count (right-to-left) should succeed"); + assert_eq!(got_root, expected_root); + assert_eq!(results.len(), 3, "expected 3 outer-key matches"); + // Descending lex: brand_002, brand_001, brand_000. + assert_eq!(results[0].0, b"brand_002".to_vec()); + assert_eq!(results[1].0, b"brand_001".to_vec()); + assert_eq!(results[2].0, b"brand_000".to_vec()); + for (_, count) in results { + assert_eq!(count, 50); + } + } + + #[test] + fn per_key_rejects_non_aggregate_count_path_query() { + // The per-key entry point rejects path queries that aren't aggregate-count + // queries at all — neither leaf nor carrier — before decoding + // proof bytes. + let v = GroveVersion::latest(); + let bad_query = PathQuery::new_single_query_item( + vec![TEST_LEAF.to_vec()], + QueryItem::Key(b"k".to_vec()), + ); + let dummy_proof = vec![0u8; 16]; + let err = GroveDb::verify_aggregate_count_query_per_key(&dummy_proof, &bad_query, v) + .expect_err("non-aggregate-count path_query must be rejected up front"); + match err { + crate::Error::InvalidQuery(_) => {} + other => panic!("expected InvalidQuery, got {:?}", other), + } + } + + #[test] + fn carrier_count_forgery_is_caught() { + // Same spirit as `count_forgery_is_caught_at_grovedb_level` but + // against a carrier proof: pick the first leaf merk + // `HashWithCount` op in any of the per-outer-key sub-proofs and + // bump its count. The verifier must reject. + use bincode::config; + use grovedb_merk::proofs::{encoding::encode_into, Decoder, Node, Op}; + + use crate::operations::proof::{ + GroveDBProof, GroveDBProofV0, GroveDBProofV1, LayerProof, MerkOnlyLayerProof, + ProofBytes, + }; + + let v = GroveVersion::latest(); + let (db, _root) = setup_brand_color_carrier_tree(v, &[b"brand_000", b"brand_001"], 100); + let path_query = carrier_count_path_query( + &[b"brand_000", b"brand_001"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + + let cfg = config::standard() + .with_big_endian() + .with_limit::<{ 256 * 1024 * 1024 }>(); + let (mut decoded, _): (GroveDBProof, _) = + bincode::decode_from_slice(&proof, cfg).expect("decode envelope"); + + // Walk to the first leaf merk proof bytes via depth-first + // descent: we expect the path `TEST_LEAF -> byBrand -> brand_000 + // -> color`. The "leaf" is the deepest layer (a leaf has no + // further lower_layers; in our test setup that's the count proof + // at the color subtree). + fn first_leaf_v0(mut layer: &mut MerkOnlyLayerProof) -> &mut Vec { + while let Some((_, child)) = layer.lower_layers.iter_mut().next() { + layer = child; + } + &mut layer.merk_proof + } + fn first_leaf_v1(mut layer: &mut LayerProof) -> &mut Vec { + while let Some((_, child)) = layer.lower_layers.iter_mut().next() { + layer = child; + } + match &mut layer.merk_proof { + ProofBytes::Merk(b) => b, + _ => panic!("expected Merk leaf bytes"), + } + } + let leaf_bytes: &mut Vec = match &mut decoded { + GroveDBProof::V0(GroveDBProofV0 { root_layer, .. }) => first_leaf_v0(root_layer), + GroveDBProof::V1(GroveDBProofV1 { root_layer }) => first_leaf_v1(root_layer), + }; + let mut ops: Vec = Decoder::new(leaf_bytes) + .map(|r| r.expect("decode op")) + .collect(); + let mut tampered = false; + for op in ops.iter_mut() { + match op { + Op::Push(Node::HashWithCount(_, _, _, count)) + | Op::PushInverted(Node::HashWithCount(_, _, _, count)) => { + *count = count.wrapping_add(1); + tampered = true; + break; + } + _ => {} + } + } + assert!(tampered, "expected a HashWithCount in the leaf proof"); + let mut new_leaf = Vec::new(); + encode_into(ops.iter(), &mut new_leaf); + *leaf_bytes = new_leaf; + let new_proof = bincode::encode_to_vec( + decoded, + config::standard().with_big_endian().with_no_limit(), + ) + .expect("re-encode"); + + let result = GroveDb::verify_aggregate_count_query_per_key(&new_proof, &path_query, v); + assert!( + result.is_err(), + "tampered carrier count must be rejected, got {:?}", + result.map(|(_, c)| c) + ); + } + + #[test] + fn carrier_rejects_v0_envelope() { + // V0 proof envelopes predate aggregate-count and cannot legitimately carry + // an aggregate-count proof — neither leaf nor carrier. The + // prover-side entry-point gate refuses to emit V0+aggregate-count. + let v2 = &GROVE_V2; + let (db, _root) = setup_brand_color_carrier_tree(v2, &[b"brand_000"], 100); + let path_query = carrier_count_path_query( + &[b"brand_000"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + match db.grove_db.prove_query(&path_query, None, v2).unwrap() { + Err(crate::Error::NotSupported(msg)) => assert!( + msg.contains("V1 proof envelopes"), + "unexpected message: {msg}" + ), + other => panic!( + "expected NotSupported for V0+carrier aggregate-count, got {:?}", + other.map(|b| b.len()) + ), + } + } + + #[test] + fn carrier_sql_style_fixed_prefix_range_then_count_succeeds() { + // Demonstrates the SQL-style 3-column aggregate query + // + // SELECT COUNT(*) FROM t WHERE a = 1 AND b > 4 AND c > 4 + // + // against an `(a, b, c)`-indexed grove laid out as + // + // TEST_LEAF / byA / / byB / / byC / + // + // + // The mapping is: + // - `A = 1` is a fixed prefix → lives in `path_query.path` + // (the verifier walks it via single-key descents). + // - `B > 4` is the variable outer dimension → carrier's + // `RangeAfter("b_4")` item. + // - per matched `B`, walk `byC` → carrier's `subquery_path`. + // - `COUNT(C > 4)` is the leaf aggregate-count subquery. + // + // Expected: one `(b_val, count)` entry per matched `b > 4`, + // each carrying the count of `c > 4` under that `(a=1, b)` cell. + use grovedb_query::Query; + let v = GroveVersion::latest(); + let db = make_test_grovedb(v); + + // Build the tree: TEST_LEAF/byA/1/byB//byC/. + db.insert( + [TEST_LEAF].as_ref(), + b"byA", + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert byA"); + // Insert two `A` values so we can confirm the path prefix + // actually scopes the count (queries with `A = 1` must not + // see anything under `A = 2`). + for a_val in [b"1".as_ref(), b"2".as_ref()] { + db.insert( + [TEST_LEAF, b"byA"].as_ref(), + a_val, + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert a_val"); + db.insert( + [TEST_LEAF, b"byA", a_val].as_ref(), + b"byB", + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert byB"); + for b_val in [b"b_3".as_ref(), b"b_5".as_ref(), b"b_7".as_ref()] { + db.insert( + [TEST_LEAF, b"byA", a_val, b"byB"].as_ref(), + b_val, + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert b_val"); + db.insert( + [TEST_LEAF, b"byA", a_val, b"byB", b_val].as_ref(), + b"byC", + Element::empty_provable_count_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert byC"); + for i in 0..10u8 { + let c_key = format!("c_{i}"); + db.insert( + [TEST_LEAF, b"byA", a_val, b"byB", b_val, b"byC"].as_ref(), + c_key.as_bytes(), + Element::new_item(c_key.as_bytes().to_vec()), + None, + None, + v, + ) + .unwrap() + .expect("insert c"); + } + } + } + let expected_root = db.grove_db.root_hash(None, v).unwrap().expect("root_hash"); + + // Carrier: `B > "b_4"` outer, walk `byC`, count `C > "c_4"`. + let mut carrier = Query::new(); + carrier.items.push(QueryItem::RangeAfter(b"b_4".to_vec()..)); + carrier.set_subquery_path(vec![b"byC".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range(QueryItem::RangeAfter( + b"c_4".to_vec().., + ))); + + // PathQuery: fix `A = 1` via the path prefix. + let path_query = PathQuery::new( + vec![ + TEST_LEAF.to_vec(), + b"byA".to_vec(), + b"1".to_vec(), + b"byB".to_vec(), + ], + SizedQuery::new(carrier, None, None), + ); + + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove (A=1, B>4, COUNT C>4) should succeed"); + let (got_root, results) = + GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("verify should succeed"); + assert_eq!(got_root, expected_root); + // B > "b_4" matches `b_5` and `b_7` (not `b_3`). For each + // matched B, count C > "c_4" → c_5..=c_9 → 5 elements. + assert_eq!(results.len(), 2, "expected b_5 and b_7"); + assert_eq!(results[0].0, b"b_5".to_vec()); + assert_eq!(results[1].0, b"b_7".to_vec()); + assert_eq!(results[0].1, 5); + assert_eq!(results[1].1, 5); + } + + #[test] + fn carrier_returns_zero_count_for_empty_leaf_subtree() { + // An outer-key match exists, the subquery_path resolves + // cleanly, but the **leaf** `ProvableCountTree` is empty + // (root_key = None). The verifier must still get a clean + // `(brand, 0)` entry — the leaf count proof for an empty + // merk is the empty op stream, which the merk-level verifier + // reads as `(NULL_HASH, 0)`, and the chain check + // `combine_hash(H(empty_tree_value), NULL_HASH) == + // parent_value_hash` holds because the parent committed the + // tree element with the same NULL_HASH child root. + use grovedb_query::Query; + let v = GroveVersion::latest(); + let db = make_test_grovedb(v); + db.insert( + [TEST_LEAF].as_ref(), + b"byBrand", + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert byBrand"); + db.insert( + [TEST_LEAF, b"byBrand"].as_ref(), + b"brand_000", + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert brand"); + // Insert the leaf `color` count tree but leave it empty. + db.insert( + [TEST_LEAF, b"byBrand", b"brand_000"].as_ref(), + b"color", + Element::empty_provable_count_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert empty color count tree"); + let expected_root = db.grove_db.root_hash(None, v).unwrap().expect("root_hash"); + + let mut carrier = Query::new(); + carrier.insert_key(b"brand_000".to_vec()); + carrier.set_subquery_path(vec![b"color".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range(QueryItem::Range( + b"a".to_vec()..b"z".to_vec(), + ))); + let path_query = PathQuery::new( + vec![TEST_LEAF.to_vec(), b"byBrand".to_vec()], + SizedQuery::new(carrier, None, None), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed even when leaf count tree is empty"); + let (got_root, results) = + GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("verify should succeed with (brand_000, 0)"); + assert_eq!(got_root, expected_root); + assert_eq!(results.len(), 1, "expected one entry for brand_000"); + assert_eq!(results[0].0, b"brand_000".to_vec()); + assert_eq!( + results[0].1, 0, + "empty leaf count tree must yield count = 0" + ); + } + + #[test] + fn carrier_with_long_subquery_path_succeeds() { + // Exercises a non-trivial `subquery_path` (length > 1) in the + // carrier shape: TEST_LEAF / "outer" / / "level1" / + // "level2" / . The verifier must walk both + // intermediate single-key layers between each outer-key match + // and the leaf merk. + use grovedb_query::Query; + let v = GroveVersion::latest(); + let db = make_test_grovedb(v); + db.insert( + [TEST_LEAF].as_ref(), + b"outer", + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert outer"); + for brand in [b"a".as_ref(), b"b".as_ref()] { + db.insert( + [TEST_LEAF, b"outer"].as_ref(), + brand, + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert brand"); + db.insert( + [TEST_LEAF, b"outer", brand].as_ref(), + b"level1", + Element::empty_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert level1"); + db.insert( + [TEST_LEAF, b"outer", brand, b"level1"].as_ref(), + b"level2", + Element::empty_provable_count_tree(), + None, + None, + v, + ) + .unwrap() + .expect("insert level2"); + for c in b'a'..=b'e' { + db.insert( + [TEST_LEAF, b"outer", brand, b"level1", b"level2"].as_ref(), + &[c], + Element::new_item(vec![c]), + None, + None, + v, + ) + .unwrap() + .expect("insert leaf"); + } + } + let expected_root = db.grove_db.root_hash(None, v).unwrap().expect("root_hash"); + + // Carrier path query: walks "level1" → "level2" between each + // outer-brand match and the leaf count proof. + let mut carrier = Query::new(); + carrier.insert_key(b"a".to_vec()); + carrier.insert_key(b"b".to_vec()); + carrier.set_subquery_path(vec![b"level1".to_vec(), b"level2".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range( + QueryItem::RangeInclusive(b"b".to_vec()..=b"d".to_vec()), + )); + let path_query = PathQuery::new( + vec![TEST_LEAF.to_vec(), b"outer".to_vec()], + SizedQuery::new(carrier, None, None), + ); + + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query (carrier with long subquery_path) should succeed"); + let (got_root, results) = + GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect("verify carrier (long subquery_path) should succeed"); + assert_eq!(got_root, expected_root); + assert_eq!(results.len(), 2); + assert_eq!(results[0].0, b"a".to_vec()); + assert_eq!(results[1].0, b"b".to_vec()); + assert_eq!(results[0].1, 3, "{{b, c, d}} expected for brand a"); + assert_eq!(results[1].1, 3, "{{b, c, d}} expected for brand b"); + } + + #[test] + fn carrier_corrupted_outer_layer_byte_is_rejected() { + // Flip a byte deep inside the carrier-layer merk proof bytes + // (which encode the outer-Keys multi-key proof). Either the + // merk-level execute_proof rejects the bytes, or the chain + // check downstream rejects the resulting hash mismatch. + let v = GroveVersion::latest(); + let (db, _root) = setup_brand_color_carrier_tree(v, &[b"brand_000", b"brand_001"], 100); + let path_query = carrier_count_path_query( + &[b"brand_000", b"brand_001"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + let mut proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + // Flip a byte ~3/4 through the proof — far enough into the + // envelope to land inside the carrier-layer merk_proof bytes. + let target = (proof.len() * 3) / 4; + proof[target] ^= 0x55; + let result = GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v); + assert!( + result.is_err(), + "tampered carrier-layer byte must be rejected, got {:?}", + result.map(|(_, c)| c) + ); + } + + #[test] + fn carrier_undecodable_proof_is_rejected() { + // Send garbage bytes — the bincode decoder rejects the + // envelope up front with `Error::CorruptedData`. + let v = GroveVersion::latest(); + let path_query = carrier_count_path_query( + &[b"brand_000"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + let garbage = vec![0xffu8; 32]; + let err = GroveDb::verify_aggregate_count_query_per_key(&garbage, &path_query, v) + .expect_err("undecodable proof must be rejected"); + match err { + crate::Error::CorruptedData(_) => {} + other => panic!("expected CorruptedData, got {:?}", other), + } + } + + #[test] + fn aggregate_count_proof_with_trailing_bytes_is_rejected() { + // Decoding is canonical — a valid proof with any trailing + // bytes appended must be rejected, even though the + // cryptographic chain check would still bind the same + // `(RootHash, count)` result. Otherwise the same logical + // proof would have many distinct byte encodings, which breaks + // proof-equality / caching assumptions. + let v = GroveVersion::latest(); + let (db, _root) = setup_15_key_provable_count_tree(v); + let path_query = PathQuery::new_aggregate_count_on_range( + vec![TEST_LEAF.to_vec(), b"ct".to_vec()], + QueryItem::RangeInclusive(b"c".to_vec()..=b"l".to_vec()), + ); + let mut proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + // Sanity: the untouched proof verifies. + GroveDb::verify_aggregate_count_query(&proof, &path_query, v) + .expect("clean proof should verify"); + // Now append a single trailing byte and expect rejection from + // both entry points. + proof.push(0u8); + let leaf_err = GroveDb::verify_aggregate_count_query(&proof, &path_query, v) + .expect_err("leaf entry: trailing-byte proof must be rejected"); + match leaf_err { + crate::Error::CorruptedData(msg) => { + assert!(msg.contains("trailing bytes"), "unexpected message: {msg}") + } + other => panic!("expected CorruptedData, got {:?}", other), + } + let per_key_err = GroveDb::verify_aggregate_count_query_per_key(&proof, &path_query, v) + .expect_err("per-key entry: trailing-byte proof must be rejected"); + match per_key_err { + crate::Error::CorruptedData(msg) => { + assert!(msg.contains("trailing bytes"), "unexpected message: {msg}") + } + other => panic!("expected CorruptedData, got {:?}", other), + } + } + + #[test] + fn carrier_legacy_verifier_rejects_carrier_query() { + // The legacy single-`u64` `verify_aggregate_count_query` strictly + // validates the leaf shape and rejects carrier queries — even + // though the proof bytes themselves are well-formed. Callers + // must use `verify_aggregate_count_query_per_key` for carriers. + let v = GroveVersion::latest(); + let (db, _root) = setup_brand_color_carrier_tree(v, &[b"brand_000"], 50); + let path_query = carrier_count_path_query( + &[b"brand_000"], + QueryItem::Range(b"color_00010".to_vec()..b"color_00020".to_vec()), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + let err = GroveDb::verify_aggregate_count_query(&proof, &path_query, v) + .expect_err("legacy leaf verifier must reject carrier shape"); + match err { + crate::Error::InvalidQuery(_) => {} + other => panic!("expected InvalidQuery, got {:?}", other), + } + } + + #[test] + fn carrier_missing_outer_lower_layer_is_rejected() { + // Decode the carrier proof envelope, drop one of the + // `lower_layers[outer_key]` entries, re-encode, and verify the + // verifier rejects with "missing lower layer for outer key". + // Exercises the `lower_layers.get(&outer_key).ok_or_else(...)` + // branch in `verify_v1_carrier_layer`. + use bincode::config; + + use crate::operations::proof::{GroveDBProof, GroveDBProofV1}; + + let v = GroveVersion::latest(); + let (db, _root) = setup_brand_color_carrier_tree(v, &[b"brand_000", b"brand_001"], 100); + let path_query = carrier_count_path_query( + &[b"brand_000", b"brand_001"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + + let cfg = config::standard() + .with_big_endian() + .with_limit::<{ 256 * 1024 * 1024 }>(); + let (mut decoded, _): (GroveDBProof, _) = + bincode::decode_from_slice(&proof, cfg).expect("decode envelope"); + let GroveDBProof::V1(GroveDBProofV1 { root_layer }) = &mut decoded else { + panic!("expected V1 envelope"); + }; + // Walk to the carrier layer: TEST_LEAF -> byBrand. + let carrier_layer = root_layer + .lower_layers + .get_mut(&TEST_LEAF.to_vec()) + .expect("TEST_LEAF layer") + .lower_layers + .get_mut(&b"byBrand".to_vec()) + .expect("byBrand carrier layer"); + // Drop brand_001's lower_layer — its row will still be in the + // multi-key proof but the descent will fail. + let removed = carrier_layer.lower_layers.remove(&b"brand_001".to_vec()); + assert!( + removed.is_some(), + "test setup: expected brand_001 in carrier lower_layers" + ); + let new_proof = bincode::encode_to_vec( + decoded, + config::standard().with_big_endian().with_no_limit(), + ) + .expect("re-encode"); + + let err = GroveDb::verify_aggregate_count_query_per_key(&new_proof, &path_query, v) + .expect_err("missing outer lower_layer must be rejected"); + match err { + crate::Error::InvalidProof(_, msg) => assert!( + msg.contains("missing lower layer for outer key"), + "unexpected message: {msg}" + ), + other => panic!("expected InvalidProof, got {:?}", other), + } + } + + #[test] + fn carrier_missing_subquery_path_layer_is_rejected() { + // Same idea as the previous test but one level deeper: drop the + // `subquery_path` layer ("color") that sits between the outer + // brand match and the leaf merk. Exercises the + // `verify_v1_subquery_path` "missing subquery_path layer" branch. + use bincode::config; + + use crate::operations::proof::{GroveDBProof, GroveDBProofV1}; + + let v = GroveVersion::latest(); + let (db, _root) = setup_brand_color_carrier_tree(v, &[b"brand_000"], 100); + let path_query = carrier_count_path_query( + &[b"brand_000"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + + let cfg = config::standard() + .with_big_endian() + .with_limit::<{ 256 * 1024 * 1024 }>(); + let (mut decoded, _): (GroveDBProof, _) = + bincode::decode_from_slice(&proof, cfg).expect("decode envelope"); + let GroveDBProof::V1(GroveDBProofV1 { root_layer }) = &mut decoded else { + panic!("expected V1 envelope"); + }; + // Walk to brand_000 layer and drop the "color" subquery_path + // descent. + let brand_layer = root_layer + .lower_layers + .get_mut(&TEST_LEAF.to_vec()) + .expect("TEST_LEAF layer") + .lower_layers + .get_mut(&b"byBrand".to_vec()) + .expect("byBrand layer") + .lower_layers + .get_mut(&b"brand_000".to_vec()) + .expect("brand_000 layer"); + let removed = brand_layer.lower_layers.remove(&b"color".to_vec()); + assert!( + removed.is_some(), + "test setup: expected color in brand_000 lower_layers" + ); + let new_proof = bincode::encode_to_vec( + decoded, + config::standard().with_big_endian().with_no_limit(), + ) + .expect("re-encode"); + + let err = GroveDb::verify_aggregate_count_query_per_key(&new_proof, &path_query, v) + .expect_err("missing subquery_path layer must be rejected"); + match err { + crate::Error::InvalidProof(_, msg) => assert!( + msg.contains("missing subquery_path layer"), + "unexpected message: {msg}" + ), + other => panic!("expected InvalidProof, got {:?}", other), + } + } + + #[test] + fn carrier_non_merk_proof_bytes_is_rejected() { + // Replace the subquery_path layer's `ProofBytes::Merk(...)` with + // a `ProofBytes::MMR(...)` variant. The verifier rejects the + // mismatched proof-bytes flavor through `expect_merk_bytes`. + use bincode::config; + + use crate::operations::proof::{GroveDBProof, GroveDBProofV1, ProofBytes}; + + let v = GroveVersion::latest(); + let (db, _root) = setup_brand_color_carrier_tree(v, &[b"brand_000"], 100); + let path_query = carrier_count_path_query( + &[b"brand_000"], + QueryItem::RangeAfter(b"color_00049".to_vec()..), + ); + let proof = db + .grove_db + .prove_query(&path_query, None, v) + .unwrap() + .expect("prove_query should succeed"); + let cfg = config::standard() + .with_big_endian() + .with_limit::<{ 256 * 1024 * 1024 }>(); + let (mut decoded, _): (GroveDBProof, _) = + bincode::decode_from_slice(&proof, cfg).expect("decode envelope"); + let GroveDBProof::V1(GroveDBProofV1 { root_layer }) = &mut decoded else { + panic!("expected V1 envelope"); + }; + // Swap the subquery_path "color" layer's proof bytes from Merk + // to MMR — `verify_v1_subquery_path` will refuse via + // `expect_merk_bytes`. + let color_layer = root_layer + .lower_layers + .get_mut(&TEST_LEAF.to_vec()) + .expect("TEST_LEAF layer") + .lower_layers + .get_mut(&b"byBrand".to_vec()) + .expect("byBrand layer") + .lower_layers + .get_mut(&b"brand_000".to_vec()) + .expect("brand_000 layer") + .lower_layers + .get_mut(&b"color".to_vec()) + .expect("color layer"); + color_layer.merk_proof = ProofBytes::MMR(vec![]); + let new_proof = bincode::encode_to_vec( + decoded, + config::standard().with_big_endian().with_no_limit(), + ) + .expect("re-encode"); + + let err = GroveDb::verify_aggregate_count_query_per_key(&new_proof, &path_query, v) + .expect_err("non-Merk proof bytes must be rejected"); + match err { + crate::Error::InvalidProof(_, msg) => assert!( + msg.contains("unexpected non-merk layer bytes"), + "unexpected message: {msg}" + ), + other => panic!("expected InvalidProof, got {:?}", other), + } + } + + #[test] + fn carrier_pagination_is_rejected_at_entry() { + // Carriers (like leaves) forbid SizedQuery::limit and offset. + // The PathQuery-level validator surfaces this before any proof + // bytes are decoded. + use grovedb_query::Query; + let v = GroveVersion::latest(); + let mut carrier = Query::new(); + carrier.insert_key(b"brand_000".to_vec()); + carrier.set_subquery_path(vec![b"color".to_vec()]); + carrier.set_subquery(Query::new_aggregate_count_on_range(QueryItem::Range( + b"color_00010".to_vec()..b"color_00020".to_vec(), + ))); + let path_query = PathQuery::new( + vec![TEST_LEAF.to_vec(), b"byBrand".to_vec()], + SizedQuery::new(carrier, Some(10), None), + ); + let dummy_proof = vec![0u8; 8]; + let err = GroveDb::verify_aggregate_count_query_per_key(&dummy_proof, &path_query, v) + .expect_err("carrier aggregate-count with limit must be rejected at entry"); + match err { + crate::Error::InvalidQuery(msg) => { + assert!(msg.contains("limit"), "unexpected message: {msg}") + } + other => panic!("expected InvalidQuery, got {:?}", other), + } + } }