Skip to main content

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

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            // Remove document that uses $defs, so we can safely remove it for testing
266            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        //
356        // ──────────────────────────────────────────────────────────────────────────
357        //  Group‑related rules
358        // ──────────────────────────────────────────────────────────────────────────
359        //
360
361        #[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            // Clone & bump version
383            let mut new_data_contract = old_data_contract.clone();
384            new_data_contract.set_version(old_data_contract.version() + 1);
385
386            // Remove the first (and normally only) group
387            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            // Clone & bump version
431            let mut new_data_contract = old_data_contract.clone();
432            new_data_contract.set_version(old_data_contract.version() + 1);
433
434            // Mutate the first group in some trivial way so that
435            // `old_group != new_group` evaluates to true.
436            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            // Tweak required power
447            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        //
469        // ──────────────────────────────────────────────────────────────────────────
470        //  Token‑related rules
471        // ──────────────────────────────────────────────────────────────────────────
472        //
473
474        #[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            // Remove an existing token
489            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            // Modify an existing token configuration
526            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            // Tweak base supply
537            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            // Create a new token with a past timestamp
582            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            // build a fully valid token configuration
637            let valid_token_cfg = {
638                let mut cfg =
639                    TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
640
641                cfg.set_base_supply(1_000_000); // within limits
642
643                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            // insert at contiguous position 0 (old contract had no tokens)
661            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        //
674        // ──────────────────────────────────────────────────────────────────────────
675        //  Happy‑path check: no token / group changes
676        // ──────────────────────────────────────────────────────────────────────────
677        //
678
679        #[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}