Skip to main content

drive_proof_verifier/proof/
document_count.rs

1use crate::error::MapGroveDbError;
2use crate::verify::{document_query_proof_bytes, 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(
49                document_query_proof_bytes(proof, mtd, platform_version)?,
50                platform_version,
51            )
52            .map_drive_error(proof, mtd)?;
53
54        let count = documents.len() as u64;
55
56        verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
57
58        Ok((Some(DocumentCount(count)), mtd.clone(), proof.clone()))
59    }
60}
61
62/// Verify a grovedb `AggregateCountOnRange` proof and the surrounding
63/// tenderdash commit, returning the verified document count.
64///
65/// Thin tenderdash-composition wrapper over
66/// [`DriveDocumentCountQuery::verify_aggregate_count_proof`] in
67/// rs-drive (which does the merk-level verification). Both helpers
68/// reuse the prover's `aggregate_count_path_query` internally so the
69/// path query bytes match byte-for-byte and the merk root
70/// recomputation succeeds; the caller passes the `query` struct
71/// itself rather than a pre-built `PathQuery`, removing a step
72/// where the SDK and server could drift.
73///
74/// Counterpart to the materialize-and-count path in
75/// [`FromProof<DriveDocumentQuery> for DocumentCount`] above: where
76/// that one verifies a regular grovedb proof that yields concrete
77/// documents and counts them client-side, this verifies the
78/// merk-level aggregate primitive that yields a single `u64`
79/// directly (capped only by the merk tree size, not `u16::MAX`).
80///
81/// `platform_version` must be the protocol version `mtd` names, or the latest known
82/// version when `mtd` names a newer one; otherwise the answer is refused with
83/// [`Error::ProofProtocolVersionMismatch`].
84pub fn verify_aggregate_count_proof(
85    query: &DriveDocumentCountQuery,
86    proof: &Proof,
87    mtd: &ResponseMetadata,
88    platform_version: &PlatformVersion,
89    provider: &dyn ContextProvider,
90) -> Result<u64, Error> {
91    let (root_hash, count) = query
92        .verify_aggregate_count_proof(
93            document_query_proof_bytes(proof, mtd, platform_version)?,
94            platform_version,
95        )
96        .map_drive_error(proof, mtd)?;
97
98    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
99
100    Ok(count)
101}
102
103/// Verify a regular grovedb range proof against a `ProvableCountTree`
104/// and the surrounding tenderdash commit, returning the verified
105/// per-`(in_key, key)` counts the proof commits to.
106///
107/// Thin tenderdash-composition wrapper over
108/// [`DriveDocumentCountQuery::verify_distinct_count_proof`] in
109/// rs-drive (which does the merk-level verification and the
110/// in_key extraction from `(path, key, element)` triples).
111///
112/// ## No cross-fork merge
113///
114/// For compound queries (an `In` clause on a prefix property) each
115/// returned [`SplitCountEntry`] retains its `in_key` (the In value
116/// for that fork) alongside the terminator `key`. Cross-fork
117/// aggregation is intentionally NOT done here — see
118/// [`SplitCountEntry`]'s doc for the rationale.
119///
120/// ## Trade-off vs. the aggregate path
121///
122/// Proof size is O(distinct `(in_key, terminator)` pairs matched)
123/// rather than O(log n), because each distinct in-range pair emits
124/// its own `KVCount` op instead of being collapsed into a boundary
125/// subtree. Still strictly smaller than materialize-and-count.
126///
127/// `platform_version` must be the protocol version `mtd` names, or the latest known
128/// version when `mtd` names a newer one; otherwise the answer is refused with
129/// [`Error::ProofProtocolVersionMismatch`].
130pub fn verify_distinct_count_proof(
131    query: &DriveDocumentCountQuery,
132    proof: &Proof,
133    mtd: &ResponseMetadata,
134    limit: u16,
135    left_to_right: bool,
136    platform_version: &PlatformVersion,
137    provider: &dyn ContextProvider,
138) -> Result<Vec<SplitCountEntry>, Error> {
139    let (root_hash, entries) = query
140        .verify_distinct_count_proof(
141            document_query_proof_bytes(proof, mtd, platform_version)?,
142            limit,
143            left_to_right,
144            platform_version,
145        )
146        .map_drive_error(proof, mtd)?;
147
148    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
149
150    Ok(entries)
151}
152
153/// Verify a grovedb point-lookup count proof against a
154/// `countable: true` index and return the per-branch entries.
155///
156/// Thin tenderdash-composition wrapper over
157/// [`DriveDocumentCountQuery::verify_point_lookup_count_proof`] in
158/// rs-drive (which does the merk-level verification and walks the
159/// verified elements to extract `count_value`).
160///
161/// ## Entry shape
162///
163/// The verifier walks grovedb's
164/// `(path, key, Option<Element>)` triples and emits one
165/// [`SplitCountEntry`] per **present** queried key. The current
166/// path-query shape does NOT set
167/// `absence_proofs_for_non_existing_searched_keys: true`, so absent
168/// branches are silently omitted from grovedb's elements stream
169/// rather than surfaced as `(path, key, None)` triples.
170///
171/// - **Equal-only, fully covered**: zero or one entry. One entry
172///   with empty `key` and `count: Some(n)` if the covered branch
173///   exists; no entries at all if the branch is absent.
174/// - **Equal prefix + `In` on last property**: one entry per
175///   **present** queried In value, with
176///   `key = <serialized_in_value>` and `count: Some(n)`. Absent In
177///   values are omitted from the returned list. Callers that need
178///   to distinguish "verified with n docs" from "queried but
179///   absent" diff their request's In array against the returned
180///   entries by `key`.
181///
182/// The `count: Option<u64>` field's `None` variant is reserved for a
183/// future variant that flips `absence_proofs_for_non_existing_searched_keys`
184/// — see [`SplitCountEntry::count`] and
185/// [`DriveDocumentCountQuery::verify_point_lookup_count_proof`] for
186/// the forward-compat path.
187///
188/// ## Replaces materialize-and-count
189///
190/// Before this primitive landed, prove count queries with no range
191/// clause used `DriveDocumentQuery::execute_with_proof` to prove
192/// every matching document and counted them client-side. That path
193/// scaled with matching docs and was capped at `u16::MAX`. The
194/// CountTree element proof is O(k × log n) where k is the number of
195/// covered branches — bandwidth and CPU drop by orders of magnitude
196/// on counted indexes and the cap disappears.
197///
198/// `platform_version` must be the protocol version `mtd` names, or the latest known
199/// version when `mtd` names a newer one; otherwise the answer is refused with
200/// [`Error::ProofProtocolVersionMismatch`].
201pub fn verify_point_lookup_count_proof(
202    query: &DriveDocumentCountQuery,
203    proof: &Proof,
204    mtd: &ResponseMetadata,
205    platform_version: &PlatformVersion,
206    provider: &dyn ContextProvider,
207) -> Result<Vec<SplitCountEntry>, Error> {
208    let (root_hash, entries) = query
209        .verify_point_lookup_count_proof(
210            document_query_proof_bytes(proof, mtd, platform_version)?,
211            platform_version,
212        )
213        .map_drive_error(proof, mtd)?;
214
215    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
216
217    Ok(entries)
218}
219
220/// Verify a grovedb proof of the document type's primary-key
221/// `CountTree` element and return the unfiltered total count.
222///
223/// Thin tenderdash-composition wrapper over
224/// [`DriveDocumentCountQuery::verify_primary_key_count_tree_proof`].
225/// Used by the prove path's `documents_countable: true` fast path —
226/// when the where clauses are empty and the document type has
227/// `documents_countable: true`, the server proves the type-level
228/// CountTree element directly and the SDK extracts the count from
229/// the verified element.
230///
231/// `platform_version` must be the protocol version `mtd` names, or the latest known
232/// version when `mtd` names a newer one; otherwise the answer is refused with
233/// [`Error::ProofProtocolVersionMismatch`].
234pub fn verify_primary_key_count_tree_proof(
235    contract_id: [u8; 32],
236    document_type_name: &str,
237    proof: &Proof,
238    mtd: &ResponseMetadata,
239    platform_version: &PlatformVersion,
240    provider: &dyn ContextProvider,
241) -> Result<u64, Error> {
242    let (root_hash, count) = DriveDocumentCountQuery::verify_primary_key_count_tree_proof(
243        document_query_proof_bytes(proof, mtd, platform_version)?,
244        contract_id,
245        document_type_name,
246        platform_version,
247    )
248    .map_drive_error(proof, mtd)?;
249
250    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
251
252    Ok(count)
253}
254
255/// Verify a **carrier** `AggregateCountOnRange` proof against a
256/// `rangeCountable: true` index and return the per-`In`-branch
257/// counts.
258///
259/// Thin tenderdash-composition wrapper over
260/// [`DriveDocumentCountQuery::verify_carrier_aggregate_count_proof`]
261/// in rs-drive. Used by the prove path when the request shape
262/// is `select=COUNT, group_by=[in_field], where = In(in_field) +
263/// range(other_field), prove=true` — drive's `detect_mode` routes
264/// that shape to `DocumentCountMode::RangeAggregateCarrierProof`
265/// (grovedb PR #663's carrier-ACOR primitive), which collapses
266/// each In branch's range into a single committed `u64` rather
267/// than emitting per-distinct-key entries. Result is one
268/// [`SplitCountEntry`] per **present** In branch:
269/// `in_key = <serialized In value>`, `key = []` (no terminator —
270/// the count is for the whole range slice under that In branch),
271/// `count = Some(n)`. Absent In branches are omitted; callers
272/// that need to surface "queried but absent" diff their In array
273/// against the returned `in_key`s.
274///
275/// ## Trade-off vs. `verify_distinct_count_proof`
276///
277/// Both shapes verify range-count queries with an In on the
278/// prefix. The distinct variant emits one `KVCount` op per
279/// `(in_key, range_key)` pair — proof size scales with the
280/// number of distinct values matched. The carrier variant emits
281/// one `u64` per In branch — proof size scales with `|In|`,
282/// independent of how many distinct range values each branch
283/// covers. Drive picks between them based on whether the caller
284/// asked for distinct entries (`GroupByCompound`) or per-In
285/// aggregates (`GroupByIn`).
286///
287/// ## Limit semantics
288///
289/// `limit: Option<u16>` mirrors the prover's `SizedQuery::limit`
290/// — caps the per-branch carrier walk. The verifier
291/// reconstructs the same path query bytes from `(query, limit)`,
292/// so the value passed here must match what the server used to
293/// generate the proof (validate-don't-clamp on the prove path,
294/// same contract as `verify_distinct_count_proof`).
295///
296/// `platform_version` must be the protocol version `mtd` names, or the latest known
297/// version when `mtd` names a newer one; otherwise the answer is refused with
298/// [`Error::ProofProtocolVersionMismatch`].
299pub fn verify_carrier_aggregate_count_proof(
300    query: &DriveDocumentCountQuery,
301    proof: &Proof,
302    mtd: &ResponseMetadata,
303    limit: Option<u16>,
304    left_to_right: bool,
305    platform_version: &PlatformVersion,
306    provider: &dyn ContextProvider,
307) -> Result<Vec<SplitCountEntry>, Error> {
308    let (root_hash, per_key_counts) = query
309        .verify_carrier_aggregate_count_proof(
310            document_query_proof_bytes(proof, mtd, platform_version)?,
311            limit,
312            left_to_right,
313            platform_version,
314        )
315        .map_drive_error(proof, mtd)?;
316
317    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
318
319    // Map drive's `Vec<(Vec<u8>, u64)>` carrier shape onto the
320    // SDK's `Vec<SplitCountEntry>` so the call sites can stay
321    // uniform across `verify_distinct_count_proof` /
322    // `verify_point_lookup_count_proof` / this. `key` is empty
323    // because the carrier variant doesn't emit terminator keys —
324    // each entry's `in_key` is the only routable handle.
325    let entries = per_key_counts
326        .into_iter()
327        .map(|(in_key, count)| SplitCountEntry {
328            in_key: Some(in_key),
329            key: Vec::new(),
330            count: Some(count),
331        })
332        .collect();
333    Ok(entries)
334}
335
336#[cfg(test)]
337mod tests {
338    //! Local-only tests for parts of this module that don't need a
339    //! populated Drive. The full happy-path verification of
340    //! `verify_aggregate_count_proof` / `verify_distinct_count_proof`
341    //! is covered end-to-end in the drive crate's
342    //! `range_countable_index_e2e_tests` (where the prover and
343    //! verifier roundtrip on a real Drive), and in the rs-sdk
344    //! integration tests. Here we cover the error-mapping branch
345    //! for garbage proof bytes: the rs-drive verify call fails, and
346    //! the `MapGroveDbError` adapter must thread the grovedb error
347    //! into our `Error::GroveDBError` variant with the right
348    //! correlation fields (proof_bytes, height, time_ms).
349    use super::*;
350    use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
351    use dash_context_provider::ContextProviderError;
352    use dpp::data_contract::TokenConfiguration;
353    use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
354    use std::sync::Arc;
355
356    /// Provider that panics if called — the GroveDBError path
357    /// short-circuits before reaching tenderdash verification, so
358    /// the provider must never be touched by these tests.
359    struct UnreachableProvider;
360
361    impl ContextProvider for UnreachableProvider {
362        fn get_data_contract(
363            &self,
364            _id: &Identifier,
365            _pv: &PlatformVersion,
366        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
367            panic!("should not be called")
368        }
369        fn get_token_configuration(
370            &self,
371            _id: &Identifier,
372        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
373            panic!("should not be called")
374        }
375        fn get_quorum_public_key(
376            &self,
377            _qt: u32,
378            _qh: [u8; 32],
379            _h: u32,
380        ) -> Result<[u8; 48], ContextProviderError> {
381            panic!("should not be called")
382        }
383        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
384            panic!("should not be called")
385        }
386    }
387
388    fn arbitrary_metadata() -> ResponseMetadata {
389        ResponseMetadata {
390            height: 1,
391            time_ms: 0,
392            ..Default::default()
393        }
394    }
395
396    #[test]
397    fn split_count_entry_struct_constructs_and_clones() {
398        // Pins the `SplitCountEntry` public-API shape (Clone + Eq +
399        // per-field accessors). The struct now lives in rs-drive and
400        // is re-exported from drive-proof-verifier, but SDK callers
401        // pattern-match on it heavily, so a stable derivation set is
402        // load-bearing for the API surface.
403        let a = SplitCountEntry {
404            in_key: Some(b"acme".to_vec()),
405            key: b"red".to_vec(),
406            count: Some(42),
407        };
408        let b = a.clone();
409        assert_eq!(a, b);
410        assert_eq!(a.in_key.as_deref(), Some(b"acme".as_slice()));
411        assert_eq!(a.key, b"red".to_vec());
412        assert_eq!(a.count, Some(42));
413
414        let flat = SplitCountEntry {
415            in_key: None,
416            key: b"green".to_vec(),
417            count: Some(7),
418        };
419        assert!(flat.in_key.is_none());
420
421        // Inequality across each field.
422        let different_in_key = SplitCountEntry {
423            in_key: Some(b"contoso".to_vec()),
424            ..a.clone()
425        };
426        assert_ne!(a, different_in_key);
427        let different_key = SplitCountEntry {
428            key: b"blue".to_vec(),
429            ..a.clone()
430        };
431        assert_ne!(a, different_key);
432        let different_count = SplitCountEntry {
433            count: Some(99),
434            ..a
435        };
436        assert_ne!(b, different_count);
437    }
438
439    /// Tests for the error-mapping path require a real
440    /// `DriveDocumentCountQuery` (the new API takes the query rather
441    /// than a pre-built path query). Constructing one needs a
442    /// `DocumentTypeRef` + `Index` which require dpp/fixtures-and-
443    /// mocks. The error-mapping is exercised end-to-end by the
444    /// drive crate's range_countable_index_e2e_tests instead.
445    ///
446    /// What we can pin here: the wrappers are thin enough that
447    /// running them isn't more interesting than running the
448    /// underlying rs-drive verify methods. The structural test
449    /// above is the load-bearing guarantee for the public API.
450    #[test]
451    fn proof_metadata_helper_round_trips() {
452        // Defense-in-depth: the wrappers carry `Proof` and
453        // `ResponseMetadata` through `MapGroveDbError`. Pin that
454        // the helper types are constructible with the fields we
455        // depend on (height, time_ms, grovedb_proof) so a future
456        // dapi-grpc refactor that renames any of them fails this
457        // test in addition to breaking the call sites in this file.
458        let proof = Proof {
459            grovedb_proof: vec![0xab, 0xcd],
460            ..Default::default()
461        };
462        let mtd = arbitrary_metadata();
463        assert_eq!(proof.grovedb_proof, vec![0xab, 0xcd]);
464        assert_eq!(mtd.height, 1);
465        assert_eq!(mtd.time_ms, 0);
466
467        // Touch the provider type so unused-import linters don't
468        // strip it (it's not used by other assertions in this
469        // module).
470        let _provider: &dyn ContextProvider = &UnreachableProvider;
471    }
472}