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. 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.axis,
108                AxisQuery::top_k(
109                    self.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,
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.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 = axis_keys_to_ranked(self.axis, keys)?;
171        // A `RankedPage` traversal always attests its skip; its absence
172        // would mean grovedb answered a different traversal than asked.
173        let skipped = skipped.ok_or_else(|| {
174            Error::Drive(DriveError::CorruptedDriveState(
175                "a paginated ranked read carried no skip attestation".to_string(),
176            ))
177        })?;
178
179        // `k` is the contract with the caller, and on the prove path it
180        // is re-checked inside the proof envelope. Asserting it here too
181        // keeps the no-proof and prove responses shape-identical: a
182        // caller must never see an over-long list from one path and a
183        // capped one from the other.
184        if entries.len() > self.k as usize {
185            return Err(Error::Drive(DriveError::CorruptedDriveState(format!(
186                "ranked {:?} read returned {} entries for k = {}",
187                self.axis,
188                entries.len(),
189                self.k
190            ))));
191        }
192        Ok(RankedPage { skipped, entries })
193    }
194
195    /// Generate the grovedb indexed-axis paginated top-k proof for this
196    /// query.
197    ///
198    /// The envelope commits the walked secondary entries, the number of
199    /// entries skipped to reach them, the primary's root hash, the
200    /// sibling axes' root hashes, and a per-ancestor attestation chain
201    /// up to the grovedb root — so the client reconstructs the platform
202    /// root hash from it. `(axis, k, offset, descending)` bind by
203    /// RECONSTRUCTION, not echo: the verifier rebuilds the same
204    /// `PathQuery` from the request and
205    /// [`grovedb::GroveDb::verify_path_query`] re-executes the proof
206    /// against that traversal, so a proof for a different ranking or a
207    /// different page fails to cover it; that is why `k` is validated
208    /// rather than clamped upstream (a clamped `k` would produce a page
209    /// the client's reconstruction did not ask for).
210    ///
211    /// The paginated primitive is used unconditionally, with
212    /// `offset = 0` for offset-free requests, so there is exactly one
213    /// proof shape on this surface: a client never has to guess which of
214    /// two envelope formats a server produced.
215    ///
216    /// Verified by
217    /// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`](crate::query::DriveDocumentRankedQuery::verify_ranked_top_k_proof).
218    ///
219    /// # Empty rankings prove fine
220    ///
221    /// An index holding no documents has an empty axis secondary. The
222    /// older non-paginated prover refused that outright ("Cannot create
223    /// proof for empty tree"), which made a freshly registered contract
224    /// unqueryable with `prove = true`; the paginated prover emits a
225    /// guaranteed-empty range against the secondary instead, so the
226    /// proved and unproven paths agree on empty state. Pinned by the
227    /// `ranking_an_empty_index_reads_empty_and_proves_empty` test. A
228    /// pinned prefix whose value tree does not exist yet proves empty
229    /// too: grovedb authenticates the absent path and the verifier
230    /// reads it as an empty page (pinned by
231    /// `an_absent_equality_pin_reads_empty_and_proves_empty`).
232    pub fn execute_top_k_with_proof(
233        &self,
234        drive: &Drive,
235        transaction: TransactionArg,
236        platform_version: &PlatformVersion,
237    ) -> Result<Vec<u8>, Error> {
238        self.reject_offset_with_branches()?;
239        // grovedb's `prove_query` — since the indexed-axis prover
240        // retirement, the only proof surface — proves COMMITTED state
241        // only: it takes one internal snapshot and threads it through
242        // every proof layer, and cannot see the caller's transaction.
243        // Serving a proof for a different snapshot than the unproved
244        // read would silently desynchronize the two paths, so a
245        // transactional prove fails closed, single-prefix and branched
246        // alike.
247        if transaction.is_some() {
248            return Err(Error::Drive(DriveError::NotSupported(
249                "a ranked proof is generated from committed state only: grovedb's \
250                 prove_query cannot see the caller's transaction — commit first",
251            )));
252        }
253        if self.prefix_branches.len() > 1 {
254            // One grovedb **branched** envelope: shared ancestor layers
255            // once, one multi-key proof at the branching level, one
256            // secondary proof per branch — a single proof with a single
257            // root hash. The verifier re-derives the branch set from
258            // the request, so a dropped, duplicated, or reordered
259            // branch fails there.
260            let grove_version = &platform_version.drive.grove_version;
261            let paths = (0..self.prefix_branches.len())
262                .map(|branch| self.indexed_property_name_tree_path(branch))
263                .collect::<Result<Vec<_>, Error>>()?;
264            let (prefix, keys, suffix) = decompose_branch_paths(&paths)?;
265            let path_query = PathQuery::new_branched_axis(
266                prefix,
267                keys,
268                suffix,
269                AxisQuery::top_k(
270                    self.axis.into(),
271                    self.k,
272                    self.offset as u64,
273                    self.descending,
274                ),
275            );
276            let CostContext { value, cost: _ } =
277                drive.grove.prove_query(&path_query, None, grove_version);
278            return value.map_err(|e| Error::GroveDB(Box::new(e)));
279        }
280        self.execute_top_k_with_proof_branch(0, drive, platform_version)
281    }
282
283    /// One branch's proof — the entire pre-`IN` prover, parameterized by
284    /// the prefix branch.
285    fn execute_top_k_with_proof_branch(
286        &self,
287        branch: usize,
288        drive: &Drive,
289        platform_version: &PlatformVersion,
290    ) -> Result<Vec<u8>, Error> {
291        let grove_version = &platform_version.drive.grove_version;
292        let path = self.indexed_property_name_tree_path(branch)?;
293        let path_query = PathQuery::new_axis_top_k(
294            path,
295            self.axis.into(),
296            self.k,
297            self.offset as u64,
298            self.descending,
299        );
300        // Same destructure-don't-unwrap rationale as the no-proof arm.
301        let CostContext { value, cost: _ } =
302            drive.grove.prove_query(&path_query, None, grove_version);
303        value.map_err(|e| Error::GroveDB(Box::new(e)))
304    }
305}