Skip to main content

drive_proof_verifier/proof/
document_count.rs

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