diff --git a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json index baefe8f7f47..473ff4a3f6d 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json @@ -98,14 +98,56 @@ "enum": [ "identity", "contract", - "token" + "token", + "permanentDocument" ] + }, + "contractId": { + "description": "The id of the data contract the referenced document lives in, as a base58 string or a 32-byte array; when absent the reference targets the declaring contract itself", + "oneOf": [ + { + "type": "string", + "minLength": 32, + "maxLength": 44, + "pattern": "^[123456789A-HJ-NP-Za-km-z]{32,44}$" + }, + { + "type": "array", + "minItems": 32, + "maxItems": 32, + "items": { + "type": "integer", + "minimum": 0, + "maximum": 255 + } + } + ] + }, + "documentType": { + "description": "The name of the referenced document type; it must forbid deletion (canBeDeleted: false)", + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9-_]{1,64}$" } }, "required": [ "type" ], - "additionalProperties": false + "additionalProperties": false, + "if": { + "properties": { "type": { "const": "permanentDocument" } }, + "required": ["type"] + }, + "then": { + "required": ["type", "documentType"] + }, + "else": { + "properties": { + "contractId": false, + "documentType": false + } + } }, "contains": { "$ref": "https://json-schema.org/draft/2020-12/meta/applicator#/properties/contains" 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 63abaf8496c..2481cb9c773 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 @@ -354,6 +354,34 @@ fn apply_property_reference_v0( "identity" => DocumentPropertyReferenceTarget::Identity, "contract" => DocumentPropertyReferenceTarget::Contract, "token" => DocumentPropertyReferenceTarget::Token, + "permanentDocument" => { + // An absent contractId means the reference targets a document + // type of the declaring contract itself + let contract_id = refers_to_map + .get(property_names::CONTRACT_ID) + .map(|value| { + value + .to_identifier() + .map_err(|e| DataContractError::ValueWrongType(e.to_string())) + }) + .transpose()?; + + let document_type_name = refers_to_map + .get_str(property_names::DOCUMENT_TYPE) + .map_err(|e| DataContractError::ValueWrongType(e.to_string()))?; + + if document_type_name.is_empty() || document_type_name.len() > 64 { + return Err(DataContractError::InvalidContractStructure( + "permanentDocument refersTo documentType must be between 1 and 64 characters" + .to_string(), + )); + } + + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id, + document_type_name: document_type_name.to_string(), + } + } other => { return Err(DataContractError::InvalidContractStructure(format!( "invalid refersTo type {other}" @@ -369,6 +397,7 @@ mod tests { use super::*; use crate::data_contract::config::DataContractConfig; use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; + use platform_value::string_encoding::Encoding; use serde_json::json; fn try_document_type_from_schema( @@ -461,6 +490,174 @@ mod tests { ); } + #[test] + fn should_parse_permanent_document_refers_to() { + let contract_id = Identifier::from([7u8; 32]); + + let document_type = try_document_type_from_schema(json!({ + "type": "object", + "properties": { + "parentNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "contractId": contract_id.to_string(Encoding::Base58), + "documentType": "note" + } + } + }, + "required": [], + "additionalProperties": false + })) + .expect("should parse"); + + let property_type = document_type + .as_ref() + .flattened_properties() + .get("parentNoteId") + .map(|p| p.property_type.clone()) + .expect("property should be present"); + + assert_eq!( + property_type, + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id: Some(contract_id), + document_type_name: "note".to_string(), + } + ) + ); + } + + #[test] + fn should_parse_permanent_document_refers_to_without_contract_id_as_own_contract() { + let document_type = try_document_type_from_schema(json!({ + "type": "object", + "properties": { + "parentNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "documentType": "note" + } + } + }, + "required": [], + "additionalProperties": false + })) + .expect("should parse"); + + let property_type = document_type + .as_ref() + .flattened_properties() + .get("parentNoteId") + .map(|p| p.property_type.clone()) + .expect("property should be present"); + + assert_eq!( + property_type, + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id: None, + document_type_name: "note".to_string(), + } + ) + ); + } + + #[test] + fn should_reject_permanent_document_refers_to_with_invalid_contract_id() { + let err = try_document_type_from_schema(json!({ + "type": "object", + "properties": { + "parentNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "contractId": "not-a-valid-identifier", + "documentType": "note" + } + } + }, + "required": [], + "additionalProperties": false + })) + .expect_err("should fail"); + + let message = err.to_string(); + assert!(message.contains("base 58"), "unexpected error: {message}"); + } + + #[test] + fn should_reject_permanent_document_refers_to_with_oversized_document_type_name() { + let err = try_document_type_from_schema(json!({ + "type": "object", + "properties": { + "parentNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "contractId": Identifier::from([7u8; 32]).to_string(Encoding::Base58), + "documentType": "a".repeat(65) + } + } + }, + "required": [], + "additionalProperties": false + })) + .expect_err("should fail"); + + let message = err.to_string(); + assert!( + message.contains("between 1 and 64 characters"), + "unexpected error: {message}" + ); + } + + #[test] + fn should_reject_permanent_document_refers_to_without_document_type() { + try_document_type_from_schema(json!({ + "type": "object", + "properties": { + "parentNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "contractId": Identifier::from([7u8; 32]).to_string(Encoding::Base58) + } + } + }, + "required": [], + "additionalProperties": false + })) + .expect_err("should fail"); + } + #[test] fn should_ignore_refers_to_on_platform_versions_predating_it() { // Platform versions whose tables carry `apply_property_reference: None` diff --git a/packages/rs-dpp/src/data_contract/document_type/mod.rs b/packages/rs-dpp/src/data_contract/document_type/mod.rs index 7a77778410c..158995b5568 100644 --- a/packages/rs-dpp/src/data_contract/document_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/mod.rs @@ -80,6 +80,8 @@ pub(crate) mod property_names { pub const ENCRYPTION_KEY_REQUIREMENTS: &str = "encryptionKeyReqs"; pub const DECRYPTION_KEY_REQUIREMENTS: &str = "decryptionKeyReqs"; pub const REFERS_TO: &str = "refersTo"; + pub const CONTRACT_ID: &str = "contractId"; + pub const DOCUMENT_TYPE: &str = "documentType"; pub const DOCUMENTS_COUNTABLE: &str = "documentsCountable"; pub const RANGE_COUNTABLE: &str = "rangeCountable"; /// Doctype-level flag naming the property whose values are summed into diff --git a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs index 779c82a37ed..bfb3cb593f0 100644 --- a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs @@ -53,6 +53,8 @@ pub struct ByteArrayPropertySizes { pub max_size: Option, } +// This enum is embedded in consensus errors, so it is consensus-serialized. +// @append_only #[derive( Debug, PartialEq, Eq, Clone, Serialize, Encode, Decode, PlatformSerialize, PlatformDeserialize, )] @@ -61,6 +63,18 @@ pub enum DocumentPropertyReferenceTarget { Identity, Contract, Token, + /// A document of a document type whose documents can never be deleted + /// (`canBeDeleted: false`). Only such document types may be referenced: + /// together with document types being non-removable and the + /// `canBeDeleted` flag being immutable on contract updates, this + /// guarantees a validated reference can never dangle. + #[serde(rename = "permanentDocument")] + PermanentDocument { + /// The contract the referenced document type lives in; `None` means + /// the declaring contract itself + contract_id: Option, + document_type_name: String, + }, } impl std::fmt::Display for DocumentPropertyReferenceTarget { @@ -69,6 +83,20 @@ impl std::fmt::Display for DocumentPropertyReferenceTarget { DocumentPropertyReferenceTarget::Identity => write!(f, "identity"), DocumentPropertyReferenceTarget::Contract => write!(f, "contract"), DocumentPropertyReferenceTarget::Token => write!(f, "token"), + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id: Some(contract_id), + document_type_name, + } => write!( + f, + "permanent document (contract {contract_id}, document type {document_type_name})" + ), + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id: None, + document_type_name, + } => write!( + f, + "permanent document (own contract, document type {document_type_name})" + ), } } } @@ -7134,4 +7162,35 @@ mod tests { })) ); } + + #[test] + fn should_display_reference_targets() { + let contract_id = Identifier::from([7u8; 32]); + + assert_eq!( + DocumentPropertyReferenceTarget::Identity.to_string(), + "identity" + ); + assert_eq!( + DocumentPropertyReferenceTarget::Contract.to_string(), + "contract" + ); + assert_eq!(DocumentPropertyReferenceTarget::Token.to_string(), "token"); + assert_eq!( + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id: Some(contract_id), + document_type_name: "note".to_string(), + } + .to_string(), + format!("permanent document (contract {contract_id}, document type note)") + ); + assert_eq!( + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id: None, + document_type_name: "note".to_string(), + } + .to_string(), + "permanent document (own contract, document type note)" + ); + } } diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index fceb51d471f..0d57e72afab 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -312,6 +312,8 @@ impl ErrorWithCode for StateError { Self::DocumentContestIndexMismatchError(_) => 40118, Self::DocumentContestNotRequiredError(_) => 40119, Self::ReferencedEntityNotFoundError(_) => 40120, + Self::ReferencedDocumentTypeNotFoundError(_) => 40121, + Self::ReferencedDocumentTypeDeletableError(_) => 40122, // Identity Errors: 40200-40299 Self::IdentityAlreadyExistsError(_) => 40200, diff --git a/packages/rs-dpp/src/errors/consensus/state/document/mod.rs b/packages/rs-dpp/src/errors/consensus/state/document/mod.rs index bc684d57f1f..3b7996c91ef 100644 --- a/packages/rs-dpp/src/errors/consensus/state/document/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/document/mod.rs @@ -15,4 +15,6 @@ pub mod document_timestamps_are_equal_error; pub mod document_timestamps_mismatch_error; pub mod duplicate_unique_index_error; pub mod invalid_document_revision_error; +pub mod referenced_document_type_deletable_error; +pub mod referenced_document_type_not_found_error; pub mod referenced_entity_not_found_error; diff --git a/packages/rs-dpp/src/errors/consensus/state/document/referenced_document_type_deletable_error.rs b/packages/rs-dpp/src/errors/consensus/state/document/referenced_document_type_deletable_error.rs new file mode 100644 index 00000000000..c8a300dbb60 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/document/referenced_document_type_deletable_error.rs @@ -0,0 +1,51 @@ +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::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("documents of referenced document type {document_type_name} in contract {contract_id} can be deleted; a permanentDocument reference at path {path} requires a document type with canBeDeleted: false")] +#[platform_serialize(unversioned)] +pub struct ReferencedDocumentTypeDeletableError { + /* + + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + + */ + contract_id: Identifier, + document_type_name: String, + path: String, +} + +impl ReferencedDocumentTypeDeletableError { + pub fn new(contract_id: Identifier, document_type_name: String, path: String) -> Self { + Self { + contract_id, + document_type_name, + path, + } + } + + pub fn contract_id(&self) -> &Identifier { + &self.contract_id + } + + pub fn document_type_name(&self) -> &str { + &self.document_type_name + } + + pub fn path(&self) -> &str { + &self.path + } +} + +impl From for ConsensusError { + fn from(err: ReferencedDocumentTypeDeletableError) -> Self { + Self::StateError(StateError::ReferencedDocumentTypeDeletableError(err)) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/state/document/referenced_document_type_not_found_error.rs b/packages/rs-dpp/src/errors/consensus/state/document/referenced_document_type_not_found_error.rs new file mode 100644 index 00000000000..0981f9ebc3c --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/document/referenced_document_type_not_found_error.rs @@ -0,0 +1,51 @@ +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::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("referenced document type {document_type_name} not found in contract {contract_id} for path {path}")] +#[platform_serialize(unversioned)] +pub struct ReferencedDocumentTypeNotFoundError { + /* + + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + + */ + contract_id: Identifier, + document_type_name: String, + path: String, +} + +impl ReferencedDocumentTypeNotFoundError { + pub fn new(contract_id: Identifier, document_type_name: String, path: String) -> Self { + Self { + contract_id, + document_type_name, + path, + } + } + + pub fn contract_id(&self) -> &Identifier { + &self.contract_id + } + + pub fn document_type_name(&self) -> &str { + &self.document_type_name + } + + pub fn path(&self) -> &str { + &self.path + } +} + +impl From for ConsensusError { + fn from(err: ReferencedDocumentTypeNotFoundError) -> Self { + Self::StateError(StateError::ReferencedDocumentTypeNotFoundError(err)) + } +} 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 4b7aec58651..cb54245057e 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -41,6 +41,8 @@ use crate::consensus::state::document::document_contest_index_mismatch_error::Do use crate::consensus::state::document::document_contest_not_joinable_error::DocumentContestNotJoinableError; use crate::consensus::state::document::document_contest_not_paid_for_error::DocumentContestNotPaidForError; use crate::consensus::state::document::document_contest_not_required_error::DocumentContestNotRequiredError; +use crate::consensus::state::document::referenced_document_type_deletable_error::ReferencedDocumentTypeDeletableError; +use crate::consensus::state::document::referenced_document_type_not_found_error::ReferencedDocumentTypeNotFoundError; use crate::consensus::state::document::referenced_entity_not_found_error::ReferencedEntityNotFoundError; use crate::consensus::state::document::document_incorrect_purchase_price_error::DocumentIncorrectPurchasePriceError; use crate::consensus::state::document::document_not_for_sale_error::DocumentNotForSaleError; @@ -366,6 +368,12 @@ pub enum StateError { #[error(transparent)] ReferencedEntityNotFoundError(ReferencedEntityNotFoundError), + + #[error(transparent)] + ReferencedDocumentTypeNotFoundError(ReferencedDocumentTypeNotFoundError), + + #[error(transparent)] + ReferencedDocumentTypeDeletableError(ReferencedDocumentTypeDeletableError), } impl From for ConsensusError { @@ -442,5 +450,25 @@ mod tests { )), 93 ); + assert_eq!( + discriminant_of(StateError::ReferencedDocumentTypeNotFoundError( + ReferencedDocumentTypeNotFoundError::new( + Identifier::from([1; 32]), + "note".to_string(), + "parentNoteId".to_string(), + ) + )), + 94 + ); + assert_eq!( + discriminant_of(StateError::ReferencedDocumentTypeDeletableError( + ReferencedDocumentTypeDeletableError::new( + Identifier::from([1; 32]), + "note".to_string(), + "parentNoteId".to_string(), + ) + )), + 95 + ); } } diff --git a/packages/rs-dpp/src/validation/meta_validators/mod.rs b/packages/rs-dpp/src/validation/meta_validators/mod.rs index 82b3c38f2c3..d05da98b1e4 100644 --- a/packages/rs-dpp/src/validation/meta_validators/mod.rs +++ b/packages/rs-dpp/src/validation/meta_validators/mod.rs @@ -339,6 +339,92 @@ mod tests { } } + #[test] + fn should_accept_permanent_document_refers_to_in_v3_document_schema() { + let schema = document_schema_with_refers_to(json!({ + "type": "permanentDocument", + "contractId": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "documentType": "note" + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_ok(), + "expected permanentDocument refersTo to be valid" + ); + } + + #[test] + fn should_accept_permanent_document_refers_to_with_byte_array_contract_id() { + let schema = document_schema_with_refers_to(json!({ + "type": "permanentDocument", + "contractId": vec![7u8; 32], + "documentType": "note" + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_ok(), + "expected a byte-array contractId to be valid" + ); + } + + #[test] + fn should_accept_permanent_document_refers_to_without_contract_id() { + // An absent contractId targets the declaring contract itself + let schema = document_schema_with_refers_to(json!({ + "type": "permanentDocument", + "documentType": "note" + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_ok(), + "expected permanentDocument refersTo without contractId to be valid" + ); + } + + #[test] + fn should_reject_permanent_document_refers_to_without_document_type() { + let schema = document_schema_with_refers_to(json!({ + "type": "permanentDocument", + "contractId": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd" + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_err(), + "expected permanentDocument refersTo without documentType to be invalid" + ); + } + + #[test] + fn should_reject_contract_id_on_non_document_refers_to_targets() { + for target in ["identity", "contract", "token"] { + let schema = document_schema_with_refers_to(json!({ + "type": target, + "contractId": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd" + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_err(), + "expected contractId on a {target} target to be invalid" + ); + } + } + + #[test] + fn should_reject_permanent_document_refers_to_with_invalid_contract_id() { + for bad in [json!("not-base58-0OIl"), json!(vec![7u8; 31]), json!(42)] { + let schema = document_schema_with_refers_to(json!({ + "type": "permanentDocument", + "contractId": bad, + "documentType": "note" + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_err(), + "expected invalid contractId {bad} to be rejected" + ); + } + } + #[test] fn should_reject_refers_to_with_unknown_properties() { let schema = document_schema_with_refers_to(json!({ 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 d3b026d6462..d444bd7cb02 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 @@ -10,6 +10,9 @@ use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::{ DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentTypeRef, }; +use dpp::data_contract::DataContract; +use dpp::errors::consensus::state::document::referenced_document_type_deletable_error::ReferencedDocumentTypeDeletableError; +use dpp::errors::consensus::state::document::referenced_document_type_not_found_error::ReferencedDocumentTypeNotFoundError; use dpp::errors::consensus::state::document::referenced_entity_not_found_error::ReferencedEntityNotFoundError; use dpp::identifier::Identifier; use dpp::platform_value::btreemap_extensions::BTreeValueMapPathHelper; @@ -26,6 +29,7 @@ use crate::execution::types::execution_operation::{RetrieveIdentityInfo, Validat use crate::execution::types::state_transition_execution_context::{ StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, }; +use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_document_with_id; use crate::platform_types::platform::PlatformStateRef; /// Versioned, stateful validation of document references using the v0 rules. @@ -71,6 +75,7 @@ impl DocumentReferenceValidationV0 for DocumentBaseTransitionAction { }; validate_document_type_references_v0( + contract, document_type, document_data, changed_fields, @@ -85,6 +90,7 @@ impl DocumentReferenceValidationV0 for DocumentBaseTransitionAction { #[allow(clippy::too_many_arguments)] fn validate_document_type_references_v0( + contract: &DataContract, document_type: DocumentTypeRef<'_>, document_data: &BTreeMap, changed_fields: Option<&BTreeSet>, @@ -165,6 +171,93 @@ fn validate_document_type_references_v0( referenced_token_info.is_some() } + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id: referenced_contract_id, + document_type_name, + } => { + // An absent contract id targets the declaring contract itself; the + // declaring contract may also name its own id explicitly. Either + // way it is already loaded for this transition, so no fetch is + // billed for it + let effective_contract_id = referenced_contract_id.unwrap_or(contract.id()); + let referenced_contract_fetch_info; + let referenced_contract = if effective_contract_id == contract.id() { + contract + } else { + let (fee, fetch_info) = platform.drive.get_contract_with_fetch_info_and_fee( + effective_contract_id.to_buffer(), + Some(&block_info.epoch), + false, + transaction, + platform_version, + )?; + + let fee = + fee.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "fee must exist when fetching a referenced contract with an epoch", + )))?; + + // The cost is added even if the referenced contract does not exist or was cached + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee)); + + let Some(fetch_info) = fetch_info else { + // A missing contract and a missing document type resolve to the + // same failure: the declared document type could not be found + return Ok(SimpleConsensusValidationResult::new_with_error( + ReferencedDocumentTypeNotFoundError::new( + effective_contract_id, + document_type_name.clone(), + path.to_string(), + ) + .into(), + )); + }; + + referenced_contract_fetch_info = fetch_info; + &referenced_contract_fetch_info.contract + }; + + let Some(referenced_document_type) = + referenced_contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + ReferencedDocumentTypeNotFoundError::new( + effective_contract_id, + document_type_name.clone(), + path.to_string(), + ) + .into(), + )); + }; + + // Only document types whose documents can never be deleted may be + // referenced: `canBeDeleted` is immutable on contract updates and + // document types can not be removed, so a reference validated here + // can never dangle + if referenced_document_type.documents_can_be_deleted() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ReferencedDocumentTypeDeletableError::new( + effective_contract_id, + document_type_name.clone(), + path.to_string(), + ) + .into(), + )); + } + + fetch_document_with_id( + platform.drive, + referenced_contract, + referenced_document_type, + Identifier::from(referenced_id), + &block_info.epoch, + execution_context, + transaction, + platform_version, + )? + .is_some() + } }; if !exists { 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 a221eba8be8..70120bda45f 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 @@ -5007,4 +5007,306 @@ mod creation_tests { } ); } + + const REFERENCE_VALIDATION_PERMANENT_DOC_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc.json"; + const REFERENCE_VALIDATION_PERMANENT_DOC_FOREIGN_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-foreign.json"; + + /// Committed documents the permanent-document reference tests can point at: + /// a `note` in the declaring contract and a `note` in the foreign fixture + /// contract (both types have `canBeDeleted: false`). + struct PermanentReferenceTargets { + note_id: Identifier, + foreign_note_id: Identifier, + } + + /// Registers the permanent-document fixture contract and its foreign + /// counterpart, creates and commits a `note` document in each, then creates + /// a `message` document mutated by the test and returns that transition's + /// execution result. + async fn run_permanent_document_reference_creation( + mutator: F, + ) -> StateTransitionExecutionResult + where + F: FnOnce(&mut Document, &PermanentReferenceTargets), + { + 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 contract = setup_contract( + &platform.drive, + REFERENCE_VALIDATION_PERMANENT_DOC_CONTRACT_PATH, + None, + None, + None::, + None, + None, + ); + + let foreign_contract = setup_contract( + &platform.drive, + REFERENCE_VALIDATION_PERMANENT_DOC_FOREIGN_CONTRACT_PATH, + None, + None, + None::, + None, + None, + ); + + let mut note_ids = Vec::new(); + + for (note_contract, nonce) in [(&contract, 2), (&foreign_contract, 3)] { + let note = note_contract + .document_type_for_name("note") + .expect("expected a note document type"); + + let note_entropy = Bytes32::random_with_rng(&mut rng); + + let note_document = note + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + note_entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random note document"); + + note_ids.push(note_document.id()); + + let note_create_transition = + BatchTransition::new_document_creation_transition_from_document( + note_document, + note, + note_entropy.0, + &key, + nonce, + 0, + None, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create note batch transition"); + + let note_create_serialized_transition = note_create_transition + .serialize_to_bytes() + .expect("expected note batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[note_create_serialized_transition], + &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"); + } + + let targets = PermanentReferenceTargets { + note_id: note_ids[0], + foreign_note_id: note_ids[1], + }; + + 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, + // The reference properties are all optional; each test sets + // only the one it exercises + DocumentFieldFillType::DoNotFillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random message document"); + + mutator(&mut document, &targets); + + let documents_batch_create_transition = + BatchTransition::new_document_creation_transition_from_document( + document, + message, + entropy.0, + &key, + 4, + 0, + None, + &signer, + platform_version, + None, + ) + .await + .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() + } + + #[tokio::test] + async fn should_document_creation_succeed_when_referenced_permanent_document_exists() { + let result = run_permanent_document_reference_creation(|document, targets| { + document.set("noteId", targets.note_id.into()); + }) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_referenced_permanent_document_in_other_contract() + { + // Exercises the foreign-contract path: the referenced contract is + // fetched from state (billed), its document type resolved, and the + // referenced document's existence checked in that contract's tree + let result = run_permanent_document_reference_creation(|document, targets| { + document.set("crossContractNoteId", targets.foreign_note_id.into()); + }) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_referenced_permanent_document_missing() { + let result = run_permanent_document_reference_creation(|document, _| { + document.set("noteId", Identifier::random().into()); + }) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_referenced_document_type_is_deletable() { + // The referenced document type exists but allows deletion, so even an + // existing document of that type may not be referenced + let result = run_permanent_document_reference_creation(|document, targets| { + document.set("deletableNoteId", targets.note_id.into()); + }) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError( + StateError::ReferencedDocumentTypeDeletableError(_) + ), + .. + } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_referenced_document_type_missing() { + let result = run_permanent_document_reference_creation(|document, targets| { + document.set("unknownTypeNoteId", targets.note_id.into()); + }) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedDocumentTypeNotFoundError( + _ + )), + .. + } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_referenced_document_contract_missing() { + let result = run_permanent_document_reference_creation(|document, targets| { + document.set("foreignNoteId", targets.note_id.into()); + }) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedDocumentTypeNotFoundError( + _ + )), + .. + } + ); + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/mod.rs new file mode 100644 index 00000000000..f129339b307 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/mod.rs @@ -0,0 +1,50 @@ +mod v0; + +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::DataContract; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::drive::Drive; +use drive::query::TransactionArg; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; + +/// Validates the `refersTo` reference declarations carried by a contract's +/// document types, at contract create or update time. +/// +/// Only `permanentDocument` targets carry declaration content to check: the +/// referenced contract (the declaring contract itself when no contract id is +/// named) must contain the referenced document type, and that type must forbid +/// deletion. Identity, contract and token targets declare nothing beyond their +/// kind, so they have nothing to validate here. +pub(in crate::execution::validation::state_transition) fn validate_data_contract_references( + contract: &DataContract, + drive: &Drive, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .data_contract_reference_validation + { + 0 => v0::validate_data_contract_references_v0( + contract, + drive, + block_info, + execution_context, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "validate_data_contract_references".to_string(), + known_versions: vec![0], + received: version, + })), + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs new file mode 100644 index 00000000000..1d319d85318 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs @@ -0,0 +1,143 @@ +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::{DocumentPropertyReferenceTarget, DocumentPropertyType}; +use dpp::data_contract::DataContract; +use dpp::errors::consensus::state::document::referenced_document_type_deletable_error::ReferencedDocumentTypeDeletableError; +use dpp::errors::consensus::state::document::referenced_document_type_not_found_error::ReferencedDocumentTypeNotFoundError; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::drive::contract::DataContractFetchInfo; +use drive::drive::Drive; +use drive::query::TransactionArg; +use std::collections::BTreeMap; +use std::sync::Arc; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; + +/// Checks every `permanentDocument` reference declaration of the given +/// contract: the referenced contract must exist (the declaring contract +/// itself when no contract id is named, including when it names its own id), +/// the referenced document type must exist in it, and that type must forbid +/// deletion. Self references are checked against the in-flight contract, so a +/// contract may reference its own document types on creation; foreign contract +/// fetches are billed. +/// +/// The error paths name the failing declaration as +/// `documentTypeName.propertyPath`. Validation stops at the first invalid +/// declaration: this bounds the billed work an invalid contract can cause and +/// matches document write-time reference validation. Foreign contract +/// resolutions are memoized per contract id, so a contract declaring many +/// references into the same foreign contract is billed one fetch for it. +pub(super) fn validate_data_contract_references_v0( + contract: &DataContract, + drive: &Drive, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + // Memoizes foreign contract resolutions (including misses) so repeated + // declarations naming the same contract are billed a single fetch + let mut fetched_contracts: BTreeMap>> = + BTreeMap::new(); + + for (declaring_type_name, document_type) in contract.document_types() { + for (path, property) in document_type.as_ref().flattened_properties() { + let DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id, + document_type_name, + }, + ) = &property.property_type + else { + continue; + }; + + let declaration_path = format!("{declaring_type_name}.{path}"); + + let effective_contract_id = contract_id.unwrap_or(contract.id()); + + let referenced_contract_fetch_info; + let referenced_contract = if effective_contract_id == contract.id() { + contract + } else { + let resolved = match fetched_contracts.get(&effective_contract_id) { + Some(cached) => cached.clone(), + None => { + let (fee, fetch_info) = drive.get_contract_with_fetch_info_and_fee( + effective_contract_id.to_buffer(), + Some(&block_info.epoch), + false, + transaction, + platform_version, + )?; + + let fee = + fee.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "fee must exist when fetching a referenced contract with an epoch", + )))?; + + // The cost is added even if the referenced contract does not exist + // or was served from Drive's own contract cache; only locally + // memoized repeats above skip it + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee)); + + fetched_contracts.insert(effective_contract_id, fetch_info.clone()); + + fetch_info + } + }; + + let Some(fetch_info) = resolved else { + // A missing contract and a missing document type resolve to the + // same failure: the declared document type could not be found + return Ok(SimpleConsensusValidationResult::new_with_error( + ReferencedDocumentTypeNotFoundError::new( + effective_contract_id, + document_type_name.clone(), + declaration_path, + ) + .into(), + )); + }; + + referenced_contract_fetch_info = fetch_info; + &referenced_contract_fetch_info.contract + }; + + let Some(referenced_document_type) = + referenced_contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + ReferencedDocumentTypeNotFoundError::new( + effective_contract_id, + document_type_name.clone(), + declaration_path, + ) + .into(), + )); + }; + + if referenced_document_type.documents_can_be_deleted() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ReferencedDocumentTypeDeletableError::new( + effective_contract_id, + document_type_name.clone(), + declaration_path, + ) + .into(), + )); + } + } + } + + Ok(SimpleConsensusValidationResult::new()) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/mod.rs new file mode 100644 index 00000000000..89c40f4e7f6 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/mod.rs @@ -0,0 +1,2 @@ +/// Validation of the reference declarations a contract's document types carry. +pub mod data_contract_reference_validation; 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 cb805523300..20c4a9ba5fd 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 @@ -26,6 +26,7 @@ use crate::execution::types::state_transition_execution_context::StateTransition use crate::execution::validation::state_transition::data_contract_create::advanced_structure::v0::DataContractCreatedStateTransitionAdvancedStructureValidationV0; use crate::execution::validation::state_transition::data_contract_create::state::v0::DataContractCreateStateTransitionStateValidationV0; +use crate::execution::validation::state_transition::data_contract_create::state::v1::DataContractCreateStateTransitionStateValidationV1; use crate::execution::validation::state_transition::processor::advanced_structure_without_state::StateTransitionAdvancedStructureValidationV0; use crate::execution::validation::state_transition::processor::basic_structure::StateTransitionBasicStructureValidationV0; use crate::execution::validation::state_transition::processor::state::StateTransitionStateValidation; @@ -171,9 +172,17 @@ impl StateTransitionStateValidation for DataContractCreateTransition { execution_context, platform_version, ), + 1 => self.validate_state_v1( + platform, + block_info, + validation_mode, + tx, + execution_context, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "data contract create transition: validate_state".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } @@ -5220,4 +5229,163 @@ mod tests { [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); } + + mod permanent_document_reference_declarations { + use super::*; + use dpp::consensus::state::state_error::StateError; + use drive::util::test_helpers::setup_contract; + + const FOREIGN_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-foreign.json"; + + /// Processes a data contract create transition built from the given + /// fixture, with the foreign permanent-document fixture contract + /// already in state, and returns the execution result. + async fn run_contract_create(fixture_path: &str) -> StateTransitionExecutionResult { + 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, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(2.0)); + + setup_contract( + &platform.drive, + FOREIGN_CONTRACT_PATH, + None, + None, + None::, + None, + None, + ); + + let mut data_contract = json_document_to_contract_with_ids( + fixture_path, + None, + None, + false, //no need to validate the data contracts in tests for drive + platform_version, + ) + .expect("expected to get json based contract"); + + data_contract + .set_config(DataContractConfig::default_for_version(platform_version).unwrap()); + + let data_contract_create_transition = + DataContractCreateTransition::new_from_data_contract( + data_contract, + 1, + &identity.into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .await + .expect("expect to create data contract create transition"); + + let data_contract_create_serialized_transition = data_contract_create_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_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() + } + + #[tokio::test] + async fn should_register_contract_with_valid_permanent_document_references() { + // A self reference (no contractId) and a reference into the + // registered foreign contract are both valid declarations + let result = run_contract_create( + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-valid.json", + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_reject_contract_referencing_deletable_document_type() { + let result = run_contract_create( + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-deletable.json", + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::ReferencedDocumentTypeDeletableError(_) + ), + .. + } + ); + } + + #[tokio::test] + async fn should_reject_contract_referencing_unknown_own_document_type() { + let result = run_contract_create( + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-unknown-type.json", + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::ReferencedDocumentTypeNotFoundError(_) + ), + .. + } + ); + } + + #[tokio::test] + async fn should_reject_contract_referencing_missing_contract() { + let result = run_contract_create( + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-missing-contract.json", + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::ReferencedDocumentTypeNotFoundError(_) + ), + .. + } + ); + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/mod.rs index 9a1925de7fc..008be12cc67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v1/mod.rs new file mode 100644 index 00000000000..5e3167b9440 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v1/mod.rs @@ -0,0 +1,84 @@ +use dpp::block::block_info::BlockInfo; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::system::bump_identity_nonce_action::BumpIdentityNonceAction; +use drive::state_transition_action::StateTransitionAction; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::data_contract_common::data_contract_reference_validation::validate_data_contract_references; +use crate::execution::validation::state_transition::state_transitions::data_contract_create::state::v0::DataContractCreateStateTransitionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformRef; +use crate::rpc::core::CoreRPCLike; + +pub(in crate::execution::validation::state_transition::state_transitions::data_contract_create) trait DataContractCreateStateTransitionStateValidationV1 +{ + fn validate_state_v1( + &self, + platform: &PlatformRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + tx: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl DataContractCreateStateTransitionStateValidationV1 for DataContractCreateTransition { + fn validate_state_v1( + &self, + platform: &PlatformRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + tx: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let action = self.validate_state_v0::( + platform, + block_info, + validation_mode, + tx, + execution_context, + platform_version, + )?; + + if !action.is_valid() { + return Ok(action); + } + + let reference_result = { + let StateTransitionAction::DataContractCreateAction(create_action) = + action.data_as_borrowed()? + else { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "a valid data contract create state validation must contain a create action", + ))); + }; + + validate_data_contract_references( + create_action.data_contract_ref(), + platform.drive, + block_info, + execution_context, + tx, + platform_version, + )? + }; + + if !reference_result.is_valid() { + return Ok(ConsensusValidationResult::new_with_data_and_errors( + StateTransitionAction::BumpIdentityNonceAction( + BumpIdentityNonceAction::from_borrowed_data_contract_create_transition(self), + ), + reference_result.errors, + )); + } + + Ok(action) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs index 2f607a2d768..c7198c7bee6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs @@ -3137,4 +3137,148 @@ mod tests { assert!(!docs_after.contains(&"old1".to_string())); } } + + mod permanent_document_reference_declarations { + use super::*; + use dpp::consensus::state::state_error::StateError; + use drive::util::test_helpers::setup_contract; + + const V1_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-valid.json"; + const FOREIGN_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-foreign.json"; + + /// Applies the valid permanent-document reference fixture (and its + /// foreign counterpart) to the state, then processes an update built + /// from the given fixture at version 2 and returns the execution + /// result. + async fn run_contract_update(updated_fixture_path: &str) -> StateTransitionExecutionResult { + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected to get current platform version"); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + setup_contract( + &platform.drive, + FOREIGN_CONTRACT_PATH, + None, + None, + None::, + None, + None, + ); + + let mut contract = json_document_to_contract(V1_PATH, true, platform_version) + .expect("expected to get data contract"); + + contract.set_owner_id(identity.id()); + contract.set_config(DataContractConfig::default_for_version(platform_version).unwrap()); + + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + let mut updated_contract = + json_document_to_contract(updated_fixture_path, true, platform_version) + .expect("expected to get updated data contract"); + + updated_contract.set_owner_id(identity.id()); + updated_contract + .set_config(DataContractConfig::default_for_version(platform_version).unwrap()); + updated_contract.set_version(2); + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create data contract update transition"); + + let data_contract_update_serialized_transition = data_contract_update_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[data_contract_update_serialized_transition], + &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() + } + + #[tokio::test] + async fn should_update_contract_adding_valid_permanent_document_reference() { + let result = run_contract_update( + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-good.json", + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_reject_contract_update_adding_invalid_permanent_document_reference() { + // The updated version adds a property referencing a document type + // the contract does not define + let result = run_contract_update( + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-bad.json", + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::ReferencedDocumentTypeNotFoundError(_) + ), + .. + } + ); + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/mod.rs index b6e1af89cff..9a79cc76590 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/mod.rs @@ -2,6 +2,7 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::data_contract_update::state::v0::DataContractUpdateStateTransitionStateValidationV0; +use crate::execution::validation::state_transition::data_contract_update::state::v1::DataContractUpdateStateTransitionStateValidationV1; use crate::execution::validation::state_transition::processor::state::StateTransitionStateValidation; use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformRef; @@ -14,6 +15,7 @@ use drive::grovedb::TransactionArg; use drive::state_transition_action::StateTransitionAction; pub(crate) mod v0; +pub(crate) mod v1; impl StateTransitionStateValidation for DataContractUpdateTransition { fn validate_state( @@ -47,9 +49,22 @@ impl StateTransitionStateValidation for DataContractUpdateTransition { platform_version, ) } + 1 => { + if action.is_some() { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution("data contract update is calling validate state, and the action is already known. It should not be known at this point"))); + } + self.validate_state_v1( + platform, + block_info, + validation_mode, + execution_context, + tx, + platform_version, + ) + } version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "data contract update transition: validate_state".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v1/mod.rs new file mode 100644 index 00000000000..be5b3d42dc2 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v1/mod.rs @@ -0,0 +1,88 @@ +use dpp::block::block_info::BlockInfo; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use drive::state_transition_action::StateTransitionAction; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::data_contract_common::data_contract_reference_validation::validate_data_contract_references; +use crate::execution::validation::state_transition::state_transitions::data_contract_update::state::v0::DataContractUpdateStateTransitionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformRef; +use crate::rpc::core::CoreRPCLike; + +pub(in crate::execution::validation::state_transition::state_transitions::data_contract_update) trait DataContractUpdateStateTransitionStateValidationV1 +{ + fn validate_state_v1( + &self, + platform: &PlatformRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl DataContractUpdateStateTransitionStateValidationV1 for DataContractUpdateTransition { + fn validate_state_v1( + &self, + platform: &PlatformRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let action = self.validate_state_v0::( + platform, + block_info, + validation_mode, + execution_context, + tx, + platform_version, + )?; + + if !action.is_valid() { + return Ok(action); + } + + // The updated contract may add document types or properties carrying + // reference declarations, so they are re-validated on every update + let reference_result = { + let StateTransitionAction::DataContractUpdateAction(update_action) = + action.data_as_borrowed()? + else { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "a valid data contract update state validation must contain an update action", + ))); + }; + + validate_data_contract_references( + update_action.data_contract_ref(), + platform.drive, + block_info, + execution_context, + tx, + platform_version, + )? + }; + + if !reference_result.is_valid() { + return Ok(ConsensusValidationResult::new_with_data_and_errors( + StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_data_contract_update_transition( + self, + ), + ), + reference_result.errors, + )); + } + + Ok(action) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs index 91b3532908a..31b3596bc4f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs @@ -16,6 +16,9 @@ pub mod identity_top_up; /// Module for updating an existing identity entity. pub mod identity_update; +/// Validation shared by the data contract create and update transitions. +pub mod data_contract_common; + /// Module for creating a data contract entity. pub mod data_contract_create; diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-foreign.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-foreign.json new file mode 100644 index 00000000000..f8a9e02b623 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-foreign.json @@ -0,0 +1,22 @@ +{ + "$formatVersion": "1", + "id": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "note": { + "type": "object", + "canBeDeleted": false, + "properties": { + "content": { + "type": "string", + "position": 0, + "maxLength": 100 + } + }, + "required": [], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-deletable.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-deletable.json new file mode 100644 index 00000000000..f1d69dca9f5 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-deletable.json @@ -0,0 +1,41 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "deletableNote": { + "type": "object", + "canBeDeleted": true, + "properties": { + "content": { + "type": "string", + "position": 0, + "maxLength": 100 + } + }, + "required": [], + "additionalProperties": false + }, + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "noteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "documentType": "deletableNote" + } + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-missing-contract.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-missing-contract.json new file mode 100644 index 00000000000..a686f155f9f --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-missing-contract.json @@ -0,0 +1,29 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "noteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "contractId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "documentType": "note" + } + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-unknown-type.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-unknown-type.json new file mode 100644 index 00000000000..3cd26c43f28 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-unknown-type.json @@ -0,0 +1,41 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "note": { + "type": "object", + "canBeDeleted": false, + "properties": { + "content": { + "type": "string", + "position": 0, + "maxLength": 100 + } + }, + "required": [], + "additionalProperties": false + }, + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "noteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "documentType": "ghost" + } + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-bad.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-bad.json new file mode 100644 index 00000000000..668bdce4543 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-bad.json @@ -0,0 +1,79 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "note": { + "type": "object", + "canBeDeleted": false, + "properties": { + "content": { + "type": "string", + "position": 0, + "maxLength": 100 + } + }, + "required": [], + "additionalProperties": false + }, + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "noteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "documentType": "note" + } + }, + "crossContractNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 1, + "refersTo": { + "type": "permanentDocument", + "contractId": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", + "documentType": "note" + } + }, + "ghostNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 3, + "refersTo": { + "type": "permanentDocument", + "documentType": "ghost" + } + }, + "secondCrossContractNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 2, + "refersTo": { + "type": "permanentDocument", + "contractId": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", + "documentType": "note" + } + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-good.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-good.json new file mode 100644 index 00000000000..cef122385fd --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-good.json @@ -0,0 +1,79 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "note": { + "type": "object", + "canBeDeleted": false, + "properties": { + "content": { + "type": "string", + "position": 0, + "maxLength": 100 + } + }, + "required": [], + "additionalProperties": false + }, + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "noteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "documentType": "note" + } + }, + "crossContractNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 1, + "refersTo": { + "type": "permanentDocument", + "contractId": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", + "documentType": "note" + } + }, + "secondNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 3, + "refersTo": { + "type": "permanentDocument", + "documentType": "note" + } + }, + "secondCrossContractNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 2, + "refersTo": { + "type": "permanentDocument", + "contractId": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", + "documentType": "note" + } + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-valid.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-valid.json new file mode 100644 index 00000000000..dcd7933b80e --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-valid.json @@ -0,0 +1,67 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "note": { + "type": "object", + "canBeDeleted": false, + "properties": { + "content": { + "type": "string", + "position": 0, + "maxLength": 100 + } + }, + "required": [], + "additionalProperties": false + }, + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "noteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "documentType": "note" + } + }, + "crossContractNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 1, + "refersTo": { + "type": "permanentDocument", + "contractId": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", + "documentType": "note" + } + }, + "secondCrossContractNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 2, + "refersTo": { + "type": "permanentDocument", + "contractId": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", + "documentType": "note" + } + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc.json new file mode 100644 index 00000000000..9eab77cbe0a --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc.json @@ -0,0 +1,114 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "note": { + "type": "object", + "canBeDeleted": false, + "properties": { + "content": { + "type": "string", + "position": 0, + "maxLength": 100 + } + }, + "required": [], + "indices": [], + "additionalProperties": false + }, + "deletableNote": { + "type": "object", + "canBeDeleted": true, + "properties": { + "content": { + "type": "string", + "position": 0, + "maxLength": 100 + } + }, + "required": [], + "indices": [], + "additionalProperties": false + }, + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "noteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "permanentDocument", + "documentType": "note" + } + }, + "deletableNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 1, + "refersTo": { + "type": "permanentDocument", + "contractId": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "documentType": "deletableNote" + } + }, + "unknownTypeNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 2, + "refersTo": { + "type": "permanentDocument", + "contractId": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "documentType": "ghost" + } + }, + "foreignNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 3, + "refersTo": { + "type": "permanentDocument", + "contractId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "documentType": "note" + } + }, + "crossContractNoteId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 4, + "refersTo": { + "type": "permanentDocument", + "contractId": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", + "documentType": "note" + } + }, + "note": { + "type": "string", + "position": 5, + "maxLength": 64 + } + }, + "required": [], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 57405878d69..b80eb387299 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -115,6 +115,10 @@ pub struct DriveAbciStateTransitionValidationVersions { pub masternode_vote_state_transition_balance_pre_check: FeatureVersion, pub contract_create_state_transition: DriveAbciStateTransitionValidationVersion, pub contract_update_state_transition: DriveAbciStateTransitionValidationVersion, + /// Validation of the `refersTo` reference declarations a contract's + /// document types carry, run at contract create and update. Only + /// reachable from contract create/update state validation 1 and above. + pub data_contract_reference_validation: FeatureVersion, pub batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions, pub identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion, pub identity_top_up_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index 241823540b6..6358f0f9d08 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -100,6 +100,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index 08a99d7c2fa..9195558adb4 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -99,7 +99,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = advanced_structure: Some(1), identity_signatures: None, nonce: Some(0), - state: 0, + state: 1, // changed: runs data_contract_reference_validation on the created contract's refersTo declarations transform_into_action: 0, }, contract_update_state_transition: DriveAbciStateTransitionValidationVersion { @@ -107,9 +107,10 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = advanced_structure: None, identity_signatures: None, nonce: Some(0), - state: 0, + state: 1, // changed: runs data_contract_reference_validation on the updated contract's refersTo declarations transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index 58332887853..2bfc9d718ee 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -100,6 +100,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 1579c41caca..7b1f614673e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -100,6 +100,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index e1c030a65c8..0e97b7cc9c0 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -103,6 +103,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index cc45526c0d4..d8af07df7eb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -104,6 +104,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 80c2fb7091e..6f663a3e5e2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -107,6 +107,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 952e6063fc7..e47f2a297e9 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -101,6 +101,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index fe3cd4b6bb0..b7563920b2a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -115,6 +115,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index d278784555c..a652b10d6cc 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -108,6 +108,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, advanced_structure: 0, diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 85aaf45d489..91ff55de7e0 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -92,6 +92,8 @@ use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized 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, InvalidTokenPositionStateError}; use dpp::consensus::state::address_funds::{AddressDoesNotExistError, AddressInvalidNonceError, AddressNotEnoughFundsError, AddressesNotEnoughFundsError}; +use dpp::consensus::state::document::referenced_document_type_deletable_error::ReferencedDocumentTypeDeletableError; +use dpp::consensus::state::document::referenced_document_type_not_found_error::ReferencedDocumentTypeNotFoundError; use dpp::consensus::state::shielded::insufficient_pool_notes_error::InsufficientPoolNotesError; use dpp::consensus::state::shielded::insufficient_shielded_fee_error::InsufficientShieldedFeeError; use dpp::consensus::state::shielded::invalid_anchor_error::InvalidAnchorError; @@ -475,6 +477,12 @@ pub fn from_state_error(state_error: &StateError) -> JsValue { StateError::ReferencedEntityNotFoundError(e) => { ReferencedEntityNotFoundErrorWasm::from(e).into() } + StateError::ReferencedDocumentTypeNotFoundError(e) => { + generic_consensus_error!(ReferencedDocumentTypeNotFoundError, e).into() + } + StateError::ReferencedDocumentTypeDeletableError(e) => { + generic_consensus_error!(ReferencedDocumentTypeDeletableError, e).into() + } } }