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}