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}