Skip to main content

drive_proof_verifier/proof/
document_ranked.rs

1//! Verified **ranked** (`GROUP BY … ORDER BY <aggregate> LIMIT n`)
2//! document results.
3//!
4//! A ranked query answers "which `n` groups score highest (or lowest)
5//! on an aggregate?" — `SELECT AVG(grade) GROUP BY restaurantId
6//! ORDER BY grade DESC LIMIT 5`. The answer is read straight out of
7//! the per-axis *secondary* Merk of an indexed tree (grovedb PR #657),
8//! so it costs `O(log n + k)` and comes with a proof that commits to
9//! exactly the `k` returned `(aggregate, group key)` pairs — plus the
10//! `OFFSET`, which grovedb counts from the subtree aggregates rather
11//! than by walking the skipped region — and additionally attests, on
12//! this proved path — so a deep page costs `O(log n + k)` like any
13//! other rather than growing with the offset.
14//!
15//! This module holds the client-facing result type
16//! ([`DocumentRankedEntries`]), the tenderdash-composition wrapper
17//! around rs-drive's merk-level verifier
18//! ([`verify_ranked_top_k_proof`]), and the decoder for the unproven
19//! `ResultData.ranked` wire payload
20//! ([`DocumentRankedEntries::from_unproved_response`]).
21//!
22//! Per-shape routing (which index covers the axis, which
23//! `(axis, descending, k, offset)` tuple the request resolves to) lives
24//! in rs-sdk's `ranked_proof_helpers`, exactly as count's four-way
25//! dispatch lives in `count_proof_helpers` — it needs the data
26//! contract, which this crate does not carry.
27
28use crate::error::MapGroveDbError;
29use crate::verify::{document_query_proof_bytes, verify_tenderdash_proof};
30use crate::{ContextProvider, Error, FromProof};
31use dapi_grpc::platform::v0::get_documents_response::get_documents_response_v1::{
32    ranked_entry, result_data, RankedEntry as ProtoRankedEntry, ResultData,
33};
34use dapi_grpc::platform::v0::get_documents_response::{
35    get_documents_response_v1, Version as ResponseVersion,
36};
37use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
38use dpp::dashcore::Network;
39use dpp::version::PlatformVersion;
40use drive::query::{
41    DriveDocumentQuery, DriveDocumentRankedQuery, RankedEntry, RankedEntryValue, RankedPage,
42    RANKED_AVG_SCALE,
43};
44use drive::verify::RootHash;
45
46/// One page of a `GROUP BY … ORDER BY <aggregate> LIMIT n [OFFSET m]`
47/// query: the ranked groups, plus the rank the page starts at.
48///
49/// **Entry order is the ranking order** — best-first for `DESC`,
50/// worst-first for `ASC`. Callers must not re-sort; ties (groups with
51/// equal aggregates) come back in group-key order *in the direction of
52/// the walk*, which is descending group-key order for `DESC`.
53///
54/// Fewer than `n` entries is normal — the index simply holds fewer
55/// groups than were asked for — and is not an error.
56///
57/// Each [`RankedEntry`]'s `key` is the raw index-key bytes of the
58/// `GROUP BY` property's value (for a `string` property, its UTF-8
59/// bytes); its `value` is the aggregate, one of
60/// [`RankedEntryValue::Count`] / [`RankedEntryValue::Sum`] /
61/// [`RankedEntryValue::AvgFixedPoint`]. Averages are fixed-point
62/// integers scaled by [`crate::RANKED_AVG_SCALE`]; divide by it (or
63/// call [`RankedEntryValue::as_f64`]) to render one.
64///
65/// The fixed point is **exact on the proved path only**. A page built
66/// by [`Self::from_verified`] carries the very integer the proof
67/// commits to; one built by [`Self::from_unproved_response`] carries a
68/// best-effort reconstruction from the wire's `double` — see that
69/// method for what that costs.
70#[derive(Debug, Clone, PartialEq, Eq, Default)]
71pub struct DocumentRankedEntries {
72    /// The 0-based rank of `entries[0]` — the query's `OFFSET`, as
73    /// actually honoured.
74    ///
75    /// This is what turns a page back into a *ranking*: entry `i` is
76    /// the group at rank `starting_rank + i`. Without it a caller who
77    /// asked for `ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` receives
78    /// one entry and has no way to tell it really is the 5th-best
79    /// group rather than the best.
80    ///
81    /// On the **proved** path this is grovedb's cryptographically
82    /// attested count, re-derived by the verifier from the counted
83    /// subtree commitments in the proof bytes rather than trusted from
84    /// the response. It equals the requested offset unless the walk ran
85    /// out of groups first, in which case `entries` is empty and this
86    /// is a *proof* that the ranking holds exactly this many groups in
87    /// total — an offset past the end is a positive answer, not an
88    /// error.
89    ///
90    /// On the **unproven** decode it is whatever the node put on the
91    /// wire (`0` when the field is absent), and carries no more weight
92    /// than the entries beside it.
93    pub starting_rank: u64,
94    /// The groups on this page, **in ranking order**.
95    pub entries: Vec<RankedEntry>,
96}
97
98impl DocumentRankedEntries {
99    /// Build a [`DocumentRankedEntries`] from a verifier-side
100    /// [`RankedPage`] — the shape rs-drive's merk-level verifier
101    /// returns, carrying the attested skip alongside the entries.
102    ///
103    /// Mirrors
104    /// [`DocumentSplitCounts::from_verified`](crate::DocumentSplitCounts::from_verified),
105    /// except that it is not the identity: `RankedPage` is rs-drive's
106    /// internal type and this is the client-facing one, so the rename
107    /// of `skipped` → `starting_rank` happens here, where the value
108    /// stops being "how far the walk skipped" and starts being "which
109    /// rank you are looking at".
110    pub fn from_verified(page: RankedPage) -> Self {
111        DocumentRankedEntries {
112            starting_rank: page.skipped,
113            entries: page.entries,
114        }
115    }
116
117    /// Decode the **unproven** ranked payload of a `getDocuments`
118    /// response — the `ResultData.ranked` variant a node returns for a
119    /// ranked request sent with `prove = false`.
120    ///
121    /// Order is preserved verbatim: the server emits entries in
122    /// ranking order and this decoder never re-sorts.
123    ///
124    /// This is a plain wire decode with **no cryptographic guarantee
125    /// whatsoever** — it is the "trust the node" path, and that applies
126    /// to [`Self::starting_rank`] every bit as much as to the entries:
127    /// an unproven page claiming to start at rank 4 is a claim, not a
128    /// fact. Prefer [`verify_ranked_top_k_proof`] (via rs-sdk's
129    /// `DocumentRankedEntries::fetch`) unless you are deliberately
130    /// reading from a node you already trust.
131    ///
132    /// A node that predates the wire `skipped` field leaves it unset;
133    /// that decodes to `starting_rank == 0`, which is the right answer
134    /// for the offset-less queries such a node could serve at all.
135    ///
136    /// ## Averages come back approximate here
137    ///
138    /// The wire's `avg` is a `double`, deliberately: these entries only
139    /// exist on this path, and a proof-verifying client reconstructs
140    /// the exact fixed point from the proof instead. To keep one
141    /// [`RankedEntryValue`] type across both paths this decoder
142    /// multiplies the double back up by [`crate::RANKED_AVG_SCALE`] and
143    /// rounds, so the [`RankedEntryValue::AvgFixedPoint`] it yields is a
144    /// **best-effort reconstruction, not the committed integer** — its
145    /// low digits are noise beyond `f64`'s ~15–16 significant decimal
146    /// digits. Render it, compare it loosely, but do not treat it as the
147    /// value grovedb ranked on; ask for the proof if you need that.
148    ///
149    /// # Errors
150    ///
151    /// - [`Error::EmptyVersion`] when the response carries no version.
152    /// - [`Error::ResponseDecodeError`] when the response is a V0
153    ///   response (which has no ranked shape), carries a proof rather
154    ///   than data, or carries a non-ranked `ResultData` variant.
155    /// - [`Error::ResponseDecodeError`] when an entry's `value` oneof
156    ///   is unset, or its `avg` is not a finite double that scales into
157    ///   `i128` range.
158    pub fn from_unproved_response(
159        response: &GetDocumentsResponse,
160    ) -> Result<(Self, ResponseMetadata), Error> {
161        let version = response.version.as_ref().ok_or(Error::EmptyVersion)?;
162        let ResponseVersion::V1(v1) = version else {
163            return Err(Error::ResponseDecodeError {
164                error: "ranked results are a V1-only response shape; got a V0 getDocuments \
165                        response. Ranked queries require protocol version 14+."
166                    .to_string(),
167            });
168        };
169        let metadata = v1.metadata.clone().ok_or(Error::EmptyResponseMetadata)?;
170        let (starting_rank, entries) = match v1.result.as_ref() {
171            Some(get_documents_response_v1::Result::Data(ResultData {
172                variant: Some(result_data::Variant::Ranked(ranked)),
173            })) => (
174                ranked.skipped.unwrap_or(0),
175                ranked
176                    .entries
177                    .iter()
178                    .map(ranked_entry_from_proto)
179                    .collect::<Result<Vec<_>, _>>()?,
180            ),
181            Some(get_documents_response_v1::Result::Proof(_)) => {
182                return Err(Error::ResponseDecodeError {
183                    error: "the response carries a proof, not unproven ranked entries; verify \
184                            it with `verify_ranked_top_k_proof` instead of decoding it"
185                        .to_string(),
186                });
187            }
188            other => {
189                return Err(Error::ResponseDecodeError {
190                    error: format!(
191                        "expected a `ResultData.ranked` payload for a ranked request, got \
192                         {}. A response on another variant means the node routed the \
193                         request to a different executor — check that the request carries a \
194                         `group_by` and a single `order_by` naming the single `select`'s \
195                         aggregate (`$count` for `COUNT(*)`).",
196                        result_variant_name(other)
197                    ),
198                });
199            }
200        };
201        Ok((
202            DocumentRankedEntries {
203                starting_rank,
204                entries,
205            },
206            metadata,
207        ))
208    }
209}
210
211/// The received-but-unexpected shape of a `getDocuments` V1 result, by
212/// **name only** — never the payload. Interpolating the payload into an
213/// error would make the message (and any log line carrying it) grow
214/// with an untrusted response, and could copy returned document bytes
215/// into logs. Shared by the ranked and having-range decoders.
216pub(crate) fn result_variant_name(
217    result: Option<&get_documents_response_v1::Result>,
218) -> &'static str {
219    match result {
220        None => "an absent result",
221        Some(get_documents_response_v1::Result::Proof(_)) => "a proof",
222        Some(get_documents_response_v1::Result::Data(ResultData { variant })) => match variant {
223            None => "a ResultData with no variant",
224            Some(result_data::Variant::Documents(_)) => "a ResultData.documents payload",
225            Some(result_data::Variant::Counts(_)) => "a ResultData.counts payload",
226            Some(result_data::Variant::Sums(_)) => "a ResultData.sums payload",
227            Some(result_data::Variant::Averages(_)) => "a ResultData.averages payload",
228            Some(result_data::Variant::Ranked(_)) => "a ResultData.ranked payload",
229            Some(result_data::Variant::Chained(_)) => "a ResultData.chained payload",
230            Some(result_data::Variant::Composite(_)) => "a ResultData.composite payload",
231        },
232    }
233}
234
235/// Decode one wire [`ProtoRankedEntry`] into rs-drive's
236/// [`RankedEntry`].
237///
238/// The `avg` arm re-scales the wire's `double` into the fixed-point
239/// `i128` [`RankedEntryValue`] carries, so callers see one type
240/// regardless of which path produced the page. The round trip is lossy
241/// in one direction only — the server divided an exact integer by
242/// [`RANKED_AVG_SCALE`], we multiply back and round — so the result is
243/// the closest fixed point to what the node reported, not necessarily
244/// the one it committed to. `from_unproved_response` documents that;
245/// exactness lives on the proof path.
246///
247/// A non-finite `avg`, or one that scales past `i128`, is rejected
248/// rather than saturated: `as` casts would silently turn `NaN` into
249/// `0` (an average of zero — a plausible-looking lie) and an
250/// out-of-range double into `i128::MIN`/`MAX`. Every legitimate value
251/// fits comfortably, since `|sum| ≤ i64::MAX` bounds the true fixed
252/// point at `i64::MAX * 10^19 ≈ 9.2e37 < i128::MAX`.
253pub(crate) fn ranked_entry_from_proto(entry: &ProtoRankedEntry) -> Result<RankedEntry, Error> {
254    let value = match entry.value.as_ref() {
255        Some(ranked_entry::Value::Count(count)) => RankedEntryValue::Count(*count),
256        Some(ranked_entry::Value::Sum(sum)) => RankedEntryValue::Sum(*sum),
257        Some(ranked_entry::Value::Avg(avg)) => {
258            let scaled = avg * (RANKED_AVG_SCALE as f64);
259            // `i128::MIN as f64` is exactly −2^127; `i128::MAX as f64`
260            // rounds *up* to 2^127, hence the asymmetric comparisons.
261            if !scaled.is_finite() || scaled < (i128::MIN as f64) || scaled >= -(i128::MIN as f64) {
262                return Err(Error::ResponseDecodeError {
263                    error: format!(
264                        "`avg` must be a finite double that scales into i128 range when \
265                         multiplied by {RANKED_AVG_SCALE}, got {avg}"
266                    ),
267                });
268            }
269            RankedEntryValue::AvgFixedPoint(scaled.round() as i128)
270        }
271        None => {
272            return Err(Error::ResponseDecodeError {
273                error: "ranked entry carries no `value`; the server always sets exactly one \
274                        of `count` / `sum` / `avg`"
275                    .to_string(),
276            });
277        }
278    };
279    Ok(RankedEntry {
280        // Present exactly on `IN`-pinned responses; the wire's absent
281        // state maps to the drive type's `None` untouched.
282        in_key: entry.in_key.clone(),
283        key: entry.key.clone(),
284        value,
285    })
286}
287
288/// Verify a grovedb indexed-axis top-k proof **and the surrounding
289/// tenderdash commit**, returning the reconstructed root hash and the
290/// [`RankedPage`] it commits to.
291///
292/// The page is returned whole rather than as a bare entry list because
293/// [`RankedPage::skipped`] is verified evidence in its own right: it is
294/// re-derived from the counted subtree commitments in the proof bytes,
295/// so it pins each entry to an absolute rank, and on a page past the
296/// end of the ranking it is the *only* payload — an attested total
297/// population under an empty entry list.
298///
299/// Thin tenderdash-composition wrapper over
300/// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`] in rs-drive
301/// (which does the merk-level verification). Both sides derive the
302/// proved subtree from the same
303/// `DriveDocumentRankedQuery::indexed_property_name_tree_path`, so
304/// prover and verifier cannot drift on *which* ranking is being
305/// checked, and grovedb re-executes the proof against the
306/// `(axis, k, offset, descending)` traversal rebuilt from the request —
307/// a proof of one ranking does not cover another.
308///
309/// ## The root hash is the whole point
310///
311/// The merk-level verifier returning `Ok` is **not** by itself
312/// evidence of anything. Sweeping every bit of a real ranked envelope
313/// shows why: most flips do error out, but roughly 9% of them (bytes
314/// of sibling-subtree hashes inside the ancestor layer proofs) verify
315/// cleanly and return the correct entries — under a *different*
316/// reconstructed root hash. What rejects those is the
317/// [`verify_tenderdash_proof`] call below, which checks the
318/// reconstructed root against the quorum-signed app hash for the
319/// response's block. This function exists so that composition can
320/// never be skipped by accident: there is no way to obtain the entries
321/// from it without the binding having run.
322///
323/// The `RootHash` is returned as well, already bound, so callers can
324/// log or cross-check it (e.g. against a root hash they verified for a
325/// different query at the same height). Callers must not treat it as
326/// something they still have to check.
327///
328/// `platform_version` must be the protocol version `mtd` names, or the latest known
329/// version when `mtd` names a newer one; otherwise the answer is refused with
330/// [`Error::ProofProtocolVersionMismatch`].
331pub fn verify_ranked_top_k_proof(
332    query: &DriveDocumentRankedQuery,
333    proof: &Proof,
334    mtd: &ResponseMetadata,
335    platform_version: &PlatformVersion,
336    provider: &dyn ContextProvider,
337) -> Result<(RootHash, RankedPage), Error> {
338    let (root_hash, page) = query
339        .verify_ranked_top_k_proof(
340            document_query_proof_bytes(proof, mtd, platform_version)?,
341            platform_version,
342        )
343        .map_drive_error(proof, mtd)?;
344
345    verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
346
347    Ok((root_hash, page))
348}
349
350/// Reject the generic [`FromProof`] entry point for
351/// [`DocumentRankedEntries`].
352///
353/// `DocumentRankedEntries` is reached from rs-sdk via the
354/// `FromProof<DocumentQuery>` impl defined alongside the SDK's
355/// `DocumentQuery` type (see
356/// `rs-sdk/src/platform/documents/document_ranked_entries.rs`), which
357/// resolves the `(axis, descending, k, offset)` tuple and the covering
358/// index from the request's `(select, group_by, order_by, limit,
359/// offset)` shape plus the data contract. The generic
360/// `FromProof<Q: TryInto<DriveDocumentQuery>>` path carries neither —
361/// `DriveDocumentQuery` has no notion of a ranking — so it errors out
362/// explicitly rather than verifying the wrong thing; calling this impl
363/// directly is a programmer mistake.
364impl<'dq, Q> FromProof<Q> for DocumentRankedEntries
365where
366    Q: TryInto<DriveDocumentQuery<'dq>> + Clone + 'dq,
367    Q::Error: std::fmt::Display,
368{
369    type Request = Q;
370    type Response = GetDocumentsResponse;
371
372    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
373        _request: I,
374        _response: O,
375        _network: Network,
376        _platform_version: &PlatformVersion,
377        _provider: &'a dyn ContextProvider,
378    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
379    where
380        Self: 'a,
381    {
382        Err(Error::RequestError {
383            error: "DocumentRankedEntries can't be verified via the generic FromProof path; \
384                 call DocumentRankedEntries::fetch on a DocumentQuery carrying \
385                 .with_select(<aggregate>), .with_group_by(<property>), \
386                 .order_by_selected_aggregate(<direction>) and .with_limit(n), which \
387                 resolves the ranking axis and the covering index from the data contract"
388                .to_string(),
389        })
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    //! Offline tests for the parts of the ranked surface that need
396    //! neither a grovedb proof nor a populated Drive:
397    //!
398    //! - the unproven `ResultData.ranked` decode, including order
399    //!   preservation, negative fixed-point averages, and the
400    //!   malformed-length rejection;
401    //! - the response-shape rejections (proof instead of data, wrong
402    //!   `ResultData` variant, V0 response);
403    //! - the generic `FromProof<Q>` impl that intentionally errors to
404    //!   prevent a silently-wrong verification.
405    //!
406    //! Proof verification itself is exercised end-to-end by rs-drive's
407    //! `drive_document_ranked_query::tests` (prover and verifier run
408    //! against a real Drive, with a bit-flip sweep asserting no tamper
409    //! survives with the honest root hash) and by rs-drive-abci's
410    //! `ranked_tests` (wire encoding of the same values). Reproducing
411    //! it here would need a populated Drive, which is outside this
412    //! crate's feature surface.
413    use super::*;
414    use dapi_grpc::platform::v0::get_documents_response::get_documents_response_v1::{
415        Documents, RankedEntries,
416    };
417    use dapi_grpc::platform::v0::get_documents_response::GetDocumentsResponseV1;
418    use drive::query::RANKED_AVG_SCALE;
419
420    fn count_entry(key: &str, count: u64) -> ProtoRankedEntry {
421        ProtoRankedEntry {
422            in_key: None,
423            key: key.as_bytes().to_vec(),
424            value: Some(ranked_entry::Value::Count(count)),
425        }
426    }
427
428    /// An entry as the *server* would emit it for a group whose exact
429    /// fixed-point average is `fixed_point`: the wire carries
430    /// `fixed_point as f64 / RANKED_AVG_SCALE as f64`, the same
431    /// conversion `RankedEntryValue::as_f64` performs in rs-drive-abci.
432    fn avg_entry(key: &str, fixed_point: i128) -> ProtoRankedEntry {
433        avg_entry_raw(key, (fixed_point as f64) / (RANKED_AVG_SCALE as f64))
434    }
435
436    /// An entry carrying an arbitrary double, for the malformed cases
437    /// that no fixed point maps to.
438    fn avg_entry_raw(key: &str, avg: f64) -> ProtoRankedEntry {
439        ProtoRankedEntry {
440            in_key: None,
441            key: key.as_bytes().to_vec(),
442            value: Some(ranked_entry::Value::Avg(avg)),
443        }
444    }
445
446    fn response_with(result: get_documents_response_v1::Result) -> GetDocumentsResponse {
447        GetDocumentsResponse {
448            version: Some(ResponseVersion::V1(GetDocumentsResponseV1 {
449                result: Some(result),
450                metadata: Some(ResponseMetadata {
451                    height: 42,
452                    ..Default::default()
453                }),
454            })),
455        }
456    }
457
458    fn ranked_response(entries: Vec<ProtoRankedEntry>) -> GetDocumentsResponse {
459        paged_ranked_response(entries, None)
460    }
461
462    fn paged_ranked_response(
463        entries: Vec<ProtoRankedEntry>,
464        skipped: Option<u64>,
465    ) -> GetDocumentsResponse {
466        response_with(get_documents_response_v1::Result::Data(ResultData {
467            variant: Some(result_data::Variant::Ranked(RankedEntries {
468                entries,
469                skipped,
470            })),
471        }))
472    }
473
474    /// The headline decode: `SELECT AVG(grade) … GROUP BY restaurantId
475    /// ORDER BY grade DESC LIMIT 3`. Entry order is the ranking order
476    /// and must survive the decode verbatim, and each double must
477    /// re-scale into the fixed point it was rendered from.
478    ///
479    /// These particular averages survive the double round trip *bit for
480    /// bit* — `95`, `85` and `10.5` times `10^19` all fit in `f64`'s 53
481    /// significand bits — so the assertion can be exact. That is a
482    /// property of the fixtures, not a guarantee of the wire format:
483    /// `from_unproved_response` reconstructs a best-effort fixed point,
484    /// and an average with more significant digits than `f64` holds
485    /// would come back slightly off. Exactness lives on the proof path.
486    #[test]
487    fn decodes_avg_entries_preserving_ranking_order() {
488        let response = ranked_response(vec![
489            avg_entry("gamma", 95 * RANKED_AVG_SCALE),
490            avg_entry("alpha", 85 * RANKED_AVG_SCALE),
491            // 21/2 = 10.5 — a non-integral average, so the fixed-point
492            // floor is actually exercised rather than a round multiple.
493            avg_entry("epsilon", (21 * RANKED_AVG_SCALE).div_euclid(2)),
494        ]);
495
496        let (decoded, metadata) = DocumentRankedEntries::from_unproved_response(&response)
497            .expect("a well-formed ranked payload decodes");
498
499        assert_eq!(metadata.height, 42, "metadata rides along with the entries");
500        let keys: Vec<&[u8]> = decoded.entries.iter().map(|e| e.key.as_slice()).collect();
501        assert_eq!(
502            keys,
503            vec![
504                b"gamma".as_slice(),
505                b"alpha".as_slice(),
506                b"epsilon".as_slice()
507            ],
508            "entry order is the ranking order and must not be re-sorted"
509        );
510        assert_eq!(
511            decoded.entries[2].value,
512            RankedEntryValue::AvgFixedPoint((21 * RANKED_AVG_SCALE).div_euclid(2))
513        );
514        assert_eq!(
515            decoded.entries[2].value.as_f64(),
516            10.5,
517            "dividing by RANKED_AVG_SCALE recovers the average a caller renders"
518        );
519    }
520
521    /// Averages are signed: a group whose summable property is
522    /// negative ranks below zero, and the sign must survive the double
523    /// round trip. A decoder that mishandled it would turn `-0.5` into
524    /// a positive number and silently invert the ranking's meaning.
525    ///
526    /// The last entry is the largest magnitude the axis can actually
527    /// produce — `sum = i64::MIN` over a single document — which pins
528    /// that the `i128`-range guard rejects only genuinely impossible
529    /// doubles, not legitimate extremes.
530    #[test]
531    fn decodes_negative_averages() {
532        let negative_half = (-RANKED_AVG_SCALE).div_euclid(2);
533        let extreme = (i64::MIN as i128) * RANKED_AVG_SCALE;
534        let response = ranked_response(vec![
535            avg_entry("above", RANKED_AVG_SCALE),
536            avg_entry("below", negative_half),
537            avg_entry("floor", extreme),
538        ]);
539
540        let (decoded, _) = DocumentRankedEntries::from_unproved_response(&response)
541            .expect("negative averages are well-formed");
542
543        assert_eq!(
544            decoded.entries[1].value,
545            RankedEntryValue::AvgFixedPoint(negative_half)
546        );
547        assert_eq!(decoded.entries[1].value.as_f64(), -0.5);
548        assert_eq!(
549            decoded.entries[2].value,
550            RankedEntryValue::AvgFixedPoint(extreme),
551            "the most negative average the axis can hold is decoded, not rejected"
552        );
553    }
554
555    /// Count entries decode to `u64` untouched — including values
556    /// above 2^53, which is why the wire field is `jstype = JS_STRING`.
557    #[test]
558    fn decodes_count_entries() {
559        let response = ranked_response(vec![
560            count_entry("delta", 4),
561            count_entry("beta", 3),
562            count_entry("huge", u64::MAX),
563        ]);
564
565        let (decoded, _) = DocumentRankedEntries::from_unproved_response(&response)
566            .expect("count entries are well-formed");
567
568        assert_eq!(
569            decoded.entries.iter().map(|e| e.value).collect::<Vec<_>>(),
570            vec![
571                RankedEntryValue::Count(4),
572                RankedEntryValue::Count(3),
573                RankedEntryValue::Count(u64::MAX)
574            ]
575        );
576    }
577
578    /// Sums are signed `sint64` on the wire, so a negative running
579    /// total (a group of refunds) must decode as-is.
580    #[test]
581    fn decodes_signed_sum_entries() {
582        let response = ranked_response(vec![ProtoRankedEntry {
583            in_key: None,
584            key: b"refunds".to_vec(),
585            value: Some(ranked_entry::Value::Sum(-1_000)),
586        }]);
587
588        let (decoded, _) = DocumentRankedEntries::from_unproved_response(&response)
589            .expect("signed sums are well-formed");
590
591        assert_eq!(decoded.entries[0].value, RankedEntryValue::Sum(-1_000));
592    }
593
594    /// An empty ranking is a legitimate answer, not an error: the
595    /// index simply has no groups yet.
596    #[test]
597    fn decodes_an_empty_ranking() {
598        let (decoded, _) = DocumentRankedEntries::from_unproved_response(&ranked_response(vec![]))
599            .expect("an empty ranking is well-formed");
600        assert!(decoded.entries.is_empty());
601        assert_eq!(decoded.starting_rank, 0);
602    }
603
604    /// `skipped` is what makes a page a ranking rather than a list.
605    /// A single entry at rank 4 is the *5th* best group, and the decode
606    /// must carry that through — dropping it would leave the caller
607    /// unable to distinguish it from the winner.
608    #[test]
609    fn decodes_the_starting_rank_of_a_page() {
610        let (decoded, _) = DocumentRankedEntries::from_unproved_response(&paged_ranked_response(
611            vec![avg_entry("epsilon", 10 * RANKED_AVG_SCALE)],
612            Some(4),
613        ))
614        .expect("a paged ranked payload decodes");
615        assert_eq!(decoded.starting_rank, 4);
616        assert_eq!(decoded.entries.len(), 1);
617    }
618
619    /// An offset past the end: no entries, but `skipped` still carries
620    /// the population. Empty entries plus a positive rank is a
621    /// meaningful answer ("there are only 12 groups"), not a
622    /// contradiction to be normalized away.
623    #[test]
624    fn decodes_a_page_past_the_end_of_the_ranking() {
625        let (decoded, _) =
626            DocumentRankedEntries::from_unproved_response(&paged_ranked_response(vec![], Some(12)))
627                .expect("a past-the-end page decodes");
628        assert!(decoded.entries.is_empty());
629        assert_eq!(decoded.starting_rank, 12);
630    }
631
632    /// A node that predates the wire field leaves `skipped` unset.
633    /// That must read as rank 0 — the right answer for the offset-less
634    /// queries such a node could serve at all — rather than as a decode
635    /// failure.
636    #[test]
637    fn an_absent_skipped_field_decodes_as_rank_zero() {
638        let (decoded, _) = DocumentRankedEntries::from_unproved_response(&paged_ranked_response(
639            vec![count_entry("delta", 4)],
640            None,
641        ))
642        .expect("an absent `skipped` is not a decode failure");
643        assert_eq!(decoded.starting_rank, 0);
644    }
645
646    /// An `avg` that cannot be a scaled fixed point is rejected rather
647    /// than cast. `as` casts on `f64 -> i128` saturate and map `NaN` to
648    /// `0`, so a malformed wire value would otherwise decode as a
649    /// confident average of zero (or of `i128::MIN`) — a
650    /// plausible-looking lie is far worse than a loud failure.
651    ///
652    /// This is the double-shaped replacement for the old
653    /// exactly-16-bytes length check: a `double` field has no length to
654    /// validate, but it does have values no legitimate average maps to.
655    #[test]
656    fn rejects_an_avg_that_is_not_a_scaled_fixed_point() {
657        for avg in [
658            f64::NAN,
659            f64::INFINITY,
660            f64::NEG_INFINITY,
661            // Past i128 range once multiplied by 10^19.
662            1e30,
663            -1e30,
664        ] {
665            let response = ranked_response(vec![avg_entry_raw("alpha", avg)]);
666            let err = match DocumentRankedEntries::from_unproved_response(&response) {
667                Err(err) => err,
668                Ok(decoded) => panic!("an `avg` of {avg} must be rejected, decoded {decoded:?}"),
669            };
670            let message = format!("{err}");
671            assert!(
672                message.contains("finite double") && message.contains("i128 range"),
673                "the rejection must name the contract it violates; got {message}"
674            );
675        }
676    }
677
678    /// A `RankedEntry` with no `value` set means the server (or a
679    /// middlebox) produced a message this client cannot interpret.
680    /// Defaulting it to zero would fabricate a ranking position.
681    #[test]
682    fn rejects_an_entry_with_no_value() {
683        let response = ranked_response(vec![ProtoRankedEntry {
684            in_key: None,
685            key: b"alpha".to_vec(),
686            value: None,
687        }]);
688        let err = DocumentRankedEntries::from_unproved_response(&response)
689            .expect_err("an entry with no value must be rejected");
690        assert!(format!("{err}").contains("no `value`"));
691    }
692
693    /// A proved response reaching the unproven decoder is a caller
694    /// mistake with a security consequence — silently returning
695    /// nothing (or erroring vaguely) would let a caller believe an
696    /// unverified path was the verified one. Name the right entry
697    /// point in the error.
698    #[test]
699    fn rejects_a_proof_response() {
700        let response = response_with(get_documents_response_v1::Result::Proof(Proof::default()));
701        let err = DocumentRankedEntries::from_unproved_response(&response)
702            .expect_err("a proof is not an unproven ranked payload");
703        assert!(format!("{err}").contains("verify_ranked_top_k_proof"));
704    }
705
706    /// A response on another `ResultData` variant means the node
707    /// routed the request somewhere else entirely (no ranking operand
708    /// reached it, or it landed on the count / sum / average
709    /// executor). Report the shape rather than an empty list.
710    #[test]
711    fn rejects_a_non_ranked_result_variant() {
712        let response = response_with(get_documents_response_v1::Result::Data(ResultData {
713            variant: Some(result_data::Variant::Documents(Documents {
714                documents: Vec::new(),
715            })),
716        }));
717        let err = DocumentRankedEntries::from_unproved_response(&response)
718            .expect_err("a documents payload is not a ranked one");
719        assert!(format!("{err}").contains("ResultData.ranked"));
720    }
721
722    /// The V0 `getDocuments` response predates the whole SQL-shaped
723    /// surface and has no ranked variant; a node answering V0 is a
724    /// node that cannot serve this query at all.
725    #[test]
726    fn rejects_a_v0_response() {
727        let response = GetDocumentsResponse {
728            version: Some(ResponseVersion::V0(Default::default())),
729        };
730        let err = DocumentRankedEntries::from_unproved_response(&response)
731            .expect_err("V0 has no ranked shape");
732        assert!(format!("{err}").contains("V1-only"));
733    }
734
735    /// A versionless response is a decode failure, not an empty
736    /// ranking.
737    #[test]
738    fn rejects_a_versionless_response() {
739        let err =
740            DocumentRankedEntries::from_unproved_response(&GetDocumentsResponse { version: None })
741                .expect_err("a versionless response must be rejected");
742        assert!(matches!(err, Error::EmptyVersion));
743    }
744
745    /// `from_verified` is what the SDK's proof path wraps a verified
746    /// [`RankedPage`] with. Both halves must survive: dropping
747    /// `skipped` here would silently turn every proved page into a
748    /// claim about rank 0.
749    #[test]
750    fn from_verified_carries_both_halves_of_the_page() {
751        let entries = vec![
752            RankedEntry {
753                in_key: None,
754                key: b"gamma".to_vec(),
755                value: RankedEntryValue::Count(9),
756            },
757            RankedEntry {
758                in_key: None,
759                key: b"alpha".to_vec(),
760                value: RankedEntryValue::Count(2),
761            },
762        ];
763        let wrapped = DocumentRankedEntries::from_verified(RankedPage {
764            skipped: 7,
765            entries: entries.clone(),
766        });
767        assert_eq!(wrapped.entries, entries);
768        assert_eq!(
769            wrapped.starting_rank, 7,
770            "the attested skip is the page's rank base and must not be dropped"
771        );
772    }
773
774    // The generic `FromProof<Q>` rejection is covered by the SDK's
775    // tests, which can construct a valid `DriveDocumentQuery` via
776    // dpp's `fixtures-and-mocks` feature. drive-proof-verifier itself
777    // doesn't depend on `dpp/fixtures-and-mocks` outside dev-deps for
778    // the contract fixtures that path needs.
779}