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}