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/// Verify a **carrier** `AggregateCountOnRange` proof against a
226/// `rangeCountable: true` index and return the per-`In`-branch
227/// counts.
228///
229/// Thin tenderdash-composition wrapper over
230/// [`DriveDocumentCountQuery::verify_carrier_aggregate_count_proof`]
231/// in rs-drive. Used by the prove path when the request shape
232/// is `select=COUNT, group_by=[in_field], where = In(in_field) +
233/// range(other_field), prove=true` — drive's `detect_mode` routes
234/// that shape to `DocumentCountMode::RangeAggregateCarrierProof`
235/// (grovedb PR #663's carrier-ACOR primitive), which collapses
236/// each In branch's range into a single committed `u64` rather
237/// than emitting per-distinct-key entries. Result is one
238/// [`SplitCountEntry`] per **present** In branch:
239/// `in_key = <serialized In value>`, `key = []` (no terminator —
240/// the count is for the whole range slice under that In branch),
241/// `count = Some(n)`. Absent In branches are omitted; callers
242/// that need to surface "queried but absent" diff their In array
243/// against the returned `in_key`s.
244///
245/// ## Trade-off vs. `verify_distinct_count_proof`
246///
247/// Both shapes verify range-count queries with an In on the
248/// prefix. The distinct variant emits one `KVCount` op per
249/// `(in_key, range_key)` pair — proof size scales with the
250/// number of distinct values matched. The carrier variant emits
251/// one `u64` per In branch — proof size scales with `|In|`,
252/// independent of how many distinct range values each branch
253/// covers. Drive picks between them based on whether the caller
254/// asked for distinct entries (`GroupByCompound`) or per-In
255/// aggregates (`GroupByIn`).
256///
257/// ## Limit semantics
258///
259/// `limit: Option<u16>` mirrors the prover's `SizedQuery::limit`
260/// — caps the per-branch carrier walk. The verifier
261/// reconstructs the same path query bytes from `(query, limit)`,
262/// so the value passed here must match what the server used to
263/// generate the proof (validate-don't-clamp on the prove path,
264/// same contract as `verify_distinct_count_proof`).
265pub fn verify_carrier_aggregate_count_proof(
266    query: &DriveDocumentCountQuery,
267    proof: &Proof,
268    mtd: &ResponseMetadata,
269    limit: Option<u16>,
270    left_to_right: bool,
271    platform_version: &PlatformVersion,
272    provider: &dyn ContextProvider,
273) -> Result<Vec<SplitCountEntry>, Error> {
274    let (root_hash, per_key_counts) = query
275        .verify_carrier_aggregate_count_proof(
276            &proof.grovedb_proof,
277            limit,
278            left_to_right,
279            platform_version,
280        )
281        .map_drive_error(proof, mtd)?;
282
283    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
284
285    // Map drive's `Vec<(Vec<u8>, u64)>` carrier shape onto the
286    // SDK's `Vec<SplitCountEntry>` so the call sites can stay
287    // uniform across `verify_distinct_count_proof` /
288    // `verify_point_lookup_count_proof` / this. `key` is empty
289    // because the carrier variant doesn't emit terminator keys —
290    // each entry's `in_key` is the only routable handle.
291    let entries = per_key_counts
292        .into_iter()
293        .map(|(in_key, count)| SplitCountEntry {
294            in_key: Some(in_key),
295            key: Vec::new(),
296            count: Some(count),
297        })
298        .collect();
299    Ok(entries)
300}
301
302#[cfg(test)]
303mod tests {
304    //! Local-only tests for parts of this module that don't need a
305    //! populated Drive. The full happy-path verification of
306    //! `verify_aggregate_count_proof` / `verify_distinct_count_proof`
307    //! is covered end-to-end in the drive crate's
308    //! `range_countable_index_e2e_tests` (where the prover and
309    //! verifier roundtrip on a real Drive), and in the rs-sdk
310    //! integration tests. Here we cover the error-mapping branch
311    //! for garbage proof bytes: the rs-drive verify call fails, and
312    //! the `MapGroveDbError` adapter must thread the grovedb error
313    //! into our `Error::GroveDBError` variant with the right
314    //! correlation fields (proof_bytes, height, time_ms).
315    use super::*;
316    use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
317    use dash_context_provider::ContextProviderError;
318    use dpp::data_contract::TokenConfiguration;
319    use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
320    use std::sync::Arc;
321
322    /// Provider that panics if called — the GroveDBError path
323    /// short-circuits before reaching tenderdash verification, so
324    /// the provider must never be touched by these tests.
325    struct UnreachableProvider;
326
327    impl ContextProvider for UnreachableProvider {
328        fn get_data_contract(
329            &self,
330            _id: &Identifier,
331            _pv: &PlatformVersion,
332        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
333            panic!("should not be called")
334        }
335        fn get_token_configuration(
336            &self,
337            _id: &Identifier,
338        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
339            panic!("should not be called")
340        }
341        fn get_quorum_public_key(
342            &self,
343            _qt: u32,
344            _qh: [u8; 32],
345            _h: u32,
346        ) -> Result<[u8; 48], ContextProviderError> {
347            panic!("should not be called")
348        }
349        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
350            panic!("should not be called")
351        }
352    }
353
354    fn arbitrary_metadata() -> ResponseMetadata {
355        ResponseMetadata {
356            height: 1,
357            time_ms: 0,
358            ..Default::default()
359        }
360    }
361
362    #[test]
363    fn split_count_entry_struct_constructs_and_clones() {
364        // Pins the `SplitCountEntry` public-API shape (Clone + Eq +
365        // per-field accessors). The struct now lives in rs-drive and
366        // is re-exported from drive-proof-verifier, but SDK callers
367        // pattern-match on it heavily, so a stable derivation set is
368        // load-bearing for the API surface.
369        let a = SplitCountEntry {
370            in_key: Some(b"acme".to_vec()),
371            key: b"red".to_vec(),
372            count: Some(42),
373        };
374        let b = a.clone();
375        assert_eq!(a, b);
376        assert_eq!(a.in_key.as_deref(), Some(b"acme".as_slice()));
377        assert_eq!(a.key, b"red".to_vec());
378        assert_eq!(a.count, Some(42));
379
380        let flat = SplitCountEntry {
381            in_key: None,
382            key: b"green".to_vec(),
383            count: Some(7),
384        };
385        assert!(flat.in_key.is_none());
386
387        // Inequality across each field.
388        let different_in_key = SplitCountEntry {
389            in_key: Some(b"contoso".to_vec()),
390            ..a.clone()
391        };
392        assert_ne!(a, different_in_key);
393        let different_key = SplitCountEntry {
394            key: b"blue".to_vec(),
395            ..a.clone()
396        };
397        assert_ne!(a, different_key);
398        let different_count = SplitCountEntry {
399            count: Some(99),
400            ..a
401        };
402        assert_ne!(b, different_count);
403    }
404
405    /// Tests for the error-mapping path require a real
406    /// `DriveDocumentCountQuery` (the new API takes the query rather
407    /// than a pre-built path query). Constructing one needs a
408    /// `DocumentTypeRef` + `Index` which require dpp/fixtures-and-
409    /// mocks. The error-mapping is exercised end-to-end by the
410    /// drive crate's range_countable_index_e2e_tests instead.
411    ///
412    /// What we can pin here: the wrappers are thin enough that
413    /// running them isn't more interesting than running the
414    /// underlying rs-drive verify methods. The structural test
415    /// above is the load-bearing guarantee for the public API.
416    #[test]
417    fn proof_metadata_helper_round_trips() {
418        // Defense-in-depth: the wrappers carry `Proof` and
419        // `ResponseMetadata` through `MapGroveDbError`. Pin that
420        // the helper types are constructible with the fields we
421        // depend on (height, time_ms, grovedb_proof) so a future
422        // dapi-grpc refactor that renames any of them fails this
423        // test in addition to breaking the call sites in this file.
424        let proof = Proof {
425            grovedb_proof: vec![0xab, 0xcd],
426            ..Default::default()
427        };
428        let mtd = arbitrary_metadata();
429        assert_eq!(proof.grovedb_proof, vec![0xab, 0xcd]);
430        assert_eq!(mtd.height, 1);
431        assert_eq!(mtd.time_ms, 0);
432
433        // Touch the provider type so unused-import linters don't
434        // strip it (it's not used by other assertions in this
435        // module).
436        let _provider: &dyn ContextProvider = &UnreachableProvider;
437    }
438}