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