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}