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//! 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}