dpp/data_contract/methods/validate_document/v0/
mod.rs1use 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 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 if !validator.is_compiled(platform_version)? {
104 let root_schema = DocumentType::enrich_with_base_schema(
107 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 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}