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 4427bfbdecd..f0e4053c270 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 @@ -473,4 +473,60 @@ mod tests { )); assert!(!reference.must_exist); } + + #[test] + fn should_parse_refers_to_with_contract_target() { + let platform_version = PlatformVersion::latest(); + let config = + DataContractConfig::default_for_version(platform_version).expect("config should build"); + + let schema = json!({ + "type": "object", + "properties": { + "toContractId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "contract" + } + } + }, + "required": [], + "additionalProperties": false + }); + + let value = platform_value::to_value(schema).expect("schema should convert"); + + let document_type = DocumentType::try_from_schema( + Identifier::random(), + 0, + config.version(), + "msg", + value, + None, + &BTreeMap::new(), + &config, + false, + &mut vec![], + platform_version, + ) + .expect("should parse"); + + let reference = document_type + .as_ref() + .flattened_properties() + .get("toContractId") + .and_then(|p| p.reference.clone()) + .expect("reference should be present"); + + assert!(matches!( + reference.target, + DocumentPropertyReferenceTarget::Contract + )); + assert!(reference.must_exist); + } } diff --git a/packages/rs-drive-abci/src/execution/types/execution_operation/mod.rs b/packages/rs-drive-abci/src/execution/types/execution_operation/mod.rs index a7a2e5541f9..0cf3b91ed37 100644 --- a/packages/rs-drive-abci/src/execution/types/execution_operation/mod.rs +++ b/packages/rs-drive-abci/src/execution/types/execution_operation/mod.rs @@ -75,6 +75,7 @@ pub enum ValidationOperation { Protocol(ProtocolValidationOperation), RetrieveIdentityTokenBalance, RetrieveIdentity(RetrieveIdentityInfo), + RetrieveContract, RetrievePrefundedSpecializedBalance, RetrieveAddressNonceAndBalance(u16), PerformNetworkThresholdSigning, @@ -199,6 +200,19 @@ impl ValidationOperation { "execution processing fee overflow error", ))?; } + ValidationOperation::RetrieveContract => { + fee_result.processing_fee = fee_result + .processing_fee + .checked_add( + platform_version + .fee_version + .processing + .fetch_contract_processing_cost, + ) + .ok_or(ExecutionError::Overflow( + "execution processing fee overflow error", + ))?; + } ValidationOperation::RetrievePrefundedSpecializedBalance => { let operation_cost = platform_version .fee_version diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs index d93e3530d19..44526c60dbd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs @@ -109,7 +109,20 @@ fn validate_document_type_references_v0( return Ok(validation_result); } } - _ => continue, + DocumentPropertyReferenceTarget::Contract => { + let validation_result = validate_contract_reference_v0( + document_data, + path, + reference.must_exist, + platform, + transaction, + execution_context, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + } } } @@ -170,3 +183,55 @@ fn validate_identity_reference_v0( Ok(SimpleConsensusValidationResult::new()) } + +fn validate_contract_reference_v0( + document_data: &BTreeMap, + path: &str, + must_exist: bool, + platform: &PlatformStateRef, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, +) -> Result { + if !must_exist { + return Ok(SimpleConsensusValidationResult::new()); + } + + let contract_bytes = match document_data.get_optional_identifier_at_path(path) { + Ok(Some(contract_bytes)) => contract_bytes, + Ok(None) => { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidIdentifierError::new(path.to_string(), "not set".to_string()).into(), + )) + } + Err(err) => { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidIdentifierError::new(path.to_string(), err.to_string()).into(), + )) + } + }; + + execution_context.add_operation(ValidationOperation::RetrieveContract); + + let maybe_contract = platform + .drive + .fetch_contract(contract_bytes, None, None, transaction, platform_version) + .value?; + + if maybe_contract.is_none() { + let missing_id = + Identifier::from_bytes(&contract_bytes).map_err(|e| Error::Protocol(e.into()))?; + + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::ReferencedEntityNotFoundError( + ReferencedEntityNotFoundError::new( + missing_id, + DocumentPropertyReferenceTarget::Contract, + path.to_string(), + ), + )), + )); + } + + Ok(SimpleConsensusValidationResult::new()) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs index aba1b2db037..b4fb535d276 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs @@ -56,6 +56,8 @@ mod creation_tests { "tests/supporting_files/contract/reference-validation/reference-validation-contract-must-exist-false.json"; const REFERENCE_VALIDATION_NESTED_CONTRACT_PATH: &str = "tests/supporting_files/contract/reference-validation/reference-validation-contract-nested.json"; + const REFERENCE_VALIDATION_CONTRACT_REFERENCE_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-refers-to-contract.json"; // Helper to run document creation with custom reference mutations. fn run_reference_validation_creation_with_mutator( @@ -155,6 +157,103 @@ mod creation_tests { .clone() } + fn run_contract_reference_creation_with_mutator( + contract_path: &str, + mutator: F, + ) -> StateTransitionExecutionResult + where + F: FnOnce(&mut Document, Identifier, Identifier, Identifier), + { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(433); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + let (other_identity, ..) = setup_identity(&mut platform, 959, dash_to_credits!(0.1)); + + let contract = setup_contract( + &platform.drive, + contract_path, + None, + None, + None::, + None, + None, + ); + + let message = contract + .document_type_for_name("message") + .expect("expected a message document type"); + + let entropy = Bytes32::random_with_rng(&mut rng); + + let mut document = message + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random document"); + + mutator(&mut document, identity.id(), other_identity.id(), contract.id()); + + let documents_batch_create_transition = + BatchTransition::new_document_creation_transition_from_document( + document, + message, + entropy.0, + &key, + 2, + 0, + None, + &signer, + platform_version, + None, + ) + .expect("expect to create documents batch transition"); + + let documents_batch_create_serialized_transition = documents_batch_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( + &[documents_batch_create_serialized_transition], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + processing_result + .execution_results() + .first() + .expect("expected one execution result") + .clone() + } + #[test] fn test_document_creation() { let platform_version = PlatformVersion::latest(); @@ -4116,4 +4215,37 @@ mod creation_tests { } ); } + + #[test] + fn test_document_creation_succeeds_when_referenced_contract_exists() { + let result = run_contract_reference_creation_with_mutator( + REFERENCE_VALIDATION_CONTRACT_REFERENCE_PATH, + |document, _, _, contract_id| { + document.set("toContractId", contract_id.into()); + }, + ); + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[test] + fn test_document_creation_fails_when_referenced_contract_missing() { + let result = run_contract_reference_creation_with_mutator( + REFERENCE_VALIDATION_CONTRACT_REFERENCE_PATH, + |document, _, _, _| { + document.set("toContractId", Identifier::random().into()); + }, + ); + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index d4b533a97fc..7be5d5bfa8c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -14,6 +14,8 @@ mod replacement_tests { const REFERENCE_VALIDATION_CONTRACT_PATH: &str = "tests/supporting_files/contract/reference-validation/reference-validation-contract.json"; + const REFERENCE_VALIDATION_CONTRACT_REFERENCE_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-refers-to-contract.json"; fn run_reference_validation_replace_with_contract( contract_path: &str, @@ -170,6 +172,172 @@ mod replacement_tests { (result, processing_result.aggregated_fees().clone()) } + fn run_contract_reference_replace_with_contract( + contract_path: &str, + to_contract_id: F, + change_note: bool, + ) -> (StateTransitionExecutionResult, FeeResult) + where + F: FnOnce(Identifier, Identifier) -> Identifier, + { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(433); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + let (other_identity, ..) = setup_identity(&mut platform, 959, dash_to_credits!(0.1)); + + let contract = setup_contract( + &platform.drive, + contract_path, + None, + None, + None::, + None, + None, + ); + + let other_contract_id = Identifier::random(); + setup_contract( + &platform.drive, + contract_path, + Some(other_contract_id.to_buffer()), + None, + None::, + None, + None, + ); + + let message = contract + .document_type_for_name("message") + .expect("expected a message document type"); + + let entropy = Bytes32::random_with_rng(&mut rng); + + let mut document = message + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random document"); + + document.set("toContractId", contract.id().into()); + document.set("note", "before".into()); + + let documents_batch_create_transition = + BatchTransition::new_document_creation_transition_from_document( + document.clone(), + message, + entropy.0, + &key, + 2, + 0, + None, + &signer, + platform_version, + None, + ) + .expect("expect to create documents batch transition"); + + let documents_batch_create_serialized_transition = documents_batch_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( + &[documents_batch_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::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + document.increment_revision().unwrap(); + if change_note { + document.set("note", "after".into()); + } + document.set( + "toContractId", + to_contract_id(contract.id(), other_contract_id).into(), + ); + + let documents_batch_replace_transition = + BatchTransition::new_document_replacement_transition_from_document( + document, + message, + &key, + 3, + 0, + None, + &signer, + platform_version, + None, + ) + .expect("expect to create documents batch transition"); + + let documents_batch_replace_serialized_transition = documents_batch_replace_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( + &[documents_batch_replace_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let result = processing_result + .execution_results() + .first() + .expect("expected one execution result") + .clone(); + + (result, processing_result.aggregated_fees().clone()) + } + #[test] fn test_document_replace_on_document_type_that_is_mutable() { let platform_version = PlatformVersion::latest(); @@ -2315,4 +2483,41 @@ mod replacement_tests { } ); } + + #[test] + fn test_document_replace_fails_when_contract_reference_missing() { + let (result, _) = run_contract_reference_replace_with_contract( + REFERENCE_VALIDATION_CONTRACT_REFERENCE_PATH, + |_, _| Identifier::random(), + true, + ); + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } + + #[test] + fn test_document_replace_validates_only_changed_contract_fields() { + let (_, fee_without_reference) = run_contract_reference_replace_with_contract( + REFERENCE_VALIDATION_CONTRACT_REFERENCE_PATH, + |contract_id, _| contract_id, + true, + ); + + let (_, fee_with_reference) = run_contract_reference_replace_with_contract( + REFERENCE_VALIDATION_CONTRACT_REFERENCE_PATH, + |_, other_contract_id| other_contract_id, + true, + ); + + assert!( + fee_with_reference.processing_fee > fee_without_reference.processing_fee, + "expected contract reference validation to increase processing fee" + ); + } } diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/address_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/address_tests.rs index dfc2f85b1e7..c25744bdfc1 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/address_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/address_tests.rs @@ -3520,6 +3520,7 @@ mod tests { /// Funding operations create new addresses in "staged" state. After block commit, /// they become "committed" and available for transfers in subsequent blocks. #[test] + #[stack_size(4 * 1024 * 1024)] fn run_chain_high_throughput_address_operations() { drive_abci::logging::init_for_tests(LogLevel::Silent); diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-refers-to-contract.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-refers-to-contract.json new file mode 100644 index 00000000000..20a32124e06 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-refers-to-contract.json @@ -0,0 +1,38 @@ +{ + "$format_version": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "keywords": [], + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "toContractId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "contract" + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "toContractId" + ], + "indices": [], + "additionalProperties": false + } + }, + "groups": {}, + "tokens": {} +} diff --git a/packages/rs-platform-version/src/version/fee/processing/mod.rs b/packages/rs-platform-version/src/version/fee/processing/mod.rs index 723e08406fb..70086acf8c9 100644 --- a/packages/rs-platform-version/src/version/fee/processing/mod.rs +++ b/packages/rs-platform-version/src/version/fee/processing/mod.rs @@ -10,6 +10,7 @@ pub struct FeeProcessingVersion { pub fetch_identity_balance_and_revision_processing_cost: u64, pub fetch_identity_cost_per_look_up_key_by_id: u64, pub fetch_identity_token_balance_processing_cost: u64, + pub fetch_contract_processing_cost: u64, pub fetch_prefunded_specialized_balance_processing_cost: u64, pub fetch_key_with_type_nonce_and_balance_cost: u64, pub fetch_single_identity_key_processing_cost: u64, @@ -44,6 +45,7 @@ impl From for FeeProcessingVersi fetch_identity_token_balance_processing_cost: FEE_VERSION1 .processing .fetch_identity_token_balance_processing_cost, + fetch_contract_processing_cost: FEE_VERSION1.processing.fetch_contract_processing_cost, fetch_prefunded_specialized_balance_processing_cost: old .fetch_prefunded_specialized_balance_processing_cost, fetch_key_with_type_nonce_and_balance_cost: FEE_VERSION1 diff --git a/packages/rs-platform-version/src/version/fee/processing/v1.rs b/packages/rs-platform-version/src/version/fee/processing/v1.rs index 78192475054..87ddd91cebd 100644 --- a/packages/rs-platform-version/src/version/fee/processing/v1.rs +++ b/packages/rs-platform-version/src/version/fee/processing/v1.rs @@ -6,6 +6,7 @@ pub const FEE_PROCESSING_VERSION1: FeeProcessingVersion = FeeProcessingVersion { fetch_identity_balance_and_revision_processing_cost: 15000, fetch_identity_cost_per_look_up_key_by_id: 9000, fetch_identity_token_balance_processing_cost: 10000, + fetch_contract_processing_cost: 9000, // TODO: discuss cost fetch_prefunded_specialized_balance_processing_cost: 10000, fetch_key_with_type_nonce_and_balance_cost: 12000, fetch_single_identity_key_processing_cost: 10000,