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::{document_query_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.
70///
71/// `platform_version` must be the protocol version `mtd` names, or the latest known
72/// version when `mtd` names a newer one; otherwise the answer is refused with
73/// [`Error::ProofProtocolVersionMismatch`].
74pub fn verify_composite_documents_proof(
75 query: &DriveDocumentQuery,
76 proof: &Proof,
77 mtd: &ResponseMetadata,
78 platform_version: &PlatformVersion,
79 provider: &dyn ContextProvider,
80) -> Result<(RootHash, CompositeDocuments), Error> {
81 let (root_hash, result) = query
82 .verify_composite_documents_proof(
83 document_query_proof_bytes(proof, mtd, platform_version)?,
84 platform_version,
85 )
86 .map_drive_error(proof, mtd)?;
87
88 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
89
90 Ok((
91 root_hash,
92 CompositeDocuments {
93 page_documents: result.page_documents,
94 sub_results: result.sub_results,
95 sub_result_missing_ids: result.sub_result_missing_ids,
96 sub_result_removals: result.sub_result_removals,
97 },
98 ))
99}
100
101impl<'dq, Q> FromProof<Q> for CompositeDocuments
102where
103 Q: TryInto<DriveDocumentQuery<'dq>> + Clone + 'dq,
104 Q::Error: std::fmt::Display,
105{
106 type Request = Q;
107 type Response = GetDocumentsResponse;
108
109 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
110 request: I,
111 response: O,
112 _network: Network,
113 platform_version: &PlatformVersion,
114 provider: &'a dyn ContextProvider,
115 ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
116 where
117 Self: 'a,
118 {
119 let request: Self::Request = request.into();
120 let response: Self::Response = response.into();
121
122 let query: DriveDocumentQuery<'dq> =
123 request
124 .clone()
125 .try_into()
126 .map_err(|e: Q::Error| Error::RequestError {
127 error: e.to_string(),
128 })?;
129
130 // The standard envelope carries the single MERGED proof, and
131 // the proof alone is enough: the verifier bootstraps the page
132 // from it via a subset pass and re-derives the rest.
133 let proof = response.proof().or(Err(Error::NoProofInResult))?;
134 let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
135
136 let (_root_hash, composite) =
137 verify_composite_documents_proof(&query, proof, mtd, platform_version, provider)?;
138
139 // An empty page is a valid, proven "nothing here" — surface it
140 // as Some(empty) rather than None so callers can tell it apart
141 // from a missing object.
142 Ok((Some(composite), mtd.clone(), proof.clone()))
143 }
144}