drive/query/drive_document_count_query/path_query.rs
1//! Path-query builders for the count query.
2//!
3//! These are the **load-bearing prover/verifier-agreement boundary**:
4//! the bytes these builders produce must match byte-for-byte between
5//! the prover and the verifier, or the merk-root recomputation
6//! fails. Touching anything here without updating both the
7//! server-side prove executor AND the SDK's verifier path-query
8//! reconstruction simultaneously is a bug waiting to happen.
9//!
10//! All three builders are gated `#[cfg(any(feature = "server",
11//! feature = "verify"))]` so the verifier crate (which only enables
12//! `verify`) can reach them via `DriveDocumentCountQuery::*` method
13//! syntax.
14
15#![cfg(any(feature = "server", feature = "verify"))]
16
17use super::super::conditions::{WhereClause, WhereOperator};
18use super::DriveDocumentCountQuery;
19use crate::drive::RootTree;
20use crate::error::query::QuerySyntaxError;
21use crate::error::Error;
22use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
23use dpp::version::PlatformVersion;
24use grovedb::{PathQuery, Query, QueryItem, SizedQuery};
25
26impl DriveDocumentCountQuery<'_> {
27 /// Convert a single range where-clause + value into the grovedb
28 /// `QueryItem` used to walk children of the property-name
29 /// `ProvableCountTree`. The clause's value is serialized via the
30 /// document type's `serialize_value_for_key`, which produces the
31 /// canonical bytes used everywhere else in the index path.
32 ///
33 /// Range mappings:
34 /// - `>` → `RangeAfter(value..)` (exclusive lower)
35 /// - `>=` → `RangeFrom(value..)` (inclusive lower)
36 /// - `<` → `RangeTo(..value)` (exclusive upper)
37 /// - `<=` → `RangeToInclusive(..=value)` (inclusive upper)
38 /// - `between [a, b]` → `RangeInclusive(a..=b)` (inclusive both)
39 /// - `between (a, b)` → `RangeAfterTo(a..b)` (exclusive both — the
40 /// inner range is half-open in grovedb terms; this models
41 /// exclude-bounds)
42 /// - `between (a, b]` → `RangeAfterToInclusive(a..=b)`
43 /// - `between [a, b)` → `Range(a..b)`
44 /// - `startsWith "p"` → `Range(serialize("p")..serialize("p") with
45 /// last byte +1)` — same byte-incremented half-open encoding the
46 /// normal docs path uses (see `conditions.rs:1129`'s `StartsWith`
47 /// arm). `value_shape_ok` constrains the prefix to `Value::Text`,
48 /// and valid UTF-8 never contains `0xFF`, so the `+1` doesn't
49 /// overflow for valid string keys; the unlikely 0xFF-tail case is
50 /// caught via `checked_add` and rejected with a clear error.
51 fn range_clause_to_query_item(
52 &self,
53 clause: &WhereClause,
54 platform_version: &PlatformVersion,
55 ) -> Result<QueryItem, Error> {
56 let serialize = |v: &dpp::platform_value::Value| -> Result<Vec<u8>, Error> {
57 Ok(self.document_type.serialize_value_for_key(
58 clause.field.as_str(),
59 v,
60 platform_version,
61 )?)
62 };
63 // Shared helper for all four `between*` operators. The
64 // operator the caller used (`between`, `betweenExcludeBounds`,
65 // etc.) is not woven into error messages because
66 // `InvalidWhereClauseComponents` takes `&'static str` — a
67 // String-typed error variant would let us do that, but the
68 // existing static-string contract is fine to live with: the
69 // arm name (`WhereOperator::Between` etc.) is visible in
70 // backtraces if a malformed payload reaches this far, and
71 // mode detection has already filtered out non-range operators.
72 let serialize_pair = || -> Result<(Vec<u8>, Vec<u8>), Error> {
73 let arr = clause.value.as_array().ok_or_else(|| {
74 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
75 "range bounds value must be a 2-element array",
76 ))
77 })?;
78 if arr.len() != 2 {
79 return Err(Error::Query(
80 QuerySyntaxError::InvalidWhereClauseComponents(
81 "range bounds value must be a 2-element array",
82 ),
83 ));
84 }
85 let a = serialize(&arr[0])?;
86 let b = serialize(&arr[1])?;
87 if a > b {
88 return Err(Error::Query(
89 QuerySyntaxError::InvalidWhereClauseComponents(
90 "range lower bound must be <= upper bound",
91 ),
92 ));
93 }
94 Ok((a, b))
95 };
96
97 Ok(match clause.operator {
98 WhereOperator::GreaterThan => {
99 let v = serialize(&clause.value)?;
100 QueryItem::RangeAfter(v..)
101 }
102 WhereOperator::GreaterThanOrEquals => {
103 let v = serialize(&clause.value)?;
104 QueryItem::RangeFrom(v..)
105 }
106 WhereOperator::LessThan => {
107 let v = serialize(&clause.value)?;
108 QueryItem::RangeTo(..v)
109 }
110 WhereOperator::LessThanOrEquals => {
111 let v = serialize(&clause.value)?;
112 QueryItem::RangeToInclusive(..=v)
113 }
114 WhereOperator::Between => {
115 let (a, b) = serialize_pair()?;
116 QueryItem::RangeInclusive(a..=b)
117 }
118 WhereOperator::BetweenExcludeBounds => {
119 let (a, b) = serialize_pair()?;
120 QueryItem::RangeAfterTo(a..b)
121 }
122 WhereOperator::BetweenExcludeLeft => {
123 let (a, b) = serialize_pair()?;
124 QueryItem::RangeAfterToInclusive(a..=b)
125 }
126 WhereOperator::BetweenExcludeRight => {
127 let (a, b) = serialize_pair()?;
128 QueryItem::Range(a..b)
129 }
130 WhereOperator::StartsWith => {
131 let left_key = serialize(&clause.value)?;
132 let mut right_key = left_key.clone();
133 // Byte-increment the last byte to form the half-open
134 // upper bound `[prefix, prefix+1)`. Mirrors the
135 // normal-docs encoding in `conditions.rs:1129`'s
136 // `StartsWith` arm; we use `checked_add` so the
137 // pathological `0xFF`-tail input fails loudly instead
138 // of wrapping silently (UTF-8 never contains 0xFF so
139 // valid string keys never hit this).
140 let last = right_key.last_mut().ok_or_else(|| {
141 Error::Query(QuerySyntaxError::InvalidStartsWithClause(
142 "startsWith prefix must have at least one byte",
143 ))
144 })?;
145 *last = last.checked_add(1).ok_or_else(|| {
146 Error::Query(QuerySyntaxError::InvalidStartsWithClause(
147 "startsWith prefix ends in 0xFF; cannot form half-open upper bound",
148 ))
149 })?;
150 QueryItem::Range(left_key..right_key)
151 }
152 _ => {
153 return Err(Error::Query(
154 QuerySyntaxError::InvalidWhereClauseComponents(
155 "range_clause_to_query_item called on a non-range operator",
156 ),
157 ));
158 }
159 })
160 }
161
162 /// Build the grovedb `PathQuery` for an `AggregateCountOnRange`
163 /// query against this count query's `range_countable` index.
164 ///
165 /// Shared between the server-side prove path
166 /// ([`Self::execute_aggregate_count_with_proof`]) and the client-
167 /// side verify path (the SDK's `FromProof<DocumentQuery>` for
168 /// `DocumentCount`, via the shared `verify_aggregate_count`
169 /// helper). Both sides must produce the *exact same* `PathQuery`
170 /// for verification to recompute the same merk root.
171 ///
172 /// Aggregate-count specifically restricts prefix props to `Equal`:
173 /// grovedb's `AggregateCountOnRange` primitive wraps a *single*
174 /// inner range and emits one aggregate `u64` — there's no way for
175 /// it to cartesian-fork over multiple In values at the merk
176 /// layer. For per-distinct-value counts with In on prefix, use
177 /// [`Self::distinct_count_path_query`] instead.
178 ///
179 /// Errors:
180 /// - No range where-clause / multiple range where-clauses →
181 /// `InvalidWhereClauseComponents`
182 /// - `In` on a prefix property → `InvalidWhereClauseComponents`
183 /// (aggregate primitive can't fork)
184 /// - Missing prefix clause → `InvalidWhereClauseComponents`
185 pub fn aggregate_count_path_query(
186 &self,
187 platform_version: &PlatformVersion,
188 ) -> Result<PathQuery, Error> {
189 let range_clause = self
190 .where_clauses
191 .iter()
192 .find(|wc| Self::is_range_operator(wc.operator))
193 .ok_or(Error::Query(
194 QuerySyntaxError::InvalidWhereClauseComponents(
195 "aggregate_count_path_query requires a range where-clause",
196 ),
197 ))?;
198 let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
199
200 let mut path = vec![
201 vec![RootTree::DataContractDocuments as u8],
202 self.contract_id.to_vec(),
203 vec![1u8],
204 self.document_type_name.as_bytes().to_vec(),
205 ];
206 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
207 for prop in prefix_props {
208 let clause = self
209 .where_clauses
210 .iter()
211 .find(|wc| wc.field == prop.name)
212 .ok_or(Error::Query(
213 QuerySyntaxError::InvalidWhereClauseComponents(
214 "aggregate-count proof: missing where clause for an index prefix property",
215 ),
216 ))?;
217 if clause.operator != WhereOperator::Equal {
218 return Err(Error::Query(
219 QuerySyntaxError::InvalidWhereClauseComponents(
220 "aggregate-count proof: prefix properties must use `==` (no `in`); \
221 use a two-field `group_by = [in_field, range_field]` for compound \
222 In-on-prefix queries",
223 ),
224 ));
225 }
226 path.push(self.index.level_key_for_property(&prop.name).into_bytes());
227 path.push(self.document_type.serialize_value_for_key(
228 prop.name.as_str(),
229 &clause.value,
230 platform_version,
231 )?);
232 }
233 let range_prop_name = &self
234 .index
235 .properties
236 .last()
237 .ok_or(Error::Query(
238 QuerySyntaxError::InvalidWhereClauseComponents(
239 "range_countable index must have at least one property",
240 ),
241 ))?
242 .name;
243 path.push(
244 self.index
245 .level_key_for_property(range_prop_name)
246 .into_bytes(),
247 );
248
249 Ok(PathQuery::new_aggregate_count_on_range(path, query_item))
250 }
251
252 /// Build the grovedb `PathQuery` for a **carrier**
253 /// `AggregateCountOnRange` proof — one outer Key per `In`
254 /// value, each terminating in an ACOR boundary walk over the
255 /// per-branch range subtree. Returns one `(in_key, u64)` pair
256 /// per resolved In branch via
257 /// [`grovedb::GroveDb::query_aggregate_count_per_key`] (no-
258 /// proof) and
259 /// [`grovedb::GroveDb::verify_aggregate_count_query_per_key`]
260 /// (verify).
261 ///
262 /// Required where-clause shape (validated upstream by
263 /// [`Self::detect_mode`] routing to
264 /// [`DocumentCountMode::RangeAggregateCarrierProof`]):
265 /// - Exactly one `In` clause on the In-property
266 /// - Exactly one range clause on the *terminator* property of
267 /// a `range_countable: true` index whose first property is
268 /// the In-property
269 /// - Any prefix properties between In and range must use
270 /// `==` (mirror of [`Self::aggregate_count_path_query`]'s
271 /// non-In prefix rule)
272 ///
273 /// Path-query structure:
274 /// - Outer path stops one level above the In-bearing property
275 /// subtree's children (`@/doc_prefix/0x01/doctype/<In-prop>`).
276 /// - Outer Query: `Key(in_value_0)`, `Key(in_value_1)`, … in
277 /// lex-asc serialized order (grovedb's multi-key walker
278 /// invariant).
279 /// - `subquery_path`: the terminator property name (and any
280 /// trailing `==` clause names between In and range, in
281 /// index order).
282 /// - `subquery`: `Query::new_aggregate_count_on_range(range_item)`.
283 ///
284 /// Enabled by [grovedb PR #663](https://github.com/dashpay/grovedb/pull/663).
285 /// Before that PR, `AggregateCountOnRange` was required to be
286 /// the only item in its query and could not appear under a
287 /// `subquery` field — the dispatcher rejected this shape with
288 /// "range count queries with an `in` clause are not supported on
289 /// the aggregate prove path".
290 ///
291 /// Errors:
292 /// - No range where-clause / multiple range where-clauses →
293 /// `InvalidWhereClauseComponents`
294 /// - No In where-clause → `InvalidWhereClauseComponents`
295 /// - In on a non-prefix property → `InvalidWhereClauseComponents`
296 /// - Prefix property between In and range uses non-Equal →
297 /// `InvalidWhereClauseComponents`
298 pub fn carrier_aggregate_count_path_query(
299 &self,
300 limit: Option<u16>,
301 left_to_right: bool,
302 platform_version: &PlatformVersion,
303 ) -> Result<PathQuery, Error> {
304 // The terminator property (last in the index) carries the
305 // ACOR target range. The "carrier" property — the one whose
306 // clause becomes the outer Query items — is either:
307 // - An `In` clause (G7 shape: one Key per In value)
308 // - A range clause on a prefix prop (G8 shape: one QueryItem
309 // bounding the outer range, with `SizedQuery::limit` capping
310 // how many outer matches the carrier walks — see
311 // [grovedb PR #664](https://github.com/dashpay/grovedb/pull/664))
312 //
313 // The terminator's clause must be a range and is converted to
314 // the inner ACOR `QueryItem`. Any properties between the
315 // carrier and the terminator must use `==` and extend the
316 // subquery_path.
317 let terminator_prop_name = &self
318 .index
319 .properties
320 .last()
321 .ok_or(Error::Query(
322 QuerySyntaxError::InvalidWhereClauseComponents(
323 "range_countable index must have at least one property",
324 ),
325 ))?
326 .name;
327 let terminator_clause = self
328 .where_clauses
329 .iter()
330 .find(|wc| wc.field == *terminator_prop_name && Self::is_range_operator(wc.operator))
331 .ok_or(Error::Query(
332 QuerySyntaxError::InvalidWhereClauseComponents(
333 "carrier_aggregate_count_path_query requires a range where-clause on the \
334 terminator property of the chosen index",
335 ),
336 ))?;
337 let inner_range_item =
338 self.range_clause_to_query_item(terminator_clause, platform_version)?;
339
340 let mut base_path: Vec<Vec<u8>> = vec![
341 vec![RootTree::DataContractDocuments as u8],
342 self.contract_id.to_vec(),
343 vec![1u8],
344 self.document_type_name.as_bytes().to_vec(),
345 ];
346 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
347
348 // Carrier clause state: either `None` (not seen yet, still on
349 // the `==`-prefix run), `Some(In)` (G7), or `Some(Range)` (G8).
350 enum Carrier {
351 Pending,
352 In(WhereClause),
353 Range(WhereClause),
354 }
355 let mut carrier = Carrier::Pending;
356 let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
357
358 for prop in prefix_and_carrier_props {
359 let clause = self
360 .where_clauses
361 .iter()
362 .find(|wc| wc.field == prop.name)
363 .ok_or(
364 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
365 "carrier-aggregate proof: missing where clause for an index prefix property",
366 )),
367 )?;
368 match (&carrier, clause.operator) {
369 (Carrier::Pending, WhereOperator::Equal) => {
370 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
371 base_path.push(self.document_type.serialize_value_for_key(
372 prop.name.as_str(),
373 &clause.value,
374 platform_version,
375 )?);
376 }
377 (Carrier::Pending, WhereOperator::In) => {
378 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
379 carrier = Carrier::In(clause.clone());
380 }
381 (Carrier::Pending, op) if Self::is_range_operator(op) => {
382 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
383 carrier = Carrier::Range(clause.clone());
384 }
385 (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
386 subquery_path_extension
387 .push(self.index.level_key_for_property(&prop.name).into_bytes());
388 subquery_path_extension.push(self.document_type.serialize_value_for_key(
389 prop.name.as_str(),
390 &clause.value,
391 platform_version,
392 )?);
393 }
394 (Carrier::In(_) | Carrier::Range(_), _) => {
395 return Err(Error::Query(
396 QuerySyntaxError::InvalidWhereClauseComponents(
397 "carrier-aggregate proof: at most one carrier clause (In or range) \
398 is supported on prefix properties; subsequent prefix clauses must \
399 use `==`",
400 ),
401 ));
402 }
403 _ => {
404 return Err(Error::Query(
405 QuerySyntaxError::InvalidWhereClauseComponents(
406 "carrier-aggregate proof: prefix property operator unsupported",
407 ),
408 ));
409 }
410 }
411 }
412 subquery_path_extension.push(
413 self.index
414 .level_key_for_property(terminator_prop_name)
415 .into_bytes(),
416 );
417
418 let mut outer_query = Query::new_with_direction(left_to_right);
419 match carrier {
420 Carrier::Pending => {
421 return Err(Error::Query(
422 QuerySyntaxError::InvalidWhereClauseComponents(
423 "carrier-aggregate proof: an In or range clause must appear on a prefix \
424 property of the chosen index to act as the carrier dimension",
425 ),
426 ));
427 }
428 Carrier::In(in_clause) => {
429 // Build one Key per In value, sorted lex-ascending
430 // (grovedb's multi-key walker invariant per PR #663).
431 let in_values = in_clause.in_values().into_data_with_error()??;
432 let mut serialized_in_keys: Vec<Vec<u8>> = in_values
433 .iter()
434 .map(|v| {
435 self.document_type.serialize_value_for_key(
436 in_clause.field.as_str(),
437 v,
438 platform_version,
439 )
440 })
441 .collect::<Result<_, _>>()?;
442 serialized_in_keys.sort();
443 serialized_in_keys.dedup();
444 for key in serialized_in_keys {
445 outer_query.insert_key(key);
446 }
447 }
448 Carrier::Range(range_clause) => {
449 // Single QueryItem bounding the outer range. The
450 // carrier walks this range and emits one `(key, u64)`
451 // pair per matched outer key.
452 let outer_range_item =
453 self.range_clause_to_query_item(&range_clause, platform_version)?;
454 outer_query.items.push(outer_range_item);
455 }
456 }
457 outer_query.set_subquery_path(subquery_path_extension);
458 outer_query.set_subquery(Query::new_aggregate_count_on_range(inner_range_item));
459
460 // `SizedQuery::limit` is permitted on carriers as of grovedb
461 // PR #664; for In-outer carriers the |IN| array already
462 // bounds the result so `limit` is typically `None`, but for
463 // Range-outer carriers `limit` caps the outer walk and is
464 // load-bearing for proof bytes.
465 Ok(PathQuery::new(
466 base_path,
467 SizedQuery::new(outer_query, limit, None),
468 ))
469 }
470
471 /// Build the grovedb `PathQuery` for a *regular* range query
472 /// against this count query's `range_countable` index — the
473 /// distinct-counts variant. Used by:
474 /// - the server's prove-distinct executor
475 /// ([`Self::execute_distinct_count_with_proof`])
476 /// - the server's no-proof range executor
477 /// ([`Self::execute_range_count_no_proof`])
478 /// - the SDK's per-key-count verifier
479 /// ([`drive_proof_verifier::verify_distinct_count_proof`])
480 ///
481 /// **In-on-prefix support via grovedb subqueries.** Where
482 /// [`Self::aggregate_count_path_query`] rejects In on prefix
483 /// (the aggregate merk primitive can't cartesian-fork), this
484 /// builder uses grovedb's native subquery primitive:
485 ///
486 /// - **Flat shape** (no In on prefix, only Equal): path includes
487 /// the range terminator; outer Query has the range item.
488 /// - **Compound shape** (one In on prefix): path stops at the
489 /// In-bearing prop's property-name subtree; outer Query has
490 /// one `Key(value)` item per In value; `set_subquery_path`
491 /// carries any post-In Equal-clause `(name, value)` pairs plus
492 /// the terminator name; `set_subquery` is the range item.
493 ///
494 /// Both shapes return `(path, branched-or-flat Query)` and feed
495 /// the same `grove_get_raw_path_query` / `get_proved_path_query`
496 /// pipelines downstream. The compound shape replaces the
497 /// pre-existing cartesian-fork loop in
498 /// `execute_range_count_no_proof`.
499 ///
500 /// `limit` IS load-bearing for prove-path verification: the
501 /// prover bounds the proof at `limit` matched keys, and the
502 /// verifier must build the exact same `PathQuery` (including
503 /// this cap) for the merk-root recomputation to match. The
504 /// dispatcher pre-validates `limit ≤ max_query_limit` on the
505 /// prove path, so unbounded queries can't reach this builder
506 /// with `Some(...)` greater than the cap. The no-proof path
507 /// passes `None` (full walk) so cross-In-fork merging sees
508 /// every emitted element before the result-set-level limit is
509 /// applied in post-processing.
510 ///
511 /// `left_to_right` controls grovedb's iteration direction:
512 /// `true` (the default, used for ascending `order_by_ascending`)
513 /// walks the range from low key to high key; `false` reverses.
514 /// On the prove path this is load-bearing: the path query's
515 /// `Query.left_to_right` is part of the serialized PathQuery
516 /// bytes, so the prover and verifier must agree on the value or
517 /// the merk-root recomputation fails. For compound queries the
518 /// flag is applied to BOTH the outer In-keys Query and the
519 /// inner range subquery, so descending iteration walks
520 /// `(in_key_desc, key_desc)` tuples (matching what
521 /// `RangeCountOptions::order_by_ascending = false` callers
522 /// expect).
523 ///
524 /// Errors:
525 /// - No range where-clause / multiple range where-clauses
526 /// - Multiple In clauses on prefix props
527 /// - Non-Equal-non-In operator on a prefix prop
528 /// - Missing prefix clause
529 pub fn distinct_count_path_query(
530 &self,
531 limit: Option<u16>,
532 left_to_right: bool,
533 platform_version: &PlatformVersion,
534 ) -> Result<PathQuery, Error> {
535 let range_clause = self
536 .where_clauses
537 .iter()
538 .find(|wc| Self::is_range_operator(wc.operator))
539 .ok_or(Error::Query(
540 QuerySyntaxError::InvalidWhereClauseComponents(
541 "distinct_count_path_query requires a range where-clause",
542 ),
543 ))?;
544 let range_item = self.range_clause_to_query_item(range_clause, platform_version)?;
545
546 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
547 let terminator_name = &self
548 .index
549 .properties
550 .last()
551 .ok_or(Error::Query(
552 QuerySyntaxError::InvalidWhereClauseComponents(
553 "range_countable index must have at least one property",
554 ),
555 ))?
556 .name;
557
558 let mut base_path: Vec<Vec<u8>> = vec![
559 vec![RootTree::DataContractDocuments as u8],
560 self.contract_id.to_vec(),
561 vec![1u8],
562 self.document_type_name.as_bytes().to_vec(),
563 ];
564
565 // `Some(keys)` once an In clause has been encountered on a
566 // prefix property. From that point on, subsequent Equal
567 // clauses go into `subquery_path_extension` rather than
568 // `base_path`. Only one In allowed (multiple Ins would
569 // multiply the fork count beyond what a single Query can
570 // express via `set_subquery_path`).
571 let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
572 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
573
574 for prop in prefix_props {
575 let clause = self
576 .where_clauses
577 .iter()
578 .find(|wc| wc.field == prop.name)
579 .ok_or(Error::Query(
580 QuerySyntaxError::InvalidWhereClauseComponents(
581 "distinct_count_path_query: missing where clause for an index \
582 prefix property",
583 ),
584 ))?;
585
586 match clause.operator {
587 WhereOperator::Equal => {
588 let serialized = self.document_type.serialize_value_for_key(
589 prop.name.as_str(),
590 &clause.value,
591 platform_version,
592 )?;
593 if in_outer_keys.is_some() {
594 subquery_path_extension
595 .push(self.index.level_key_for_property(&prop.name).into_bytes());
596 subquery_path_extension.push(serialized);
597 } else {
598 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
599 base_path.push(serialized);
600 }
601 }
602 WhereOperator::In => {
603 if in_outer_keys.is_some() {
604 return Err(Error::Query(
605 QuerySyntaxError::InvalidWhereClauseComponents(
606 "distinct_count_path_query: at most one `In` clause is supported \
607 on prefix properties",
608 ),
609 ));
610 }
611 // Path stops at the In-bearing prop's property-
612 // name subtree; outer Query lives at that level.
613 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
614 let in_values = clause.in_values().into_data_with_error()??;
615 let mut keys: Vec<Vec<u8>> = in_values
616 .iter()
617 .map(|v| {
618 self.document_type.serialize_value_for_key(
619 prop.name.as_str(),
620 v,
621 platform_version,
622 )
623 })
624 .collect::<Result<_, _>>()?;
625 // Sort the serialized In keys lex-ascending before
626 // building the outer Query. This is load-bearing
627 // for both correctness and DoS-resistance:
628 // - **Order parity**: grovedb iterates `Key` items
629 // in insert order. Without sorting, the emitted
630 // `(in_key, key)` tuples come out in user-input
631 // order on the prefix dimension, which diverges
632 // from the documented lex-asc order contract on
633 // the no-proof distinct path (which sorts post-
634 // walk) and forces a per-side sort step.
635 // - **`left_to_right`-driven descent**: with sorted
636 // keys, `left_to_right = false` walks the outer
637 // In dimension lex-descending — what the caller
638 // asked for. Without the sort, descending
639 // `left_to_right` just reverses user-input
640 // order, which is gibberish.
641 // - **Pushed-limit safety**: callers that push the
642 // path-query limit (no-proof distinct mode) get
643 // the bottom-N or top-N entries by lex order,
644 // which is the documented limit-on-distinct
645 // semantics. With unsorted keys, the path-query
646 // limit would give the first-N entries in user-
647 // input order — useless for distinct pagination.
648 //
649 // Both the prover and the verifier go through this
650 // builder, so the byte-equality contract still
651 // holds — the sort happens identically on both
652 // sides.
653 keys.sort();
654 in_outer_keys = Some(keys);
655 }
656 _ => {
657 return Err(Error::Query(
658 QuerySyntaxError::InvalidWhereClauseComponents(
659 "distinct_count_path_query: prefix properties must use `==` or `in`",
660 ),
661 ));
662 }
663 }
664 }
665
666 match in_outer_keys {
667 None => {
668 // Flat shape — path includes terminator, single
669 // range-only Query.
670 base_path.push(terminator_name.as_bytes().to_vec());
671 let mut query = Query::new_with_direction(left_to_right);
672 query.insert_item(range_item);
673 Ok(PathQuery::new(
674 base_path,
675 SizedQuery::new(query, limit, None),
676 ))
677 }
678 Some(keys) => {
679 // Compound shape — outer Query has one Key per In
680 // value at the In-bearing prop's property-name
681 // subtree. `subquery_path` carries any post-In Equal
682 // pairs + terminator. Subquery is the range item.
683 //
684 // `left_to_right` applies to BOTH the outer Query
685 // and the subquery so descending iteration walks
686 // `(in_key_desc, key_desc)` tuples — otherwise we'd
687 // get e.g. In keys ascending but per-fork terminator
688 // values descending, which is a weird order no
689 // user would expect.
690 let mut outer_query = Query::new_with_direction(left_to_right);
691 for key in keys {
692 outer_query.insert_key(key);
693 }
694 subquery_path_extension.push(terminator_name.as_bytes().to_vec());
695
696 let mut subquery = Query::new_with_direction(left_to_right);
697 subquery.insert_item(range_item);
698
699 outer_query.set_subquery_path(subquery_path_extension);
700 outer_query.set_subquery(subquery);
701
702 Ok(PathQuery::new(
703 base_path,
704 SizedQuery::new(outer_query, limit, None),
705 ))
706 }
707 }
708 }
709
710 /// Build the grovedb `PathQuery` for a point-lookup count proof
711 /// against a `countable: true` index. Returns one element per
712 /// covered branch whose `count_value` is the per-branch document
713 /// count.
714 ///
715 /// Shared between the server-side prove path
716 /// ([`Self::execute_point_lookup_count_with_proof`]) and the
717 /// client-side verify path
718 /// ([`Self::verify_point_lookup_count_proof`]). Both sides must
719 /// produce the *exact same* `PathQuery` for the merk-root
720 /// recomputation to match.
721 ///
722 /// ## Two terminator shapes depending on `range_countable`
723 ///
724 /// The proof's terminal element is at one of two layers, picked
725 /// from [`Index::range_countable`]:
726 ///
727 /// - **Normal `countable: true`** (NOT `range_countable`): the
728 /// terminator's value tree is a `NormalTree`, and the doc-count
729 /// `CountTree` sits inside it at the conventional `[0]` child.
730 /// Proof targets `[..., last_field, last_value, 0]`.
731 /// - **`range_countable: true`**: the terminator's value tree is
732 /// itself a `CountTree` (continuation property-name subtrees
733 /// sit beneath as `Element::NonCounted` so they don't pollute
734 /// the parent count — see `add_indices_for_index_level_for_contract_operations_v0`).
735 /// The value tree's own `count_value_or_default()` already IS
736 /// the per-branch doc count, so the proof targets the value
737 /// tree directly at `[..., last_field, last_value]` and saves
738 /// one merk-path layer per covered branch.
739 ///
740 /// Concretely the optimization replaces a trailing `Key([0])`
741 /// with `Key(last_value)` against `[..., last_field]` (Equal-
742 /// only, no In) — or against the In-bearing prop's property-name
743 /// subtree (In on terminator) — or replaces the trailing pair in
744 /// `set_subquery_path` (In on prefix + trailing Equals that reach
745 /// the terminator). The query shape stays in the same Query/
746 /// subquery topology so byte-equality across prover and verifier
747 /// is preserved by construction.
748 ///
749 /// ## Shape support
750 ///
751 /// The builder requires the where clauses to **fully cover** the
752 /// index — every property in `self.index.properties` must have a
753 /// matching `Equal` or `In` clause. Partial-coverage shapes
754 /// (where some index properties have no matching clause) require
755 /// a recursive subquery enumeration that this builder does not
756 /// implement (and that the strict picker already rejects upstream).
757 ///
758 /// **`In` may appear at any position in the index.** Equal
759 /// clauses before the In contribute to `base_path`; Equal clauses
760 /// after the In feed `set_subquery_path` on the outer Query so the
761 /// descent under each matched In value lands at the right
762 /// CountTree leaf. At most one `In` clause per query (multiple
763 /// would cartesian-fork beyond what a single `set_subquery`
764 /// expresses).
765 ///
766 /// This is **more permissive than the regular document query
767 /// path's `Index::matches` rule** (`packages/rs-dpp/src/
768 /// data_contract/document_type/index/mod.rs:503`), which restricts
769 /// `In` to the last or before-last index property because its
770 /// path-construction code positionally zips intermediate index
771 /// names with Equal-clause values (see
772 /// `DriveDocumentQuery::get_non_primary_key_path_query`). The
773 /// count path doesn't have that constraint: it's a pure CountTree
774 /// element lookup with no document-key terminator descent, no
775 /// `order_by` interpretation, and no `limit/offset` semantics, so
776 /// `set_subquery_path` with an arbitrary trailing tail just
777 /// works. Both no-proof ([`Self::execute_no_proof`]) and prove
778 /// ([`Self::execute_point_lookup_count_with_proof`]) executors
779 /// route through this single builder, so they accept the same
780 /// query shapes by construction.
781 ///
782 /// Output shapes (`countable` / `range_countable` differ only in
783 /// whether the trailing `Key([0])` is replaced by `Key(last_value)`):
784 /// - **Equal-only, fully covered**:
785 /// - `countable`: path `[..., last_field, last_value]`, single `Key([0])`.
786 /// - `range_countable`: path `[..., last_field]`, single
787 /// `Key(last_value)`.
788 /// - **Equal prefix + `In` (any position) [+ trailing Equals]**:
789 /// compound query with `base_path` ending at the In-bearing
790 /// property's property-name subtree (Equal clauses before the
791 /// In are baked into `base_path`); outer Query has one `Key`
792 /// per In value (sorted lex-asc for prove/no-proof parity and
793 /// pushed-limit safety — same convention as
794 /// [`Self::distinct_count_path_query`]).
795 /// - **In on terminator**:
796 /// - `countable`: subquery `Key([0])` under each In value's
797 /// value tree (`set_subquery_path` unset).
798 /// - `range_countable`: outer `Key`s already point at the
799 /// CountTree value trees themselves; no subquery is set.
800 /// - **In on a prefix + trailing Equals reaching the
801 /// terminator**: `set_subquery_path` carries the post-In
802 /// Equal `(name, value)` pairs in index order:
803 /// - `countable`: full pairs, subquery `Key([0])`.
804 /// - `range_countable`: last pair's `value` is hoisted out as
805 /// the subquery's single `Key(value)`; `set_subquery_path`
806 /// ends at the terminator's property-name segment.
807 ///
808 /// ## Errors
809 ///
810 /// Rejects shapes the builder doesn't support:
811 /// - Partial coverage (uncovered index property)
812 /// - More than one `In` clause
813 /// - Any non-`Equal` / non-`In` operator (defense-in-depth; mode
814 /// detection already filters these out)
815 ///
816 /// [`Index::range_countable`]: dpp::data_contract::document_type::index::Index::range_countable
817 pub fn point_lookup_count_path_query(
818 &self,
819 platform_version: &PlatformVersion,
820 ) -> Result<PathQuery, Error> {
821 if self.index.properties.is_empty() {
822 return Err(Error::Query(
823 QuerySyntaxError::InvalidWhereClauseComponents(
824 "point_lookup_count_path_query: index must have at least one property",
825 ),
826 ));
827 }
828
829 let mut base_path: Vec<Vec<u8>> = vec![
830 vec![RootTree::DataContractDocuments as u8],
831 self.contract_id.to_vec(),
832 vec![1u8],
833 self.document_type_name.as_bytes().to_vec(),
834 ];
835
836 // `in_outer_keys` is populated when we encounter the (single)
837 // `In` clause. Equal clauses *before* the In contribute to
838 // `base_path`; Equal clauses *after* the In feed
839 // `subquery_path_extension`, which becomes the outer Query's
840 // `set_subquery_path` — i.e., the descent under each matched
841 // In value walks `[trailing_field_1, trailing_value_1, ...,
842 // trailing_field_n, trailing_value_n]` before the
843 // selector subquery (either `Key([0])` for normal countable
844 // or a `Key(terminator_value)` lift for range_countable —
845 // see the post-loop selector decision below) picks off the
846 // count-bearing element.
847 //
848 // No position restriction on the In clause: any index
849 // position works because the count path doesn't have the
850 // positional path-construction assumption the regular
851 // document query path makes (see this method's docstring for
852 // the divergence rationale).
853 let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
854 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
855
856 for (position, prop) in self.index.properties.iter().enumerate() {
857 // The path segment is the level key — grid-qualified for a
858 // time-range index's first property — while the clause lookup
859 // and value serialization stay on the bare property name.
860 let level_key = self.index.level_key(position, &prop.name);
861 let clause = self
862 .where_clauses
863 .iter()
864 .find(|wc| wc.field == prop.name)
865 .ok_or_else(|| {
866 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
867 "prove count requires the where clauses to fully cover the \
868 countable index; one or more index properties have no \
869 matching `==` or `in` clause — use a more specific index \
870 (define a `countable: true` index whose properties exactly \
871 match the clauses) or use `prove=false`",
872 ))
873 })?;
874
875 match clause.operator {
876 WhereOperator::Equal => {
877 let serialized = self.document_type.serialize_value_for_key(
878 prop.name.as_str(),
879 &clause.value,
880 platform_version,
881 )?;
882 if in_outer_keys.is_some() {
883 // Trailing Equal after the (already-seen) In:
884 // descend through it as part of the subquery
885 // path. Any number of these may accumulate —
886 // one for each Equal that sits *after* the In
887 // in the index ordering.
888 subquery_path_extension.push(level_key.as_bytes().to_vec());
889 subquery_path_extension.push(serialized);
890 } else {
891 base_path.push(level_key.as_bytes().to_vec());
892 base_path.push(serialized);
893 }
894 }
895 WhereOperator::In => {
896 if in_outer_keys.is_some() {
897 return Err(Error::Query(
898 QuerySyntaxError::InvalidWhereClauseComponents(
899 "prove count: at most one `in` clause is supported on \
900 the covering countable index",
901 ),
902 ));
903 }
904 // Stops `base_path` at the In-bearing property's
905 // property-name subtree; outer Query lives at
906 // that level. Any trailing Equal property then
907 // routes through `subquery_path_extension`.
908 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
909 let in_values = clause.in_values().into_data_with_error()??;
910 let mut keys: Vec<Vec<u8>> = in_values
911 .iter()
912 .map(|v| {
913 self.document_type.serialize_value_for_key(
914 prop.name.as_str(),
915 v,
916 platform_version,
917 )
918 })
919 .collect::<Result<_, _>>()?;
920 // Sort lex-asc for prove/no-proof entry-order
921 // parity and so the pushed-limit (if any) gives
922 // the documented "first N by lex" semantics.
923 // Same convention as `distinct_count_path_query`.
924 keys.sort();
925 in_outer_keys = Some(keys);
926 }
927 _ => {
928 return Err(Error::Query(
929 QuerySyntaxError::InvalidWhereClauseComponents(
930 "point_lookup_count_path_query: index properties must use \
931 `==` or `in`",
932 ),
933 ));
934 }
935 }
936 }
937
938 // Whether the terminator's value tree is itself a `CountTree`
939 // (carries the per-branch doc count directly) vs. a
940 // `NormalTree` whose `[0]` child is the `CountTree`. Drives
941 // the selector-element decision below.
942 //
943 // The insertion side
944 // (`add_indices_for_index_level_for_contract_operations_v0`)
945 // makes the terminator value tree a `CountTree` for **any**
946 // countable index — not just `range_countable: true`. Both
947 // tiers (`Countable` and `CountableAllowingOffset`) layout
948 // the value tree the same way: a `CountTree` whose count
949 // equals the `[0]` ref-bucket's doc count (continuations
950 // wrapped `NonCounted` so they don't pollute the parent).
951 // `range_countable` only additionally upgrades the
952 // property-name tree to `ProvableCountTree` for
953 // `AggregateCountOnRange` queries — that's orthogonal to the
954 // point-lookup proof shape.
955 //
956 // So gate the optimization on `countable.is_countable()`:
957 // every countable index uses the compact shape. The picker
958 // upstream already requires the index to be countable to be
959 // selected (`find_countable_index_for_where_clauses` / the
960 // range_countable picker for range shapes), so reaching this
961 // builder with a non-countable index would be a bug — but
962 // we keep the gate explicit for clarity.
963 //
964 // The loop above already enforces full coverage of every
965 // index property, so the terminator is always proven; this
966 // flag is the only differentiator between the two output
967 // shapes.
968 let count_tree_terminator = self.index.countable.is_countable();
969
970 // CountTree storage convention for non-countable indexes
971 // (defensive — picker upstream filters these out): the count
972 // lives at the `[0]` child of the value
973 // tree. See the book's "Count Trees and Provable Counts"
974 // chapter for the layout.
975 const COUNT_TREE_KEY: u8 = 0;
976
977 match in_outer_keys {
978 None => {
979 // Equal-only, fully covered.
980 //
981 // - normal countable: `base_path` ends at
982 // `[..., last_field, last_value]`; query asks for
983 // the single key `[0]` (the CountTree under the
984 // value tree).
985 // - `range_countable`: peel the trailing `last_value`
986 // off `base_path` and use it as the query's Key.
987 // The resolved element is the value tree itself
988 // (a CountTree), and its `count_value_or_default()`
989 // is the per-branch count — one merk layer shorter
990 // per resolved branch than the `[0]` shape.
991 let mut query = Query::new();
992 if count_tree_terminator {
993 // The Equal loop always pushes (name, value) per
994 // prop, so `base_path` has at least the trailing
995 // serialized `last_value` to lift. The expect()
996 // here would fire only if the loop above changed
997 // its push contract — a load-bearing invariant
998 // checked by every test in this module that
999 // routes through this builder.
1000 let last_value = base_path.pop().expect(
1001 "Equal-only loop pushes (name, value) per prop; \
1002 base_path must hold the terminator's serialized value",
1003 );
1004 query.insert_key(last_value);
1005 } else {
1006 query.insert_key(vec![COUNT_TREE_KEY]);
1007 }
1008 Ok(PathQuery::new(
1009 base_path,
1010 SizedQuery::new(query, None, None),
1011 ))
1012 }
1013 Some(keys) => {
1014 // Compound shape. `base_path` ends at the In-bearing
1015 // property's property-name subtree; the outer Query
1016 // enumerates serialized In values; the subquery
1017 // (when present) descends from each matched In value
1018 // to the count-bearing element.
1019 //
1020 // `subquery_path_extension` carries 0..N segments,
1021 // one `(prop_name, serialized_value)` pair per Equal
1022 // clause that sits *after* the In in the index
1023 // ordering. The exact subquery topology depends on
1024 // both whether trailing Equals exist AND whether the
1025 // terminator is range_countable; see the inline
1026 // branches below.
1027 let mut outer_query = Query::new();
1028 for key in keys {
1029 outer_query.insert_key(key);
1030 }
1031
1032 if subquery_path_extension.is_empty() {
1033 // **In on the terminator** (no trailing Equals).
1034 if count_tree_terminator {
1035 // Outer `Key`s already point at the terminator
1036 // value trees, which are themselves CountTrees.
1037 // No subquery is needed — grovedb returns one
1038 // element per matched outer Key.
1039 } else {
1040 // Normal countable: descend one more layer
1041 // under each matched In value's NormalTree
1042 // value tree to grab the `Key([0])` CountTree
1043 // child.
1044 let mut subquery = Query::new();
1045 subquery.insert_key(vec![COUNT_TREE_KEY]);
1046 outer_query.set_subquery(subquery);
1047 }
1048 } else {
1049 // **In on a prefix + trailing Equals** that
1050 // collectively reach the terminator.
1051 let mut subquery = Query::new();
1052 if count_tree_terminator {
1053 // The terminator's serialized value is the
1054 // last element pushed into
1055 // `subquery_path_extension` (the trailing-
1056 // Equal loop pushes `[name, value, ...,
1057 // termname, termval]`). Lift `termval` out
1058 // as the subquery's Key so the descent stops
1059 // at the terminator's property-name subtree
1060 // and the subquery resolves the CountTree
1061 // value tree directly. `subquery_path_extension`
1062 // is left at an odd length on purpose — it
1063 // ends with the terminator's `name` segment,
1064 // exactly where the subquery's `Key(termval)`
1065 // picks up.
1066 let termval = subquery_path_extension.pop().expect(
1067 "trailing-Equal loop pushes (name, value) pairs; \
1068 non-empty extension's tail must be the terminator's \
1069 serialized value",
1070 );
1071 subquery.insert_key(termval);
1072 } else {
1073 // Normal countable: subquery descends to the
1074 // `Key([0])` CountTree at the resolved leaf,
1075 // with the full `(name, value)` pairs of the
1076 // trailing Equals consumed by
1077 // `set_subquery_path`.
1078 subquery.insert_key(vec![COUNT_TREE_KEY]);
1079 }
1080 outer_query.set_subquery_path(subquery_path_extension);
1081 outer_query.set_subquery(subquery);
1082 }
1083
1084 // `SizedQuery::new(_, None, None)` is intentional —
1085 // PointLookupProof always returns ALL In branches.
1086 // The handler rejects `limit` upstream on this path
1087 // (see [`CountMode::accepts_limit`]'s `GroupByIn`
1088 // arm) because the In array is already capped at 100
1089 // by `WhereClause::in_values()`, and a partial-In
1090 // selection isn't representable in this `SizedQuery`
1091 // shape without rebuilding the verifier to know
1092 // which subset got truncated.
1093 Ok(PathQuery::new(
1094 base_path,
1095 SizedQuery::new(outer_query, None, None),
1096 ))
1097 }
1098 }
1099 }
1100
1101 /// Build the grovedb `PathQuery` for proving the document type's
1102 /// primary-key `CountTree` element at `[contract_doc, contract_id,
1103 /// 1, doctype, 0]`. Used for unfiltered total counts when the
1104 /// document type has `documents_countable: true` — the
1105 /// type-level CountTree's `count_value` IS the total document
1106 /// count, no index walk needed.
1107 ///
1108 /// Shared between the server-side prove path
1109 /// ([`Drive::execute_document_count_point_lookup_proof`]'s
1110 /// documents_countable fast path) and the client-side verify path
1111 /// ([`Self::verify_primary_key_count_tree_proof`]). Both sides
1112 /// produce the exact same `PathQuery` for merk-root recomputation.
1113 ///
1114 /// Free function rather than a method on `DriveDocumentCountQuery`
1115 /// because the documents_countable case isn't tied to any index —
1116 /// it operates at the doctype level directly.
1117 pub fn primary_key_count_tree_path_query(
1118 contract_id: [u8; 32],
1119 document_type_name: &str,
1120 ) -> PathQuery {
1121 let path = vec![
1122 vec![RootTree::DataContractDocuments as u8],
1123 contract_id.to_vec(),
1124 vec![1u8],
1125 document_type_name.as_bytes().to_vec(),
1126 ];
1127 let mut query = Query::new();
1128 query.insert_key(vec![0]);
1129 PathQuery::new(path, SizedQuery::new(query, None, None))
1130 }
1131}