drive/query/chained_document_query/mod.rs
1//! Chained document queries: a provable semi-join.
2//!
3//! `SELECT * FROM post WHERE $id IN (SELECT postId FROM like WHERE
4//! $ownerId = <me>)` — the INNER query runs against an indexOnly document
5//! type and projects a `refersTo: permanentDocument` property (the JOIN
6//! property); its proven values are reinjected as the OUTER query's
7//! primary keys. Both halves are proven as ONE merged grovedb proof —
8//! `prove_query_many` merges the limited inner query with the derived
9//! outer by-ids query, the inner limit carried as a per-instance cap on
10//! its root query (the form grovedb's merge lifts a global limit into),
11//! so a single root binds the whole composition by construction; the
12//! surrounding tenderdash layer then binds that root to the
13//! quorum-signed app hash (see `rs-drive-proof-verifier`).
14//!
15//! There is no separate chained query type: a chained query is a
16//! [`DriveDocumentQuery`] — the inner half — whose
17//! [`sub_queries`](DriveDocumentQuery::sub_queries) carry exactly one
18//! by-id join bound to it, the shape
19//! [`DriveDocumentQuery::with_by_id_join`] builds (the same shape the
20//! composite surface generalizes). This module holds the chained
21//! behaviour of `DriveDocumentQuery`: shape validation, join-value
22//! derivation, the outer by-ids builder, proof merging, and the
23//! server-side executors behind `Drive::query_chained_documents` /
24//! `query_chained_documents_with_proof` (the verifier half lives in
25//! `verify::chained_document`).
26//!
27//! Soundness never rests on the server's join: the verifier re-derives
28//! the outer query from the INNER proof's results
29//! ([`DriveDocumentQuery::chained_join_values`] →
30//! [`DriveDocumentQuery::derive_chained_outer_query`], the same functions
31//! the server executes), so a server cannot substitute, omit, or inject
32//! outer documents. When the join property's `refersTo` targets a
33//! `permanentDocument` type (non-deletable, enforced at write time),
34//! every proven join value MUST resolve to a document — a missing outer
35//! document is an invalid proof, not an absence. A `moderatedDocument`
36//! target leaves state only through a moderator's recorded removal, so
37//! the merged proof also covers the removal records of the join values
38//! (see [`moderated_join`](crate::query::moderated_join)): a join value
39//! with no outer document is reported with its proven record, and one
40//! with neither is an invalid proof. A `deletableDocument` target
41//! promises only that the document existed when the inner one was
42//! written, so there a join value with no outer document is left out.
43//! That omission is proven, not trusted: every derived `$id` is a queried
44//! key of the merged query, and grovedb refuses a proof without the
45//! coverage to show a queried key present or absent, so a prover cannot
46//! pass an existing document off as deleted.
47//!
48//! Guardrails (v1): the inner query must resolve to an indexOnly index
49//! that carries the join property (as terminal or prefix property, so
50//! every synthesized projection provably carries its value); the join
51//! edge must be a same-contract `refersTo: permanentDocument`,
52//! `refersTo: moderatedDocument` or `refersTo: deletableDocument` whose
53//! target is the outer type; the
54//! inner limit is required (it is what
55//! bounds the outer fan-out); the outer half takes no clauses, no
56//! limit and no cursor — it is purely the derived by-ids fetch, and
57//! pagination lives on the inner query alone.
58
59use crate::drive::contract::moderation::types::ContractDocumentRemovalEntry;
60use crate::error::drive::DriveError;
61use crate::error::proof::ProofError;
62use crate::error::query::QuerySyntaxError;
63use crate::error::Error;
64#[cfg(feature = "server")]
65use crate::query::moderated_join::fetch_removals;
66use crate::query::moderated_join::{pair_missing_with_removals, removals_path_query};
67use crate::query::{
68 BindingSource, DriveDocumentQuery, InternalClauses, SubQueryBinding, SubQueryKind, WhereClause,
69 WhereOperator,
70};
71use dpp::data_contract::accessors::v0::DataContractV0Getters;
72use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters};
73use dpp::data_contract::document_type::{
74 DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentReferenceDeclaration,
75 DocumentReferenceKind, DocumentTypeRef,
76};
77use dpp::data_contract::DataContract;
78use dpp::document::{Document, DocumentV0Getters};
79use dpp::identifier::Identifier;
80use dpp::platform_value::Value;
81use dpp::version::PlatformVersion;
82use std::collections::BTreeMap;
83
84/// The most join values one chained query can carry — the derived
85/// outer query is a single `$id IN [...]` clause, and `in` clauses
86/// admit at most 100 values (`WhereClause::in_values`).
87/// [`DriveDocumentQuery::validate_chained`] caps the inner limit here so
88/// every reachable page fits, and
89/// [`DriveDocumentQuery::chained_proof_path_queries`] enforces it on the
90/// (untrusted, verifier-supplied) join-value list itself.
91pub const MAX_CHAINED_JOIN_VALUES: usize = 100;
92
93/// The materialized result of a chained query, in inner-proof order.
94#[derive(Debug, Default)]
95pub struct ChainedDocumentsResult {
96 /// The join values that have NO outer document, in first-appearance
97 /// order. Only a join off a `refersTo: deletableDocument` property
98 /// can report any (a referenced document deleted after the inner one
99 /// was written); off a `permanentDocument` property a missing document
100 /// is refused instead. On the proof path each is a proven absence.
101 pub missing_outer_ids: Vec<Identifier>,
102 /// The join values whose outer document the contract's moderators
103 /// removed, each with its removal record, in first-appearance order.
104 /// Only a join off a `refersTo: moderatedDocument` property can report
105 /// any; there a join value with neither a document nor a record is
106 /// refused. On the proof path each is a proven absence of the document
107 /// and a proven record.
108 pub removed_outer_documents: Vec<ContractDocumentRemovalEntry>,
109 /// The inner projections (synthesized indexOnly documents), exactly
110 /// as the inner query alone would return them — the caller reads its
111 /// pagination cursor (the last join value) from here.
112 pub inner_documents: Vec<Document>,
113 /// The referenced outer documents, ordered by FIRST APPEARANCE of
114 /// their id in `inner_documents` (deduplicated).
115 pub outer_documents: Vec<Document>,
116}
117
118/// The outer half of a chained query, assembled against its join values by
119/// [`DriveDocumentQuery::assemble_chained_outer_documents`].
120#[derive(Debug, Default)]
121pub struct ChainedOuterDocuments {
122 /// The outer documents, in first-appearance order of their ids.
123 pub documents: Vec<Document>,
124 /// The join values with no outer document, off a `deletableDocument`
125 /// join property.
126 pub missing: Vec<Identifier>,
127 /// The removal records of the join values with no outer document, off a
128 /// `moderatedDocument` join property.
129 pub removed: Vec<ContractDocumentRemovalEntry>,
130}
131
132impl<'a> DriveDocumentQuery<'a> {
133 /// The join edge of a chained query. There is no separate chained
134 /// query type: a chained query is this query (the inner half) whose
135 /// [`sub_queries`](Self::sub_queries) carry EXACTLY ONE by-id join
136 /// bound to it — the shape [`Self::with_by_id_join`] builds. Returns
137 /// the join's source property (the inner property whose proven values
138 /// become the outer `$id`s) and the outer document type with its
139 /// contract; refuses any other sub-query shape.
140 pub(crate) fn chained_join(
141 &self,
142 ) -> Result<(&str, DocumentTypeRef<'a>, &'a DataContract), Error> {
143 let unsupported =
144 |message: &str| Error::Query(QuerySyntaxError::Unsupported(message.to_string()));
145 let [join] = self.sub_queries.as_slice() else {
146 return Err(unsupported(
147 "a chained query carries exactly one sub-query: the by-id join whose source \
148 property's proven values become the outer `$id`s (build it with \
149 with_by_id_join); a query with more sub-queries belongs on the composite \
150 surface",
151 ));
152 };
153 let Some(SubQueryBinding {
154 source: BindingSource::Page,
155 source_property,
156 field,
157 }) = &join.binding
158 else {
159 return Err(unsupported(
160 "a chained query's sub-query must be bound to the inner query itself",
161 ));
162 };
163 if field.as_str() != dpp::document::property_names::ID {
164 return Err(unsupported(
165 "a chained query's sub-query must be a by-id join (bound field `$id`); other \
166 bindings live on the composite surface",
167 ));
168 }
169 if join.kind != SubQueryKind::Documents {
170 return Err(unsupported(
171 "a chained join returns documents; counts live on the composite surface",
172 ));
173 }
174 if !join.where_clauses.is_empty() || !join.order_by.is_empty() || join.limit.is_some() {
175 return Err(unsupported(
176 "a chained by-id join takes no fixed clauses, no ordering and no limit: the \
177 outer half is purely the derived by-ids fetch, complete by set equality",
178 ));
179 }
180 Ok((source_property.as_str(), join.document_type, join.contract))
181 }
182
183 /// Validates the chained shape: this query as the inner indexOnly
184 /// half plus the single by-id join its
185 /// [`sub_queries`](Self::sub_queries) carry (see
186 /// [`Self::chained_join`]). Called by the server before executing and
187 /// by the verifier before verifying, so an invalid spec fails
188 /// identically on both sides.
189 pub fn validate_chained(&self, platform_version: &PlatformVersion) -> Result<(), Error> {
190 let unsupported = |message: String| Error::Query(QuerySyntaxError::Unsupported(message));
191
192 let (join_property, outer_document_type, outer_contract) = self.chained_join()?;
193 // Chained joins are same-contract (v1): the join sub-query's
194 // contract must be the inner query's own.
195 if outer_contract.id() != self.contract.id() {
196 return Err(unsupported(
197 "chained document queries support same-contract joins only: the join \
198 sub-query targets another contract"
199 .to_string(),
200 ));
201 }
202 if !self.document_type.index_only() {
203 return Err(unsupported(
204 "chained document queries require an indexOnly inner document type: only \
205 indexOnly projections prove their values positionally"
206 .to_string(),
207 ));
208 }
209 if outer_document_type.index_only() {
210 return Err(unsupported(
211 "the outer document type of a chained query cannot be indexOnly: outer \
212 documents are fetched by id from primary storage, which indexOnly types \
213 do not have"
214 .to_string(),
215 ));
216 }
217 match self.limit {
218 None => {
219 return Err(unsupported(
220 "chained document queries require an explicit limit on the inner query: \
221 the inner page size is what bounds the derived outer query"
222 .to_string(),
223 ));
224 }
225 Some(limit) if limit as usize > MAX_CHAINED_JOIN_VALUES => {
226 return Err(unsupported(format!(
227 "a chained inner limit of {} exceeds {}: the derived outer query is a \
228 single `$id IN` clause, which admits at most that many values",
229 limit, MAX_CHAINED_JOIN_VALUES,
230 )));
231 }
232 Some(_) => {}
233 }
234 if self.offset.is_some() {
235 return Err(unsupported(
236 "chained document queries do not support an inner offset; paginate with a \
237 range clause on the join property"
238 .to_string(),
239 ));
240 }
241
242 // The join property must be a same-contract document reference
243 // targeting the outer type. `refersTo` writes are
244 // existence-validated; a permanentDocument target can never be
245 // deleted, so every proven join value MUST resolve and a missing
246 // outer document is an invalid proof, while a deletableDocument
247 // target may be gone and is then left out (see
248 // `assemble_chained_outer_documents`).
249 let Some(join_document_property) =
250 self.document_type.flattened_properties().get(join_property)
251 else {
252 return Err(unsupported(format!(
253 "chained query join property \"{}\" does not name a property of inner \
254 document type \"{}\"",
255 join_property,
256 self.document_type.name(),
257 )));
258 };
259 // A typed array of references is no join property: it is not
260 // indexable, and a join value is one identifier
261 let document_reference = match &join_document_property.property_type {
262 DocumentPropertyType::IdentifierWithReference(reference_target) => {
263 reference_target.as_document_reference()
264 }
265 _ => None,
266 };
267 // A reference found by `findBy` is a document reference whose value
268 // is not the outer document's id, so `as_document_reference` leaves it
269 // out; it is named here so the refusal says why
270 if let DocumentPropertyType::IdentifierWithReference(
271 DocumentPropertyReferenceTarget::PermanentDocumentLookup { lookup, .. }
272 | DocumentPropertyReferenceTarget::DeletableDocumentLookup { lookup, .. },
273 ) = &join_document_property.property_type
274 {
275 return Err(unsupported(format!(
276 "chained query join property \"{}\" refers to its document by findBy ({}), \
277 so its value is not the outer document's id: a join needs a reference whose \
278 value is the referenced document's $id",
279 join_property,
280 lookup.find_by_names(),
281 )));
282 }
283 // A reference expression is no single document reference either: an
284 // `anyOf` value may be the id of any of its leaves, and an `allOf`
285 // names no one outer document type to join through
286 if let DocumentPropertyType::IdentifierWithReference(
287 DocumentPropertyReferenceTarget::AnyOf(_) | DocumentPropertyReferenceTarget::AllOf(_),
288 ) = &join_document_property.property_type
289 {
290 return Err(unsupported(format!(
291 "chained query join property \"{}\" declares a refersTo anyOf or allOf \
292 expression: a join needs a reference to one document type",
293 join_property,
294 )));
295 }
296 match document_reference {
297 Some(DocumentReferenceDeclaration {
298 contract_id,
299 document_type_name,
300 ..
301 }) => {
302 if let Some(referenced_contract_id) = contract_id {
303 if referenced_contract_id != self.contract.id() {
304 return Err(unsupported(
305 "chained document queries support same-contract joins only: \
306 the join property's refersTo names another contract"
307 .to_string(),
308 ));
309 }
310 }
311 if document_type_name != outer_document_type.name() {
312 return Err(unsupported(format!(
313 "chained query outer document type \"{}\" does not match the join \
314 property's refersTo target \"{}\"",
315 outer_document_type.name(),
316 document_type_name,
317 )));
318 }
319 }
320 None => {
321 return Err(unsupported(format!(
322 "chained query join property \"{}\" must carry a `refersTo: \
323 permanentDocument`, `refersTo: moderatedDocument` or `refersTo: \
324 deletableDocument` declaration: it is what names the outer document type \
325 the proven join values resolve in",
326 join_property,
327 )));
328 }
329 }
330
331 // The resolved index must carry the join property, so every
332 // synthesized inner projection provably carries its value.
333 let index = self.index_only_query_index(platform_version)?;
334 let index_carries_join_property = index.terminal_contains(join_property)
335 || index
336 .properties
337 .iter()
338 .any(|property| property.name == join_property);
339 if !index_carries_join_property {
340 return Err(unsupported(format!(
341 "the inner query resolves to index \"{}\", which does not carry the join \
342 property \"{}\"; constrain the query so an index carrying it serves it",
343 index.name, join_property,
344 )));
345 }
346
347 Ok(())
348 }
349
350 /// Extracts the join values from the inner documents in their proof
351 /// order, deduplicated to first appearance. ONE extraction both the
352 /// server and the verifier run — the single-builder rule that keeps
353 /// the derived outer query identical on both sides.
354 pub fn chained_join_values(
355 &self,
356 inner_documents: &[Document],
357 ) -> Result<Vec<Identifier>, Error> {
358 use dpp::platform_value::btreemap_extensions::BTreeValueMapPathHelper;
359
360 let (join_property, _, _) = self.chained_join()?;
361 let mut seen: std::collections::BTreeSet<Identifier> = std::collections::BTreeSet::new();
362 let mut join_values = Vec::with_capacity(inner_documents.len());
363 for document in inner_documents {
364 // Path-aware read: `validate_chained` admits any property
365 // `flattened_properties()` names — dotted (nested) keys
366 // included — and the synthesis builder stores those nested
367 // (`insert_at_path`), so a flat `.get` would miss them.
368 let value = document
369 .properties()
370 .get_optional_at_path(join_property)
371 .ok()
372 .flatten()
373 .ok_or(Error::Drive(DriveError::CorruptedCodeExecution(
374 "an inner projection is missing the join property: validate_chained() \
375 guarantees the resolved index carries it",
376 )))?;
377 let identifier = value.to_identifier().map_err(|_| {
378 Error::Drive(DriveError::CorruptedCodeExecution(
379 "a chained join property must decode as an identifier: the parser \
380 only admits identifier-typed refersTo properties",
381 ))
382 })?;
383 if seen.insert(identifier) {
384 join_values.push(identifier);
385 }
386 }
387 Ok(join_values)
388 }
389
390 /// The derived outer query: a pure by-ids fetch of the join values
391 /// from the outer type's primary storage. No clauses, no limit, no
392 /// cursor — completeness is set-equality against `join_values`,
393 /// checked by the verifier.
394 pub fn derive_chained_outer_query(
395 &self,
396 join_values: &[Identifier],
397 ) -> Result<DriveDocumentQuery<'a>, Error> {
398 let (_, outer_document_type, outer_contract) = self.chained_join()?;
399 // Canonical value order: byte-ascending. Grove sorts query keys
400 // internally either way; sorting here keeps the built query —
401 // and therefore the proof — byte-identical between the server
402 // and a verifier that extracted the ids in any order.
403 let mut ids: Vec<Identifier> = join_values.to_vec();
404 ids.sort();
405 Ok(DriveDocumentQuery {
406 contract: outer_contract,
407 document_type: outer_document_type,
408 internal_clauses: InternalClauses {
409 primary_key_in_clause: Some(WhereClause {
410 field: dpp::document::property_names::ID.to_string(),
411 operator: WhereOperator::In,
412 value: Value::Array(
413 ids.into_iter()
414 .map(|id| Value::Identifier(id.to_buffer()))
415 .collect(),
416 ),
417 }),
418 primary_key_equal_clause: None,
419 in_clauses: Vec::new(),
420 range_clause: None,
421 equal_clauses: Default::default(),
422 },
423 offset: None,
424 limit: None,
425 order_by: Default::default(),
426 start_at: None,
427 start_at_included: false,
428 block_time_ms: None,
429 resolved_time_ranges: Vec::new(),
430 sub_queries: vec![],
431 })
432 }
433
434 /// What the join property's reference guarantees of its target: that
435 /// it stays in state (`permanentDocument`), leaves it only on a
436 /// moderator's record (`moderatedDocument`), or nothing
437 /// (`deletableDocument`). A join property that is none of them, which
438 /// `validate_chained` refuses, is held to the strict rule.
439 fn chained_join_kind(&self) -> Result<DocumentReferenceKind, Error> {
440 let (join_property, _, _) = self.chained_join()?;
441 Ok(self
442 .document_type
443 .flattened_properties()
444 .get(join_property)
445 .and_then(|property| match &property.property_type {
446 DocumentPropertyType::IdentifierWithReference(reference_target) => {
447 reference_target.as_document_reference()
448 }
449 _ => None,
450 })
451 .map_or(DocumentReferenceKind::Permanent, |declaration| {
452 declaration.kind
453 }))
454 }
455
456 /// The removal records component of the chained proof: for a join off
457 /// a `moderatedDocument` property with join values, the records of
458 /// every join value (see [`removals_path_query`]), walking as the outer
459 /// by-ids query does; `None` for every other join.
460 fn chained_removals_path_query(
461 &self,
462 join_values: &[Identifier],
463 outer_left_to_right: bool,
464 ) -> Result<Option<grovedb::PathQuery>, Error> {
465 if join_values.is_empty() || self.chained_join_kind()? != DocumentReferenceKind::Moderated {
466 return Ok(None);
467 }
468 let (_, outer_document_type, outer_contract) = self.chained_join()?;
469 Ok(Some(removals_path_query(
470 outer_contract.id(),
471 outer_document_type.name(),
472 join_values,
473 outer_left_to_right,
474 )))
475 }
476
477 /// Reorders the outer documents (returned in key order by the by-ids
478 /// query) into first-appearance join order, and checks them against
479 /// the derived join values. An outer document carried twice, or one
480 /// no join value references, is always refused. A join value with no
481 /// outer document is refused when the join property is a
482 /// `permanentDocument` reference (it cannot dangle: corrupted state
483 /// on the server, an invalid proof in the verifier), reported with its
484 /// removal record, taken from `removals`, when it is a
485 /// `moderatedDocument` reference (one without a record is refused as a
486 /// permanent one is), and left out when it is a `deletableDocument`
487 /// reference (the target was deleted after the inner document was
488 /// written, and the by-ids query that found nothing under its `$id` is
489 /// the very query the proof covers); the join values left out and the
490 /// records are returned alongside, each in first-appearance order.
491 /// Shared by the server and the verifier.
492 pub fn assemble_chained_outer_documents(
493 &self,
494 join_values: &[Identifier],
495 outer_documents: Vec<Document>,
496 removals: &BTreeMap<Identifier, ContractDocumentRemovalEntry>,
497 ) -> Result<ChainedOuterDocuments, Error> {
498 let mut by_id: BTreeMap<Identifier, Document> = BTreeMap::new();
499 for document in outer_documents {
500 let id = document.id();
501 if by_id.insert(id, document).is_some() {
502 return Err(Error::Proof(ProofError::CorruptedProof(format!(
503 "chained outer results carry document {} twice",
504 id
505 ))));
506 }
507 }
508 let kind = self.chained_join_kind()?;
509 let mut ordered = Vec::with_capacity(join_values.len());
510 let mut missing = Vec::new();
511 for join_value in join_values {
512 match by_id.remove(join_value) {
513 Some(document) => ordered.push(document),
514 None if kind.is_permanent() => {
515 return Err(Error::Proof(ProofError::CorruptedProof(format!(
516 "chained outer results are missing referenced document {}: a \
517 permanentDocument reference cannot dangle, so the outer half \
518 does not prove the derived query",
519 join_value
520 ))));
521 }
522 // A moderatedDocument or deletableDocument target that is no
523 // longer in state.
524 None => missing.push(*join_value),
525 }
526 }
527 if let Some((extra_id, _)) = by_id.into_iter().next() {
528 return Err(Error::Proof(ProofError::CorruptedProof(format!(
529 "chained outer results carry document {} that no proven join value \
530 references",
531 extra_id
532 ))));
533 }
534 // A moderated target leaves state on a moderator's record only
535 if kind == DocumentReferenceKind::Moderated {
536 return Ok(ChainedOuterDocuments {
537 removed: pair_missing_with_removals(&missing, removals)?,
538 documents: ordered,
539 missing: Vec::new(),
540 });
541 }
542 Ok(ChainedOuterDocuments {
543 documents: ordered,
544 missing,
545 removed: Vec::new(),
546 })
547 }
548
549 /// The component path queries the chained proof covers: the inner
550 /// query's own path query, plus — for a non-empty join — the outer
551 /// by-ids path query derived from `join_values`, and for a join off a
552 /// `moderatedDocument` property the removal records of the same ids. ONE builder both
553 /// the prover (`prove_query_many` merges these) and the verifier
554 /// (`PathQuery::merge` on the same inputs at the same grove
555 /// version) call, so the merged query is byte-identical on both
556 /// sides.
557 ///
558 /// The inner component is the composite page
559 /// ([`Self::page_path_query`]): a chained query is that shape with one
560 /// by-id join, and the page carries its limit as its root query's
561 /// per-instance cap, exactly what grovedb's merge lifts a global limit
562 /// into (exact here: the branch instance executes once). Authoring the
563 /// cap up front gives the inner half one form whether or not anything
564 /// merges with it, so the server's read of the inner page, the proof
565 /// and the verifier's bootstrap pass select the same rows. Under a
566 /// global limit they would not: grovedb cuts a layer that fans out
567 /// across an index's prefix values to that many branches and charges
568 /// an empty branch against it, while the cap budgets only the rows
569 /// below.
570 pub fn chained_proof_path_queries(
571 &self,
572 join_values: &[Identifier],
573 platform_version: &PlatformVersion,
574 ) -> Result<Vec<grovedb::PathQuery>, Error> {
575 // `join_values` may be an UNTRUSTED verifier-side hint; cap it
576 // before deriving, so an oversized list fails here with a clear
577 // message instead of deep in the `in`-clause lowering. An
578 // honest list cannot exceed this: it is deduplicated from an
579 // inner page whose limit `validate_chained` bounds to the same
580 // cap.
581 if join_values.len() > MAX_CHAINED_JOIN_VALUES {
582 return Err(Error::Query(QuerySyntaxError::Unsupported(format!(
583 "{} chained join values exceed the {} an outer `$id IN` clause admits",
584 join_values.len(),
585 MAX_CHAINED_JOIN_VALUES,
586 ))));
587 }
588 // Edited in place, as are the inner page reads of both executors
589 // below: a chained query needs an indexOnly inner type
590 // (`validate_chained`), which only protocol version 14 parses.
591 let inner = self.page_path_query(platform_version)?;
592 if join_values.is_empty() {
593 return Ok(vec![inner]);
594 }
595 let outer = self
596 .derive_chained_outer_query(join_values)?
597 .construct_path_query(None, platform_version)?;
598 match self.chained_removals_path_query(join_values, outer.query.query.left_to_right)? {
599 Some(removals) => Ok(vec![inner, outer, removals]),
600 None => Ok(vec![inner, outer]),
601 }
602 }
603}
604
605#[cfg(feature = "server")]
606impl DriveDocumentQuery<'_> {
607 /// Executes the chained query without proofs.
608 pub(crate) fn execute_chained_no_proof_internal(
609 &self,
610 drive: &crate::drive::Drive,
611 transaction: grovedb::TransactionArg,
612 drive_operations: &mut Vec<crate::fees::op::LowLevelDriveOperation>,
613 platform_version: &PlatformVersion,
614 ) -> Result<ChainedDocumentsResult, Error> {
615 use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0;
616
617 self.validate_chained(platform_version)?;
618 // The inner documents are serialized whole, as a documents query's
619 // are: refused before any read when the inner index lacks a property.
620 self.refuse_an_uncovered_index_only_projection(platform_version)?;
621
622 // Read from the inner component the proof covers, so a read with and
623 // without a proof return the same page (see `chained_proof_path_queries`)
624 let inner_documents = Self::materialize_component(
625 self,
626 &self.page_path_query(platform_version)?,
627 drive,
628 transaction,
629 drive_operations,
630 platform_version,
631 )?;
632 let join_values = self.chained_join_values(&inner_documents)?;
633 if join_values.is_empty() {
634 return Ok(ChainedDocumentsResult {
635 inner_documents,
636 outer_documents: Vec::new(),
637 missing_outer_ids: Vec::new(),
638 removed_outer_documents: Vec::new(),
639 });
640 }
641
642 let outer_query = self.derive_chained_outer_query(&join_values)?;
643 let outer_document_type = outer_query.document_type;
644 let (serialized_outer, _outer_skipped) = outer_query
645 .execute_raw_results_no_proof_internal(
646 drive,
647 transaction,
648 drive_operations,
649 platform_version,
650 )?;
651 let outer_documents = serialized_outer
652 .into_iter()
653 .map(|serialized| {
654 Document::from_bytes(serialized.as_slice(), outer_document_type, platform_version)
655 .map_err(|e| Error::Protocol(Box::new(e)))
656 })
657 .collect::<Result<Vec<Document>, Error>>()?;
658 // The records component the proof covers; a fetch by keys selects the same records
659 // whichever way it walks
660 let removals = match self.chained_removals_path_query(&join_values, true)? {
661 Some(path_query) => fetch_removals(
662 drive,
663 &path_query,
664 transaction,
665 drive_operations,
666 platform_version,
667 )?,
668 None => BTreeMap::new(),
669 };
670 let ChainedOuterDocuments {
671 documents: outer_documents,
672 missing: missing_outer_ids,
673 removed: removed_outer_documents,
674 } = self.assemble_chained_outer_documents(&join_values, outer_documents, &removals)?;
675
676 Ok(ChainedDocumentsResult {
677 inner_documents,
678 outer_documents,
679 missing_outer_ids,
680 removed_outer_documents,
681 })
682 }
683
684 /// Executes the chained query AND generates its single merged
685 /// proof.
686 ///
687 /// The inner page and the derived outer by-ids fetch are proven as
688 /// ONE grovedb proof: [`Self::chained_proof_path_queries`] builds the
689 /// component path queries and `prove_query_many` merges them, the
690 /// inner query's limit riding as its branch's per-instance
691 /// `Query::limit`. One proof means one root by construction.
692 ///
693 /// The materialize pass reads the inner page from that very inner
694 /// component, so the join values the outer component derives from
695 /// are the ones the proof's inner branch shows. The materialize pass
696 /// and the prove pass still both read
697 /// committed state — grovedb proves committed state only — so the
698 /// sequence is BRACKETED by root-hash reads and retried if a block
699 /// commit interleaved; otherwise the proof's inner branch could
700 /// disagree with the outer branch derived from the stale
701 /// materialization, and every verifier would reject the
702 /// composition.
703 ///
704 /// Returns the proof and the materialized INNER projections (the
705 /// join values, and with them the caller's response hint and
706 /// pagination cursor, derive from these). The outer documents are
707 /// deliberately NOT materialized here — the proof pass covers them,
708 /// so reading their bodies a second time would double the state
709 /// reads for data the proved response never carries inline.
710 pub(crate) fn execute_chained_with_proof_internal(
711 &self,
712 drive: &crate::drive::Drive,
713 drive_operations: &mut Vec<crate::fees::op::LowLevelDriveOperation>,
714 platform_version: &PlatformVersion,
715 ) -> Result<(Vec<u8>, Vec<Document>), Error> {
716 self.validate_chained(platform_version)?;
717 let inner_path_query = self.page_path_query(platform_version)?;
718
719 // Block commits are seconds apart while an attempt is
720 // milliseconds, so a bracket collision is rare and two in a row
721 // vanishingly so; three attempts is generosity, not need.
722 const MAX_ATTEMPTS: usize = 3;
723 for _ in 0..MAX_ATTEMPTS {
724 let root_before = drive
725 .grove
726 .root_hash(None, &platform_version.drive.grove_version)
727 .unwrap()?;
728
729 // Materialize the INNER half only — the join values the
730 // outer component derives from live in its projections.
731 let inner_documents = Self::materialize_component(
732 self,
733 &inner_path_query,
734 drive,
735 None,
736 drive_operations,
737 platform_version,
738 )?;
739 let join_values = self.chained_join_values(&inner_documents)?;
740
741 let path_queries = self.chained_proof_path_queries(&join_values, platform_version)?;
742 let path_query_refs: Vec<&grovedb::PathQuery> = path_queries.iter().collect();
743 let proof = drive
744 .grove
745 .prove_query_many(path_query_refs, None, &platform_version.drive.grove_version)
746 .unwrap()?;
747
748 let root_after = drive
749 .grove
750 .root_hash(None, &platform_version.drive.grove_version)
751 .unwrap()?;
752 if root_before != root_after {
753 continue;
754 }
755
756 return Ok((proof, inner_documents));
757 }
758 Err(Error::Drive(DriveError::NotSupported(
759 "chained proof generation raced a block commit on every attempt; \
760 transient — retry the request",
761 )))
762 }
763}