diff --git a/packages/rs-dpp/src/data_contract/data_contract_facade.rs b/packages/rs-dpp/src/data_contract/data_contract_facade.rs index aa352c4769..29f20989f7 100644 --- a/packages/rs-dpp/src/data_contract/data_contract_facade.rs +++ b/packages/rs-dpp/src/data_contract/data_contract_facade.rs @@ -94,10 +94,14 @@ impl DataContractFacade { /// Create Data Contract Update State Transition pub fn create_data_contract_update_transition( &self, - data_contract: DataContract, + old_data_contract: &DataContract, + new_data_contract: &DataContract, identity_contract_nonce: IdentityNonce, ) -> Result { - self.factory - .create_data_contract_update_transition(data_contract, identity_contract_nonce) + self.factory.create_data_contract_update_transition( + old_data_contract, + new_data_contract, + identity_contract_nonce, + ) } } diff --git a/packages/rs-dpp/src/data_contract/factory/mod.rs b/packages/rs-dpp/src/data_contract/factory/mod.rs index 0953489760..38ce0b1d01 100644 --- a/packages/rs-dpp/src/data_contract/factory/mod.rs +++ b/packages/rs-dpp/src/data_contract/factory/mod.rs @@ -146,12 +146,14 @@ impl DataContractFactory { /// Create a DataContractUpdateTransition pub fn create_data_contract_update_transition( &self, - data_contract: DataContract, + old_data_contract: &DataContract, + new_data_contract: &DataContract, identity_contract_nonce: IdentityNonce, ) -> Result { match self { DataContractFactory::V0(v0) => v0.create_unsigned_data_contract_update_transition( - data_contract, + old_data_contract, + new_data_contract, identity_contract_nonce, ), } diff --git a/packages/rs-dpp/src/data_contract/factory/v0/mod.rs b/packages/rs-dpp/src/data_contract/factory/v0/mod.rs index 635d90ec23..1f9ae5948b 100644 --- a/packages/rs-dpp/src/data_contract/factory/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/factory/v0/mod.rs @@ -197,11 +197,14 @@ impl DataContractFactoryV0 { #[cfg(feature = "state-transitions")] pub fn create_unsigned_data_contract_update_transition( &self, - data_contract: DataContract, + old_data_contract: &DataContract, + new_data_contract: &DataContract, identity_contract_nonce: IdentityNonce, ) -> Result { - DataContractUpdateTransition::try_from_platform_versioned( - (data_contract, identity_contract_nonce), + DataContractUpdateTransition::from_contract_update( + old_data_contract, + new_data_contract, + identity_contract_nonce, PlatformVersion::get(self.protocol_version)?, ) } diff --git a/packages/rs-dpp/src/data_contract/methods/apply_update/mod.rs b/packages/rs-dpp/src/data_contract/methods/apply_update/mod.rs new file mode 100644 index 0000000000..24b2274e3d --- /dev/null +++ b/packages/rs-dpp/src/data_contract/methods/apply_update/mod.rs @@ -0,0 +1,58 @@ +mod v0; + +use crate::block::block_info::BlockInfo; +use crate::data_contract::update_values::DataContractUpdateValues; +use crate::data_contract::DataContract; +use crate::validation::operations::ProtocolValidationOperation; +use crate::validation::ConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +impl DataContract { + /// Applies update values to this contract to produce a new contract. + /// + /// This method takes the current contract and applies the changes specified in the + /// update values to produce a new updated contract. It merges document schemas, + /// groups, tokens, and keywords from the update into the existing contract. + /// + /// This method dispatches to a version-specific implementation based on the + /// platform version configuration. If the version is unrecognized, it returns a version mismatch error. + /// + /// # Arguments + /// - `update_values`: The update values containing the changes to apply. + /// - `block_info`: Block information containing timestamp, epoch, and height for the update. + /// - `full_validation`: Whether to perform full validation on the resulting contract. + /// - `validation_operations`: A vector to collect validation operations. + /// - `platform_version`: The current platform version. + /// + /// # Returns + /// - `Ok(ConsensusValidationResult)`: The validation result containing the new contract if successful. + /// - `Err(ProtocolError)`: If the platform version is unrecognized or if a protocol error occurs. + /// + /// # Version Behavior + /// - Version 0: Applies updates using the standard merge logic for document schemas, + /// groups, tokens, keywords, and description. + pub fn apply_update( + &self, + update_values: DataContractUpdateValues<'_>, + block_info: &BlockInfo, + full_validation: bool, + validation_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result, ProtocolError> { + match platform_version.dpp.contract_versions.methods.apply_update { + 0 => self.apply_update_v0( + update_values, + block_info, + full_validation, + validation_operations, + platform_version, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DataContract::apply_update".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/apply_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/apply_update/v0/mod.rs new file mode 100644 index 0000000000..9cf9ad155a --- /dev/null +++ b/packages/rs-dpp/src/data_contract/methods/apply_update/v0/mod.rs @@ -0,0 +1,291 @@ +use std::collections::BTreeMap; + +use crate::block::block_info::BlockInfo; +use crate::consensus::basic::data_contract::InvalidDataContractVersionError; +use crate::consensus::state::data_contract::document_type_update_error::DocumentTypeUpdateError; +use crate::consensus::state::state_error::StateError; +use crate::consensus::state::token::PreProgrammedDistributionTimestampInPastError; +use crate::data_contract::accessors::v0::DataContractV0Getters; +use crate::data_contract::accessors::v1::{DataContractV1Getters, DataContractV1Setters}; +use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use crate::data_contract::associated_token::token_pre_programmed_distribution::accessors::v0::TokenPreProgrammedDistributionV0Methods; +use crate::data_contract::schema::DataContractSchemaMethodsV0; +use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1; +use crate::data_contract::serialized_version::DataContractInSerializationFormat; +use crate::data_contract::update_values::DataContractUpdateValues; +use crate::data_contract::DataContract; +use crate::validation::operations::ProtocolValidationOperation; +use crate::validation::ConsensusValidationResult; +use crate::ProtocolError; +use platform_value::Value; +use platform_version::version::PlatformVersion; + +impl DataContract { + /// Applies update values to this contract to produce a new contract. + /// + /// This method takes the current contract and applies the changes specified in the + /// update values to produce a new updated contract. It merges document schemas, + /// groups, tokens, and keywords from the update into the existing contract. + /// + /// # Arguments + /// - `update_values`: The update values containing the changes to apply. + /// - `block_info`: Block information containing timestamp, epoch, and height for the update. + /// - `full_validation`: Whether to perform full validation on the resulting contract. + /// - `validation_operations`: A vector to collect validation operations. + /// - `platform_version`: The current platform version. + /// + /// # Returns + /// - `Ok(ConsensusValidationResult)`: The validation result containing the new contract if successful. + /// - `Err(ProtocolError)`: If a protocol error occurs during transformation. + pub(super) fn apply_update_v0( + &self, + update_values: DataContractUpdateValues<'_>, + block_info: &BlockInfo, + full_validation: bool, + validation_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result, ProtocolError> { + // Check version is bumped by exactly 1 + let new_version = update_values.revision; + let old_version = self.version(); + if new_version < old_version || new_version - old_version != 1 { + return Ok(ConsensusValidationResult::new_with_error( + InvalidDataContractVersionError::new(old_version + 1, new_version).into(), + )); + } + + // Validate that updated document schemas correspond to existing document types + for document_type_name in update_values.updated_document_schemas.keys() { + if self + .document_type_optional_for_name(document_type_name) + .is_none() + { + return Ok(ConsensusValidationResult::new_with_error( + DocumentTypeUpdateError::new( + self.id(), + document_type_name, + "document type does not exist and cannot be updated", + ) + .into(), + )); + } + } + + // Validate schema_defs update (existence and compatibility) + let schema_defs_validation_result = DataContract::validate_schema_defs_update( + self.id(), + self.schema_defs(), + update_values.updated_schema_defs, + update_values.new_schema_defs, + platform_version, + )?; + if !schema_defs_validation_result.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + schema_defs_validation_result.errors, + )); + } + + // Start with the old contract's document schemas + let mut document_schemas: BTreeMap = self + .document_schemas() + .into_iter() + .map(|(name, schema)| (name, schema.clone())) + .collect(); + + // Apply updated document schemas + for (name, schema) in update_values.updated_document_schemas { + document_schemas.insert(name.clone(), schema.clone()); + } + + // Add new document schemas + for (name, schema) in update_values.new_document_schemas { + document_schemas.insert(name.clone(), schema.clone()); + } + + // Merge groups + let mut groups = self.groups().clone(); + for (pos, group) in update_values.new_groups { + groups.insert(*pos, group.clone()); + } + + // Validate combined groups if new groups were added + if !update_values.new_groups.is_empty() { + let validation_result = + DataContract::validate_groups(&groups, false, platform_version)?; + if !validation_result.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + validation_result.errors, + )); + } + } + + // Validate that new tokens are contiguous with existing tokens + if !update_values.new_tokens.is_empty() { + let existing_tokens = self.tokens(); + let highest_existing_position = existing_tokens.keys().max().copied(); + let first_new_position = update_values.new_tokens.keys().min().copied(); + + if let Some(first_new) = first_new_position { + let expected_first = highest_existing_position.map(|h| h + 1).unwrap_or(0); + if first_new != expected_first { + return Ok(ConsensusValidationResult::new_with_error( + crate::consensus::basic::data_contract::NonContiguousContractTokenPositionsError::new( + expected_first, // missing_position + first_new, // followed_position + ) + .into(), + )); + } + } + } + + // Merge tokens + let mut tokens = self.tokens().clone(); + for (pos, token) in update_values.new_tokens { + tokens.insert(*pos, token.clone()); + } + + // Validate that new tokens reference groups that exist in the combined groups + for token_configuration in update_values.new_tokens.values() { + let validation_result = token_configuration + .validate_token_config_groups_exist(&groups, platform_version)?; + if !validation_result.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + validation_result.errors, + )); + } + } + + // Update keywords + let mut keywords: Vec = self + .keywords() + .iter() + .filter(|k| !update_values.remove_keywords.contains(k)) + .cloned() + .collect(); + keywords.extend(update_values.add_keywords.iter().cloned()); + + // Validate combined keywords (structure already validated in basic validation) + let validation_result = + DataContract::validate_keywords(self.id(), &keywords, false, platform_version)?; + if !validation_result.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + validation_result.errors, + )); + } + + // Update description + let description = match update_values.update_description { + None => self.description().cloned(), + Some(new_desc) => new_desc.clone(), + }; + + // Update schema_defs + let schema_defs = if update_values.updated_schema_defs.is_empty() + && update_values.new_schema_defs.is_empty() + { + // No changes to schema_defs + self.schema_defs().cloned() + } else { + // Start with old schema_defs or empty map + let mut defs = self.schema_defs().cloned().unwrap_or_default(); + // Apply updates + for (name, def) in update_values.updated_schema_defs { + defs.insert(name.clone(), def.clone()); + } + // Add new defs + for (name, def) in update_values.new_schema_defs { + defs.insert(name.clone(), def.clone()); + } + Some(defs) + }; + + // Validate pre-programmed distribution timestamps for new tokens + for (token_contract_position, token_configuration) in update_values.new_tokens { + if let Some(distribution) = token_configuration + .distribution_rules() + .pre_programmed_distribution() + { + if let Some((timestamp, _)) = distribution.distributions().iter().next() { + if timestamp < &block_info.time_ms { + return Ok(ConsensusValidationResult::new_with_error( + StateError::PreProgrammedDistributionTimestampInPastError( + PreProgrammedDistributionTimestampInPastError::new( + self.id(), + *token_contract_position, + *timestamp, + block_info.time_ms, + ), + ) + .into(), + )); + } + } + } + } + + // Create the new contract using serialization format + let serialization_format = + DataContractInSerializationFormat::V1(DataContractInSerializationFormatV1 { + id: self.id(), + config: *self.config(), + version: update_values.revision, + owner_id: self.owner_id(), + schema_defs, + document_schemas, + created_at: self.created_at(), + updated_at: None, // Will be set below + created_at_block_height: self.created_at_block_height(), + updated_at_block_height: None, // Will be set below + created_at_epoch: self.created_at_epoch(), + updated_at_epoch: None, // Will be set below + groups, + tokens, + keywords, + description, + }); + + let mut new_contract = DataContract::try_from_platform_versioned( + serialization_format, + full_validation, + validation_operations, + platform_version, + )?; + + // Validate document type update rules for updated schemas + for document_type_name in update_values.updated_document_schemas.keys() { + let old_document_type = self + .document_type_optional_for_name(document_type_name) + .expect("document type existence was already validated"); + + let Some(new_document_type) = + new_contract.document_type_optional_for_name(document_type_name) + else { + return Ok(ConsensusValidationResult::new_with_error( + DocumentTypeUpdateError::new( + self.id(), + document_type_name, + "document type failed to be created from updated schema", + ) + .into(), + )); + }; + + let validate_update_result = + old_document_type.validate_update(new_document_type, platform_version)?; + + if !validate_update_result.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + validate_update_result.errors, + )); + } + } + + new_contract.set_updated_at(Some(block_info.time_ms)); + new_contract.set_updated_at_epoch(Some(block_info.epoch.index)); + new_contract.set_updated_at_block_height(Some(block_info.height)); + + Ok(ConsensusValidationResult::new_with_data(new_contract)) + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/mod.rs b/packages/rs-dpp/src/data_contract/methods/mod.rs index 04f79db887..d94d33dabc 100644 --- a/packages/rs-dpp/src/data_contract/methods/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "validation")] +mod apply_update; mod equal_ignoring_time_based_fields; mod registration_cost; pub mod schema; @@ -6,4 +8,10 @@ pub mod validate_document; #[cfg(feature = "validation")] pub mod validate_groups; #[cfg(feature = "validation")] +pub mod validate_keywords; +#[cfg(feature = "validation")] +pub mod validate_schema_defs_update; +#[cfg(feature = "validation")] +pub mod validate_tokens; +#[cfg(feature = "validation")] pub mod validate_update; diff --git a/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs b/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs index 6dce125363..a5ad915042 100644 --- a/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/registration_cost/v1/mod.rs @@ -100,32 +100,36 @@ impl DataContractInSerializationFormat { let fee_version = &platform_version.fee_version.data_contract_registration; let mut cost = fee_version.base_contract_registration_fee; - for document_type_schema in self.document_schemas().values() { - cost = cost.saturating_add(fee_version.document_type_registration_fee); - - // If this is not okay the registration will fail on basic validation - if let Ok(schema_map) = document_type_schema.to_map() { - // Initialize indices - if let Ok(Some(index_values)) = Value::inner_optional_array_slice_value( - schema_map, - crate::data_contract::document_type::property_names::INDICES, - ) { - for index_value in index_values { - if let Ok(index_value_map) = index_value.to_map() { - if let Ok(index) = Index::try_from(index_value_map.as_slice()) { - let base_index_fee = if index.contested_index.is_some() { - fee_version.document_type_base_contested_index_registration_fee - } else if index.unique { - fee_version.document_type_base_unique_index_registration_fee - } else { - fee_version.document_type_base_non_unique_index_registration_fee - }; - cost = cost.saturating_add(base_index_fee); + if let Some(document_schemas) = self.document_schemas() { + for document_type_schema in document_schemas.values() { + cost = cost.saturating_add(fee_version.document_type_registration_fee); + + // If this is not okay the registration will fail on basic validation + if let Ok(schema_map) = document_type_schema.to_map() { + // Initialize indices + if let Ok(Some(index_values)) = Value::inner_optional_array_slice_value( + schema_map, + crate::data_contract::document_type::property_names::INDICES, + ) { + for index_value in index_values { + if let Ok(index_value_map) = index_value.to_map() { + if let Ok(index) = Index::try_from(index_value_map.as_slice()) { + let base_index_fee = if index.contested_index.is_some() { + fee_version + .document_type_base_contested_index_registration_fee + } else if index.unique { + fee_version.document_type_base_unique_index_registration_fee + } else { + fee_version + .document_type_base_non_unique_index_registration_fee + }; + cost = cost.saturating_add(base_index_fee); + } } } } - } - }; + }; + } } for token_config in self.tokens().values() { diff --git a/packages/rs-dpp/src/data_contract/methods/validate_groups/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_groups/mod.rs index d6a6bef8a8..8ad568fc56 100644 --- a/packages/rs-dpp/src/data_contract/methods/validate_groups/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/validate_groups/mod.rs @@ -15,6 +15,8 @@ impl DataContract { /// - `groups`: A reference to a `BTreeMap` of group contract positions (`GroupContractPosition`) /// mapped to their corresponding `Group` objects. These represent the groups associated with /// the data contract. + /// - `allow_offset_start`: If true, allows the groups to start at a position other than 0. + /// This is useful for update transitions where new groups continue from existing ones. /// - `platform_version`: A reference to the [`PlatformVersion`](crate::version::PlatformVersion) /// object specifying the version of the platform and determining which validation method to use. /// @@ -39,6 +41,7 @@ impl DataContract { /// - Invalid individual group configurations (e.g., power-related errors or exceeding member limits). pub fn validate_groups( groups: &BTreeMap, + allow_offset_start: bool, platform_version: &PlatformVersion, ) -> Result { match platform_version @@ -47,7 +50,7 @@ impl DataContract { .methods .validate_groups { - 0 => Self::validate_groups_v0(groups, platform_version), + 0 => Self::validate_groups_v0(groups, allow_offset_start, platform_version), version => Err(ProtocolError::UnknownVersionMismatch { method: "DataContract::validate_groups".to_string(), known_versions: vec![0], diff --git a/packages/rs-dpp/src/data_contract/methods/validate_groups/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_groups/v0/mod.rs index faaf9e7a21..ef50aabd1f 100644 --- a/packages/rs-dpp/src/data_contract/methods/validate_groups/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/validate_groups/v0/mod.rs @@ -11,13 +11,20 @@ impl DataContract { #[inline(always)] pub(super) fn validate_groups_v0( groups: &BTreeMap, + allow_offset_start: bool, platform_version: &PlatformVersion, ) -> Result { // Check for gaps in the group contract positions - for (expected_position, position) in groups.keys().enumerate() { - let expected_position = expected_position as GroupContractPosition; + let start_position = if allow_offset_start { + groups.keys().next().copied().unwrap_or(0) + } else { + 0 + }; - if *position != expected_position as GroupContractPosition { + for (index, position) in groups.keys().enumerate() { + let expected_position = start_position + index as GroupContractPosition; + + if *position != expected_position { return Ok(SimpleConsensusValidationResult::new_with_error( NonContiguousContractGroupPositionsError::new(expected_position, *position) .into(), diff --git a/packages/rs-dpp/src/data_contract/methods/validate_keywords/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_keywords/mod.rs new file mode 100644 index 0000000000..e5914cbc7e --- /dev/null +++ b/packages/rs-dpp/src/data_contract/methods/validate_keywords/mod.rs @@ -0,0 +1,56 @@ +use crate::prelude::DataContract; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; + +mod v0; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; + +impl DataContract { + /// Validates the provided keywords to ensure they meet the requirements for data contracts. + /// + /// # Parameters + /// - `contract_id`: The identifier of the data contract these keywords belong to. + /// - `keywords`: A slice of keyword strings to validate. + /// - `validate_structure`: If true, validates individual keyword structure (length, characters). + /// If false, only validates count and uniqueness. Use false when structure was already + /// validated in basic structure validation. + /// - `platform_version`: A reference to the [`PlatformVersion`](crate::version::PlatformVersion) + /// object specifying the version of the platform and determining which validation method to use. + /// + /// # Returns + /// - `Ok(SimpleConsensusValidationResult)` if all the keywords pass validation: + /// - No more than 50 keywords + /// - Each keyword is between 3 and 50 characters (if `validate_structure` is true) + /// - Each keyword contains no control or whitespace characters (if `validate_structure` is true) + /// - All keywords are unique + /// - `Err(ProtocolError)` if an unknown or unsupported platform version is provided. + /// + /// # Errors + /// - Returns a `ProtocolError::UnknownVersionMismatch` if the platform version is not recognized. + /// - Returns validation errors for: + /// - Too many keywords (`TooManyKeywordsError`) + /// - Invalid keyword length (`InvalidKeywordLengthError`) - only if `validate_structure` is true + /// - Invalid keyword characters (`InvalidKeywordCharacterError`) - only if `validate_structure` is true + /// - Duplicate keywords (`DuplicateKeywordsError`) + pub fn validate_keywords( + contract_id: Identifier, + keywords: &[String], + validate_structure: bool, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .contract_versions + .methods + .validate_keywords + { + 0 => Self::validate_keywords_v0(contract_id, keywords, validate_structure), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DataContract::validate_keywords".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/validate_keywords/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_keywords/v0/mod.rs new file mode 100644 index 0000000000..e71971803e --- /dev/null +++ b/packages/rs-dpp/src/data_contract/methods/validate_keywords/v0/mod.rs @@ -0,0 +1,57 @@ +use crate::consensus::basic::data_contract::{ + DuplicateKeywordsError, InvalidKeywordCharacterError, InvalidKeywordLengthError, + TooManyKeywordsError, +}; +use crate::data_contract::DataContract; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_value::Identifier; +use std::collections::HashSet; + +impl DataContract { + #[inline(always)] + pub(super) fn validate_keywords_v0( + contract_id: Identifier, + keywords: &[String], + validate_structure: bool, + ) -> Result { + // Validate there are no more than 50 keywords + if keywords.len() > 50 { + return Ok(SimpleConsensusValidationResult::new_with_error( + TooManyKeywordsError::new(contract_id, keywords.len() as u8).into(), + )); + } + + // Validate the keywords are all unique and between 3 and 50 characters + let mut seen_keywords = HashSet::new(); + for keyword in keywords { + if validate_structure { + // Check keyword length + if keyword.len() < 3 || keyword.len() > 50 { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidKeywordLengthError::new(contract_id, keyword.to_string()).into(), + )); + } + + if !keyword + .chars() + .all(|c| !c.is_control() && !c.is_whitespace()) + { + // This would mean we have an invalid character + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidKeywordCharacterError::new(contract_id, keyword.to_string()).into(), + )); + } + } + + // Check uniqueness (always done) + if !seen_keywords.insert(keyword) { + return Ok(SimpleConsensusValidationResult::new_with_error( + DuplicateKeywordsError::new(contract_id, keyword.to_string()).into(), + )); + } + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/validate_schema_defs_update/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_schema_defs_update/mod.rs new file mode 100644 index 0000000000..1b3d2642b3 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/methods/validate_schema_defs_update/mod.rs @@ -0,0 +1,58 @@ +use std::collections::BTreeMap; + +use crate::prelude::DataContract; +use platform_value::{Identifier, Value}; +use platform_version::version::PlatformVersion; + +mod v0; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; + +impl DataContract { + /// Validates schema $defs updates for compatibility with existing definitions. + /// + /// This method validates that: + /// 1. Updated schema_defs reference existing definitions (can't update non-existent defs) + /// 2. Updated schema_defs are compatible with the old definitions (schema compatibility) + /// + /// # Parameters + /// - `contract_id`: The identifier of the data contract these schema defs belong to. + /// - `old_schema_defs`: The existing schema $defs (if any) from the current contract. + /// - `updated_schema_defs`: Schema definitions being updated (must exist in old_schema_defs). + /// - `new_schema_defs`: New schema definitions being added. + /// - `platform_version`: A reference to the [`PlatformVersion`] specifying which + /// validation method version to use. + /// + /// # Returns + /// - `Ok(SimpleConsensusValidationResult)` containing validation errors if any: + /// - `IncompatibleDataContractSchemaError` if trying to update non-existent defs + /// - `IncompatibleDataContractSchemaError` if updated defs are incompatible + /// - `Err(ProtocolError)` if an unknown or unsupported platform version is provided. + pub fn validate_schema_defs_update( + contract_id: Identifier, + old_schema_defs: Option<&BTreeMap>, + updated_schema_defs: &BTreeMap, + new_schema_defs: &BTreeMap, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .contract_versions + .methods + .validate_schema_defs_update + { + 0 => Self::validate_schema_defs_update_v0( + contract_id, + old_schema_defs, + updated_schema_defs, + new_schema_defs, + platform_version, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DataContract::validate_schema_defs_update".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/validate_schema_defs_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_schema_defs_update/v0/mod.rs new file mode 100644 index 0000000000..cc09091bb5 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/methods/validate_schema_defs_update/v0/mod.rs @@ -0,0 +1,126 @@ +use std::collections::BTreeMap; + +use crate::consensus::basic::data_contract::IncompatibleDataContractSchemaError; +use crate::data_contract::document_type::schema::validate_schema_compatibility; +use crate::data_contract::DataContract; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_value::{Identifier, Value}; +use platform_version::version::PlatformVersion; +use serde_json::json; + +impl DataContract { + /// Validates schema $defs updates for compatibility with existing definitions. + /// + /// This method validates that: + /// 1. Updated schema_defs reference existing definitions + /// 2. Updated schema_defs are compatible with the old definitions + /// + /// # Arguments + /// - `contract_id`: The identifier of the data contract. + /// - `old_schema_defs`: The existing schema $defs (if any). + /// - `updated_schema_defs`: The schema $defs being updated. + /// - `new_schema_defs`: The new schema $defs being added. + /// - `platform_version`: The current platform version. + /// + /// # Returns + /// - `Ok(SimpleConsensusValidationResult)` containing validation errors if any. + /// - `Err(ProtocolError)` if a protocol error occurs. + #[inline(always)] + pub(super) fn validate_schema_defs_update_v0( + contract_id: Identifier, + old_schema_defs: Option<&BTreeMap>, + updated_schema_defs: &BTreeMap, + new_schema_defs: &BTreeMap, + platform_version: &PlatformVersion, + ) -> Result { + // If no updates, nothing to validate + if updated_schema_defs.is_empty() && new_schema_defs.is_empty() { + return Ok(SimpleConsensusValidationResult::new()); + } + + // Validate that updated schema_defs reference existing ones + if let Some(old_defs_map) = old_schema_defs { + for schema_def_name in updated_schema_defs.keys() { + if !old_defs_map.contains_key(schema_def_name) { + return Ok(SimpleConsensusValidationResult::new_with_error( + IncompatibleDataContractSchemaError::new( + contract_id, + "add".to_string(), + format!("/$defs/{}", schema_def_name), + ) + .into(), + )); + } + } + } else if !updated_schema_defs.is_empty() { + // Can't update schema defs if none exist + return Ok(SimpleConsensusValidationResult::new_with_error( + IncompatibleDataContractSchemaError::new( + contract_id, + "update".to_string(), + "/$defs".to_string(), + ) + .into(), + )); + } + + // If we have updates, validate compatibility + if !updated_schema_defs.is_empty() { + if let Some(old_defs_map) = old_schema_defs { + // Build merged new defs + let mut merged_defs = old_defs_map.clone(); + for (name, def) in updated_schema_defs { + merged_defs.insert(name.clone(), def.clone()); + } + for (name, def) in new_schema_defs { + merged_defs.insert(name.clone(), def.clone()); + } + + // Validate compatibility + if old_defs_map != &merged_defs { + let old_defs_json = Value::from(old_defs_map) + .try_into_validating_json() + .map_err(ProtocolError::ValueError)?; + + let new_defs_json = Value::from(&merged_defs) + .try_into_validating_json() + .map_err(ProtocolError::ValueError)?; + + let old_defs_schema = json!({ + "$defs": old_defs_json + }); + + let new_defs_schema = json!({ + "$defs": new_defs_json + }); + + let compatibility_validation_result = validate_schema_compatibility( + &old_defs_schema, + &new_defs_schema, + platform_version, + )?; + + if !compatibility_validation_result.is_valid() { + let errors = compatibility_validation_result + .errors + .into_iter() + .map(|operation| { + IncompatibleDataContractSchemaError::new( + contract_id, + operation.name, + operation.path, + ) + .into() + }) + .collect(); + + return Ok(SimpleConsensusValidationResult::new_with_errors(errors)); + } + } + } + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/validate_tokens/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_tokens/mod.rs new file mode 100644 index 0000000000..92031a33b6 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/methods/validate_tokens/mod.rs @@ -0,0 +1,72 @@ +use crate::data_contract::associated_token::token_configuration::TokenConfiguration; +use crate::data_contract::TokenContractPosition; +use crate::prelude::DataContract; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; +use std::collections::BTreeMap; + +mod v0; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; + +impl DataContract { + /// Validates the provided tokens to ensure they meet the requirements for data contracts. + /// + /// # Parameters + /// - `contract_id`: The identifier of the data contract these tokens belong to. + /// - `tokens`: A reference to a `BTreeMap` of token contract positions (`TokenContractPosition`) + /// mapped to their corresponding `TokenConfiguration` objects. + /// - `allow_offset_start`: If true, allows the tokens to start at a position other than 0. + /// This is useful for update transitions where new tokens continue from existing ones. + /// - `network`: The network type for validation rules. + /// - `platform_version`: A reference to the [`PlatformVersion`](crate::version::PlatformVersion) + /// object specifying the version of the platform and determining which validation method to use. + /// + /// # Returns + /// - `Ok(SimpleConsensusValidationResult)` if all the tokens pass validation: + /// - Token contract positions must be contiguous, i.e., no gaps between positions. + /// - Each token must meet its individual validation criteria. + /// - `Err(ProtocolError)` if: + /// - An unknown or unsupported platform version is provided. + /// - Validation of any token fails. + /// + /// # Behavior + /// - Delegates the actual validation logic to the appropriate versioned implementation + /// (`validate_tokens_v0`) based on the provided platform version. + /// - If an unknown platform version is encountered, a `ProtocolError::UnknownVersionMismatch` + /// is returned. + /// + /// # Errors + /// - Returns a `ProtocolError::UnknownVersionMismatch` if the platform version is not recognized. + /// - Returns validation errors for: + /// - Non-contiguous token contract positions (`NonContiguousContractTokenPositionsError`). + /// - Invalid token base supply (`InvalidTokenBaseSupplyError`). + /// - Missing destination identity when required (`NewTokensDestinationIdentityOptionRequiredError`). + pub fn validate_tokens( + contract_id: Identifier, + tokens: &BTreeMap, + allow_offset_start: bool, + network: dashcore::Network, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .contract_versions + .methods + .validate_tokens + { + 0 => Self::validate_tokens_v0( + contract_id, + tokens, + allow_offset_start, + network, + platform_version, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DataContract::validate_tokens".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/validate_tokens/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_tokens/v0/mod.rs new file mode 100644 index 0000000000..869272418c --- /dev/null +++ b/packages/rs-dpp/src/data_contract/methods/validate_tokens/v0/mod.rs @@ -0,0 +1,113 @@ +use crate::consensus::basic::data_contract::{ + InvalidTokenBaseSupplyError, NewTokensDestinationIdentityOptionRequiredError, + NonContiguousContractTokenPositionsError, +}; +use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use crate::data_contract::associated_token::token_configuration::TokenConfiguration; +use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use crate::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Accessors; +use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use crate::data_contract::{DataContract, TokenContractPosition}; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; +use std::collections::BTreeMap; + +impl DataContract { + #[inline(always)] + pub(super) fn validate_tokens_v0( + contract_id: Identifier, + tokens: &BTreeMap, + allow_offset_start: bool, + network: dashcore::Network, + platform_version: &PlatformVersion, + ) -> Result { + // Get the start position from the first token (allows non-zero start for updates) + let start_position = if allow_offset_start { + tokens.keys().next().copied().unwrap_or(0) + } else { + 0 + }; + + for (index, (token_contract_position, token_configuration)) in tokens.iter().enumerate() { + let expected_position = start_position + index as TokenContractPosition; + if expected_position != *token_contract_position { + return Ok(SimpleConsensusValidationResult::new_with_error( + NonContiguousContractTokenPositionsError::new( + expected_position, + *token_contract_position, + ) + .into(), + )); + } + + if token_configuration.base_supply() > i64::MAX as u64 { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenBaseSupplyError::new(token_configuration.base_supply()).into(), + )); + } + + let validation_result = token_configuration + .conventions() + .validate_localizations(platform_version)?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + if let Some(perpetual_distribution) = token_configuration + .distribution_rules() + .perpetual_distribution() + { + // we validate the interval (that it's more than one hour or over 100 blocks) + // also that if it is time based we are using minute intervals + let validation_result = perpetual_distribution + .distribution_type() + .validate_structure_interval(network, platform_version)?; + + if !validation_result.is_valid() { + return Ok(validation_result); + } + + // We use 0 as the start moment to show that we are starting now with no offset + let validation_result = perpetual_distribution + .distribution_type() + .function() + .validate(0, platform_version)?; + + if !validation_result.is_valid() { + return Ok(validation_result); + } + } + + if token_configuration + .distribution_rules() + .new_tokens_destination_identity() + .is_none() + && !token_configuration + .distribution_rules() + .minting_allow_choosing_destination() + && !(token_configuration + .distribution_rules() + .minting_allow_choosing_destination_rules() + .authorized_to_make_change_action_takers() + == &AuthorizedActionTakers::NoOne + && token_configuration + .distribution_rules() + .minting_allow_choosing_destination_rules() + .admin_action_takers() + == &AuthorizedActionTakers::NoOne) + { + return Ok(SimpleConsensusValidationResult::new_with_error( + NewTokensDestinationIdentityOptionRequiredError::new( + contract_id, + *token_contract_position, + ) + .into(), + )); + } + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs index 45aeef36e7..e564a978f5 100644 --- a/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs @@ -1,14 +1,11 @@ -use std::collections::HashSet; - use crate::block::block_info::BlockInfo; use crate::consensus::state::state_error::StateError; use crate::consensus::state::token::PreProgrammedDistributionTimestampInPastError; use crate::data_contract::accessors::v0::DataContractV0Getters; use crate::consensus::basic::data_contract::{ - DuplicateKeywordsError, IncompatibleDataContractSchemaError, InvalidDataContractVersionError, - InvalidDescriptionLengthError, InvalidKeywordCharacterError, InvalidKeywordLengthError, - TooManyKeywordsError, + IncompatibleDataContractSchemaError, InvalidDataContractVersionError, + InvalidDescriptionLengthError, }; use crate::consensus::state::data_contract::data_contract_update_action_not_allowed_error::DataContractUpdateActionNotAllowedError; use crate::consensus::state::data_contract::data_contract_update_permission_error::DataContractUpdatePermissionError; @@ -268,44 +265,14 @@ impl DataContract { } if self.keywords() != new_data_contract.keywords() { - // Validate there are no more than 50 keywords - if new_data_contract.keywords().len() > 50 { - return Ok(SimpleConsensusValidationResult::new_with_error( - TooManyKeywordsError::new(self.id(), new_data_contract.keywords().len() as u8) - .into(), - )); - } - - // Validate the keywords are all unique and between 3 and 50 characters - let mut seen_keywords = HashSet::new(); - for keyword in new_data_contract.keywords() { - // First check keyword length - if keyword.len() < 3 || keyword.len() > 50 { - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidKeywordLengthError::new(self.id(), keyword.to_string()).into(), - )); - } - - if !keyword - .chars() - .all(|c| !c.is_control() && !c.is_whitespace()) - { - // This would mean we have an invalid character - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidKeywordCharacterError::new( - new_data_contract.id(), - keyword.to_string(), - ) - .into(), - )); - } - - // Then check uniqueness - if !seen_keywords.insert(keyword) { - return Ok(SimpleConsensusValidationResult::new_with_error( - DuplicateKeywordsError::new(self.id(), keyword.to_string()).into(), - )); - } + let validation_result = DataContract::validate_keywords( + self.id(), + new_data_contract.keywords(), + true, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); } } diff --git a/packages/rs-dpp/src/data_contract/mod.rs b/packages/rs-dpp/src/data_contract/mod.rs index 1a98b2fc48..632c695c1e 100644 --- a/packages/rs-dpp/src/data_contract/mod.rs +++ b/packages/rs-dpp/src/data_contract/mod.rs @@ -46,6 +46,7 @@ pub mod change_control_rules; pub mod config; pub mod group; pub mod storage_requirements; +pub mod update_values; use crate::data_contract::serialized_version::{ DataContractInSerializationFormat, CONTRACT_DESERIALIZATION_LIMIT, @@ -65,7 +66,7 @@ use platform_versioning::PlatformVersioned; pub use serde_json::Value as JsonValue; type JsonSchema = JsonValue; -type DefinitionName = String; +pub(crate) type DefinitionName = String; pub type DocumentName = String; pub type TokenName = String; pub type GroupContractPosition = u16; diff --git a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs index bf3ee4fad3..dbea66cf44 100644 --- a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs +++ b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs @@ -23,8 +23,8 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt; -pub(in crate::data_contract) mod v0; -pub(in crate::data_contract) mod v1; +pub mod v0; +pub mod v1; pub mod property_names { pub const ID: &str = "id"; @@ -104,77 +104,73 @@ pub enum DataContractInSerializationFormat { V1(DataContractInSerializationFormatV1), } +/// Matches all variants of `DataContractInSerializationFormat`, binding the inner +/// value to `$v` and evaluating `$body` for each. +macro_rules! match_all { + ($self:expr, |$v:ident| $body:expr) => { + match $self { + DataContractInSerializationFormat::V0($v) => $body, + DataContractInSerializationFormat::V1($v) => $body, + } + }; +} + +/// Matches variants at or above the given version with field access, +/// returning `$default` for earlier variants. +macro_rules! match_since_version_or_default { + ($self:expr, 1, $default:expr, |$v:ident| $body:expr) => { + match $self { + DataContractInSerializationFormat::V0(_) => $default, + DataContractInSerializationFormat::V1($v) => $body, + } + }; +} + impl DataContractInSerializationFormat { /// Returns the unique identifier for the data contract. pub fn id(&self) -> Identifier { - match self { - DataContractInSerializationFormat::V0(v0) => v0.id, - DataContractInSerializationFormat::V1(v1) => v1.id, - } + match_all!(self, |v| v.id) } /// Returns the owner identifier for the data contract. pub fn owner_id(&self) -> Identifier { - match self { - DataContractInSerializationFormat::V0(v0) => v0.owner_id, - DataContractInSerializationFormat::V1(v1) => v1.owner_id, - } + match_all!(self, |v| v.owner_id) } - pub fn document_schemas(&self) -> &BTreeMap { + pub fn document_schemas(&self) -> Option<&BTreeMap> { match self { - DataContractInSerializationFormat::V0(v0) => &v0.document_schemas, - DataContractInSerializationFormat::V1(v1) => &v1.document_schemas, + DataContractInSerializationFormat::V0(v0) => Some(&v0.document_schemas), + DataContractInSerializationFormat::V1(v1) => Some(&v1.document_schemas), } } pub fn schema_defs(&self) -> Option<&BTreeMap> { - match self { - DataContractInSerializationFormat::V0(v0) => v0.schema_defs.as_ref(), - DataContractInSerializationFormat::V1(v1) => v1.schema_defs.as_ref(), - } + match_all!(self, |v| v.schema_defs.as_ref()) } pub fn version(&self) -> u32 { - match self { - DataContractInSerializationFormat::V0(v0) => v0.version, - DataContractInSerializationFormat::V1(v1) => v1.version, - } + match_all!(self, |v| v.version) } /// Returns the config for the data contract. pub fn config(&self) -> &DataContractConfig { - match self { - DataContractInSerializationFormat::V0(v0) => &v0.config, - DataContractInSerializationFormat::V1(v1) => &v1.config, - } + match_all!(self, |v| &v.config) } pub fn groups(&self) -> &BTreeMap { - match self { - DataContractInSerializationFormat::V0(_) => &EMPTY_GROUPS, - DataContractInSerializationFormat::V1(v1) => &v1.groups, - } + match_since_version_or_default!(self, 1, &EMPTY_GROUPS, |v| &v.groups) } + pub fn tokens(&self) -> &BTreeMap { - match self { - DataContractInSerializationFormat::V0(_) => &EMPTY_TOKENS, - DataContractInSerializationFormat::V1(v1) => &v1.tokens, - } + match_since_version_or_default!(self, 1, &EMPTY_TOKENS, |v| &v.tokens) } pub fn keywords(&self) -> &Vec { - match self { - DataContractInSerializationFormat::V0(_) => &EMPTY_KEYWORDS, - DataContractInSerializationFormat::V1(v1) => &v1.keywords, - } + match_since_version_or_default!(self, 1, &EMPTY_KEYWORDS, |v| &v.keywords) } pub fn description(&self) -> &Option { - match self { - DataContractInSerializationFormat::V0(_) => &None, - DataContractInSerializationFormat::V1(v1) => &v1.description, - } + match_since_version_or_default!(self, 1, &None, |v| &v.description) } /// Compares `self` to another `DataContractInSerializationFormat` instance diff --git a/packages/rs-dpp/src/data_contract/update_values/mod.rs b/packages/rs-dpp/src/data_contract/update_values/mod.rs new file mode 100644 index 0000000000..332951a9ad --- /dev/null +++ b/packages/rs-dpp/src/data_contract/update_values/mod.rs @@ -0,0 +1,55 @@ +use std::collections::BTreeMap; + +use crate::data_contract::associated_token::token_configuration::TokenConfiguration; +use crate::data_contract::group::Group; +use crate::data_contract::{GroupContractPosition, TokenContractPosition}; +use platform_value::Value; + +/// Represents the values to apply to a data contract during an update. +#[derive(Debug)] +pub struct DataContractUpdateValues<'a> { + /// The new revision number for the contract. + pub revision: u32, + /// Updated schema definitions (must be compatible with existing ones). + pub updated_schema_defs: &'a BTreeMap, + /// New schema definitions to add. + pub new_schema_defs: &'a BTreeMap, + /// Updates to existing document schemas (keyed by document type name). + pub updated_document_schemas: &'a BTreeMap, + /// New document schemas to add (keyed by document type name). + pub new_document_schemas: &'a BTreeMap, + /// New groups to add to the contract. + pub new_groups: &'a BTreeMap, + /// New tokens to add to the contract. + pub new_tokens: &'a BTreeMap, + /// Keywords to remove from the contract. + pub remove_keywords: &'a [String], + /// Keywords to add to the contract. + pub add_keywords: &'a [String], + /// Update to the contract description. None means no change, Some(None) clears it, + /// Some(Some(desc)) sets a new description. + pub update_description: &'a Option>, +} + +#[cfg(feature = "state-transitions")] +impl<'a> + From<&'a crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1> + for DataContractUpdateValues<'a> +{ + fn from( + value: &'a crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1, + ) -> Self { + DataContractUpdateValues { + revision: value.revision, + updated_schema_defs: &value.updated_schema_defs, + new_schema_defs: &value.new_schema_defs, + updated_document_schemas: &value.updated_document_schemas, + new_document_schemas: &value.new_document_schemas, + new_groups: &value.new_groups, + new_tokens: &value.new_tokens, + remove_keywords: &value.remove_keywords, + add_keywords: &value.add_keywords, + update_description: &value.update_description, + } + } +} diff --git a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs index 08f6a5aa87..95f38c26c6 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs @@ -87,8 +87,9 @@ use crate::consensus::basic::{ use crate::consensus::ConsensusError; use super::data_contract::{ - DuplicateKeywordsError, InvalidDescriptionLengthError, InvalidKeywordLengthError, - TooManyKeywordsError, + DataContractUpdateTransitionConflictingKeywordError, + DataContractUpdateTransitionOverlappingFieldsError, DuplicateKeywordsError, + InvalidDescriptionLengthError, InvalidKeywordLengthError, TooManyKeywordsError, }; use crate::consensus::basic::group::GroupActionNotAllowedOnTransitionError; use crate::consensus::basic::overflow_error::OverflowError; @@ -653,6 +654,16 @@ pub enum BasicError { #[error(transparent)] OutputAddressAlsoInputError(OutputAddressAlsoInputError), + + #[error(transparent)] + DataContractUpdateTransitionOverlappingFieldsError( + DataContractUpdateTransitionOverlappingFieldsError, + ), + + #[error(transparent)] + DataContractUpdateTransitionConflictingKeywordError( + DataContractUpdateTransitionConflictingKeywordError, + ), } impl From for ConsensusError { diff --git a/packages/rs-dpp/src/errors/consensus/basic/data_contract/data_contract_update_transition_conflicting_keyword_error.rs b/packages/rs-dpp/src/errors/consensus/basic/data_contract/data_contract_update_transition_conflicting_keyword_error.rs new file mode 100644 index 0000000000..dd7d72accb --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/basic/data_contract/data_contract_update_transition_conflicting_keyword_error.rs @@ -0,0 +1,45 @@ +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::errors::ProtocolError; +use bincode::{Decode, Encode}; +use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; +use platform_value::Identifier; +use thiserror::Error; + +#[derive( + Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize, +)] +#[error("Data contract update transition {data_contract_id} has conflicting keyword '{keyword}': cannot add and remove the same keyword.")] +#[platform_serialize(unversioned)] +pub struct DataContractUpdateTransitionConflictingKeywordError { + /* + + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + + */ + data_contract_id: Identifier, + keyword: String, +} + +impl DataContractUpdateTransitionConflictingKeywordError { + pub fn new(data_contract_id: Identifier, keyword: String) -> Self { + Self { + data_contract_id, + keyword, + } + } + + pub fn data_contract_id(&self) -> &Identifier { + &self.data_contract_id + } + + pub fn keyword(&self) -> &str { + &self.keyword + } +} + +impl From for ConsensusError { + fn from(err: DataContractUpdateTransitionConflictingKeywordError) -> Self { + Self::BasicError(BasicError::DataContractUpdateTransitionConflictingKeywordError(err)) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/basic/data_contract/data_contract_update_transition_overlapping_fields_error.rs b/packages/rs-dpp/src/errors/consensus/basic/data_contract/data_contract_update_transition_overlapping_fields_error.rs new file mode 100644 index 0000000000..ff7b8b8fe5 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/basic/data_contract/data_contract_update_transition_overlapping_fields_error.rs @@ -0,0 +1,51 @@ +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::errors::ProtocolError; +use bincode::{Decode, Encode}; +use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; +use platform_value::Identifier; +use thiserror::Error; + +#[derive( + Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize, +)] +#[error("Data contract update transition {data_contract_id} has overlapping {field_type}: '{overlapping_key}' appears in both updated and new fields.")] +#[platform_serialize(unversioned)] +pub struct DataContractUpdateTransitionOverlappingFieldsError { + /* + + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + + */ + data_contract_id: Identifier, + field_type: String, + overlapping_key: String, +} + +impl DataContractUpdateTransitionOverlappingFieldsError { + pub fn new(data_contract_id: Identifier, field_type: String, overlapping_key: String) -> Self { + Self { + data_contract_id, + field_type, + overlapping_key, + } + } + + pub fn data_contract_id(&self) -> &Identifier { + &self.data_contract_id + } + + pub fn field_type(&self) -> &str { + &self.field_type + } + + pub fn overlapping_key(&self) -> &str { + &self.overlapping_key + } +} + +impl From for ConsensusError { + fn from(err: DataContractUpdateTransitionOverlappingFieldsError) -> Self { + Self::BasicError(BasicError::DataContractUpdateTransitionOverlappingFieldsError(err)) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/basic/data_contract/mod.rs b/packages/rs-dpp/src/errors/consensus/basic/data_contract/mod.rs index a43ea4793a..f47b1ae71b 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/data_contract/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/data_contract/mod.rs @@ -6,6 +6,8 @@ mod data_contract_invalid_index_definition_update_error; pub mod data_contract_max_depth_exceed_error; mod data_contract_token_configuration_update_error; mod data_contract_unique_indices_changed_error; +mod data_contract_update_transition_conflicting_keyword_error; +mod data_contract_update_transition_overlapping_fields_error; mod document_types_are_missing_error; mod duplicate_index_error; mod duplicate_index_name_error; @@ -64,6 +66,8 @@ pub use data_contract_immutable_properties_update_error::*; pub use data_contract_invalid_index_definition_update_error::*; pub use data_contract_token_configuration_update_error::*; pub use data_contract_unique_indices_changed_error::*; +pub use data_contract_update_transition_conflicting_keyword_error::*; +pub use data_contract_update_transition_overlapping_fields_error::*; pub use document_types_are_missing_error::*; pub use duplicate_index_error::*; pub use duplicate_index_name_error::*; diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index b25a99a690..2db96c9131 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -231,6 +231,8 @@ impl ErrorWithCode for BasicError { Self::InputsNotLessThanOutputsError(_) => 10815, Self::OutputAddressAlsoInputError(_) => 10816, Self::InvalidRemainderOutputCountError(_) => 10817, + Self::DataContractUpdateTransitionOverlappingFieldsError(_) => 10818, + Self::DataContractUpdateTransitionConflictingKeywordError(_) => 10819, } } } diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 8f41b601de..39cb081d9d 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -91,11 +91,9 @@ use crate::state_transition::batch_transition::batched_transition::BatchedTransi #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionSignable}; -use crate::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; use crate::state_transition::data_contract_create_transition::{ DataContractCreateTransition, DataContractCreateTransitionSignable, }; -use crate::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use crate::state_transition::data_contract_update_transition::{ DataContractUpdateTransition, DataContractUpdateTransitionSignable, }; @@ -426,15 +424,21 @@ impl StateTransition { pub fn active_version_range(&self) -> RangeInclusive { match self { StateTransition::DataContractCreate(data_contract_create_transition) => { - match data_contract_create_transition.data_contract() { - DataContractInSerializationFormat::V0(_) => ALL_VERSIONS, - DataContractInSerializationFormat::V1(_) => 9..=LATEST_VERSION, + match data_contract_create_transition { + DataContractCreateTransition::V0(v0) => match &v0.data_contract { + DataContractInSerializationFormat::V0(_) => ALL_VERSIONS, + DataContractInSerializationFormat::V1(_) => 9..=LATEST_VERSION, + }, + DataContractCreateTransition::V1(_) => 12..=LATEST_VERSION, } } StateTransition::DataContractUpdate(data_contract_update_transition) => { - match data_contract_update_transition.data_contract() { - DataContractInSerializationFormat::V0(_) => ALL_VERSIONS, - DataContractInSerializationFormat::V1(_) => 9..=LATEST_VERSION, + match data_contract_update_transition { + DataContractUpdateTransition::V0(v0) => match &v0.data_contract { + DataContractInSerializationFormat::V0(_) => ALL_VERSIONS, + DataContractInSerializationFormat::V1(_) => 9..=LATEST_VERSION, + }, + DataContractUpdateTransition::V1(_) => 12..=LATEST_VERSION, } } StateTransition::Batch(batch_transition) => match batch_transition { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/accessors/mod.rs index 301bb1743e..acafad7d73 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/accessors/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/accessors/mod.rs @@ -10,12 +10,16 @@ impl DataContractCreateTransitionAccessorsV0 for DataContractCreateTransition { fn data_contract(&self) -> &DataContractInSerializationFormat { match self { DataContractCreateTransition::V0(transition) => &transition.data_contract, + DataContractCreateTransition::V1(_) => { + panic!("data_contract() accessor is not available for DataContractCreateTransitionV1 - use individual field accessors instead") + } } } fn identity_nonce(&self) -> IdentityNonce { match self { DataContractCreateTransition::V0(transition) => transition.identity_nonce, + DataContractCreateTransition::V1(transition) => transition.identity_nonce, } } @@ -24,6 +28,9 @@ impl DataContractCreateTransitionAccessorsV0 for DataContractCreateTransition { DataContractCreateTransition::V0(transition) => { transition.data_contract = data_contract; } + DataContractCreateTransition::V1(_) => { + panic!("set_data_contract() is not available for DataContractCreateTransitionV1 - use individual field setters instead") + } } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/identity_signed.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/identity_signed.rs index 4b2704c07d..f633d7ab00 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/identity_signed.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/identity_signed.rs @@ -6,6 +6,7 @@ impl StateTransitionIdentitySigned for DataContractCreateTransition { fn signature_public_key_id(&self) -> KeyID { match self { DataContractCreateTransition::V0(transition) => transition.signature_public_key_id(), + DataContractCreateTransition::V1(transition) => transition.signature_public_key_id(), } } @@ -14,6 +15,9 @@ impl StateTransitionIdentitySigned for DataContractCreateTransition { DataContractCreateTransition::V0(transition) => { transition.set_signature_public_key_id(key_id) } + DataContractCreateTransition::V1(transition) => { + transition.set_signature_public_key_id(key_id) + } } } @@ -22,6 +26,9 @@ impl StateTransitionIdentitySigned for DataContractCreateTransition { DataContractCreateTransition::V0(transition) => { transition.security_level_requirement(purpose) } + DataContractCreateTransition::V1(transition) => { + transition.security_level_requirement(purpose) + } } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/json_conversion.rs index b7b51af827..f8588ee372 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/json_conversion.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/json_conversion.rs @@ -22,6 +22,15 @@ impl StateTransitionJsonConvert<'_> for DataContractCreateTransition { ); Ok(value) } + DataContractCreateTransition::V1(transition) => { + let mut value = transition.to_json(options)?; + let map_value = value.as_object_mut().expect("expected an object"); + map_value.insert( + STATE_TRANSITION_PROTOCOL_VERSION.to_string(), + JsonValue::Number(Number::from(1)), + ); + Ok(value) + } } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/mod.rs index 18480ca03c..8168dfb364 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/mod.rs @@ -1,3 +1,4 @@ +pub(in crate::state_transition::state_transitions::contract) mod registration_cost; pub mod v0; pub use v0::*; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/registration_cost/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/registration_cost/mod.rs new file mode 100644 index 0000000000..6d7aaad606 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/registration_cost/mod.rs @@ -0,0 +1,76 @@ +mod v1; + +pub(in crate::state_transition::state_transitions::contract) use v1::registration_cost_from_fields; + +use crate::fee::Credits; +use crate::state_transition::data_contract_create_transition::DataContractCreateTransition; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +impl DataContractCreateTransition { + /// Returns the registration cost of the data contract based on the current platform version + /// and the number of associated search keywords. + /// + /// This method dispatches to a version-specific implementation based on the + /// platform version configuration. If the version is unrecognized, it returns a version mismatch error. + /// + /// # Arguments + /// - `platform_version`: A reference to the platform version, used to determine which + /// registration cost algorithm to apply. + /// + /// # Returns + /// - `Ok(u64)`: The total registration cost in credits for this contract. + /// - `Err(ProtocolError)`: If the platform version is unrecognized or if the fee computation overflows. + /// + /// # Version Behavior + /// - Version 0: Always returns `0` (used before protocol version 9, ie before 2.0, where registration cost was not charged). + /// - Version 1: Uses a detailed cost model based on document types, indexes, tokens, and keyword count. + pub fn registration_cost( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .contract_versions + .methods + .registration_cost + { + 0 => Ok(0), // Before 2.0 it's just 0 (There was some validation cost) + 1 => { + let base_fee = platform_version + .fee_version + .data_contract_registration + .base_contract_registration_fee; + + match self { + DataContractCreateTransition::V0(v0) => { + let document_schemas = v0 + .data_contract + .document_schemas() + .cloned() + .unwrap_or_default(); + Ok(registration_cost_from_fields( + &document_schemas, + v0.data_contract.tokens(), + v0.data_contract.keywords().len(), + base_fee, + platform_version, + )) + } + DataContractCreateTransition::V1(v1) => Ok(registration_cost_from_fields( + &v1.document_schemas, + &v1.tokens, + v1.keywords.len(), + base_fee, + platform_version, + )), + } + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DataContractCreateTransition::registration_cost".to_string(), + known_versions: vec![0, 1], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/registration_cost/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/registration_cost/v1/mod.rs new file mode 100644 index 0000000000..66d35dfe53 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/methods/registration_cost/v1/mod.rs @@ -0,0 +1,97 @@ +use std::collections::BTreeMap; + +use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use crate::data_contract::associated_token::token_configuration::TokenConfiguration; +use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use crate::data_contract::document_type::Index; +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use platform_value::Value; +use platform_version::version::PlatformVersion; + +/// Computes the registration cost based on document schemas, indexes, token configurations, +/// and keyword count. +/// +/// # Parameters +/// - `document_schemas`: A map of document names to their JSON schema values. +/// - `tokens`: A map of token positions to their configurations. +/// - `keywords_count`: The number of keywords to register. +/// - `base_fee`: The base fee to start with (use base_contract_registration_fee for create, 0 for update). +/// - `platform_version`: A reference to the current platform version providing fee parameters. +/// +/// # Returns +/// - `Credits`: The total registration cost in credits. +/// +/// # Fee Components +/// - Base fee (passed as parameter). +/// - Per document type registration fee. +/// - Per index registration fee (contested, unique, and non-unique). +/// - Token registration fee per token. +/// - Additional fees for tokens using perpetual or pre-programmed distribution. +/// - Search keyword fees (`keywords_count * search_keyword_fee`). +pub(in crate::state_transition::state_transitions::contract) fn registration_cost_from_fields( + document_schemas: &BTreeMap, + tokens: &BTreeMap, + keywords_count: usize, + base_fee: Credits, + platform_version: &PlatformVersion, +) -> Credits { + let fee_version = &platform_version.fee_version.data_contract_registration; + let mut cost = base_fee; + + // Calculate cost for document schemas + for document_type_schema in document_schemas.values() { + cost = cost.saturating_add(fee_version.document_type_registration_fee); + + // Parse indexes from the schema if present + if let Ok(schema_map) = document_type_schema.to_map() { + if let Ok(Some(index_values)) = Value::inner_optional_array_slice_value( + schema_map, + crate::data_contract::document_type::property_names::INDICES, + ) { + for index_value in index_values { + if let Ok(index_value_map) = index_value.to_map() { + if let Ok(index) = Index::try_from(index_value_map.as_slice()) { + let base_index_fee = if index.contested_index.is_some() { + fee_version.document_type_base_contested_index_registration_fee + } else if index.unique { + fee_version.document_type_base_unique_index_registration_fee + } else { + fee_version.document_type_base_non_unique_index_registration_fee + }; + cost = cost.saturating_add(base_index_fee); + } + } + } + } + } + } + + // Calculate cost for tokens + for token_config in tokens.values() { + cost = cost.saturating_add(fee_version.token_registration_fee); + + if token_config + .distribution_rules() + .perpetual_distribution() + .is_some() + { + cost = cost.saturating_add(fee_version.token_uses_perpetual_distribution_fee); + } + + if token_config + .distribution_rules() + .pre_programmed_distribution() + .is_some() + { + cost = cost.saturating_add(fee_version.token_uses_pre_programmed_distribution_fee); + } + } + + // Calculate cost for keywords + let keyword_cost = fee_version + .search_keyword_fee + .saturating_mul(keywords_count as u64); + + cost.saturating_add(keyword_cost) +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/mod.rs index 6d8953ad27..4e45c3c5b8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/mod.rs @@ -7,6 +7,7 @@ pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_like; mod v0; +mod v1; #[cfg(feature = "state-transition-value-conversion")] mod value_conversion; mod version; @@ -30,6 +31,7 @@ use serde::{Deserialize, Serialize}; use crate::data_contract::created_data_contract::CreatedDataContract; use crate::identity::state_transition::OptionallyAssetLockProved; pub use v0::*; +pub use v1::*; pub type DataContractCreateTransitionLatest = DataContractCreateTransitionV0; @@ -57,6 +59,8 @@ pub type DataContractCreateTransitionLatest = DataContractCreateTransitionV0; pub enum DataContractCreateTransition { #[cfg_attr(feature = "state-transition-serde-conversion", serde(rename = "0"))] V0(DataContractCreateTransitionV0), + #[cfg_attr(feature = "state-transition-serde-conversion", serde(rename = "1"))] + V1(DataContractCreateTransitionV1), } impl TryFromPlatformVersioned for DataContractCreateTransition { @@ -77,9 +81,14 @@ impl TryFromPlatformVersioned for DataContractCreateTransit value.try_into_platform_versioned(platform_version)?; Ok(data_contract_create_transition.into()) } + 1 => { + let data_contract_create_transition: DataContractCreateTransitionV1 = + value.try_into_platform_versioned(platform_version)?; + Ok(data_contract_create_transition.into()) + } version => Err(ProtocolError::UnknownVersionMismatch { method: "DataContractCreateTransition::try_from(CreatedDataContract)".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, }), } @@ -117,9 +126,14 @@ impl TryFromPlatformVersioned for DataContractCreateTransition { value.try_into_platform_versioned(platform_version)?; Ok(data_contract_create_transition.into()) } + 1 => { + let data_contract_create_transition: DataContractCreateTransitionV1 = + value.try_into_platform_versioned(platform_version)?; + Ok(data_contract_create_transition.into()) + } version => Err(ProtocolError::UnknownVersionMismatch { method: "DataContractCreateTransition::try_from(DataContract)".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, }), } @@ -144,6 +158,7 @@ impl DataContractCreateTransition { pub fn state_transition_version(&self) -> u16 { match self { DataContractCreateTransition::V0(_) => 0, + DataContractCreateTransition::V1(_) => 1, } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/state_transition_estimated_fee_validation.rs index 05810bbbec..bc0dafb84f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/state_transition_estimated_fee_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/state_transition_estimated_fee_validation.rs @@ -1,6 +1,5 @@ use crate::consensus::state::identity::IdentityInsufficientBalanceError; use crate::fee::Credits; -use crate::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; use crate::state_transition::data_contract_create_transition::DataContractCreateTransition; use crate::state_transition::{ StateTransitionEstimatedFeeValidation, StateTransitionIdentityEstimatedFeeValidation, @@ -20,7 +19,7 @@ impl StateTransitionEstimatedFeeValidation for DataContractCreateTransition { .state_transition_min_fees .contract_create; - let registration_cost = self.data_contract().registration_cost(platform_version)?; + let registration_cost = self.registration_cost(platform_version)?; Ok(base_fee.saturating_add(registration_cost)) } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/state_transition_like.rs index 8645f42dfa..8c29fffb77 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/state_transition_like.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/state_transition_like.rs @@ -11,18 +11,26 @@ impl StateTransitionLike for DataContractCreateTransition { fn modified_data_ids(&self) -> Vec { match self { DataContractCreateTransition::V0(transition) => transition.modified_data_ids(), + DataContractCreateTransition::V1(transition) => transition.modified_data_ids(), } } fn state_transition_protocol_version(&self) -> FeatureVersion { match self { - DataContractCreateTransition::V0(_) => 0, + DataContractCreateTransition::V0(transition) => { + transition.state_transition_protocol_version() + } + DataContractCreateTransition::V1(transition) => { + transition.state_transition_protocol_version() + } } } + /// returns the type of State Transition fn state_transition_type(&self) -> StateTransitionType { match self { DataContractCreateTransition::V0(transition) => transition.state_transition_type(), + DataContractCreateTransition::V1(transition) => transition.state_transition_type(), } } @@ -30,20 +38,26 @@ impl StateTransitionLike for DataContractCreateTransition { fn user_fee_increase(&self) -> UserFeeIncrease { match self { DataContractCreateTransition::V0(transition) => transition.user_fee_increase(), + DataContractCreateTransition::V1(transition) => transition.user_fee_increase(), } } + /// set a fee multiplier fn set_user_fee_increase(&mut self, user_fee_increase: UserFeeIncrease) { match self { DataContractCreateTransition::V0(transition) => { transition.set_user_fee_increase(user_fee_increase) } + DataContractCreateTransition::V1(transition) => { + transition.set_user_fee_increase(user_fee_increase) + } } } fn unique_identifiers(&self) -> Vec { match self { DataContractCreateTransition::V0(transition) => transition.unique_identifiers(), + DataContractCreateTransition::V1(transition) => transition.unique_identifiers(), } } } @@ -53,12 +67,15 @@ impl StateTransitionSingleSigned for DataContractCreateTransition { fn signature(&self) -> &BinaryData { match self { DataContractCreateTransition::V0(transition) => transition.signature(), + DataContractCreateTransition::V1(transition) => transition.signature(), } } + /// set a new signature fn set_signature(&mut self, signature: BinaryData) { match self { DataContractCreateTransition::V0(transition) => transition.set_signature(signature), + DataContractCreateTransition::V1(transition) => transition.set_signature(signature), } } @@ -67,6 +84,9 @@ impl StateTransitionSingleSigned for DataContractCreateTransition { DataContractCreateTransition::V0(transition) => { transition.set_signature_bytes(signature) } + DataContractCreateTransition::V1(transition) => { + transition.set_signature_bytes(signature) + } } } } @@ -75,6 +95,7 @@ impl StateTransitionOwned for DataContractCreateTransition { fn owner_id(&self) -> Identifier { match self { DataContractCreateTransition::V0(transition) => transition.owner_id(), + DataContractCreateTransition::V1(transition) => transition.owner_id(), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/mod.rs index 054004a6dc..6266109cca 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/mod.rs @@ -20,11 +20,10 @@ use crate::data_contract::created_data_contract::CreatedDataContract; use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::prelude::{IdentityNonce, UserFeeIncrease}; use crate::state_transition::data_contract_create_transition::DataContractCreateTransition; -use bincode::{Decode, Encode}; -use platform_version::{TryFromPlatformVersioned, TryIntoPlatformVersioned}; - use crate::state_transition::StateTransition; use crate::version::PlatformVersion; +use bincode::{Decode, Encode}; +use platform_version::{TryFromPlatformVersioned, TryIntoPlatformVersioned}; ///DataContractCreateTransitionV0 has the same encoding structure diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/identity_signed.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/identity_signed.rs new file mode 100644 index 0000000000..dd3f3cfbd9 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/identity_signed.rs @@ -0,0 +1,18 @@ +use crate::identity::SecurityLevel::{CRITICAL, HIGH}; +use crate::identity::{KeyID, Purpose, SecurityLevel}; +use crate::state_transition::data_contract_create_transition::DataContractCreateTransitionV1; +use crate::state_transition::StateTransitionIdentitySigned; + +impl StateTransitionIdentitySigned for DataContractCreateTransitionV1 { + fn signature_public_key_id(&self) -> KeyID { + self.signature_public_key_id + } + + fn set_signature_public_key_id(&mut self, key_id: crate::identity::KeyID) { + self.signature_public_key_id = key_id + } + + fn security_level_requirement(&self, _purpose: Purpose) -> Vec { + vec![CRITICAL, HIGH] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/json_conversion.rs new file mode 100644 index 0000000000..52a2667aef --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/json_conversion.rs @@ -0,0 +1,4 @@ +use crate::state_transition::data_contract_create_transition::DataContractCreateTransitionV1; +use crate::state_transition::StateTransitionJsonConvert; + +impl StateTransitionJsonConvert<'_> for DataContractCreateTransitionV1 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/mod.rs new file mode 100644 index 0000000000..323a31ae1e --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/mod.rs @@ -0,0 +1,181 @@ +mod identity_signed; +#[cfg(feature = "state-transition-json-conversion")] +mod json_conversion; +mod state_transition_like; +mod types; +pub(crate) mod v0_methods; +#[cfg(feature = "state-transition-value-conversion")] +mod value_conversion; +mod version; + +use platform_serialization_derive::PlatformSignable; +use std::collections::BTreeMap; + +use platform_value::{BinaryData, Identifier, Value}; +#[cfg(feature = "state-transition-serde-conversion")] +use serde::{Deserialize, Serialize}; + +use crate::data_contract::accessors::v0::DataContractV0Getters; +use crate::data_contract::accessors::v1::DataContractV1Getters; +use crate::data_contract::schema::DataContractSchemaMethodsV0; +use crate::{data_contract::DataContract, identity::KeyID, ProtocolError}; + +use crate::data_contract::associated_token::token_configuration::TokenConfiguration; +use crate::data_contract::config::DataContractConfig; +use crate::data_contract::created_data_contract::CreatedDataContract; +use crate::data_contract::group::Group; +use crate::data_contract::{ + DefinitionName, DocumentName, GroupContractPosition, TokenContractPosition, +}; +use crate::prelude::{IdentityNonce, UserFeeIncrease}; +use crate::state_transition::data_contract_create_transition::DataContractCreateTransition; +use crate::state_transition::StateTransition; +use crate::version::PlatformVersion; +use bincode::{Decode, Encode}; +use platform_version::TryFromPlatformVersioned; + +/// DataContractCreateTransitionV1 stores the contract fields directly +/// rather than embedding a serialization format. The contract `id` is +/// derived from `owner_id + identity_nonce` and is not stored. + +#[derive(Debug, Clone, Encode, Decode, PartialEq, PlatformSignable)] +#[cfg_attr( + feature = "state-transition-serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct DataContractCreateTransitionV1 { + /// The contract system version specifying which system features should be + /// activated for this data contract. + pub contract_system_version: u16, + + /// The identifier of the contract owner (the identity creating the contract). + pub owner_id: Identifier, + + /// Internal configuration for the contract. + pub config: DataContractConfig, + + /// Shared subschemas to reuse across documents as $defs object. + pub schema_defs: Option>, + + /// Document JSON Schemas per type. + pub document_schemas: BTreeMap, + + /// Groups that allow for specific multiparty actions on the contract. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub groups: BTreeMap, + + /// The tokens on the contract. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub tokens: BTreeMap, + + /// The contract's keywords for searching. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub keywords: Vec, + + /// The contract's description. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub description: Option, + + /// The identity nonce used to derive the contract id. + pub identity_nonce: IdentityNonce, + + /// User fee increase for priority processing. + pub user_fee_increase: UserFeeIncrease, + + /// The public key id used to sign. + #[platform_signable(exclude_from_sig_hash)] + pub signature_public_key_id: KeyID, + + /// The signature. + #[platform_signable(exclude_from_sig_hash)] + pub signature: BinaryData, +} + +impl DataContractCreateTransitionV1 { + /// Computes the data contract id from owner_id and identity_nonce. + pub fn data_contract_id(&self) -> Identifier { + DataContract::generate_data_contract_id_v0(self.owner_id, self.identity_nonce) + } +} + +impl From for StateTransition { + fn from(value: DataContractCreateTransitionV1) -> Self { + let transition: DataContractCreateTransition = value.into(); + transition.into() + } +} + +impl From<&DataContractCreateTransitionV1> for StateTransition { + fn from(value: &DataContractCreateTransitionV1) -> Self { + let transition: DataContractCreateTransition = value.clone().into(); + transition.into() + } +} + +impl TryFromPlatformVersioned for DataContractCreateTransitionV1 { + type Error = ProtocolError; + + fn try_from_platform_versioned( + data_contract: DataContract, + _platform_version: &PlatformVersion, + ) -> Result { + let contract_system_version = match &data_contract { + DataContract::V0(_) => 0, + DataContract::V1(_) => 1, + }; + Ok(DataContractCreateTransitionV1 { + contract_system_version, + owner_id: data_contract.owner_id(), + config: *data_contract.config(), + schema_defs: data_contract.schema_defs().cloned(), + document_schemas: data_contract + .document_schemas() + .into_iter() + .map(|(k, v)| (k, v.clone())) + .collect(), + groups: data_contract.groups().clone(), + tokens: data_contract.tokens().clone(), + keywords: data_contract.keywords().clone(), + description: data_contract.description().cloned(), + identity_nonce: Default::default(), + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }) + } +} + +impl TryFromPlatformVersioned for DataContractCreateTransitionV1 { + type Error = ProtocolError; + + fn try_from_platform_versioned( + value: CreatedDataContract, + _platform_version: &PlatformVersion, + ) -> Result { + let (data_contract, identity_nonce) = value.data_contract_and_identity_nonce(); + let contract_system_version = match &data_contract { + DataContract::V0(_) => 0, + DataContract::V1(_) => 1, + }; + Ok(DataContractCreateTransitionV1 { + contract_system_version, + owner_id: data_contract.owner_id(), + config: *data_contract.config(), + schema_defs: data_contract.schema_defs().cloned(), + document_schemas: data_contract + .document_schemas() + .into_iter() + .map(|(k, v)| (k, v.clone())) + .collect(), + groups: data_contract.groups().clone(), + tokens: data_contract.tokens().clone(), + keywords: data_contract.keywords().clone(), + description: data_contract.description().cloned(), + identity_nonce, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/state_transition_like.rs new file mode 100644 index 0000000000..ee2aa8e39d --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/state_transition_like.rs @@ -0,0 +1,63 @@ +use platform_value::BinaryData; + +use crate::prelude::UserFeeIncrease; +use crate::{ + prelude::Identifier, + state_transition::{StateTransitionLike, StateTransitionOwned, StateTransitionType}, +}; + +use crate::state_transition::data_contract_create_transition::DataContractCreateTransitionV1; +use crate::state_transition::StateTransitionSingleSigned; +use crate::state_transition::StateTransitionType::DataContractCreate; +use crate::version::FeatureVersion; + +impl StateTransitionLike for DataContractCreateTransitionV1 { + /// Returns ID of the created contract (derived from owner_id + identity_nonce) + fn modified_data_ids(&self) -> Vec { + vec![self.data_contract_id()] + } + + fn state_transition_protocol_version(&self) -> FeatureVersion { + 1 + } + + /// returns the type of State Transition + fn state_transition_type(&self) -> StateTransitionType { + DataContractCreate + } + + fn unique_identifiers(&self) -> Vec { + vec![format!("dcc-{}-{}", self.owner_id, self.data_contract_id())] + } + + fn user_fee_increase(&self) -> UserFeeIncrease { + self.user_fee_increase + } + + fn set_user_fee_increase(&mut self, user_fee_increase: UserFeeIncrease) { + self.user_fee_increase = user_fee_increase + } +} + +impl StateTransitionSingleSigned for DataContractCreateTransitionV1 { + /// returns the signature as a byte-array + fn signature(&self) -> &BinaryData { + &self.signature + } + + /// set a new signature + fn set_signature(&mut self, signature: BinaryData) { + self.signature = signature + } + + fn set_signature_bytes(&mut self, signature: Vec) { + self.signature = BinaryData::new(signature) + } +} + +impl StateTransitionOwned for DataContractCreateTransitionV1 { + /// Get owner ID + fn owner_id(&self) -> Identifier { + self.owner_id + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/types.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/types.rs new file mode 100644 index 0000000000..1a8617d2f0 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/types.rs @@ -0,0 +1,19 @@ +use crate::state_transition::data_contract_create_transition::DataContractCreateTransitionV1; +use crate::state_transition::state_transitions::common_fields::property_names::{ + SIGNATURE, SIGNATURE_PUBLIC_KEY_ID, +}; +use crate::state_transition::StateTransitionFieldTypes; + +impl StateTransitionFieldTypes for DataContractCreateTransitionV1 { + fn signature_property_paths() -> Vec<&'static str> { + vec![SIGNATURE, SIGNATURE_PUBLIC_KEY_ID] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![SIGNATURE] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/v0_methods.rs new file mode 100644 index 0000000000..028331afec --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/v0_methods.rs @@ -0,0 +1,111 @@ +use crate::state_transition::data_contract_create_transition::DataContractCreateTransitionV1; + +use crate::{data_contract::DataContract, identity::KeyID, NonConsensusError, ProtocolError}; + +use crate::serialization::Signable; + +use crate::consensus::signature::{InvalidSignaturePublicKeySecurityLevelError, SignatureError}; +use crate::data_contract::accessors::v0::DataContractV0Getters; +use crate::data_contract::accessors::v1::DataContractV1Getters; +use crate::data_contract::schema::DataContractSchemaMethodsV0; +use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use crate::identity::signer::Signer; +use crate::identity::{IdentityPublicKey, PartialIdentity}; +use crate::prelude::IdentityNonce; +use crate::state_transition::data_contract_create_transition::methods::DataContractCreateTransitionMethodsV0; +use crate::state_transition::data_contract_create_transition::DataContractCreateTransition; +use crate::state_transition::StateTransition; +use crate::version::FeatureVersion; +use platform_version::version::PlatformVersion; + +impl DataContractCreateTransitionMethodsV0 for DataContractCreateTransitionV1 { + fn new_from_data_contract>( + data_contract: DataContract, + identity_nonce: IdentityNonce, + identity: &PartialIdentity, + key_id: KeyID, + signer: &S, + _platform_version: &PlatformVersion, + _feature_version: Option, + ) -> Result { + // Note: For V1, we don't set id/owner_id on the contract because we store + // the fields directly. The id is derived from owner_id + identity_nonce. + + // Derive contract_system_version from the DataContract variant + let contract_system_version = match &data_contract { + DataContract::V0(_) => 0, + DataContract::V1(_) => 1, + }; + + let transition = DataContractCreateTransition::V1(DataContractCreateTransitionV1 { + contract_system_version, + owner_id: identity.id, + config: *data_contract.config(), + schema_defs: data_contract.schema_defs().cloned(), + document_schemas: data_contract + .document_schemas() + .into_iter() + .map(|(k, v)| (k, v.clone())) + .collect(), + groups: data_contract.groups().clone(), + tokens: data_contract.tokens().clone(), + keywords: data_contract.keywords().clone(), + description: data_contract.description().cloned(), + identity_nonce, + user_fee_increase: 0, + signature_public_key_id: key_id, + signature: Default::default(), + }); + + let mut state_transition: StateTransition = transition.into(); + let value = state_transition.signable_bytes()?; + + // The public key ids don't always match the keys in the map, so we need to do this. + let matching_key = identity + .loaded_public_keys + .iter() + .find_map(|(&key, public_key)| { + if public_key.id() == key_id { + Some(key) + } else { + None + } + }) + .expect("No matching public key id found in the map"); + + let public_key = identity.loaded_public_keys.get(&matching_key).ok_or( + ProtocolError::NonConsensusError(NonConsensusError::StateTransitionCreationError( + "public key did not exist".to_string(), + )), + )?; + + let security_level_requirements = state_transition + .security_level_requirement(public_key.purpose()) + .ok_or(ProtocolError::CorruptedCodeExecution( + "expected security level requirements".to_string(), + ))?; + + if !security_level_requirements.contains(&public_key.security_level()) { + return Err(ProtocolError::ConsensusError(Box::new( + SignatureError::InvalidSignaturePublicKeySecurityLevelError( + InvalidSignaturePublicKeySecurityLevelError::new( + public_key.security_level(), + security_level_requirements, + ), + ) + .into(), + ))); + } + + match signer.sign(public_key, &value) { + Ok(signature) => { + state_transition.set_signature(signature); + } + Err(e) => { + return Err(e); + } + } + + Ok(state_transition) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/value_conversion.rs new file mode 100644 index 0000000000..8d45b567f4 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/value_conversion.rs @@ -0,0 +1,194 @@ +use std::collections::BTreeMap; + +use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; +use platform_value::{IntegerReplacementType, ReplacementType, Value}; + +use crate::ProtocolError; + +use platform_version::version::PlatformVersion; +use crate::state_transition::{StateTransitionFieldTypes, StateTransitionValueConvert}; +use crate::state_transition::data_contract_create_transition::DataContractCreateTransitionV1; +use crate::state_transition::data_contract_create_transition::fields::*; +use crate::state_transition::state_transitions::common_fields::property_names::USER_FEE_INCREASE; +use crate::state_transition::state_transitions::contract::data_contract_create_transition::fields::{BINARY_FIELDS, IDENTIFIER_FIELDS, U32_FIELDS}; + +// Field names for V1 transition +const CONTRACT_SYSTEM_VERSION: &str = "contractSystemVersion"; +const OWNER_ID: &str = "ownerId"; +const CONFIG: &str = "config"; +const SCHEMA_DEFS: &str = "schemaDefs"; +const DOCUMENT_SCHEMAS: &str = "documentSchemas"; +const GROUPS: &str = "groups"; +const TOKENS: &str = "tokens"; +const KEYWORDS: &str = "keywords"; +const DESCRIPTION: &str = "description"; + +impl StateTransitionValueConvert<'_> for DataContractCreateTransitionV1 { + fn to_object(&self, skip_signature: bool) -> Result { + let mut object: Value = platform_value::to_value(self)?; + if skip_signature { + Self::signature_property_paths() + .into_iter() + .try_for_each(|path| { + object + .remove_values_matching_path(path) + .map_err(ProtocolError::ValueError) + .map(|_| ()) + })?; + } + Ok(object) + } + + fn to_cleaned_object(&self, skip_signature: bool) -> Result { + let mut object: Value = platform_value::to_value(self)?; + if skip_signature { + Self::signature_property_paths() + .into_iter() + .try_for_each(|path| { + object + .remove_values_matching_path(path) + .map_err(ProtocolError::ValueError) + .map(|_| ()) + })?; + } + Ok(object) + } + + fn from_object( + mut raw_object: Value, + _platform_version: &PlatformVersion, + ) -> Result { + Ok(DataContractCreateTransitionV1 { + contract_system_version: raw_object + .get_integer(CONTRACT_SYSTEM_VERSION) + .map_err(ProtocolError::ValueError)?, + owner_id: raw_object + .remove_identifier(OWNER_ID) + .map_err(ProtocolError::ValueError)?, + config: platform_value::from_value(raw_object.remove(CONFIG).map_err(|_| { + ProtocolError::DecodingError("config missing on state transition".to_string()) + })?)?, + schema_defs: raw_object + .remove(SCHEMA_DEFS) + .ok() + .map(platform_value::from_value) + .transpose()?, + document_schemas: platform_value::from_value( + raw_object.remove(DOCUMENT_SCHEMAS).map_err(|_| { + ProtocolError::DecodingError( + "document_schemas missing on state transition".to_string(), + ) + })?, + )?, + groups: raw_object + .remove(GROUPS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + tokens: raw_object + .remove(TOKENS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + keywords: raw_object + .remove(KEYWORDS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + description: raw_object + .remove(DESCRIPTION) + .ok() + .map(platform_value::from_value) + .transpose()?, + identity_nonce: raw_object + .get_optional_integer(IDENTITY_NONCE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + user_fee_increase: raw_object + .get_optional_integer(USER_FEE_INCREASE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + signature_public_key_id: raw_object + .get_optional_integer(SIGNATURE_PUBLIC_KEY_ID) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + signature: raw_object + .remove_optional_binary_data(SIGNATURE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + }) + } + + fn from_value_map( + mut raw_value_map: BTreeMap, + _platform_version: &PlatformVersion, + ) -> Result { + Ok(DataContractCreateTransitionV1 { + contract_system_version: raw_value_map + .remove_integer(CONTRACT_SYSTEM_VERSION) + .map_err(ProtocolError::ValueError)?, + owner_id: raw_value_map + .remove_identifier(OWNER_ID) + .map_err(ProtocolError::ValueError)?, + config: platform_value::from_value(raw_value_map.remove(CONFIG).ok_or( + ProtocolError::DecodingError("config missing on state transition".to_string()), + )?)?, + schema_defs: raw_value_map + .remove(SCHEMA_DEFS) + .map(platform_value::from_value) + .transpose()?, + document_schemas: platform_value::from_value( + raw_value_map + .remove(DOCUMENT_SCHEMAS) + .ok_or(ProtocolError::DecodingError( + "document_schemas missing on state transition".to_string(), + ))?, + )?, + groups: raw_value_map + .remove(GROUPS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + tokens: raw_value_map + .remove(TOKENS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + keywords: raw_value_map + .remove(KEYWORDS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + description: raw_value_map + .remove(DESCRIPTION) + .map(platform_value::from_value) + .transpose()?, + identity_nonce: raw_value_map + .remove_optional_integer(IDENTITY_NONCE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + user_fee_increase: raw_value_map + .remove_optional_integer(USER_FEE_INCREASE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + signature_public_key_id: raw_value_map + .remove_optional_integer(SIGNATURE_PUBLIC_KEY_ID) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + signature: raw_value_map + .remove_optional_binary_data(SIGNATURE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + }) + } + + fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { + value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; + value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; + value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; + Ok(()) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/version.rs new file mode 100644 index 0000000000..13effcdd9f --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v1/version.rs @@ -0,0 +1,9 @@ +use crate::state_transition::data_contract_create_transition::DataContractCreateTransitionV1; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for DataContractCreateTransitionV1 { + fn feature_version(&self) -> FeatureVersion { + 1 + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/value_conversion.rs index b4ec7a648f..65d9894496 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/value_conversion.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/value_conversion.rs @@ -5,7 +5,7 @@ use platform_value::Value; use crate::ProtocolError; use crate::state_transition::data_contract_create_transition::{ - DataContractCreateTransition, DataContractCreateTransitionV0, + DataContractCreateTransition, DataContractCreateTransitionV0, DataContractCreateTransitionV1, }; use crate::state_transition::state_transitions::data_contract_create_transition::fields::*; use crate::state_transition::StateTransitionValueConvert; @@ -21,6 +21,11 @@ impl StateTransitionValueConvert<'_> for DataContractCreateTransition { value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; Ok(value) } + DataContractCreateTransition::V1(transition) => { + let mut value = transition.to_object(skip_signature)?; + value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; + Ok(value) + } } } @@ -31,6 +36,11 @@ impl StateTransitionValueConvert<'_> for DataContractCreateTransition { value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; Ok(value) } + DataContractCreateTransition::V1(transition) => { + let mut value = transition.to_canonical_object(skip_signature)?; + value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; + Ok(value) + } } } @@ -41,6 +51,11 @@ impl StateTransitionValueConvert<'_> for DataContractCreateTransition { value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; Ok(value) } + DataContractCreateTransition::V1(transition) => { + let mut value = transition.to_canonical_cleaned_object(skip_signature)?; + value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; + Ok(value) + } } } @@ -51,6 +66,11 @@ impl StateTransitionValueConvert<'_> for DataContractCreateTransition { value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; Ok(value) } + DataContractCreateTransition::V1(transition) => { + let mut value = transition.to_cleaned_object(skip_signature)?; + value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; + Ok(value) + } } } @@ -73,6 +93,9 @@ impl StateTransitionValueConvert<'_> for DataContractCreateTransition { 0 => Ok( DataContractCreateTransitionV0::from_object(raw_object, platform_version)?.into(), ), + 1 => Ok( + DataContractCreateTransitionV1::from_object(raw_object, platform_version)?.into(), + ), n => Err(ProtocolError::UnknownVersionError(format!( "Unknown DataContractCreateTransition version {n}" ))), @@ -100,6 +123,11 @@ impl StateTransitionValueConvert<'_> for DataContractCreateTransition { platform_version, )? .into()), + 1 => Ok(DataContractCreateTransitionV1::from_value_map( + raw_value_map, + platform_version, + )? + .into()), n => Err(ProtocolError::UnknownVersionError(format!( "Unknown DataContractCreateTransition version {n}" ))), @@ -113,6 +141,7 @@ impl StateTransitionValueConvert<'_> for DataContractCreateTransition { match version { 0 => DataContractCreateTransitionV0::clean_value(value), + 1 => DataContractCreateTransitionV1::clean_value(value), n => Err(ProtocolError::UnknownVersionError(format!( "Unknown DataContractCreateTransition version {n}" ))), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/version.rs index a83280151b..fb2b647038 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/version.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/version.rs @@ -6,6 +6,7 @@ impl FeatureVersioned for DataContractCreateTransition { fn feature_version(&self) -> FeatureVersion { match self { DataContractCreateTransition::V0(v0) => v0.feature_version(), + DataContractCreateTransition::V1(v1) => v1.feature_version(), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/accessors/mod.rs index a928d77b22..0ac1cf2ab5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/accessors/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/accessors/mod.rs @@ -7,23 +7,27 @@ use crate::state_transition::data_contract_update_transition::DataContractUpdate pub use v0::*; impl DataContractUpdateTransitionAccessorsV0 for DataContractUpdateTransition { - fn data_contract(&self) -> &DataContractInSerializationFormat { + fn data_contract(&self) -> Option<&DataContractInSerializationFormat> { match self { - DataContractUpdateTransition::V0(transition) => &transition.data_contract, + DataContractUpdateTransition::V0(transition) => Some(&transition.data_contract), + DataContractUpdateTransition::V1(_) => None, } } - fn set_data_contract(&mut self, data_contract: DataContractInSerializationFormat) { + fn set_data_contract(&mut self, data_contract: DataContractInSerializationFormat) -> bool { match self { DataContractUpdateTransition::V0(transition) => { - transition.data_contract = data_contract + transition.data_contract = data_contract; + true } + DataContractUpdateTransition::V1(_) => false, } } fn identity_contract_nonce(&self) -> IdentityNonce { match self { DataContractUpdateTransition::V0(transition) => transition.identity_contract_nonce, + DataContractUpdateTransition::V1(transition) => transition.identity_contract_nonce, } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/accessors/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/accessors/v0/mod.rs index 49907ccd40..c6a896062a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/accessors/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/accessors/v0/mod.rs @@ -2,8 +2,10 @@ use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::prelude::IdentityNonce; pub trait DataContractUpdateTransitionAccessorsV0 { - fn data_contract(&self) -> &DataContractInSerializationFormat; - fn set_data_contract(&mut self, data_contract: DataContractInSerializationFormat); + /// Returns the data contract for V0 transitions, None for V1+ + fn data_contract(&self) -> Option<&DataContractInSerializationFormat>; + /// Sets the data contract for V0 transitions, returns false for V1+ + fn set_data_contract(&mut self, data_contract: DataContractInSerializationFormat) -> bool; fn identity_contract_nonce(&self) -> IdentityNonce; } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/identity_signed.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/identity_signed.rs index 50c8cb9ce4..e98ac09f8f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/identity_signed.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/identity_signed.rs @@ -7,6 +7,7 @@ impl StateTransitionIdentitySigned for DataContractUpdateTransition { fn signature_public_key_id(&self) -> KeyID { match self { DataContractUpdateTransition::V0(transition) => transition.signature_public_key_id(), + DataContractUpdateTransition::V1(transition) => transition.signature_public_key_id(), } } @@ -15,6 +16,9 @@ impl StateTransitionIdentitySigned for DataContractUpdateTransition { DataContractUpdateTransition::V0(transition) => { transition.set_signature_public_key_id(key_id) } + DataContractUpdateTransition::V1(transition) => { + transition.set_signature_public_key_id(key_id) + } } } @@ -23,6 +27,9 @@ impl StateTransitionIdentitySigned for DataContractUpdateTransition { DataContractUpdateTransition::V0(transition) => { transition.security_level_requirement(purpose) } + DataContractUpdateTransition::V1(transition) => { + transition.security_level_requirement(purpose) + } } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/json_conversion.rs index b1ee11537d..2bc7eb7911 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/json_conversion.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/json_conversion.rs @@ -22,6 +22,15 @@ impl StateTransitionJsonConvert<'_> for DataContractUpdateTransition { ); Ok(value) } + DataContractUpdateTransition::V1(transition) => { + let mut value = transition.to_json(options)?; + let map_value = value.as_object_mut().expect("expected an object"); + map_value.insert( + STATE_TRANSITION_PROTOCOL_VERSION.to_string(), + JsonValue::Number(Number::from(1)), + ); + Ok(value) + } } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/mod.rs index eb3366de83..40b01018b3 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/mod.rs @@ -1,3 +1,5 @@ +mod registration_cost; +mod update_contract_cost; mod v0; pub use v0::*; @@ -16,6 +18,11 @@ use crate::prelude::{IdentityNonce, UserFeeIncrease}; use platform_version::version::PlatformVersion; impl DataContractUpdateTransitionMethodsV0 for DataContractUpdateTransition { + /// Creates an update transition from a single data contract. + /// + /// Note: This method always creates a V0 transition (embedding the full contract) + /// because V1 transitions require both old and new contracts to compute deltas. + /// For V1 delta-based transitions, use `from_contract_update` instead. fn new_from_data_contract>( data_contract: DataContract, identity: &PartialIdentity, @@ -26,26 +33,17 @@ impl DataContractUpdateTransitionMethodsV0 for DataContractUpdateTransition { platform_version: &PlatformVersion, feature_version: Option, ) -> Result { - match feature_version.unwrap_or( - platform_version - .dpp - .state_transition_serialization_versions - .contract_update_state_transition - .default_current_version, - ) { - 0 => DataContractUpdateTransitionV0::new_from_data_contract( - data_contract, - identity, - key_id, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - feature_version, - ), - v => Err(ProtocolError::UnknownVersionError(format!( - "Unknown DataContractUpdateTransition version for new_from_data_contract {v}" - ))), - } + // Always use V0 (embed full contract) since we only have a single contract. + // V1 delta-based transitions require both old and new contracts. + DataContractUpdateTransitionV0::new_from_data_contract( + data_contract, + identity, + key_id, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + feature_version, + ) } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/registration_cost/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/registration_cost/mod.rs new file mode 100644 index 0000000000..1e68fbea51 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/registration_cost/mod.rs @@ -0,0 +1,74 @@ +mod v1; + +use crate::fee::Credits; +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use crate::state_transition::state_transitions::contract::data_contract_create_transition::methods::registration_cost::registration_cost_from_fields; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +impl DataContractUpdateTransition { + /// Returns the registration cost of the data contract update based on the current platform version. + /// + /// For V0 transitions, this calculates cost based on the embedded data contract's schemas, tokens, and keywords. + /// For V1 transitions, this calculates the cost based on new items being added + /// (new document schemas, new tokens, added keywords). + /// + /// # Arguments + /// - `platform_version`: A reference to the platform version, used to determine which + /// registration cost algorithm to apply. + /// + /// # Returns + /// - `Ok(u64)`: The total registration cost in credits for this update. + /// - `Err(ProtocolError)`: If the platform version is unrecognized. + /// + /// # Version Behavior + /// - Version 0: Always returns `0` (used before protocol version 9). + /// - Version 1: Uses a detailed cost model for items being registered. + /// Note: For updates, there is no base contract fee (only new items are charged). + pub fn registration_cost( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .contract_versions + .methods + .registration_cost + { + 0 => Ok(0), // Before 2.0 it's just 0 + 1 => { + // For updates, no base fee - only charge for new items + let base_fee: Credits = 0; + + match self { + DataContractUpdateTransition::V0(v0) => { + let document_schemas = v0 + .data_contract + .document_schemas() + .cloned() + .unwrap_or_default(); + Ok(registration_cost_from_fields( + &document_schemas, + v0.data_contract.tokens(), + v0.data_contract.keywords().len(), + base_fee, + platform_version, + )) + } + DataContractUpdateTransition::V1(v1) => Ok(registration_cost_from_fields( + &v1.new_document_schemas, + &v1.new_tokens, + v1.add_keywords.len(), + base_fee, + platform_version, + )), + } + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DataContractUpdateTransition::registration_cost".to_string(), + known_versions: vec![0, 1], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/registration_cost/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/registration_cost/v1/mod.rs new file mode 100644 index 0000000000..5fbd7e9a8d --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/registration_cost/v1/mod.rs @@ -0,0 +1,90 @@ +use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use crate::data_contract::document_type::Index; +use crate::fee::Credits; +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1; +use platform_value::Value; +use platform_version::version::PlatformVersion; + +impl DataContractUpdateTransitionV1 { + /// Computes the registration cost of a data contract update transition based on + /// new items being added (new document schemas, new tokens, added keywords). + /// + /// # Parameters + /// - `platform_version`: A reference to the current platform version providing fee parameters. + /// + /// # Returns + /// - `Credits`: The total registration cost in credits for new items. + /// + /// # Fee Components + /// - Per new document type registration fee. + /// - Per index registration fee for new document types (unique and non-unique). + /// - Token registration fee per new token. + /// - Additional fees for new tokens using perpetual or pre-programmed distribution. + /// - Search keyword fees for added keywords (`added_keyword_count * search_keyword_fee`). + pub(in crate::state_transition::state_transitions::contract::data_contract_update_transition::methods) fn registration_cost_v1( + &self, + platform_version: &PlatformVersion, + ) -> Credits { + let fee_version = &platform_version.fee_version.data_contract_registration; + let mut cost: Credits = 0; + + // Calculate cost for new document schemas + for document_type_schema in self.new_document_schemas.values() { + cost = cost.saturating_add(fee_version.document_type_registration_fee); + + // Parse indexes from the schema if present + if let Ok(schema_map) = document_type_schema.to_map() { + if let Ok(Some(index_values)) = Value::inner_optional_array_slice_value( + schema_map, + crate::data_contract::document_type::property_names::INDICES, + ) { + for index_value in index_values { + if let Ok(index_value_map) = index_value.to_map() { + if let Ok(index) = Index::try_from(index_value_map.as_slice()) { + let base_index_fee = if index.contested_index.is_some() { + fee_version.document_type_base_contested_index_registration_fee + } else if index.unique { + fee_version.document_type_base_unique_index_registration_fee + } else { + fee_version.document_type_base_non_unique_index_registration_fee + }; + cost = cost.saturating_add(base_index_fee); + } + } + } + } + } + } + + // Calculate cost for new tokens + for token_config in self.new_tokens.values() { + cost = cost.saturating_add(fee_version.token_registration_fee); + + if token_config + .distribution_rules() + .perpetual_distribution() + .is_some() + { + cost = cost.saturating_add(fee_version.token_uses_perpetual_distribution_fee); + } + + if token_config + .distribution_rules() + .pre_programmed_distribution() + .is_some() + { + cost = cost.saturating_add(fee_version.token_uses_pre_programmed_distribution_fee); + } + } + + // Calculate cost for added keywords + let keyword_cost = fee_version + .search_keyword_fee + .saturating_mul(self.add_keywords.len() as u64); + + cost = cost.saturating_add(keyword_cost); + + cost + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/update_contract_cost/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/update_contract_cost/mod.rs new file mode 100644 index 0000000000..af503293dc --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/update_contract_cost/mod.rs @@ -0,0 +1,73 @@ +mod v1; + +pub(in crate::state_transition::state_transitions::contract) use v1::update_contract_cost_from_fields; + +use crate::fee::Credits; +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use crate::state_transition::state_transitions::contract::data_contract_create_transition::methods::registration_cost::registration_cost_from_fields; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +impl DataContractUpdateTransition { + /// Returns the update cost of the data contract update based on the current platform version. + /// + /// This calculates the cost for updating existing document schemas (not new items). + /// For V0 transitions, this uses the registration cost logic (charging for all schemas/tokens/keywords). + /// For V1 transitions, this calculates the cost based on updated_document_schemas only. + /// + /// # Arguments + /// - `platform_version`: A reference to the platform version, used to determine which + /// update cost algorithm to apply. + /// + /// # Returns + /// - `Ok(u64)`: The total update cost in credits for this update. + /// - `Err(ProtocolError)`: If the platform version is unrecognized. + /// + /// # Version Behavior + /// - Version 0: Always returns `0` (used before protocol version 9). + /// - Version 1: For V0 transitions, uses registration cost logic for all items in the contract. + /// For V1 transitions, calculates cost based on updated_document_schemas. + pub fn update_contract_cost( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .contract_versions + .methods + .update_contract_cost + { + 0 => Ok(0), // Before 2.0 it's just 0 + 1 => { + match self { + // V0 transitions embed the full contract, so we use the + // registration cost logic (charging for all schemas/tokens/keywords) + DataContractUpdateTransition::V0(v0) => { + let document_schemas = v0 + .data_contract + .document_schemas() + .cloned() + .unwrap_or_default(); + Ok(registration_cost_from_fields( + &document_schemas, + v0.data_contract.tokens(), + v0.data_contract.keywords().len(), + 0, // no base fee for updates + platform_version, + )) + } + // V1 transitions explicitly specify updated_document_schemas + DataContractUpdateTransition::V1(v1) => Ok(update_contract_cost_from_fields( + &v1.updated_document_schemas, + platform_version, + )), + } + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DataContractUpdateTransition::update_contract_cost".to_string(), + known_versions: vec![0, 1], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/update_contract_cost/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/update_contract_cost/v1/mod.rs new file mode 100644 index 0000000000..b9e256c7aa --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/methods/update_contract_cost/v1/mod.rs @@ -0,0 +1,59 @@ +use std::collections::BTreeMap; + +use crate::data_contract::document_type::Index; +use crate::fee::Credits; +use platform_value::Value; +use platform_version::version::PlatformVersion; + +/// Computes the update contract cost based on updated document schemas. +/// +/// # Parameters +/// - `updated_document_schemas`: A map of document names to their updated JSON schema values. +/// - `platform_version`: A reference to the current platform version providing fee parameters. +/// +/// # Returns +/// - `Credits`: The total update cost in credits. +/// +/// # Fee Components +/// - Per updated document type fee (same as registration fee for document types). +/// - Per index fee for indexes in updated schemas. +/// +/// Note: This charges for the full schema content of updated documents, as we cannot +/// determine what specifically changed within a schema without the original. +pub(in crate::state_transition::state_transitions::contract) fn update_contract_cost_from_fields( + updated_document_schemas: &BTreeMap, + platform_version: &PlatformVersion, +) -> Credits { + let fee_version = &platform_version.fee_version.data_contract_registration; + let mut cost: Credits = 0; + + // Calculate cost for updated document schemas + for document_type_schema in updated_document_schemas.values() { + cost = cost.saturating_add(fee_version.document_type_registration_fee); + + // Parse indexes from the schema if present + if let Ok(schema_map) = document_type_schema.to_map() { + if let Ok(Some(index_values)) = Value::inner_optional_array_slice_value( + schema_map, + crate::data_contract::document_type::property_names::INDICES, + ) { + for index_value in index_values { + if let Ok(index_value_map) = index_value.to_map() { + if let Ok(index) = Index::try_from(index_value_map.as_slice()) { + let base_index_fee = if index.contested_index.is_some() { + fee_version.document_type_base_contested_index_registration_fee + } else if index.unique { + fee_version.document_type_base_unique_index_registration_fee + } else { + fee_version.document_type_base_non_unique_index_registration_fee + }; + cost = cost.saturating_add(base_index_fee); + } + } + } + } + } + } + + cost +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/mod.rs index 5ff6d655d8..ea7f284fe3 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/mod.rs @@ -19,19 +19,21 @@ mod serialize; mod state_transition_estimated_fee_validation; mod state_transition_like; mod v0; +mod v1; #[cfg(feature = "state-transition-value-conversion")] mod value_conversion; mod version; pub use fields::*; use platform_version::version::PlatformVersion; -use platform_version::{TryFromPlatformVersioned, TryIntoPlatformVersioned}; +use platform_version::TryIntoPlatformVersioned; use crate::data_contract::DataContract; use crate::identity::state_transition::OptionallyAssetLockProved; use crate::prelude::IdentityNonce; pub use v0::*; +pub use v1::*; pub type DataContractUpdateTransitionLatest = DataContractUpdateTransitionV0; @@ -59,15 +61,31 @@ pub type DataContractUpdateTransitionLatest = DataContractUpdateTransitionV0; pub enum DataContractUpdateTransition { #[cfg_attr(feature = "state-transition-serde-conversion", serde(rename = "0"))] V0(DataContractUpdateTransitionV0), + #[cfg_attr(feature = "state-transition-serde-conversion", serde(rename = "1"))] + V1(DataContractUpdateTransitionV1), } -impl TryFromPlatformVersioned<(DataContract, IdentityNonce)> for DataContractUpdateTransition { - type Error = ProtocolError; +impl DataContractUpdateTransition { + /// Creates a V0 update transition from a full data contract. + /// This embeds the entire contract in the transition (legacy behavior). + pub fn from_data_contract_v0( + data_contract: DataContract, + identity_nonce: IdentityNonce, + platform_version: &PlatformVersion, + ) -> Result { + let v0: DataContractUpdateTransitionV0 = + (data_contract, identity_nonce).try_into_platform_versioned(platform_version)?; + Ok(v0.into()) + } - fn try_from_platform_versioned( - value: (DataContract, IdentityNonce), + /// Creates a V1 update transition by computing the delta between old and new contracts. + /// This is the preferred method for V1 transitions as it properly captures the changes. + pub fn from_contract_update( + old_contract: &DataContract, + new_contract: &DataContract, + identity_nonce: IdentityNonce, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result { match platform_version .dpp .state_transition_serialization_versions @@ -75,14 +93,19 @@ impl TryFromPlatformVersioned<(DataContract, IdentityNonce)> for DataContractUpd .default_current_version { 0 => { - let data_contract_update_transition: DataContractUpdateTransitionV0 = - value.try_into_platform_versioned(platform_version)?; - Ok(data_contract_update_transition.into()) + Self::from_data_contract_v0(new_contract.clone(), identity_nonce, platform_version) + } + 1 => { + let v1 = DataContractUpdateTransitionV1::from_contract_update( + old_contract, + new_contract, + identity_nonce, + )?; + Ok(v1.into()) } version => Err(ProtocolError::UnknownVersionMismatch { - method: "DataContractUpdateTransition::try_from_platform_versioned(DataContract)" - .to_string(), - known_versions: vec![0], + method: "DataContractUpdateTransition::from_contract_update".to_string(), + known_versions: vec![0, 1], received: version, }), } @@ -108,7 +131,6 @@ impl OptionallyAssetLockProved for DataContractUpdateTransition {} #[cfg(test)] mod test { use crate::data_contract::DataContract; - use crate::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use crate::tests::fixtures::get_data_contract_fixture; use crate::version::LATEST_PLATFORM_VERSION; @@ -116,7 +138,7 @@ mod test { use platform_version::version::PlatformVersion; use super::*; - use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; use crate::state_transition::{StateTransitionLike, StateTransitionOwned, StateTransitionType}; struct TestData { @@ -125,13 +147,22 @@ mod test { } fn get_test_data() -> TestData { - let platform_version = PlatformVersion::first(); - let data_contract = get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - - let state_transition: DataContractUpdateTransition = (data_contract.clone(), 1) - .try_into_platform_versioned(platform_version) - .expect("expected to get transition"); + let platform_version = PlatformVersion::latest(); + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + + // Create a modified version for the update transition + let old_contract = data_contract.clone(); + data_contract.set_version(2); + + let state_transition = DataContractUpdateTransition::from_contract_update( + &old_contract, + &data_contract, + 1, + platform_version, + ) + .expect("expected to get transition"); TestData { data_contract, @@ -161,19 +192,6 @@ mod test { ); } - #[test] - #[cfg(feature = "state-transition-json-conversion")] - fn should_return_data_contract() { - let data = get_test_data(); - - assert_eq!( - data.state_transition.data_contract().clone(), - data.data_contract - .try_into_platform_versioned(PlatformVersion::first()) - .unwrap() - ); - } - #[test] fn should_return_owner_id() { let data = get_test_data(); diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/state_transition_estimated_fee_validation.rs index 26066ee8ee..d6d32e023c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/state_transition_estimated_fee_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/state_transition_estimated_fee_validation.rs @@ -1,6 +1,5 @@ use crate::consensus::state::identity::IdentityInsufficientBalanceError; use crate::fee::Credits; -use crate::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use crate::state_transition::data_contract_update_transition::DataContractUpdateTransition; use crate::state_transition::{ StateTransitionEstimatedFeeValidation, StateTransitionIdentityEstimatedFeeValidation, @@ -20,7 +19,7 @@ impl StateTransitionEstimatedFeeValidation for DataContractUpdateTransition { .state_transition_min_fees .contract_update; - let registration_cost = self.data_contract().registration_cost(platform_version)?; + let registration_cost = self.registration_cost(platform_version)?; Ok(base_fee.saturating_add(registration_cost)) } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/state_transition_like.rs index fde49e3526..081f5222a3 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/state_transition_like.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/state_transition_like.rs @@ -11,24 +11,28 @@ impl StateTransitionLike for DataContractUpdateTransition { fn modified_data_ids(&self) -> Vec { match self { DataContractUpdateTransition::V0(transition) => transition.modified_data_ids(), + DataContractUpdateTransition::V1(transition) => transition.modified_data_ids(), } } fn state_transition_protocol_version(&self) -> FeatureVersion { match self { DataContractUpdateTransition::V0(_) => 0, + DataContractUpdateTransition::V1(_) => 1, } } /// returns the type of State Transition fn state_transition_type(&self) -> StateTransitionType { match self { DataContractUpdateTransition::V0(transition) => transition.state_transition_type(), + DataContractUpdateTransition::V1(transition) => transition.state_transition_type(), } } fn unique_identifiers(&self) -> Vec { match self { DataContractUpdateTransition::V0(transition) => transition.unique_identifiers(), + DataContractUpdateTransition::V1(transition) => transition.unique_identifiers(), } } @@ -36,6 +40,7 @@ impl StateTransitionLike for DataContractUpdateTransition { fn user_fee_increase(&self) -> UserFeeIncrease { match self { DataContractUpdateTransition::V0(transition) => transition.user_fee_increase(), + DataContractUpdateTransition::V1(transition) => transition.user_fee_increase(), } } /// set a fee increase multiplier @@ -44,6 +49,9 @@ impl StateTransitionLike for DataContractUpdateTransition { DataContractUpdateTransition::V0(transition) => { transition.set_user_fee_increase(user_fee_increase) } + DataContractUpdateTransition::V1(transition) => { + transition.set_user_fee_increase(user_fee_increase) + } } } } @@ -53,12 +61,14 @@ impl StateTransitionSingleSigned for DataContractUpdateTransition { fn signature(&self) -> &BinaryData { match self { DataContractUpdateTransition::V0(transition) => transition.signature(), + DataContractUpdateTransition::V1(transition) => transition.signature(), } } /// set a new signature fn set_signature(&mut self, signature: BinaryData) { match self { DataContractUpdateTransition::V0(transition) => transition.set_signature(signature), + DataContractUpdateTransition::V1(transition) => transition.set_signature(signature), } } @@ -67,6 +77,9 @@ impl StateTransitionSingleSigned for DataContractUpdateTransition { DataContractUpdateTransition::V0(transition) => { transition.set_signature_bytes(signature) } + DataContractUpdateTransition::V1(transition) => { + transition.set_signature_bytes(signature) + } } } } @@ -75,6 +88,7 @@ impl StateTransitionOwned for DataContractUpdateTransition { fn owner_id(&self) -> Identifier { match self { DataContractUpdateTransition::V0(transition) => transition.owner_id(), + DataContractUpdateTransition::V1(transition) => transition.owner_id(), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/identity_signed.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/identity_signed.rs new file mode 100644 index 0000000000..f9cb9cc790 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/identity_signed.rs @@ -0,0 +1,18 @@ +use crate::identity::SecurityLevel::CRITICAL; +use crate::identity::{KeyID, Purpose, SecurityLevel}; +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1; +use crate::state_transition::StateTransitionIdentitySigned; + +impl StateTransitionIdentitySigned for DataContractUpdateTransitionV1 { + fn signature_public_key_id(&self) -> KeyID { + self.signature_public_key_id + } + + fn set_signature_public_key_id(&mut self, key_id: crate::identity::KeyID) { + self.signature_public_key_id = key_id + } + + fn security_level_requirement(&self, _purpose: Purpose) -> Vec { + vec![CRITICAL] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/json_conversion.rs new file mode 100644 index 0000000000..e2b5e2a829 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/json_conversion.rs @@ -0,0 +1,4 @@ +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1; +use crate::state_transition::StateTransitionJsonConvert; + +impl StateTransitionJsonConvert<'_> for DataContractUpdateTransitionV1 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/mod.rs new file mode 100644 index 0000000000..9afd56b0b2 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/mod.rs @@ -0,0 +1,239 @@ +mod identity_signed; +#[cfg(feature = "state-transition-json-conversion")] +mod json_conversion; +mod state_transition_like; +mod types; +pub(super) mod v0_methods; +#[cfg(feature = "state-transition-value-conversion")] +mod value_conversion; +mod version; + +use std::collections::BTreeMap; + +use platform_value::{BinaryData, Identifier, Value}; +#[cfg(feature = "state-transition-serde-conversion")] +use serde::{Deserialize, Serialize}; + +use bincode::{Decode, Encode}; +use platform_serialization_derive::PlatformSignable; + +use crate::data_contract::accessors::v0::DataContractV0Getters; +use crate::data_contract::associated_token::token_configuration::TokenConfiguration; +use crate::data_contract::group::Group; +use crate::data_contract::schema::DataContractSchemaMethodsV0; +use crate::data_contract::DataContract; +use crate::data_contract::{ + DefinitionName, DocumentName, GroupContractPosition, TokenContractPosition, +}; +use crate::prelude::{IdentityNonce, UserFeeIncrease}; +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use crate::state_transition::StateTransition; +use crate::{identity::KeyID, ProtocolError}; + +/// DataContractUpdateTransitionV1 stores the contract fields directly +/// rather than embedding a serialization format. + +#[derive(Debug, Clone, Encode, Decode, PartialEq, PlatformSignable)] +#[cfg_attr( + feature = "state-transition-serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct DataContractUpdateTransitionV1 { + /// Optional updated contract system version. When present, the contract + /// will be upgraded to this system version. + /// The system version defines the features of the contract. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub update_contract_system_version: Option, + + /// The unique identifier of the data contract being updated. + pub id: Identifier, + + /// The identifier of the contract owner. + pub owner_id: Identifier, + + /// The new revision number for this update. + pub revision: u32, + + /// Updated shared subschemas ($defs) - must be compatible with existing ones. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub updated_schema_defs: BTreeMap, + + /// New shared subschemas to add to $defs object (when none existed before). + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub new_schema_defs: BTreeMap, + + /// Updated document JSON Schemas for existing document types. + /// Currently, we can not update document schemas as of version 3.1 + /// This will change in the future so we have this field + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub updated_document_schemas: BTreeMap, + + /// New document JSON Schemas for new document types. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub new_document_schemas: BTreeMap, + + /// New groups that allow for specific multiparty actions on the contract. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub new_groups: BTreeMap, + + /// New tokens on the contract. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub new_tokens: BTreeMap, + + /// Keywords to remove from the contract. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub remove_keywords: Vec, + + /// Keywords to add to the contract. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub add_keywords: Vec, + + /// Updated description for the contract. + /// None = don't update, Some(None) = clear description, Some(Some(value)) = set new description. + #[cfg_attr(feature = "state-transition-serde-conversion", serde(default))] + pub update_description: Option>, + + /// The identity contract nonce. + #[cfg_attr( + feature = "state-transition-serde-conversion", + serde(rename = "$identity-contract-nonce") + )] + pub identity_contract_nonce: IdentityNonce, + + /// User fee increase for priority processing. + pub user_fee_increase: UserFeeIncrease, + + /// The public key id used to sign. + #[platform_signable(exclude_from_sig_hash)] + pub signature_public_key_id: KeyID, + + /// The signature. + #[platform_signable(exclude_from_sig_hash)] + pub signature: BinaryData, +} + +impl From for StateTransition { + fn from(value: DataContractUpdateTransitionV1) -> Self { + let transition: DataContractUpdateTransition = value.into(); + transition.into() + } +} + +impl From<&DataContractUpdateTransitionV1> for StateTransition { + fn from(value: &DataContractUpdateTransitionV1) -> Self { + let transition: DataContractUpdateTransition = value.clone().into(); + transition.into() + } +} + +impl DataContractUpdateTransitionV1 { + /// Creates a V1 update transition by computing the delta between old and new contracts. + pub fn from_contract_update( + old_contract: &DataContract, + new_contract: &DataContract, + identity_nonce: IdentityNonce, + ) -> Result { + use crate::data_contract::accessors::v1::DataContractV1Getters; + + // Compute schema_defs delta + let old_schema_defs = old_contract.schema_defs().cloned().unwrap_or_default(); + let new_schema_defs_map = new_contract.schema_defs().cloned().unwrap_or_default(); + + let mut updated_schema_defs = BTreeMap::new(); + let mut new_schema_defs = BTreeMap::new(); + + for (name, new_def) in &new_schema_defs_map { + if let Some(old_def) = old_schema_defs.get(name) { + if old_def != new_def { + updated_schema_defs.insert(name.clone(), new_def.clone()); + } + } else { + new_schema_defs.insert(name.clone(), new_def.clone()); + } + } + + // Compute document schemas delta + let old_doc_schemas: BTreeMap<_, _> = old_contract + .document_schemas() + .into_iter() + .map(|(name, schema)| (name, schema.clone())) + .collect(); + let new_doc_schemas: BTreeMap<_, _> = new_contract + .document_schemas() + .into_iter() + .map(|(name, schema)| (name, schema.clone())) + .collect(); + + let mut updated_document_schemas = BTreeMap::new(); + let mut new_document_schemas = BTreeMap::new(); + + for (name, new_schema) in &new_doc_schemas { + if let Some(old_schema) = old_doc_schemas.get(name) { + if old_schema != new_schema { + updated_document_schemas.insert(name.clone(), new_schema.clone()); + } + } else { + new_document_schemas.insert(name.clone(), new_schema.clone()); + } + } + + // Compute groups delta (only new groups, can't modify existing) + let old_groups = old_contract.groups(); + let new_groups_map = new_contract.groups(); + + let mut new_groups = BTreeMap::new(); + for (pos, group) in new_groups_map { + if !old_groups.contains_key(pos) { + new_groups.insert(*pos, group.clone()); + } + } + + // Compute tokens delta (only new tokens, can't modify existing) + let old_tokens = old_contract.tokens(); + let new_tokens_map = new_contract.tokens(); + + let mut new_tokens = BTreeMap::new(); + for (pos, token) in new_tokens_map { + if !old_tokens.contains_key(pos) { + new_tokens.insert(*pos, token.clone()); + } + } + + // Compute keywords delta + let old_keywords: std::collections::HashSet<_> = + old_contract.keywords().iter().cloned().collect(); + let new_keywords: std::collections::HashSet<_> = + new_contract.keywords().iter().cloned().collect(); + + let remove_keywords: Vec<_> = old_keywords.difference(&new_keywords).cloned().collect(); + let add_keywords: Vec<_> = new_keywords.difference(&old_keywords).cloned().collect(); + + // Compute description delta + let update_description = if old_contract.description() != new_contract.description() { + Some(new_contract.description().cloned()) + } else { + None + }; + + Ok(DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: new_contract.id(), + owner_id: new_contract.owner_id(), + revision: new_contract.version(), + updated_schema_defs, + new_schema_defs, + updated_document_schemas, + new_document_schemas, + new_groups, + new_tokens, + remove_keywords, + add_keywords, + update_description, + identity_contract_nonce: identity_nonce, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/state_transition_like.rs new file mode 100644 index 0000000000..28d4f6c700 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/state_transition_like.rs @@ -0,0 +1,70 @@ +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use platform_value::BinaryData; + +use crate::prelude::UserFeeIncrease; +use crate::{ + prelude::Identifier, + state_transition::{StateTransitionLike, StateTransitionOwned, StateTransitionType}, +}; + +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1; +use crate::state_transition::StateTransitionSingleSigned; +use crate::state_transition::StateTransitionType::DataContractUpdate; +use crate::version::FeatureVersion; + +impl StateTransitionLike for DataContractUpdateTransitionV1 { + /// Returns ID of the updated contract + fn modified_data_ids(&self) -> Vec { + vec![self.id] + } + + fn state_transition_protocol_version(&self) -> FeatureVersion { + 1 + } + + /// returns the type of State Transition + fn state_transition_type(&self) -> StateTransitionType { + DataContractUpdate + } + + fn unique_identifiers(&self) -> Vec { + vec![format!( + "{}-{}-{:x}", + BASE64_STANDARD.encode(self.owner_id), + BASE64_STANDARD.encode(self.id), + self.identity_contract_nonce + )] + } + + fn user_fee_increase(&self) -> UserFeeIncrease { + self.user_fee_increase + } + + fn set_user_fee_increase(&mut self, user_fee_increase: UserFeeIncrease) { + self.user_fee_increase = user_fee_increase + } +} + +impl StateTransitionSingleSigned for DataContractUpdateTransitionV1 { + /// returns the signature as a byte-array + fn signature(&self) -> &BinaryData { + &self.signature + } + + /// set a new signature + fn set_signature(&mut self, signature: BinaryData) { + self.signature = signature + } + + fn set_signature_bytes(&mut self, signature: Vec) { + self.signature = BinaryData::new(signature) + } +} + +impl StateTransitionOwned for DataContractUpdateTransitionV1 { + /// Get owner ID + fn owner_id(&self) -> Identifier { + self.owner_id + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/types.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/types.rs new file mode 100644 index 0000000000..c30f6851a6 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/types.rs @@ -0,0 +1,19 @@ +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1; +use crate::state_transition::state_transitions::common_fields::property_names::{ + SIGNATURE, SIGNATURE_PUBLIC_KEY_ID, +}; +use crate::state_transition::StateTransitionFieldTypes; + +impl StateTransitionFieldTypes for DataContractUpdateTransitionV1 { + fn signature_property_paths() -> Vec<&'static str> { + vec![SIGNATURE, SIGNATURE_PUBLIC_KEY_ID] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![SIGNATURE] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/v0_methods.rs new file mode 100644 index 0000000000..f6cc056100 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/v0_methods.rs @@ -0,0 +1,65 @@ +use std::collections::BTreeMap; + +use crate::data_contract::accessors::v0::DataContractV0Getters; +use crate::data_contract::schema::DataContractSchemaMethodsV0; +use crate::data_contract::DataContract; +use crate::identity::signer::Signer; +use crate::identity::{IdentityPublicKey, KeyID, PartialIdentity}; +use crate::serialization::Signable; + +use crate::prelude::{IdentityNonce, UserFeeIncrease}; +use crate::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; +use crate::state_transition::data_contract_update_transition::{ + DataContractUpdateTransition, DataContractUpdateTransitionV1, +}; +use crate::state_transition::StateTransition; +use crate::version::FeatureVersion; +use crate::{NonConsensusError, ProtocolError}; +use platform_version::version::PlatformVersion; + +impl DataContractUpdateTransitionMethodsV0 for DataContractUpdateTransitionV1 { + fn new_from_data_contract>( + data_contract: DataContract, + identity: &PartialIdentity, + key_id: KeyID, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + _feature_version: Option, + ) -> Result { + let transition = DataContractUpdateTransition::V1(DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: data_contract.version(), + updated_schema_defs: BTreeMap::new(), + new_schema_defs: data_contract.schema_defs().cloned().unwrap_or_default(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups: BTreeMap::new(), + new_tokens: BTreeMap::new(), + remove_keywords: Vec::new(), + add_keywords: Vec::new(), + update_description: None, + identity_contract_nonce, + user_fee_increase, + signature_public_key_id: key_id, + signature: Default::default(), + }); + + let mut state_transition: StateTransition = transition.into(); + let value = state_transition.signable_bytes()?; + let public_key = + identity + .loaded_public_keys + .get(&key_id) + .ok_or(ProtocolError::NonConsensusError( + NonConsensusError::StateTransitionCreationError( + "public key did not exist".to_string(), + ), + ))?; + state_transition.set_signature(signer.sign(public_key, &value)?); + Ok(state_transition) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/value_conversion.rs new file mode 100644 index 0000000000..490617500f --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/value_conversion.rs @@ -0,0 +1,245 @@ +use crate::state_transition::data_contract_update_transition::fields::*; +use crate::state_transition::data_contract_update_transition::{ + DataContractUpdateTransitionV1, BINARY_FIELDS, IDENTIFIER_FIELDS, U32_FIELDS, +}; +use crate::state_transition::state_transitions::common_fields::property_names::{ + IDENTITY_CONTRACT_NONCE, USER_FEE_INCREASE, +}; +use crate::state_transition::StateTransitionFieldTypes; +use crate::state_transition::StateTransitionValueConvert; +use crate::ProtocolError; +use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; +use platform_value::{IntegerReplacementType, ReplacementType, Value}; +use platform_version::version::PlatformVersion; +use std::collections::BTreeMap; + +// Field names for V1 transition +const UPDATE_CONTRACT_SYSTEM_VERSION: &str = "updateContractSystemVersion"; +const ID: &str = "id"; +const OWNER_ID: &str = "ownerId"; +const REVISION: &str = "revision"; +const UPDATED_SCHEMA_DEFS: &str = "updatedSchemaDefs"; +const NEW_SCHEMA_DEFS: &str = "newSchemaDefs"; +const UPDATED_DOCUMENT_SCHEMAS: &str = "updatedDocumentSchemas"; +const NEW_DOCUMENT_SCHEMAS: &str = "newDocumentSchemas"; +const NEW_GROUPS: &str = "newGroups"; +const NEW_TOKENS: &str = "newTokens"; +const REMOVE_KEYWORDS: &str = "removeKeywords"; +const ADD_KEYWORDS: &str = "addKeywords"; +const UPDATE_DESCRIPTION: &str = "updateDescription"; + +impl StateTransitionValueConvert<'_> for DataContractUpdateTransitionV1 { + fn to_object(&self, skip_signature: bool) -> Result { + let mut object: Value = platform_value::to_value(self)?; + if skip_signature { + Self::signature_property_paths() + .into_iter() + .try_for_each(|path| { + object + .remove_values_matching_path(path) + .map_err(ProtocolError::ValueError) + .map(|_| ()) + })?; + } + Ok(object) + } + + fn to_cleaned_object(&self, skip_signature: bool) -> Result { + let mut object: Value = platform_value::to_value(self)?; + if skip_signature { + Self::signature_property_paths() + .into_iter() + .try_for_each(|path| { + object + .remove_values_matching_path(path) + .map_err(ProtocolError::ValueError) + .map(|_| ()) + })?; + } + Ok(object) + } + + fn from_object( + mut raw_object: Value, + _platform_version: &PlatformVersion, + ) -> Result { + Ok(DataContractUpdateTransitionV1 { + update_contract_system_version: raw_object + .get_optional_integer(UPDATE_CONTRACT_SYSTEM_VERSION) + .map_err(ProtocolError::ValueError)?, + id: raw_object + .remove_identifier(ID) + .map_err(ProtocolError::ValueError)?, + owner_id: raw_object + .remove_identifier(OWNER_ID) + .map_err(ProtocolError::ValueError)?, + revision: raw_object + .get_integer(REVISION) + .map_err(ProtocolError::ValueError)?, + updated_schema_defs: raw_object + .remove(UPDATED_SCHEMA_DEFS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + new_schema_defs: raw_object + .remove(NEW_SCHEMA_DEFS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + updated_document_schemas: raw_object + .remove(UPDATED_DOCUMENT_SCHEMAS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + new_document_schemas: raw_object + .remove(NEW_DOCUMENT_SCHEMAS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + new_groups: raw_object + .remove(NEW_GROUPS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + new_tokens: raw_object + .remove(NEW_TOKENS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + remove_keywords: raw_object + .remove(REMOVE_KEYWORDS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + add_keywords: raw_object + .remove(ADD_KEYWORDS) + .ok() + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + update_description: raw_object + .remove(UPDATE_DESCRIPTION) + .ok() + .map(platform_value::from_value) + .transpose()?, + identity_contract_nonce: raw_object.remove_integer(IDENTITY_CONTRACT_NONCE).map_err( + |_| { + ProtocolError::DecodingError( + "identity contract nonce missing on data contract update state transition" + .to_string(), + ) + }, + )?, + user_fee_increase: raw_object + .get_optional_integer(USER_FEE_INCREASE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + signature_public_key_id: raw_object + .get_optional_integer(SIGNATURE_PUBLIC_KEY_ID) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + signature: raw_object + .remove_optional_binary_data(SIGNATURE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + }) + } + + fn from_value_map( + mut raw_value_map: BTreeMap, + _platform_version: &PlatformVersion, + ) -> Result { + Ok(DataContractUpdateTransitionV1 { + update_contract_system_version: raw_value_map + .remove_optional_integer(UPDATE_CONTRACT_SYSTEM_VERSION) + .map_err(ProtocolError::ValueError)?, + id: raw_value_map + .remove_identifier(ID) + .map_err(ProtocolError::ValueError)?, + owner_id: raw_value_map + .remove_identifier(OWNER_ID) + .map_err(ProtocolError::ValueError)?, + revision: raw_value_map + .remove_integer(REVISION) + .map_err(ProtocolError::ValueError)?, + updated_schema_defs: raw_value_map + .remove(UPDATED_SCHEMA_DEFS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + new_schema_defs: raw_value_map + .remove(NEW_SCHEMA_DEFS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + updated_document_schemas: raw_value_map + .remove(UPDATED_DOCUMENT_SCHEMAS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + new_document_schemas: raw_value_map + .remove(NEW_DOCUMENT_SCHEMAS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + new_groups: raw_value_map + .remove(NEW_GROUPS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + new_tokens: raw_value_map + .remove(NEW_TOKENS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + remove_keywords: raw_value_map + .remove(REMOVE_KEYWORDS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + add_keywords: raw_value_map + .remove(ADD_KEYWORDS) + .map(platform_value::from_value) + .transpose()? + .unwrap_or_default(), + update_description: raw_value_map + .remove(UPDATE_DESCRIPTION) + .map(platform_value::from_value) + .transpose()?, + identity_contract_nonce: raw_value_map + .remove_integer(IDENTITY_CONTRACT_NONCE) + .map_err(|_| { + ProtocolError::DecodingError( + "identity contract nonce missing on data contract update state transition" + .to_string(), + ) + })?, + user_fee_increase: raw_value_map + .remove_optional_integer(USER_FEE_INCREASE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + signature_public_key_id: raw_value_map + .remove_optional_integer(SIGNATURE_PUBLIC_KEY_ID) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + signature: raw_value_map + .remove_optional_binary_data(SIGNATURE) + .map_err(ProtocolError::ValueError)? + .unwrap_or_default(), + }) + } + + fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { + value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; + value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; + value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; + Ok(()) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/version.rs new file mode 100644 index 0000000000..fbcbc011f3 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/version.rs @@ -0,0 +1,9 @@ +use crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for DataContractUpdateTransitionV1 { + fn feature_version(&self) -> FeatureVersion { + 1 + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/value_conversion.rs index 1ec80c0e69..18bdfe055b 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/value_conversion.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/value_conversion.rs @@ -5,7 +5,7 @@ use platform_value::Value; use crate::ProtocolError; use crate::state_transition::data_contract_update_transition::{ - DataContractUpdateTransition, DataContractUpdateTransitionV0, + DataContractUpdateTransition, DataContractUpdateTransitionV0, DataContractUpdateTransitionV1, }; use crate::state_transition::state_transitions::data_contract_update_transition::fields::*; use crate::state_transition::StateTransitionValueConvert; @@ -21,6 +21,11 @@ impl StateTransitionValueConvert<'_> for DataContractUpdateTransition { value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; Ok(value) } + DataContractUpdateTransition::V1(transition) => { + let mut value = transition.to_object(skip_signature)?; + value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; + Ok(value) + } } } @@ -31,6 +36,11 @@ impl StateTransitionValueConvert<'_> for DataContractUpdateTransition { value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; Ok(value) } + DataContractUpdateTransition::V1(transition) => { + let mut value = transition.to_canonical_object(skip_signature)?; + value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; + Ok(value) + } } } @@ -41,6 +51,11 @@ impl StateTransitionValueConvert<'_> for DataContractUpdateTransition { value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; Ok(value) } + DataContractUpdateTransition::V1(transition) => { + let mut value = transition.to_canonical_cleaned_object(skip_signature)?; + value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; + Ok(value) + } } } @@ -51,6 +66,11 @@ impl StateTransitionValueConvert<'_> for DataContractUpdateTransition { value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; Ok(value) } + DataContractUpdateTransition::V1(transition) => { + let mut value = transition.to_cleaned_object(skip_signature)?; + value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; + Ok(value) + } } } @@ -73,6 +93,9 @@ impl StateTransitionValueConvert<'_> for DataContractUpdateTransition { 0 => Ok( DataContractUpdateTransitionV0::from_object(raw_object, platform_version)?.into(), ), + 1 => Ok( + DataContractUpdateTransitionV1::from_object(raw_object, platform_version)?.into(), + ), n => Err(ProtocolError::UnknownVersionError(format!( "Unknown DataContractUpdateTransition version {n}" ))), @@ -100,6 +123,11 @@ impl StateTransitionValueConvert<'_> for DataContractUpdateTransition { platform_version, )? .into()), + 1 => Ok(DataContractUpdateTransitionV1::from_value_map( + raw_value_map, + platform_version, + )? + .into()), n => Err(ProtocolError::UnknownVersionError(format!( "Unknown DataContractUpdateTransition version {n}" ))), @@ -113,6 +141,7 @@ impl StateTransitionValueConvert<'_> for DataContractUpdateTransition { match version { 0 => DataContractUpdateTransitionV0::clean_value(value), + 1 => DataContractUpdateTransitionV1::clean_value(value), n => Err(ProtocolError::UnknownVersionError(format!( "Unknown DataContractUpdateTransition version {n}" ))), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/version.rs index a49b3c55b6..e7c29397e9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/version.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/version.rs @@ -6,6 +6,7 @@ impl FeatureVersioned for DataContractUpdateTransition { fn feature_version(&self) -> FeatureVersion { match self { DataContractUpdateTransition::V0(v0) => v0.feature_version(), + DataContractUpdateTransition::V1(v1) => v1.feature_version(), } } } diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index acacd8cb07..25a66487a1 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -274,7 +274,9 @@ mod tests { use dpp::identity::signer::Signer; use dpp::platform_value::Bytes32; use dpp::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; - use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; + use dpp::state_transition::data_contract_update_transition::{ + DataContractUpdateTransition, DataContractUpdateTransitionV0, + }; use dpp::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0; use dpp::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters; use dpp::system_data_contracts::SystemDataContract::Dashpay; @@ -1384,7 +1386,8 @@ mod tests { let dashpay_created_contract = get_dashpay_contract_fixture(Some(identity.id()), 1, protocol_version); - let mut modified_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let original_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let mut modified_dashpay_contract = original_dashpay_contract.clone(); let mut create_contract_state_transition: StateTransition = dashpay_created_contract .try_into_platform_versioned(platform_version) .expect("expected a state transition"); @@ -1451,8 +1454,10 @@ mod tests { ); let mut update_contract_state_transition: StateTransition = - DataContractUpdateTransition::try_from_platform_versioned( - (modified_dashpay_contract, 2), + DataContractUpdateTransition::from_contract_update( + &original_dashpay_contract, + &modified_dashpay_contract, + 2, platform_version, ) .expect("expected a state transition") @@ -1594,7 +1599,8 @@ mod tests { let dashpay_created_contract = get_dashpay_contract_fixture(Some(identity.id()), 1, protocol_version); - let mut modified_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let original_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let mut modified_dashpay_contract = original_dashpay_contract.clone(); let mut create_contract_state_transition: StateTransition = dashpay_created_contract .try_into_platform_versioned(platform_version) .expect("expected a state transition"); @@ -1664,8 +1670,10 @@ mod tests { ); let mut update_contract_state_transition: StateTransition = - DataContractUpdateTransition::try_from_platform_versioned( - (modified_dashpay_contract, 2), + DataContractUpdateTransition::from_contract_update( + &original_dashpay_contract, + &modified_dashpay_contract, + 2, platform_version, ) .expect("expected a state transition") @@ -1720,7 +1728,223 @@ mod tests { assert_eq!( update_processing_result.aggregated_fees().processing_fee, - 27002504030 + 27002430530 // V1 transitions have slightly different fees + ); + + let check_result = platform + .check_tx( + serialized_update.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(check_result.is_valid()); // it should still be valid, because we didn't commit the transaction + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + let check_result = platform + .check_tx( + serialized_update.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(!check_result.is_valid()); // it should no longer be valid, because of the nonce check + + assert!(matches!( + check_result.errors.first().expect("expected an error"), + ConsensusError::StateError(StateError::InvalidIdentityNonceError(_)) + )); + } + + #[test] + fn data_contract_update_check_tx_protocol_version_11() { + let platform_config = PlatformConfig { + testing_configs: PlatformTestConfig { + disable_instant_lock_signature_verification: true, + ..Default::default() + }, + ..Default::default() + }; + + let platform = TestPlatformBuilder::new() + .with_config(platform_config) + .with_initial_protocol_version(11) + .build_with_mock_rpc(); + + let platform_state = platform.state.load(); + let protocol_version = platform_state.current_protocol_version_in_consensus(); + let platform_version = PlatformVersion::get(protocol_version).unwrap(); + + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &platform_state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + + let (key, private_key) = IdentityPublicKey::random_ecdsa_critical_level_authentication_key( + 1, + Some(1), + platform_version, + ) + .expect("expected to get key pair"); + + platform + .drive + .create_initial_state_structure(None, platform_version) + .expect("expected to create state structure"); + let identity: Identity = IdentityV0 { + id: Identifier::new([ + 158, 113, 180, 126, 91, 83, 62, 44, 83, 54, 97, 88, 240, 215, 84, 139, 167, 156, + 166, 203, 222, 4, 64, 31, 215, 199, 149, 151, 190, 246, 251, 44, + ]), + public_keys: BTreeMap::from([(1, key.clone())]), + balance: 100_000_000_000, // 1.0 Dash + revision: 0, + } + .into(); + + let dashpay_created_contract = + get_dashpay_contract_fixture(Some(identity.id()), 1, protocol_version); + let original_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let mut modified_dashpay_contract = original_dashpay_contract.clone(); + let mut create_contract_state_transition: StateTransition = dashpay_created_contract + .try_into_platform_versioned(platform_version) + .expect("expected a state transition"); + create_contract_state_transition + .sign(&key, private_key.as_slice(), &NativeBlsModule) + .expect("expected to sign transition"); + let serialized = create_contract_state_transition + .serialize_to_bytes() + .expect("serialized state transition"); + platform + .drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to insert identity"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + std::slice::from_ref(&serialized), + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_eq!( + processing_result.aggregated_fees().processing_fee, + 24002489210 + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + // Now let's do the data contract update + let _dashpay_id = modified_dashpay_contract.id(); + + modified_dashpay_contract.set_version(2); + + let document_types = modified_dashpay_contract.document_types_mut(); + + let dpns_contract = + get_dpns_data_contract_fixture(Some(identity.id()), 1, protocol_version) + .data_contract_owned(); + + document_types.insert( + "preorder".to_string(), + dpns_contract + .document_type_for_name("preorder") + .expect("expected document type") + .to_owned_document_type(), + ); + + let mut update_contract_state_transition: StateTransition = + DataContractUpdateTransition::from_contract_update( + &original_dashpay_contract, + &modified_dashpay_contract, + 2, + platform_version, + ) + .expect("expected a state transition") + .into(); + + update_contract_state_transition + .sign(&key, private_key.as_slice(), &NativeBlsModule) + .expect("expected to sign transition"); + let serialized_update = update_contract_state_transition + .serialize_to_bytes() + .expect("serialized state transition"); + + let validation_result = platform + .check_tx( + serialized_update.as_slice(), + FirstTimeCheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert_eq!(validation_result.errors.as_slice(), &[]); + + let check_result = platform + .check_tx( + serialized_update.as_slice(), + Recheck, + &platform_ref, + platform_version, + ) + .expect("expected to check tx"); + + assert!(check_result.is_valid()); + + let transaction = platform.drive.grove.start_transaction(); + + let update_processing_result = platform + .platform + .process_raw_state_transitions( + std::slice::from_ref(&serialized_update), + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // We have one valid state transition + assert_eq!(update_processing_result.valid_count(), 1); + + assert_eq!( + update_processing_result.aggregated_fees().processing_fee, + 27002504030 // V0 transitions used in protocol version 11 ); let check_result = platform @@ -1808,7 +2032,8 @@ mod tests { let dashpay_created_contract = get_dashpay_contract_fixture(Some(identity.id()), 1, protocol_version); - let mut modified_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let original_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let mut modified_dashpay_contract = original_dashpay_contract.clone(); let mut create_contract_state_transition: StateTransition = dashpay_created_contract .try_into_platform_versioned(platform_version) .expect("expected a state transition"); @@ -1858,6 +2083,8 @@ mod tests { let dashpay_id = modified_dashpay_contract.id(); // we need to alter dashpay to make it invalid + modified_dashpay_contract.set_version(2); + let document_types = modified_dashpay_contract.document_types_mut(); let parameters = RandomDocumentTypeParameters { @@ -1913,8 +2140,10 @@ mod tests { ); let mut update_contract_state_transition: StateTransition = - DataContractUpdateTransition::try_from_platform_versioned( - (modified_dashpay_contract, 2), + DataContractUpdateTransition::from_contract_update( + &original_dashpay_contract, + &modified_dashpay_contract, + 2, platform_version, ) .expect("expected a state transition") @@ -2053,7 +2282,8 @@ mod tests { let dashpay_created_contract = get_dashpay_contract_fixture(Some(identity.id()), 1, protocol_version); - let mut modified_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let original_dashpay_contract = dashpay_created_contract.data_contract().clone(); + let mut modified_dashpay_contract = original_dashpay_contract.clone(); let mut create_contract_state_transition: StateTransition = dashpay_created_contract .try_into_platform_versioned(platform_version) .expect("expected a state transition"); @@ -2106,6 +2336,8 @@ mod tests { let dashpay_id = modified_dashpay_contract.id(); // we need to alter dashpay to make it invalid + modified_dashpay_contract.set_version(2); + let document_types = modified_dashpay_contract.document_types_mut(); let parameters = RandomDocumentTypeParameters { @@ -2161,8 +2393,10 @@ mod tests { ); let mut update_contract_state_transition: StateTransition = - DataContractUpdateTransition::try_from_platform_versioned( - (modified_dashpay_contract, 2), + DataContractUpdateTransition::from_contract_update( + &original_dashpay_contract, + &modified_dashpay_contract, + 2, platform_version, ) .expect("expected a state transition") diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs index 8decf78865..7baf753713 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs @@ -1,18 +1,12 @@ use crate::error::Error; use dpp::consensus::basic::data_contract::{ DuplicateKeywordsError, InvalidDataContractVersionError, InvalidDescriptionLengthError, - InvalidKeywordCharacterError, InvalidKeywordLengthError, InvalidTokenBaseSupplyError, - NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractTokenPositionsError, - TooManyKeywordsError, + InvalidKeywordCharacterError, InvalidKeywordLengthError, }; use dpp::consensus::basic::BasicError; use dpp::consensus::ConsensusError; use dpp::dashcore::Network; -use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; -use dpp::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Accessors; -use dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; -use dpp::data_contract::{TokenContractPosition, INITIAL_DATA_CONTRACT_VERSION}; +use dpp::data_contract::INITIAL_DATA_CONTRACT_VERSION; use dpp::prelude::DataContract; use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; @@ -47,105 +41,45 @@ impl DataContractCreateStateTransitionBasicStructureValidationV0 for DataContrac let groups = self.data_contract().groups(); if !groups.is_empty() { - let validation_result = DataContract::validate_groups(groups, platform_version)?; + let validation_result = DataContract::validate_groups(groups, false, platform_version)?; if !validation_result.is_valid() { return Ok(validation_result); } } - for (expected_position, (token_contract_position, token_configuration)) in - self.data_contract().tokens().iter().enumerate() - { - if expected_position as TokenContractPosition != *token_contract_position { - return Ok(SimpleConsensusValidationResult::new_with_error( - NonContiguousContractTokenPositionsError::new( - expected_position as TokenContractPosition, - *token_contract_position, - ) - .into(), - )); - } - - if token_configuration.base_supply() > i64::MAX as u64 { - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidTokenBaseSupplyError::new(token_configuration.base_supply()).into(), - )); - } - - let validation_result = token_configuration - .conventions() - .validate_localizations(platform_version)?; - if !validation_result.is_valid() { - return Ok(validation_result); - } - - let validation_result = token_configuration.validate_token_config_groups_exist( - self.data_contract().groups(), + let tokens = self.data_contract().tokens(); + if !tokens.is_empty() { + // Validate token structure (positions, base supply, etc.) + let validation_result = DataContract::validate_tokens( + self.data_contract().id(), + tokens, + false, + network_type, platform_version, )?; + if !validation_result.is_valid() { return Ok(validation_result); } - if let Some(perpetual_distribution) = token_configuration - .distribution_rules() - .perpetual_distribution() - { - // we validate the interval (that it's more than one hour or over 100 blocks) - // also that if it is time based we are using minute intervals - let validation_result = perpetual_distribution - .distribution_type() - .validate_structure_interval(network_type, platform_version)?; - - if !validation_result.is_valid() { - return Ok(validation_result); - } - - // We use 0 as the start moment to show that we are starting now with no offset - let validation_result = perpetual_distribution - .distribution_type() - .function() - .validate(0, platform_version)?; - + // Validate token config groups exist + for (_, token_configuration) in tokens.iter() { + let validation_result = token_configuration.validate_token_config_groups_exist( + self.data_contract().groups(), + platform_version, + )?; if !validation_result.is_valid() { return Ok(validation_result); } } - - if token_configuration - .distribution_rules() - .new_tokens_destination_identity() - .is_none() - && !token_configuration - .distribution_rules() - .minting_allow_choosing_destination() - && !(token_configuration - .distribution_rules() - .minting_allow_choosing_destination_rules() - .authorized_to_make_change_action_takers() - == &AuthorizedActionTakers::NoOne - && token_configuration - .distribution_rules() - .minting_allow_choosing_destination_rules() - .admin_action_takers() - == &AuthorizedActionTakers::NoOne) - { - return Ok(SimpleConsensusValidationResult::new_with_error( - NewTokensDestinationIdentityOptionRequiredError::new( - self.data_contract().id(), - *token_contract_position, - ) - .into(), - )); - } } // Validate there are no more than 50 keywords if self.data_contract().keywords().len() > 50 { return Ok(SimpleConsensusValidationResult::new_with_error( ConsensusError::BasicError(BasicError::TooManyKeywordsError( - TooManyKeywordsError::new( + dpp::consensus::basic::data_contract::TooManyKeywordsError::new( self.data_contract().id(), self.data_contract().keywords().len() as u8, ), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v0/mod.rs index dc72d28a48..7113fde672 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v0/mod.rs @@ -398,7 +398,7 @@ impl DataContractCreateStateTransitionStateValidationV0 for DataContractCreateTr // The transformation of the state transition into the state transition action will transform // The contract in serialized form into it's execution form - let result = DataContractCreateTransitionAction::try_from_borrowed_transition( + let result = DataContractCreateTransitionAction::try_from_transition( self, block_info, validation_mode.should_fully_validate_contract_on_transform_into_action(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs index 9a1925de7f..008be12cc6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs index 2cdfedb27b..954d75a1db 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs @@ -1,16 +1,6 @@ use crate::error::Error; -use dpp::consensus::basic::data_contract::{ - InvalidTokenBaseSupplyError, NewTokensDestinationIdentityOptionRequiredError, - NonContiguousContractTokenPositionsError, -}; use dpp::dashcore::Network; -use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; -use dpp::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Accessors; -use dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; -use dpp::data_contract::TokenContractPosition; use dpp::prelude::DataContract; -use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; @@ -30,99 +20,50 @@ impl DataContractUpdateStateTransitionBasicStructureValidationV0 for DataContrac network_type: Network, platform_version: &PlatformVersion, ) -> Result { - let groups = self.data_contract().groups(); - if !groups.is_empty() { - let validation_result = DataContract::validate_groups(groups, platform_version)?; - - if !validation_result.is_valid() { - return Ok(validation_result); - } - } - - for (expected_position, (token_contract_position, token_configuration)) in - self.data_contract().tokens().iter().enumerate() - { - if expected_position as TokenContractPosition != *token_contract_position { - return Ok(SimpleConsensusValidationResult::new_with_error( - NonContiguousContractTokenPositionsError::new( - expected_position as TokenContractPosition, - *token_contract_position, - ) - .into(), - )); + // V0 basic structure validation only applies to V0 transitions + // V1 transitions should use validate_basic_structure_v1 + let v0 = match self { + DataContractUpdateTransition::V0(v0) => v0, + DataContractUpdateTransition::V1(_) => { + // V1 transitions don't have a full data contract embedded, + // so this validation doesn't apply + return Ok(SimpleConsensusValidationResult::new()); } + }; - if token_configuration.base_supply() > i64::MAX as u64 { - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidTokenBaseSupplyError::new(token_configuration.base_supply()).into(), - )); - } + let groups = v0.data_contract.groups(); + if !groups.is_empty() { + let validation_result = DataContract::validate_groups(groups, false, platform_version)?; - let validation_result = token_configuration - .conventions() - .validate_localizations(platform_version)?; if !validation_result.is_valid() { return Ok(validation_result); } + } - let validation_result = token_configuration.validate_token_config_groups_exist( - self.data_contract().groups(), + let tokens = v0.data_contract.tokens(); + if !tokens.is_empty() { + // Validate token structure (positions, base supply, etc.) + let validation_result = DataContract::validate_tokens( + v0.data_contract.id(), + tokens, + false, + network_type, platform_version, )?; + if !validation_result.is_valid() { return Ok(validation_result); } - if let Some(perpetual_distribution) = token_configuration - .distribution_rules() - .perpetual_distribution() - { - // we validate the interval (that it's more than one hour or over 100 blocks) - // also that if it is time based we are using minute intervals - let validation_result = perpetual_distribution - .distribution_type() - .validate_structure_interval(network_type, platform_version)?; - + // V0 has full contract, so validate token config groups exist here + for (_, token_configuration) in tokens.iter() { + let validation_result = token_configuration.validate_token_config_groups_exist( + v0.data_contract.groups(), + platform_version, + )?; if !validation_result.is_valid() { return Ok(validation_result); } - - // We use 0 as the start moment to show that we are starting now with no offset - let validation_result = perpetual_distribution - .distribution_type() - .function() - .validate(0, platform_version)?; - - if !validation_result.is_valid() { - return Ok(validation_result); - } - } - - if token_configuration - .distribution_rules() - .new_tokens_destination_identity() - .is_none() - && !token_configuration - .distribution_rules() - .minting_allow_choosing_destination() - && !(token_configuration - .distribution_rules() - .minting_allow_choosing_destination_rules() - .authorized_to_make_change_action_takers() - == &AuthorizedActionTakers::NoOne - && token_configuration - .distribution_rules() - .minting_allow_choosing_destination_rules() - .admin_action_takers() - == &AuthorizedActionTakers::NoOne) - { - return Ok(SimpleConsensusValidationResult::new_with_error( - NewTokensDestinationIdentityOptionRequiredError::new( - self.data_contract().id(), - *token_contract_position, - ) - .into(), - )); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v1/mod.rs new file mode 100644 index 0000000000..46e31da330 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v1/mod.rs @@ -0,0 +1,131 @@ +use crate::error::Error; +use crate::execution::validation::state_transition::state_transitions::data_contract_update::basic_structure::v0::DataContractUpdateStateTransitionBasicStructureValidationV0; +use dpp::consensus::basic::data_contract::{ + DataContractUpdateTransitionConflictingKeywordError, + DataContractUpdateTransitionOverlappingFieldsError, InvalidDescriptionLengthError, +}; +use dpp::dashcore::Network; +use dpp::prelude::DataContract; +use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; + +pub(in crate::execution::validation::state_transition::state_transitions::data_contract_update) trait DataContractUpdateStateTransitionBasicStructureValidationV1 +{ + fn validate_basic_structure_v1( + &self, + network_type: Network, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DataContractUpdateStateTransitionBasicStructureValidationV1 for DataContractUpdateTransition { + fn validate_basic_structure_v1( + &self, + network_type: Network, + platform_version: &PlatformVersion, + ) -> Result { + // V0 transitions use V0 validation + match self { + DataContractUpdateTransition::V0(_) => { + self.validate_basic_structure_v0(network_type, platform_version) + } + DataContractUpdateTransition::V1(v1) => { + // Validate that updated_schema_defs and new_schema_defs don't overlap + for key in v1.updated_schema_defs.keys() { + if v1.new_schema_defs.contains_key(key) { + return Ok(SimpleConsensusValidationResult::new_with_error( + DataContractUpdateTransitionOverlappingFieldsError::new( + v1.id, + "schema_defs".to_string(), + key.clone(), + ) + .into(), + )); + } + } + + // Validate that updated_document_schemas and new_document_schemas don't overlap + for key in v1.updated_document_schemas.keys() { + if v1.new_document_schemas.contains_key(key) { + return Ok(SimpleConsensusValidationResult::new_with_error( + DataContractUpdateTransitionOverlappingFieldsError::new( + v1.id, + "document_schemas".to_string(), + key.clone(), + ) + .into(), + )); + } + } + + // Validate that add_keywords and remove_keywords don't overlap + for keyword in &v1.add_keywords { + if v1.remove_keywords.contains(keyword) { + return Ok(SimpleConsensusValidationResult::new_with_error( + DataContractUpdateTransitionConflictingKeywordError::new( + v1.id, + keyword.clone(), + ) + .into(), + )); + } + } + + // Validate new groups (allow offset start for updates) + if !v1.new_groups.is_empty() { + let validation_result = + DataContract::validate_groups(&v1.new_groups, true, platform_version)?; + + if !validation_result.is_valid() { + return Ok(validation_result); + } + } + + // Validate new tokens (allow offset start for updates) + // Note: validate_token_config_groups_exist is done in apply_update + // where we have access to all groups (existing + new) + if !v1.new_tokens.is_empty() { + let validation_result = DataContract::validate_tokens( + v1.id, + &v1.new_tokens, + true, + network_type, + platform_version, + )?; + + if !validation_result.is_valid() { + return Ok(validation_result); + } + } + + // Validate added keywords structure + // Note: Full keyword validation (including combined keywords) is done in apply_update + if !v1.add_keywords.is_empty() { + let validation_result = DataContract::validate_keywords( + v1.id, + &v1.add_keywords, + true, + platform_version, + )?; + + if !validation_result.is_valid() { + return Ok(validation_result); + } + } + + // Validate description if being updated + if let Some(Some(description)) = &v1.update_description { + let char_count = description.chars().count(); + if !(3..=100).contains(&char_count) { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDescriptionLengthError::new(v1.id, description.clone()).into(), + )); + } + } + + Ok(SimpleConsensusValidationResult::new()) + } + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/identity_contract_nonce/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/identity_contract_nonce/v0/mod.rs index 2f012f5a0c..39cdb16c38 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/identity_contract_nonce/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/identity_contract_nonce/v0/mod.rs @@ -48,8 +48,14 @@ impl DataContractUpdateStateTransitionIdentityContractNonceV0 for DataContractUp )); } - let identity_id = self.data_contract().owner_id(); - let contract_id = self.data_contract().id(); + // Get identity_id and contract_id based on transition version + let (identity_id, contract_id) = match self { + DataContractUpdateTransition::V0(v0) => { + (v0.data_contract.owner_id(), v0.data_contract.id()) + } + DataContractUpdateTransition::V1(v1) => (v1.owner_id, v1.id), + }; + let (existing_nonce, fee) = platform.drive.fetch_identity_contract_nonce_with_fees( identity_id.to_buffer(), contract_id.to_buffer(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs index 5de155b061..cdf58279d2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs @@ -1,8 +1,11 @@ mod basic_structure; mod identity_contract_nonce; mod state; +#[cfg(test)] +mod tests; use basic_structure::v0::DataContractUpdateStateTransitionBasicStructureValidationV0; +use basic_structure::v1::DataContractUpdateStateTransitionBasicStructureValidationV1; use dpp::address_funds::PlatformAddress; use dpp::block::block_info::BlockInfo; use dpp::dashcore::Network; @@ -24,6 +27,7 @@ use crate::execution::validation::state_transition::processor::basic_structure:: use drive::state_transition_action::StateTransitionAction; use crate::execution::validation::state_transition::data_contract_update::state::v0::DataContractUpdateStateTransitionStateValidationV0; +use crate::execution::validation::state_transition::data_contract_update::state::v1::DataContractUpdateStateTransitionStateValidationV1; use crate::execution::validation::state_transition::transformer::StateTransitionActionTransformer; use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformRef; @@ -44,14 +48,15 @@ impl StateTransitionBasicStructureValidationV0 for DataContractUpdateTransition .basic_structure { Some(0) => self.validate_basic_structure_v0(network_type, platform_version), + Some(1) => self.validate_basic_structure_v1(network_type, platform_version), Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "data contract update transition: validate_basic_structure".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), None => Err(Error::Execution(ExecutionError::VersionNotActive { method: "data contract update transition: validate_basic_structure".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], })), } } @@ -67,7 +72,7 @@ impl StateTransitionActionTransformer for DataContractUpdateTransition { >, validation_mode: ValidationMode, execution_context: &mut StateTransitionExecutionContext, - _tx: TransactionArg, + tx: TransactionArg, ) -> Result, Error> { let platform_version = platform.state.current_platform_version()?; @@ -84,2899 +89,30 @@ impl StateTransitionActionTransformer for DataContractUpdateTransition { execution_context, platform_version, ), - version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { - method: "data contract update transition: transform_into_action".to_string(), - known_versions: vec![0], - received: version, - })), - } - } -} - -#[cfg(test)] -mod tests { - use crate::config::{ExecutionConfig, PlatformConfig, PlatformTestConfig, ValidatorSetConfig}; - use crate::platform_types::platform::PlatformRef; - use crate::rpc::core::MockCoreRPCLike; - use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; - use dpp::block::block_info::BlockInfo; - use dpp::consensus::state::state_error::StateError; - use dpp::consensus::ConsensusError; - use dpp::dash_to_credits; - use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; - use rand::prelude::StdRng; - use rand::SeedableRng; - use std::collections::BTreeMap; - - use dpp::data_contract::DataContract; - use dpp::fee::Credits; - use dpp::identifier::Identifier; - use dpp::identity::accessors::IdentityGettersV0; - use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; - use dpp::identity::{Identity, IdentityPublicKey, IdentityV0}; - use dpp::platform_value::BinaryData; - use dpp::serialization::PlatformSerializable; - use dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; - use dpp::state_transition::data_contract_update_transition::{ - DataContractUpdateTransition, DataContractUpdateTransitionV0, - }; - - use crate::platform_types::platform_state::PlatformStateV0Methods; - use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; - use assert_matches::assert_matches; - use dpp::consensus::basic::BasicError; - use dpp::data_contract::accessors::v1::DataContractV1Getters; - use dpp::data_contract::group::v0::GroupV0; - use dpp::data_contract::group::Group; - use dpp::tests::fixtures::get_data_contract_fixture; - use dpp::tests::json_document::json_document_to_contract; - use dpp::version::PlatformVersion; - use drive::util::storage_flags::StorageFlags; - use simple_signer::signer::SimpleSigner; - - struct TestData { - data_contract: DataContract, - platform: TempPlatform, - } - - fn setup_identity( - platform: &mut TempPlatform, - seed: u64, - credits: Credits, - ) -> (Identity, SimpleSigner, IdentityPublicKey) { - let platform_version = PlatformVersion::latest(); - let mut signer = SimpleSigner::default(); - - let mut rng = StdRng::seed_from_u64(seed); - - let (master_key, master_private_key) = - IdentityPublicKey::random_ecdsa_master_authentication_key_with_rng( - 0, - &mut rng, - platform_version, - ) - .expect("expected to get key pair"); - - signer.add_identity_public_key(master_key.clone(), master_private_key); - - let (critical_public_key, private_key) = - IdentityPublicKey::random_ecdsa_critical_level_authentication_key_with_rng( - 1, - &mut rng, - platform_version, - ) - .expect("expected to get key pair"); - - signer.add_identity_public_key(critical_public_key.clone(), private_key); - - let identity: Identity = IdentityV0 { - id: Identifier::random_with_rng(&mut rng), - public_keys: BTreeMap::from([ - (0, master_key.clone()), - (1, critical_public_key.clone()), - ]), - balance: credits, - revision: 0, - } - .into(); - - // We just add this identity to the system first - - platform - .drive - .add_new_identity( - identity.clone(), - false, - &BlockInfo::default(), - true, - None, - platform_version, - ) - .expect("expected to add a new identity"); - - (identity, signer, critical_public_key) - } - - fn apply_contract( - platform: &TempPlatform, - data_contract: &DataContract, - block_info: BlockInfo, - ) { - let platform_version = PlatformVersion::latest(); - platform - .drive - .apply_contract( - data_contract, - block_info, - true, - None, - None, - platform_version, - ) - .expect("to apply contract"); - } - - fn setup_test() -> TestData { - let platform_version = PlatformVersion::latest(); - let data_contract = get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - - let config = PlatformConfig { - validator_set: ValidatorSetConfig { - quorum_size: 10, - ..Default::default() - }, - execution: ExecutionConfig { - verify_sum_trees: true, - ..Default::default() - }, - block_spacing_ms: 300, - testing_configs: PlatformTestConfig::default_minimal_verifications(), - ..Default::default() - }; - let platform = TestPlatformBuilder::new() - .with_config(config) - .build_with_mock_rpc(); - - TestData { - data_contract, - platform: platform.set_initial_state_structure(), - } - } - - mod validate_state { - use super::*; - use serde_json::json; - - use dpp::assert_state_consensus_errors; - use dpp::consensus::state::state_error::StateError; - use dpp::consensus::state::state_error::StateError::DataContractIsReadonlyError; - use dpp::errors::consensus::ConsensusError; - - use crate::execution::validation::state_transition::processor::traits::state::StateTransitionStateValidation; - use dpp::block::block_info::BlockInfo; - use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; - - use dpp::data_contract::config::v0::DataContractConfigSettersV0; - use dpp::data_contract::schema::DataContractSchemaMethodsV0; - - use dpp::data_contract::serialized_version::DataContractInSerializationFormat; - use dpp::platform_value::platform_value; - use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; - - use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; - use crate::execution::validation::state_transition::ValidationMode; - use dpp::version::TryFromPlatformVersioned; - use platform_version::{DefaultForPlatformVersion, TryIntoPlatformVersioned}; - - #[test] - pub fn should_return_error_if_trying_to_update_document_schema_in_a_readonly_contract() { - let platform_version = PlatformVersion::latest(); - let TestData { - mut data_contract, - platform, - } = setup_test(); - - data_contract.config_mut().set_readonly(true); - apply_contract(&platform, &data_contract, Default::default()); - - let updated_document = platform_value!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "position": 0 - }, - "newProp": { - "type": "integer", - "minimum": 0, - "position": 1 - } - }, - "required": [ - "$createdAt" - ], - "additionalProperties": false - }); - - data_contract.increment_version(); - data_contract - .set_document_schema( - "niceDocument", - updated_document, - true, - &mut vec![], - platform_version, - ) - .expect("to be able to set document schema"); - - let state_transition = DataContractUpdateTransitionV0 { - identity_contract_nonce: 1, - data_contract: DataContractInSerializationFormat::try_from_platform_versioned( - data_contract, - platform_version, - ) - .expect("to be able to convert data contract to serialization format"), - user_fee_increase: 0, - signature: BinaryData::new(vec![0; 65]), - signature_public_key_id: 0, - }; - - let state = platform.state.load(); - - let platform_ref = PlatformRef { - drive: &platform.drive, - state: &state, - config: &platform.config, - core_rpc: &platform.core_rpc, - }; - - let mut execution_context = - StateTransitionExecutionContext::default_for_platform_version(platform_version) - .expect("expected a platform version"); - - let result = DataContractUpdateTransition::V0(state_transition) - .validate_state( - None, - &platform_ref, - ValidationMode::Validator, - &BlockInfo::default(), - &mut execution_context, - None, - ) - .expect("state transition to be validated"); - - assert!(!result.is_valid()); - assert_state_consensus_errors!(result, DataContractIsReadonlyError, 1); - } - - #[test] - pub fn should_keep_history_if_contract_config_keeps_history_is_true() { - let TestData { - mut data_contract, - platform, - } = setup_test(); - - let platform_version = PlatformVersion::latest(); - - data_contract.config_mut().set_keeps_history(true); - data_contract.config_mut().set_readonly(false); - - apply_contract( - &platform, - &data_contract, - BlockInfo { - time_ms: 1000, - height: 100, - core_height: 10, - epoch: Default::default(), - }, - ); - - let updated_document = platform_value!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "position": 0 - }, - "newProp": { - "type": "integer", - "minimum": 0, - "position": 1 - } - }, - "required": [ - "$createdAt" - ], - "additionalProperties": false - }); - - data_contract.increment_version(); - data_contract - .set_document_schema( - "niceDocument", - updated_document, - true, - &mut vec![], - platform_version, - ) - .expect("to be able to set document schema"); - - // TODO: add a data contract stop transition - let state_transition = DataContractUpdateTransitionV0 { - identity_contract_nonce: 1, - data_contract: DataContractInSerializationFormat::try_from_platform_versioned( - data_contract.clone(), - platform_version, - ) - .expect("to be able to convert data contract to serialization format"), - user_fee_increase: 0, - signature: BinaryData::new(vec![0; 65]), - signature_public_key_id: 0, - }; - - let state = platform.state.load(); - - let platform_ref = PlatformRef { - drive: &platform.drive, - state: &state, - config: &platform.config, - core_rpc: &platform.core_rpc, - }; - - let mut execution_context = - StateTransitionExecutionContext::default_for_platform_version(platform_version) - .expect("expected a platform version"); - - let result = DataContractUpdateTransition::V0(state_transition) - .validate_state( - None, - &platform_ref, - ValidationMode::Validator, - &BlockInfo::default(), - &mut execution_context, - None, - ) - .expect("state transition to be validated"); - - assert!(result.is_valid()); - - // This should store update and history - apply_contract( - &platform, - &data_contract, - BlockInfo { - time_ms: 2000, - height: 110, - core_height: 11, - epoch: Default::default(), - }, - ); - - // Fetch from time 0 without a limit or offset - let contract_history = platform - .drive - .fetch_contract_with_history( - *data_contract.id().as_bytes(), - None, - 0, - None, - None, - platform_version, - ) - .expect("to get contract history"); - - let keys = contract_history.keys().copied().collect::>(); - - // Check that keys sorted from oldest to newest - assert_eq!(contract_history.len(), 2); - assert_eq!(keys[0], 1000); - assert_eq!(keys[1], 2000); - - // Fetch with an offset should offset from the newest to oldest - let contract_history = platform - .drive - .fetch_contract_with_history( - *data_contract.id().as_bytes(), - None, - 0, - None, - Some(1), - platform_version, - ) - .expect("to get contract history"); - - let keys = contract_history.keys().copied().collect::>(); - - assert_eq!(contract_history.len(), 1); - assert_eq!(keys[0], 1000); - - // Check that when we limit ny 1 we get only the most recent contract - let contract_history = platform - .drive - .fetch_contract_with_history( - *data_contract.id().as_bytes(), - None, - 0, - Some(1), - None, - platform_version, - ) - .expect("to get contract history"); - - let keys = contract_history.keys().copied().collect::>(); - - // Check that when we limit ny 1 we get only the most recent contract - assert_eq!(contract_history.len(), 1); - assert_eq!(keys[0], 2000); - } - - #[test] - fn should_return_invalid_result_if_trying_to_update_config() { - let TestData { - mut data_contract, - platform, - } = setup_test(); - - let platform_version = PlatformVersion::latest(); - - data_contract.config_mut().set_keeps_history(true); - data_contract.config_mut().set_readonly(false); - - apply_contract( - &platform, - &data_contract, - BlockInfo { - time_ms: 1000, - height: 100, - core_height: 10, - epoch: Default::default(), - }, - ); - - let updated_document_type = json!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "position": 0, - }, - "newProp": { - "type": "integer", - "minimum": 0, - "position": 1, - } - }, - "required": [ - "$createdAt" - ], - "additionalProperties": false - }); - - data_contract.increment_version(); - data_contract - .set_document_schema( - "niceDocument", - updated_document_type.into(), - true, - &mut vec![], - platform_version, - ) - .expect("to be able to set document schema"); - - // It should be not possible to modify this - data_contract.config_mut().set_keeps_history(false); - - let state_transition: DataContractUpdateTransitionV0 = (data_contract, 1) - .try_into_platform_versioned(platform_version) - .expect("expected an update transition"); - - let state_transition: DataContractUpdateTransition = state_transition.into(); - - let state = platform.state.load(); - - let platform_ref = PlatformRef { - drive: &platform.drive, - state: &state, - config: &platform.config, - core_rpc: &platform.core_rpc, - }; - - let mut execution_context = - StateTransitionExecutionContext::default_for_platform_version(platform_version) - .expect("expected a platform version"); - - let result = state_transition - .validate_state( - None, - &platform_ref, - ValidationMode::Validator, - &BlockInfo::default(), - &mut execution_context, - None, - ) - .expect("state transition to be validated"); - - assert!(!result.is_valid()); - let errors = assert_state_consensus_errors!( - result, - StateError::DataContractConfigUpdateError, - 1 - ); - let error = errors.first().expect("to have an error"); - assert_eq!( - error.additional_message(), - "contract can not change whether it keeps history: changing from true to false" - ); - } - } - - #[test] - fn test_data_contract_update_changing_various_document_type_options() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let card_game_path = "tests/supporting_files/contract/crypto-card-game/crypto-card-game-direct-purchase-creation-restricted-to-owner.json"; - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // let's construct the grovedb structure for the card game data contract - let mut contract = json_document_to_contract(card_game_path, true, platform_version) - .expect("expected to get data contract"); - - contract.set_owner_id(identity.id()); - - platform - .drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - let updated_card_game_path = "tests/supporting_files/contract/crypto-card-game/crypto-card-game-direct-purchase.json"; - - // let's construct the grovedb structure for the card game data contract - let mut contract_not_restricted_to_owner = - json_document_to_contract(updated_card_game_path, true, platform_version) - .expect("expected to get data contract"); - - contract_not_restricted_to_owner.set_owner_id(identity.id()); - - contract_not_restricted_to_owner.set_version(2); - - let data_contract_update_transition = DataContractUpdateTransition::new_from_data_contract( - contract_not_restricted_to_owner, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create documents batch transition"); - - let data_contract_update_serialized_transition = data_contract_update_transition - .serialize_to_bytes() - .expect("expected documents batch serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - - let processing_result = platform - .platform - .process_raw_state_transitions( - &[data_contract_update_serialized_transition.clone()], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - // There is no issue because the creator of the contract made the document - - assert_eq!(processing_result.invalid_paid_count(), 1); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - - let result = processing_result.into_execution_results().remove(0); - - assert!(matches!( - result, - StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::StateError( - StateError::DocumentTypeUpdateError(ref error) - ), .. - } if error.data_contract_id() == &contract.id() - && error.document_type_name() == "card" - && error.additional_message() == "document type can not change creation restriction mode: changing from Owner Only to No Restrictions" - )); - } - - mod group_tests { - use super::*; - use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::UnpaidConsensusError; - - #[test] - fn test_data_contract_update_can_not_remove_groups() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let (identity_2, _, _) = setup_identity(&mut platform, 123, dash_to_credits!(1.0)); - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // Create an initial data contract with groups - let mut data_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - - data_contract.set_owner_id(identity.id()); - - { - // Add groups to the contract - let groups = data_contract.groups_mut().expect("expected groups"); - groups.insert( - 0, - Group::V0(GroupV0 { - members: [(identity.id(), 1), (identity_2.id(), 1)].into(), - required_power: 1, - }), - ); - groups.insert( - 1, - Group::V0(GroupV0 { - members: [(identity.id(), 1), (identity_2.id(), 1)].into(), - required_power: 1, - }), - ); - } - - platform - .drive - .apply_contract( - &data_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - // Create an updated contract with one group removed - let mut updated_data_contract = data_contract.clone(); - updated_data_contract.set_version(2); - - { - // Remove a group from the updated contract - let groups = updated_data_contract.groups_mut().expect("expected groups"); - groups.remove(&1).expect("expected to remove group"); - } - - let data_contract_update_transition = - DataContractUpdateTransition::new_from_data_contract( - updated_data_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create data contract update transition"); - - let data_contract_update_serialized_transition = data_contract_update_transition - .serialize_to_bytes() - .expect("expected serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - - let processing_result = platform - .platform - .process_raw_state_transitions( - &[data_contract_update_serialized_transition.clone()], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - // Extract the error and check the message - if let [StateTransitionExecutionResult::PaidConsensusError { - error: - ConsensusError::StateError(StateError::DataContractUpdateActionNotAllowedError( - error, - )), - .. - }] = processing_result.execution_results().as_slice() - { - assert_eq!( - error.action(), - "remove group", - "expected error message to match 'remove group'" - ); - assert_eq!( - error.data_contract_id(), - data_contract.id(), - "expected the error to reference the correct data contract ID" - ); - } else { - panic!("Expected a DataContractUpdateActionNotAllowedError"); - } - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - } - - #[test] - fn test_data_contract_update_can_not_alter_group() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let (identity_2, _, _) = setup_identity(&mut platform, 123, dash_to_credits!(1.0)); - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // Create an initial data contract with groups - let mut data_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - - data_contract.set_owner_id(identity.id()); - - { - // Add groups to the contract - let groups = data_contract.groups_mut().expect("expected groups"); - groups.insert( - 0, - Group::V0(GroupV0 { - members: [(identity.id(), 1), (identity_2.id(), 1)].into(), - required_power: 1, - }), - ); - groups.insert( - 1, - Group::V0(GroupV0 { - members: [(identity.id(), 1), (identity_2.id(), 1)].into(), - required_power: 1, - }), - ); - } - - platform - .drive - .apply_contract( - &data_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - // Create an updated contract with one group removed - let mut updated_data_contract = data_contract.clone(); - updated_data_contract.set_version(2); - - { - // Add a group to the updated contract - let groups = updated_data_contract.groups_mut().expect("expected groups"); - groups.insert( - 1, - Group::V0(GroupV0 { - members: [(identity.id(), 1), (identity_2.id(), 1)].into(), - required_power: 2, - }), - ); - } - - let data_contract_update_transition = - DataContractUpdateTransition::new_from_data_contract( - updated_data_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create data contract update transition"); - - let data_contract_update_serialized_transition = data_contract_update_transition - .serialize_to_bytes() - .expect("expected serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - - let processing_result = platform - .platform - .process_raw_state_transitions( - &[data_contract_update_serialized_transition.clone()], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - // Extract the error and check the message - if let [StateTransitionExecutionResult::PaidConsensusError { - error: - ConsensusError::StateError(StateError::DataContractUpdateActionNotAllowedError( - error, - )), - .. - }] = processing_result.execution_results().as_slice() - { - assert_eq!( - error.action(), - "change group at position 1 is not allowed", - "expected error message to match 'change group at position 1 is not allowed'" - ); - assert_eq!( - error.data_contract_id(), - data_contract.id(), - "expected the error to reference the correct data contract ID" - ); - } else { - panic!("Expected a DataContractUpdateActionNotAllowedError"); - } - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - } - - #[test] - fn test_data_contract_update_can_not_add_new_group_with_gap() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // Create an initial data contract with groups - let mut data_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - - data_contract.set_owner_id(identity.id()); - - { - // Add groups to the contract - let groups = data_contract.groups_mut().expect("expected groups"); - groups.insert( - 0, - Group::V0(GroupV0 { - members: [(identity.id(), 1)].into(), - required_power: 1, - }), - ); - groups.insert( - 1, - Group::V0(GroupV0 { - members: [(identity.id(), 1)].into(), - required_power: 1, - }), - ); - } - - platform - .drive - .apply_contract( - &data_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - // Create an updated contract with one group removed - let mut updated_data_contract = data_contract.clone(); - updated_data_contract.set_version(2); - - { - // Remove a group from the updated contract - let groups = updated_data_contract.groups_mut().expect("expected groups"); - groups.insert( - 3, - Group::V0(GroupV0 { - members: [(identity.id(), 2)].into(), - required_power: 2, - }), - ); - } - - let data_contract_update_transition = - DataContractUpdateTransition::new_from_data_contract( - updated_data_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create data contract update transition"); - - let data_contract_update_serialized_transition = data_contract_update_transition - .serialize_to_bytes() - .expect("expected serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - - let processing_result = platform - .platform - .process_raw_state_transitions( - &[data_contract_update_serialized_transition.clone()], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - assert_matches!( - processing_result.execution_results().as_slice(), - [UnpaidConsensusError(ConsensusError::BasicError( - BasicError::NonContiguousContractGroupPositionsError(_) - ))] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - } - - #[test] - fn test_data_contract_update_can_add_new_group() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let (identity_2, _, _) = setup_identity(&mut platform, 928, dash_to_credits!(0.1)); - - let (identity_3, _, _) = setup_identity(&mut platform, 8, dash_to_credits!(0.1)); - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // Create an initial data contract with groups - let mut data_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - - data_contract.set_owner_id(identity.id()); - - { - // Add groups to the contract - let groups = data_contract.groups_mut().expect("expected groups"); - groups.insert( - 0, - Group::V0(GroupV0 { - members: [ - (identity.id(), 1), - (identity_2.id(), 1), - (identity_3.id(), 1), - ] - .into(), - required_power: 3, - }), - ); - groups.insert( - 1, - Group::V0(GroupV0 { - members: [ - (identity.id(), 1), - (identity_2.id(), 2), - (identity_3.id(), 1), - ] - .into(), - required_power: 3, - }), - ); - } - - platform - .drive - .apply_contract( - &data_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - // Create an updated contract with one group removed - let mut updated_data_contract = data_contract.clone(); - updated_data_contract.set_version(2); - - { - // Remove a group from the updated contract - let groups = updated_data_contract.groups_mut().expect("expected groups"); - groups.insert( - 2, - Group::V0(GroupV0 { - members: [ - (identity.id(), 1), - (identity_2.id(), 2), - (identity_3.id(), 2), - ] - .into(), - required_power: 3, - }), - ); - } - - let data_contract_update_transition = - DataContractUpdateTransition::new_from_data_contract( - updated_data_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create data contract update transition"); - - let data_contract_update_serialized_transition = data_contract_update_transition - .serialize_to_bytes() - .expect("expected serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - - let processing_result = platform - .platform - .process_raw_state_transitions( - &[data_contract_update_serialized_transition.clone()], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - } - } - - mod token_tests { - use super::*; - use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::UnpaidConsensusError; - use dpp::data_contract::accessors::v1::DataContractV1Setters; - use dpp::data_contract::associated_token::token_configuration::accessors::v0::{TokenConfigurationV0Getters, TokenConfigurationV0Setters}; - use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; - use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; - use dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; - use dpp::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; - use dpp::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; - use dpp::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0; - use dpp::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization; - use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; - use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; - use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; - use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; - use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; - use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; - use dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; - use dpp::data_contract::change_control_rules::ChangeControlRules; - use dpp::data_contract::change_control_rules::v0::ChangeControlRulesV0; - use dpp::state_transition::proof_result::StateTransitionProofResult; - use drive::drive::Drive; - - #[test] - fn test_data_contract_update_can_add_new_token() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // ── original contract (no tokens) ───────────────────────────────── - let mut data_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - data_contract.set_owner_id(identity.id()); - - platform - .drive - .apply_contract( - &data_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - // ── updated contract: add a well‑formed token at position 0 ────── - let mut updated_data_contract = data_contract.clone(); - updated_data_contract.set_version(2); - - let valid_token_cfg = { - let mut cfg = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - cfg.set_base_supply(1_000_000); - - cfg.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::from([( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "credit".to_string(), - plural_form: "credits".to_string(), - }), - )]), - decimals: 8, - }, - )); - cfg - }; - - updated_data_contract.add_token(0, valid_token_cfg); - - let data_contract_update_transition = - DataContractUpdateTransition::new_from_data_contract( - updated_data_contract.clone(), - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create data contract update transition"); - - let tx_bytes = data_contract_update_transition - .serialize_to_bytes() - .expect("expected serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - let processing_result = platform - .platform - .process_raw_state_transitions( - &[tx_bytes], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - - // Prove & verify - let proof = platform - .drive - .prove_state_transition(&data_contract_update_transition, None, platform_version) - .expect("expect to prove state transition"); - let (_root_hash, result) = Drive::verify_state_transition_was_executed_with_proof( - &data_contract_update_transition, - &BlockInfo::default(), - proof.data.as_ref().expect("expected data"), - &|_| Ok(None), - platform_version, - ) - .unwrap_or_else(|e| { - panic!( - "expect to verify state transition proof {}, error is {}", - hex::encode(proof.data.expect("expected data")), - e - ) - }); - assert_matches!(result, StateTransitionProofResult::VerifiedDataContract(_)); - } - - #[test] - fn test_data_contract_update_with_token_setting_identifier_that_does_exist() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - let (identity2, _signer2, _key2) = - setup_identity(&mut platform, 93, dash_to_credits!(0.2)); - - let platform_state = platform.state.load(); - let platform_version = PlatformVersion::latest(); - - let mut original_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - original_contract.set_owner_id(identity.id()); - - platform - .drive - .apply_contract( - &original_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract"); - - let mut updated_contract = original_contract.clone(); - updated_contract.set_version(2); - - let mut token_config = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - token_config.set_base_supply(100_000); - token_config.set_manual_minting_rules(ChangeControlRules::V0(ChangeControlRulesV0 { - authorized_to_make_change: AuthorizedActionTakers::Identity(identity2.id()), - admin_action_takers: AuthorizedActionTakers::ContractOwner, - changing_authorized_action_takers_to_no_one_allowed: false, - changing_admin_action_takers_to_no_one_allowed: false, - self_changing_admin_action_takers_allowed: false, - })); - - token_config.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::from([( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "test".to_string(), - plural_form: "tests".to_string(), - }), - )]), - decimals: 8, - }, - )); - - updated_contract.add_token(0, token_config); - - let transition = DataContractUpdateTransition::new_from_data_contract( - updated_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expected update transition"); - - let serialized = transition.serialize_to_bytes().expect("serialize"); - - let transaction = platform.drive.grove.start_transaction(); - let result = platform - .platform - .process_raw_state_transitions( - &[serialized], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected processing"); - - assert_matches!( - result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("commit"); - } - #[test] - fn test_data_contract_update_with_token_setting_identifier_that_does_not_exist() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - let platform_state = platform.state.load(); - let platform_version = PlatformVersion::latest(); - - let mut original_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - original_contract.set_owner_id(identity.id()); - - platform - .drive - .apply_contract( - &original_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract"); - - let mut updated_contract = original_contract.clone(); - updated_contract.set_version(2); - - let mut token_config = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - token_config.set_base_supply(1_000_000); - - token_config.set_manual_minting_rules(ChangeControlRules::V0(ChangeControlRulesV0 { - authorized_to_make_change: AuthorizedActionTakers::Identity(Identifier::from( - [4; 32], - )), // doesn't exist - admin_action_takers: AuthorizedActionTakers::ContractOwner, - changing_authorized_action_takers_to_no_one_allowed: false, - changing_admin_action_takers_to_no_one_allowed: false, - self_changing_admin_action_takers_allowed: false, - })); - - token_config.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::from([( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "test".to_string(), - plural_form: "tests".to_string(), - }), - )]), - decimals: 8, - }, - )); - - updated_contract.add_token(0, token_config); - - let transition = DataContractUpdateTransition::new_from_data_contract( - updated_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expected update transition"); - - let serialized = transition.serialize_to_bytes().expect("serialize"); - - let transaction = platform.drive.grove.start_transaction(); - let result = platform - .platform - .process_raw_state_transitions( - &[serialized], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected processing"); - - assert_matches!( - result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::StateError( - StateError::IdentityInTokenConfigurationNotFoundError(_) + 1 => { + // V0 transitions use the V0 transformer, V1 transitions use the V1 transformer + match self { + DataContractUpdateTransition::V0(_) => self.transform_into_action_v0( + block_info, + validation_mode, + execution_context, + platform_version, ), - .. - }] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("commit"); - } - - #[test] - fn test_data_contract_update_can_not_add_new_token_with_gap() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // ── original contract with token at position 0 ─────────────────── - let mut data_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - data_contract.set_owner_id(identity.id()); - data_contract.add_token( - 0, - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()), - ); - data_contract - .tokens_mut() - .expect("expected tokens") - .get_mut(&0) - .expect("expected token") - .conventions_mut() - .localizations_mut() - .insert( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "test".to_string(), - plural_form: "tests".to_string(), - }), - ); - - platform - .drive - .apply_contract( - &data_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - // ── updated contract: try to add token at position 2 (gap) ─────── - let mut updated_data_contract = data_contract.clone(); - updated_data_contract.set_version(2); - - updated_data_contract.add_token( - 2, // <‑‑ non‑contiguous - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()), - ); - - let data_contract_update_transition = - DataContractUpdateTransition::new_from_data_contract( - updated_data_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create data contract update transition"); - - let tx_bytes = data_contract_update_transition - .serialize_to_bytes() - .expect("expected serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - let processing_result = platform - .platform - .process_raw_state_transitions( - &[tx_bytes], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - assert_matches!( - processing_result.execution_results().as_slice(), - [UnpaidConsensusError(ConsensusError::BasicError( - BasicError::NonContiguousContractTokenPositionsError(_) - ))] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - } - - #[test] - fn test_data_contract_update_can_not_add_new_token_with_large_base_supply() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // ── original contract (no tokens) ──────────────────────────────── - let mut data_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - data_contract.set_owner_id(identity.id()); - - platform - .drive - .apply_contract( - &data_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - // ── updated contract: token with base_supply > i64::MAX ────────── - let mut updated_data_contract = data_contract.clone(); - updated_data_contract.set_version(2); - - let mut huge_supply_cfg = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - huge_supply_cfg.set_base_supply(i64::MAX as u64 + 1); - - updated_data_contract.add_token(0, huge_supply_cfg); - - let data_contract_update_transition = - DataContractUpdateTransition::new_from_data_contract( - updated_data_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create data contract update transition"); - - let tx_bytes = data_contract_update_transition - .serialize_to_bytes() - .expect("expected serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - let processing_result = platform - .platform - .process_raw_state_transitions( - &[tx_bytes], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - assert_matches!( - processing_result.execution_results().as_slice(), - [UnpaidConsensusError(ConsensusError::BasicError( - BasicError::InvalidTokenBaseSupplyError(_) - ))] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - } - - #[test] - fn test_data_contract_update_can_not_add_new_token_with_invalid_localization() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); - - // ── original contract (no tokens) ──────────────────────────────── - let mut data_contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - data_contract.set_owner_id(identity.id()); - - platform - .drive - .apply_contract( - &data_contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("expected to apply contract successfully"); - - // ── updated contract: token with empty localization map ────────── - let mut updated_data_contract = data_contract.clone(); - updated_data_contract.set_version(2); - - let empty_localization_cfg = { - let mut cfg = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - cfg.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::new(), // <‑‑ invalid - decimals: 8, - }, - )); - cfg - }; - - updated_data_contract.add_token(0, empty_localization_cfg); - - let data_contract_update_transition = - DataContractUpdateTransition::new_from_data_contract( - updated_data_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .expect("expect to create data contract update transition"); - - let tx_bytes = data_contract_update_transition - .serialize_to_bytes() - .expect("expected serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - let processing_result = platform - .platform - .process_raw_state_transitions( - &[tx_bytes], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - assert_matches!( - processing_result.execution_results().as_slice(), - [UnpaidConsensusError(ConsensusError::BasicError( - BasicError::MissingDefaultLocalizationError(_) - ))] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - } - - #[test] - fn update_token_with_missing_main_group_should_fail() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - let (identity, signer, key) = - setup_identity(&mut platform, 1234, dash_to_credits!(0.1)); - let platform_state = platform.state.load(); - let platform_version = PlatformVersion::latest(); - - let mut contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - contract.set_owner_id(identity.id()); - platform - .drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .unwrap(); - - let mut updated_contract = contract.clone(); - updated_contract.set_version(2); - - let mut config = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - config.set_main_control_group(Some(1)); // Missing group - config.set_manual_minting_rules(ChangeControlRules::V0(ChangeControlRulesV0 { - authorized_to_make_change: AuthorizedActionTakers::MainGroup, - admin_action_takers: AuthorizedActionTakers::MainGroup, - changing_authorized_action_takers_to_no_one_allowed: false, - changing_admin_action_takers_to_no_one_allowed: false, - self_changing_admin_action_takers_allowed: false, - })); - config.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::from([( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "test".to_string(), - plural_form: "tests".to_string(), - }), - )]), - decimals: 8, - }, - )); - updated_contract.add_token(0, config); - - let transition = DataContractUpdateTransition::new_from_data_contract( - updated_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .unwrap(); - let tx = platform.drive.grove.start_transaction(); - let result = platform - .platform - .process_raw_state_transitions( - &[transition.serialize_to_bytes().unwrap()], - &platform_state, - &BlockInfo::default(), - &tx, - platform_version, - false, - None, - ) - .unwrap(); - - assert_matches!( - result.execution_results().as_slice(), - [UnpaidConsensusError(ConsensusError::BasicError( - BasicError::GroupPositionDoesNotExistError(_) - ))] - ); - } - - #[test] - fn update_token_with_invalid_distribution_function_should_fail() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - let (identity, signer, key) = - setup_identity(&mut platform, 1234, dash_to_credits!(0.1)); - let platform_state = platform.state.load(); - let platform_version = PlatformVersion::latest(); - - let mut contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - contract.set_owner_id(identity.id()); - platform - .drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .unwrap(); - - let mut updated_contract = contract.clone(); - updated_contract.set_version(2); - - let mut config = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - config - .distribution_rules_mut() - .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( - TokenPerpetualDistributionV0 { - distribution_type: RewardDistributionType::BlockBasedDistribution { - interval: 100, - function: DistributionFunction::Exponential { - a: 0, - d: 0, - m: 0, - n: 0, - o: 0, - start_moment: None, - b: 0, - min_value: None, - max_value: None, - }, - }, - distribution_recipient: TokenDistributionRecipient::Identity(identity.id()), - }, - ))); - config.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::from([( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "test".to_string(), - plural_form: "tests".to_string(), - }), - )]), - decimals: 8, - }, - )); - updated_contract.add_token(0, config); - - let transition = DataContractUpdateTransition::new_from_data_contract( - updated_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .unwrap(); - let tx = platform.drive.grove.start_transaction(); - let result = platform - .platform - .process_raw_state_transitions( - &[transition.serialize_to_bytes().unwrap()], - &platform_state, - &BlockInfo::default(), - &tx, - platform_version, - false, - None, - ) - .unwrap(); - - assert_matches!( - result.execution_results().as_slice(), - [UnpaidConsensusError(ConsensusError::BasicError( - BasicError::InvalidTokenDistributionFunctionDivideByZeroError(_) - ))] - ); - } - - #[test] - fn update_token_with_random_distribution_should_fail() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - let (identity, signer, key) = - setup_identity(&mut platform, 1234, dash_to_credits!(0.1)); - let platform_state = platform.state.load(); - let platform_version = PlatformVersion::latest(); - - let mut contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - contract.set_owner_id(identity.id()); - platform - .drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .unwrap(); - - let mut updated_contract = contract.clone(); - updated_contract.set_version(2); - - let mut config = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - config - .distribution_rules_mut() - .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( - TokenPerpetualDistributionV0 { - distribution_type: RewardDistributionType::BlockBasedDistribution { - interval: 100, - function: DistributionFunction::Random { min: 0, max: 10 }, - }, - distribution_recipient: TokenDistributionRecipient::Identity(identity.id()), - }, - ))); - config.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::from([( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "test".to_string(), - plural_form: "tests".to_string(), - }), - )]), - decimals: 8, - }, - )); - updated_contract.add_token(0, config); - - let transition = DataContractUpdateTransition::new_from_data_contract( - updated_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .unwrap(); - let tx = platform.drive.grove.start_transaction(); - let result = platform - .platform - .process_raw_state_transitions( - &[transition.serialize_to_bytes().unwrap()], - &platform_state, - &BlockInfo::default(), - &tx, - platform_version, - false, - None, - ) - .unwrap(); - - assert_matches!( - result.execution_results().as_slice(), - [UnpaidConsensusError(ConsensusError::BasicError( - BasicError::UnsupportedFeatureError(_) - ))] - ); - } - - #[test] - fn update_token_overwriting_existing_position_should_fail() { - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_initial_state_structure(); - let (identity, signer, key) = - setup_identity(&mut platform, 1234, dash_to_credits!(1.0)); - let platform_state = platform.state.load(); - let platform_version = PlatformVersion::latest(); - - let mut contract = - get_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - contract.set_owner_id(identity.id()); - let mut config = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - config.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::from([( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "test".to_string(), - plural_form: "tests".to_string(), - }), - )]), - decimals: 8, - }, - )); - - let mut config_2 = - TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - config_2.set_conventions(TokenConfigurationConvention::V0( - TokenConfigurationConventionV0 { - localizations: BTreeMap::from([( - "en".to_string(), - TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "test_1".to_string(), - plural_form: "tests_2".to_string(), - }), - )]), - decimals: 8, - }, - )); - contract.add_token(0, config); - - platform - .drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .unwrap(); - - let mut updated_contract = contract.clone(); - updated_contract.set_version(2); - updated_contract.add_token(0, config_2); - - let transition = DataContractUpdateTransition::new_from_data_contract( - updated_contract, - &identity.into_partial_identity_info(), - key.id(), - 2, - 0, - &signer, - platform_version, - None, - ) - .unwrap(); - let tx = platform.drive.grove.start_transaction(); - let result = platform - .platform - .process_raw_state_transitions( - &[transition.serialize_to_bytes().unwrap()], - &platform_state, - &BlockInfo::default(), - &tx, - platform_version, - false, - None, - ) - .unwrap(); - - assert_matches!( - result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::StateError( - StateError::DataContractUpdateActionNotAllowedError(_) + DataContractUpdateTransition::V1(_) => self.transform_into_action_v1( + platform, + block_info, + validation_mode, + execution_context, + tx, + platform_version, ), - .. - }] - ); - } - } - - mod keyword_updates { - use super::*; - use dpp::{ - data_contract::conversion::value::v0::DataContractValueConversionMethodsV0, - data_contracts::SystemDataContract, - document::DocumentV0Getters, - platform_value::{string_encoding::Encoding, Value}, - state_transition::{ - data_contract_create_transition::{ - methods::DataContractCreateTransitionMethodsV0, DataContractCreateTransition, - }, - StateTransition, - }, - system_data_contracts::load_system_data_contract, - tests::json_document::json_document_to_contract_with_ids, - }; - use drive::{ - drive::document::query::QueryDocumentsOutcomeV0Methods, - query::{DriveDocumentQuery, WhereClause, WhereOperator}, - }; - - // ──────────────────────────────────────────────────────────────────────── - // helpers - // ──────────────────────────────────────────────────────────────────────── - - /// Creates a contract with the supplied keywords and commits it to Drive. - /// Returns `(contract_id, create_transition)`. - fn create_contract_with_keywords( - platform: &mut TempPlatform, - identity: &Identity, - signer: &SimpleSigner, - key: &IdentityPublicKey, - keywords: &[&str], - platform_version: &PlatformVersion, - ) -> (Identifier, StateTransition) { - let base = json_document_to_contract_with_ids( - "tests/supporting_files/contract/keyword_test/keyword_base_contract.json", - None, - None, - false, - platform_version, - ) - .expect("load base contract"); - - let mut val = base.to_value(platform_version).expect("to_value"); - - val["keywords"] = Value::Array( - keywords - .iter() - .map(|k| Value::Text(k.to_string())) - .collect(), - ); - - let contract = - DataContract::from_value(val, true, platform_version).expect("from_value"); - - let create = DataContractCreateTransition::new_from_data_contract( - contract, - 2, - &identity.clone().into_partial_identity_info(), - key.id(), - signer, - platform_version, - None, - ) - .expect("create transition"); - - let tx_bytes = create.serialize_to_bytes().expect("serialize"); - - let tx = platform.drive.grove.start_transaction(); - let platform_state = platform.state.load(); - - let res = platform - .platform - .process_raw_state_transitions( - &[tx_bytes], - &platform_state, - &BlockInfo::default(), - &tx, - platform_version, - false, - None, - ) - .expect("process create"); - - assert_matches!( - res.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - - platform - .drive - .grove - .commit_transaction(tx) - .unwrap() - .expect("commit create"); - - // pull id from unique_identifiers - let contract_id = Identifier::from_string( - create - .unique_identifiers() - .first() - .unwrap() - .as_str() - .split('-') - .last() - .unwrap(), - Encoding::Base58, - ) - .unwrap(); - - (contract_id, create) - } - - /// Convenience for building and applying an **update** transition that - /// only changes the `keywords` array. - fn apply_keyword_update( - platform: &mut TempPlatform, - contract_id: Identifier, - identity: &Identity, - signer: &SimpleSigner, - key: &IdentityPublicKey, - new_keywords: &[&str], - platform_version: &PlatformVersion, - ) -> Result<(), Vec> { - // fetch existing contract - let fetched = platform - .drive - .fetch_contract(contract_id.into(), None, None, None, platform_version) - .value - .unwrap() - .unwrap(); - - let mut val = fetched.contract.to_value(platform_version).unwrap(); - - val["keywords"] = Value::Array( - new_keywords - .iter() - .map(|k| Value::Text(k.to_string())) - .collect(), - ); - - let mut updated_contract = - DataContract::from_value(val, true, platform_version).unwrap(); - updated_contract.set_version(2); - - let update = DataContractUpdateTransition::new_from_data_contract( - updated_contract, - &identity.clone().into_partial_identity_info(), - key.id(), - 2, - 0, - signer, - platform_version, - None, - ) - .expect("build update"); - - let bytes = update.serialize_to_bytes().unwrap(); - - let tx = platform.drive.grove.start_transaction(); - let platform_state = platform.state.load(); - - let outcome = platform - .platform - .process_raw_state_transitions( - &[bytes], - &platform_state, - &BlockInfo::default(), - &tx, - platform_version, - false, - None, - ) - .expect("process update"); - - if matches!( - outcome.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ) { - platform - .drive - .grove - .commit_transaction(tx) - .unwrap() - .expect("commit update"); - Ok(()) - } else { - Err(outcome.execution_results().to_vec()) - } - } - - /// Helper to read all keyword docs for a contract id. - fn keyword_docs_for_contract( - platform: &TempPlatform, - contract_id: Identifier, - platform_version: &PlatformVersion, - ) -> Vec { - let search_contract = - load_system_data_contract(SystemDataContract::KeywordSearch, platform_version) - .unwrap(); - let doc_type = search_contract - .document_type_for_name("contractKeywords") - .unwrap(); - - let mut query = DriveDocumentQuery { - contract: &search_contract, - document_type: doc_type, - internal_clauses: Default::default(), - offset: None, - limit: None, - order_by: Default::default(), - start_at: None, - start_at_included: false, - block_time_ms: None, - }; - query.internal_clauses.equal_clauses.insert( - "contractId".to_string(), - WhereClause { - field: "contractId".to_string(), - operator: WhereOperator::Equal, - value: contract_id.into(), - }, - ); - - let res = platform - .drive - .query_documents(query, None, false, None, None) - .unwrap(); - - res.documents() - .iter() - .map(|d| d.get("keyword").unwrap().as_str().unwrap().to_owned()) - .collect() - } - - // ──────────────────────────────────────────────────────────────────────── - // negative cases – same validation as create - // ──────────────────────────────────────────────────────────────────────── - - macro_rules! invalid_update_test { - ($name:ident, $keywords:expr, $error:pat_param) => { - #[test] - fn $name() { - let platform_version = PlatformVersion::latest(); - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_genesis_state(); - - let (identity, signer, key) = - setup_identity(&mut platform, 958, dash_to_credits!(10.0)); - - // create initial contract with one keyword so update is allowed - let (cid, _) = create_contract_with_keywords( - &mut platform, - &identity, - &signer, - &key, - &["orig"], - &platform_version, - ); - - // try invalid update - let err = apply_keyword_update( - &mut platform, - cid, - &identity, - &signer, - &key, - &$keywords, - &platform_version, - ) - .unwrap_err(); - - assert_matches!( - err.as_slice(), - [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::BasicError($error), - .. - }] - ); - - // original keyword docs must still be there - let docs = keyword_docs_for_contract(&platform, cid, &platform_version); - assert_eq!(docs, vec!["orig"]); } - }; - } - - invalid_update_test!( - update_fails_too_many_keywords, - [ - "kw0", "kw1", "kw2", "kw3", "kw4", "kw5", "kw6", "kw7", "kw8", "kw9", "kw10", - "kw11", "kw12", "kw13", "kw14", "kw15", "kw16", "kw17", "kw18", "kw19", "kw20", - "kw21", "kw22", "kw23", "kw24", "kw25", "kw26", "kw27", "kw28", "kw29", "kw30", - "kw31", "kw32", "kw33", "kw34", "kw35", "kw36", "kw37", "kw38", "kw39", "kw40", - "kw41", "kw42", "kw43", "kw44", "kw45", "kw46", "kw47", "kw48", "kw49", "kw50", - ], - BasicError::TooManyKeywordsError(_) - ); - - invalid_update_test!( - update_fails_duplicate_keywords, - ["dup", "dup"], - BasicError::DuplicateKeywordsError(_) - ); - - invalid_update_test!( - update_fails_keyword_too_short, - ["hi"], - BasicError::InvalidKeywordLengthError(_) - ); - - invalid_update_test!( - update_fails_keyword_too_long, - [&"x".repeat(51)], - BasicError::InvalidKeywordLengthError(_) - ); - - // ──────────────────────────────────────────────────────────────────────── - // positive case – old docs removed, new docs inserted - // ──────────────────────────────────────────────────────────────────────── - - #[test] - fn update_keywords_replaces_search_docs() { - let platform_version = PlatformVersion::latest(); - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_genesis_state(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - // initial contract with two keywords - let (cid, _) = create_contract_with_keywords( - &mut platform, - &identity, - &signer, - &key, - &["old1", "old2"], - platform_version, - ); - - // verify initial docs - let initial_docs = keyword_docs_for_contract(&platform, cid, &platform_version); - assert_eq!(initial_docs.len(), 2); - - // apply update to ["newA", "newB", "newC"] - apply_keyword_update( - &mut platform, - cid, - &identity, - &signer, - &key, - &["newA", "newB", "newC"], - platform_version, - ) - .expect("update should succeed"); - - // fetch contract – keywords updated? - let fetched = platform - .drive - .fetch_contract(cid.into(), None, None, None, platform_version) - .value - .unwrap() - .unwrap(); - assert_eq!( - *fetched.contract.keywords(), - ["newa", "newb", "newc"] - .iter() - .map(|&s| s.to_string()) - .collect::>() - ); - - // search‑contract docs updated? - let docs_after = keyword_docs_for_contract(&platform, cid, platform_version); - assert_eq!(docs_after.len(), 3); - assert!(docs_after.contains(&"newa".to_string())); - assert!(docs_after.contains(&"newb".to_string())); - assert!(docs_after.contains(&"newc".to_string())); - // old docs gone - assert!(!docs_after.contains(&"old1".to_string())); - assert!(!docs_after.contains(&"old2".to_string())); - } - } - - mod description_updates { - use super::*; - use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; - use dpp::{ - data_contract::conversion::value::v0::DataContractValueConversionMethodsV0, - data_contracts::SystemDataContract, - document::DocumentV0Getters, - platform_value::{string_encoding::Encoding, Value}, - state_transition::{ - data_contract_create_transition::{ - methods::DataContractCreateTransitionMethodsV0, DataContractCreateTransition, - }, - StateTransition, - }, - system_data_contracts::load_system_data_contract, - tests::json_document::json_document_to_contract_with_ids, - }; - use drive::{ - drive::document::query::QueryDocumentsOutcomeV0Methods, - query::{DriveDocumentQuery, WhereClause, WhereOperator}, - }; - - // ──────────────────────────────────────────────────────────────────────── - // helpers - // ──────────────────────────────────────────────────────────────────────── - - /// Creates a contract with the supplied description and commits it to Drive. - /// Returns `(contract_id, create_transition)`. - fn create_contract_with_description( - platform: &mut TempPlatform, - identity: &Identity, - signer: &SimpleSigner, - key: &IdentityPublicKey, - description: &str, - platform_version: &PlatformVersion, - ) -> (Identifier, StateTransition) { - let base = json_document_to_contract_with_ids( - "tests/supporting_files/contract/keyword_test/keyword_base_contract.json", - None, - None, - false, - platform_version, - ) - .expect("load base contract"); - - let mut val = base.to_value(platform_version).expect("to_value"); - - val["description"] = Value::Text(description.to_string()); - - let contract = - DataContract::from_value(val, true, platform_version).expect("from_value"); - - let create = DataContractCreateTransition::new_from_data_contract( - contract, - 2, - &identity.clone().into_partial_identity_info(), - key.id(), - signer, - platform_version, - None, - ) - .expect("create transition"); - - let tx_bytes = create.serialize_to_bytes().expect("serialize"); - - let tx = platform.drive.grove.start_transaction(); - let platform_state = platform.state.load(); - - let res = platform - .platform - .process_raw_state_transitions( - &[tx_bytes], - &platform_state, - &BlockInfo::default(), - &tx, - platform_version, - false, - None, - ) - .expect("process create"); - - assert_matches!( - res.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - - platform - .drive - .grove - .commit_transaction(tx) - .unwrap() - .expect("commit create"); - - // pull id from unique_identifiers - let contract_id = Identifier::from_string( - create - .unique_identifiers() - .first() - .unwrap() - .as_str() - .split('-') - .last() - .unwrap(), - Encoding::Base58, - ) - .unwrap(); - - (contract_id, create) - } - - /// Convenience for building and applying an **update** transition that - /// only changes the `description` string. - fn apply_description_update( - platform: &mut TempPlatform, - contract_id: Identifier, - identity: &Identity, - signer: &SimpleSigner, - key: &IdentityPublicKey, - new_description: &str, - platform_version: &PlatformVersion, - ) -> Result<(), Vec> { - // fetch existing contract - let fetched = platform - .drive - .fetch_contract(contract_id.into(), None, None, None, platform_version) - .value - .unwrap() - .unwrap(); - - let mut val = fetched.contract.to_value(platform_version).unwrap(); - - val["description"] = Value::Text(new_description.to_string()); - - let mut updated_contract = - DataContract::from_value(val, true, platform_version).unwrap(); - updated_contract.set_version(2); - - let update = DataContractUpdateTransition::new_from_data_contract( - updated_contract, - &identity.clone().into_partial_identity_info(), - key.id(), - 2, - 0, - signer, - platform_version, - None, - ) - .expect("build update"); - - let bytes = update.serialize_to_bytes().unwrap(); - - let tx = platform.drive.grove.start_transaction(); - let platform_state = platform.state.load(); - - let outcome = platform - .platform - .process_raw_state_transitions( - &[bytes], - &platform_state, - &BlockInfo::default(), - &tx, - platform_version, - false, - None, - ) - .expect("process update"); - - if matches!( - outcome.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ) { - platform - .drive - .grove - .commit_transaction(tx) - .unwrap() - .expect("commit update"); - Ok(()) - } else { - Err(outcome.execution_results().to_vec()) } - } - - /// Helper to read all description docs for a contract id. - fn description_docs_for_contract( - platform: &TempPlatform, - contract_id: Identifier, - platform_version: &PlatformVersion, - ) -> String { - let search_contract = - load_system_data_contract(SystemDataContract::KeywordSearch, platform_version) - .unwrap(); - let doc_type = search_contract - .document_type_for_name("shortDescription") - .unwrap(); - - let mut query = DriveDocumentQuery { - contract: &search_contract, - document_type: doc_type, - internal_clauses: Default::default(), - offset: None, - limit: None, - order_by: Default::default(), - start_at: None, - start_at_included: false, - block_time_ms: None, - }; - query.internal_clauses.equal_clauses.insert( - "contractId".to_string(), - WhereClause { - field: "contractId".to_string(), - operator: WhereOperator::Equal, - value: contract_id.into(), - }, - ); - - let mut res = platform - .drive - .query_documents(query, None, false, None, None) - .expect("expected query to succeed") - .documents_owned(); - - if res.is_empty() { - panic!("expected a document description"); - } - - let first_document = res.remove(0); - - first_document - .properties() - .get_string("description") - .expect("expected description to exist") - } - - // ──────────────────────────────────────────────────────────────────────── - // negative cases – same validation as create - // ──────────────────────────────────────────────────────────────────────── - - macro_rules! invalid_update_test { - ($name:ident, $description:expr, $error:pat_param) => { - #[test] - fn $name() { - let platform_version = PlatformVersion::latest(); - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_genesis_state(); - - let (identity, signer, key) = - setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - // create initial contract with description so update is allowed - let (cid, _) = create_contract_with_description( - &mut platform, - &identity, - &signer, - &key, - &"orig", - &platform_version, - ); - - // try invalid update - let err = apply_description_update( - &mut platform, - cid, - &identity, - &signer, - &key, - &$description, - &platform_version, - ) - .unwrap_err(); - - assert_matches!( - err.as_slice(), - [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::BasicError($error), - .. - }] - ); - - // original description docs must still be there - let docs = description_docs_for_contract(&platform, cid, &platform_version); - assert_eq!(docs, "orig".to_string()); - } - }; - } - - invalid_update_test!( - update_fails_description_too_short, - "hi", - BasicError::InvalidDescriptionLengthError(_) - ); - - invalid_update_test!( - update_fails_description_too_long, - &"x".repeat(101), - BasicError::InvalidDescriptionLengthError(_) - ); - - // ──────────────────────────────────────────────────────────────────────── - // positive case – old docs removed, new docs inserted - // ──────────────────────────────────────────────────────────────────────── - - #[test] - fn update_description_replaces_search_docs() { - let platform_version = PlatformVersion::latest(); - let mut platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_genesis_state(); - - let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); - - // initial contract with description - let (cid, _) = create_contract_with_description( - &mut platform, - &identity, - &signer, - &key, - "old1", - platform_version, - ); - - // verify initial docs - let initial_docs = description_docs_for_contract(&platform, cid, platform_version); - assert_eq!(initial_docs, "old1".to_string()); - - // apply update to "newA" - apply_description_update( - &mut platform, - cid, - &identity, - &signer, - &key, - "newA", - platform_version, - ) - .expect("update should succeed"); - - // fetch contract – description updated? - let fetched = platform - .drive - .fetch_contract(cid.into(), None, None, None, platform_version) - .value - .unwrap() - .unwrap(); - assert_eq!( - fetched.contract.description(), - Some("newA".to_string()).as_ref() - ); - - // search‑contract docs updated? - let docs_after = description_docs_for_contract(&platform, cid, platform_version); - assert_eq!(docs_after, "newA".to_string()); - // old docs gone - assert!(!docs_after.contains(&"old1".to_string())); + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "data contract update transition: transform_into_action".to_string(), + known_versions: vec![0, 1], + received: version, + })), } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/mod.rs index b6e1af89cf..9b6d41d9a2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/mod.rs @@ -2,6 +2,7 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::data_contract_update::state::v0::DataContractUpdateStateTransitionStateValidationV0; +use crate::execution::validation::state_transition::data_contract_update::state::v1::DataContractUpdateStateTransitionStateValidationV1; use crate::execution::validation::state_transition::processor::state::StateTransitionStateValidation; use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformRef; @@ -14,6 +15,7 @@ use drive::grovedb::TransactionArg; use drive::state_transition_action::StateTransitionAction; pub(crate) mod v0; +pub(crate) mod v1; impl StateTransitionStateValidation for DataContractUpdateTransition { fn validate_state( @@ -47,9 +49,33 @@ impl StateTransitionStateValidation for DataContractUpdateTransition { platform_version, ) } + 1 => { + if action.is_some() { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution("data contract update is calling validate state, and the action is already known. It should not be known at this point"))); + } + // V0 transitions use the V0 validator, V1 transitions use the V1 validator + match self { + DataContractUpdateTransition::V0(_) => self.validate_state_v0( + platform, + block_info, + validation_mode, + execution_context, + tx, + platform_version, + ), + DataContractUpdateTransition::V1(_) => self.validate_state_v1( + platform, + block_info, + validation_mode, + execution_context, + tx, + platform_version, + ), + } + } version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "data contract update transition: validate_state".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v0/mod.rs index b44ecb8b3b..9480c632a9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v0/mod.rs @@ -28,11 +28,11 @@ use dpp::data_contract::validate_update::DataContractUpdateValidationMethodsV0; use crate::error::execution::ExecutionError; use crate::execution::validation::state_transition::ValidationMode; use dpp::prelude::ConsensusValidationResult; -use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; use dpp::version::PlatformVersion; use dpp::ProtocolError; use drive::grovedb::TransactionArg; +use drive::state_transition_action::contract::data_contract_update::v0::DataContractUpdateTransitionActionV0; use drive::state_transition_action::contract::data_contract_update::DataContractUpdateTransitionAction; use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; @@ -172,7 +172,15 @@ impl DataContractUpdateStateTransitionStateValidationV0 for DataContractUpdateTr let mut validated_identities = BTreeSet::new(); - for (position, group) in self.data_contract().groups() { + // Get groups from the transition - V0 has embedded contract, V1 has new_groups field + let groups_to_validate = match self { + DataContractUpdateTransition::V0(v0) => v0.data_contract.groups(), + DataContractUpdateTransition::V1(v1) => &v1.new_groups, + }; + + let contract_id = new_data_contract.id(); + + for (position, group) in groups_to_validate { for member_identity_id in group.members().keys() { if !validated_identities.contains(member_identity_id) { let identity_exists = validate_non_masternode_identity_exists( @@ -193,7 +201,7 @@ impl DataContractUpdateStateTransitionStateValidationV0 for DataContractUpdateTr bump_action, vec![StateError::IdentityMemberOfGroupNotFoundError( IdentityMemberOfGroupNotFoundError::new( - self.data_contract().id(), + contract_id, *position, *member_identity_id, ), @@ -467,8 +475,18 @@ impl DataContractUpdateStateTransitionStateValidationV0 for DataContractUpdateTr ) -> Result, Error> { let mut validation_operations = vec![]; - let result = DataContractUpdateTransitionAction::try_from_borrowed_transition( - self, + // Extract the V0 transition - this validator only handles V0 transitions + let v0 = match self { + DataContractUpdateTransition::V0(v0) => v0, + DataContractUpdateTransition::V1(_) => { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "transform_into_action_v0 called with V1 transition", + ))); + } + }; + + let result = DataContractUpdateTransitionActionV0::try_from_borrowed_transition( + v0, block_info, validation_mode.should_fully_validate_contract_on_transform_into_action(), &mut validation_operations, @@ -491,8 +509,9 @@ impl DataContractUpdateStateTransitionStateValidationV0 for DataContractUpdateTr )) } Err(protocol_error) => Err(protocol_error.into()), - Ok(create_action) => { - let action: StateTransitionAction = create_action.into(); + Ok(action_v0) => { + let update_action: DataContractUpdateTransitionAction = action_v0.into(); + let action: StateTransitionAction = update_action.into(); Ok(action.into()) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v1/mod.rs new file mode 100644 index 0000000000..e27a024af3 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v1/mod.rs @@ -0,0 +1,486 @@ +use crate::error::Error; +use crate::platform_types::platform::PlatformRef; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use std::collections::BTreeSet; + +use dpp::consensus::state::data_contract::data_contract_not_found_error::DataContractNotFoundError; +use dpp::consensus::state::group::IdentityMemberOfGroupNotFoundError; +use dpp::consensus::state::identity::identity_for_token_configuration_not_found_error::{ + IdentityInTokenConfigurationNotFoundError, TokenConfigurationIdentityContext, +}; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::InvalidTokenPositionStateError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; +use dpp::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Accessors; +use dpp::data_contract::associated_token::token_pre_programmed_distribution::accessors::v0::TokenPreProgrammedDistributionV0Methods; +use dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dpp::data_contract::document_type::accessors::DocumentTypeV1Getters; +use dpp::data_contract::group::accessors::v0::GroupV0Getters; +use dpp::data_contract::validate_update::DataContractUpdateValidationMethodsV0; + +use crate::error::execution::ExecutionError; +use crate::execution::validation::state_transition::ValidationMode; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use dpp::version::PlatformVersion; +use dpp::ProtocolError; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::contract::data_contract_update::DataContractUpdateTransitionAction; +use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; + +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; +use drive::state_transition_action::StateTransitionAction; +use crate::execution::validation::state_transition::common::validate_identity_exists::validate_identity_exists; +use crate::execution::validation::state_transition::common::validate_non_masternode_identity_exists::validate_non_masternode_identity_exists; + +pub(in crate::execution::validation::state_transition::state_transitions::data_contract_update) trait DataContractUpdateStateTransitionStateValidationV1 { + fn validate_state_v1( + &self, + platform: &PlatformRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error>; + + fn transform_into_action_v1( + &self, + platform: &PlatformRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl DataContractUpdateStateTransitionStateValidationV1 for DataContractUpdateTransition { + fn validate_state_v1( + &self, + platform: &PlatformRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let action = self.transform_into_action_v1( + platform, + block_info, + validation_mode, + execution_context, + tx, + platform_version, + )?; + + if !action.is_valid() { + return Ok(action); + } + + // For V1 transitions, the old contract was already fetched in transform_into_action_v1 + // and the new contract already has created_at fields copied from old contract. + // Get references to both for validation. + let state_transition_action = action.data.as_ref().ok_or(Error::Execution( + ExecutionError::CorruptedCodeExecution( + "we should always have an action at this point in data contract update", + ), + ))?; + + let (new_data_contract, old_data_contract_ref) = match state_transition_action { + StateTransitionAction::DataContractUpdateAction(update_action) => { + let old = update_action + .old_data_contract_ref() + .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "V1 update action should have old_data_contract", + )))?; + (update_action.data_contract_ref(), old) + } + _ => { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "we should always have an update action at this point in data contract update", + ))); + } + }; + + // Validate the update against the old data contract + let validation_result = old_data_contract_ref.validate_update( + new_data_contract, + block_info, + platform_version, + )?; + + if !validation_result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + validation_result.errors, + )); + } + + let mut validated_identities = BTreeSet::new(); + + // Get groups from the transition - V0 has embedded contract, V1 has new_groups field + let groups_to_validate = match self { + DataContractUpdateTransition::V0(v0) => v0.data_contract.groups(), + DataContractUpdateTransition::V1(v1) => &v1.new_groups, + }; + + let contract_id = new_data_contract.id(); + + for (position, group) in groups_to_validate { + for member_identity_id in group.members().keys() { + if !validated_identities.contains(member_identity_id) { + let identity_exists = validate_non_masternode_identity_exists( + platform.drive, + member_identity_id, + execution_context, + tx, + platform_version, + )?; + + if !identity_exists { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ); + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![StateError::IdentityMemberOfGroupNotFoundError( + IdentityMemberOfGroupNotFoundError::new( + contract_id, + *position, + *member_identity_id, + ), + ) + .into()], + )); + } else { + validated_identities.insert(*member_identity_id); + } + } + } + } + + // Validate any newly added tokens + for (token_contract_position, token_configuration) in new_data_contract.tokens() { + if !old_data_contract_ref + .tokens() + .contains_key(token_contract_position) + { + for (name, change_control_rules) in token_configuration.all_change_control_rules() { + if let AuthorizedActionTakers::Identity(identity_id) = + change_control_rules.authorized_to_make_change_action_takers() + { + // we need to make sure this identity exists + if !validated_identities.contains(identity_id) { + let identity_exists = validate_non_masternode_identity_exists( + platform.drive, + identity_id, + execution_context, + tx, + platform_version, + )?; + + if !identity_exists { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![StateError::IdentityInTokenConfigurationNotFoundError( + IdentityInTokenConfigurationNotFoundError::new( + old_data_contract_ref.id(), + *token_contract_position, + TokenConfigurationIdentityContext::ChangeControlRule( + name.to_string(), + ), + *identity_id, + ), + ) + .into()], + )); + } else { + validated_identities.insert(*identity_id); + } + } + } + } + + if let Some(distribution) = token_configuration + .distribution_rules() + .perpetual_distribution() + { + if let TokenDistributionRecipient::Identity(identifier) = + distribution.distribution_recipient() + { + if !validated_identities.contains(&identifier) { + let identity_exists = validate_identity_exists( + platform.drive, + &identifier, + execution_context, + tx, + platform_version, + )?; + + if !identity_exists { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ); + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![StateError::IdentityInTokenConfigurationNotFoundError( + IdentityInTokenConfigurationNotFoundError::new( + old_data_contract_ref.id(), + *token_contract_position, + TokenConfigurationIdentityContext::PerpetualDistributionRecipient, + identifier, + ), + ) + .into()], + )); + } else { + validated_identities.insert(identifier); + } + } + } + } + + if let Some(distributions) = token_configuration + .distribution_rules() + .pre_programmed_distribution() + { + for distribution in distributions.distributions().values() { + for identifier in distribution.keys() { + if !validated_identities.contains(identifier) { + let identity_exists = validate_identity_exists( + platform.drive, + identifier, + execution_context, + tx, + platform_version, + )?; + + if !identity_exists { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ); + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![StateError::IdentityInTokenConfigurationNotFoundError( + IdentityInTokenConfigurationNotFoundError::new( + old_data_contract_ref.id(), + *token_contract_position, + TokenConfigurationIdentityContext::PreProgrammedDistributionRecipient, + *identifier, + ), + ) + .into()], + )); + } else { + validated_identities.insert(*identifier); + } + } + } + } + } + + // We validate that if we set a minting distribution that this identity exists + // It can be an evonode, so we just use the balance as a check + + if let Some(minting_recipient) = token_configuration + .distribution_rules() + .new_tokens_destination_identity() + { + if !validated_identities.contains(minting_recipient) { + let identity_exists = validate_identity_exists( + platform.drive, + minting_recipient, + execution_context, + tx, + platform_version, + )?; + + if !identity_exists { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![StateError::IdentityInTokenConfigurationNotFoundError( + IdentityInTokenConfigurationNotFoundError::new( + old_data_contract_ref.id(), + *token_contract_position, + TokenConfigurationIdentityContext::DefaultMintingRecipient, + *minting_recipient, + ), + ) + .into()], + )); + } else { + validated_identities.insert(*minting_recipient); + } + } + } + } + } + + // now we need to validate that all documents with token costs using external tokens + // point to tokens that actually exist + if let StateTransitionAction::DataContractUpdateAction(update_action) = + action.data_as_borrowed()? + { + // this should always be the case, except if we already have a bump action, + // in which case we don't need to validate anymore + for document_type in update_action.data_contract_ref().document_types().values() { + for (contract_id, token_positions) in + document_type.all_external_token_costs_contract_tokens() + { + let contract_fetch_info = platform.drive.get_contract_with_fetch_info_and_fee( + contract_id.to_buffer(), + Some(&block_info.epoch), + false, + tx, + platform_version, + )?; + + let fee = + contract_fetch_info + .0 + .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "fee must exist in validate state for data contract update transition", + )))?; + + // We add the cost for fetching the contract even if the contract doesn't exist or was in cache + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee)); + + // Data contract should exist + if let Some(fetch_info) = contract_fetch_info.1 { + let contract_tokens = fetch_info.contract.tokens(); + for token_position in &token_positions { + if !contract_tokens.contains_key(token_position) { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ); + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![StateError::InvalidTokenPositionStateError( + InvalidTokenPositionStateError::new( + contract_tokens.last_key_value().map( + |(token_contract_position, _)| { + *token_contract_position + }, + ), + *token_position, + ), + ) + .into()], + )); + } + } + } else { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![StateError::DataContractNotFoundError( + DataContractNotFoundError::new(contract_id), + ) + .into()], + )); + } + } + } + } + + Ok(action) + } + + fn transform_into_action_v1( + &self, + platform: &PlatformRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut validation_operations = vec![]; + + let result = DataContractUpdateTransitionAction::try_from_borrowed_transition( + self, + platform.drive, + tx, + block_info, + validation_mode.should_fully_validate_contract_on_transform_into_action(), + &mut validation_operations, + platform_version, + ); + + execution_context.add_dpp_operations(validation_operations); + + // Return validation result if any consensus errors happened + // during data contract validation + match result { + Err(drive::error::Error::Protocol(protocol_error)) => { + if let ProtocolError::ConsensusError(consensus_error) = *protocol_error { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition(self), + ); + + Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![*consensus_error], + )) + } else { + Err(Error::Protocol(*protocol_error)) + } + } + Err(drive_error) => Err(drive_error.into()), + Ok(validation_result) => { + if !validation_result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition(self), + ); + Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + validation_result.errors, + )) + } else { + Ok(validation_result.map(|update_action| update_action.into())) + } + } + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/mod.rs new file mode 100644 index 0000000000..0fa6e896eb --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/mod.rs @@ -0,0 +1,124 @@ +mod v0_tests; +mod v1_tests; + +use crate::config::{ExecutionConfig, PlatformConfig, PlatformTestConfig, ValidatorSetConfig}; +use crate::rpc::core::MockCoreRPCLike; +use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::DataContract; +use dpp::fee::Credits; +pub use dpp::identifier::Identifier; +pub use dpp::identity::{Identity, IdentityPublicKey, IdentityV0}; +use dpp::tests::fixtures::get_data_contract_fixture; +use dpp::version::PlatformVersion; +use rand::prelude::StdRng; +use rand::SeedableRng; +use simple_signer::signer::SimpleSigner; +use std::collections::BTreeMap; + +pub struct TestData { + pub data_contract: DataContract, + pub platform: TempPlatform, +} + +pub fn setup_identity( + platform: &mut TempPlatform, + seed: u64, + credits: Credits, +) -> (Identity, SimpleSigner, IdentityPublicKey) { + let platform_version = PlatformVersion::latest(); + let mut signer = SimpleSigner::default(); + + let mut rng = StdRng::seed_from_u64(seed); + + let (master_key, master_private_key) = + IdentityPublicKey::random_ecdsa_master_authentication_key_with_rng( + 0, + &mut rng, + platform_version, + ) + .expect("expected to get key pair"); + + signer.add_identity_public_key(master_key.clone(), master_private_key); + + let (critical_public_key, private_key) = + IdentityPublicKey::random_ecdsa_critical_level_authentication_key_with_rng( + 1, + &mut rng, + platform_version, + ) + .expect("expected to get key pair"); + + signer.add_identity_public_key(critical_public_key.clone(), private_key); + + let identity: Identity = IdentityV0 { + id: Identifier::random_with_rng(&mut rng), + public_keys: BTreeMap::from([(0, master_key.clone()), (1, critical_public_key.clone())]), + balance: credits, + revision: 0, + } + .into(); + + // We just add this identity to the system first + + platform + .drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add a new identity"); + + (identity, signer, critical_public_key) +} + +pub fn apply_contract( + platform: &TempPlatform, + data_contract: &DataContract, + block_info: BlockInfo, +) { + let platform_version = PlatformVersion::latest(); + platform + .drive + .apply_contract( + data_contract, + block_info, + true, + None, + None, + platform_version, + ) + .expect("to apply contract"); +} + +pub fn setup_test() -> TestData { + let platform_version = PlatformVersion::latest(); + let data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version).data_contract_owned(); + + let config = PlatformConfig { + validator_set: ValidatorSetConfig { + quorum_size: 10, + ..Default::default() + }, + execution: ExecutionConfig { + verify_sum_trees: true, + ..Default::default() + }, + block_spacing_ms: 300, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }; + let platform = TestPlatformBuilder::new() + .with_config(config) + .build_with_mock_rpc(); + + TestData { + data_contract, + platform: platform.set_initial_state_structure(), + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/v0_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/v0_tests.rs new file mode 100644 index 0000000000..8f4a379d94 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/v0_tests.rs @@ -0,0 +1,2775 @@ +#[cfg(test)] +mod tests { + use super::super::{ + apply_contract, setup_identity, setup_test, Identifier, Identity, IdentityPublicKey, + TestData, + }; + use crate::platform_types::platform::PlatformRef; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::block::block_info::BlockInfo; + use dpp::consensus::state::state_error::StateError; + use dpp::consensus::ConsensusError; + use dpp::dash_to_credits; + use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; + use std::collections::BTreeMap; + + use dpp::data_contract::DataContract; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::platform_value::BinaryData; + use dpp::serialization::PlatformSerializable; + use dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; + use dpp::state_transition::data_contract_update_transition::{ + DataContractUpdateTransition, DataContractUpdateTransitionV0, + }; + + use crate::platform_types::platform_state::PlatformStateV0Methods; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; + use assert_matches::assert_matches; + use dpp::consensus::basic::BasicError; + use dpp::data_contract::accessors::v1::DataContractV1Getters; + use dpp::data_contract::group::v0::GroupV0; + use dpp::data_contract::group::Group; + use dpp::tests::fixtures::get_data_contract_fixture; + use dpp::tests::json_document::json_document_to_contract; + use dpp::version::PlatformVersion; + use drive::util::storage_flags::StorageFlags; + use simple_signer::signer::SimpleSigner; + + mod validate_state { + use super::*; + use serde_json::json; + + use dpp::assert_state_consensus_errors; + use dpp::consensus::state::state_error::StateError; + use dpp::consensus::state::state_error::StateError::DataContractIsReadonlyError; + use dpp::errors::consensus::ConsensusError; + + use crate::execution::validation::state_transition::processor::traits::state::StateTransitionStateValidation; + use dpp::block::block_info::BlockInfo; + use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; + + use dpp::data_contract::config::v0::DataContractConfigSettersV0; + use dpp::data_contract::schema::DataContractSchemaMethodsV0; + + use dpp::data_contract::serialized_version::DataContractInSerializationFormat; + use dpp::platform_value::platform_value; + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; + + use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; + use crate::execution::validation::state_transition::ValidationMode; + use dpp::version::TryFromPlatformVersioned; + use platform_version::{DefaultForPlatformVersion, TryIntoPlatformVersioned}; + + #[test] + pub fn should_return_error_if_trying_to_update_document_schema_in_a_readonly_contract() { + let platform_version = PlatformVersion::latest(); + let TestData { + mut data_contract, + platform, + } = setup_test(); + + data_contract.config_mut().set_readonly(true); + apply_contract(&platform, &data_contract, Default::default()); + + let updated_document = platform_value!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "position": 0 + }, + "newProp": { + "type": "integer", + "minimum": 0, + "position": 1 + } + }, + "required": [ + "$createdAt" + ], + "additionalProperties": false + }); + + data_contract.increment_version(); + data_contract + .set_document_schema( + "niceDocument", + updated_document, + true, + &mut vec![], + platform_version, + ) + .expect("to be able to set document schema"); + + let state_transition = DataContractUpdateTransitionV0 { + identity_contract_nonce: 1, + data_contract: DataContractInSerializationFormat::try_from_platform_versioned( + data_contract, + platform_version, + ) + .expect("to be able to convert data contract to serialization format"), + user_fee_increase: 0, + signature: BinaryData::new(vec![0; 65]), + signature_public_key_id: 0, + }; + + let state = platform.state.load(); + + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + + let mut execution_context = + StateTransitionExecutionContext::default_for_platform_version(platform_version) + .expect("expected a platform version"); + + let result = DataContractUpdateTransition::V0(state_transition) + .validate_state( + None, + &platform_ref, + ValidationMode::Validator, + &BlockInfo::default(), + &mut execution_context, + None, + ) + .expect("state transition to be validated"); + + assert!(!result.is_valid()); + assert_state_consensus_errors!(result, DataContractIsReadonlyError, 1); + } + + #[test] + pub fn should_keep_history_if_contract_config_keeps_history_is_true() { + let TestData { + mut data_contract, + platform, + } = setup_test(); + + let platform_version = PlatformVersion::latest(); + + data_contract.config_mut().set_keeps_history(true); + data_contract.config_mut().set_readonly(false); + + apply_contract( + &platform, + &data_contract, + BlockInfo { + time_ms: 1000, + height: 100, + core_height: 10, + epoch: Default::default(), + }, + ); + + let updated_document = platform_value!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "position": 0 + }, + "newProp": { + "type": "integer", + "minimum": 0, + "position": 1 + } + }, + "required": [ + "$createdAt" + ], + "additionalProperties": false + }); + + data_contract.increment_version(); + data_contract + .set_document_schema( + "niceDocument", + updated_document, + true, + &mut vec![], + platform_version, + ) + .expect("to be able to set document schema"); + + // TODO: add a data contract stop transition + let state_transition = DataContractUpdateTransitionV0 { + identity_contract_nonce: 1, + data_contract: DataContractInSerializationFormat::try_from_platform_versioned( + data_contract.clone(), + platform_version, + ) + .expect("to be able to convert data contract to serialization format"), + user_fee_increase: 0, + signature: BinaryData::new(vec![0; 65]), + signature_public_key_id: 0, + }; + + let state = platform.state.load(); + + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + + let mut execution_context = + StateTransitionExecutionContext::default_for_platform_version(platform_version) + .expect("expected a platform version"); + + let result = DataContractUpdateTransition::V0(state_transition) + .validate_state( + None, + &platform_ref, + ValidationMode::Validator, + &BlockInfo::default(), + &mut execution_context, + None, + ) + .expect("state transition to be validated"); + + assert!(result.is_valid()); + + // This should store update and history + apply_contract( + &platform, + &data_contract, + BlockInfo { + time_ms: 2000, + height: 110, + core_height: 11, + epoch: Default::default(), + }, + ); + + // Fetch from time 0 without a limit or offset + let contract_history = platform + .drive + .fetch_contract_with_history( + *data_contract.id().as_bytes(), + None, + 0, + None, + None, + platform_version, + ) + .expect("to get contract history"); + + let keys = contract_history.keys().copied().collect::>(); + + // Check that keys sorted from oldest to newest + assert_eq!(contract_history.len(), 2); + assert_eq!(keys[0], 1000); + assert_eq!(keys[1], 2000); + + // Fetch with an offset should offset from the newest to oldest + let contract_history = platform + .drive + .fetch_contract_with_history( + *data_contract.id().as_bytes(), + None, + 0, + None, + Some(1), + platform_version, + ) + .expect("to get contract history"); + + let keys = contract_history.keys().copied().collect::>(); + + assert_eq!(contract_history.len(), 1); + assert_eq!(keys[0], 1000); + + // Check that when we limit ny 1 we get only the most recent contract + let contract_history = platform + .drive + .fetch_contract_with_history( + *data_contract.id().as_bytes(), + None, + 0, + Some(1), + None, + platform_version, + ) + .expect("to get contract history"); + + let keys = contract_history.keys().copied().collect::>(); + + // Check that when we limit ny 1 we get only the most recent contract + assert_eq!(contract_history.len(), 1); + assert_eq!(keys[0], 2000); + } + + #[test] + fn should_return_invalid_result_if_trying_to_update_config() { + let TestData { + mut data_contract, + platform, + } = setup_test(); + + let platform_version = PlatformVersion::latest(); + + data_contract.config_mut().set_keeps_history(true); + data_contract.config_mut().set_readonly(false); + + apply_contract( + &platform, + &data_contract, + BlockInfo { + time_ms: 1000, + height: 100, + core_height: 10, + epoch: Default::default(), + }, + ); + + let updated_document_type = json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "position": 0, + }, + "newProp": { + "type": "integer", + "minimum": 0, + "position": 1, + } + }, + "required": [ + "$createdAt" + ], + "additionalProperties": false + }); + + data_contract.increment_version(); + data_contract + .set_document_schema( + "niceDocument", + updated_document_type.into(), + true, + &mut vec![], + platform_version, + ) + .expect("to be able to set document schema"); + + // It should be not possible to modify this + data_contract.config_mut().set_keeps_history(false); + + let state_transition: DataContractUpdateTransitionV0 = (data_contract, 1) + .try_into_platform_versioned(platform_version) + .expect("expected an update transition"); + + let state_transition: DataContractUpdateTransition = state_transition.into(); + + let state = platform.state.load(); + + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + + let mut execution_context = + StateTransitionExecutionContext::default_for_platform_version(platform_version) + .expect("expected a platform version"); + + let result = state_transition + .validate_state( + None, + &platform_ref, + ValidationMode::Validator, + &BlockInfo::default(), + &mut execution_context, + None, + ) + .expect("state transition to be validated"); + + assert!(!result.is_valid()); + let errors = assert_state_consensus_errors!( + result, + StateError::DataContractConfigUpdateError, + 1 + ); + let error = errors.first().expect("to have an error"); + assert_eq!( + error.additional_message(), + "contract can not change whether it keeps history: changing from true to false" + ); + } + } + + #[test] + fn test_data_contract_update_changing_various_document_type_options() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let card_game_path = "tests/supporting_files/contract/crypto-card-game/crypto-card-game-direct-purchase-creation-restricted-to-owner.json"; + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // let's construct the grovedb structure for the card game data contract + let mut contract = json_document_to_contract(card_game_path, true, platform_version) + .expect("expected to get data contract"); + + contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + let updated_card_game_path = "tests/supporting_files/contract/crypto-card-game/crypto-card-game-direct-purchase.json"; + + // let's construct the grovedb structure for the card game data contract + let mut contract_not_restricted_to_owner = + json_document_to_contract(updated_card_game_path, true, platform_version) + .expect("expected to get data contract"); + + contract_not_restricted_to_owner.set_owner_id(identity.id()); + + contract_not_restricted_to_owner.set_version(2); + + let data_contract_update_transition = DataContractUpdateTransition::new_from_data_contract( + contract_not_restricted_to_owner, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create documents batch transition"); + + let data_contract_update_serialized_transition = data_contract_update_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_update_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // There is no issue because the creator of the contract made the document + + assert_eq!(processing_result.invalid_paid_count(), 1); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let result = processing_result.into_execution_results().remove(0); + + assert!(matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::DocumentTypeUpdateError(ref error) + ), .. + } if error.data_contract_id() == &contract.id() + && error.document_type_name() == "card" + && error.additional_message() == "document type can not change creation restriction mode: changing from Owner Only to No Restrictions" + )); + } + + mod group_tests { + use super::*; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::UnpaidConsensusError; + + #[test] + fn test_data_contract_update_can_not_remove_groups() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let (identity_2, _, _) = setup_identity(&mut platform, 123, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create an initial data contract with groups + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + + data_contract.set_owner_id(identity.id()); + + { + // Add groups to the contract + let groups = data_contract.groups_mut().expect("expected groups"); + groups.insert( + 0, + Group::V0(GroupV0 { + members: [(identity.id(), 1), (identity_2.id(), 1)].into(), + required_power: 1, + }), + ); + groups.insert( + 1, + Group::V0(GroupV0 { + members: [(identity.id(), 1), (identity_2.id(), 1)].into(), + required_power: 1, + }), + ); + } + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Create an updated contract with one group removed + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + { + // Remove a group from the updated contract + let groups = updated_data_contract.groups_mut().expect("expected groups"); + groups.remove(&1).expect("expected to remove group"); + } + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create data contract update transition"); + + let data_contract_update_serialized_transition = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_update_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // Extract the error and check the message + if let [StateTransitionExecutionResult::PaidConsensusError { + error: + ConsensusError::StateError(StateError::DataContractUpdateActionNotAllowedError( + error, + )), + .. + }] = processing_result.execution_results().as_slice() + { + assert_eq!( + error.action(), + "remove group", + "expected error message to match 'remove group'" + ); + assert_eq!( + error.data_contract_id(), + data_contract.id(), + "expected the error to reference the correct data contract ID" + ); + } else { + panic!("Expected a DataContractUpdateActionNotAllowedError"); + } + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_update_can_not_alter_group() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let (identity_2, _, _) = setup_identity(&mut platform, 123, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create an initial data contract with groups + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + + data_contract.set_owner_id(identity.id()); + + { + // Add groups to the contract + let groups = data_contract.groups_mut().expect("expected groups"); + groups.insert( + 0, + Group::V0(GroupV0 { + members: [(identity.id(), 1), (identity_2.id(), 1)].into(), + required_power: 1, + }), + ); + groups.insert( + 1, + Group::V0(GroupV0 { + members: [(identity.id(), 1), (identity_2.id(), 1)].into(), + required_power: 1, + }), + ); + } + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Create an updated contract with one group removed + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + { + // Add a group to the updated contract + let groups = updated_data_contract.groups_mut().expect("expected groups"); + groups.insert( + 1, + Group::V0(GroupV0 { + members: [(identity.id(), 1), (identity_2.id(), 1)].into(), + required_power: 2, + }), + ); + } + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create data contract update transition"); + + let data_contract_update_serialized_transition = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_update_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // Extract the error and check the message + if let [StateTransitionExecutionResult::PaidConsensusError { + error: + ConsensusError::StateError(StateError::DataContractUpdateActionNotAllowedError( + error, + )), + .. + }] = processing_result.execution_results().as_slice() + { + assert_eq!( + error.action(), + "change group at position 1 is not allowed", + "expected error message to match 'change group at position 1 is not allowed'" + ); + assert_eq!( + error.data_contract_id(), + data_contract.id(), + "expected the error to reference the correct data contract ID" + ); + } else { + panic!("Expected a DataContractUpdateActionNotAllowedError"); + } + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_update_can_not_add_new_group_with_gap() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create an initial data contract with groups + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + + data_contract.set_owner_id(identity.id()); + + { + // Add groups to the contract + let groups = data_contract.groups_mut().expect("expected groups"); + groups.insert( + 0, + Group::V0(GroupV0 { + members: [(identity.id(), 1)].into(), + required_power: 1, + }), + ); + groups.insert( + 1, + Group::V0(GroupV0 { + members: [(identity.id(), 1)].into(), + required_power: 1, + }), + ); + } + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Create an updated contract with one group removed + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + { + // Remove a group from the updated contract + let groups = updated_data_contract.groups_mut().expect("expected groups"); + groups.insert( + 3, + Group::V0(GroupV0 { + members: [(identity.id(), 2)].into(), + required_power: 2, + }), + ); + } + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create data contract update transition"); + + let data_contract_update_serialized_transition = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_update_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [UnpaidConsensusError(ConsensusError::BasicError( + BasicError::NonContiguousContractGroupPositionsError(_) + ))] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_update_can_add_new_group() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let (identity_2, _, _) = setup_identity(&mut platform, 928, dash_to_credits!(0.1)); + + let (identity_3, _, _) = setup_identity(&mut platform, 8, dash_to_credits!(0.1)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create an initial data contract with groups + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + + data_contract.set_owner_id(identity.id()); + + { + // Add groups to the contract + let groups = data_contract.groups_mut().expect("expected groups"); + groups.insert( + 0, + Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 1), + (identity_3.id(), 1), + ] + .into(), + required_power: 3, + }), + ); + groups.insert( + 1, + Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 2), + (identity_3.id(), 1), + ] + .into(), + required_power: 3, + }), + ); + } + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Create an updated contract with one group removed + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + { + // Remove a group from the updated contract + let groups = updated_data_contract.groups_mut().expect("expected groups"); + groups.insert( + 2, + Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 2), + (identity_3.id(), 2), + ] + .into(), + required_power: 3, + }), + ); + } + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create data contract update transition"); + + let data_contract_update_serialized_transition = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_update_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + } + + mod token_tests { + use super::*; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::UnpaidConsensusError; + use dpp::data_contract::accessors::v1::DataContractV1Setters; + use dpp::data_contract::associated_token::token_configuration::accessors::v0::{TokenConfigurationV0Getters, TokenConfigurationV0Setters}; + use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; + use dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; + use dpp::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + use dpp::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; + use dpp::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0; + use dpp::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization; + use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; + use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; + use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; + use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; + use dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use dpp::data_contract::change_control_rules::ChangeControlRules; + use dpp::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use dpp::state_transition::proof_result::StateTransitionProofResult; + use drive::drive::Drive; + + #[test] + fn test_data_contract_update_can_add_new_token() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // ── original contract (no tokens) ───────────────────────────────── + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // ── updated contract: add a well‑formed token at position 0 ────── + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + let valid_token_cfg = { + let mut cfg = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + cfg.set_base_supply(1_000_000); + + cfg.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "credit".to_string(), + plural_form: "credits".to_string(), + }), + )]), + decimals: 8, + }, + )); + cfg + }; + + updated_data_contract.add_token(0, valid_token_cfg); + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract.clone(), + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create data contract update transition"); + + let tx_bytes = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // Prove & verify + let proof = platform + .drive + .prove_state_transition(&data_contract_update_transition, None, platform_version) + .expect("expect to prove state transition"); + let (_root_hash, result) = Drive::verify_state_transition_was_executed_with_proof( + &data_contract_update_transition, + &BlockInfo::default(), + proof.data.as_ref().expect("expected data"), + &|_| Ok(None), + platform_version, + ) + .unwrap_or_else(|e| { + panic!( + "expect to verify state transition proof {}, error is {}", + hex::encode(proof.data.expect("expected data")), + e + ) + }); + assert_matches!(result, StateTransitionProofResult::VerifiedDataContract(_)); + } + + #[test] + fn test_data_contract_update_with_token_setting_identifier_that_does_exist() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + let (identity2, _signer2, _key2) = + setup_identity(&mut platform, 93, dash_to_credits!(0.2)); + + let platform_state = platform.state.load(); + let platform_version = PlatformVersion::latest(); + + let mut original_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + original_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &original_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract"); + + let mut updated_contract = original_contract.clone(); + updated_contract.set_version(2); + + let mut token_config = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + token_config.set_base_supply(100_000); + token_config.set_manual_minting_rules(ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Identity(identity2.id()), + admin_action_takers: AuthorizedActionTakers::ContractOwner, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + })); + + token_config.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test".to_string(), + plural_form: "tests".to_string(), + }), + )]), + decimals: 8, + }, + )); + + updated_contract.add_token(0, token_config); + + let transition = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expected update transition"); + + let serialized = transition.serialize_to_bytes().expect("serialize"); + + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected processing"); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + } + #[test] + fn test_data_contract_update_with_token_setting_identifier_that_does_not_exist() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + let platform_state = platform.state.load(); + let platform_version = PlatformVersion::latest(); + + let mut original_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + original_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &original_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract"); + + let mut updated_contract = original_contract.clone(); + updated_contract.set_version(2); + + let mut token_config = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + token_config.set_base_supply(1_000_000); + + token_config.set_manual_minting_rules(ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Identity(Identifier::from( + [4; 32], + )), // doesn't exist + admin_action_takers: AuthorizedActionTakers::ContractOwner, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + })); + + token_config.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test".to_string(), + plural_form: "tests".to_string(), + }), + )]), + decimals: 8, + }, + )); + + updated_contract.add_token(0, token_config); + + let transition = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expected update transition"); + + let serialized = transition.serialize_to_bytes().expect("serialize"); + + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected processing"); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::IdentityInTokenConfigurationNotFoundError(_) + ), + .. + }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + } + + #[test] + fn test_data_contract_update_can_not_add_new_token_with_gap() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // ── original contract with token at position 0 ─────────────────── + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + data_contract.add_token( + 0, + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()), + ); + data_contract + .tokens_mut() + .expect("expected tokens") + .get_mut(&0) + .expect("expected token") + .conventions_mut() + .localizations_mut() + .insert( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test".to_string(), + plural_form: "tests".to_string(), + }), + ); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // ── updated contract: try to add token at position 2 (gap) ─────── + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + updated_data_contract.add_token( + 2, // <‑‑ non‑contiguous + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()), + ); + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create data contract update transition"); + + let tx_bytes = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [UnpaidConsensusError(ConsensusError::BasicError( + BasicError::NonContiguousContractTokenPositionsError(_) + ))] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_update_can_not_add_new_token_with_large_base_supply() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // ── original contract (no tokens) ──────────────────────────────── + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // ── updated contract: token with base_supply > i64::MAX ────────── + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + let mut huge_supply_cfg = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + huge_supply_cfg.set_base_supply(i64::MAX as u64 + 1); + + updated_data_contract.add_token(0, huge_supply_cfg); + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create data contract update transition"); + + let tx_bytes = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [UnpaidConsensusError(ConsensusError::BasicError( + BasicError::InvalidTokenBaseSupplyError(_) + ))] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_update_can_not_add_new_token_with_invalid_localization() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // ── original contract (no tokens) ──────────────────────────────── + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // ── updated contract: token with empty localization map ────────── + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + let empty_localization_cfg = { + let mut cfg = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + cfg.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::new(), // <‑‑ invalid + decimals: 8, + }, + )); + cfg + }; + + updated_data_contract.add_token(0, empty_localization_cfg); + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .expect("expect to create data contract update transition"); + + let tx_bytes = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [UnpaidConsensusError(ConsensusError::BasicError( + BasicError::MissingDefaultLocalizationError(_) + ))] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn update_token_with_missing_main_group_should_fail() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + let (identity, signer, key) = + setup_identity(&mut platform, 1234, dash_to_credits!(0.1)); + let platform_state = platform.state.load(); + let platform_version = PlatformVersion::latest(); + + let mut contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.set_owner_id(identity.id()); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .unwrap(); + + let mut updated_contract = contract.clone(); + updated_contract.set_version(2); + + let mut config = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + config.set_main_control_group(Some(1)); // Missing group + config.set_manual_minting_rules(ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::MainGroup, + admin_action_takers: AuthorizedActionTakers::MainGroup, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + })); + config.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test".to_string(), + plural_form: "tests".to_string(), + }), + )]), + decimals: 8, + }, + )); + updated_contract.add_token(0, config); + + let transition = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .unwrap(); + let tx = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[transition.serialize_to_bytes().unwrap()], + &platform_state, + &BlockInfo::default(), + &tx, + platform_version, + false, + None, + ) + .unwrap(); + + assert_matches!( + result.execution_results().as_slice(), + [UnpaidConsensusError(ConsensusError::BasicError( + BasicError::GroupPositionDoesNotExistError(_) + ))] + ); + } + + #[test] + fn update_token_with_invalid_distribution_function_should_fail() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + let (identity, signer, key) = + setup_identity(&mut platform, 1234, dash_to_credits!(0.1)); + let platform_state = platform.state.load(); + let platform_version = PlatformVersion::latest(); + + let mut contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.set_owner_id(identity.id()); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .unwrap(); + + let mut updated_contract = contract.clone(); + updated_contract.set_version(2); + + let mut config = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + config + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 100, + function: DistributionFunction::Exponential { + a: 0, + d: 0, + m: 0, + n: 0, + o: 0, + start_moment: None, + b: 0, + min_value: None, + max_value: None, + }, + }, + distribution_recipient: TokenDistributionRecipient::Identity(identity.id()), + }, + ))); + config.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test".to_string(), + plural_form: "tests".to_string(), + }), + )]), + decimals: 8, + }, + )); + updated_contract.add_token(0, config); + + let transition = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .unwrap(); + let tx = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[transition.serialize_to_bytes().unwrap()], + &platform_state, + &BlockInfo::default(), + &tx, + platform_version, + false, + None, + ) + .unwrap(); + + assert_matches!( + result.execution_results().as_slice(), + [UnpaidConsensusError(ConsensusError::BasicError( + BasicError::InvalidTokenDistributionFunctionDivideByZeroError(_) + ))] + ); + } + + #[test] + fn update_token_with_random_distribution_should_fail() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + let (identity, signer, key) = + setup_identity(&mut platform, 1234, dash_to_credits!(0.1)); + let platform_state = platform.state.load(); + let platform_version = PlatformVersion::latest(); + + let mut contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.set_owner_id(identity.id()); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .unwrap(); + + let mut updated_contract = contract.clone(); + updated_contract.set_version(2); + + let mut config = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + config + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 100, + function: DistributionFunction::Random { min: 0, max: 10 }, + }, + distribution_recipient: TokenDistributionRecipient::Identity(identity.id()), + }, + ))); + config.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test".to_string(), + plural_form: "tests".to_string(), + }), + )]), + decimals: 8, + }, + )); + updated_contract.add_token(0, config); + + let transition = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .unwrap(); + let tx = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[transition.serialize_to_bytes().unwrap()], + &platform_state, + &BlockInfo::default(), + &tx, + platform_version, + false, + None, + ) + .unwrap(); + + assert_matches!( + result.execution_results().as_slice(), + [UnpaidConsensusError(ConsensusError::BasicError( + BasicError::UnsupportedFeatureError(_) + ))] + ); + } + + #[test] + fn update_token_overwriting_existing_position_should_fail() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + let (identity, signer, key) = + setup_identity(&mut platform, 1234, dash_to_credits!(1.0)); + let platform_state = platform.state.load(); + let platform_version = PlatformVersion::latest(); + + let mut contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.set_owner_id(identity.id()); + let mut config = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + config.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test".to_string(), + plural_form: "tests".to_string(), + }), + )]), + decimals: 8, + }, + )); + + let mut config_2 = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + config_2.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test_1".to_string(), + plural_form: "tests_2".to_string(), + }), + )]), + decimals: 8, + }, + )); + contract.add_token(0, config); + + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .unwrap(); + + let mut updated_contract = contract.clone(); + updated_contract.set_version(2); + updated_contract.add_token(0, config_2); + + let transition = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + Some(0), + ) + .unwrap(); + let tx = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[transition.serialize_to_bytes().unwrap()], + &platform_state, + &BlockInfo::default(), + &tx, + platform_version, + false, + None, + ) + .unwrap(); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::DataContractUpdateActionNotAllowedError(_) + ), + .. + }] + ); + } + } + + mod keyword_updates { + use super::*; + use dpp::{ + data_contract::conversion::value::v0::DataContractValueConversionMethodsV0, + data_contracts::SystemDataContract, + document::DocumentV0Getters, + platform_value::{string_encoding::Encoding, Value}, + state_transition::{ + data_contract_create_transition::{ + methods::DataContractCreateTransitionMethodsV0, DataContractCreateTransition, + }, + StateTransition, + }, + system_data_contracts::load_system_data_contract, + tests::json_document::json_document_to_contract_with_ids, + }; + use drive::{ + drive::document::query::QueryDocumentsOutcomeV0Methods, + query::{DriveDocumentQuery, WhereClause, WhereOperator}, + }; + + // ──────────────────────────────────────────────────────────────────────── + // helpers + // ──────────────────────────────────────────────────────────────────────── + + /// Creates a contract with the supplied keywords and commits it to Drive. + /// Returns `(contract_id, create_transition)`. + fn create_contract_with_keywords( + platform: &mut TempPlatform, + identity: &Identity, + signer: &SimpleSigner, + key: &IdentityPublicKey, + keywords: &[&str], + platform_version: &PlatformVersion, + ) -> (Identifier, StateTransition) { + let base = json_document_to_contract_with_ids( + "tests/supporting_files/contract/keyword_test/keyword_base_contract.json", + None, + None, + false, + platform_version, + ) + .expect("load base contract"); + + let mut val = base.to_value(platform_version).expect("to_value"); + + val["keywords"] = Value::Array( + keywords + .iter() + .map(|k| Value::Text(k.to_string())) + .collect(), + ); + + let contract = + DataContract::from_value(val, true, platform_version).expect("from_value"); + + let create = DataContractCreateTransition::new_from_data_contract( + contract, + 2, + &identity.clone().into_partial_identity_info(), + key.id(), + signer, + platform_version, + None, + ) + .expect("create transition"); + + let tx_bytes = create.serialize_to_bytes().expect("serialize"); + + let tx = platform.drive.grove.start_transaction(); + let platform_state = platform.state.load(); + + let res = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &tx, + platform_version, + false, + None, + ) + .expect("process create"); + + assert_matches!( + res.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(tx) + .unwrap() + .expect("commit create"); + + // pull id from unique_identifiers + let contract_id = Identifier::from_string( + create + .unique_identifiers() + .first() + .unwrap() + .as_str() + .split('-') + .last() + .unwrap(), + Encoding::Base58, + ) + .unwrap(); + + (contract_id, create) + } + + /// Convenience for building and applying an **update** transition that + /// only changes the `keywords` array. + fn apply_keyword_update( + platform: &mut TempPlatform, + contract_id: Identifier, + identity: &Identity, + signer: &SimpleSigner, + key: &IdentityPublicKey, + new_keywords: &[&str], + platform_version: &PlatformVersion, + ) -> Result<(), Vec> { + // fetch existing contract + let fetched = platform + .drive + .fetch_contract(contract_id.into(), None, None, None, platform_version) + .value + .unwrap() + .unwrap(); + + let mut val = fetched.contract.to_value(platform_version).unwrap(); + + val["keywords"] = Value::Array( + new_keywords + .iter() + .map(|k| Value::Text(k.to_string())) + .collect(), + ); + + let mut updated_contract = + DataContract::from_value(val, true, platform_version).unwrap(); + updated_contract.set_version(2); + + let update = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.clone().into_partial_identity_info(), + key.id(), + 2, + 0, + signer, + platform_version, + Some(0), + ) + .expect("build update"); + + let bytes = update.serialize_to_bytes().unwrap(); + + let tx = platform.drive.grove.start_transaction(); + let platform_state = platform.state.load(); + + let outcome = platform + .platform + .process_raw_state_transitions( + &[bytes], + &platform_state, + &BlockInfo::default(), + &tx, + platform_version, + false, + None, + ) + .expect("process update"); + + if matches!( + outcome.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ) { + platform + .drive + .grove + .commit_transaction(tx) + .unwrap() + .expect("commit update"); + Ok(()) + } else { + Err(outcome.execution_results().to_vec()) + } + } + + /// Helper to read all keyword docs for a contract id. + fn keyword_docs_for_contract( + platform: &TempPlatform, + contract_id: Identifier, + platform_version: &PlatformVersion, + ) -> Vec { + let search_contract = + load_system_data_contract(SystemDataContract::KeywordSearch, platform_version) + .unwrap(); + let doc_type = search_contract + .document_type_for_name("contractKeywords") + .unwrap(); + + let mut query = DriveDocumentQuery { + contract: &search_contract, + document_type: doc_type, + internal_clauses: Default::default(), + offset: None, + limit: None, + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + query.internal_clauses.equal_clauses.insert( + "contractId".to_string(), + WhereClause { + field: "contractId".to_string(), + operator: WhereOperator::Equal, + value: contract_id.into(), + }, + ); + + let res = platform + .drive + .query_documents(query, None, false, None, None) + .unwrap(); + + res.documents() + .iter() + .map(|d| d.get("keyword").unwrap().as_str().unwrap().to_owned()) + .collect() + } + + // ──────────────────────────────────────────────────────────────────────── + // negative cases – same validation as create + // ──────────────────────────────────────────────────────────────────────── + + macro_rules! invalid_update_test { + ($name:ident, $keywords:expr, $error:pat_param) => { + #[test] + fn $name() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let (identity, signer, key) = + setup_identity(&mut platform, 958, dash_to_credits!(10.0)); + + // create initial contract with one keyword so update is allowed + let (cid, _) = create_contract_with_keywords( + &mut platform, + &identity, + &signer, + &key, + &["orig"], + &platform_version, + ); + + // try invalid update + let err = apply_keyword_update( + &mut platform, + cid, + &identity, + &signer, + &key, + &$keywords, + &platform_version, + ) + .unwrap_err(); + + assert_matches!( + err.as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError($error), + .. + }] + ); + + // original keyword docs must still be there + let docs = keyword_docs_for_contract(&platform, cid, &platform_version); + assert_eq!(docs, vec!["orig"]); + } + }; + } + + invalid_update_test!( + update_fails_too_many_keywords, + [ + "kw0", "kw1", "kw2", "kw3", "kw4", "kw5", "kw6", "kw7", "kw8", "kw9", "kw10", + "kw11", "kw12", "kw13", "kw14", "kw15", "kw16", "kw17", "kw18", "kw19", "kw20", + "kw21", "kw22", "kw23", "kw24", "kw25", "kw26", "kw27", "kw28", "kw29", "kw30", + "kw31", "kw32", "kw33", "kw34", "kw35", "kw36", "kw37", "kw38", "kw39", "kw40", + "kw41", "kw42", "kw43", "kw44", "kw45", "kw46", "kw47", "kw48", "kw49", "kw50", + ], + BasicError::TooManyKeywordsError(_) + ); + + invalid_update_test!( + update_fails_duplicate_keywords, + ["dup", "dup"], + BasicError::DuplicateKeywordsError(_) + ); + + invalid_update_test!( + update_fails_keyword_too_short, + ["hi"], + BasicError::InvalidKeywordLengthError(_) + ); + + invalid_update_test!( + update_fails_keyword_too_long, + [&"x".repeat(51)], + BasicError::InvalidKeywordLengthError(_) + ); + + // ──────────────────────────────────────────────────────────────────────── + // positive case – old docs removed, new docs inserted + // ──────────────────────────────────────────────────────────────────────── + + #[test] + fn update_keywords_replaces_search_docs() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + // initial contract with two keywords + let (cid, _) = create_contract_with_keywords( + &mut platform, + &identity, + &signer, + &key, + &["old1", "old2"], + platform_version, + ); + + // verify initial docs + let initial_docs = keyword_docs_for_contract(&platform, cid, &platform_version); + assert_eq!(initial_docs.len(), 2); + + // apply update to ["newA", "newB", "newC"] + apply_keyword_update( + &mut platform, + cid, + &identity, + &signer, + &key, + &["newA", "newB", "newC"], + platform_version, + ) + .expect("update should succeed"); + + // fetch contract – keywords updated? + let fetched = platform + .drive + .fetch_contract(cid.into(), None, None, None, platform_version) + .value + .unwrap() + .unwrap(); + assert_eq!( + *fetched.contract.keywords(), + ["newa", "newb", "newc"] + .iter() + .map(|&s| s.to_string()) + .collect::>() + ); + + // search‑contract docs updated? + let docs_after = keyword_docs_for_contract(&platform, cid, platform_version); + assert_eq!(docs_after.len(), 3); + assert!(docs_after.contains(&"newa".to_string())); + assert!(docs_after.contains(&"newb".to_string())); + assert!(docs_after.contains(&"newc".to_string())); + // old docs gone + assert!(!docs_after.contains(&"old1".to_string())); + assert!(!docs_after.contains(&"old2".to_string())); + } + } + + mod description_updates { + use super::*; + use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; + use dpp::{ + data_contract::conversion::value::v0::DataContractValueConversionMethodsV0, + data_contracts::SystemDataContract, + document::DocumentV0Getters, + platform_value::{string_encoding::Encoding, Value}, + state_transition::{ + data_contract_create_transition::{ + methods::DataContractCreateTransitionMethodsV0, DataContractCreateTransition, + }, + StateTransition, + }, + system_data_contracts::load_system_data_contract, + tests::json_document::json_document_to_contract_with_ids, + }; + use drive::{ + drive::document::query::QueryDocumentsOutcomeV0Methods, + query::{DriveDocumentQuery, WhereClause, WhereOperator}, + }; + + // ──────────────────────────────────────────────────────────────────────── + // helpers + // ──────────────────────────────────────────────────────────────────────── + + /// Creates a contract with the supplied description and commits it to Drive. + /// Returns `(contract_id, create_transition)`. + fn create_contract_with_description( + platform: &mut TempPlatform, + identity: &Identity, + signer: &SimpleSigner, + key: &IdentityPublicKey, + description: &str, + platform_version: &PlatformVersion, + ) -> (Identifier, StateTransition) { + let base = json_document_to_contract_with_ids( + "tests/supporting_files/contract/keyword_test/keyword_base_contract.json", + None, + None, + false, + platform_version, + ) + .expect("load base contract"); + + let mut val = base.to_value(platform_version).expect("to_value"); + + val["description"] = Value::Text(description.to_string()); + + let contract = + DataContract::from_value(val, true, platform_version).expect("from_value"); + + let create = DataContractCreateTransition::new_from_data_contract( + contract, + 2, + &identity.clone().into_partial_identity_info(), + key.id(), + signer, + platform_version, + None, + ) + .expect("create transition"); + + let tx_bytes = create.serialize_to_bytes().expect("serialize"); + + let tx = platform.drive.grove.start_transaction(); + let platform_state = platform.state.load(); + + let res = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &tx, + platform_version, + false, + None, + ) + .expect("process create"); + + assert_matches!( + res.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(tx) + .unwrap() + .expect("commit create"); + + // pull id from unique_identifiers + let contract_id = Identifier::from_string( + create + .unique_identifiers() + .first() + .unwrap() + .as_str() + .split('-') + .last() + .unwrap(), + Encoding::Base58, + ) + .unwrap(); + + (contract_id, create) + } + + /// Convenience for building and applying an **update** transition that + /// only changes the `description` string. + fn apply_description_update( + platform: &mut TempPlatform, + contract_id: Identifier, + identity: &Identity, + signer: &SimpleSigner, + key: &IdentityPublicKey, + new_description: &str, + platform_version: &PlatformVersion, + ) -> Result<(), Vec> { + // fetch existing contract + let fetched = platform + .drive + .fetch_contract(contract_id.into(), None, None, None, platform_version) + .value + .unwrap() + .unwrap(); + + let mut val = fetched.contract.to_value(platform_version).unwrap(); + + val["description"] = Value::Text(new_description.to_string()); + + let mut updated_contract = + DataContract::from_value(val, true, platform_version).unwrap(); + updated_contract.set_version(2); + + let update = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.clone().into_partial_identity_info(), + key.id(), + 2, + 0, + signer, + platform_version, + Some(0), + ) + .expect("build update"); + + let bytes = update.serialize_to_bytes().unwrap(); + + let tx = platform.drive.grove.start_transaction(); + let platform_state = platform.state.load(); + + let outcome = platform + .platform + .process_raw_state_transitions( + &[bytes], + &platform_state, + &BlockInfo::default(), + &tx, + platform_version, + false, + None, + ) + .expect("process update"); + + if matches!( + outcome.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ) { + platform + .drive + .grove + .commit_transaction(tx) + .unwrap() + .expect("commit update"); + Ok(()) + } else { + Err(outcome.execution_results().to_vec()) + } + } + + /// Helper to read all description docs for a contract id. + fn description_docs_for_contract( + platform: &TempPlatform, + contract_id: Identifier, + platform_version: &PlatformVersion, + ) -> String { + let search_contract = + load_system_data_contract(SystemDataContract::KeywordSearch, platform_version) + .unwrap(); + let doc_type = search_contract + .document_type_for_name("shortDescription") + .unwrap(); + + let mut query = DriveDocumentQuery { + contract: &search_contract, + document_type: doc_type, + internal_clauses: Default::default(), + offset: None, + limit: None, + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + query.internal_clauses.equal_clauses.insert( + "contractId".to_string(), + WhereClause { + field: "contractId".to_string(), + operator: WhereOperator::Equal, + value: contract_id.into(), + }, + ); + + let mut res = platform + .drive + .query_documents(query, None, false, None, None) + .expect("expected query to succeed") + .documents_owned(); + + if res.is_empty() { + panic!("expected a document description"); + } + + let first_document = res.remove(0); + + first_document + .properties() + .get_string("description") + .expect("expected description to exist") + } + + // ──────────────────────────────────────────────────────────────────────── + // negative cases – same validation as create + // ──────────────────────────────────────────────────────────────────────── + + macro_rules! invalid_update_test { + ($name:ident, $description:expr, $error:pat_param) => { + #[test] + fn $name() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let (identity, signer, key) = + setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + // create initial contract with description so update is allowed + let (cid, _) = create_contract_with_description( + &mut platform, + &identity, + &signer, + &key, + &"orig", + &platform_version, + ); + + // try invalid update + let err = apply_description_update( + &mut platform, + cid, + &identity, + &signer, + &key, + &$description, + &platform_version, + ) + .unwrap_err(); + + assert_matches!( + err.as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError($error), + .. + }] + ); + + // original description docs must still be there + let docs = description_docs_for_contract(&platform, cid, &platform_version); + assert_eq!(docs, "orig".to_string()); + } + }; + } + + invalid_update_test!( + update_fails_description_too_short, + "hi", + BasicError::InvalidDescriptionLengthError(_) + ); + + invalid_update_test!( + update_fails_description_too_long, + &"x".repeat(101), + BasicError::InvalidDescriptionLengthError(_) + ); + + // ──────────────────────────────────────────────────────────────────────── + // positive case – old docs removed, new docs inserted + // ──────────────────────────────────────────────────────────────────────── + + #[test] + fn update_description_replaces_search_docs() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + // initial contract with description + let (cid, _) = create_contract_with_description( + &mut platform, + &identity, + &signer, + &key, + "old1", + platform_version, + ); + + // verify initial docs + let initial_docs = description_docs_for_contract(&platform, cid, platform_version); + assert_eq!(initial_docs, "old1".to_string()); + + // apply update to "newA" + apply_description_update( + &mut platform, + cid, + &identity, + &signer, + &key, + "newA", + platform_version, + ) + .expect("update should succeed"); + + // fetch contract – description updated? + let fetched = platform + .drive + .fetch_contract(cid.into(), None, None, None, platform_version) + .value + .unwrap() + .unwrap(); + assert_eq!( + fetched.contract.description(), + Some("newA".to_string()).as_ref() + ); + + // search‑contract docs updated? + let docs_after = description_docs_for_contract(&platform, cid, platform_version); + assert_eq!(docs_after, "newA".to_string()); + // old docs gone + assert!(!docs_after.contains(&"old1".to_string())); + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/v1_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/v1_tests.rs new file mode 100644 index 0000000000..c492d907ef --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/tests/v1_tests.rs @@ -0,0 +1,1215 @@ +#[cfg(test)] +mod tests { + use super::super::{ + apply_contract, setup_identity, setup_test, Identifier, Identity, IdentityPublicKey, + TestData, + }; + use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::block::block_info::BlockInfo; + use dpp::consensus::ConsensusError; + use dpp::dash_to_credits; + use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; + use std::collections::BTreeMap; + + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::platform_value::BinaryData; + use dpp::serialization::PlatformSerializable; + use dpp::state_transition::data_contract_update_transition::{ + DataContractUpdateTransition, DataContractUpdateTransitionV1, + }; + + use crate::platform_types::platform_state::PlatformStateV0Methods; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; + use assert_matches::assert_matches; + use dpp::consensus::basic::BasicError; + use dpp::data_contract::accessors::v1::{DataContractV1Getters, DataContractV1Setters}; + use dpp::data_contract::group::v0::GroupV0; + use dpp::data_contract::group::Group; + use dpp::identity::signer::Signer; + use dpp::serialization::Signable; + use dpp::state_transition::StateTransitionSingleSigned; + use dpp::tests::fixtures::get_data_contract_fixture; + use dpp::version::PlatformVersion; + use drive::util::storage_flags::StorageFlags; + use simple_signer::signer::SimpleSigner; + + mod group_tests { + use super::*; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::UnpaidConsensusError; + use dpp::state_transition::StateTransition; + + #[test] + fn test_data_contract_update_can_add_new_group_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let (identity_2, _, _) = setup_identity(&mut platform, 928, dash_to_credits!(0.1)); + + let (identity_3, _, _) = setup_identity(&mut platform, 8, dash_to_credits!(0.1)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create an initial data contract with groups + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + + data_contract.set_owner_id(identity.id()); + + { + // Add groups to the contract + let groups = data_contract.groups_mut().expect("expected groups"); + groups.insert( + 0, + Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 1), + (identity_3.id(), 1), + ] + .into(), + required_power: 3, + }), + ); + groups.insert( + 1, + Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 2), + (identity_3.id(), 1), + ] + .into(), + required_power: 3, + }), + ); + } + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Create V1 update transition to add a new group at position 2 + let new_group = Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 2), + (identity_3.id(), 2), + ] + .into(), + required_power: 3, + }); + + let mut new_groups = BTreeMap::new(); + new_groups.insert(2, new_group); + + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups, + new_tokens: BTreeMap::new(), + remove_keywords: vec![], + add_keywords: vec![], + update_description: None, + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + // Sign the transition + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let data_contract_update_serialized_transition = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_update_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_update_can_not_add_new_group_with_gap_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let (identity_2, _, _) = setup_identity(&mut platform, 928, dash_to_credits!(0.1)); + + let (identity_3, _, _) = setup_identity(&mut platform, 8, dash_to_credits!(0.1)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create an initial data contract with groups at positions 0 and 1 + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + + data_contract.set_owner_id(identity.id()); + + { + let groups = data_contract.groups_mut().expect("expected groups"); + groups.insert( + 0, + Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 1), + (identity_3.id(), 1), + ] + .into(), + required_power: 3, + }), + ); + groups.insert( + 1, + Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 1), + (identity_3.id(), 1), + ] + .into(), + required_power: 3, + }), + ); + } + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Try to add a new group at position 3 (gap - should have been position 2) + let new_group = Group::V0(GroupV0 { + members: [ + (identity.id(), 1), + (identity_2.id(), 1), + (identity_3.id(), 1), + ] + .into(), + required_power: 3, + }); + + let mut new_groups = BTreeMap::new(); + new_groups.insert(3, new_group); + + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups, + new_tokens: BTreeMap::new(), + remove_keywords: vec![], + add_keywords: vec![], + update_description: None, + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let data_contract_update_serialized_transition = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_update_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError( + BasicError::NonContiguousContractGroupPositionsError(_) + ), + .. + }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + } + + mod token_tests { + use super::*; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::UnpaidConsensusError; + use dpp::state_transition::StateTransition; + use dpp::data_contract::associated_token::token_configuration::accessors::v0::{TokenConfigurationV0Getters, TokenConfigurationV0Setters}; + use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; + use dpp::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + use dpp::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; + use dpp::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0; + use dpp::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization; + + #[test] + fn test_data_contract_update_can_add_new_token_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create initial contract without tokens + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Create a valid token configuration + let valid_token_cfg = { + let mut cfg = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + cfg.set_base_supply(1_000_000); + cfg.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "credit".to_string(), + plural_form: "credits".to_string(), + }), + )]), + decimals: 8, + }, + )); + cfg + }; + + let mut new_tokens = BTreeMap::new(); + new_tokens.insert(0, valid_token_cfg); + + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups: BTreeMap::new(), + new_tokens, + remove_keywords: vec![], + add_keywords: vec![], + update_description: None, + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let tx_bytes = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_update_can_not_add_new_token_with_gap_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create initial contract with token at position 0 + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + let initial_token = { + let mut cfg = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + cfg.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "test".to_string(), + plural_form: "tests".to_string(), + }), + )]), + decimals: 8, + }, + )); + cfg + }; + data_contract.add_token(0, initial_token); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Try to add token at position 2 (gap - should be position 1) + let new_token = { + let mut cfg = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + cfg.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "coin".to_string(), + plural_form: "coins".to_string(), + }), + )]), + decimals: 8, + }, + )); + cfg + }; + + let mut new_tokens = BTreeMap::new(); + new_tokens.insert(2, new_token); // Position 2 creates a gap + + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups: BTreeMap::new(), + new_tokens, + remove_keywords: vec![], + add_keywords: vec![], + update_description: None, + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let tx_bytes = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError( + BasicError::NonContiguousContractTokenPositionsError(_) + ), + .. + }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_update_can_not_add_new_token_with_large_base_supply_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + // Create initial contract without tokens + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Try to add token with base_supply > i64::MAX + let mut huge_supply_cfg = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + huge_supply_cfg.set_base_supply(i64::MAX as u64 + 1); + + let mut new_tokens = BTreeMap::new(); + new_tokens.insert(0, huge_supply_cfg); + + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups: BTreeMap::new(), + new_tokens, + remove_keywords: vec![], + add_keywords: vec![], + update_description: None, + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let tx_bytes = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [UnpaidConsensusError(ConsensusError::BasicError( + BasicError::InvalidTokenBaseSupplyError(_) + ))] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + } + + mod basic_validation_tests { + use super::*; + use dpp::consensus::basic::data_contract::{ + DataContractUpdateTransitionConflictingKeywordError, + DataContractUpdateTransitionOverlappingFieldsError, + }; + use dpp::platform_value::Value; + use dpp::state_transition::StateTransition; + + #[test] + fn test_overlapping_schema_defs_should_fail_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Try to have the same key in both updated_schema_defs and new_schema_defs + let mut updated_schema_defs = BTreeMap::new(); + updated_schema_defs.insert( + "overlapping".to_string(), + Value::Text("updated".to_string()), + ); + + let mut new_schema_defs = BTreeMap::new(); + new_schema_defs.insert("overlapping".to_string(), Value::Text("new".to_string())); + + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs, + new_schema_defs, + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups: BTreeMap::new(), + new_tokens: BTreeMap::new(), + remove_keywords: vec![], + add_keywords: vec![], + update_description: None, + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let tx_bytes = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError( + BasicError::DataContractUpdateTransitionOverlappingFieldsError(err) + ) + )] if err.field_type() == "schema_defs" + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_overlapping_document_schemas_should_fail_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Try to have the same key in both updated_document_schemas and new_document_schemas + let mut updated_document_schemas = BTreeMap::new(); + updated_document_schemas.insert( + "overlappingDoc".to_string(), + Value::Text("updated".to_string()), + ); + + let mut new_document_schemas = BTreeMap::new(); + new_document_schemas + .insert("overlappingDoc".to_string(), Value::Text("new".to_string())); + + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas, + new_document_schemas, + new_groups: BTreeMap::new(), + new_tokens: BTreeMap::new(), + remove_keywords: vec![], + add_keywords: vec![], + update_description: None, + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let tx_bytes = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError( + BasicError::DataContractUpdateTransitionOverlappingFieldsError(err) + ) + )] if err.field_type() == "document_schemas" + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_conflicting_keywords_should_fail_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Try to add and remove the same keyword + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups: BTreeMap::new(), + new_tokens: BTreeMap::new(), + remove_keywords: vec!["conflicting".to_string()], + add_keywords: vec!["conflicting".to_string()], + update_description: None, + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let tx_bytes = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError( + BasicError::DataContractUpdateTransitionConflictingKeywordError(err) + ) + )] if err.keyword() == "conflicting" + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_description_too_short_should_fail_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Try to set a description that is too short (< 3 chars) + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups: BTreeMap::new(), + new_tokens: BTreeMap::new(), + remove_keywords: vec![], + add_keywords: vec![], + update_description: Some(Some("ab".to_string())), // 2 chars, too short + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let tx_bytes = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::InvalidDescriptionLengthError(_)) + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_description_too_long_should_fail_v1() { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + // Try to set a description that is too long (> 100 chars) + let state_transition = DataContractUpdateTransitionV1 { + update_contract_system_version: None, + id: data_contract.id(), + owner_id: data_contract.owner_id(), + revision: 2, + updated_schema_defs: BTreeMap::new(), + new_schema_defs: BTreeMap::new(), + updated_document_schemas: BTreeMap::new(), + new_document_schemas: BTreeMap::new(), + new_groups: BTreeMap::new(), + new_tokens: BTreeMap::new(), + remove_keywords: vec![], + add_keywords: vec![], + update_description: Some(Some("x".repeat(101))), // 101 chars, too long + identity_contract_nonce: 1, + user_fee_increase: 0, + signature_public_key_id: key.id(), + signature: BinaryData::default(), + }; + + let update_transition: DataContractUpdateTransition = state_transition.into(); + let mut state_transition: StateTransition = update_transition.into(); + + let signable_bytes = state_transition + .signable_bytes() + .expect("expected signable bytes"); + let signature = signer + .sign(&key, &signable_bytes) + .expect("expected to sign"); + state_transition.set_signature(signature); + + let tx_bytes = state_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[tx_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::InvalidDescriptionLengthError(_)) + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + } +} diff --git a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs index 6f3837e020..e2b398315d 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs @@ -72,7 +72,12 @@ impl Drive { } } StateTransition::DataContractUpdate(st) => { - if st.data_contract().config().keeps_history() { + // V0 has the full data contract, V1 has partial updates + let keeps_history = st + .data_contract() + .map(|dc| dc.config().keeps_history()) + .unwrap_or(false); // V1 defaults to non-historical + if keeps_history { contract_ids_to_historical_path_query(&st.modified_data_ids()) } else { contract_ids_to_non_historical_path_query(&st.modified_data_ids()) diff --git a/packages/rs-drive/src/state_transition_action/contract/data_contract_create/transformer.rs b/packages/rs-drive/src/state_transition_action/contract/data_contract_create/transformer.rs index 3dafecf3d2..2e5e8a13a0 100644 --- a/packages/rs-drive/src/state_transition_action/contract/data_contract_create/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/contract/data_contract_create/transformer.rs @@ -7,44 +7,30 @@ use dpp::ProtocolError; use platform_version::version::PlatformVersion; impl DataContractCreateTransitionAction { - /// tries to transform the DataContractCreateTransition into a DataContractCreateTransitionAction + /// tries to transform the borrowed DataContractCreateTransition into a DataContractCreateTransitionAction /// if validation is true the data contract transformation verifies that the data contract is valid /// if validation is false, the data contract base structure is created regardless of if it is valid pub fn try_from_transition( - value: DataContractCreateTransition, + value: &DataContractCreateTransition, block_info: &BlockInfo, full_validation: bool, validation_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result { match value { - DataContractCreateTransition::V0(v0) => { - Ok(DataContractCreateTransitionActionV0::try_from_transition( + DataContractCreateTransition::V0(v0) => Ok( + DataContractCreateTransitionActionV0::try_from_v0_transition( v0, block_info, full_validation, validation_operations, platform_version, )? - .into()) - } - } - } - - /// tries to transform the borrowed DataContractCreateTransition into a DataContractCreateTransitionAction - /// if validation is true the data contract transformation verifies that the data contract is valid - /// if validation is false, the data contract base structure is created regardless of if it is valid - pub fn try_from_borrowed_transition( - value: &DataContractCreateTransition, - block_info: &BlockInfo, - full_validation: bool, - validation_operations: &mut Vec, - platform_version: &PlatformVersion, - ) -> Result { - match value { - DataContractCreateTransition::V0(v0) => Ok( - DataContractCreateTransitionActionV0::try_from_borrowed_transition( - v0, + .into(), + ), + DataContractCreateTransition::V1(v1) => Ok( + DataContractCreateTransitionActionV0::try_from_v1_transition( + v1, block_info, full_validation, validation_operations, diff --git a/packages/rs-drive/src/state_transition_action/contract/data_contract_create/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/contract/data_contract_create/v0/transformer.rs index 8b829dfecc..72915e442e 100644 --- a/packages/rs-drive/src/state_transition_action/contract/data_contract_create/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/contract/data_contract_create/v0/transformer.rs @@ -1,22 +1,26 @@ use crate::state_transition_action::contract::data_contract_create::v0::DataContractCreateTransitionActionV0; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v1::DataContractV1Setters; +use dpp::data_contract::serialized_version::v1::DataContractInSerializationFormatV1; +use dpp::data_contract::serialized_version::DataContractInSerializationFormat; use dpp::prelude::DataContract; -use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; +use dpp::state_transition::data_contract_create_transition::{ + DataContractCreateTransitionV0, DataContractCreateTransitionV1, +}; use dpp::validation::operations::ProtocolValidationOperation; use dpp::ProtocolError; use platform_version::version::PlatformVersion; impl DataContractCreateTransitionActionV0 { - pub(in crate::state_transition_action::contract::data_contract_create) fn try_from_transition( - value: DataContractCreateTransitionV0, + pub(in crate::state_transition_action::contract::data_contract_create) fn try_from_v0_transition( + value: &DataContractCreateTransitionV0, block_info: &BlockInfo, full_validation: bool, validation_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result { let mut data_contract = DataContract::try_from_platform_versioned( - value.data_contract, + value.data_contract.clone(), full_validation, validation_operations, platform_version, @@ -31,26 +35,66 @@ impl DataContractCreateTransitionActionV0 { }) } - pub(in crate::state_transition_action::contract::data_contract_create) fn try_from_borrowed_transition( - value: &DataContractCreateTransitionV0, + pub(in crate::state_transition_action::contract::data_contract_create) fn try_from_v1_transition( + value: &DataContractCreateTransitionV1, block_info: &BlockInfo, full_validation: bool, validation_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result { + let DataContractCreateTransitionV1 { + owner_id, + config, + schema_defs, + document_schemas, + groups, + tokens, + keywords, + description, + identity_nonce, + user_fee_increase, + .. + } = value; + + // Generate contract ID from owner_id and identity_nonce + let id = DataContract::generate_data_contract_id_v0(*owner_id, *identity_nonce); + + // Create a serialization format from the V1 transition fields + let serialization_format = + DataContractInSerializationFormat::V1(DataContractInSerializationFormatV1 { + id, + config: *config, + version: 1, // New contract starts at version 1 + owner_id: *owner_id, + schema_defs: schema_defs.clone(), + document_schemas: document_schemas.clone(), + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: groups.clone(), + tokens: tokens.clone(), + keywords: keywords.clone(), + description: description.clone(), + }); + let mut data_contract = DataContract::try_from_platform_versioned( - value.data_contract.clone(), + serialization_format, full_validation, validation_operations, platform_version, )?; + data_contract.set_created_at(Some(block_info.time_ms)); data_contract.set_created_at_epoch(Some(block_info.epoch.index)); data_contract.set_created_at_block_height(Some(block_info.height)); + Ok(DataContractCreateTransitionActionV0 { data_contract, - identity_nonce: value.identity_nonce, - user_fee_increase: value.user_fee_increase, + identity_nonce: *identity_nonce, + user_fee_increase: *user_fee_increase, }) } } diff --git a/packages/rs-drive/src/state_transition_action/contract/data_contract_update/mod.rs b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/mod.rs index cf1811e0da..7fc2bf1d68 100644 --- a/packages/rs-drive/src/state_transition_action/contract/data_contract_update/mod.rs +++ b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/mod.rs @@ -2,17 +2,23 @@ pub mod transformer; /// v0 pub mod v0; +/// v1 +pub mod v1; use crate::state_transition_action::contract::data_contract_update::v0::DataContractUpdateTransitionActionV0; +use crate::state_transition_action::contract::data_contract_update::v1::DataContractUpdateTransitionActionV1; use derive_more::From; use dpp::data_contract::DataContract; use dpp::prelude::{IdentityNonce, UserFeeIncrease}; /// data contract update transition action #[derive(Debug, Clone, From)] +#[allow(clippy::large_enum_variant)] pub enum DataContractUpdateTransitionAction { /// v0 V0(DataContractUpdateTransitionActionV0), + /// v1 + V1(DataContractUpdateTransitionActionV1), } impl DataContractUpdateTransitionAction { @@ -20,12 +26,14 @@ impl DataContractUpdateTransitionAction { pub fn data_contract(self) -> DataContract { match self { DataContractUpdateTransitionAction::V0(transition) => transition.data_contract, + DataContractUpdateTransitionAction::V1(transition) => transition.data_contract, } } /// data contract ref pub fn data_contract_ref(&self) -> &DataContract { match self { DataContractUpdateTransitionAction::V0(transition) => &transition.data_contract, + DataContractUpdateTransitionAction::V1(transition) => &transition.data_contract, } } @@ -33,6 +41,7 @@ impl DataContractUpdateTransitionAction { pub fn data_contract_mut(&mut self) -> &mut DataContract { match self { DataContractUpdateTransitionAction::V0(transition) => &mut transition.data_contract, + DataContractUpdateTransitionAction::V1(transition) => &mut transition.data_contract, } } @@ -42,6 +51,9 @@ impl DataContractUpdateTransitionAction { DataContractUpdateTransitionAction::V0(transition) => { transition.identity_contract_nonce } + DataContractUpdateTransitionAction::V1(transition) => { + transition.identity_contract_nonce + } } } @@ -49,6 +61,17 @@ impl DataContractUpdateTransitionAction { pub fn user_fee_increase(&self) -> UserFeeIncrease { match self { DataContractUpdateTransitionAction::V0(transition) => transition.user_fee_increase, + DataContractUpdateTransitionAction::V1(transition) => transition.user_fee_increase, + } + } + + /// old data contract ref (only available for V1) + pub fn old_data_contract_ref(&self) -> Option<&DataContract> { + match self { + DataContractUpdateTransitionAction::V0(_) => None, + DataContractUpdateTransitionAction::V1(transition) => { + Some(&transition.old_data_contract) + } } } } diff --git a/packages/rs-drive/src/state_transition_action/contract/data_contract_update/transformer.rs b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/transformer.rs index adf23f48b5..2f8e632727 100644 --- a/packages/rs-drive/src/state_transition_action/contract/data_contract_update/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/transformer.rs @@ -1,57 +1,55 @@ +use crate::drive::Drive; +use crate::error::Error; use crate::state_transition_action::contract::data_contract_update::v0::DataContractUpdateTransitionActionV0; +use crate::state_transition_action::contract::data_contract_update::v1::DataContractUpdateTransitionActionV1; use crate::state_transition_action::contract::data_contract_update::DataContractUpdateTransitionAction; use dpp::block::block_info::BlockInfo; use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; use dpp::validation::operations::ProtocolValidationOperation; -use dpp::ProtocolError; +use dpp::validation::ConsensusValidationResult; +use grovedb::TransactionArg; use platform_version::version::PlatformVersion; impl DataContractUpdateTransitionAction { - /// tries to transform the DataContractUpdateTransition into a DataContractUpdateTransitionAction - /// if validation is true the data contract transformation verifies that the data contract is valid - /// if validation is false, the data contract base structure is created regardless of if it is valid - pub fn try_from_transition( - value: DataContractUpdateTransition, - block_info: &BlockInfo, - full_validation: bool, - validation_operations: &mut Vec, - platform_version: &PlatformVersion, - ) -> Result { - match value { - DataContractUpdateTransition::V0(v0) => { - Ok(DataContractUpdateTransitionActionV0::try_from_transition( - v0, - block_info, - full_validation, - validation_operations, - platform_version, - )? - .into()) - } - } - } - - /// tries to transform the borrowed DataContractUpdateTransition into a DataContractUpdateTransitionAction - /// if validation is true the data contract transformation verifies that the data contract is valid - /// if validation is false, the data contract base structure is created regardless of if it is valid + /// Tries to transform the borrowed DataContractUpdateTransition into a DataContractUpdateTransitionAction. + /// This method supports both V0 and V1 transitions. + /// V1 transitions require Drive access to fetch the old contract from state. + /// + /// If validation is true, the data contract transformation verifies that the data contract is valid. + /// If validation is false, the data contract base structure is created regardless of if it is valid. pub fn try_from_borrowed_transition( value: &DataContractUpdateTransition, + drive: &Drive, + transaction: TransactionArg, block_info: &BlockInfo, full_validation: bool, validation_operations: &mut Vec, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result, Error> { match value { - DataContractUpdateTransition::V0(v0) => Ok( + DataContractUpdateTransition::V0(v0) => Ok(ConsensusValidationResult::new_with_data( DataContractUpdateTransitionActionV0::try_from_borrowed_transition( v0, block_info, full_validation, validation_operations, platform_version, - )? + ) + .map_err(|e| Error::Protocol(Box::new(e)))? .into(), - ), + )), + DataContractUpdateTransition::V1(v1) => { + let validation_result = DataContractUpdateTransitionActionV1::try_from_transition( + v1, + drive, + transaction, + block_info, + full_validation, + validation_operations, + platform_version, + )?; + Ok(validation_result.map(|action| action.into())) + } } } } diff --git a/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v0/transformer.rs index 413320d903..fd35459131 100644 --- a/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v0/transformer.rs @@ -8,30 +8,10 @@ use dpp::ProtocolError; use platform_version::version::PlatformVersion; impl DataContractUpdateTransitionActionV0 { - pub(in crate::state_transition_action::contract::data_contract_update) fn try_from_transition( - value: DataContractUpdateTransitionV0, - block_info: &BlockInfo, - full_validation: bool, - validation_operations: &mut Vec, - platform_version: &PlatformVersion, - ) -> Result { - let mut data_contract = DataContract::try_from_platform_versioned( - value.data_contract, - full_validation, - validation_operations, - platform_version, - )?; - data_contract.set_updated_at(Some(block_info.time_ms)); - data_contract.set_updated_at_epoch(Some(block_info.epoch.index)); - data_contract.set_updated_at_block_height(Some(block_info.height)); - Ok(DataContractUpdateTransitionActionV0 { - data_contract, - identity_contract_nonce: value.identity_contract_nonce, - user_fee_increase: value.user_fee_increase, - }) - } - - pub(in crate::state_transition_action::contract::data_contract_update) fn try_from_borrowed_transition( + /// Tries to transform the borrowed DataContractUpdateTransitionV0 into a DataContractUpdateTransitionActionV0. + /// If validation is true, the data contract transformation verifies that the data contract is valid. + /// If validation is false, the data contract base structure is created regardless of if it is valid. + pub fn try_from_borrowed_transition( value: &DataContractUpdateTransitionV0, block_info: &BlockInfo, full_validation: bool, diff --git a/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v1/mod.rs b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v1/mod.rs new file mode 100644 index 0000000000..152d80b51a --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v1/mod.rs @@ -0,0 +1,44 @@ +/// transformer +pub mod transformer; + +use std::collections::BTreeMap; + +use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; +use dpp::data_contract::group::Group; +use dpp::data_contract::{ + DataContract, DocumentName, GroupContractPosition, TokenContractPosition, +}; +use dpp::platform_value::Value; +use dpp::prelude::{IdentityNonce, UserFeeIncrease}; + +/// Data contract update transition action v1. +/// This version is used for V1 state transitions that contain partial update information +/// and require fetching the old contract from state. +#[derive(Debug, Clone)] +pub struct DataContractUpdateTransitionActionV1 { + /// The existing data contract before the update (fetched from state). + pub old_data_contract: DataContract, + /// The new data contract after applying the update. + pub data_contract: DataContract, + /// Identity contract nonce. + pub identity_contract_nonce: IdentityNonce, + /// Fee multiplier. + pub user_fee_increase: UserFeeIncrease, + /// Updated document schemas (for V1 transitions). + /// These are schemas for existing document types that are being modified. + pub updated_document_schemas: BTreeMap, + /// New document schemas (for V1 transitions). + /// These are schemas for new document types being added. + pub new_document_schemas: BTreeMap, + /// New groups being added (for V1 transitions). + pub new_groups: BTreeMap, + /// New tokens being added (for V1 transitions). + pub new_tokens: BTreeMap, + /// Keywords to remove (for V1 transitions). + pub remove_keywords: Vec, + /// Keywords to add (for V1 transitions). + pub add_keywords: Vec, + /// Updated description (for V1 transitions). + /// None = don't update, Some(None) = clear description, Some(Some(value)) = set new description. + pub update_description: Option>, +} diff --git a/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v1/transformer.rs b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v1/transformer.rs new file mode 100644 index 0000000000..3fffcdae3a --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/contract/data_contract_update/v1/transformer.rs @@ -0,0 +1,70 @@ +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::contract::data_contract_update::v1::DataContractUpdateTransitionActionV1; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::errors::DataContractNotPresentError; +use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV1; +use dpp::validation::operations::ProtocolValidationOperation; +use dpp::validation::ConsensusValidationResult; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; + +impl DataContractUpdateTransitionActionV1 { + /// Transforms a V1 update transition into an action by fetching the old contract + /// from Drive and applying the updates. + pub(in crate::state_transition_action::contract::data_contract_update) fn try_from_transition( + value: &DataContractUpdateTransitionV1, + drive: &Drive, + transaction: TransactionArg, + block_info: &BlockInfo, + full_validation: bool, + validation_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result, Error> { + // Fetch the old contract from Drive + let old_data_contract = drive + .get_contract_with_fetch_info_and_fee( + value.id.to_buffer(), + None, + false, + transaction, + platform_version, + )? + .1 + .ok_or_else(|| { + Error::Protocol(Box::new(ProtocolError::DataContractNotPresentError( + DataContractNotPresentError::new(value.id), + ))) + })? + .contract + .clone(); + + // Build the new contract by applying updates to the old contract + let validation_result = old_data_contract + .apply_update( + value.into(), + block_info, + full_validation, + validation_operations, + platform_version, + ) + .map_err(|e| Error::Protocol(Box::new(e)))?; + + Ok( + validation_result.map(|new_data_contract| DataContractUpdateTransitionActionV1 { + old_data_contract, + data_contract: new_data_contract, + identity_contract_nonce: value.identity_contract_nonce, + user_fee_increase: value.user_fee_increase, + updated_document_schemas: value.updated_document_schemas.clone(), + new_document_schemas: value.new_document_schemas.clone(), + new_groups: value.new_groups.clone(), + new_tokens: value.new_tokens.clone(), + remove_keywords: value.remove_keywords.clone(), + add_keywords: value.add_keywords.clone(), + update_description: value.update_description.clone(), + }), + ) + } +} diff --git a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs index 38a9d977f7..a4960497c5 100644 --- a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs @@ -213,6 +213,9 @@ impl BumpIdentityDataContractNonceAction { DataContractUpdateTransition::V0(v0) => { BumpIdentityDataContractNonceActionV0::from_data_contract_update(v0).into() } + DataContractUpdateTransition::V1(v1) => { + BumpIdentityDataContractNonceActionV0::from_data_contract_update_v1(v1).into() + } } } @@ -224,6 +227,10 @@ impl BumpIdentityDataContractNonceAction { DataContractUpdateTransition::V0(v0) => { BumpIdentityDataContractNonceActionV0::from_borrowed_data_contract_update(v0).into() } + DataContractUpdateTransition::V1(v1) => { + BumpIdentityDataContractNonceActionV0::from_borrowed_data_contract_update_v1(v1) + .into() + } } } @@ -235,6 +242,10 @@ impl BumpIdentityDataContractNonceAction { DataContractUpdateTransitionAction::V0(v0) => { BumpIdentityDataContractNonceActionV0::from_data_contract_update_action(v0).into() } + DataContractUpdateTransitionAction::V1(v1) => { + BumpIdentityDataContractNonceActionV0::from_data_contract_update_action_v1(v1) + .into() + } } } @@ -247,6 +258,12 @@ impl BumpIdentityDataContractNonceAction { BumpIdentityDataContractNonceActionV0::from_borrowed_data_contract_update_action(v0) .into() } + DataContractUpdateTransitionAction::V1(v1) => { + BumpIdentityDataContractNonceActionV0::from_borrowed_data_contract_update_action_v1( + v1, + ) + .into() + } } } } diff --git a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/v0/transformer.rs index d9b2bf74e9..6de16472bb 100644 --- a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/v0/transformer.rs @@ -2,11 +2,12 @@ use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::platform_value::Identifier; use dpp::prelude::UserFeeIncrease; use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; -use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; +use dpp::state_transition::data_contract_update_transition::{DataContractUpdateTransitionV0, DataContractUpdateTransitionV1}; use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use crate::state_transition_action::contract::data_contract_update::v0::DataContractUpdateTransitionActionV0; +use crate::state_transition_action::contract::data_contract_update::v1::DataContractUpdateTransitionActionV1; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{DocumentBaseTransitionAction, DocumentBaseTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0}; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceActionV0; @@ -156,6 +157,33 @@ impl BumpIdentityDataContractNonceActionV0 { } } + /// from data contract update V1 + pub fn from_data_contract_update_v1(value: DataContractUpdateTransitionV1) -> Self { + let DataContractUpdateTransitionV1 { + id, + owner_id, + identity_contract_nonce, + user_fee_increase, + .. + } = value; + BumpIdentityDataContractNonceActionV0 { + identity_id: owner_id, + data_contract_id: id, + identity_contract_nonce, + user_fee_increase, + } + } + + /// from borrowed data contract update V1 + pub fn from_borrowed_data_contract_update_v1(value: &DataContractUpdateTransitionV1) -> Self { + BumpIdentityDataContractNonceActionV0 { + identity_id: value.owner_id, + data_contract_id: value.id, + identity_contract_nonce: value.identity_contract_nonce, + user_fee_increase: value.user_fee_increase, + } + } + /// from data contract update action pub fn from_data_contract_update_action(value: DataContractUpdateTransitionActionV0) -> Self { let DataContractUpdateTransitionActionV0 { @@ -189,4 +217,40 @@ impl BumpIdentityDataContractNonceActionV0 { user_fee_increase: *user_fee_increase, } } + + /// from data contract update action v1 + pub fn from_data_contract_update_action_v1( + value: DataContractUpdateTransitionActionV1, + ) -> Self { + let DataContractUpdateTransitionActionV1 { + data_contract, + identity_contract_nonce, + user_fee_increase, + .. + } = value; + BumpIdentityDataContractNonceActionV0 { + identity_id: data_contract.owner_id(), + data_contract_id: data_contract.id(), + identity_contract_nonce, + user_fee_increase, + } + } + + /// from borrowed data contract update action v1 + pub fn from_borrowed_data_contract_update_action_v1( + value: &DataContractUpdateTransitionActionV1, + ) -> Self { + let DataContractUpdateTransitionActionV1 { + data_contract, + identity_contract_nonce, + user_fee_increase, + .. + } = value; + BumpIdentityDataContractNonceActionV0 { + identity_id: data_contract.owner_id(), + data_contract_id: data_contract.id(), + identity_contract_nonce: *identity_contract_nonce, + user_fee_increase: *user_fee_increase, + } + } } diff --git a/packages/rs-drive/src/state_transition_action/system/bump_identity_nonce_action/transformer.rs b/packages/rs-drive/src/state_transition_action/system/bump_identity_nonce_action/transformer.rs index ae5afef5ff..006f4596f0 100644 --- a/packages/rs-drive/src/state_transition_action/system/bump_identity_nonce_action/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/system/bump_identity_nonce_action/transformer.rs @@ -55,6 +55,9 @@ impl BumpIdentityNonceAction { DataContractCreateTransition::V0(v0) => { BumpIdentityNonceActionV0::from_contract_create(v0).into() } + DataContractCreateTransition::V1(v1) => { + BumpIdentityNonceActionV0::from_contract_create_v1(v1).into() + } } } @@ -66,6 +69,9 @@ impl BumpIdentityNonceAction { DataContractCreateTransition::V0(v0) => { BumpIdentityNonceActionV0::from_borrowed_contract_create(v0).into() } + DataContractCreateTransition::V1(v1) => { + BumpIdentityNonceActionV0::from_borrowed_contract_create_v1(v1).into() + } } } diff --git a/packages/rs-drive/src/state_transition_action/system/bump_identity_nonce_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/system/bump_identity_nonce_action/v0/transformer.rs index 8c5520f7d7..645b699b23 100644 --- a/packages/rs-drive/src/state_transition_action/system/bump_identity_nonce_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/system/bump_identity_nonce_action/v0/transformer.rs @@ -4,7 +4,9 @@ use crate::state_transition_action::identity::identity_credit_withdrawal::v0::Id use crate::state_transition_action::identity::identity_update::v0::IdentityUpdateTransitionActionV0; use crate::state_transition_action::system::bump_identity_nonce_action::BumpIdentityNonceActionV0; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; +use dpp::state_transition::data_contract_create_transition::{ + DataContractCreateTransitionV0, DataContractCreateTransitionV1, +}; use dpp::state_transition::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; use dpp::state_transition::identity_credit_withdrawal_transition::accessors::IdentityCreditWithdrawalTransitionAccessorsV0; use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; @@ -102,6 +104,30 @@ impl BumpIdentityNonceActionV0 { } } + /// from contract create V1 + pub fn from_contract_create_v1(value: DataContractCreateTransitionV1) -> Self { + let DataContractCreateTransitionV1 { + owner_id, + identity_nonce, + user_fee_increase, + .. + } = value; + BumpIdentityNonceActionV0 { + identity_id: owner_id, + identity_nonce, + user_fee_increase, + } + } + + /// from borrowed contract create V1 + pub fn from_borrowed_contract_create_v1(value: &DataContractCreateTransitionV1) -> Self { + BumpIdentityNonceActionV0 { + identity_id: value.owner_id, + identity_nonce: value.identity_nonce, + user_fee_increase: value.user_fee_increase, + } + } + /// from contract create action pub fn from_contract_create_action(value: DataContractCreateTransitionActionV0) -> Self { let DataContractCreateTransitionActionV0 { diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index e04c54f25e..8f41b43b53 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -19,7 +19,6 @@ use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dpp::prelude::{AddressNonce, Identifier}; use dpp::state_transition::address_credit_withdrawal_transition::accessors::AddressCreditWithdrawalTransitionAccessorsV0; use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; -use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; @@ -99,29 +98,49 @@ impl Drive { Ok((root_hash, VerifiedDataContract(contract))) } StateTransition::DataContractUpdate(data_contract_update) => { - // we expect to get a contract that matches the state transition - let keeps_history = data_contract_update - .data_contract() - .config() - .keeps_history(); - let (root_hash, contract) = Drive::verify_contract( - proof, - Some(keeps_history), - false, - true, - data_contract_update.data_contract().id().into_buffer(), - platform_version, - )?; - let contract = contract.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain contract with id {} expected to exist because of state transition (update", data_contract_update.data_contract().id()))))?; - let contract_for_serialization: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version)?; - if let Some(mismatch) = - contract_for_serialization.first_mismatch(data_contract_update.data_contract()) - { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after update with id {}: {}", data_contract_update.data_contract().id(), mismatch)))); + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; + + match data_contract_update { + DataContractUpdateTransition::V0(v0) => { + // V0 has the full data contract embedded + let keeps_history = v0.data_contract.config().keeps_history(); + let (root_hash, contract) = Drive::verify_contract( + proof, + Some(keeps_history), + false, + true, + v0.data_contract.id().into_buffer(), + platform_version, + )?; + let contract = contract.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain contract with id {} expected to exist because of state transition (update)", v0.data_contract.id()))))?; + let contract_for_serialization: DataContractInSerializationFormat = + contract + .clone() + .try_into_platform_versioned(platform_version)?; + if let Some(mismatch) = + contract_for_serialization.first_mismatch(&v0.data_contract) + { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after update with id {}: {}", v0.data_contract.id(), mismatch)))); + } + Ok((root_hash, VerifiedDataContract(contract))) + } + DataContractUpdateTransition::V1(v1) => { + // V1 has individual fields, not a full data contract + // We verify the contract exists and get it from the proof + let (root_hash, contract) = Drive::verify_contract( + proof, + None, // We don't know keeps_history from V1 transition alone + false, + true, + v1.id.into_buffer(), + platform_version, + )?; + let contract = contract.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain contract with id {} expected to exist because of state transition (update)", v1.id))))?; + // For V1, we can't do a full mismatch check since we only have partial updates + // The contract in the proof should have the updates applied + Ok((root_hash, VerifiedDataContract(contract))) + } } - Ok((root_hash, VerifiedDataContract(contract))) } StateTransition::Batch(documents_batch_transition) => { if documents_batch_transition.transitions_len() > 1 { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs index ea970e7d98..5be1da3efe 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs @@ -26,8 +26,13 @@ pub struct DataContractMethodVersions { pub validate_update: FeatureVersion, pub schema: FeatureVersion, pub validate_groups: FeatureVersion, + pub validate_tokens: FeatureVersion, + pub validate_keywords: FeatureVersion, + pub validate_schema_defs_update: FeatureVersion, pub equal_ignoring_time_fields: FeatureVersion, pub registration_cost: FeatureVersion, + pub update_contract_cost: FeatureVersion, + pub apply_update: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs index 77619d972d..ea5acedb77 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs @@ -24,8 +24,13 @@ pub const CONTRACT_VERSIONS_V1: DPPContractVersions = DPPContractVersions { validate_update: 0, schema: 0, validate_groups: 0, + validate_tokens: 0, + validate_keywords: 0, + validate_schema_defs_update: 0, equal_ignoring_time_fields: 0, registration_cost: 0, + update_contract_cost: 0, + apply_update: 0, }, document_type_versions: DocumentTypeVersions { index_versions: DocumentTypeIndexVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs index 891323d771..32d200ea07 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs @@ -24,8 +24,13 @@ pub const CONTRACT_VERSIONS_V2: DPPContractVersions = DPPContractVersions { validate_update: 0, schema: 0, validate_groups: 0, + validate_tokens: 0, + validate_keywords: 0, + validate_schema_defs_update: 0, equal_ignoring_time_fields: 0, - registration_cost: 1, //changed to version 1 + registration_cost: 1, //changed to version 1 + update_contract_cost: 1, //changed to version 1 + apply_update: 0, }, document_type_versions: DocumentTypeVersions { index_versions: DocumentTypeIndexVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs index 1f20fd2793..9ccc4f410f 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs @@ -25,8 +25,13 @@ pub const CONTRACT_VERSIONS_V3: DPPContractVersions = DPPContractVersions { validate_update: 0, schema: 0, validate_groups: 0, + validate_tokens: 0, + validate_keywords: 0, + validate_schema_defs_update: 0, equal_ignoring_time_fields: 0, registration_cost: 1, + update_contract_cost: 1, + apply_update: 0, }, document_type_versions: DocumentTypeVersions { index_versions: DocumentTypeIndexVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs index b1a027dc43..d8983bc07b 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs @@ -2,6 +2,7 @@ use versioned_feature_core::FeatureVersionBounds; pub mod v1; pub mod v2; +pub mod v3; #[derive(Clone, Debug, Default)] pub struct DPPStateTransitionSerializationVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs new file mode 100644 index 0000000000..cbd3fd8c6a --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs @@ -0,0 +1,135 @@ +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::{ + DPPStateTransitionSerializationVersions, DocumentFeatureVersionBounds, +}; +use versioned_feature_core::FeatureVersionBounds; + +pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V3: DPPStateTransitionSerializationVersions = + DPPStateTransitionSerializationVersions { + identity_public_key_in_creation: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + identity_create_from_addresses_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + identity_create_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + identity_update_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + identity_top_up_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + identity_top_up_from_addresses_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + identity_credit_withdrawal_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + identity_credit_transfer_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + identity_credit_transfer_to_addresses_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + masternode_vote_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + contract_create_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + contract_update_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 1, + default_current_version: 1, + }, + batch_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 1, + default_current_version: 1, + }, + document_base_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 1, + }, + document_create_state_transition: DocumentFeatureVersionBounds { + bounds: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }, + document_replace_state_transition: DocumentFeatureVersionBounds { + bounds: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }, + document_delete_state_transition: DocumentFeatureVersionBounds { + bounds: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }, + document_transfer_state_transition: DocumentFeatureVersionBounds { + bounds: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }, + document_update_price_state_transition: DocumentFeatureVersionBounds { + bounds: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }, + document_purchase_state_transition: DocumentFeatureVersionBounds { + bounds: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }, + address_funds_transfer_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + address_funding_from_asset_lock_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + address_credit_withdrawal_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 2316360624..fc8364f820 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -5,6 +5,7 @@ pub mod v4; pub mod v5; pub mod v6; pub mod v7; +pub mod v8; use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs new file mode 100644 index 0000000000..f61bdd162f --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -0,0 +1,221 @@ +use crate::version::drive_abci_versions::drive_abci_validation_versions::{ + DriveAbciAssetLockValidationVersions, DriveAbciDocumentsStateTransitionValidationVersions, + DriveAbciStateTransitionCommonValidationVersions, DriveAbciStateTransitionValidationVersion, + DriveAbciStateTransitionValidationVersions, DriveAbciValidationConstants, + DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, + DriveAbciValidationVersions, PenaltyAmounts, +}; + +// Introduced in protocol version 12 (3.1) +pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = + DriveAbciValidationVersions { + state_transitions: DriveAbciStateTransitionValidationVersions { + common_validation_methods: DriveAbciStateTransitionCommonValidationVersions { + asset_locks: DriveAbciAssetLockValidationVersions { + fetch_asset_lock_transaction_output_sync: 0, + verify_asset_lock_is_not_spent_and_has_enough_balance: 0, + }, + validate_identity_public_key_contract_bounds: 0, + validate_identity_public_key_ids_dont_exist_in_state: 0, + validate_identity_public_key_ids_exist_in_state: 0, + validate_state_transition_identity_signed: 0, + validate_unique_identity_public_key_hashes_in_state: 1, + validate_master_key_uniqueness: 0, + validate_non_masternode_identity_exists: 0, + validate_identity_exists: 0, + }, + max_asset_lock_usage_attempts: 16, + identity_create_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(0), + identity_signatures: Some(0), + nonce: None, + state: 0, + transform_into_action: 0, + }, + identity_update_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(0), + identity_signatures: Some(0), + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_top_up_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + identity_credit_withdrawal_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(1), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_credit_withdrawal_state_transition_purpose_matches_requirements: 0, + identity_credit_transfer_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_credit_transfer_to_addresses_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + masternode_vote_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: Some(0), + identity_signatures: None, + nonce: Some(1), + state: 0, + transform_into_action: 0, + }, + masternode_vote_state_transition_balance_pre_check: 0, + contract_create_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(1), + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + contract_update_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(1), // changed + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 1, // changed + transform_into_action: 1, // changed + }, + batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { + basic_structure: 0, + advanced_structure: 0, + state: 0, + revision: 0, + transform_into_action: 0, + data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { + bindings: 0, + triggers: DriveAbciValidationDataTriggerVersions { + create_contact_request_data_trigger: 0, + create_domain_data_trigger: 0, + create_identity_data_trigger: 0, + create_feature_flag_data_trigger: 0, + create_masternode_reward_shares_data_trigger: 0, + delete_withdrawal_data_trigger: 0, + reject_data_trigger: 0, + }, + }, + is_allowed: 0, + document_create_transition_structure_validation: 0, + document_delete_transition_structure_validation: 0, + document_replace_transition_structure_validation: 0, + document_transfer_transition_structure_validation: 0, + document_purchase_transition_structure_validation: 0, + document_update_price_transition_structure_validation: 0, + document_base_transition_state_validation: 0, + document_create_transition_state_validation: 1, + document_delete_transition_state_validation: 0, + document_replace_transition_state_validation: 0, + document_transfer_transition_state_validation: 0, + document_purchase_transition_state_validation: 0, + document_update_price_transition_state_validation: 0, + token_mint_transition_structure_validation: 0, + token_burn_transition_structure_validation: 0, + token_transfer_transition_structure_validation: 0, + token_mint_transition_state_validation: 0, + token_burn_transition_state_validation: 0, + token_transfer_transition_state_validation: 0, + token_base_transition_structure_validation: 0, + token_base_transition_state_validation: 0, + token_freeze_transition_structure_validation: 0, + token_unfreeze_transition_structure_validation: 0, + token_freeze_transition_state_validation: 0, + token_unfreeze_transition_state_validation: 0, + token_destroy_frozen_funds_transition_structure_validation: 0, + token_destroy_frozen_funds_transition_state_validation: 0, + token_emergency_action_transition_structure_validation: 0, + token_emergency_action_transition_state_validation: 0, + token_config_update_transition_structure_validation: 0, + token_config_update_transition_state_validation: 0, + token_base_transition_group_action_validation: 0, + token_claim_transition_structure_validation: 0, + token_claim_transition_state_validation: 0, + token_direct_purchase_transition_structure_validation: 0, + token_direct_purchase_transition_state_validation: 0, + token_set_price_for_direct_purchase_transition_structure_validation: 0, + token_set_price_for_direct_purchase_transition_state_validation: 0, + }, + identity_create_from_addresses_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(0), + identity_signatures: Some(0), + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_top_up_from_addresses_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + address_credit_withdrawal: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + address_funds_from_asset_lock: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(0), + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + address_funds_transfer: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + }, + has_nonce_validation: 1, + has_address_witness_validation: 0, + validate_address_witnesses: 0, + process_state_transition: 0, + state_transition_to_execution_event_for_check_tx: 0, + penalties: PenaltyAmounts { + identity_id_not_correct: 50000000, + unique_key_already_present: 10000000, + validation_of_added_keys_structure_failure: 10000000, + validation_of_added_keys_proof_of_possession_failure: 50000000, + address_funds_insufficient_balance: 10000000, + }, + event_constants: DriveAbciValidationConstants { + maximum_vote_polls_to_process: 2, + maximum_contenders_to_consider: 100, + }, + }; diff --git a/packages/rs-platform-version/src/version/v12.rs b/packages/rs-platform-version/src/version/v12.rs index 486f4be1fb..9de31a740c 100644 --- a/packages/rs-platform-version/src/version/v12.rs +++ b/packages/rs-platform-version/src/version/v12.rs @@ -8,7 +8,7 @@ use crate::version::dpp_versions::dpp_identity_versions::v1::IDENTITY_VERSIONS_V use crate::version::dpp_versions::dpp_method_versions::v2::DPP_METHOD_VERSIONS_V2; use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2::STATE_TRANSITION_CONVERSION_VERSIONS_V2; use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1; -use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v2::STATE_TRANSITION_SERIALIZATION_VERSIONS_V2; +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; use crate::version::dpp_versions::dpp_token_versions::v1::TOKEN_VERSIONS_V1; use crate::version::dpp_versions::dpp_validation_versions::v2::DPP_VALIDATION_VERSIONS_V2; @@ -18,7 +18,7 @@ use crate::version::drive_abci_versions::drive_abci_checkpoint_parameters::v1::D use crate::version::drive_abci_versions::drive_abci_method_versions::v7::DRIVE_ABCI_METHOD_VERSIONS_V7; use crate::version::drive_abci_versions::drive_abci_query_versions::v1::DRIVE_ABCI_QUERY_VERSIONS_V1; use crate::version::drive_abci_versions::drive_abci_structure_versions::v1::DRIVE_ABCI_STRUCTURE_VERSIONS_V1; -use crate::version::drive_abci_versions::drive_abci_validation_versions::v7::DRIVE_ABCI_VALIDATION_VERSIONS_V7; +use crate::version::drive_abci_versions::drive_abci_validation_versions::v8::DRIVE_ABCI_VALIDATION_VERSIONS_V8; use crate::version::drive_abci_versions::drive_abci_withdrawal_constants::v2::DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V2; use crate::version::drive_abci_versions::DriveAbciVersion; use crate::version::drive_versions::v6::DRIVE_VERSION_V6; @@ -37,7 +37,7 @@ pub const PLATFORM_V12: PlatformVersion = PlatformVersion { drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V1, methods: DRIVE_ABCI_METHOD_VERSIONS_V7, - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V7, + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V8, //changed for data contract updates withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V2, query: DRIVE_ABCI_QUERY_VERSIONS_V1, checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, @@ -45,7 +45,7 @@ pub const PLATFORM_V12: PlatformVersion = PlatformVersion { dpp: DPPVersion { costs: DPP_COSTS_VERSIONS_V1, validation: DPP_VALIDATION_VERSIONS_V2, - state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V2, + state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V1, state_transitions: STATE_TRANSITION_VERSIONS_V3, diff --git a/packages/strategy-tests/src/lib.rs b/packages/strategy-tests/src/lib.rs index 66cba5e90b..9d32821d42 100644 --- a/packages/strategy-tests/src/lib.rs +++ b/packages/strategy-tests/src/lib.rs @@ -1804,7 +1804,7 @@ impl Strategy { *identity_contract_nonce += 1; // Prepare the DataContractUpdateTransition with the updated contract_ref - match DataContractUpdateTransition::try_from_platform_versioned((DataContract::V0(contract_ref.clone()), *identity_contract_nonce), platform_version) { + match DataContractUpdateTransition::from_data_contract_v0(DataContract::V0(contract_ref.clone()), *identity_contract_nonce, platform_version) { Ok(data_contract_update_transition) => { let identity_public_key = identity .get_first_public_key_matching( diff --git a/packages/wasm-dpp/src/data_contract/data_contract_facade.rs b/packages/wasm-dpp/src/data_contract/data_contract_facade.rs index 1f7b1066cf..8843911a7f 100644 --- a/packages/wasm-dpp/src/data_contract/data_contract_facade.rs +++ b/packages/wasm-dpp/src/data_contract/data_contract_facade.rs @@ -125,12 +125,14 @@ impl DataContractFacadeWasm { #[wasm_bindgen(js_name=createDataContractUpdateTransition)] pub fn create_data_contract_update_transition( &self, - data_contract: &DataContractWasm, + old_data_contract: &DataContractWasm, + new_data_contract: &DataContractWasm, identity_contract_nonce: IdentityNonce, ) -> Result { self.0 .create_data_contract_update_transition( - data_contract.to_owned().into(), + &old_data_contract.to_owned().into(), + &new_data_contract.to_owned().into(), identity_contract_nonce, ) .map(Into::into) diff --git a/packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs b/packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs index c6339ede34..1316c00747 100644 --- a/packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs +++ b/packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs @@ -71,8 +71,12 @@ impl DataContractUpdateTransitionWasm { PlatformVersion::latest() }; + let data_contract = self + .0 + .data_contract() + .ok_or_else(|| JsError::new("V1 update transitions do not contain a data contract"))?; DataContractWasm::try_from_serialization_format_with_platform_version( - self.0.data_contract().clone(), + data_contract.clone(), false, platform_version, ) diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 095a5b63a8..176950e254 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -60,7 +60,7 @@ use dpp::consensus::state::data_trigger::DataTriggerError::{ DataTriggerConditionError, DataTriggerExecutionError, DataTriggerInvalidResultError, }; use wasm_bindgen::{JsError, JsValue}; -use dpp::consensus::basic::data_contract::{ContestedUniqueIndexOnMutableDocumentTypeError, ContestedUniqueIndexWithUniqueIndexError, DataContractTokenConfigurationUpdateError, DecimalsOverLimitError, DuplicateKeywordsError, GroupExceedsMaxMembersError, GroupHasTooFewMembersError, GroupMemberHasPowerOfZeroError, GroupMemberHasPowerOverLimitError, GroupNonUnilateralMemberPowerHasLessThanRequiredPowerError, GroupPositionDoesNotExistError, GroupRequiredPowerIsInvalidError, GroupTotalPowerLessThanRequiredError, InvalidDescriptionLengthError, InvalidDocumentTypeRequiredSecurityLevelError, InvalidKeywordCharacterError, InvalidKeywordLengthError, InvalidTokenBaseSupplyError, InvalidTokenDistributionFunctionDivideByZeroError, InvalidTokenDistributionFunctionIncoherenceError, InvalidTokenDistributionFunctionInvalidParameterError, InvalidTokenDistributionFunctionInvalidParameterTupleError, InvalidTokenLanguageCodeError, InvalidTokenNameCharacterError, InvalidTokenNameLengthError, MainGroupIsNotDefinedError, NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError, NonContiguousContractTokenPositionsError, RedundantDocumentPaidForByTokenWithContractId, TokenPaymentByBurningOnlyAllowedOnInternalTokenError, TooManyKeywordsError, UnknownDocumentActionTokenEffectError, UnknownDocumentCreationRestrictionModeError, UnknownGasFeesPaidByError, UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, UnknownTradeModeError, UnknownTransferableTypeError}; +use dpp::consensus::basic::data_contract::{ContestedUniqueIndexOnMutableDocumentTypeError, ContestedUniqueIndexWithUniqueIndexError, DataContractTokenConfigurationUpdateError, DataContractUpdateTransitionConflictingKeywordError, DataContractUpdateTransitionOverlappingFieldsError, DecimalsOverLimitError, DuplicateKeywordsError, GroupExceedsMaxMembersError, GroupHasTooFewMembersError, GroupMemberHasPowerOfZeroError, GroupMemberHasPowerOverLimitError, GroupNonUnilateralMemberPowerHasLessThanRequiredPowerError, GroupPositionDoesNotExistError, GroupRequiredPowerIsInvalidError, GroupTotalPowerLessThanRequiredError, InvalidDescriptionLengthError, InvalidDocumentTypeRequiredSecurityLevelError, InvalidKeywordCharacterError, InvalidKeywordLengthError, InvalidTokenBaseSupplyError, InvalidTokenDistributionFunctionDivideByZeroError, InvalidTokenDistributionFunctionIncoherenceError, InvalidTokenDistributionFunctionInvalidParameterError, InvalidTokenDistributionFunctionInvalidParameterTupleError, InvalidTokenLanguageCodeError, InvalidTokenNameCharacterError, InvalidTokenNameLengthError, MainGroupIsNotDefinedError, NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError, NonContiguousContractTokenPositionsError, RedundantDocumentPaidForByTokenWithContractId, TokenPaymentByBurningOnlyAllowedOnInternalTokenError, TooManyKeywordsError, UnknownDocumentActionTokenEffectError, UnknownDocumentCreationRestrictionModeError, UnknownGasFeesPaidByError, UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, UnknownTradeModeError, UnknownTransferableTypeError}; use dpp::consensus::basic::document::{ContestedDocumentsTemporarilyNotAllowedError, DocumentCreationNotAllowedError, DocumentFieldMaxSizeExceededError, MaxDocumentsTransitionsExceededError, MissingPositionsInDocumentTypePropertiesError}; use dpp::consensus::basic::group::GroupActionNotAllowedOnTransitionError; use dpp::consensus::basic::identity::{DataContractBoundsNotPresentError, DisablingKeyIdAlsoBeingAddedInSameTransitionError, InvalidIdentityCreditWithdrawalTransitionAmountError, InvalidIdentityUpdateTransitionDisableKeysError, InvalidIdentityUpdateTransitionEmptyError, InvalidKeyPurposeForContractBoundsError, TooManyMasterPublicKeyError, WithdrawalOutputScriptNotAllowedWhenSigningWithOwnerKeyError}; @@ -920,6 +920,12 @@ fn from_basic_error(basic_error: &BasicError) -> JsValue { BasicError::InvalidRemainderOutputCountError(e) => { generic_consensus_error!(InvalidRemainderOutputCountError, e).into() } + BasicError::DataContractUpdateTransitionOverlappingFieldsError(e) => { + generic_consensus_error!(DataContractUpdateTransitionOverlappingFieldsError, e).into() + } + BasicError::DataContractUpdateTransitionConflictingKeywordError(e) => { + generic_consensus_error!(DataContractUpdateTransitionConflictingKeywordError, e).into() + } } } diff --git a/packages/wasm-dpp2/src/data_contract/transitions/update.rs b/packages/wasm-dpp2/src/data_contract/transitions/update.rs index 2ba663a201..460324b583 100644 --- a/packages/wasm-dpp2/src/data_contract/transitions/update.rs +++ b/packages/wasm-dpp2/src/data_contract/transitions/update.rs @@ -43,8 +43,9 @@ impl DataContractUpdateTransitionWasm { }; let rs_data_contract_update_transition = - DataContractUpdateTransition::try_from_platform_versioned( - (DataContract::from(data_contract.clone()), identity_nonce), + DataContractUpdateTransition::from_data_contract_v0( + DataContract::from(data_contract.clone()), + identity_nonce, &platform_version.into(), )?; @@ -147,7 +148,9 @@ impl DataContractUpdateTransitionWasm { false => PlatformVersionWasm::try_from(js_platform_version)?, }; - let data_contract_serialization_format = self.0.data_contract(); + let data_contract_serialization_format = self.0.data_contract().ok_or_else(|| { + WasmDppError::invalid_argument("V1 update transitions do not contain a data contract") + })?; let mut validation_operations: Vec = Vec::new(); diff --git a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs index fbd2a28ce3..800b1afb29 100644 --- a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs @@ -464,20 +464,22 @@ impl StateTransitionWasm { self.0 = DataContractCreate(contract_create); } DataContractUpdate(mut contract_update) => { - let new_contract = match contract_update.data_contract().clone() { - DataContractInSerializationFormat::V0(mut v0) => { - v0.owner_id = owner_id; + if let Some(data_contract) = contract_update.data_contract() { + let new_contract = match data_contract.clone() { + DataContractInSerializationFormat::V0(mut v0) => { + v0.owner_id = owner_id; - DataContractInSerializationFormat::V0(v0) - } - DataContractInSerializationFormat::V1(mut v1) => { - v1.owner_id = owner_id; + DataContractInSerializationFormat::V0(v0) + } + DataContractInSerializationFormat::V1(mut v1) => { + v1.owner_id = owner_id; - DataContractInSerializationFormat::V1(v1) - } - }; + DataContractInSerializationFormat::V1(v1) + } + }; - contract_update.set_data_contract(new_contract); + contract_update.set_data_contract(new_contract); + } self.0 = DataContractUpdate(contract_update); } @@ -569,6 +571,11 @@ impl StateTransitionWasm { DataContractUpdateTransition::V0(v0).into() } + DataContractUpdateTransition::V1(mut v1) => { + v1.identity_contract_nonce = nonce; + + DataContractUpdateTransition::V1(v1).into() + } }, Batch(mut batch) => { batch.set_identity_contract_nonce(nonce); @@ -630,6 +637,10 @@ impl StateTransitionWasm { v0.identity_nonce = nonce; v0.into() } + DataContractCreateTransition::V1(mut v1) => { + v1.identity_nonce = nonce; + v1.into() + } }; contract_create.into()