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