Skip to main content

dpp/data_contract/methods/validate_document/v0/
mod.rs

1use crate::data_contract::accessors::v0::DataContractV0Getters;
2use crate::data_contract::document_type::accessors::DocumentTypeV0Getters;
3use crate::data_contract::document_type::methods::{
4    DocumentTypeBasicMethods, DocumentTypeV0Methods,
5};
6use crate::data_contract::document_type::property_constraints::DocumentSystemValues;
7use crate::data_contract::document_type::DocumentType;
8
9use crate::consensus::basic::document::{
10    DocumentFieldMaxSizeExceededError, InvalidDocumentTypeError,
11};
12use crate::consensus::basic::value_error::ValueError;
13use crate::consensus::basic::BasicError;
14use crate::consensus::ConsensusError;
15use crate::data_contract::schema::DataContractSchemaMethodsV0;
16use crate::data_contract::DataContract;
17use crate::document::{Document, DocumentV0Getters};
18use crate::validation::SimpleConsensusValidationResult;
19use crate::ProtocolError;
20use platform_value::Value;
21use platform_version::version::PlatformVersion;
22use std::ops::Deref;
23
24pub trait DataContractDocumentValidationMethodsV0 {
25    fn validate_document(
26        &self,
27        name: &str,
28        document: &Document,
29        platform_version: &PlatformVersion,
30    ) -> Result<SimpleConsensusValidationResult, ProtocolError>;
31
32    /// Validates a document's properties, `value`, against its document type: the
33    /// schema, the string byte caps and the `propertyConstraints` rules. `system` holds
34    /// the system values of the document version being written, what a rule's `$ownerId`
35    /// and system times and heights read: an owner the caller does not know equals no
36    /// identifier, and a rule reading a time or height it does not know is not judged.
37    /// Consensus passes the writer and the block's time and heights on create, and the
38    /// stored ones where a replace keeps them.
39    fn validate_document_properties(
40        &self,
41        name: &str,
42        value: Value,
43        system: &DocumentSystemValues,
44        platform_version: &PlatformVersion,
45    ) -> Result<SimpleConsensusValidationResult, ProtocolError>;
46}
47
48impl DataContract {
49    #[inline(always)]
50    pub(super) fn validate_document_properties_v0(
51        &self,
52        name: &str,
53        value: Value,
54        system: &DocumentSystemValues,
55        platform_version: &PlatformVersion,
56    ) -> Result<SimpleConsensusValidationResult, ProtocolError> {
57        let Some(document_type) = self.document_type_optional_for_name(name) else {
58            return Ok(SimpleConsensusValidationResult::new_with_error(
59                InvalidDocumentTypeError::new(name.to_owned(), self.id()).into(),
60            ));
61        };
62
63        if let Some(max_depth) = platform_version.system_limits.max_document_value_depth {
64            let max_depth = max_depth as usize;
65            // The enclosing properties map mirrors the transition's plain `BTreeMap` data
66            // wrapper, which the wire decoder never counts: each property value receives the
67            // full depth budget so no decodable payload can violate this rule.
68            let excess_depth = match &value {
69                Value::Map(map) => map.iter().find_map(|(key, property_value)| {
70                    key.first_depth_exceeding(max_depth)
71                        .or_else(|| property_value.first_depth_exceeding(max_depth))
72                }),
73                other => other.first_depth_exceeding(max_depth),
74            };
75            if let Some(actual_depth) = excess_depth {
76                return Ok(SimpleConsensusValidationResult::new_with_error(
77                    ConsensusError::BasicError(BasicError::ValueError(
78                        ValueError::new_from_string(format!(
79                            "document value depth {actual_depth} exceeds system maximum {max_depth}"
80                        )),
81                    )),
82                ));
83            }
84        }
85
86        let validator = document_type.json_schema_validator_ref().deref();
87
88        if let Some((key, size)) =
89            value.has_data_larger_than(platform_version.system_limits.max_field_value_size)
90        {
91            let field = match key {
92                Some(Value::Text(field)) => field.clone(),
93                _ => "".to_string(),
94            };
95            return Ok(SimpleConsensusValidationResult::new_with_error(
96                ConsensusError::BasicError(BasicError::DocumentFieldMaxSizeExceededError(
97                    DocumentFieldMaxSizeExceededError::new(
98                        field,
99                        size as u64,
100                        platform_version.system_limits.max_field_value_size as u64,
101                    ),
102                )),
103            ));
104        }
105
106        // Added in place at protocol version 14, inert before it: the meta-schemas there
107        // refuse `maxBytes`, their parser ignores it (`apply_max_bytes` is `None`, so no
108        // string carries a byte cap) and `validate_max_bytes` is `None`, so the check returns
109        // an empty result. Computed before the JSON conversion consumes `value`; reported
110        // only when the schema validation passes, so a schema error keeps precedence.
111        let max_bytes_result =
112            document_type.validate_max_bytes_properties(&value, platform_version)?;
113
114        // Added in place at protocol version 14, inert before it: the meta-schemas there
115        // refuse `generatedFrom`, their parser ignores it (`apply_generated_from` is
116        // `None`, so no property carries a declaration) and `validate_generated_from` is
117        // `None`, so the check returns an empty result without reading `value`. Computed and
118        // reported like `maxBytes`, after it, so a schema error keeps precedence and every
119        // value it compares is known to be a string.
120        let generated_from_result =
121            document_type.validate_generated_from_properties(&value, platform_version)?;
122
123        // Added in place at protocol version 14, inert for every earlier version that
124        // selects this generation: `validate_property_constraints` is `None` in all of their
125        // tables, so the call returns an empty result without reading `value`. (Only parser
126        // generation 3 reads `propertyConstraints` at all; meta-schema v0, protocol versions
127        // 1 to 11, leaves the document type level open, so the version gate is the proof,
128        // not the meta-schemas.) Computed and reported like `maxBytes`, after it, so a
129        // schema error keeps precedence and every value a rule reads is known to be an
130        // integer.
131        let property_constraints_result =
132            document_type.validate_property_constraints(&value, system, platform_version)?;
133
134        let json_value = match value.try_into_validating_json() {
135            Ok(json_value) => json_value,
136            Err(e) => {
137                return Ok(SimpleConsensusValidationResult::new_with_error(
138                    ConsensusError::BasicError(BasicError::ValueError(e.into())),
139                ))
140            }
141        };
142
143        // Compile json schema validator if it's not yet compiled
144        let schema_result = if !validator.is_compiled(platform_version)? {
145            // It is normal that we get a protocol error here, since the document type is coming
146            // from the state
147            let root_schema = DocumentType::enrich_with_base_schema(
148                // TODO: I just wondering if we could you references here
149                //  instead of cloning
150                document_type.schema().clone(),
151                self.schema_defs().map(|defs| Value::from(defs.clone())),
152                platform_version,
153            )?;
154
155            let root_json_schema = root_schema
156                .try_to_validating_json()
157                .map_err(ProtocolError::ValueError)?;
158
159            validator.compile_and_validate(&root_json_schema, &json_value, platform_version)?
160        } else {
161            validator.validate(&json_value, platform_version)?
162        };
163        if !schema_result.is_valid() {
164            return Ok(schema_result);
165        }
166        if !max_bytes_result.is_valid() {
167            return Ok(max_bytes_result);
168        }
169        if !generated_from_result.is_valid() {
170            return Ok(generated_from_result);
171        }
172
173        Ok(property_constraints_result)
174    }
175
176    #[inline(always)]
177    pub(super) fn validate_document_v0(
178        &self,
179        name: &str,
180        document: &Document,
181        platform_version: &PlatformVersion,
182    ) -> Result<SimpleConsensusValidationResult, ProtocolError> {
183        // Validate user defined properties
184        self.validate_document_properties_v0(
185            name,
186            document.properties().into(),
187            &DocumentSystemValues::of_document(document),
188            platform_version,
189        )
190    }
191}
192
193#[cfg(all(test, feature = "fixtures-and-mocks"))]
194mod tests {
195    use super::DataContractDocumentValidationMethodsV0;
196    use crate::consensus::basic::value_error::ValueError;
197    use crate::consensus::basic::BasicError;
198    use crate::consensus::ConsensusError;
199    use crate::data_contract::created_data_contract::CreatedDataContract;
200    use crate::data_contract::document_type::property_constraints::DocumentSystemValues;
201    use crate::tests::fixtures::get_data_contract_fixture;
202    use platform_value::Value;
203    use platform_version::version::PlatformVersion;
204
205    fn data_contract() -> CreatedDataContract {
206        let platform_version = PlatformVersion::get(13).expect("protocol version 13 exists");
207        get_data_contract_fixture(None, 0, platform_version.protocol_version)
208    }
209
210    fn nested_document_value(container_count: usize, leaf: Value) -> Value {
211        let nested = (0..container_count).fold(leaf, |value, depth| {
212            if depth % 2 == 0 {
213                Value::Array(vec![value])
214            } else {
215                Value::Map(vec![(Value::Text("nested".to_owned()), value)])
216            }
217        });
218
219        Value::Map(vec![(Value::Text("name".to_owned()), nested)])
220    }
221
222    #[test]
223    fn should_reject_excessive_document_value_depth_before_field_size_validation() {
224        let platform_version = PlatformVersion::get(13).expect("protocol version 13 exists");
225        let data_contract = data_contract().data_contract_owned();
226        let max_depth = platform_version
227            .system_limits
228            .max_document_value_depth
229            .expect("protocol version 13 should enforce document value depth");
230        let value = nested_document_value(
231            max_depth as usize + 1,
232            Value::Text(
233                "x".repeat(platform_version.system_limits.max_field_value_size as usize + 1),
234            ),
235        );
236
237        let result = data_contract
238            .validate_document_properties(
239                "noTimeDocument",
240                value,
241                &DocumentSystemValues::default(),
242                platform_version,
243            )
244            .expect("validation should return a consensus result");
245
246        let Some(ConsensusError::BasicError(BasicError::ValueError(ValueError { .. }))) =
247            result.first_error()
248        else {
249            panic!("expected document value depth error, got {result:?}");
250        };
251        assert_eq!(
252            result.first_error().expect("expected an error").to_string(),
253            format!(
254                "document value depth {} exceeds system maximum {max_depth}",
255                max_depth + 1
256            )
257        );
258    }
259
260    #[test]
261    fn should_allow_document_value_depth_at_the_limit() {
262        let platform_version = PlatformVersion::get(13).expect("protocol version 13 exists");
263        let data_contract = data_contract().data_contract_owned();
264        let max_depth = platform_version
265            .system_limits
266            .max_document_value_depth
267            .expect("protocol version 13 should enforce document value depth");
268        let value = nested_document_value(
269            max_depth as usize,
270            Value::Text(
271                "x".repeat(platform_version.system_limits.max_field_value_size as usize + 1),
272            ),
273        );
274
275        let result = data_contract
276            .validate_document_properties(
277                "noTimeDocument",
278                value,
279                &DocumentSystemValues::default(),
280                platform_version,
281            )
282            .expect("validation should return a consensus result");
283
284        assert!(matches!(
285            result.first_error(),
286            Some(ConsensusError::BasicError(
287                BasicError::DocumentFieldMaxSizeExceededError(_)
288            ))
289        ));
290    }
291
292    #[test]
293    fn should_preserve_valid_document_properties() {
294        let platform_version = PlatformVersion::get(13).expect("protocol version 13 exists");
295        let data_contract = data_contract().data_contract_owned();
296        let value = Value::Map(vec![(
297            Value::Text("name".to_owned()),
298            Value::Text("Alice".to_owned()),
299        )]);
300
301        let result = data_contract
302            .validate_document_properties(
303                "noTimeDocument",
304                value,
305                &DocumentSystemValues::default(),
306                platform_version,
307            )
308            .expect("validation should return a consensus result");
309
310        assert!(
311            result.is_valid(),
312            "expected valid properties, got {result:?}"
313        );
314    }
315}