Skip to main content

drive_proof_verifier/proof/
document_split_sum.rs

1//! Verified per-entry sum result.
2//!
3//! Sum-side analog of [`super::document_split_count::DocumentSplitCounts`].
4//! Holds one verified `(in_key, key, sum)` triple per matched group.
5//! Returned by `select=SUM, group_by=[...]` queries; aggregate sums
6//! use [`super::document_sum::DocumentSum`] instead.
7//!
8//! The generic `FromProof<Q>` impl below intentionally rejects
9//! calls (matching [`super::document_split_count::DocumentSplitCounts`]'s
10//! pattern). Real dispatch lives in the
11//! `FromProof<DocumentQuery>` impl in
12//! `rs-sdk/src/platform/documents/document_split_sums.rs`, which
13//! picks the right per-shape verifier (carrier-aggregate /
14//! point-lookup) based on the resolved `DocumentSumMode`.
15
16/// A single verified `(in_key?, key, sum)` entry from a sum query
17/// with `group_by`. Mirrors count's `SplitCountEntry` shape.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct SplitSumEntry {
20    /// Outer In-prefix value for compound `(In, range)` queries;
21    /// `None` for flat queries.
22    pub in_key: Option<Vec<u8>>,
23    /// The terminator key value.
24    pub key: Vec<u8>,
25    /// The aggregated sum at that key. `Some(n)` for matched keys;
26    /// `None` for keys proven absent.
27    pub sum: Option<i64>,
28}
29
30/// The full per-entry sum result, verified from proof.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct DocumentSplitSums(pub Vec<SplitSumEntry>);
33
34impl DocumentSplitSums {
35    /// Convenience: collapse compound `(in_key, key)` entries into a
36    /// flat `BTreeMap<key, summed_sum>` by combining each In-fork's
37    /// contribution at the same terminator key. Same shape as
38    /// `DocumentSplitCounts::into_flat_map` on the count side.
39    ///
40    /// Uses [`i64::checked_add`] on each accumulator step and
41    /// surfaces overflow as
42    /// [`Error::RequestError`](crate::Error::RequestError) rather
43    /// than panicking (debug) or wrapping (release). Mirrors the
44    /// SDK's aggregate-side `DocumentSum::fold_sum_entries` so a
45    /// wasm caller that collapses split sums lands at the same
46    /// hardening boundary the SDK aggregate path already enforces.
47    ///
48    /// On overflow, drop back to iterating `DocumentSplitSums.0`
49    /// directly — per-branch entries preserve the verified `i64`
50    /// values, and the caller can fold with its own arithmetic
51    /// (e.g. `i128`).
52    pub fn try_into_flat_map(
53        self,
54    ) -> Result<std::collections::BTreeMap<Vec<u8>, i64>, crate::Error> {
55        let mut out: std::collections::BTreeMap<Vec<u8>, i64> = std::collections::BTreeMap::new();
56        for entry in self.0 {
57            if let Some(sum) = entry.sum {
58                let slot = out.entry(entry.key).or_insert(0i64);
59                *slot =
60                    slot.checked_add(sum).ok_or_else(|| {
61                        crate::Error::RequestError {
62                    error:
63                        "DocumentSplitSums::try_into_flat_map: i64 overflow merging per-In-fork \
64                         sums at the same terminator key. The verified per-branch entries are \
65                         each valid i64, but their fold exceeds the i64 range — iterate \
66                         DocumentSplitSums.0 directly to access per-branch sums and fold with \
67                         your own arithmetic (e.g. i128)."
68                            .to_string(),
69                }
70                    })?;
71            }
72        }
73        Ok(out)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    /// `try_into_flat_map` over a single In-fork must pass the sum
82    /// through unchanged — the baseline check the overflow guard
83    /// has to preserve.
84    #[test]
85    fn try_into_flat_map_passes_through_flat_entries() {
86        let splits = DocumentSplitSums(vec![SplitSumEntry {
87            in_key: None,
88            key: b"key-a".to_vec(),
89            sum: Some(123),
90        }]);
91        let flat = splits.try_into_flat_map().expect("non-overflowing fold");
92        assert_eq!(flat.len(), 1);
93        assert_eq!(flat.get(b"key-a".as_slice()), Some(&123));
94    }
95
96    /// Two In-forks landing at the same terminator key get summed.
97    #[test]
98    fn try_into_flat_map_sums_across_in_forks() {
99        let splits = DocumentSplitSums(vec![
100            SplitSumEntry {
101                in_key: Some(b"alice".to_vec()),
102                key: b"red".to_vec(),
103                sum: Some(10),
104            },
105            SplitSumEntry {
106                in_key: Some(b"bob".to_vec()),
107                key: b"red".to_vec(),
108                sum: Some(32),
109            },
110            SplitSumEntry {
111                in_key: Some(b"alice".to_vec()),
112                key: b"blue".to_vec(),
113                sum: Some(7),
114            },
115        ]);
116        let flat = splits.try_into_flat_map().expect("non-overflowing fold");
117        assert_eq!(flat.get(b"red".as_slice()), Some(&42));
118        assert_eq!(flat.get(b"blue".as_slice()), Some(&7));
119    }
120
121    /// Crossing `i64::MAX` from two valid per-branch i64s must
122    /// surface as `RequestError`, not panic / wrap. This is the
123    /// exact boundary the SDK's aggregate-side fold already
124    /// hardens for `DocumentSum`.
125    #[test]
126    fn try_into_flat_map_overflow_surfaces_request_error() {
127        let splits = DocumentSplitSums(vec![
128            SplitSumEntry {
129                in_key: Some(b"alice".to_vec()),
130                key: b"shared".to_vec(),
131                sum: Some(i64::MAX),
132            },
133            SplitSumEntry {
134                in_key: Some(b"bob".to_vec()),
135                key: b"shared".to_vec(),
136                sum: Some(1),
137            },
138        ]);
139        let err = splits
140            .try_into_flat_map()
141            .expect_err("i64::MAX + 1 must surface as overflow error, not wrap or panic");
142        let msg = format!("{err:?}");
143        assert!(
144            msg.contains("i64 overflow") && msg.contains("DocumentSplitSums"),
145            "error message must name the overflow and the helper: got {msg}"
146        );
147    }
148
149    /// Underflow on the negative end must surface symmetrically.
150    #[test]
151    fn try_into_flat_map_underflow_surfaces_request_error() {
152        let splits = DocumentSplitSums(vec![
153            SplitSumEntry {
154                in_key: Some(b"alice".to_vec()),
155                key: b"shared".to_vec(),
156                sum: Some(i64::MIN),
157            },
158            SplitSumEntry {
159                in_key: Some(b"bob".to_vec()),
160                key: b"shared".to_vec(),
161                sum: Some(-1),
162            },
163        ]);
164        let err = splits
165            .try_into_flat_map()
166            .expect_err("i64::MIN - 1 must surface as overflow error, not wrap or panic");
167        let msg = format!("{err:?}");
168        assert!(msg.contains("i64 overflow"), "got {msg}");
169    }
170
171    /// `None`-sum entries are skipped (the verifier emits them for
172    /// keys proven absent). They must not poison the accumulator
173    /// or trigger a spurious overflow signal.
174    #[test]
175    fn try_into_flat_map_skips_absent_entries() {
176        let splits = DocumentSplitSums(vec![
177            SplitSumEntry {
178                in_key: None,
179                key: b"key-a".to_vec(),
180                sum: None,
181            },
182            SplitSumEntry {
183                in_key: None,
184                key: b"key-b".to_vec(),
185                sum: Some(5),
186            },
187        ]);
188        let flat = splits.try_into_flat_map().expect("absent entries skipped");
189        assert!(!flat.contains_key(b"key-a".as_slice()));
190        assert_eq!(flat.get(b"key-b".as_slice()), Some(&5));
191    }
192}
193
194// No generic `FromProof<Q>` impl — callers reach this through
195// `FromProof<DocumentQuery> for DocumentSplitSums` in
196// `rs-sdk/src/platform/documents/document_split_sums.rs`. Same
197// rationale as `DocumentSum` / `DocumentSplitCounts`: per-mode
198// proof dispatch needs the SDK's `DocumentQuery` shape.