drive_proof_verifier/proof/composite_document.rs
1//! Verified **composite document** results: a page plus the
2//! sub-queries derived from it, answered as ONE merged grovedb proof.
3//!
4//! The server proves the limited page and every sub-query (by-id
5//! joins, indexed lookups, grouped counts, siblings) as one merged
6//! path query. The verifier
7//! ([`DriveDocumentQuery::verify_composite_documents_proof`])
8//! bootstraps the page from the proof with a subset pass, re-derives
9//! every sub-query's `IN` clause from the PROVEN page (or the proven
10//! earlier sub-query it binds), rebuilds the same merged query, verifies
11//! it in one authoritative pass, and routes the proved entries back to
12//! their components — refusing unclaimed entries, dangling joins and
13//! derivation divergence. This module's [`FromProof`] impl composes
14//! that with the tenderdash signature binding of the single root.
15//!
16//! There is deliberately **no unproven decoder with verification
17//! semantics** here: an unproven composite response is free to
18//! fabricate any sub-result, which is precisely what the surface exists
19//! to prevent. [`CompositeDocuments`] can still be built from a trusted
20//! node's unproven wire by the SDK if it chooses, but the canonical
21//! path proves.
22
23use crate::error::MapGroveDbError;
24use crate::verify::{supported_grovedb_proof_bytes, verify_tenderdash_proof};
25use crate::{ContextProvider, Error, FromProof};
26use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
27use dapi_grpc::platform::VersionedGrpcResponse;
28use dpp::dashcore::Network;
29use dpp::document::Document;
30use dpp::version::PlatformVersion;
31use drive::drive::contract::moderation::types::ContractDocumentRemovalEntry;
32use drive::query::{DriveDocumentQuery, SubQueryResult};
33use drive::verify::RootHash;
34
35/// The verified result of a composite document query.
36#[derive(Debug, Clone, PartialEq, Default)]
37pub struct CompositeDocuments {
38 /// The page, exactly as the page query alone would return it.
39 pub page_documents: Vec<Document>,
40 /// One result per sub-query, in request order: a by-id join's
41 /// documents in first-appearance order of their ids among the
42 /// source documents, a lookup's or sibling's in query order, or one
43 /// count per derived value that has a count tree (a value without
44 /// an entry counts zero).
45 pub sub_results: Vec<SubQueryResult>,
46 /// One list per sub-query, in request order: for a by-id join off a
47 /// `deletableDocument` property, the derived ids that have NO
48 /// document, in first-appearance order, each one a PROVEN absence;
49 /// empty for every other sub-query.
50 pub sub_result_missing_ids: Vec<Vec<dpp::identifier::Identifier>>,
51 /// One list per sub-query, in request order: for a by-id join off a
52 /// `moderatedDocument` property, the derived ids whose document the
53 /// contract's moderators removed, each with its PROVEN removal record,
54 /// in first-appearance order; empty for every other sub-query. There a
55 /// derived id with neither a document nor a record is an invalid proof.
56 pub sub_result_removals: Vec<Vec<ContractDocumentRemovalEntry>>,
57}
58
59/// Verify a composite query's single merged proof and bind its root
60/// hash to the quorum signature.
61///
62/// The merk-level composition (bootstrap subset pass on the page,
63/// re-derivation of every sub-query, authoritative full verification,
64/// routing with the completeness checks) lives in rs-drive's
65/// [`DriveDocumentQuery::verify_composite_documents_proof`];
66/// this wrapper adds the [`verify_tenderdash_proof`] binding — the root
67/// hash the proof commits to is only an attested fact once it is tied
68/// to the quorum-signed app hash, and this function exists so the
69/// composition can never be skipped by accident.
70pub fn verify_composite_documents_proof(
71 query: &DriveDocumentQuery,
72 proof: &Proof,
73 mtd: &ResponseMetadata,
74 platform_version: &PlatformVersion,
75 provider: &dyn ContextProvider,
76) -> Result<(RootHash, CompositeDocuments), Error> {
77 let (root_hash, result) = query
78 .verify_composite_documents_proof(supported_grovedb_proof_bytes(proof)?, platform_version)
79 .map_drive_error(proof, mtd)?;
80
81 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
82
83 Ok((
84 root_hash,
85 CompositeDocuments {
86 page_documents: result.page_documents,
87 sub_results: result.sub_results,
88 sub_result_missing_ids: result.sub_result_missing_ids,
89 sub_result_removals: result.sub_result_removals,
90 },
91 ))
92}
93
94impl<'dq, Q> FromProof<Q> for CompositeDocuments
95where
96 Q: TryInto<DriveDocumentQuery<'dq>> + Clone + 'dq,
97 Q::Error: std::fmt::Display,
98{
99 type Request = Q;
100 type Response = GetDocumentsResponse;
101
102 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
103 request: I,
104 response: O,
105 _network: Network,
106 platform_version: &PlatformVersion,
107 provider: &'a dyn ContextProvider,
108 ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
109 where
110 Self: 'a,
111 {
112 let request: Self::Request = request.into();
113 let response: Self::Response = response.into();
114
115 let query: DriveDocumentQuery<'dq> =
116 request
117 .clone()
118 .try_into()
119 .map_err(|e: Q::Error| Error::RequestError {
120 error: e.to_string(),
121 })?;
122
123 // The standard envelope carries the single MERGED proof, and
124 // the proof alone is enough: the verifier bootstraps the page
125 // from it via a subset pass and re-derives the rest.
126 let proof = response.proof().or(Err(Error::NoProofInResult))?;
127 let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
128
129 let (_root_hash, composite) =
130 verify_composite_documents_proof(&query, proof, mtd, platform_version, provider)?;
131
132 // An empty page is a valid, proven "nothing here" — surface it
133 // as Some(empty) rather than None so callers can tell it apart
134 // from a missing object.
135 Ok((Some(composite), mtd.clone(), proof.clone()))
136 }
137}