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