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}