Skip to main content

drive/query/drive_document_ranked_query/
path.rs

1//! The grove path a ranked read / proof / verification is issued against.
2//!
3//! This is one half of the **prover/verifier-agreement boundary** for
4//! the ranked surface: both sides build the same axis `PathQuery` —
5//! these path segments plus the traversal (`axis, k, offset,
6//! descending`) — and grovedb re-executes the proof against that
7//! reconstruction at verification time. Prover and verifier
8//! both call [`DriveDocumentRankedQuery::indexed_property_name_tree_path`];
9//! a divergence here surfaces as a failed root-hash reconstruction, not a
10//! wrong answer, but it is still the one place the two sides must not
11//! drift.
12//!
13//! Gated `any(server, verify)` so the verifier crate reaches it through
14//! `DriveDocumentRankedQuery::*` method syntax.
15
16use super::DriveDocumentRankedQuery;
17use crate::drive::RootTree;
18use crate::error::drive::DriveError;
19use crate::error::Error;
20use dpp::data_contract::document_type::{Index, IndexProperty};
21
22/// The ranked level a request with `pin_count` pins addresses, split
23/// into the leading properties (one pin each) and the ranked property
24/// itself. An index can host secondaries at up to TWO levels — its
25/// `rankedCountable.at` property and, independently, its terminal
26/// property (the boolean axes) — so the level cannot be derived from
27/// the index alone: the request's pin count names it, since every
28/// property before the ranked level must be pinned and none after it
29/// may appear. Fails closed when the pin count lands on a level that
30/// hosts no secondary.
31///
32/// Shared by the path builder and the prefix encoder — and, through
33/// them, by the server executors and the SDK verifier — so both sides
34/// agree on where a request's secondary lives without re-deriving it.
35pub(crate) fn ranked_level_split(
36    index: &Index,
37    pin_count: usize,
38) -> Result<(&[IndexProperty], &IndexProperty), Error> {
39    let Some(ranked_property) = index.properties.get(pin_count) else {
40        return Err(Error::Drive(DriveError::NotSupported(
41            "ranked and having-range queries require exactly one pinned value per property \
42             before the ranked level — the resolved pin set addresses no index property",
43        )));
44    };
45    let is_at_level = index
46        .ranked_countable_at
47        .iter()
48        .any(|at| at == &ranked_property.name);
49    let is_terminal = pin_count + 1 == index.properties.len();
50    if !is_at_level && !is_terminal {
51        return Err(Error::Drive(DriveError::NotSupported(
52            "ranked and having-range queries must land on a level hosting a ranking \
53             secondary — the index's rankedCountable.at property or its terminal property; \
54             the resolved pin set addresses an intermediate level",
55        )));
56    }
57    Ok((&index.properties[..pin_count], ranked_property))
58}
59
60/// Path of an index's **ranked-level** property-name tree — the terminal
61/// one for the boolean ranking axes, the `at` level for a prefix-level
62/// `rankedCountable`. Shared by the ranked and having-range query
63/// surfaces, which read the same indexed tree. See
64/// [`DriveDocumentRankedQuery::indexed_property_name_tree_path`]
65/// for the segment layout.
66///
67/// The branch's prefix segments carry the **encoded index-key bytes** of
68/// each leading property's pinned value, in index-property order — one
69/// per property before the ranked one. Empty for an index ranked at its
70/// first property. The arity must match exactly: the ranked level's
71/// tree sits under one prefix value tree per leading property, and only
72/// a `where` pin (an equality, or one element of the single permitted
73/// `IN`) can name those values, so a missing or surplus value means the
74/// caller resolved the wrong index — a typed error, not a guess.
75pub(crate) fn indexed_property_name_tree_path_for_index(
76    contract_id: &[u8; 32],
77    document_type_name: &str,
78    index: &Index,
79    equality_prefix_values: &[Vec<u8>],
80) -> Result<Vec<Vec<u8>>, Error> {
81    // The pin count names the ranked level (see `ranked_level_split`),
82    // which also makes the leading-property arity match by construction;
83    // the split's fail-closed error replaces the old arity backstop.
84    let (leading_properties, terminal_property) =
85        ranked_level_split(index, equality_prefix_values.len())?;
86    let mut path = Vec::with_capacity(5 + 2 * leading_properties.len());
87    path.push(vec![RootTree::DataContractDocuments as u8]);
88    path.push(contract_id.to_vec());
89    path.push(vec![1u8]);
90    path.push(document_type_name.as_bytes().to_vec());
91    // Level segments go through `Index::level_key`, the single source of
92    // the level-key rule: a bucketed (timeRange) first level is keyed by
93    // the grid-qualified `storage_key`, every other level by its bare
94    // property name. The pinned value under a bucketed level is the
95    // resolved bucket start, encoded exactly like the timestamp itself.
96    for (position, (property, value)) in leading_properties
97        .iter()
98        .zip(equality_prefix_values)
99        .enumerate()
100    {
101        path.push(index.level_key(position, &property.name).into_bytes());
102        path.push(value.clone());
103    }
104    path.push(
105        index
106            .level_key(leading_properties.len(), &terminal_property.name)
107            .into_bytes(),
108    );
109    Ok(path)
110}
111
112impl DriveDocumentRankedQuery<'_> {
113    /// Path of the **terminal property-name tree** — the indexed tree
114    /// whose primary holds one value tree per group and whose per-axis
115    /// secondaries hold the ranking.
116    ///
117    /// For a single-property index:
118    ///
119    /// ```text
120    /// [ RootTree::DataContractDocuments as u8 ]   // 0x01
121    ///   / <contract_id: 32 bytes>
122    ///   / [ 0x01 ]                                // "documents", not "contract"
123    ///   / <document_type_name: utf-8>
124    ///   / <index property name: utf-8>
125    /// ```
126    ///
127    /// For a compound index `[p1, …, pn]`, each leading property
128    /// contributes two segments — its name and the **encoded index-key
129    /// bytes of its pinned value** (from
130    /// [`Self::prefix_branches`]) — and the terminal property
131    /// name closes the path:
132    ///
133    /// ```text
134    ///   … / <p1 name> / <p1 pinned value bytes> / … / <pn name>
135    /// ```
136    ///
137    /// so the ranking read lands on **that prefix's** indexed tree: the
138    /// per-prefix secondary orders only the pinned prefix's groups.
139    ///
140    /// The children of the terminal tree are the groups, keyed by the
141    /// raw index-key bytes of the terminal property value — the same
142    /// bytes that come back as [`super::RankedEntry::key`].
143    ///
144    /// Errors when the number of encoded prefix values does not match
145    /// the index's leading-property count — the fail-closed backstop
146    /// for a caller that resolved the query against the wrong index.
147    ///
148    /// `branch` indexes into [`Self::prefix_branches`]; single-branch
149    /// queries (no `IN` pin) always pass `0`.
150    pub fn indexed_property_name_tree_path(&self, branch: usize) -> Result<Vec<Vec<u8>>, Error> {
151        let prefix_values =
152            self.prefix_branches
153                .get(branch)
154                .ok_or(Error::Drive(DriveError::NotSupported(
155                    "ranked and having-range queries addressed a prefix branch outside the \
156                 query's resolved branch set",
157                )))?;
158        indexed_property_name_tree_path_for_index(
159            &self.contract_id,
160            &self.document_type_name,
161            self.index,
162            prefix_values,
163        )
164    }
165}