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::DocumentType;
4
5use crate::consensus::basic::document::{
6    DocumentFieldMaxSizeExceededError, InvalidDocumentTypeError,
7};
8use crate::consensus::basic::value_error::ValueError;
9use crate::consensus::basic::BasicError;
10use crate::consensus::ConsensusError;
11use crate::data_contract::schema::DataContractSchemaMethodsV0;
12use crate::data_contract::DataContract;
13use crate::document::{Document, DocumentV0Getters};
14use crate::validation::SimpleConsensusValidationResult;
15use crate::ProtocolError;
16use platform_value::Value;
17use platform_version::version::PlatformVersion;
18use std::ops::Deref;
19
20pub trait DataContractDocumentValidationMethodsV0 {
21    fn validate_document(
22        &self,
23        name: &str,
24        document: &Document,
25        platform_version: &PlatformVersion,
26    ) -> Result<SimpleConsensusValidationResult, ProtocolError>;
27
28    fn validate_document_properties(
29        &self,
30        name: &str,
31        value: Value,
32        platform_version: &PlatformVersion,
33    ) -> Result<SimpleConsensusValidationResult, ProtocolError>;
34}
35
36impl DataContract {
37    #[inline(always)]
38    pub(super) fn validate_document_properties_v0(
39        &self,
40        name: &str,
41        value: Value,
42        platform_version: &PlatformVersion,
43    ) -> Result<SimpleConsensusValidationResult, ProtocolError> {
44        let Some(document_type) = self.document_type_optional_for_name(name) else {
45            return Ok(SimpleConsensusValidationResult::new_with_error(
46                InvalidDocumentTypeError::new(name.to_owned(), self.id()).into(),
47            ));
48        };
49
50        if let Some(max_depth) = platform_version.system_limits.max_document_value_depth {
51            let max_depth = max_depth as usize;
52            // The enclosing properties map mirrors the transition's plain `BTreeMap` data
53            // wrapper, which the wire decoder never counts: each property value receives the
54            // full depth budget so no decodable payload can violate this rule.
55            let excess_depth = match &value {
56                Value::Map(map) => map.iter().find_map(|(key, property_value)| {
57                    key.first_depth_exceeding(max_depth)
58                        .or_else(|| property_value.first_depth_exceeding(max_depth))
59                }),
60                other => other.first_depth_exceeding(max_depth),
61            };
62            if let Some(actual_depth) = excess_depth {
63                return Ok(SimpleConsensusValidationResult::new_with_error(
64                    ConsensusError::BasicError(BasicError::ValueError(
65                        ValueError::new_from_string(format!(
66                            "document value depth {actual_depth} exceeds system maximum {max_depth}"
67                        )),
68                    )),
69                ));
70            }
71        }
72
73        let validator = document_type.json_schema_validator_ref().deref();
74
75        if let Some((key, size)) =
76            value.has_data_larger_than(platform_version.system_limits.max_field_value_size)
77        {
78            let field = match key {
79                Some(Value::Text(field)) => field.clone(),
80                _ => "".to_string(),
81            };
82            return Ok(SimpleConsensusValidationResult::new_with_error(
83                ConsensusError::BasicError(BasicError::DocumentFieldMaxSizeExceededError(
84                    DocumentFieldMaxSizeExceededError::new(
85                        field,
86                        size as u64,
87                        platform_version.system_limits.max_field_value_size as u64,
88                    ),
89                )),
90            ));
91        }
92
93        let json_value = match value.try_into_validating_json() {
94            Ok(json_value) => json_value,
95            Err(e) => {
96                return Ok(SimpleConsensusValidationResult::new_with_error(
97                    ConsensusError::BasicError(BasicError::ValueError(e.into())),
98                ))
99            }
100        };
101
102        // Compile json schema validator if it's not yet compiled
103        if !validator.is_compiled(platform_version)? {
104            // It is normal that we get a protocol error here, since the document type is coming
105            // from the state
106            let root_schema = DocumentType::enrich_with_base_schema(
107                // TODO: I just wondering if we could you references here
108                //  instead of cloning
109                document_type.schema().clone(),
110                self.schema_defs().map(|defs| Value::from(defs.clone())),
111                platform_version,
112            )?;
113
114            let root_json_schema = root_schema
115                .try_to_validating_json()
116                .map_err(ProtocolError::ValueError)?;
117
118            validator.compile_and_validate(&root_json_schema, &json_value, platform_version)
119        } else {
120            validator.validate(&json_value, platform_version)
121        }
122    }
123
124    #[inline(always)]
125    pub(super) fn validate_document_v0(
126        &self,
127        name: &str,
128        document: &Document,
129        platform_version: &PlatformVersion,
130    ) -> Result<SimpleConsensusValidationResult, ProtocolError> {
131        // Validate user defined properties
132        self.validate_document_properties_v0(name, document.properties().into(), platform_version)
133    }
134}
135
136#[cfg(all(test, feature = "fixtures-and-mocks"))]
137mod tests {
138    use super::DataContractDocumentValidationMethodsV0;
139    use crate::consensus::basic::value_error::ValueError;
140    use crate::consensus::basic::BasicError;
141    use crate::consensus::ConsensusError;
142    use crate::data_contract::created_data_contract::CreatedDataContract;
143    use crate::tests::fixtures::get_data_contract_fixture;
144    use platform_value::Value;
145    use platform_version::version::PlatformVersion;
146
147    fn data_contract() -> CreatedDataContract {
148        let platform_version = PlatformVersion::latest();
149        get_data_contract_fixture(None, 0, platform_version.protocol_version)
150    }
151
152    fn nested_document_value(container_count: usize, leaf: Value) -> Value {
153        let nested = (0..container_count).fold(leaf, |value, depth| {
154            if depth % 2 == 0 {
155                Value::Array(vec![value])
156            } else {
157                Value::Map(vec![(Value::Text("nested".to_owned()), value)])
158            }
159        });
160
161        Value::Map(vec![(Value::Text("name".to_owned()), nested)])
162    }
163
164    #[test]
165    fn should_reject_excessive_document_value_depth_before_field_size_validation() {
166        let platform_version = PlatformVersion::latest();
167        let data_contract = data_contract().data_contract_owned();
168        let max_depth = platform_version
169            .system_limits
170            .max_document_value_depth
171            .expect("latest protocol should enforce document value depth");
172        let value = nested_document_value(
173            max_depth as usize + 1,
174            Value::Text(
175                "x".repeat(platform_version.system_limits.max_field_value_size as usize + 1),
176            ),
177        );
178
179        let result = data_contract
180            .validate_document_properties("noTimeDocument", value, platform_version)
181            .expect("validation should return a consensus result");
182
183        let Some(ConsensusError::BasicError(BasicError::ValueError(ValueError { .. }))) =
184            result.first_error()
185        else {
186            panic!("expected document value depth error, got {result:?}");
187        };
188        assert_eq!(
189            result.first_error().expect("expected an error").to_string(),
190            format!(
191                "document value depth {} exceeds system maximum {max_depth}",
192                max_depth + 1
193            )
194        );
195    }
196
197    #[test]
198    fn should_allow_document_value_depth_at_the_limit() {
199        let platform_version = PlatformVersion::latest();
200        let data_contract = data_contract().data_contract_owned();
201        let max_depth = platform_version
202            .system_limits
203            .max_document_value_depth
204            .expect("latest protocol should enforce document value depth");
205        let value = nested_document_value(
206            max_depth as usize,
207            Value::Text(
208                "x".repeat(platform_version.system_limits.max_field_value_size as usize + 1),
209            ),
210        );
211
212        let result = data_contract
213            .validate_document_properties("noTimeDocument", value, platform_version)
214            .expect("validation should return a consensus result");
215
216        assert!(matches!(
217            result.first_error(),
218            Some(ConsensusError::BasicError(
219                BasicError::DocumentFieldMaxSizeExceededError(_)
220            ))
221        ));
222    }
223
224    #[test]
225    fn should_preserve_valid_document_properties() {
226        let platform_version = PlatformVersion::latest();
227        let data_contract = data_contract().data_contract_owned();
228        let value = Value::Map(vec![(
229            Value::Text("name".to_owned()),
230            Value::Text("Alice".to_owned()),
231        )]);
232
233        let result = data_contract
234            .validate_document_properties("noTimeDocument", value, platform_version)
235            .expect("validation should return a consensus result");
236
237        assert!(
238            result.is_valid(),
239            "expected valid properties, got {result:?}"
240        );
241    }
242}