Skip to main content

dash_platform_queries/documents/
array_element_clauses.rs

1//! The where clauses that read the elements of a typed array (protocol
2//! version 14), keyed for the aggregate and ranked proof verifiers exactly as
3//! the server keys them before it routes the request: `contains` as the
4//! equality and `containsAny` as the `In` an index level of the array holds.
5
6use crate::documents::document_query::DocumentQuery;
7use dpp::data_contract::accessors::v0::DataContractV0Getters;
8use dpp::data_contract::document_type::DocumentTypeRef;
9use dpp::data_contract::DataContract;
10use dpp::version::PlatformVersion;
11use drive::query::{
12    key_array_element_clauses, validate_and_canonicalize_where_clauses, WhereClause, WhereOperator,
13};
14
15/// `where_clauses`, read against the document type `document_type_name` of
16/// `data_contract`, with every clause reading the elements of a typed array
17/// keyed ([`key_array_element_clauses`]), for a request that reads one
18/// element at most: a ranked or having query (`grouped_by_in` false), or a
19/// test of the keying itself.
20///
21/// `grouped_by_in` is whether the request is grouped by its `In` field
22/// ([`grouped_by_in_field`]); several elements are read only then, and
23/// without a range clause, as the server reads them, so a verifier refuses
24/// exactly the requests the server refuses and never accepts a proof
25/// answering a total over several elements, which would count a document
26/// once per element it holds.
27pub fn key_element_clauses(
28    data_contract: &DataContract,
29    document_type_name: &str,
30    where_clauses: Vec<WhereClause>,
31    grouped_by_in: bool,
32    platform_version: &PlatformVersion,
33) -> Result<Vec<WhereClause>, drive_proof_verifier::Error> {
34    key_clauses_of(
35        document_type_named(data_contract, document_type_name)?,
36        where_clauses,
37        grouped_by_in,
38        platform_version,
39    )
40}
41
42/// [`aggregate_where_clauses`] of `request` before it is sent, its pending
43/// window selections left unresolved: a relative time selection resolves only
44/// against the answer's signed block time. Resolving a selection adds an
45/// equality pin alone, which can add a refusal but never lift one, so whatever
46/// this refuses the verifier refuses too.
47pub(super) fn aggregate_request_where_clauses(
48    request: &DocumentQuery,
49    folds_entries: bool,
50    platform_version: &PlatformVersion,
51) -> Result<Vec<WhereClause>, drive_proof_verifier::Error> {
52    aggregate_where_clauses(
53        document_type_named(&request.data_contract, &request.document_type_name)?,
54        &request.group_by,
55        request.where_clauses.clone(),
56        folds_entries,
57        platform_version,
58    )
59}
60
61fn document_type_named<'a>(
62    data_contract: &'a DataContract,
63    document_type_name: &str,
64) -> Result<DocumentTypeRef<'a>, drive_proof_verifier::Error> {
65    data_contract
66        .document_type_for_name(document_type_name)
67        .map_err(|e| drive_proof_verifier::Error::RequestError {
68            error: format!("document type {document_type_name} not found in contract: {e}"),
69        })
70}
71
72/// Whether a request grouped by `group_by` is grouped by one field that an
73/// `In` or a `containsAny` constrains: the shape drive-abci routes as
74/// `GroupByIn` (its `is_in_field`), which the SDK cannot call.
75pub fn grouped_by_in_field(group_by: &[String], where_clauses: &[WhereClause]) -> bool {
76    match group_by {
77        [field] => where_clauses.iter().any(|clause| {
78            matches!(
79                clause.operator,
80                WhereOperator::In | WhereOperator::ContainsAny
81            ) && clause.field == *field
82        }),
83        _ => false,
84    }
85}
86
87/// The where clauses of a count, sum or average proof request on
88/// `document_type`, as the server's dispatcher reads them before its mode
89/// detection: the elements of a typed array keyed, then canonicalized (the
90/// shared step in `drive::query::canonicalize`), since the prover merged
91/// `[f > A, f < B]` pairs into one `between*` clause before its mode
92/// detection, and the verifier's must run over the same shape or a valid
93/// proof is refused.
94///
95/// `folds_entries` is whether the caller adds the verified entries up into
96/// one total (`DocumentCount`, `DocumentSum`, `DocumentAverage`): several
97/// elements are then refused even grouped by the array, since the total
98/// would count a document once per element it holds.
99pub(super) fn aggregate_where_clauses(
100    document_type: DocumentTypeRef,
101    group_by: &[String],
102    where_clauses: Vec<WhereClause>,
103    folds_entries: bool,
104    platform_version: &PlatformVersion,
105) -> Result<Vec<WhereClause>, drive_proof_verifier::Error> {
106    let grouped_by_in = !folds_entries && grouped_by_in_field(group_by, &where_clauses);
107    let keyed = key_clauses_of(
108        document_type,
109        where_clauses,
110        grouped_by_in,
111        platform_version,
112    )?;
113    validate_and_canonicalize_where_clauses(keyed, platform_version).map_err(|e| {
114        drive_proof_verifier::Error::RequestError {
115            error: format!("where-clause canonicalization failed: {e}"),
116        }
117    })
118}
119
120fn key_clauses_of(
121    document_type: DocumentTypeRef,
122    where_clauses: Vec<WhereClause>,
123    grouped_by_in: bool,
124    platform_version: &PlatformVersion,
125) -> Result<Vec<WhereClause>, drive_proof_verifier::Error> {
126    key_array_element_clauses(
127        where_clauses,
128        document_type,
129        grouped_by_in,
130        platform_version,
131    )
132    .map_err(|e| drive_proof_verifier::Error::RequestError {
133        error: format!("where-clause keying failed: {e}"),
134    })
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use dpp::data_contract::DataContractFactory;
141    use dpp::platform_value::{platform_value, Identifier, Value};
142
143    fn post_contract() -> DataContract {
144        DataContractFactory::new(PlatformVersion::latest().protocol_version)
145            .expect("factory")
146            .create_with_value_config(
147                Identifier::new([7u8; 32]),
148                0,
149                platform_value!({"post": {
150                    "type": "object",
151                    "properties": {
152                        "hashtags": {
153                            "type": "array",
154                            "maxItems": 10,
155                            "items": {"type": "string", "minLength": 1, "maxLength": 61},
156                            "position": 0
157                        }
158                    },
159                    "required": ["$createdAt"],
160                    "indices": [
161                        {"name": "byHashtagTime",
162                         "properties": [{"hashtags": "asc"}, {"$createdAt": "asc"}],
163                         "countable": "countable"}
164                    ],
165                    "additionalProperties": false
166                }}),
167                None,
168                None,
169            )
170            .expect("the post contract is well-formed")
171            .data_contract_owned()
172    }
173
174    fn several_hashtags() -> Vec<WhereClause> {
175        vec![WhereClause {
176            field: "hashtags".to_string(),
177            operator: WhereOperator::ContainsAny,
178            value: platform_value!(["dash", "news"]),
179        }]
180    }
181
182    fn created_after_zero() -> WhereClause {
183        WhereClause {
184            field: "$createdAt".to_string(),
185            operator: WhereOperator::GreaterThan,
186            value: Value::U64(0),
187        }
188    }
189
190    #[test]
191    fn should_answer_several_elements_only_grouped_by_the_array_alone() {
192        let contract = post_contract();
193        let platform_version = PlatformVersion::latest();
194        let by_hashtags = ["hashtags".to_string()];
195        let several = several_hashtags();
196        assert!(grouped_by_in_field(&by_hashtags, &several));
197        assert!(!grouped_by_in_field(&[], &several));
198        assert!(!grouped_by_in_field(
199            &["hashtags".to_string(), "$createdAt".to_string()],
200            &several
201        ));
202        // Grouped by the array, a range clause still folds a group's totals.
203        let mut in_a_range = several;
204        in_a_range.push(created_after_zero());
205        key_element_clauses(&contract, "post", in_a_range, true, platform_version)
206            .expect_err("several elements under a range are refused");
207    }
208
209    /// A dishonest node could prove the per-element counts of a total the
210    /// server refuses; summed, a post holding both elements would count
211    /// twice. The verifier refuses the request by the server's own rule.
212    #[test]
213    fn should_refuse_to_verify_a_total_over_several_elements() {
214        let contract = post_contract();
215        let platform_version = PlatformVersion::latest();
216        let several = several_hashtags();
217
218        let error = key_element_clauses(
219            &contract,
220            "post",
221            several.clone(),
222            grouped_by_in_field(&[], &several),
223            platform_version,
224        )
225        .expect_err("an ungrouped total over several elements is refused");
226        assert!(
227            format!("{error:?}").contains("only answered grouped by"),
228            "{error:?}"
229        );
230
231        let keyed = key_element_clauses(
232            &contract,
233            "post",
234            several.clone(),
235            grouped_by_in_field(&["hashtags".to_string()], &several),
236            platform_version,
237        )
238        .expect("grouped by the array, one total per element");
239        assert_eq!(keyed[0].operator, WhereOperator::In);
240
241        let one = vec![WhereClause {
242            field: "hashtags".to_string(),
243            operator: WhereOperator::Contains,
244            value: Value::Text("dash".to_string()),
245        }];
246        let keyed = key_element_clauses(&contract, "post", one, false, platform_version)
247            .expect("one element is a total of its own");
248        assert_eq!(keyed[0].operator, WhereOperator::Equal);
249    }
250}