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#[cfg(test)]
226mod tests {
227 //! Local-only tests for parts of this module that don't need a
228 //! populated Drive. The full happy-path verification of
229 //! `verify_aggregate_count_proof` / `verify_distinct_count_proof`
230 //! is covered end-to-end in the drive crate's
231 //! `range_countable_index_e2e_tests` (where the prover and
232 //! verifier roundtrip on a real Drive), and in the rs-sdk
233 //! integration tests. Here we cover the error-mapping branch
234 //! for garbage proof bytes: the rs-drive verify call fails, and
235 //! the `MapGroveDbError` adapter must thread the grovedb error
236 //! into our `Error::GroveDBError` variant with the right
237 //! correlation fields (proof_bytes, height, time_ms).
238 use super::*;
239 use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
240 use dash_context_provider::ContextProviderError;
241 use dpp::data_contract::TokenConfiguration;
242 use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
243 use std::sync::Arc;
244
245 /// Provider that panics if called — the GroveDBError path
246 /// short-circuits before reaching tenderdash verification, so
247 /// the provider must never be touched by these tests.
248 struct UnreachableProvider;
249
250 impl ContextProvider for UnreachableProvider {
251 fn get_data_contract(
252 &self,
253 _id: &Identifier,
254 _pv: &PlatformVersion,
255 ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
256 panic!("should not be called")
257 }
258 fn get_token_configuration(
259 &self,
260 _id: &Identifier,
261 ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
262 panic!("should not be called")
263 }
264 fn get_quorum_public_key(
265 &self,
266 _qt: u32,
267 _qh: [u8; 32],
268 _h: u32,
269 ) -> Result<[u8; 48], ContextProviderError> {
270 panic!("should not be called")
271 }
272 fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
273 panic!("should not be called")
274 }
275 }
276
277 fn arbitrary_metadata() -> ResponseMetadata {
278 ResponseMetadata {
279 height: 1,
280 time_ms: 0,
281 ..Default::default()
282 }
283 }
284
285 #[test]
286 fn split_count_entry_struct_constructs_and_clones() {
287 // Pins the `SplitCountEntry` public-API shape (Clone + Eq +
288 // per-field accessors). The struct now lives in rs-drive and
289 // is re-exported from drive-proof-verifier, but SDK callers
290 // pattern-match on it heavily, so a stable derivation set is
291 // load-bearing for the API surface.
292 let a = SplitCountEntry {
293 in_key: Some(b"acme".to_vec()),
294 key: b"red".to_vec(),
295 count: Some(42),
296 };
297 let b = a.clone();
298 assert_eq!(a, b);
299 assert_eq!(a.in_key.as_deref(), Some(b"acme".as_slice()));
300 assert_eq!(a.key, b"red".to_vec());
301 assert_eq!(a.count, Some(42));
302
303 let flat = SplitCountEntry {
304 in_key: None,
305 key: b"green".to_vec(),
306 count: Some(7),
307 };
308 assert!(flat.in_key.is_none());
309
310 // Inequality across each field.
311 let different_in_key = SplitCountEntry {
312 in_key: Some(b"contoso".to_vec()),
313 ..a.clone()
314 };
315 assert_ne!(a, different_in_key);
316 let different_key = SplitCountEntry {
317 key: b"blue".to_vec(),
318 ..a.clone()
319 };
320 assert_ne!(a, different_key);
321 let different_count = SplitCountEntry {
322 count: Some(99),
323 ..a
324 };
325 assert_ne!(b, different_count);
326 }
327
328 /// Tests for the error-mapping path require a real
329 /// `DriveDocumentCountQuery` (the new API takes the query rather
330 /// than a pre-built path query). Constructing one needs a
331 /// `DocumentTypeRef` + `Index` which require dpp/fixtures-and-
332 /// mocks. The error-mapping is exercised end-to-end by the
333 /// drive crate's range_countable_index_e2e_tests instead.
334 ///
335 /// What we can pin here: the wrappers are thin enough that
336 /// running them isn't more interesting than running the
337 /// underlying rs-drive verify methods. The structural test
338 /// above is the load-bearing guarantee for the public API.
339 #[test]
340 fn proof_metadata_helper_round_trips() {
341 // Defense-in-depth: the wrappers carry `Proof` and
342 // `ResponseMetadata` through `MapGroveDbError`. Pin that
343 // the helper types are constructible with the fields we
344 // depend on (height, time_ms, grovedb_proof) so a future
345 // dapi-grpc refactor that renames any of them fails this
346 // test in addition to breaking the call sites in this file.
347 let proof = Proof {
348 grovedb_proof: vec![0xab, 0xcd],
349 ..Default::default()
350 };
351 let mtd = arbitrary_metadata();
352 assert_eq!(proof.grovedb_proof, vec![0xab, 0xcd]);
353 assert_eq!(mtd.height, 1);
354 assert_eq!(mtd.time_ms, 0);
355
356 // Touch the provider type so unused-import linters don't
357 // strip it (it's not used by other assertions in this
358 // module).
359 let _provider: &dyn ContextProvider = &UnreachableProvider;
360 }
361}