Skip to main content

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