From 603679e78467a410a4da0f10bf33df0c2874d7bc Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 13:18:42 +0200 Subject: [PATCH 1/9] fix: ensure document types only target valid tokens for token payments --- .../mod.rs | 10 +++++---- .../v0/mod.rs | 8 ++++--- .../v1/mod.rs | 7 ++++--- .../class_methods/try_from_schema/mod.rs | 3 +++ .../class_methods/try_from_schema/v1/mod.rs | 21 ++++++++++++++++++- .../data_contract/serialized_version/mod.rs | 3 +-- .../src/data_contract/v0/methods/schema.rs | 2 ++ .../src/data_contract/v0/serialization/mod.rs | 3 +++ .../src/data_contract/v1/methods/schema.rs | 3 +++ .../src/data_contract/v1/serialization/mod.rs | 3 +++ .../token/invalid_token_position_error.rs | 15 +++++++------ .../structure_v0/mod.rs | 7 +------ packages/strategy-tests/src/operations.rs | 2 ++ 13 files changed, 62 insertions(+), 25 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/mod.rs index d1d9cd64b93..d5f466e5092 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/mod.rs @@ -2,9 +2,8 @@ mod v0; mod v1; use crate::data_contract::config::DataContractConfig; -use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::DocumentType; -use crate::data_contract::DocumentName; +use crate::data_contract::{DocumentName, TokenConfiguration, TokenContractPosition}; use crate::validation::operations::ProtocolValidationOperation; use crate::version::PlatformVersion; use crate::ProtocolError; @@ -40,6 +39,7 @@ impl DocumentType { data_contract_id: Identifier, document_schemas: BTreeMap, schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, data_contact_config: &DataContractConfig, full_validation: bool, has_tokens: bool, @@ -53,20 +53,22 @@ impl DocumentType { .class_method_versions .create_document_types_from_document_schemas { - 0 => DocumentTypeV0::create_document_types_from_document_schemas_v0( + 0 => DocumentType::create_document_types_from_document_schemas_v0( data_contract_id, document_schemas, schema_defs, + token_configurations, data_contact_config, full_validation, validation_operations, platform_version, ), // in v1 we add the ability to have contracts without documents and just tokens - 1 => DocumentTypeV0::create_document_types_from_document_schemas_v1( + 1 => DocumentType::create_document_types_from_document_schemas_v1( data_contract_id, document_schemas, schema_defs, + token_configurations, data_contact_config, full_validation, has_tokens, diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v0/mod.rs index 45817e8dad2..311705e9ca0 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v0/mod.rs @@ -1,20 +1,20 @@ use crate::consensus::basic::data_contract::DocumentTypesAreMissingError; use crate::data_contract::config::DataContractConfig; use crate::data_contract::document_type::class_methods::consensus_or_protocol_data_contract_error; -use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::DocumentType; -use crate::data_contract::DocumentName; +use crate::data_contract::{DocumentName, TokenConfiguration, TokenContractPosition}; use crate::validation::operations::ProtocolValidationOperation; use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::{Identifier, Value}; use std::collections::BTreeMap; -impl DocumentTypeV0 { +impl DocumentType { pub(in crate::data_contract) fn create_document_types_from_document_schemas_v0( data_contract_id: Identifier, document_schemas: BTreeMap, schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, data_contact_config: &DataContractConfig, full_validation: bool, validation_operations: &mut Vec, @@ -40,6 +40,7 @@ impl DocumentTypeV0 { &name, schema, schema_defs, + token_configurations, data_contact_config, full_validation, validation_operations, @@ -84,6 +85,7 @@ mod tests { id, Default::default(), None, + &BTreeMap::new(), &config, false, false, diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v1/mod.rs index f2b6e0fa69a..1aa1755649b 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v1/mod.rs @@ -1,21 +1,21 @@ use crate::consensus::basic::data_contract::DocumentTypesAreMissingError; use crate::data_contract::config::DataContractConfig; use crate::data_contract::document_type::class_methods::consensus_or_protocol_data_contract_error; -use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::DocumentType; -use crate::data_contract::DocumentName; +use crate::data_contract::{DocumentName, TokenConfiguration, TokenContractPosition}; use crate::validation::operations::ProtocolValidationOperation; use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::{Identifier, Value}; use std::collections::BTreeMap; -impl DocumentTypeV0 { +impl DocumentType { #[allow(clippy::too_many_arguments)] pub(in crate::data_contract) fn create_document_types_from_document_schemas_v1( data_contract_id: Identifier, document_schemas: BTreeMap, schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, data_contact_config: &DataContractConfig, full_validation: bool, has_tokens: bool, @@ -42,6 +42,7 @@ impl DocumentTypeV0 { &name, schema, schema_defs, + token_configurations, data_contact_config, full_validation, validation_operations, diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs index 480d62988a2..943c1d26424 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs @@ -5,6 +5,7 @@ use crate::data_contract::document_type::{ property_names, DocumentProperty, DocumentPropertyType, DocumentType, }; use crate::data_contract::errors::DataContractError; +use crate::data_contract::{TokenConfiguration, TokenContractPosition}; use crate::util::json_schema::resolve_uri; use crate::validation::operations::ProtocolValidationOperation; use crate::ProtocolError; @@ -42,6 +43,7 @@ impl DocumentType { name: &str, schema: Value, schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, data_contact_config: &DataContractConfig, full_validation: bool, validation_operations: &mut Vec, @@ -70,6 +72,7 @@ impl DocumentType { name, schema, schema_defs, + token_configurations, data_contact_config, full_validation, validation_operations, diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs index 6a6087aed2a..d757c18466e 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs @@ -32,6 +32,7 @@ use crate::consensus::basic::data_contract::InvalidDocumentTypeNameError; use crate::consensus::basic::data_contract::TokenPaymentByBurningOnlyAllowedOnInternalTokenError; #[cfg(feature = "validation")] use crate::consensus::basic::document::MissingPositionsInDocumentTypePropertiesError; +use crate::consensus::basic::token::InvalidTokenPositionError; #[cfg(feature = "validation")] use crate::consensus::basic::BasicError; use crate::data_contract::config::v0::DataContractConfigGettersV0; @@ -56,7 +57,7 @@ use crate::data_contract::document_type::v1::DocumentTypeV1; use crate::data_contract::document_type::{property_names, DocumentType}; use crate::data_contract::errors::DataContractError; use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; -use crate::data_contract::TokenContractPosition; +use crate::data_contract::{TokenConfiguration, TokenContractPosition}; use crate::identity::SecurityLevel; use crate::tokens::gas_fees_paid_by::GasFeesPaidBy; use crate::tokens::token_amount_on_contract_token::{ @@ -78,6 +79,7 @@ impl DocumentTypeV1 { name: &str, schema: Value, schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, data_contact_config: &DataContractConfig, full_validation: bool, // we don't need to validate if loaded from state validation_operations: &mut Vec, @@ -577,6 +579,19 @@ impl DocumentTypeV1 { #[cfg(feature = "validation")] if full_validation { + if !token_configurations.contains_key(&token_contract_position) { + return Err(ProtocolError::ConsensusError( + ConsensusError::BasicError( + BasicError::InvalidTokenPositionError( + InvalidTokenPositionError::new( + token_configurations.last_key_value().map(|(position, _)| *position), + token_contract_position, + ), + ), + ) + .into(), + )); + } // If contractId is present and user tries to burn, bail out: if let Some(contract_id) = contract_id { @@ -686,6 +701,7 @@ mod tests { "valid_name-a-b-123", schema, None, + &BTreeMap::new(), &config, true, &mut vec![], @@ -717,6 +733,7 @@ mod tests { "", schema, None, + &BTreeMap::new(), &config, true, &mut vec![], @@ -759,6 +776,7 @@ mod tests { &"a".repeat(65), schema, None, + &BTreeMap::new(), &config, true, &mut vec![], @@ -827,6 +845,7 @@ mod tests { "invalid&name", schema, None, + &BTreeMap::new(), &config, true, &mut vec![], 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 71cdd23526b..7a3648c047a 100644 --- a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs +++ b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs @@ -6,6 +6,7 @@ use crate::data_contract::{ use crate::version::PlatformVersion; use std::collections::BTreeMap; +use super::EMPTY_KEYWORDS; use crate::data_contract::associated_token::token_configuration::TokenConfiguration; use crate::data_contract::group::Group; use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1; @@ -21,8 +22,6 @@ use platform_versioning::PlatformVersioned; #[cfg(feature = "data-contract-serde-conversion")] use serde::{Deserialize, Serialize}; -use super::EMPTY_KEYWORDS; - pub(in crate::data_contract) mod v0; pub(in crate::data_contract) mod v1; diff --git a/packages/rs-dpp/src/data_contract/v0/methods/schema.rs b/packages/rs-dpp/src/data_contract/v0/methods/schema.rs index c8a0ce31e95..576e98d2a89 100644 --- a/packages/rs-dpp/src/data_contract/v0/methods/schema.rs +++ b/packages/rs-dpp/src/data_contract/v0/methods/schema.rs @@ -22,6 +22,7 @@ impl DataContractSchemaMethodsV0 for DataContractV0 { self.id, schemas, defs.as_ref(), + &BTreeMap::new(), &self.config, full_validation, false, @@ -45,6 +46,7 @@ impl DataContractSchemaMethodsV0 for DataContractV0 { name, schema, self.schema_defs.as_ref(), + &BTreeMap::new(), &self.config, full_validation, validation_operations, diff --git a/packages/rs-dpp/src/data_contract/v0/serialization/mod.rs b/packages/rs-dpp/src/data_contract/v0/serialization/mod.rs index 2d0d41e85b9..ca4048864e2 100644 --- a/packages/rs-dpp/src/data_contract/v0/serialization/mod.rs +++ b/packages/rs-dpp/src/data_contract/v0/serialization/mod.rs @@ -5,6 +5,7 @@ use crate::data_contract::v0::DataContractV0; use crate::data_contract::DataContract; use crate::version::{PlatformVersion, PlatformVersionCurrentVersion}; use crate::ProtocolError; +use std::collections::BTreeMap; use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1; use crate::validation::operations::ProtocolValidationOperation; @@ -94,6 +95,7 @@ impl DataContractV0 { id, document_schemas, schema_defs.as_ref(), + &BTreeMap::new(), &config, full_validation, false, @@ -134,6 +136,7 @@ impl DataContractV0 { id, document_schemas, schema_defs.as_ref(), + &BTreeMap::new(), &config, full_validation, false, diff --git a/packages/rs-dpp/src/data_contract/v1/methods/schema.rs b/packages/rs-dpp/src/data_contract/v1/methods/schema.rs index f66e69b5b83..f74987abe12 100644 --- a/packages/rs-dpp/src/data_contract/v1/methods/schema.rs +++ b/packages/rs-dpp/src/data_contract/v1/methods/schema.rs @@ -1,3 +1,4 @@ +use crate::data_contract::accessors::v1::DataContractV1Getters; use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; use crate::data_contract::document_type::DocumentType; use crate::data_contract::schema::DataContractSchemaMethodsV0; @@ -21,6 +22,7 @@ impl DataContractSchemaMethodsV0 for DataContractV1 { self.id, schemas, defs.as_ref(), + &self.tokens, &self.config, full_validation, !self.tokens.is_empty(), @@ -44,6 +46,7 @@ impl DataContractSchemaMethodsV0 for DataContractV1 { name, schema, self.schema_defs.as_ref(), + self.tokens(), &self.config, full_validation, validation_operations, diff --git a/packages/rs-dpp/src/data_contract/v1/serialization/mod.rs b/packages/rs-dpp/src/data_contract/v1/serialization/mod.rs index 1000499143e..8ffcbe4e25d 100644 --- a/packages/rs-dpp/src/data_contract/v1/serialization/mod.rs +++ b/packages/rs-dpp/src/data_contract/v1/serialization/mod.rs @@ -4,6 +4,7 @@ use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::data_contract::{DataContract, DataContractV1}; use crate::version::{PlatformVersion, PlatformVersionCurrentVersion}; use crate::ProtocolError; +use std::collections::BTreeMap; use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1; use crate::validation::operations::ProtocolValidationOperation; @@ -93,6 +94,7 @@ impl DataContractV1 { id, document_schemas, schema_defs.as_ref(), + &BTreeMap::new(), &config, full_validation, false, @@ -151,6 +153,7 @@ impl DataContractV1 { id, document_schemas, schema_defs.as_ref(), + &tokens, &config, full_validation, !tokens.is_empty(), diff --git a/packages/rs-dpp/src/errors/consensus/basic/token/invalid_token_position_error.rs b/packages/rs-dpp/src/errors/consensus/basic/token/invalid_token_position_error.rs index ac157f4193b..e131d553cd1 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/token/invalid_token_position_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/token/invalid_token_position_error.rs @@ -10,19 +10,22 @@ use thiserror::Error; Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize, )] #[error( - "Invalid token position {}, max {}", - invalid_token_position, - max_token_position + "Invalid token position: {invalid_token_position}. {max_token_message}", + max_token_message = if let Some(max) = self.max_token_position { + format!("The maximum allowed token position is {}", max) + } else { + "No maximum token position limit is set.".to_string() + } )] #[platform_serialize(unversioned)] pub struct InvalidTokenPositionError { - max_token_position: TokenContractPosition, + max_token_position: Option, invalid_token_position: TokenContractPosition, } impl InvalidTokenPositionError { pub fn new( - max_token_position: TokenContractPosition, + max_token_position: Option, invalid_token_position: TokenContractPosition, ) -> Self { Self { @@ -31,7 +34,7 @@ impl InvalidTokenPositionError { } } - pub fn max_token_position(&self) -> TokenContractPosition { + pub fn max_token_position(&self) -> Option { self.max_token_position } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_base_transition_action/structure_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_base_transition_action/structure_v0/mod.rs index 3fb19707d41..4b543cd8014 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_base_transition_action/structure_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_base_transition_action/structure_v0/mod.rs @@ -32,12 +32,7 @@ impl TokenBaseTransitionActionStructureValidationV0 for TokenBaseTransitionActio } else { Ok(SimpleConsensusValidationResult::new_with_error( BasicError::InvalidTokenPositionError(InvalidTokenPositionError::new( - *contract - .contract - .tokens() - .keys() - .last() - .expect("we already checked this was not empty"), + contract.contract.tokens().keys().last().copied(), token_position, )) .into(), diff --git a/packages/strategy-tests/src/operations.rs b/packages/strategy-tests/src/operations.rs index ca22237f2a8..51f4e558536 100644 --- a/packages/strategy-tests/src/operations.rs +++ b/packages/strategy-tests/src/operations.rs @@ -1,6 +1,7 @@ use crate::frequency::Frequency; use bincode::{Decode, Encode}; use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::random_document::{ DocumentFieldFillSize, DocumentFieldFillType, @@ -430,6 +431,7 @@ impl PlatformDeserializableWithPotentialValidationFromVersionedStructure for Dat name_str, schema_json, None, + contract.tokens(), contract.config(), full_validation, &mut vec![], From ca63bf60f3c5b5e3002d370bb3ade1b6f3d6102b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 13:22:03 +0200 Subject: [PATCH 2/9] clippy fix --- .../create_document_types_from_document_schemas/v0/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v0/mod.rs index 311705e9ca0..a1b8f94c837 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/create_document_types_from_document_schemas/v0/mod.rs @@ -10,6 +10,7 @@ use platform_value::{Identifier, Value}; use std::collections::BTreeMap; impl DocumentType { + #[allow(clippy::too_many_arguments)] pub(in crate::data_contract) fn create_document_types_from_document_schemas_v0( data_contract_id: Identifier, document_schemas: BTreeMap, From fb0fb0f3b52463d6ec8c679524eaf7ea524e2f81 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 13:22:59 +0200 Subject: [PATCH 3/9] test fix --- packages/rs-drive/src/query/test_index.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rs-drive/src/query/test_index.rs b/packages/rs-drive/src/query/test_index.rs index 6e2576d19d7..8997dea7d34 100644 --- a/packages/rs-drive/src/query/test_index.rs +++ b/packages/rs-drive/src/query/test_index.rs @@ -1,6 +1,7 @@ #[cfg(feature = "server")] #[cfg(test)] mod tests { + use std::collections::BTreeMap; use crate::config::DriveConfig; use crate::error::{query::QuerySyntaxError, Error}; use crate::query::DriveDocumentQuery; @@ -85,6 +86,7 @@ mod tests { "indexed_type", schema, None, + &BTreeMap::new(), &config, true, &mut vec![], From cdf70dd3a640e5b1a45dce3d3fc63c5201c44dc9 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 13:29:18 +0200 Subject: [PATCH 4/9] test fix --- .../methods/validate_update/v0/mod.rs | 24 +++++++++++++++++++ packages/rs-dpp/src/util/json_schema.rs | 3 ++- packages/rs-drive/src/query/test_index.rs | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs index 961ebef27c0..e6cc41614c4 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs @@ -255,6 +255,7 @@ mod tests { mod validate_config { use super::*; + use std::collections::BTreeMap; #[test] fn should_return_invalid_result_when_creation_restriction_mode_is_changed() { @@ -282,6 +283,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -309,6 +311,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -354,6 +357,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -381,6 +385,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -426,6 +431,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -453,6 +459,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -498,6 +505,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -525,6 +533,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -570,6 +579,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -597,6 +607,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -642,6 +653,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -669,6 +681,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -714,6 +727,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -738,6 +752,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -783,6 +798,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -807,6 +823,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -852,6 +869,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -876,6 +894,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -899,6 +918,7 @@ mod tests { mod validate_schema { use super::*; use crate::consensus::basic::BasicError; + use std::collections::BTreeMap; #[test] fn should_pass_when_schema_is_not_changed() { @@ -926,6 +946,7 @@ mod tests { document_type_name, schema.clone(), None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -941,6 +962,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -982,6 +1004,7 @@ mod tests { document_type_name, schema.clone(), None, + &BTreeMap::new(), &config, false, &mut Vec::new(), @@ -1006,6 +1029,7 @@ mod tests { document_type_name, schema, None, + &BTreeMap::new(), &config, false, &mut Vec::new(), diff --git a/packages/rs-dpp/src/util/json_schema.rs b/packages/rs-dpp/src/util/json_schema.rs index 122d9436c37..721bbd4e614 100644 --- a/packages/rs-dpp/src/util/json_schema.rs +++ b/packages/rs-dpp/src/util/json_schema.rs @@ -145,10 +145,10 @@ impl JsonSchemaExt for JsonValue { #[cfg(test)] mod test { - use crate::data_contract::config::DataContractConfig; use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; use crate::data_contract::document_type::DocumentType; + use std::collections::BTreeMap; use platform_value::Identifier; use platform_version::version::PlatformVersion; @@ -216,6 +216,7 @@ mod test { "doc", platform_value, None, + &BTreeMap::new(), &config, false, &mut vec![], diff --git a/packages/rs-drive/src/query/test_index.rs b/packages/rs-drive/src/query/test_index.rs index 8997dea7d34..357b33075da 100644 --- a/packages/rs-drive/src/query/test_index.rs +++ b/packages/rs-drive/src/query/test_index.rs @@ -1,7 +1,6 @@ #[cfg(feature = "server")] #[cfg(test)] mod tests { - use std::collections::BTreeMap; use crate::config::DriveConfig; use crate::error::{query::QuerySyntaxError, Error}; use crate::query::DriveDocumentQuery; @@ -11,6 +10,7 @@ mod tests { use dpp::platform_value::{platform_value, Identifier}; use dpp::util::cbor_serializer; use serde_json::json; + use std::collections::BTreeMap; use dpp::tests::fixtures::get_dpns_data_contract_fixture; use dpp::version::PlatformVersion; From 212f3a1b4a1166ad9e3bc6b822ac09a1635a22ac Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 13:33:49 +0200 Subject: [PATCH 5/9] test fix --- packages/rs-sdk/tests/fetch/common.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rs-sdk/tests/fetch/common.rs b/packages/rs-sdk/tests/fetch/common.rs index 676f7e2c7c4..d2ee122cb21 100644 --- a/packages/rs-sdk/tests/fetch/common.rs +++ b/packages/rs-sdk/tests/fetch/common.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use dash_sdk::{mock::Mockable, platform::Query, Sdk}; use dpp::data_contract::config::DataContractConfig; use dpp::{data_contract::DataContractFactory, prelude::Identifier}; @@ -39,6 +40,7 @@ pub fn mock_document_type() -> dpp::data_contract::document_type::DocumentType { "document_type_name", schema, None, + &BTreeMap::new(), &config, true, &mut vec![], From 81b3cae0fd712da20d8adf53b1f97b18e54e89f4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 13:34:00 +0200 Subject: [PATCH 6/9] test fix --- packages/rs-sdk/tests/fetch/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-sdk/tests/fetch/common.rs b/packages/rs-sdk/tests/fetch/common.rs index d2ee122cb21..bba48e12f61 100644 --- a/packages/rs-sdk/tests/fetch/common.rs +++ b/packages/rs-sdk/tests/fetch/common.rs @@ -1,9 +1,9 @@ -use std::collections::BTreeMap; use dash_sdk::{mock::Mockable, platform::Query, Sdk}; use dpp::data_contract::config::DataContractConfig; use dpp::{data_contract::DataContractFactory, prelude::Identifier}; use hex::ToHex; use rs_dapi_client::transport::TransportRequest; +use std::collections::BTreeMap; use super::config::Config; From f2dba50cf4e49dc849ee74de56dee9dad7edebba Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 14:32:54 +0200 Subject: [PATCH 7/9] more validation --- .../class_methods/try_from_schema/v1/mod.rs | 21 +++++++++--- .../src/errors/consensus/basic/basic_error.rs | 13 ++++--- .../consensus/basic/data_contract/mod.rs | 2 ++ ...ment_paid_for_by_token_with_contract_id.rs | 34 +++++++++++++++++++ packages/rs-dpp/src/errors/consensus/codes.rs | 1 + .../src/errors/consensus/consensus_error.rs | 5 ++- 6 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 packages/rs-dpp/src/errors/consensus/basic/data_contract/redundant_document_paid_for_by_token_with_contract_id.rs diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs index d757c18466e..0c4da311f10 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs @@ -28,6 +28,7 @@ use crate::consensus::basic::data_contract::ContestedUniqueIndexOnMutableDocumen use crate::consensus::basic::data_contract::ContestedUniqueIndexWithUniqueIndexError; #[cfg(any(test, feature = "validation"))] use crate::consensus::basic::data_contract::InvalidDocumentTypeNameError; +use crate::consensus::basic::data_contract::RedundantDocumentPaidForByTokenWithContractId; #[cfg(feature = "validation")] use crate::consensus::basic::data_contract::TokenPaymentByBurningOnlyAllowedOnInternalTokenError; #[cfg(feature = "validation")] @@ -564,7 +565,7 @@ impl DocumentTypeV1 { .transpose()? .map(|action_cost| { // Extract an optional contract_id. Adjust the key if necessary. - let contract_id = action_cost.get_optional_identifier("contractId")?; + let target_contract_id = action_cost.get_optional_identifier("contractId")?; // Extract token_contract_position as an integer, then convert it. let token_contract_position = action_cost.get_integer::("tokenPosition")?; @@ -579,7 +580,8 @@ impl DocumentTypeV1 { #[cfg(feature = "validation")] if full_validation { - if !token_configurations.contains_key(&token_contract_position) { + // contract id is none if we are on our own contract + if target_contract_id.is_none() && !token_configurations.contains_key(&token_contract_position) { return Err(ProtocolError::ConsensusError( ConsensusError::BasicError( BasicError::InvalidTokenPositionError( @@ -594,13 +596,22 @@ impl DocumentTypeV1 { } // If contractId is present and user tries to burn, bail out: - if let Some(contract_id) = contract_id { + if let Some(target_contract_id) = target_contract_id { + if target_contract_id == data_contract_id { + // we are in the same contract, but we set the data contract id + return Err(ProtocolError::ConsensusError( + ConsensusError::BasicError( + BasicError::RedundantDocumentPaidForByTokenWithContractId(RedundantDocumentPaidForByTokenWithContractId::new(target_contract_id)) + ) + .into(), + )); + } if effect == DocumentActionTokenEffect::BurnToken { return Err(ProtocolError::ConsensusError( ConsensusError::BasicError( BasicError::TokenPaymentByBurningOnlyAllowedOnInternalTokenError( TokenPaymentByBurningOnlyAllowedOnInternalTokenError::new( - contract_id, + target_contract_id, token_contract_position, key.to_string(), ), @@ -620,7 +631,7 @@ impl DocumentTypeV1 { .unwrap_or(GasFeesPaidBy::DocumentOwner); Ok(DocumentActionTokenCost { - contract_id, + contract_id: target_contract_id, token_contract_position, token_amount, effect, 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 b061b2ec83c..e2c339ce649 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs @@ -23,11 +23,11 @@ use crate::consensus::basic::data_contract::{ InvalidTokenDistributionFunctionInvalidParameterTupleError, InvalidTokenLanguageCodeError, InvalidTokenNameCharacterError, InvalidTokenNameLengthError, MainGroupIsNotDefinedError, NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError, - NonContiguousContractTokenPositionsError, SystemPropertyIndexAlreadyPresentError, - UndefinedIndexPropertyError, UniqueIndicesLimitReachedError, - UnknownDocumentCreationRestrictionModeError, UnknownGasFeesPaidByError, - UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, UnknownTradeModeError, - UnknownTransferableTypeError, + NonContiguousContractTokenPositionsError, RedundantDocumentPaidForByTokenWithContractId, + SystemPropertyIndexAlreadyPresentError, UndefinedIndexPropertyError, + UniqueIndicesLimitReachedError, UnknownDocumentCreationRestrictionModeError, + UnknownGasFeesPaidByError, UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, + UnknownTradeModeError, UnknownTransferableTypeError, }; use crate::consensus::basic::data_contract::{ InvalidJsonSchemaRefError, TokenPaymentByBurningOnlyAllowedOnInternalTokenError, @@ -564,6 +564,9 @@ pub enum BasicError { #[error(transparent)] TokenNoteOnlyAllowedWhenProposerError(TokenNoteOnlyAllowedWhenProposerError), + + #[error(transparent)] + RedundantDocumentPaidForByTokenWithContractId(RedundantDocumentPaidForByTokenWithContractId), } impl From for ConsensusError { 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 feb6cf6045c..b9348da8bc3 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 @@ -44,6 +44,7 @@ mod main_group_is_not_defined; mod new_tokens_destination_identity_option_required_error; mod non_contiguous_contract_group_positions_error; mod non_contiguous_contract_token_positions_error; +mod redundant_document_paid_for_by_token_with_contract_id; mod system_property_index_already_present_error; mod token_decimals_over_limit_error; mod token_payment_by_burning_only_allowed_on_internal_token_error; @@ -107,6 +108,7 @@ pub use main_group_is_not_defined::*; pub use new_tokens_destination_identity_option_required_error::*; pub use non_contiguous_contract_group_positions_error::*; pub use non_contiguous_contract_token_positions_error::*; +pub use redundant_document_paid_for_by_token_with_contract_id::*; pub use token_decimals_over_limit_error::*; pub use token_payment_by_burning_only_allowed_on_internal_token_error::*; pub use unknown_document_action_token_effect_error::*; diff --git a/packages/rs-dpp/src/errors/consensus/basic/data_contract/redundant_document_paid_for_by_token_with_contract_id.rs b/packages/rs-dpp/src/errors/consensus/basic/data_contract/redundant_document_paid_for_by_token_with_contract_id.rs new file mode 100644 index 00000000000..70d672829a6 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/basic/data_contract/redundant_document_paid_for_by_token_with_contract_id.rs @@ -0,0 +1,34 @@ +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("Document paid for by a token has a contractId {contract_id} set, which is redundant because it is targeting the current contract")] +#[platform_serialize(unversioned)] +pub struct RedundantDocumentPaidForByTokenWithContractId { + contract_id: Identifier, +} + +impl RedundantDocumentPaidForByTokenWithContractId { + pub fn new(contract_id: Identifier) -> Self { + Self { contract_id } + } + + pub fn contract_id(&self) -> Identifier { + self.contract_id + } +} + +impl From for ConsensusError { + fn from(err: RedundantDocumentPaidForByTokenWithContractId) -> Self { + Self::BasicError(BasicError::RedundantDocumentPaidForByTokenWithContractId( + err, + )) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index 2916cc526cd..5bf91086875 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -116,6 +116,7 @@ impl ErrorWithCode for BasicError { Self::InvalidKeywordCharacterError(_) => 10269, Self::InvalidKeywordLengthError(_) => 10270, Self::DecimalsOverLimitError(_) => 10271, + Self::RedundantDocumentPaidForByTokenWithContractId(_) => 10272, // Group Errors: 10350-10399 Self::GroupPositionDoesNotExistError(_) => 10350, diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index aa3aa32a531..2d6f8c202bf 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -61,7 +61,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, GroupMemberHasPowerOfZeroError, GroupMemberHasPowerOverLimitError, GroupNonUnilateralMemberPowerHasLessThanRequiredPowerError, GroupPositionDoesNotExistError, GroupRequiredPowerIsInvalidError, GroupTotalPowerLessThanRequiredError, InvalidDescriptionLengthError, InvalidDocumentTypeRequiredSecurityLevelError, InvalidKeywordCharacterError, InvalidKeywordLengthError, InvalidTokenBaseSupplyError, InvalidTokenDistributionFunctionDivideByZeroError, InvalidTokenDistributionFunctionIncoherenceError, InvalidTokenDistributionFunctionInvalidParameterError, InvalidTokenDistributionFunctionInvalidParameterTupleError, InvalidTokenLanguageCodeError, InvalidTokenNameCharacterError, InvalidTokenNameLengthError, MainGroupIsNotDefinedError, NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError, NonContiguousContractTokenPositionsError, TokenPaymentByBurningOnlyAllowedOnInternalTokenError, TooManyKeywordsError, UnknownDocumentActionTokenEffectError, UnknownDocumentCreationRestrictionModeError, UnknownGasFeesPaidByError, UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, UnknownTradeModeError, UnknownTransferableTypeError}; +use dpp::consensus::basic::data_contract::{ContestedUniqueIndexOnMutableDocumentTypeError, ContestedUniqueIndexWithUniqueIndexError, DataContractTokenConfigurationUpdateError, DecimalsOverLimitError, DuplicateKeywordsError, GroupExceedsMaxMembersError, 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, TooManyMasterPublicKeyError, WithdrawalOutputScriptNotAllowedWhenSigningWithOwnerKeyError}; @@ -824,6 +824,9 @@ fn from_basic_error(basic_error: &BasicError) -> JsValue { BasicError::TokenNoteOnlyAllowedWhenProposerError(e) => { generic_consensus_error!(TokenNoteOnlyAllowedWhenProposerError, e).into() } + BasicError::RedundantDocumentPaidForByTokenWithContractId(e) => { + generic_consensus_error!(RedundantDocumentPaidForByTokenWithContractId, e).into() + } } } From cbdffc13f59372eac2078b4cfe27033863694d12 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 15:49:02 +0200 Subject: [PATCH 8/9] a lot more validation --- .../document_type/accessors/mod.rs | 49 ++++ .../document_type/accessors/v1/mod.rs | 12 + .../document_type/token_costs/accessors.rs | 18 ++ .../document_type/token_costs/mod.rs | 36 +++ .../document_type/token_costs/v0/mod.rs | 24 ++ .../document_type/v1/accessors.rs | 42 +++ packages/rs-dpp/src/errors/consensus/codes.rs | 2 + .../data_contract_not_found_error.rs | 32 +++ .../consensus/state/data_contract/mod.rs | 1 + .../src/errors/consensus/state/state_error.rs | 9 +- .../token/invalid_token_position_error.rs | 50 ++++ .../src/errors/consensus/state/token/mod.rs | 2 + .../data_contract_create/mod.rs | 240 ++++++++++++++++++ .../data_contract_create/state/v0/mod.rs | 76 +++++- .../data_contract_update/state/v0/mod.rs | 78 ++++++ .../src/errors/consensus/consensus_error.rs | 9 +- 16 files changed, 677 insertions(+), 3 deletions(-) create mode 100644 packages/rs-dpp/src/errors/consensus/state/data_contract/data_contract_not_found_error.rs create mode 100644 packages/rs-dpp/src/errors/consensus/state/token/invalid_token_position_error.rs diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs index 652439075b6..6d049e9b39e 100644 --- a/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs @@ -12,6 +12,7 @@ use crate::data_contract::document_type::restricted_creation::CreationRestrictio #[cfg(feature = "validation")] use crate::data_contract::document_type::validator::StatelessJsonSchemaLazyValidator; use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; +use crate::data_contract::TokenContractPosition; use crate::document::transfer::Transferable; use crate::identity::SecurityLevel; use crate::nft::TradeMode; @@ -625,6 +626,22 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V1(v1) => v1.document_purchase_token_cost(), } } + + fn all_document_token_costs(&self) -> Vec<&DocumentActionTokenCost> { + match self { + DocumentType::V0(_) => vec![], + DocumentType::V1(v1) => v1.all_document_token_costs(), + } + } + + fn all_external_token_costs_contract_tokens( + &self, + ) -> BTreeMap> { + match self { + DocumentType::V0(_) => BTreeMap::new(), + DocumentType::V1(v1) => v1.all_external_token_costs_contract_tokens(), + } + } } impl DocumentTypeV1Getters for DocumentTypeRef<'_> { @@ -669,6 +686,22 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V1(v1) => v1.document_purchase_token_cost(), } } + + fn all_document_token_costs(&self) -> Vec<&DocumentActionTokenCost> { + match self { + DocumentTypeRef::V0(_) => vec![], + DocumentTypeRef::V1(v1) => v1.all_document_token_costs(), + } + } + + fn all_external_token_costs_contract_tokens( + &self, + ) -> BTreeMap> { + match self { + DocumentTypeRef::V0(_) => BTreeMap::new(), + DocumentTypeRef::V1(v1) => v1.all_external_token_costs_contract_tokens(), + } + } } impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { @@ -713,4 +746,20 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V1(v1) => v1.document_purchase_token_cost(), } } + + fn all_document_token_costs(&self) -> Vec<&DocumentActionTokenCost> { + match self { + DocumentTypeMutRef::V0(_) => vec![], + DocumentTypeMutRef::V1(v1) => v1.all_document_token_costs(), + } + } + + fn all_external_token_costs_contract_tokens( + &self, + ) -> BTreeMap> { + match self { + DocumentTypeMutRef::V0(_) => BTreeMap::new(), + DocumentTypeMutRef::V1(v1) => v1.all_external_token_costs_contract_tokens(), + } + } } diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/v1/mod.rs index d908adec2f5..eeacaf8d32f 100644 --- a/packages/rs-dpp/src/data_contract/document_type/accessors/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/v1/mod.rs @@ -1,4 +1,7 @@ +use crate::data_contract::TokenContractPosition; use crate::tokens::token_amount_on_contract_token::DocumentActionTokenCost; +use platform_value::Identifier; +use std::collections::{BTreeMap, BTreeSet}; /// Trait providing getters for retrieving token costs associated with different document operations. pub trait DocumentTypeV1Getters { @@ -43,6 +46,15 @@ pub trait DocumentTypeV1Getters { /// - `Some(TokenActionCost)` if a purchase cost exists. /// - `None` if no cost is set for document purchase. fn document_purchase_token_cost(&self) -> Option; + + /// Returns all document token costs. This is generally used only in internal validation. + fn all_document_token_costs(&self) -> Vec<&DocumentActionTokenCost>; + + /// Returns the tokens used by external token costs as a set of token contract positions per contract. + /// This is generally used only in internal validation. + fn all_external_token_costs_contract_tokens( + &self, + ) -> BTreeMap>; } /// Trait providing setters for assigning token costs to different document operations. diff --git a/packages/rs-dpp/src/data_contract/document_type/token_costs/accessors.rs b/packages/rs-dpp/src/data_contract/document_type/token_costs/accessors.rs index 4ae870ed0d2..363ac7117de 100644 --- a/packages/rs-dpp/src/data_contract/document_type/token_costs/accessors.rs +++ b/packages/rs-dpp/src/data_contract/document_type/token_costs/accessors.rs @@ -19,6 +19,24 @@ pub trait TokenCostGettersV0 { /// Returns the token cost associated with document purchase, if applicable. fn document_purchase_token_cost(&self) -> Option; + + /// Returns a reference to the token cost associated with document creation, if applicable. + fn document_creation_token_cost_ref(&self) -> Option<&DocumentActionTokenCost>; + + /// Returns a reference to the token cost associated with document replacement (updating), if applicable. + fn document_replacement_token_cost_ref(&self) -> Option<&DocumentActionTokenCost>; + + /// Returns a reference to the token cost associated with document deletion, if applicable. + fn document_deletion_token_cost_ref(&self) -> Option<&DocumentActionTokenCost>; + + /// Returns a reference to the token cost associated with document transfer, if applicable. + fn document_transfer_token_cost_ref(&self) -> Option<&DocumentActionTokenCost>; + + /// Returns a reference to the token cost associated with updating the price of a document, if applicable. + fn document_price_update_token_cost_ref(&self) -> Option<&DocumentActionTokenCost>; + + /// Returns a reference to the token cost associated with document purchase, if applicable. + fn document_purchase_token_cost_ref(&self) -> Option<&DocumentActionTokenCost>; } /// Trait providing setters for modifying token costs associated with different operations. diff --git a/packages/rs-dpp/src/data_contract/document_type/token_costs/mod.rs b/packages/rs-dpp/src/data_contract/document_type/token_costs/mod.rs index 70a68a94a9d..493d8a420cc 100644 --- a/packages/rs-dpp/src/data_contract/document_type/token_costs/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/token_costs/mod.rs @@ -52,6 +52,42 @@ impl TokenCostGettersV0 for TokenCosts { TokenCosts::V0(inner) => inner.document_purchase_token_cost(), } } + + fn document_creation_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + match self { + TokenCosts::V0(inner) => inner.document_creation_token_cost_ref(), + } + } + + fn document_replacement_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + match self { + TokenCosts::V0(inner) => inner.document_replacement_token_cost_ref(), + } + } + + fn document_deletion_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + match self { + TokenCosts::V0(inner) => inner.document_deletion_token_cost_ref(), + } + } + + fn document_transfer_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + match self { + TokenCosts::V0(inner) => inner.document_transfer_token_cost_ref(), + } + } + + fn document_price_update_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + match self { + TokenCosts::V0(inner) => inner.document_price_update_token_cost_ref(), + } + } + + fn document_purchase_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + match self { + TokenCosts::V0(inner) => inner.document_purchase_token_cost_ref(), + } + } } impl TokenCostSettersV0 for TokenCosts { diff --git a/packages/rs-dpp/src/data_contract/document_type/token_costs/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/token_costs/v0/mod.rs index 345663540b3..e848928830a 100644 --- a/packages/rs-dpp/src/data_contract/document_type/token_costs/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/token_costs/v0/mod.rs @@ -50,6 +50,30 @@ impl TokenCostGettersV0 for TokenCostsV0 { fn document_purchase_token_cost(&self) -> Option { self.purchase } + + fn document_creation_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + self.create.as_ref() + } + + fn document_replacement_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + self.replace.as_ref() + } + + fn document_deletion_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + self.delete.as_ref() + } + + fn document_transfer_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + self.transfer.as_ref() + } + + fn document_price_update_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + self.update_price.as_ref() + } + + fn document_purchase_token_cost_ref(&self) -> Option<&DocumentActionTokenCost> { + self.purchase.as_ref() + } } /// Implementation of the `TokenCostSettersV0` trait for `TokenCostsV0`. diff --git a/packages/rs-dpp/src/data_contract/document_type/v1/accessors.rs b/packages/rs-dpp/src/data_contract/document_type/v1/accessors.rs index 100224a3ebd..eb9b75bbf15 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v1/accessors.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v1/accessors.rs @@ -13,6 +13,7 @@ use crate::data_contract::document_type::v1::DocumentTypeV1; #[cfg(feature = "validation")] use crate::data_contract::document_type::validator::StatelessJsonSchemaLazyValidator; use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; +use crate::data_contract::TokenContractPosition; use crate::document::transfer::Transferable; use crate::identity::SecurityLevel; use crate::nft::TradeMode; @@ -153,4 +154,45 @@ impl DocumentTypeV1Getters for DocumentTypeV1 { fn document_purchase_token_cost(&self) -> Option { self.token_costs.document_purchase_token_cost() } + + fn all_document_token_costs(&self) -> Vec<&DocumentActionTokenCost> { + let mut result = Vec::new(); + + if let Some(cost) = self.token_costs.document_creation_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_replacement_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_deletion_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_transfer_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_price_update_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_purchase_token_cost_ref() { + result.push(cost); + } + + result + } + + fn all_external_token_costs_contract_tokens( + &self, + ) -> BTreeMap> { + let mut map = BTreeMap::new(); + + for cost in self.all_document_token_costs() { + if let Some(contract_id) = cost.contract_id { + map.entry(contract_id) + .or_insert_with(BTreeSet::new) + .insert(cost.token_contract_position); + } + } + + map + } } diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index 5bf91086875..48dfdc59a6d 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -248,6 +248,8 @@ impl ErrorWithCode for StateError { Self::PreProgrammedDistributionTimestampInPastError(_) => 40005, Self::IdentityInTokenConfigurationNotFoundError(_) => 40006, Self::IdentityMemberOfGroupNotFoundError(_) => 40007, + Self::DataContractNotFoundError(_) => 40008, + Self::InvalidTokenPositionStateError(_) => 40009, // Document Errors: 40100-40199 Self::DocumentAlreadyPresentError { .. } => 40100, diff --git a/packages/rs-dpp/src/errors/consensus/state/data_contract/data_contract_not_found_error.rs b/packages/rs-dpp/src/errors/consensus/state/data_contract/data_contract_not_found_error.rs new file mode 100644 index 00000000000..1f11adbf086 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/data_contract/data_contract_not_found_error.rs @@ -0,0 +1,32 @@ +use crate::consensus::state::state_error::StateError; +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, +)] +#[platform_serialize(unversioned)] +#[error("Data contract not found for id: {data_contract_id}")] +pub struct DataContractNotFoundError { + data_contract_id: Identifier, +} + +impl DataContractNotFoundError { + pub fn new(data_contract_id: Identifier) -> Self { + Self { data_contract_id } + } + + pub fn data_contract_id(&self) -> &Identifier { + &self.data_contract_id + } +} + +impl From for ConsensusError { + fn from(err: DataContractNotFoundError) -> Self { + Self::StateError(StateError::DataContractNotFoundError(err)) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/state/data_contract/mod.rs b/packages/rs-dpp/src/errors/consensus/state/data_contract/mod.rs index c06986bb44e..30b88ad8af3 100644 --- a/packages/rs-dpp/src/errors/consensus/state/data_contract/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/data_contract/mod.rs @@ -1,6 +1,7 @@ pub mod data_contract_already_present_error; pub mod data_contract_config_update_error; pub mod data_contract_is_readonly_error; +pub mod data_contract_not_found_error; pub mod data_contract_update_action_not_allowed_error; pub mod data_contract_update_permission_error; pub mod document_type_update_error; diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index 70c96a6ccd1..2ca6422d827 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -24,6 +24,7 @@ use crate::consensus::state::identity::max_identity_public_key_limit_reached_err use crate::consensus::state::identity::missing_identity_public_key_ids_error::MissingIdentityPublicKeyIdsError; use crate::consensus::state::identity::{IdentityAlreadyExistsError, IdentityInsufficientBalanceError, RecipientIdentityDoesNotExistError}; use crate::consensus::ConsensusError; +use crate::consensus::state::data_contract::data_contract_not_found_error::DataContractNotFoundError; 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; use crate::consensus::state::data_contract::document_type_update_error::DocumentTypeUpdateError; @@ -43,7 +44,7 @@ use crate::consensus::state::identity::missing_transfer_key_error::MissingTransf use crate::consensus::state::identity::no_transfer_key_for_core_withdrawal_available_error::NoTransferKeyForCoreWithdrawalAvailableError; use crate::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_insufficient_error::PrefundedSpecializedBalanceInsufficientError; use crate::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_not_found_error::PrefundedSpecializedBalanceNotFoundError; -use crate::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, IdentityTokenAccountNotFrozenError, InvalidGroupPositionError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, NewTokensDestinationIdentityDoesNotExistError, TokenMintPastMaxSupplyError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, UnauthorizedTokenActionError, IdentityTokenAccountAlreadyFrozenError, TokenAlreadyPausedError, TokenIsPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, PreProgrammedDistributionTimestampInPastError, TokenTransferRecipientIdentityNotExistError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale}; +use crate::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, IdentityTokenAccountNotFrozenError, InvalidGroupPositionError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, NewTokensDestinationIdentityDoesNotExistError, TokenMintPastMaxSupplyError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, UnauthorizedTokenActionError, IdentityTokenAccountAlreadyFrozenError, TokenAlreadyPausedError, TokenIsPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, PreProgrammedDistributionTimestampInPastError, TokenTransferRecipientIdentityNotExistError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError}; use crate::consensus::state::voting::masternode_incorrect_voter_identity_id_error::MasternodeIncorrectVoterIdentityIdError; use crate::consensus::state::voting::masternode_incorrect_voting_address_error::MasternodeIncorrectVotingAddressError; use crate::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -314,6 +315,12 @@ pub enum StateError { #[error(transparent)] IdentityToFreezeDoesNotExistError(IdentityToFreezeDoesNotExistError), + + #[error(transparent)] + DataContractNotFoundError(DataContractNotFoundError), + + #[error(transparent)] + InvalidTokenPositionStateError(InvalidTokenPositionStateError), } impl From for ConsensusError { diff --git a/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_position_error.rs b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_position_error.rs new file mode 100644 index 00000000000..faac2df3c1c --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_position_error.rs @@ -0,0 +1,50 @@ +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::data_contract::TokenContractPosition; +use crate::ProtocolError; +use bincode::{Decode, Encode}; +use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; +use thiserror::Error; + +#[derive( + Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize, +)] +#[error( + "Invalid token position: {invalid_token_position}. {max_token_message}", + max_token_message = if let Some(max) = self.max_token_position { + format!("The maximum allowed token position is {}", max) + } else { + "No maximum token position limit is set.".to_string() + } +)] +#[platform_serialize(unversioned)] +pub struct InvalidTokenPositionStateError { + max_token_position: Option, + invalid_token_position: TokenContractPosition, +} + +impl InvalidTokenPositionStateError { + pub fn new( + max_token_position: Option, + invalid_token_position: TokenContractPosition, + ) -> Self { + Self { + max_token_position, + invalid_token_position, + } + } + + pub fn max_token_position(&self) -> Option { + self.max_token_position + } + + pub fn invalid_token_position(&self) -> TokenContractPosition { + self.invalid_token_position + } +} + +impl From for ConsensusError { + fn from(err: InvalidTokenPositionStateError) -> Self { + Self::StateError(StateError::InvalidTokenPositionStateError(err)) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs index e42d7ae8b05..adf7b6eb25b 100644 --- a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs @@ -8,6 +8,7 @@ mod invalid_group_position_error; mod invalid_token_claim_no_current_rewards; mod invalid_token_claim_property_mismatch; mod invalid_token_claim_wrong_claimant; +mod invalid_token_position_error; mod new_authorized_action_taker_group_does_not_exist_error; mod new_authorized_action_taker_identity_does_not_exist_error; mod new_authorized_action_taker_main_group_not_set_error; @@ -35,6 +36,7 @@ pub use invalid_group_position_error::*; pub use invalid_token_claim_no_current_rewards::*; pub use invalid_token_claim_property_mismatch::*; pub use invalid_token_claim_wrong_claimant::*; +pub use invalid_token_position_error::*; pub use new_authorized_action_taker_group_does_not_exist_error::*; pub use new_authorized_action_taker_identity_does_not_exist_error::*; pub use new_authorized_action_taker_main_group_not_set_error::*; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs index fc2052108c9..f4a7178ffec 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs @@ -2443,6 +2443,246 @@ mod tests { .expect("expected to commit transaction"); } + #[test] + fn test_data_contract_creation_with_single_token_setting_transfer_of_external_token_that_does_not_exist( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_state = platform.state.load(); + + let (identity, contract_signer, contract_key) = + setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let mut data_contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/crypto-card-game/crypto-card-game-use-external-currency.json", + None, + None, + false, //no need to validate the data contracts in tests for drive + platform_version, + ) + .expect("expected to get json based contract"); + + { + let document_type = data_contract + .document_types_mut() + .get_mut("card") + .expect("expected a document type with name card"); + document_type.set_document_creation_token_cost(Some(DocumentActionTokenCost { + contract_id: Some(Identifier::new([0; 32])), + token_contract_position: 0, + token_amount: 5, + effect: DocumentActionTokenEffect::TransferTokenToContractOwner, + gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, + })); + let gas_fees_paid_by_int: u8 = GasFeesPaidBy::DocumentOwner.into(); + let schema = document_type.schema_mut(); + let token_cost = schema + .get_mut("tokenCost") + .expect("expected to get token cost") + .expect("expected token cost to be set"); + let creation_token_cost = token_cost + .get_mut("create") + .expect("expected to get creation token cost") + .expect("expected creation token cost to be set"); + creation_token_cost + .set_value("contractId", Identifier::new([0; 32]).into()) + .expect("expected to set token contract id"); + creation_token_cost + .set_value("tokenPosition", 0.into()) + .expect("expected to set token position"); + creation_token_cost + .set_value("amount", 5.into()) + .expect("expected to set token amount"); + creation_token_cost + .set_value( + "effect", + Value::U8( + DocumentActionTokenEffect::TransferTokenToContractOwner.into(), + ), + ) + .expect("expected to set token pay effect"); + creation_token_cost + .set_value("gasFeesPaidBy", gas_fees_paid_by_int.into()) + .expect("expected to set token amount"); + } + + let data_contract_create_transition = + DataContractCreateTransition::new_from_data_contract( + data_contract, + 1, + &identity.into_partial_identity_info(), + contract_key.id(), + &contract_signer, + platform_version, + None, + ) + .expect("expect to create data contract create batch transition"); + + let data_contract_create_serialized_transition = data_contract_create_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_create_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( + ConsensusError::StateError(StateError::DataContractNotFoundError(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + #[test] + fn test_data_contract_creation_with_single_token_setting_transfer_of_external_token_that_does_not_exist_in_contract_that_does_exist( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_state = platform.state.load(); + + let (identity, contract_signer, contract_key) = + setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let (token_contract_owner_id, _, _) = + setup_identity(&mut platform, 11, dash_to_credits!(0.1)); + + let (token_contract, _) = create_token_contract_with_owner_identity( + &mut platform, + token_contract_owner_id.id(), + None::, + None, + None, + platform_version, + ); + + let token_contract_id = token_contract.id(); + + let mut data_contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/crypto-card-game/crypto-card-game-use-external-currency.json", + None, + None, + false, //no need to validate the data contracts in tests for drive + platform_version, + ) + .expect("expected to get json based contract"); + + { + let document_type = data_contract + .document_types_mut() + .get_mut("card") + .expect("expected a document type with name card"); + document_type.set_document_creation_token_cost(Some(DocumentActionTokenCost { + contract_id: Some(token_contract_id), + token_contract_position: 4, + token_amount: 5, + effect: DocumentActionTokenEffect::TransferTokenToContractOwner, + gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, + })); + let gas_fees_paid_by_int: u8 = GasFeesPaidBy::DocumentOwner.into(); + let schema = document_type.schema_mut(); + let token_cost = schema + .get_mut("tokenCost") + .expect("expected to get token cost") + .expect("expected token cost to be set"); + let creation_token_cost = token_cost + .get_mut("create") + .expect("expected to get creation token cost") + .expect("expected creation token cost to be set"); + creation_token_cost + .set_value("contractId", token_contract_id.into()) + .expect("expected to set token contract id"); + creation_token_cost + .set_value("tokenPosition", 4.into()) + .expect("expected to set token position"); + creation_token_cost + .set_value("amount", 5.into()) + .expect("expected to set token amount"); + creation_token_cost + .set_value( + "effect", + Value::U8( + DocumentActionTokenEffect::TransferTokenToContractOwner.into(), + ), + ) + .expect("expected to set token pay effect"); + creation_token_cost + .set_value("gasFeesPaidBy", gas_fees_paid_by_int.into()) + .expect("expected to set token amount"); + } + + let data_contract_create_transition = + DataContractCreateTransition::new_from_data_contract( + data_contract, + 1, + &identity.into_partial_identity_info(), + contract_key.id(), + &contract_signer, + platform_version, + None, + ) + .expect("expect to create data contract create batch transition"); + + let data_contract_create_serialized_transition = data_contract_create_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_create_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( + ConsensusError::StateError(StateError::InvalidTokenPositionStateError(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + #[test] fn test_data_contract_creation_with_single_token_with_invalid_perpetual_distribution_should_cause_error( ) { 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 e84fd3b3847..dc72d28a481 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 @@ -5,18 +5,24 @@ use dpp::block::block_info::BlockInfo; use std::collections::BTreeSet; use dpp::consensus::state::data_contract::data_contract_already_present_error::DataContractAlreadyPresentError; +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::PreProgrammedDistributionTimestampInPastError; +use dpp::consensus::state::token::{ + InvalidTokenPositionStateError, PreProgrammedDistributionTimestampInPastError, +}; +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::prelude::ConsensusValidationResult; use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; @@ -310,6 +316,74 @@ impl DataContractCreateStateTransitionStateValidationV0 for DataContractCreateTr )); } + // now we need to validate that all documents with token costs using external tokens + // point to tokens that actually exist + if let StateTransitionAction::DataContractCreateAction(create_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 create_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 create 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) { + return Ok(ConsensusValidationResult::new_with_data_and_errors( + StateTransitionAction::BumpIdentityNonceAction( + BumpIdentityNonceAction::from_borrowed_data_contract_create_transition(self), + ), + 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::BumpIdentityNonceAction( + BumpIdentityNonceAction::from_borrowed_data_contract_create_transition( + self, + ), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![StateError::DataContractNotFoundError( + DataContractNotFoundError::new(contract_id), + ) + .into()], + )); + } + } + } + } + Ok(action) } 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 ca05859dcc2..5cdac63ab0c 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 @@ -6,11 +6,13 @@ use std::collections::BTreeSet; use dpp::consensus::basic::document::DataContractNotPresentError; use dpp::consensus::basic::BasicError; +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, DataContractV1Setters}; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; @@ -19,6 +21,7 @@ use dpp::data_contract::associated_token::token_perpetual_distribution::distribu 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; @@ -377,6 +380,81 @@ impl DataContractUpdateStateTransitionStateValidationV0 for DataContractUpdateTr } } + // 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 create 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) } diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 2d6f8c202bf..e33c010782d 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -67,6 +67,7 @@ use dpp::consensus::basic::group::GroupActionNotAllowedOnTransitionError; use dpp::consensus::basic::identity::{DataContractBoundsNotPresentError, DisablingKeyIdAlsoBeingAddedInSameTransitionError, InvalidIdentityCreditWithdrawalTransitionAmountError, InvalidIdentityUpdateTransitionDisableKeysError, InvalidIdentityUpdateTransitionEmptyError, TooManyMasterPublicKeyError, WithdrawalOutputScriptNotAllowedWhenSigningWithOwnerKeyError}; use dpp::consensus::basic::overflow_error::OverflowError; use dpp::consensus::basic::token::{ChoosingTokenMintRecipientNotAllowedError, ContractHasNoTokensError, DestinationIdentityForTokenMintingNotSetError, InvalidActionIdError, InvalidTokenAmountError, InvalidTokenConfigUpdateNoChangeError, InvalidTokenIdError, InvalidTokenNoteTooBigError, InvalidTokenPositionError, MissingDefaultLocalizationError, TokenNoteOnlyAllowedWhenProposerError, TokenTransferToOurselfError}; +use dpp::consensus::state::data_contract::data_contract_not_found_error::DataContractNotFoundError; use dpp::consensus::state::data_contract::data_contract_update_action_not_allowed_error::DataContractUpdateActionNotAllowedError; use dpp::consensus::state::data_contract::document_type_update_error::DocumentTypeUpdateError; use dpp::consensus::state::document::document_contest_currently_locked_error::DocumentContestCurrentlyLockedError; @@ -86,7 +87,7 @@ use dpp::consensus::state::identity::no_transfer_key_for_core_withdrawal_availab use dpp::consensus::state::identity::RecipientIdentityDoesNotExistError; use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_insufficient_error::PrefundedSpecializedBalanceInsufficientError; use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_not_found_error::PrefundedSpecializedBalanceNotFoundError; -use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountNotFrozenError, IdentityTokenAccountFrozenError, TokenIsPausedError, IdentityTokenAccountAlreadyFrozenError, UnauthorizedTokenActionError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, TokenMintPastMaxSupplyError, NewTokensDestinationIdentityDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, InvalidGroupPositionError, TokenAlreadyPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, TokenTransferRecipientIdentityNotExistError, PreProgrammedDistributionTimestampInPastError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale}; +use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountNotFrozenError, IdentityTokenAccountFrozenError, TokenIsPausedError, IdentityTokenAccountAlreadyFrozenError, UnauthorizedTokenActionError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, TokenMintPastMaxSupplyError, NewTokensDestinationIdentityDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, InvalidGroupPositionError, TokenAlreadyPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, TokenTransferRecipientIdentityNotExistError, PreProgrammedDistributionTimestampInPastError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError}; use dpp::consensus::state::voting::masternode_incorrect_voter_identity_id_error::MasternodeIncorrectVoterIdentityIdError; use dpp::consensus::state::voting::masternode_incorrect_voting_address_error::MasternodeIncorrectVotingAddressError; use dpp::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -423,6 +424,12 @@ pub fn from_state_error(state_error: &StateError) -> JsValue { StateError::IdentityToFreezeDoesNotExistError(e) => { generic_consensus_error!(IdentityToFreezeDoesNotExistError, e).into() } + StateError::DataContractNotFoundError(e) => { + generic_consensus_error!(DataContractNotFoundError, e).into() + } + StateError::InvalidTokenPositionStateError(e) => { + generic_consensus_error!(InvalidTokenPositionStateError, e).into() + } } } From 068536251430063e8974c8bd917b9cb1625f9382 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 23 May 2025 17:06:53 +0200 Subject: [PATCH 9/9] small fix --- .../state_transitions/data_contract_update/state/v0/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5cdac63ab0c..b44ecb8b3b5 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 @@ -403,7 +403,7 @@ impl DataContractUpdateStateTransitionStateValidationV0 for DataContractUpdateTr contract_fetch_info .0 .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( - "fee must exist in validate state for data contract create transition", + "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