drive_proof_verifier/proof/document_split_count.rs
1use crate::{ContextProvider, Error, FromProof};
2use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
3use dpp::dashcore::Network;
4use dpp::version::PlatformVersion;
5use drive::query::{DriveDocumentQuery, SplitCountEntry};
6use std::collections::BTreeMap;
7
8/// The split counts of documents matching a query, verified from proof.
9///
10/// Each entry carries the serialized split-property value (`key`) as
11/// produced by
12/// [`DocumentTypeBasicMethods::serialize_value_for_key`], the verified
13/// `count`, and an optional `in_key` carrying the In-prefix value for
14/// compound range-distinct queries (see the [`SplitCountEntry`]
15/// doc for rationale on why compound results stay unmerged).
16///
17/// For flat queries (per-`In`-value mode without a range, or per-
18/// distinct-value-in-range mode without an `In` on prefix) every
19/// entry's `in_key` is `None`. Callers can recover the historical
20/// `BTreeMap<Vec<u8>, u64>` shape by collecting `(key, count)` pairs
21/// — see [`Self::into_flat_map`].
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
23pub struct DocumentSplitCounts(pub Vec<SplitCountEntry>);
24
25impl DocumentSplitCounts {
26 /// Collect entries into a `BTreeMap<Vec<u8>, u64>` keyed by the
27 /// terminator `key`, summing across `in_key` forks. Use this when
28 /// the caller wants the merged-histogram view of a compound
29 /// query (or for backwards compatibility with the pre-no-merge
30 /// API shape). Flat queries pass through unchanged.
31 pub fn into_flat_map(self) -> BTreeMap<Vec<u8>, u64> {
32 let mut out: BTreeMap<Vec<u8>, u64> = BTreeMap::new();
33 for entry in self.0 {
34 *out.entry(entry.key).or_insert(0) += entry.count.unwrap_or(0);
35 }
36 out
37 }
38
39 /// Build a [`DocumentSplitCounts`] from a verifier-side
40 /// `Vec<SplitCountEntry>`. Identity for now; kept as a
41 /// constructor in case the internal shape evolves.
42 pub fn from_verified(entries: Vec<SplitCountEntry>) -> Self {
43 DocumentSplitCounts(entries)
44 }
45}
46
47/// Reject the generic [`FromProof`] entry point for [`DocumentSplitCounts`].
48///
49/// `DocumentSplitCounts` is reached from rs-sdk via the
50/// `FromProof<DocumentQuery>` impl defined alongside the SDK's
51/// `DocumentQuery` type (see
52/// `rs-sdk/src/platform/documents/document_count.rs`), which
53/// dispatches to the right proof shape (CountTree element /
54/// aggregate-count / distinct-count) based on
55/// `(group_by, where_clauses, prove)`. The generic
56/// `FromProof<Q: TryInto<DriveDocumentQuery>>` path doesn't carry
57/// enough information to pick a proof shape, so it errors out
58/// explicitly — calling this impl directly is a programmer mistake.
59impl<'dq, Q> FromProof<Q> for DocumentSplitCounts
60where
61 Q: TryInto<DriveDocumentQuery<'dq>> + Clone + 'dq,
62 Q::Error: std::fmt::Display,
63{
64 type Request = Q;
65 type Response = GetDocumentsResponse;
66
67 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
68 _request: I,
69 _response: O,
70 _network: Network,
71 _platform_version: &PlatformVersion,
72 _provider: &'a dyn ContextProvider,
73 ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
74 where
75 Self: 'a,
76 {
77 Err(Error::RequestError {
78 error: "DocumentSplitCounts can't be verified via the generic FromProof path; \
79 call DocumentSplitCounts::fetch on a DocumentQuery with .with_select(Count), \
80 which routes through the right proof shape (CountTree element / aggregate / \
81 distinct) based on the request"
82 .to_string(),
83 })
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 //! Local-only tests for the parts of `DocumentSplitCounts` that
90 //! don't need a real grovedb proof or a populated Drive:
91 //!
92 //! - `into_flat_map` — pure data reduction over the new
93 //! `Vec<SplitCountEntry>` shape (covers the no-merge →
94 //! merged-histogram backwards-compat path).
95 //! - `from_verified` — identity constructor wrapping the raw
96 //! verified-entries vec.
97 //! - The generic `FromProof<Q>` impl that intentionally errors
98 //! to prevent the silently-empty footgun documented above.
99 //!
100 //! The actual proof verification (CountTree-element /
101 //! aggregate / distinct) is exercised end-to-end by drive's
102 //! `range_countable_index_e2e_tests` (running the prover and
103 //! verifier on a real Drive); exercising it here would need a
104 //! populated Drive + a real proof, which is outside this
105 //! crate's feature surface.
106 use super::*;
107
108 /// Helper to make a `SplitCountEntry` with the given fields
109 /// without each call site needing to type the struct out.
110 fn entry(in_key: Option<&[u8]>, key: &[u8], count: u64) -> SplitCountEntry {
111 SplitCountEntry {
112 in_key: in_key.map(|s| s.to_vec()),
113 key: key.to_vec(),
114 // Test helper always builds verified entries; `None`
115 // entries (caller asked but verifier was silent) are
116 // tested via explicit struct construction at the SDK
117 // synthesis call site, not through this helper.
118 count: Some(count),
119 }
120 }
121
122 #[test]
123 fn from_verified_round_trips_the_input_vec() {
124 let entries = vec![
125 entry(None, b"red", 5),
126 entry(None, b"green", 3),
127 entry(None, b"blue", 8),
128 ];
129 let counts = DocumentSplitCounts::from_verified(entries.clone());
130 assert_eq!(counts.0, entries);
131 }
132
133 #[test]
134 fn from_verified_empty_round_trip() {
135 let counts = DocumentSplitCounts::from_verified(Vec::new());
136 assert!(counts.0.is_empty());
137 }
138
139 #[test]
140 fn into_flat_map_passes_through_flat_entries() {
141 // No In dimension — every entry has `in_key = None`. The flat
142 // map should be one-to-one with the input.
143 let counts = DocumentSplitCounts::from_verified(vec![
144 entry(None, b"red", 5),
145 entry(None, b"green", 3),
146 entry(None, b"blue", 8),
147 ]);
148 let flat = counts.into_flat_map();
149 assert_eq!(flat.len(), 3);
150 assert_eq!(flat.get(b"red".as_slice()), Some(&5));
151 assert_eq!(flat.get(b"green".as_slice()), Some(&3));
152 assert_eq!(flat.get(b"blue".as_slice()), Some(&8));
153 }
154
155 #[test]
156 fn into_flat_map_sums_across_in_key_forks_for_compound_entries() {
157 // Compound query result: `brand in [acme, contoso]` × `color in [red, green]`.
158 // `into_flat_map` should sum `red` across both brand forks
159 // (3 + 2 = 5) — that's the whole point of providing the
160 // historical merged-histogram view.
161 let counts = DocumentSplitCounts::from_verified(vec![
162 entry(Some(b"acme"), b"red", 3),
163 entry(Some(b"acme"), b"green", 2),
164 entry(Some(b"contoso"), b"red", 2),
165 entry(Some(b"contoso"), b"green", 4),
166 ]);
167 let flat = counts.into_flat_map();
168 assert_eq!(flat.len(), 2, "merges by `key` across in_key forks");
169 assert_eq!(flat.get(b"red".as_slice()), Some(&5));
170 assert_eq!(flat.get(b"green".as_slice()), Some(&6));
171 }
172
173 #[test]
174 fn into_flat_map_handles_mixed_in_key_and_none_entries() {
175 // Edge case: a result set that mixes flat entries (in_key=None)
176 // and compound entries (in_key=Some). Both should fold into
177 // the same `key` buckets when sharing a terminator value.
178 let counts = DocumentSplitCounts::from_verified(vec![
179 entry(None, b"red", 1),
180 entry(Some(b"acme"), b"red", 2),
181 entry(Some(b"contoso"), b"red", 3),
182 entry(Some(b"acme"), b"green", 4),
183 ]);
184 let flat = counts.into_flat_map();
185 assert_eq!(flat.get(b"red".as_slice()), Some(&6));
186 assert_eq!(flat.get(b"green".as_slice()), Some(&4));
187 }
188
189 #[test]
190 fn into_flat_map_empty_input_produces_empty_map() {
191 let counts = DocumentSplitCounts::from_verified(Vec::new());
192 assert!(counts.into_flat_map().is_empty());
193 }
194
195 // The generic `FromProof` rejection (returning the explicit
196 // "needs a split property" error rather than silently returning
197 // `Some(empty)`) is covered by the SDK integration tests, which
198 // can construct a valid `DriveDocumentQuery` via dpp's
199 // `fixtures-and-mocks` feature. drive-proof-verifier itself
200 // doesn't depend on `dpp/fixtures-and-mocks` so we can't build
201 // one here.
202}