dash_platform_queries/documents/document_sum.rs
1//! `FromProof` + `Fetch` for [`DocumentSum`] — the single-value
2//! aggregate sum view of the unified `getDocuments` endpoint.
3//!
4//! Sum-side analog of [`super::document_count`]. Callers build a
5//! `DocumentQuery` with `.with_select(Select::Sum)` and
6//! `.with_select_field("amount")`; whatever the request shape,
7//! this impl returns a single `i64` (the aggregate sum).
8//!
9//! Empty entries (verifier emitted `None` for a queried-but-absent
10//! branch) contribute 0 to the sum via `filter_map(|e| e.sum)`.
11//!
12//! Overflow handling: the fold uses [`i64::checked_add`] and returns
13//! a `RequestError` on overflow rather than panicking (debug) or
14//! wrapping (release). A grovedb sum-tree's per-node aggregate
15//! itself fits in `i64` by construction, but a multi-entry fold
16//! across carrier-aggregate / distinct branches can in principle
17//! exceed that — the verifier surfaces it explicitly so callers
18//! can switch to `DocumentSplitSums` (which preserves per-branch
19//! `i64`s and lets the caller pick its own arithmetic).
20
21use crate::documents::document_query::DocumentQuery;
22use crate::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query};
23use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
24use dash_context_provider::ContextProvider;
25use dpp::dashcore::Network;
26use dpp::version::PlatformVersion;
27use drive_proof_verifier::{DocumentSum, FromProof, SumEntry};
28
29/// Fold per-branch sums into a single `i64`. Returns `RequestError`
30/// on overflow rather than panicking (debug) or wrapping (release).
31/// Extracted into a free function so the overflow path is
32/// unit-testable without driving a full proof flow.
33fn fold_sum_entries(entries: &[SumEntry]) -> Result<i64, drive_proof_verifier::Error> {
34 let mut total: i64 = 0;
35 for e in entries {
36 if let Some(s) = e.sum {
37 total =
38 total
39 .checked_add(s)
40 .ok_or_else(|| drive_proof_verifier::Error::RequestError {
41 error: "DocumentSum: i64 overflow folding per-branch sums into a single \
42 aggregate. The proof itself verified, but the requested sum \
43 doesn't fit in i64. Use DocumentSplitSums to receive per-branch \
44 i64s and fold them with your own arithmetic (e.g. i128)."
45 .to_string(),
46 })?;
47 }
48 }
49 Ok(total)
50}
51
52impl FromProof<DocumentQuery> for DocumentSum {
53 type Request = DocumentQuery;
54 type Response = GetDocumentsResponse;
55
56 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
57 request: I,
58 response: O,
59 _network: Network,
60 platform_version: &PlatformVersion,
61 provider: &'a dyn ContextProvider,
62 ) -> Result<(Option<Self>, ResponseMetadata, Proof), drive_proof_verifier::Error>
63 where
64 Self: 'a,
65 {
66 let request: Self::Request = request.into();
67 assert_select_is_sum(&request)?;
68 let response: Self::Response = response.into();
69 let (entries, mtd, proof) =
70 verify_sum_query(request, response, platform_version, provider)?;
71 // Fold per-branch sums into a single `i64` using
72 // `checked_add` (via `fold_sum_entries`). A multi-entry
73 // fold (carrier-aggregate across many In branches, or
74 // distinct-mode across many range buckets) can in
75 // principle overflow even though each branch is itself a
76 // valid grovedb `i64` sum_value. Surface this as a
77 // `RequestError` rather than panicking (debug) or
78 // wrapping (release) — callers can switch to
79 // `DocumentSplitSums` to preserve per-branch numbers.
80 let sum = match entries {
81 None => None,
82 Some(es) => Some(DocumentSum(fold_sum_entries(&es)?)),
83 };
84 Ok((sum, mtd, proof))
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 //! Unit tests for the SUM fold. The fold logic is extracted
91 //! into `fold_sum_entries` so we can pin its overflow
92 //! behavior without driving a full proof flow.
93
94 use super::*;
95
96 fn entry(in_key: Option<Vec<u8>>, sum: Option<i64>) -> SumEntry {
97 SumEntry {
98 in_key,
99 key: vec![0u8],
100 sum,
101 }
102 }
103
104 /// Single in-range branch: fold returns the branch's value.
105 /// Smoke test that the helper hasn't regressed on the
106 /// non-overflow path. Mirrors `verify_count_query`'s single-
107 /// branch unit-test shape.
108 #[test]
109 fn fold_sum_entries_single_branch_passes_through() {
110 let entries = vec![entry(None, Some(42))];
111 let sum = fold_sum_entries(&entries).expect("single branch should fold cleanly");
112 assert_eq!(sum, 42);
113 }
114
115 /// Multi-branch fold sums all the `Some` values. `None` entries
116 /// (verifier emitted `None` for a queried-but-absent branch)
117 /// contribute 0, matching the count helper's three-valued
118 /// semantics.
119 #[test]
120 fn fold_sum_entries_multi_branch_with_absent_branches() {
121 let entries = vec![
122 entry(Some(vec![1]), Some(100)),
123 entry(Some(vec![2]), None), // absent branch → contributes 0
124 entry(Some(vec![3]), Some(50)),
125 ];
126 let sum = fold_sum_entries(&entries).expect("absent branches must contribute 0");
127 assert_eq!(sum, 150);
128 }
129
130 /// Positive overflow: two branches summing to `> i64::MAX` must
131 /// surface as a `RequestError`. Pre-fix this would panic in
132 /// debug or wrap in release (`.sum::<i64>()`) — both unsafe in
133 /// a verifier context. Regression: lock the explicit-error
134 /// behavior.
135 #[test]
136 fn fold_sum_entries_positive_overflow_returns_error() {
137 let entries = vec![
138 entry(Some(vec![1]), Some(i64::MAX)),
139 entry(Some(vec![2]), Some(1)),
140 ];
141 let err = fold_sum_entries(&entries)
142 .expect_err("positive overflow must surface as RequestError, not panic/wrap");
143 let msg = format!("{err:?}");
144 assert!(
145 msg.contains("i64 overflow") && msg.contains("DocumentSplitSums"),
146 "error must name the overflow + hint at DocumentSplitSums; got {msg}"
147 );
148 }
149
150 /// Symmetric negative-overflow guard: two branches summing to
151 /// `< i64::MIN` must surface as a `RequestError`. `checked_add`
152 /// reports both directions; we pin both so a future switch to
153 /// `saturating_*` can't silently regress only one direction.
154 #[test]
155 fn fold_sum_entries_negative_overflow_returns_error() {
156 let entries = vec![
157 entry(Some(vec![1]), Some(i64::MIN)),
158 entry(Some(vec![2]), Some(-1)),
159 ];
160 let err =
161 fold_sum_entries(&entries).expect_err("negative overflow must surface as RequestError");
162 let msg = format!("{err:?}");
163 assert!(msg.contains("i64 overflow"));
164 }
165
166 /// Empty fold: zero entries → zero sum. Pre-empts a regression
167 /// where an empty-branch optimization would return `None` and
168 /// FromProof callers would surface "no proof" errors instead
169 /// of "verified zero".
170 #[test]
171 fn fold_sum_entries_empty_returns_zero() {
172 let sum = fold_sum_entries(&[]).expect("empty fold must succeed");
173 assert_eq!(sum, 0);
174 }
175}