drive_proof_verifier/proof/document_sum.rs
1//! Verified sum result + free-function proof verifiers for the
2//! sum-tree surface.
3//!
4//! Sum-side analog of [`super::document_count`]. Holds the
5//! aggregated `i64` recovered from a sum-tree proof — `Aggregate`
6//! mode returns one [`DocumentSum`]; `Entries` mode returns
7//! [`super::document_split_sum::DocumentSplitSums`] instead.
8//!
9//! The generic `FromProof<Q>` impl below intentionally rejects
10//! calls (matching [`super::document_split_count::DocumentSplitCounts`]'s
11//! pattern): the underlying proof primitive depends on the per-mode
12//! routing of `(group_by, where_clauses, prove)`, which the generic
13//! `Q` constraint can't carry. The real dispatch lives in the
14//! `FromProof<DocumentQuery>` impl in
15//! `rs-sdk/src/platform/documents/document_sum.rs`, which picks
16//! among the free-function verifiers below based on the resolved
17//! `DocumentSumMode`.
18
19use crate::error::MapGroveDbError;
20use crate::verify::verify_tenderdash_proof;
21use crate::{ContextProvider, Error};
22use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
23use dpp::version::PlatformVersion;
24use drive::query::drive_document_sum_query::{DriveDocumentSumQuery, SumEntry};
25
26/// The aggregated sum of an integer property across documents matching
27/// a query, verified from proof.
28///
29/// Signed because grovedb's `SumTree` value type is `i64` — sums can
30/// in principle be negative (typically signaling i64 overflow into
31/// negative space, which the verifier surfaces explicitly). For
32/// tip-jar-style non-negative aggregations this stays ≥ 0.
33///
34/// No generic `FromProof<Q>` impl is provided here — the real
35/// proof dispatch needs the `DocumentQuery` request shape to pick
36/// the right per-mode verifier (range-aggregate / point-lookup /
37/// primary-key SumTree / In-carrier). Callers reach this through
38/// `FromProof<DocumentQuery> for DocumentSum` in
39/// `rs-sdk/src/platform/documents/document_sum.rs`, which routes to
40/// the per-shape verifier free functions below.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct DocumentSum(pub i64);
43
44/// Verify a grovedb `AggregateSumOnRange` proof and the surrounding
45/// tenderdash commit, returning the verified `i64` sum from one
46/// range traversal. Used by the prove path's
47/// `select=SUM, group_by=[]` with a range clause on a
48/// `rangeSummable: true` index.
49///
50/// Thin tenderdash-composition wrapper over
51/// [`DriveDocumentSumQuery::verify_aggregate_sum_proof`] in rs-drive
52/// (which does the merk-level verification via
53/// `GroveDb::verify_aggregate_sum_query`). Both helpers share the
54/// prover's `aggregate_sum_path_query` so the path query bytes
55/// match byte-for-byte across the network.
56pub fn verify_aggregate_sum_proof(
57 query: &DriveDocumentSumQuery,
58 proof: &Proof,
59 mtd: &ResponseMetadata,
60 platform_version: &PlatformVersion,
61 provider: &dyn ContextProvider,
62) -> Result<i64, Error> {
63 let (root_hash, sum) = query
64 .verify_aggregate_sum_proof(&proof.grovedb_proof, platform_version)
65 .map_drive_error(proof, mtd)?;
66
67 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
68
69 Ok(sum)
70}
71
72/// Verify a grovedb proof of the document type's primary-key
73/// `SumTree` element and return the unfiltered total sum.
74///
75/// Thin tenderdash-composition wrapper over
76/// [`DriveDocumentSumQuery::verify_primary_key_sum_tree_proof`].
77/// Used by the prove path's `documents_summable: "<prop>"` fast
78/// path — when the where clauses are empty and the document type
79/// has a matching `documents_summable`, the server proves the
80/// primary-key SumTree element directly and the SDK extracts the
81/// sum from the verified element.
82pub fn verify_primary_key_sum_tree_proof(
83 contract_id: [u8; 32],
84 document_type_name: &str,
85 proof: &Proof,
86 mtd: &ResponseMetadata,
87 platform_version: &PlatformVersion,
88 provider: &dyn ContextProvider,
89) -> Result<i64, Error> {
90 let (root_hash, sum) = DriveDocumentSumQuery::verify_primary_key_sum_tree_proof(
91 &proof.grovedb_proof,
92 contract_id,
93 document_type_name,
94 platform_version,
95 )
96 .map_drive_error(proof, mtd)?;
97
98 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
99
100 Ok(sum)
101}
102
103/// Verify a grovedb point-lookup sum proof and return the per-branch
104/// entries. Sum analog of count's
105/// [`super::document_count::verify_point_lookup_count_proof`].
106///
107/// Thin tenderdash-composition wrapper over
108/// [`DriveDocumentSumQuery::verify_point_lookup_sum_proof`]. Used
109/// by the prove path's Equal/`In` sum queries against a
110/// `summable: "<prop>"` index — one entry per **present** queried
111/// key (absent keys are silently omitted because today's path
112/// query doesn't request absence proofs, matching count's
113/// behavior).
114pub fn verify_point_lookup_sum_proof(
115 query: &DriveDocumentSumQuery,
116 proof: &Proof,
117 mtd: &ResponseMetadata,
118 platform_version: &PlatformVersion,
119 provider: &dyn ContextProvider,
120) -> Result<Vec<SumEntry>, Error> {
121 let (root_hash, entries) = query
122 .verify_point_lookup_sum_proof(&proof.grovedb_proof, platform_version)
123 .map_drive_error(proof, mtd)?;
124
125 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
126
127 Ok(entries)
128}
129
130/// Verify a per-distinct-key range-sum proof against a
131/// `rangeSummable: true` index and return the per-`(in_key, key)`
132/// sums.
133///
134/// Thin tenderdash-composition wrapper over
135/// [`DriveDocumentSumQuery::verify_distinct_sum_proof`]. Sum analog
136/// of count's
137/// [`super::document_count::verify_distinct_count_proof`]. Used by
138/// the prove path's `RangeDistinctProof` mode (GroupByRange /
139/// GroupByCompound + range + prove against a `rangeSummable: true`
140/// index).
141pub fn verify_distinct_sum_proof(
142 query: &DriveDocumentSumQuery,
143 proof: &Proof,
144 mtd: &ResponseMetadata,
145 limit: u16,
146 left_to_right: bool,
147 platform_version: &PlatformVersion,
148 provider: &dyn ContextProvider,
149) -> Result<Vec<SumEntry>, Error> {
150 let (root_hash, entries) = query
151 .verify_distinct_sum_proof(&proof.grovedb_proof, limit, left_to_right, platform_version)
152 .map_drive_error(proof, mtd)?;
153
154 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
155
156 Ok(entries)
157}
158
159/// Verify a **carrier** `AggregateSumOnRange` proof against a
160/// `rangeSummable: true` index and return the per-`In`-branch sums.
161///
162/// Thin tenderdash-composition wrapper over
163/// [`DriveDocumentSumQuery::verify_carrier_aggregate_sum_proof`].
164/// Sum analog of count's
165/// [`super::document_count::verify_carrier_aggregate_count_proof`].
166/// Used by the prove path when the request shape is
167/// `select=SUM, group_by=[in_field], where = In(in_field) +
168/// range(other_field), prove=true` — drive's `detect_sum_mode`
169/// routes that shape to
170/// `DocumentSumMode::RangeAggregateCarrierProof`, which collapses
171/// each In branch's range into a single committed `i64`.
172///
173/// Result: one [`SumEntry`] per **present** In branch with
174/// `in_key = <serialized In value>`, `key = []`, `sum = Some(n)`.
175/// Absent In branches are omitted; callers that need to surface
176/// "queried but absent" diff their In array against the returned
177/// `in_key`s.
178pub fn verify_carrier_aggregate_sum_proof(
179 query: &DriveDocumentSumQuery,
180 proof: &Proof,
181 mtd: &ResponseMetadata,
182 limit: Option<u16>,
183 left_to_right: bool,
184 platform_version: &PlatformVersion,
185 provider: &dyn ContextProvider,
186) -> Result<Vec<SumEntry>, Error> {
187 let (root_hash, per_key_sums) = query
188 .verify_carrier_aggregate_sum_proof(
189 &proof.grovedb_proof,
190 limit,
191 left_to_right,
192 platform_version,
193 )
194 .map_drive_error(proof, mtd)?;
195
196 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
197
198 // Map drive's `Vec<(Vec<u8>, i64)>` carrier shape onto the
199 // SDK's `Vec<SumEntry>` so the call sites stay uniform.
200 let entries = per_key_sums
201 .into_iter()
202 .map(|(in_key, sum)| SumEntry {
203 in_key: Some(in_key),
204 key: Vec::new(),
205 sum: Some(sum),
206 })
207 .collect();
208 Ok(entries)
209}