drive_proof_verifier/proof/document_average.rs
1//! Verified average result + free-function proof verifiers for the
2//! average surface.
3//!
4//! Average-side analog of [`super::document_sum::DocumentSum`].
5//! Holds the `(count, sum)` pair recovered from a count-sum-bearing
6//! tree proof (`CountSumTree` / `ProvableCountSumTree` /
7//! `ProvableCountProvableSumTree`). `Aggregate` mode returns one
8//! [`DocumentAverage`]; `Entries` mode returns
9//! [`super::document_split_average::DocumentSplitAverages`] instead.
10//!
11//! Averages are NOT pre-divided server-side — the verifier surfaces
12//! the raw `(count, sum)` and the caller divides. See the proto
13//! file's `AverageResults` docstring for the rationale (precision +
14//! client-chosen representation).
15//!
16//! The generic `FromProof<Q>` impl below intentionally rejects
17//! calls (matching [`super::document_split_count::DocumentSplitCounts`]'s
18//! pattern). Real dispatch lives in the
19//! `FromProof<DocumentQuery>` impl in
20//! `rs-sdk/src/platform/documents/document_average.rs`, which picks
21//! among the free-function verifiers below based on the resolved
22//! `DocumentAverageMode`.
23
24use crate::error::MapGroveDbError;
25use crate::verify::verify_tenderdash_proof;
26use crate::{ContextProvider, Error};
27use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
28use dpp::version::PlatformVersion;
29use drive::query::drive_document_average_query::AverageEntry;
30use drive::query::drive_document_sum_query::DriveDocumentSumQuery;
31
32/// Verify a grovedb point-lookup proof against a count-sum-bearing
33/// index terminator and return per-branch `(count, sum)` entries.
34/// AVG analog of [`super::document_sum::verify_point_lookup_sum_proof`].
35///
36/// Thin tenderdash-composition wrapper over
37/// [`DriveDocumentSumQuery::verify_point_lookup_count_and_sum_proof`].
38/// Used by the prove path's `Aggregate` + Equal/In + no range
39/// shape when the chosen index declares BOTH `summable: "<prop>"`
40/// AND a `countable` terminator.
41pub fn verify_point_lookup_count_and_sum_proof(
42 query: &DriveDocumentSumQuery,
43 proof: &Proof,
44 mtd: &ResponseMetadata,
45 platform_version: &PlatformVersion,
46 provider: &dyn ContextProvider,
47) -> Result<Vec<AverageEntry>, Error> {
48 let (root_hash, entries) = query
49 .verify_point_lookup_count_and_sum_proof(&proof.grovedb_proof, platform_version)
50 .map_drive_error(proof, mtd)?;
51
52 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
53
54 Ok(entries)
55}
56
57/// Verify a per-distinct-key range-AVG proof against an index that
58/// declares BOTH `rangeCountable: true` AND `rangeSummable: true`
59/// (a `rangeAverageable: true` index) and return per-`(in_key,
60/// key)` `(count, sum)` entries. AVG analog of
61/// [`super::document_sum::verify_distinct_sum_proof`].
62///
63/// Thin tenderdash-composition wrapper over
64/// [`DriveDocumentSumQuery::verify_distinct_count_and_sum_proof`].
65/// Used by the prove path's `GroupByRange` / `GroupByCompound` +
66/// range shape on the AVG surface.
67pub fn verify_distinct_count_and_sum_proof(
68 query: &DriveDocumentSumQuery,
69 proof: &Proof,
70 mtd: &ResponseMetadata,
71 limit: u16,
72 left_to_right: bool,
73 platform_version: &PlatformVersion,
74 provider: &dyn ContextProvider,
75) -> Result<Vec<AverageEntry>, Error> {
76 let (root_hash, entries) = query
77 .verify_distinct_count_and_sum_proof(
78 &proof.grovedb_proof,
79 limit,
80 left_to_right,
81 platform_version,
82 )
83 .map_drive_error(proof, mtd)?;
84
85 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
86
87 Ok(entries)
88}
89
90/// The `(count, sum)` pair across documents matching a query,
91/// verified from proof. Client computes `avg = sum / count` using
92/// whichever precision representation it wants.
93///
94/// `count` is `u64` (counts are non-negative); `sum` is `i64`
95/// (matching `DocumentSum`). The grovedb primitive that backs this
96/// is `AggregateCountAndSumOnRange` (PCPS-leaf) for range-filtered
97/// queries, or the primary-key count-sum-bearing element direct
98/// read for empty-where queries on a
99/// `documentsCountable + documentsSummable` doctype.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct DocumentAverage {
102 /// Total matched-document count for the query.
103 pub count: u64,
104 /// Total aggregated value of the `sum_property` for the query.
105 pub sum: i64,
106}
107
108impl DocumentAverage {
109 /// Convenience: compute the average as `f64`. Returns `None`
110 /// when `count == 0` (preserving the divide-by-zero contract
111 /// rather than producing `NaN` / `inf`). Callers that need a
112 /// different representation should divide `self.sum /
113 /// self.count` directly.
114 pub fn as_f64(&self) -> Option<f64> {
115 if self.count == 0 {
116 None
117 } else {
118 Some(self.sum as f64 / self.count as f64)
119 }
120 }
121}
122
123// No generic `FromProof<Q>` impl is provided here — see the
124// `DocumentSum` docstring for the rationale. Callers reach this
125// through `FromProof<DocumentQuery> for DocumentAverage` in
126// `rs-sdk/src/platform/documents/document_average.rs`.
127
128/// Verify a leaf-PCPS `AggregateCountAndSumOnRange` proof and the
129/// surrounding tenderdash commit, returning the verified
130/// `(count, sum)` pair. Used by the prove path's
131/// `select=AVG, group_by=[]` with a range clause on an index that
132/// declares BOTH `rangeCountable: true` AND `rangeSummable: true`
133/// (i.e. the terminator is a `ProvableCountProvableSumTree`).
134///
135/// Thin tenderdash-composition wrapper over
136/// [`DriveDocumentSumQuery::verify_aggregate_count_and_sum_proof`].
137/// Both metrics come from one root-hash-committed traversal of the
138/// PCPS terminator — no way for the server to splice a count from
139/// one set with a sum from another.
140pub fn verify_aggregate_count_and_sum_proof(
141 query: &DriveDocumentSumQuery,
142 proof: &Proof,
143 mtd: &ResponseMetadata,
144 platform_version: &PlatformVersion,
145 provider: &dyn ContextProvider,
146) -> Result<(u64, i64), Error> {
147 let (root_hash, count, sum) = query
148 .verify_aggregate_count_and_sum_proof(&proof.grovedb_proof, platform_version)
149 .map_drive_error(proof, mtd)?;
150
151 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
152
153 Ok((count, sum))
154}
155
156/// Verify a grovedb proof of the document type's primary-key
157/// count-sum-bearing element (`CountSumTree` /
158/// `ProvableCountSumTree` / `ProvableCountProvableSumTree`) and
159/// return the unfiltered `(count, sum)` pair.
160///
161/// Thin tenderdash-composition wrapper over
162/// [`DriveDocumentSumQuery::verify_primary_key_count_sum_tree_proof`].
163/// Used by the prove path's AVG fast path on a doctype that has
164/// both `documentsCountable: true` and `documentsSummable: "<prop>"`
165/// set, with empty where clauses — the server proves the
166/// primary-key element directly and the SDK extracts both metrics
167/// from one verified element.
168pub fn verify_primary_key_count_sum_tree_proof(
169 contract_id: [u8; 32],
170 document_type_name: &str,
171 proof: &Proof,
172 mtd: &ResponseMetadata,
173 platform_version: &PlatformVersion,
174 provider: &dyn ContextProvider,
175) -> Result<(u64, i64), Error> {
176 let (root_hash, count, sum) = DriveDocumentSumQuery::verify_primary_key_count_sum_tree_proof(
177 &proof.grovedb_proof,
178 contract_id,
179 document_type_name,
180 platform_version,
181 )
182 .map_drive_error(proof, mtd)?;
183
184 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
185
186 Ok((count, sum))
187}
188
189/// Verify a **carrier**-PCPS `AggregateCountAndSumOnRange` proof
190/// and return the per-`In`-branch `(count, sum)` triples. AVG analog
191/// of count's
192/// [`super::document_count::verify_carrier_aggregate_count_proof`]
193/// and sum's
194/// [`super::document_sum::verify_carrier_aggregate_sum_proof`].
195///
196/// Thin tenderdash-composition wrapper over
197/// [`DriveDocumentSumQuery::verify_carrier_aggregate_count_and_sum_proof`].
198/// Used by the prove path when the request shape is `select=AVG,
199/// group_by=[in_field], where = In(in_field) + range(other_field),
200/// prove=true` against a PCPS-eligible index — drive routes it to
201/// the carrier-PCPS executor.
202///
203/// Result: one [`AverageEntry`] per **present** In branch with
204/// `in_key = <serialized In value>`, `key = []`, `count = Some(n)`,
205/// `sum = Some(v)`. Absent In branches are omitted; the count and
206/// sum axes never disagree on present/absent because the proof
207/// commits both metrics from the same merk traversal.
208pub fn verify_carrier_aggregate_count_and_sum_proof(
209 query: &DriveDocumentSumQuery,
210 proof: &Proof,
211 mtd: &ResponseMetadata,
212 limit: Option<u16>,
213 left_to_right: bool,
214 platform_version: &PlatformVersion,
215 provider: &dyn ContextProvider,
216) -> Result<Vec<AverageEntry>, Error> {
217 let (root_hash, per_key_count_sum) = query
218 .verify_carrier_aggregate_count_and_sum_proof(
219 &proof.grovedb_proof,
220 limit,
221 left_to_right,
222 platform_version,
223 )
224 .map_drive_error(proof, mtd)?;
225
226 verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
227
228 let entries = per_key_count_sum
229 .into_iter()
230 .map(|(in_key, count, sum)| AverageEntry {
231 in_key: Some(in_key),
232 key: Vec::new(),
233 count: Some(count),
234 sum: Some(sum),
235 })
236 .collect();
237 Ok(entries)
238}