1use crate::block::block_info::BlockInfo;
2use crate::data_contract::DataContract;
3use crate::validation::SimpleConsensusValidationResult;
4use crate::ProtocolError;
5use platform_version::version::PlatformVersion;
6
7pub trait DataContractUpdateValidationMethodsV0 {
8 fn validate_update(
9 &self,
10 data_contract: &DataContract,
11 block_info: &BlockInfo,
12 platform_version: &PlatformVersion,
13 ) -> Result<SimpleConsensusValidationResult, ProtocolError>;
14}
15
16impl DataContract {
17 #[inline(always)]
18 pub(super) fn validate_update_v0(
19 &self,
20 new_data_contract: &DataContract,
21 block_info: &BlockInfo,
22 platform_version: &PlatformVersion,
23 ) -> Result<SimpleConsensusValidationResult, ProtocolError> {
24 let result = self.validate_update_ownership_and_version(new_data_contract);
25 if !result.is_valid() {
26 return Ok(result);
27 }
28
29 let result = self.validate_update_config(new_data_contract, platform_version)?;
30 if !result.is_valid() {
31 return Ok(result);
32 }
33
34 let result =
35 self.validate_update_existing_document_types(new_data_contract, platform_version)?;
36 if !result.is_valid() {
37 return Ok(result);
38 }
39
40 let result = self.validate_update_schema_defs(new_data_contract, platform_version)?;
41 if !result.is_valid() {
42 return Ok(result);
43 }
44
45 let result = self.validate_update_groups(new_data_contract);
46 if !result.is_valid() {
47 return Ok(result);
48 }
49
50 let result = self.validate_update_tokens(new_data_contract, block_info);
51 if !result.is_valid() {
52 return Ok(result);
53 }
54
55 let result = self.validate_update_keywords(new_data_contract);
56 if !result.is_valid() {
57 return Ok(result);
58 }
59
60 Ok(self.validate_update_description(new_data_contract))
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::consensus::basic::basic_error::BasicError;
68 use crate::consensus::state::state_error::StateError;
69 use crate::consensus::ConsensusError;
70 use crate::data_contract::accessors::v0::DataContractV0Getters;
71 use crate::data_contract::accessors::v1::DataContractV1Getters;
72 use crate::data_contract::config::v0::DataContractConfigSettersV0;
73 use crate::data_contract::methods::validate_update::DataContractUpdateValidationMethodsV0;
74 use crate::data_contract::schema::DataContractSchemaMethodsV0;
75 use crate::prelude::IdentityNonce;
76 use crate::tests::fixtures::get_data_contract_fixture;
77 use assert_matches::assert_matches;
78 use platform_value::platform_value;
79 use platform_value::Identifier;
80
81 mod validate_update {
82 use std::collections::BTreeMap;
83
84 use super::*;
85 use crate::data_contract::accessors::v0::DataContractV0Setters;
86 use crate::data_contract::accessors::v1::DataContractV1Setters;
87 use crate::data_contract::associated_token::token_configuration::accessors::v0::{
88 TokenConfigurationV0Getters, TokenConfigurationV0Setters,
89 };
90 use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0;
91 use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0;
92 use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention;
93 use crate::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0;
94 use crate::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization;
95 use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters;
96 use crate::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0;
97 use crate::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution;
98 use crate::data_contract::document_type::DocumentTypeMutRef;
99 use crate::data_contract::group::accessors::v0::{GroupV0Getters, GroupV0Setters};
100 use crate::data_contract::group::v0::GroupV0;
101 use crate::data_contract::group::Group;
102 use crate::data_contract::TokenConfiguration;
103 use crate::identity::accessors::IdentityGettersV0;
104 use crate::prelude::Identity;
105
106 #[test]
107 fn should_return_invalid_result_if_owner_id_is_not_the_same() {
108 let platform_version = PlatformVersion::latest();
109
110 let old_data_contract = get_data_contract_fixture(
111 None,
112 IdentityNonce::default(),
113 platform_version.protocol_version,
114 )
115 .data_contract_owned();
116
117 let mut new_data_contract = old_data_contract.clone();
118
119 new_data_contract.set_owner_id(Identifier::random());
120
121 let result = old_data_contract
122 .validate_update(&new_data_contract, &BlockInfo::default(), platform_version)
123 .expect("failed validate update");
124
125 assert_matches!(
126 result.errors.as_slice(),
127 [ConsensusError::StateError(
128 StateError::DataContractUpdatePermissionError(e)
129 )] if *e.data_contract_id() == old_data_contract.id() && *e.identity_id() == new_data_contract.owner_id()
130 );
131 }
132
133 #[test]
134 fn should_return_invalid_result_if_contract_version_is_not_greater_for_one() {
135 let platform_version = PlatformVersion::latest();
136
137 let old_data_contract = get_data_contract_fixture(
138 None,
139 IdentityNonce::default(),
140 platform_version.protocol_version,
141 )
142 .data_contract_owned();
143
144 let new_data_contract = old_data_contract.clone();
145
146 let result = old_data_contract
147 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
148 .expect("failed validate update");
149
150 assert_matches!(
151 result.errors.as_slice(),
152 [ConsensusError::BasicError(
153 BasicError::InvalidDataContractVersionError(e)
154 )] if e.expected_version() == old_data_contract.version() + 1 && e.version() == new_data_contract.version()
155 );
156 }
157
158 #[test]
159 fn should_return_invalid_result_if_config_was_updated() {
160 let platform_version = PlatformVersion::latest();
161
162 let old_data_contract = get_data_contract_fixture(
163 None,
164 IdentityNonce::default(),
165 platform_version.protocol_version,
166 )
167 .data_contract_owned();
168
169 let mut new_data_contract = old_data_contract.clone();
170
171 new_data_contract.set_version(old_data_contract.version() + 1);
172 new_data_contract.config_mut().set_readonly(true);
173
174 let result = old_data_contract
175 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
176 .expect("failed validate update");
177
178 assert_matches!(
179 result.errors.as_slice(),
180 [ConsensusError::StateError(
181 StateError::DataContractConfigUpdateError(e)
182 )] if e.additional_message() == "contract can not be changed to readonly"
183 );
184 }
185
186 #[test]
187 fn should_return_invalid_result_when_document_type_is_removed() {
188 let platform_version = PlatformVersion::latest();
189
190 let old_data_contract = get_data_contract_fixture(
191 None,
192 IdentityNonce::default(),
193 platform_version.protocol_version,
194 )
195 .data_contract_owned();
196
197 let mut new_data_contract = old_data_contract.clone();
198
199 new_data_contract.set_version(old_data_contract.version() + 1);
200 new_data_contract
201 .document_types_mut()
202 .remove("niceDocument");
203
204 let result = old_data_contract
205 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
206 .expect("failed validate update");
207
208 assert_matches!(
209 result.errors.as_slice(),
210 [ConsensusError::StateError(
211 StateError::DocumentTypeUpdateError(e)
212 )] if e.additional_message() == "document type can't be removed"
213 );
214 }
215
216 #[test]
217 fn should_return_invalid_result_when_document_type_has_incompatible_change() {
218 let platform_version = PlatformVersion::latest();
219
220 let old_data_contract = get_data_contract_fixture(
221 None,
222 IdentityNonce::default(),
223 platform_version.protocol_version,
224 )
225 .data_contract_owned();
226
227 let mut new_data_contract = old_data_contract.clone();
228
229 new_data_contract.set_version(old_data_contract.version() + 1);
230
231 match new_data_contract
232 .document_types_mut()
233 .get_mut("niceDocument")
234 .unwrap()
235 .as_mut_ref()
236 {
237 DocumentTypeMutRef::V0(dt) => dt.documents_mutable = false,
238 DocumentTypeMutRef::V1(dt) => dt.documents_mutable = false,
239 DocumentTypeMutRef::V2(dt) => dt.documents_mutable = false,
240 }
241
242 let result = old_data_contract
243 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
244 .expect("failed validate update");
245
246 assert_matches!(
247 result.errors.as_slice(),
248 [ConsensusError::StateError(
249 StateError::DocumentTypeUpdateError(e)
250 )] if e.additional_message() == "document type can not change whether its documents are mutable: changing from true to false"
251 );
252 }
253
254 #[test]
255 fn should_return_invalid_result_when_defs_is_removed() {
256 let platform_version = PlatformVersion::latest();
257
258 let mut old_data_contract = get_data_contract_fixture(
259 None,
260 IdentityNonce::default(),
261 platform_version.protocol_version,
262 )
263 .data_contract_owned();
264
265 old_data_contract
267 .document_types_mut()
268 .remove("prettyDocument");
269
270 let mut new_data_contract = old_data_contract.clone();
271
272 new_data_contract.set_version(old_data_contract.version() + 1);
273 new_data_contract
274 .set_schema_defs(None, false, &mut Vec::new(), platform_version)
275 .expect("failed to set schema defs");
276
277 let result = old_data_contract
278 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
279 .expect("failed validate update");
280
281 assert_matches!(
282 result.errors.as_slice(),
283 [ConsensusError::BasicError(
284 BasicError::IncompatibleDataContractSchemaError(e)
285 )] if e.operation() == "remove" && e.field_path() == "/$defs"
286 );
287 }
288
289 #[test]
290 fn should_return_invalid_result_when_updated_defs_is_incompatible() {
291 let platform_version = PlatformVersion::latest();
292
293 let old_data_contract = get_data_contract_fixture(
294 None,
295 IdentityNonce::default(),
296 platform_version.protocol_version,
297 )
298 .data_contract_owned();
299
300 let mut new_data_contract = old_data_contract.clone();
301
302 let incompatible_defs_value = platform_value!({
303 "lastName": {
304 "type": "number",
305 },
306 });
307 let incompatible_defs = incompatible_defs_value
308 .into_btree_string_map()
309 .expect("should convert to map");
310
311 new_data_contract.set_version(old_data_contract.version() + 1);
312 new_data_contract
313 .set_schema_defs(
314 Some(incompatible_defs),
315 false,
316 &mut Vec::new(),
317 platform_version,
318 )
319 .expect("failed to set schema defs");
320
321 let result = old_data_contract
322 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
323 .expect("failed validate update");
324
325 assert_matches!(
326 result.errors.as_slice(),
327 [ConsensusError::BasicError(
328 BasicError::IncompatibleDataContractSchemaError(e)
329 )] if e.operation() == "replace" && e.field_path() == "/$defs/lastName/type"
330 );
331 }
332
333 #[test]
334 fn should_pass_when_all_changes_are_compatible() {
335 let platform_version = PlatformVersion::latest();
336
337 let old_data_contract = get_data_contract_fixture(
338 None,
339 IdentityNonce::default(),
340 platform_version.protocol_version,
341 )
342 .data_contract_owned();
343
344 let mut new_data_contract = old_data_contract.clone();
345
346 new_data_contract.set_version(old_data_contract.version() + 1);
347
348 let result = old_data_contract
349 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
350 .expect("failed validate update");
351
352 assert!(result.is_valid());
353 }
354
355 #[test]
362 fn should_return_invalid_result_when_group_is_removed() {
363 let platform_version = PlatformVersion::latest();
364
365 let identity_1 = Identity::random_identity(3, Some(14), platform_version)
366 .expect("expected a platform identity");
367 let identity_1_id = identity_1.id();
368 let identity_2 = Identity::random_identity(3, Some(506), platform_version)
369 .expect("expected a platform identity");
370 let identity_2_id = identity_2.id();
371
372 let mut old_data_contract =
373 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
374 old_data_contract.set_groups(BTreeMap::from([(
375 0,
376 Group::V0(GroupV0 {
377 members: [(identity_1_id, 1), (identity_2_id, 1)].into(),
378 required_power: 2,
379 }),
380 )]));
381
382 let mut new_data_contract = old_data_contract.clone();
384 new_data_contract.set_version(old_data_contract.version() + 1);
385
386 let first_group_pos = *old_data_contract
388 .groups()
389 .keys()
390 .next()
391 .expect("fixture must have at least one group");
392 new_data_contract
393 .groups_mut()
394 .unwrap()
395 .remove(&first_group_pos);
396
397 let result = old_data_contract
398 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
399 .expect("failed validate update");
400
401 assert_matches!(
402 result.errors.as_slice(),
403 [ConsensusError::StateError(
404 StateError::DataContractUpdateActionNotAllowedError(e)
405 )] if e.action() == "remove group"
406 );
407 }
408
409 #[test]
410 fn should_return_invalid_result_when_group_is_changed() {
411 let platform_version = PlatformVersion::latest();
412
413 let identity_1 = Identity::random_identity(3, Some(14), platform_version)
414 .expect("expected a platform identity");
415 let identity_1_id = identity_1.id();
416 let identity_2 = Identity::random_identity(3, Some(506), platform_version)
417 .expect("expected a platform identity");
418 let identity_2_id = identity_2.id();
419
420 let mut old_data_contract =
421 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
422 old_data_contract.set_groups(BTreeMap::from([(
423 0,
424 Group::V0(GroupV0 {
425 members: [(identity_1_id, 1), (identity_2_id, 1)].into(),
426 required_power: 2,
427 }),
428 )]));
429
430 let mut new_data_contract = old_data_contract.clone();
432 new_data_contract.set_version(old_data_contract.version() + 1);
433
434 let first_group_pos = *new_data_contract
437 .groups()
438 .keys()
439 .next()
440 .expect("fixture must have at least one group");
441 let mut altered_group = new_data_contract
442 .groups()
443 .get(&first_group_pos)
444 .cloned()
445 .expect("group must exist");
446 altered_group.set_required_power(altered_group.required_power() + 1);
448 new_data_contract
449 .groups_mut()
450 .unwrap()
451 .insert(first_group_pos, altered_group);
452
453 let result = old_data_contract
454 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
455 .expect("failed validate update");
456
457 assert_matches!(
458 result.errors.as_slice(),
459 [ConsensusError::StateError(
460 StateError::DataContractUpdateActionNotAllowedError(e)
461 )] if e.action() == format!(
462 "change group at position {} is not allowed",
463 first_group_pos
464 )
465 );
466 }
467
468 #[test]
475 fn should_return_invalid_result_when_token_is_removed() {
476 let platform_version = PlatformVersion::latest();
477
478 let mut old_data_contract =
479 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
480 old_data_contract.set_tokens(BTreeMap::from([(
481 0,
482 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()),
483 )]));
484
485 let mut new_data_contract = old_data_contract.clone();
486 new_data_contract.set_version(old_data_contract.version() + 1);
487
488 let first_token_pos = *old_data_contract
490 .tokens()
491 .keys()
492 .next()
493 .expect("fixture must have at least one token");
494 new_data_contract
495 .tokens_mut()
496 .unwrap()
497 .remove(&first_token_pos);
498
499 let result = old_data_contract
500 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
501 .expect("failed validate update");
502
503 assert_matches!(
504 result.errors.as_slice(),
505 [ConsensusError::StateError(
506 StateError::DataContractUpdateActionNotAllowedError(e)
507 )] if e.action() == format!("remove token at position {}", first_token_pos)
508 );
509 }
510
511 #[test]
512 fn should_return_invalid_result_when_token_is_updated() {
513 let platform_version = PlatformVersion::latest();
514
515 let mut old_data_contract =
516 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
517 old_data_contract.set_tokens(BTreeMap::from([(
518 0,
519 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()),
520 )]));
521
522 let mut new_data_contract = old_data_contract.clone();
523 new_data_contract.set_version(old_data_contract.version() + 1);
524
525 let first_token_pos = *new_data_contract
527 .tokens()
528 .keys()
529 .next()
530 .expect("fixture must have at least one token");
531 let mut altered_token_cfg = new_data_contract
532 .tokens()
533 .get(&first_token_pos)
534 .cloned()
535 .expect("token must exist");
536 altered_token_cfg.set_base_supply(altered_token_cfg.base_supply() + 1);
538 new_data_contract
539 .tokens_mut()
540 .unwrap()
541 .insert(first_token_pos, altered_token_cfg);
542
543 let result = old_data_contract
544 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
545 .expect("failed validate update");
546
547 assert_matches!(
548 result.errors.as_slice(),
549 [ConsensusError::StateError(
550 StateError::DataContractUpdateActionNotAllowedError(e)
551 )] if e.action() == format!("update token at position {}", first_token_pos)
552 );
553 }
554
555 #[test]
556 fn should_return_invalid_result_when_token_is_added_with_past_timestamp() {
557 let platform_version = PlatformVersion::latest();
558
559 let mut old_data_contract =
560 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
561 let mut token_cfg =
562 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
563 token_cfg.set_conventions(TokenConfigurationConvention::V0(
564 TokenConfigurationConventionV0 {
565 localizations: BTreeMap::from([(
566 "en".to_string(),
567 TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 {
568 should_capitalize: false,
569 singular_form: "test".to_string(),
570 plural_form: "tests".to_string(),
571 }),
572 )]),
573 decimals: 8,
574 },
575 ));
576 old_data_contract.set_tokens(BTreeMap::from([(0, token_cfg)]));
577
578 let mut new_data_contract = old_data_contract.clone();
579 new_data_contract.set_version(old_data_contract.version() + 1);
580
581 let existing_cfg = new_data_contract
583 .tokens()
584 .values()
585 .next()
586 .expect("fixture must have at least one token")
587 .clone();
588 let new_position = old_data_contract
589 .tokens()
590 .keys()
591 .max()
592 .expect("fixture must have at least one token")
593 + 1;
594 let mut new_token_cfg = existing_cfg.clone();
595 new_token_cfg
596 .distribution_rules_mut()
597 .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0(
598 TokenPreProgrammedDistributionV0 {
599 distributions: BTreeMap::from([(
600 0,
601 BTreeMap::from([(new_data_contract.owner_id(), 100)]),
602 )]),
603 },
604 )));
605 new_data_contract
606 .tokens_mut()
607 .unwrap()
608 .insert(new_position, new_token_cfg);
609
610 let result = old_data_contract
611 .validate_update_v0(
612 &new_data_contract,
613 &BlockInfo::default_with_time(100000),
614 platform_version,
615 )
616 .expect("failed validate update");
617
618 assert_matches!(
619 result.errors.as_slice(),
620 [ConsensusError::StateError(
621 StateError::PreProgrammedDistributionTimestampInPastError(e)
622 )] if e.token_position() == new_position
623 );
624 }
625
626 #[test]
627 fn should_pass_when_a_well_formed_new_token_is_added() {
628 let platform_version = PlatformVersion::latest();
629
630 let old_data_contract =
631 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
632
633 let mut new_data_contract = old_data_contract.clone();
634 new_data_contract.set_version(old_data_contract.version() + 1);
635
636 let valid_token_cfg = {
638 let mut cfg =
639 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
640
641 cfg.set_base_supply(1_000_000); cfg.set_conventions(TokenConfigurationConvention::V0(
644 TokenConfigurationConventionV0 {
645 localizations: BTreeMap::from([(
646 "en".to_string(),
647 TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 {
648 should_capitalize: true,
649 singular_form: "credit".to_string(),
650 plural_form: "credits".to_string(),
651 }),
652 )]),
653 decimals: 8,
654 },
655 ));
656
657 cfg
658 };
659
660 new_data_contract
662 .tokens_mut()
663 .unwrap()
664 .insert(0, valid_token_cfg);
665
666 let result = old_data_contract
667 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
668 .expect("failed validate update");
669
670 assert!(result.is_valid(), "well‑formed token should be accepted");
671 }
672
673 #[test]
680 fn should_pass_when_groups_and_tokens_unchanged() {
681 let platform_version = PlatformVersion::latest();
682
683 let old_data_contract =
684 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
685
686 let mut new_data_contract = old_data_contract.clone();
687 new_data_contract.set_version(old_data_contract.version() + 1);
688
689 let result = old_data_contract
690 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
691 .expect("failed validate update");
692
693 assert!(result.is_valid());
694 }
695 }
696}