Skip to main content

drive_proof_verifier/proof/
document_count.rs

1use crate::error::MapGroveDbError;
2use crate::verify::verify_tenderdash_proof;
3use crate::{ContextProvider, Error, FromProof};
4use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
5use dapi_grpc::platform::VersionedGrpcResponse;
6use dpp::dashcore::Network;
7use dpp::version::PlatformVersion;
8use drive::query::{DriveDocumentCountQuery, DriveDocumentQuery, SplitCountEntry};
9
10/// The count of documents matching a query, verified from proof.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct DocumentCount(pub u64);
13
14impl<'dq, Q> FromProof<Q> for DocumentCount
15where
16    Q: TryInto<DriveDocumentQuery<'dq>> + Clone + 'dq,
17    Q::Error: std::fmt::Display,
18{
19    type Request = Q;
20    type Response = GetDocumentsResponse;
21
22    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
23        request: I,
24        response: O,
25        _network: Network,
26        platform_version: &PlatformVersion,
27        provider: &'a dyn ContextProvider,
28    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
29    where
30        Self: 'a,
31    {
32        let request: Self::Request = request.into();
33        let response: Self::Response = response.into();
34
35        let request: DriveDocumentQuery<'dq> =
36            request
37                .clone()
38                .try_into()
39                .map_err(|e: Q::Error| Error::RequestError {
40                    error: e.to_string(),
41                })?;
42
43        // Parse response to read proof and metadata
44        let proof = response.proof().or(Err(Error::NoProofInResult))?;
45        let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
46
47        let (root_hash, documents) = request
48            .verify_proof(&proof.grovedb_proof, platform_version)
49            .map_drive_error(proof, mtd)?;
50
51        let count = documents.len() as u64;
52
53        verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
54
55        Ok((Some(DocumentCount(count)), mtd.clone(), proof.clone()))
56    }
57}
58
59/// Verify a grovedb `AggregateCountOnRange` proof and the surrounding
60/// tenderdash commit, returning the verified document count.
61///
62/// Thin tenderdash-composition wrapper over
63/// [`DriveDocumentCountQuery::verify_aggregate_count_proof`] in
64/// rs-drive (which does the merk-level verification). Both helpers
65/// reuse the prover's `aggregate_count_path_query` internally so the
66/// path query bytes match byte-for-byte and the merk root
67/// recomputation succeeds; the caller passes the `query` struct
68/// itself rather than a pre-built `PathQuery`, removing a step
69/// where the SDK and server could drift.
70///
71/// Counterpart to the materialize-and-count path in
72/// [`FromProof<DriveDocumentQuery> for DocumentCount`] above: where
73/// that one verifies a regular grovedb proof that yields concrete
74/// documents and counts them client-side, this verifies the
75/// merk-level aggregate primitive that yields a single `u64`
76/// directly (capped only by the merk tree size, not `u16::MAX`).
77pub fn verify_aggregate_count_proof(
78    query: &DriveDocumentCountQuery,
79    proof: &Proof,
80    mtd: &ResponseMetadata,
81    platform_version: &PlatformVersion,
82    provider: &dyn ContextProvider,
83) -> Result<u64, Error> {
84    let (root_hash, count) = query
85        .verify_aggregate_count_proof(&proof.grovedb_proof, platform_version)
86        .map_drive_error(proof, mtd)?;
87
88    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
89
90    Ok(count)
91}
92
93/// Verify a regular grovedb range proof against a `ProvableCountTree`
94/// and the surrounding tenderdash commit, returning the verified
95/// per-`(in_key, key)` counts the proof commits to.
96///
97/// Thin tenderdash-composition wrapper over
98/// [`DriveDocumentCountQuery::verify_distinct_count_proof`] in
99/// rs-drive (which does the merk-level verification and the
100/// in_key extraction from `(path, key, element)` triples).
101///
102/// ## No cross-fork merge
103///
104/// For compound queries (an `In` clause on a prefix property) each
105/// returned [`SplitCountEntry`] retains its `in_key` (the In value
106/// for that fork) alongside the terminator `key`. Cross-fork
107/// aggregation is intentionally NOT done here — see
108/// [`SplitCountEntry`]'s doc for the rationale.
109///
110/// ## Trade-off vs. the aggregate path
111///
112/// Proof size is O(distinct `(in_key, terminator)` pairs matched)
113/// rather than O(log n), because each distinct in-range pair emits
114/// its own `KVCount` op instead of being collapsed into a boundary
115/// subtree. Still strictly smaller than materialize-and-count.
116pub fn verify_distinct_count_proof(
117    query: &DriveDocumentCountQuery,
118    proof: &Proof,
119    mtd: &ResponseMetadata,
120    limit: u16,
121    left_to_right: bool,
122    platform_version: &PlatformVersion,
123    provider: &dyn ContextProvider,
124) -> Result<Vec<SplitCountEntry>, Error> {
125    let (root_hash, entries) = query
126        .verify_distinct_count_proof(&proof.grovedb_proof, limit, left_to_right, platform_version)
127        .map_drive_error(proof, mtd)?;
128
129    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
130
131    Ok(entries)
132}
133
134/// Verify a grovedb point-lookup count proof against a
135/// `countable: true` index and return the per-branch entries.
136///
137/// Thin tenderdash-composition wrapper over
138/// [`DriveDocumentCountQuery::verify_point_lookup_count_proof`] in
139/// rs-drive (which does the merk-level verification and walks the
140/// verified elements to extract `count_value`).
141///
142/// ## Entry shape
143///
144/// The verifier walks grovedb's
145/// `(path, key, Option<Element>)` triples and emits one
146/// [`SplitCountEntry`] per **present** queried key. The current
147/// path-query shape does NOT set
148/// `absence_proofs_for_non_existing_searched_keys: true`, so absent
149/// branches are silently omitted from grovedb's elements stream
150/// rather than surfaced as `(path, key, None)` triples.
151///
152/// - **Equal-only, fully covered**: zero or one entry. One entry
153///   with empty `key` and `count: Some(n)` if the covered branch
154///   exists; no entries at all if the branch is absent.
155/// - **Equal prefix + `In` on last property**: one entry per
156///   **present** queried In value, with
157///   `key = <serialized_in_value>` and `count: Some(n)`. Absent In
158///   values are omitted from the returned list. Callers that need
159///   to distinguish "verified with n docs" from "queried but
160///   absent" diff their request's In array against the returned
161///   entries by `key`.
162///
163/// The `count: Option<u64>` field's `None` variant is reserved for a
164/// future variant that flips `absence_proofs_for_non_existing_searched_keys`
165/// — see [`SplitCountEntry::count`] and
166/// [`DriveDocumentCountQuery::verify_point_lookup_count_proof`] for
167/// the forward-compat path.
168///
169/// ## Replaces materialize-and-count
170///
171/// Before this primitive landed, prove count queries with no range
172/// clause used `DriveDocumentQuery::execute_with_proof` to prove
173/// every matching document and counted them client-side. That path
174/// scaled with matching docs and was capped at `u16::MAX`. The
175/// CountTree element proof is O(k × log n) where k is the number of
176/// covered branches — bandwidth and CPU drop by orders of magnitude
177/// on counted indexes and the cap disappears.
178pub fn verify_point_lookup_count_proof(
179    query: &DriveDocumentCountQuery,
180    proof: &Proof,
181    mtd: &ResponseMetadata,
182    platform_version: &PlatformVersion,
183    provider: &dyn ContextProvider,
184) -> Result<Vec<SplitCountEntry>, Error> {
185    let (root_hash, entries) = query
186        .verify_point_lookup_count_proof(&proof.grovedb_proof, platform_version)
187        .map_drive_error(proof, mtd)?;
188
189    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
190
191    Ok(entries)
192}
193
194/// Verify a grovedb proof of the document type's primary-key
195/// `CountTree` element and return the unfiltered total count.
196///
197/// Thin tenderdash-composition wrapper over
198/// [`DriveDocumentCountQuery::verify_primary_key_count_tree_proof`].
199/// Used by the prove path's `documents_countable: true` fast path —
200/// when the where clauses are empty and the document type has
201/// `documents_countable: true`, the server proves the type-level
202/// CountTree element directly and the SDK extracts the count from
203/// the verified element.
204pub fn verify_primary_key_count_tree_proof(
205    contract_id: [u8; 32],
206    document_type_name: &str,
207    proof: &Proof,
208    mtd: &ResponseMetadata,
209    platform_version: &PlatformVersion,
210    provider: &dyn ContextProvider,
211) -> Result<u64, Error> {
212    let (root_hash, count) = DriveDocumentCountQuery::verify_primary_key_count_tree_proof(
213        &proof.grovedb_proof,
214        contract_id,
215        document_type_name,
216        platform_version,
217    )
218    .map_drive_error(proof, mtd)?;
219
220    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
221
222    Ok(count)
223}
224
225#[cfg(test)]
226mod tests {
227    //! Local-only tests for parts of this module that don't need a
228    //! populated Drive. The full happy-path verification of
229    //! `verify_aggregate_count_proof` / `verify_distinct_count_proof`
230    //! is covered end-to-end in the drive crate's
231    //! `range_countable_index_e2e_tests` (where the prover and
232    //! verifier roundtrip on a real Drive), and in the rs-sdk
233    //! integration tests. Here we cover the error-mapping branch
234    //! for garbage proof bytes: the rs-drive verify call fails, and
235    //! the `MapGroveDbError` adapter must thread the grovedb error
236    //! into our `Error::GroveDBError` variant with the right
237    //! correlation fields (proof_bytes, height, time_ms).
238    use super::*;
239    use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
240    use dash_context_provider::ContextProviderError;
241    use dpp::data_contract::TokenConfiguration;
242    use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
243    use std::sync::Arc;
244
245    /// Provider that panics if called — the GroveDBError path
246    /// short-circuits before reaching tenderdash verification, so
247    /// the provider must never be touched by these tests.
248    struct UnreachableProvider;
249
250    impl ContextProvider for UnreachableProvider {
251        fn get_data_contract(
252            &self,
253            _id: &Identifier,
254            _pv: &PlatformVersion,
255        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
256            panic!("should not be called")
257        }
258        fn get_token_configuration(
259            &self,
260            _id: &Identifier,
261        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
262            panic!("should not be called")
263        }
264        fn get_quorum_public_key(
265            &self,
266            _qt: u32,
267            _qh: [u8; 32],
268            _h: u32,
269        ) -> Result<[u8; 48], ContextProviderError> {
270            panic!("should not be called")
271        }
272        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
273            panic!("should not be called")
274        }
275    }
276
277    fn arbitrary_metadata() -> ResponseMetadata {
278        ResponseMetadata {
279            height: 1,
280            time_ms: 0,
281            ..Default::default()
282        }
283    }
284
285    #[test]
286    fn split_count_entry_struct_constructs_and_clones() {
287        // Pins the `SplitCountEntry` public-API shape (Clone + Eq +
288        // per-field accessors). The struct now lives in rs-drive and
289        // is re-exported from drive-proof-verifier, but SDK callers
290        // pattern-match on it heavily, so a stable derivation set is
291        // load-bearing for the API surface.
292        let a = SplitCountEntry {
293            in_key: Some(b"acme".to_vec()),
294            key: b"red".to_vec(),
295            count: Some(42),
296        };
297        let b = a.clone();
298        assert_eq!(a, b);
299        assert_eq!(a.in_key.as_deref(), Some(b"acme".as_slice()));
300        assert_eq!(a.key, b"red".to_vec());
301        assert_eq!(a.count, Some(42));
302
303        let flat = SplitCountEntry {
304            in_key: None,
305            key: b"green".to_vec(),
306            count: Some(7),
307        };
308        assert!(flat.in_key.is_none());
309
310        // Inequality across each field.
311        let different_in_key = SplitCountEntry {
312            in_key: Some(b"contoso".to_vec()),
313            ..a.clone()
314        };
315        assert_ne!(a, different_in_key);
316        let different_key = SplitCountEntry {
317            key: b"blue".to_vec(),
318            ..a.clone()
319        };
320        assert_ne!(a, different_key);
321        let different_count = SplitCountEntry {
322            count: Some(99),
323            ..a
324        };
325        assert_ne!(b, different_count);
326    }
327
328    /// Tests for the error-mapping path require a real
329    /// `DriveDocumentCountQuery` (the new API takes the query rather
330    /// than a pre-built path query). Constructing one needs a
331    /// `DocumentTypeRef` + `Index` which require dpp/fixtures-and-
332    /// mocks. The error-mapping is exercised end-to-end by the
333    /// drive crate's range_countable_index_e2e_tests instead.
334    ///
335    /// What we can pin here: the wrappers are thin enough that
336    /// running them isn't more interesting than running the
337    /// underlying rs-drive verify methods. The structural test
338    /// above is the load-bearing guarantee for the public API.
339    #[test]
340    fn proof_metadata_helper_round_trips() {
341        // Defense-in-depth: the wrappers carry `Proof` and
342        // `ResponseMetadata` through `MapGroveDbError`. Pin that
343        // the helper types are constructible with the fields we
344        // depend on (height, time_ms, grovedb_proof) so a future
345        // dapi-grpc refactor that renames any of them fails this
346        // test in addition to breaking the call sites in this file.
347        let proof = Proof {
348            grovedb_proof: vec![0xab, 0xcd],
349            ..Default::default()
350        };
351        let mtd = arbitrary_metadata();
352        assert_eq!(proof.grovedb_proof, vec![0xab, 0xcd]);
353        assert_eq!(mtd.height, 1);
354        assert_eq!(mtd.time_ms, 0);
355
356        // Touch the provider type so unused-import linters don't
357        // strip it (it's not used by other assertions in this
358        // module).
359        let _provider: &dyn ContextProvider = &UnreachableProvider;
360    }
361}