Skip to main content

drive/query/drive_document_having_query/
execute_range.rs

1//! The two having-range executors on [`DriveDocumentHavingQuery`]: a
2//! direct value-bounded read of the axis secondary, and generation of
3//! the equivalent proof.
4//!
5//! Both are thin — all of the work happens inside grovedb, which seeks
6//! straight to the encoded bounds in the pre-sorted secondary Merk. No
7//! value trees are opened, no documents are materialized, and the cost
8//! is `O(log n + k)` in the number of *matching* groups returned, never
9//! in the total group population.
10//!
11//! Whole module is gated `feature = "server"` via the parent's
12//! `pub mod execute_range;` declaration.
13
14use super::super::drive_document_ranked_query::branches::{
15    axis_keys_to_ranked, decompose_branch_paths, read_branched_union,
16};
17use super::super::drive_document_ranked_query::RankedEntry;
18use super::DriveDocumentHavingQuery;
19use crate::drive::Drive;
20use crate::error::drive::DriveError;
21use crate::error::Error;
22use dpp::version::PlatformVersion;
23use grovedb::query_result_type::QueryResultType;
24use grovedb::{PathQuery, PathQueryRun, TransactionArg};
25use grovedb_costs::CostContext;
26use grovedb_query::AxisQuery;
27
28impl DriveDocumentHavingQuery<'_> {
29    /// Read the matching groups directly from the axis secondary: every
30    /// group whose aggregate falls inside the bounds, up to `limit`, in
31    /// axis order in the walk direction.
32    ///
33    /// Fewer than `limit` entries is normal (fewer groups match) and is
34    /// not an error; exactly `limit` entries may mean the match set was
35    /// cut.
36    ///
37    /// Missing paths follow the ranked surface's rule. Under a single
38    /// `==` pin (or no pins) a missing path *is* an error rather than an
39    /// empty result: the indexed property-name tree is created at
40    /// contract registration, so its absence means the contract-level
41    /// state is not what the request claims. On an `IN`-pinned request,
42    /// an element whose branch chain is missing at ANY depth — the
43    /// branch key, or any deeper pinned segment under a *present* key —
44    /// contributes an **empty branch** instead (union semantics, exactly
45    /// as the proved envelope authenticates it), and the union is served
46    /// from one committed state (a `None` read runs under a grovedb
47    /// snapshot read transaction). An index with no documents has the
48    /// tree, with an empty secondary, and yields an empty entry list.
49    pub fn execute_range_no_proof(
50        &self,
51        drive: &Drive,
52        transaction: TransactionArg,
53        platform_version: &PlatformVersion,
54    ) -> Result<Vec<RankedEntry>, Error> {
55        if self.prefix_branches.len() > 1 {
56            // ONE grovedb call for the whole union, pinned to one
57            // committed state — the entire sequence lives in the ranked
58            // surface's `branches::read_branched_union`, shared with the
59            // ranked executor so the two cannot drift.
60            let paths = (0..self.prefix_branches.len())
61                .map(|branch| self.indexed_property_name_tree_path(branch))
62                .collect::<Result<Vec<_>, Error>>()?;
63            let axis = self.bounds.axis();
64            let (lo, hi) = self.bounds.inclusive_bounds_i128();
65            return read_branched_union(
66                &drive.grove,
67                "having",
68                &self.prefix_branches,
69                &paths,
70                axis,
71                AxisQuery::bounded(axis.into(), lo, hi, self.limit, self.descending),
72                self.limit as usize,
73                self.descending,
74                transaction,
75                &platform_version.drive.grove_version,
76            );
77        }
78        self.execute_range_no_proof_branch(0, drive, transaction, platform_version)
79    }
80
81    /// One branch's in-bound page — the entire pre-`IN` executor,
82    /// parameterized by which prefix branch's terminal tree it walks.
83    fn execute_range_no_proof_branch(
84        &self,
85        branch: usize,
86        drive: &Drive,
87        transaction: TransactionArg,
88        platform_version: &PlatformVersion,
89    ) -> Result<Vec<RankedEntry>, Error> {
90        let grove_version = &platform_version.drive.grove_version;
91        let path = self.indexed_property_name_tree_path(branch)?;
92        let axis = self.bounds.axis();
93        let (lo, hi) = self.bounds.inclusive_bounds_i128();
94
95        // Costs are destructured away rather than `.unwrap()`-ed, same
96        // as the ranked executors: `CostContext::unwrap` is infallible
97        // but reads like a panicking unwrap at the call site.
98        let path_query = PathQuery::new_axis(
99            path,
100            AxisQuery::bounded(axis.into(), lo, hi, self.limit, self.descending).keys_only(),
101        );
102        let CostContext { value, cost: _ } = drive.grove.run_path_query(
103            &path_query,
104            true,
105            true,
106            true,
107            QueryResultType::QueryKeyElementPairResultType,
108            transaction,
109            grove_version,
110        );
111        let run = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
112        let PathQueryRun::AxisKeys { keys, skipped: _ } = run else {
113            return Err(Error::Drive(DriveError::CorruptedDriveState(
114                "a keys-only having read returned a different result shape".to_string(),
115            )));
116        };
117        let entries = axis_keys_to_ranked(axis, keys)?;
118        if entries.len() > self.limit as usize {
119            return Err(Error::Drive(DriveError::CorruptedDriveState(format!(
120                "having {axis:?} read returned {} entries for limit = {}",
121                entries.len(),
122                self.limit
123            ))));
124        }
125        Ok(entries)
126    }
127
128    /// Generate the grovedb indexed-axis range proof for this query.
129    ///
130    /// The envelope commits the in-range secondary entries, the
131    /// primary's root hash, the sibling axes' root hashes, and a
132    /// per-ancestor attestation chain up to the grovedb root — so the
133    /// client reconstructs the platform root hash from it. The bounds,
134    /// direction and limit bind by RECONSTRUCTION: the verifier rebuilds
135    /// the same `Bounded` axis `PathQuery` from the request
136    /// ([`AxisRangeBounds::inclusive_bounds_i128`]) and re-executes the
137    /// proof against it — which is why the bounds are validated rather
138    /// than clamped upstream, and why completeness needs no extra
139    /// machinery: a Merk range proof over a sorted keyspace commits its
140    /// boundaries, so an in-range group the server omitted fails
141    /// reconstruction.
142    ///
143    /// Verified by
144    /// [`DriveDocumentHavingQuery::verify_having_range_proof`](crate::query::DriveDocumentHavingQuery::verify_having_range_proof).
145    pub fn execute_range_with_proof(
146        &self,
147        drive: &Drive,
148        transaction: TransactionArg,
149        platform_version: &PlatformVersion,
150    ) -> Result<Vec<u8>, Error> {
151        // Same fail-closed rule as the ranked prover: grovedb's
152        // `prove_query` proves committed state only and cannot see the
153        // caller's transaction — single-prefix and branched alike.
154        if transaction.is_some() {
155            return Err(Error::Drive(DriveError::NotSupported(
156                "a having-range proof is generated from committed state only: grovedb's \
157                 prove_query cannot see the caller's transaction — commit first",
158            )));
159        }
160        if self.prefix_branches.len() > 1 {
161            // One grovedb **branched** envelope — see the ranked
162            // executor's multi-branch arm for the shape.
163            let grove_version = &platform_version.drive.grove_version;
164            let paths = (0..self.prefix_branches.len())
165                .map(|branch| self.indexed_property_name_tree_path(branch))
166                .collect::<Result<Vec<_>, Error>>()?;
167            let (prefix, keys, suffix) = decompose_branch_paths(&paths)?;
168            let (lo, hi) = self.bounds.inclusive_bounds_i128();
169            let path_query = PathQuery::new_branched_axis(
170                prefix,
171                keys,
172                suffix,
173                AxisQuery::bounded(
174                    self.bounds.axis().into(),
175                    lo,
176                    hi,
177                    self.limit,
178                    self.descending,
179                ),
180            );
181            let CostContext { value, cost: _ } =
182                drive.grove.prove_query(&path_query, None, grove_version);
183            return value.map_err(|e| Error::GroveDB(Box::new(e)));
184        }
185        self.execute_range_with_proof_branch(0, drive, platform_version)
186    }
187
188    /// One branch's proof — the entire pre-`IN` prover, parameterized by
189    /// the prefix branch.
190    fn execute_range_with_proof_branch(
191        &self,
192        branch: usize,
193        drive: &Drive,
194        platform_version: &PlatformVersion,
195    ) -> Result<Vec<u8>, Error> {
196        let grove_version = &platform_version.drive.grove_version;
197        let path = self.indexed_property_name_tree_path(branch)?;
198        let (lo, hi) = self.bounds.inclusive_bounds_i128();
199        let path_query = PathQuery::new_axis_bounded(
200            path,
201            self.bounds.axis().into(),
202            lo,
203            hi,
204            self.limit,
205            self.descending,
206        );
207        // Same destructure-don't-unwrap rationale as the no-proof arm.
208        let CostContext { value, cost: _ } =
209            drive.grove.prove_query(&path_query, None, grove_version);
210        value.map_err(|e| Error::GroveDB(Box::new(e)))
211    }
212}