Skip to main content

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}