Skip to main content

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}