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}