Skip to main content

drive/query/drive_document_ranked_query/
execute_top_k.rs

1//! The two ranked executors on [`DriveDocumentRankedQuery`]: a direct
2//! read of the axis secondary, and generation of the equivalent proof.
3//!
4//! Both are thin — all of the work happens inside grovedb, which walks
5//! the pre-sorted secondary Merk directly. That is the whole point of the
6//! ranked surface: no value trees are opened, no documents are
7//! materialized, and the cost is `O(log n + k)` rather than
8//! `O(groups × log n)`.
9//!
10//! Whole module is gated `feature = "server"` via the parent's
11//! `pub mod execute_top_k;` declaration.
12
13use super::branches::{axis_keys_to_ranked, decompose_branch_paths, read_branched_union};
14use super::{DriveDocumentRankedQuery, RankedPage};
15use crate::drive::Drive;
16use crate::error::drive::DriveError;
17use crate::error::Error;
18use dpp::version::PlatformVersion;
19use grovedb::query_result_type::QueryResultType;
20use grovedb::{PathQuery, PathQueryRun, TransactionArg};
21use grovedb_costs::CostContext;
22use grovedb_query::AxisQuery;
23
24impl DriveDocumentRankedQuery<'_> {
25    /// Read one page of the ranking directly from the axis secondary:
26    /// the `k` groups starting at rank `offset`. Entries come back in
27    /// ranking order — see [`DriveDocumentRankedQuery::descending`] for
28    /// the direction and the tie contract.
29    ///
30    /// Fewer than `k` entries is normal (the index simply has fewer
31    /// groups than `offset + k`) and is not an error. On an `IN`-pinned
32    /// request, an element whose branch chain is missing at ANY depth —
33    /// the branch key itself, or any deeper pinned segment under a
34    /// *present* key — contributes an **empty branch** (union
35    /// semantics), exactly as the proved envelope authenticates it, and
36    /// the union is served from **one committed state**: the branched
37    /// read always runs under a grovedb snapshot read transaction, so
38    /// every per-branch probe and walk reads the same RocksDB snapshot
39    /// (a caller transaction is rejected on this shape, mirroring the
40    /// branched prover — read per prefix element under a transaction).
41    /// A missing path under a single `==` pin *is* an error rather than
42    /// an empty result: the indexed property-name tree is created when
43    /// the contract is registered, so its absence means the
44    /// contract-level state is not what the request claims, not that
45    /// the ranking is empty. (An index with no documents yet has the
46    /// tree, with an empty secondary, and yields an empty entry list.)
47    ///
48    /// The paginated grovedb primitive is used unconditionally, with
49    /// `offset = 0` standing in for an unpaginated request, so the
50    /// no-proof and prove paths read the same code path in grovedb and
51    /// cannot drift on the walk's semantics for offset-free queries.
52    ///
53    /// # The offset is counted, not walked
54    ///
55    /// grovedb descends the secondary reading each subtree's aggregate
56    /// count off its link, and collapses any subtree that fits entirely
57    /// inside the remaining offset instead of stepping through it. The
58    /// skip therefore costs `O(log n)` at any offset rather than one
59    /// iterator step and one decode per skipped entry, and an offset at
60    /// or past the population is answered from the root's own count with
61    /// no descent at all — the cheapest request on this surface rather
62    /// than the most expensive. `offset = 0` keeps the plain iterator
63    /// path and never touches the tree, so the common unpaginated
64    /// request costs exactly what it always did.
65    ///
66    /// That is what makes an uncapped `OFFSET` safe rather than merely
67    /// tolerated. Ranked queries carry no fee, cannot be cancelled once
68    /// dispatched, and share their rate budget with state transitions
69    /// rather than having one of their own, so a skip whose cost grew
70    /// with the offset would be an unmetered lever for any
71    /// unauthenticated caller. It does not grow.
72    ///
73    /// [`RankedPage::skipped`] comes back from grovedb rather than being
74    /// echoed from the request: it is the requested offset when the skip
75    /// succeeded, and the secondary's whole population when the walk ran
76    /// out of groups first. That is the same quantity the proved path
77    /// attests, so the two no longer disagree — though on this path it is
78    /// the node's unverified claim rather than an attested value, exactly
79    /// like the entries beside it. See [`RankedPage::skipped`].
80    pub fn execute_top_k_no_proof(
81        &self,
82        drive: &Drive,
83        transaction: TransactionArg,
84        platform_version: &PlatformVersion,
85    ) -> Result<RankedPage, Error> {
86        self.reject_offset_with_branches()?;
87        if self.prefix_branches.len() > 1 {
88            // ONE grovedb call for the whole union, pinned to one
89            // committed state and merged with the shared comparator —
90            // the entire sequence lives in
91            // `branches::read_branched_union`, shared with the
92            // having-range surface so the two cannot drift. `offset` is
93            // grammar-rejected with `IN`, so `skipped` is always 0 here.
94            let paths = (0..self.prefix_branches.len())
95                .map(|branch| self.indexed_property_name_tree_path(branch))
96                .collect::<Result<Vec<_>, Error>>()?;
97            let entries = read_branched_union(
98                &drive.grove,
99                "ranked",
100                &self.prefix_branches,
101                &paths,
102                self.axis,
103                AxisQuery::top_k(
104                    self.axis.into(),
105                    self.k,
106                    self.offset as u64,
107                    self.descending,
108                ),
109                self.k as usize,
110                self.descending,
111                transaction,
112                &platform_version.drive.grove_version,
113            )?;
114            return Ok(RankedPage {
115                skipped: 0,
116                entries,
117            });
118        }
119        self.execute_top_k_no_proof_branch(0, drive, transaction, platform_version)
120    }
121
122    /// One branch's page — the entire pre-`IN` executor, parameterized
123    /// by which prefix branch's terminal tree it walks.
124    fn execute_top_k_no_proof_branch(
125        &self,
126        branch: usize,
127        drive: &Drive,
128        transaction: TransactionArg,
129        platform_version: &PlatformVersion,
130    ) -> Result<RankedPage, Error> {
131        let grove_version = &platform_version.drive.grove_version;
132        let path = self.indexed_property_name_tree_path(branch)?;
133
134        // The cost is dropped rather than `.unwrap()`-ed:
135        // `CostContext::unwrap` is infallible (it drops the cost field)
136        // but reads like a panicking unwrap at the call site. Dropping it
137        // is all there is to do with it — nothing meters a query on this
138        // surface. grovedb computes the `OperationCost` because its API
139        // always does, and it ends here.
140        let path_query = PathQuery::new_axis(
141            path,
142            AxisQuery::top_k(
143                self.axis.into(),
144                self.k,
145                self.offset as u64,
146                self.descending,
147            )
148            .keys_only(),
149        );
150        let CostContext { value, cost: _ } = drive.grove.run_path_query(
151            &path_query,
152            true,
153            true,
154            true,
155            QueryResultType::QueryKeyElementPairResultType,
156            transaction,
157            grove_version,
158        );
159        let run = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
160        let PathQueryRun::AxisKeys { keys, skipped } = run else {
161            return Err(Error::Drive(DriveError::CorruptedDriveState(
162                "a keys-only ranked read returned a different result shape".to_string(),
163            )));
164        };
165        let entries = axis_keys_to_ranked(self.axis, keys)?;
166        // A `RankedPage` traversal always attests its skip; its absence
167        // would mean grovedb answered a different traversal than asked.
168        let skipped = skipped.ok_or_else(|| {
169            Error::Drive(DriveError::CorruptedDriveState(
170                "a paginated ranked read carried no skip attestation".to_string(),
171            ))
172        })?;
173
174        // `k` is the contract with the caller, and on the prove path it
175        // is re-checked inside the proof envelope. Asserting it here too
176        // keeps the no-proof and prove responses shape-identical: a
177        // caller must never see an over-long list from one path and a
178        // capped one from the other.
179        if entries.len() > self.k as usize {
180            return Err(Error::Drive(DriveError::CorruptedDriveState(format!(
181                "ranked {:?} read returned {} entries for k = {}",
182                self.axis,
183                entries.len(),
184                self.k
185            ))));
186        }
187        Ok(RankedPage { skipped, entries })
188    }
189
190    /// Generate the grovedb indexed-axis paginated top-k proof for this
191    /// query.
192    ///
193    /// The envelope commits the walked secondary entries, the number of
194    /// entries skipped to reach them, the primary's root hash, the
195    /// sibling axes' root hashes, and a per-ancestor attestation chain
196    /// up to the grovedb root — so the client reconstructs the platform
197    /// root hash from it. `(axis, k, offset, descending)` bind by
198    /// RECONSTRUCTION, not echo: the verifier rebuilds the same
199    /// `PathQuery` from the request and
200    /// [`grovedb::GroveDb::verify_path_query`] re-executes the proof
201    /// against that traversal, so a proof for a different ranking or a
202    /// different page fails to cover it; that is why `k` is validated
203    /// rather than clamped upstream (a clamped `k` would produce a page
204    /// the client's reconstruction did not ask for).
205    ///
206    /// The paginated primitive is used unconditionally, with
207    /// `offset = 0` for offset-free requests, so there is exactly one
208    /// proof shape on this surface: a client never has to guess which of
209    /// two envelope formats a server produced.
210    ///
211    /// Verified by
212    /// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`](crate::query::DriveDocumentRankedQuery::verify_ranked_top_k_proof).
213    ///
214    /// # Empty rankings prove fine
215    ///
216    /// An index holding no documents has an empty axis secondary. The
217    /// older non-paginated prover refused that outright ("Cannot create
218    /// proof for empty tree"), which made a freshly registered contract
219    /// unqueryable with `prove = true`; the paginated prover emits a
220    /// guaranteed-empty range against the secondary instead, so the
221    /// proved and unproven paths agree on empty state. Pinned by the
222    /// `ranking_an_empty_index_reads_empty_and_proves_empty` test.
223    pub fn execute_top_k_with_proof(
224        &self,
225        drive: &Drive,
226        transaction: TransactionArg,
227        platform_version: &PlatformVersion,
228    ) -> Result<Vec<u8>, Error> {
229        self.reject_offset_with_branches()?;
230        // grovedb's `prove_query` — since the indexed-axis prover
231        // retirement, the only proof surface — proves COMMITTED state
232        // only: it takes one internal snapshot and threads it through
233        // every proof layer, and cannot see the caller's transaction.
234        // Serving a proof for a different snapshot than the unproved
235        // read would silently desynchronize the two paths, so a
236        // transactional prove fails closed, single-prefix and branched
237        // alike.
238        if transaction.is_some() {
239            return Err(Error::Drive(DriveError::NotSupported(
240                "a ranked proof is generated from committed state only: grovedb's \
241                 prove_query cannot see the caller's transaction — commit first",
242            )));
243        }
244        if self.prefix_branches.len() > 1 {
245            // One grovedb **branched** envelope: shared ancestor layers
246            // once, one multi-key proof at the branching level, one
247            // secondary proof per branch — a single proof with a single
248            // root hash. The verifier re-derives the branch set from
249            // the request, so a dropped, duplicated, or reordered
250            // branch fails there.
251            let grove_version = &platform_version.drive.grove_version;
252            let paths = (0..self.prefix_branches.len())
253                .map(|branch| self.indexed_property_name_tree_path(branch))
254                .collect::<Result<Vec<_>, Error>>()?;
255            let (prefix, keys, suffix) = decompose_branch_paths(&paths)?;
256            let path_query = PathQuery::new_branched_axis(
257                prefix,
258                keys,
259                suffix,
260                AxisQuery::top_k(
261                    self.axis.into(),
262                    self.k,
263                    self.offset as u64,
264                    self.descending,
265                ),
266            );
267            let CostContext { value, cost: _ } =
268                drive.grove.prove_query(&path_query, None, grove_version);
269            return value.map_err(|e| Error::GroveDB(Box::new(e)));
270        }
271        self.execute_top_k_with_proof_branch(0, drive, platform_version)
272    }
273
274    /// One branch's proof — the entire pre-`IN` prover, parameterized by
275    /// the prefix branch.
276    fn execute_top_k_with_proof_branch(
277        &self,
278        branch: usize,
279        drive: &Drive,
280        platform_version: &PlatformVersion,
281    ) -> Result<Vec<u8>, Error> {
282        let grove_version = &platform_version.drive.grove_version;
283        let path = self.indexed_property_name_tree_path(branch)?;
284        let path_query = PathQuery::new_axis_top_k(
285            path,
286            self.axis.into(),
287            self.k,
288            self.offset as u64,
289            self.descending,
290        );
291        // Same destructure-don't-unwrap rationale as the no-proof arm.
292        let CostContext { value, cost: _ } =
293            drive.grove.prove_query(&path_query, None, grove_version);
294        value.map_err(|e| Error::GroveDB(Box::new(e)))
295    }
296}