Skip to main content

drive_proof_verifier/proof/
chained_document.rs

1//! Verified **chained document** (provable semi-join) results.
2//!
3//! A chained query is `SELECT * FROM <outer> WHERE $id IN (SELECT
4//! <join_property> FROM <inner> WHERE …)` answered as ONE merged
5//! grovedb proof: the limited inner indexOnly page and the outer
6//! by-ids fetch derived from its values, merged by the server (the
7//! inner limit a per-instance cap on its branch). The
8//! verifier ([`DriveDocumentQuery::verify_chained_documents_proof`])
9//! reconstructs the merged query from the response's UNTRUSTED
10//! join-value hint, verifies in one pass, and requires the proven
11//! outer documents to match the PROVEN inner join values. For a
12//! `refersTo: permanentDocument` join property the match is exact: a
13//! missing referenced document is an invalid proof, since such a target
14//! cannot dangle. For a `refersTo: deletableDocument` join property a
15//! join value whose document is proven absent (deleted after the inner
16//! document was written) has no outer document; the proof must still
17//! show every derived `$id` present or absent, so an existing document
18//! cannot be passed off as deleted. This module's
19//! [`FromProof`] impl composes that with the tenderdash signature
20//! binding of the single root.
21//!
22//! There is deliberately **no unproven decoder with verification
23//! semantics** here: an unproven chained response is free to fabricate
24//! the join entirely (substitute, omit, inject), which is precisely
25//! what the surface exists to prevent. [`ChainedDocuments`] can still
26//! be built from a trusted node's unproven wire by the SDK if it
27//! chooses, but the canonical path proves.
28
29use crate::error::MapGroveDbError;
30use crate::verify::{supported_grovedb_proof_bytes, verify_tenderdash_proof};
31use crate::{ContextProvider, Error, FromProof};
32use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
33use dapi_grpc::platform::VersionedGrpcResponse;
34use dpp::dashcore::Network;
35use dpp::document::Document;
36use dpp::version::PlatformVersion;
37use drive::drive::contract::moderation::types::ContractDocumentRemovalEntry;
38use drive::query::DriveDocumentQuery;
39use drive::verify::RootHash;
40
41/// The verified result of a chained document query, both halves in
42/// inner-proof order.
43#[derive(Debug, Clone, PartialEq, Default)]
44pub struct ChainedDocuments {
45    /// The inner projections (synthesized indexOnly documents), exactly
46    /// as the inner query alone would return them. The last one's join
47    /// property carries the pagination cursor.
48    pub inner_documents: Vec<Document>,
49    /// The joined outer documents, ordered by first appearance of their
50    /// id among the inner projections (deduplicated). Under a
51    /// `deletableDocument` join property a deleted target has no entry
52    /// here, so this can be shorter than the distinct join values; match
53    /// the halves by id, not by position.
54    pub outer_documents: Vec<Document>,
55    /// The join values that have NO outer document, in first-appearance
56    /// order: each one a PROVEN absence (the merged proof covers every
57    /// derived `$id`). Always empty under a `permanentDocument` join
58    /// property, where a missing document is an invalid proof, and under a
59    /// `moderatedDocument` one, which reports its missing documents in
60    /// [`Self::removed_outer_documents`].
61    pub missing_outer_ids: Vec<dpp::identifier::Identifier>,
62    /// The join values whose outer document the contract's moderators
63    /// removed, each with its PROVEN removal record, in first-appearance
64    /// order. Only a `moderatedDocument` join property reports any; there a
65    /// join value with neither a document nor a record is an invalid proof.
66    pub removed_outer_documents: Vec<ContractDocumentRemovalEntry>,
67}
68
69/// Verify a chained query's single merged proof and bind its root hash
70/// to the quorum signature.
71///
72/// The merk-level composition (bootstrap subset pass on the inner
73/// query, merged-query re-derivation, authoritative full verification,
74/// exact set equality against the PROVEN join values) lives in rs-drive's
75/// [`DriveDocumentQuery::verify_chained_documents_proof`]; this
76/// wrapper adds the [`verify_tenderdash_proof`] binding — the root hash
77/// the proof commits to is only an attested fact once it is tied to the
78/// quorum-signed app hash, and this function exists so the composition
79/// can never be skipped by accident.
80///
81/// The query is the chained shape: the inner [`DriveDocumentQuery`]
82/// carrying its single by-id join in `sub_queries` (see
83/// `DriveDocumentQuery::with_by_id_join`).
84pub fn verify_chained_documents_proof(
85    query: &DriveDocumentQuery,
86    proof: &Proof,
87    mtd: &ResponseMetadata,
88    platform_version: &PlatformVersion,
89    provider: &dyn ContextProvider,
90) -> Result<(RootHash, ChainedDocuments), Error> {
91    let (root_hash, result) = query
92        .verify_chained_documents_proof(supported_grovedb_proof_bytes(proof)?, platform_version)
93        .map_drive_error(proof, mtd)?;
94
95    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
96
97    Ok((
98        root_hash,
99        ChainedDocuments {
100            inner_documents: result.inner_documents,
101            outer_documents: result.outer_documents,
102            missing_outer_ids: result.missing_outer_ids,
103            removed_outer_documents: result.removed_outer_documents,
104        },
105    ))
106}
107
108impl<'dq, Q> FromProof<Q> for ChainedDocuments
109where
110    Q: TryInto<DriveDocumentQuery<'dq>> + Clone + 'dq,
111    Q::Error: std::fmt::Display,
112{
113    type Request = Q;
114    type Response = GetDocumentsResponse;
115
116    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
117        request: I,
118        response: O,
119        _network: Network,
120        platform_version: &PlatformVersion,
121        provider: &'a dyn ContextProvider,
122    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
123    where
124        Self: 'a,
125    {
126        let request: Self::Request = request.into();
127        let response: Self::Response = response.into();
128
129        let query: DriveDocumentQuery<'dq> =
130            request
131                .clone()
132                .try_into()
133                .map_err(|e: Q::Error| Error::RequestError {
134                    error: e.to_string(),
135                })?;
136
137        // The standard envelope carries the single MERGED proof, and
138        // the proof alone is enough: the verifier bootstraps the join
139        // values from it via a subset pass.
140        let proof = response.proof().or(Err(Error::NoProofInResult))?;
141        let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
142
143        let (_root_hash, chained) =
144            verify_chained_documents_proof(&query, proof, mtd, platform_version, provider)?;
145
146        // An empty inner page is a valid, proven "you have nothing
147        // here" — surface it as Some(empty) rather than None so callers
148        // can tell it apart from a missing object.
149        Ok((Some(chained), mtd.clone(), proof.clone()))
150    }
151}