From 3ec51e4c450675343ace7f4421fbe55e3a6a6ba2 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 07:08:41 +0700 Subject: [PATCH 1/6] feat(platform)!: permanent document references (refersTo permanentDocument) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends refersTo with a permanentDocument target: an identifier property may reference a document by declaring the contract id and document type in the schema, with the property value carrying the document id. 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 that validated once can never dangle. The declaration is folded into DocumentPropertyReferenceTarget::PermanentDocument (appended, the enum is consensus-serialized inside errors). Meta-schema v3 gains the target with contractId (base58 string or 32-byte array) and documentType, required for permanentDocument and forbidden for the other targets. Write-time validation resolves the referenced contract (billed; the declaring contract may reference itself at no extra cost), requires the document type to exist and forbid deletion, then performs a billed existence check on the referenced document via fetch_document_with_id. New state errors: ReferencedDocumentTypeNotFoundError (40121, also covers a missing referenced contract) and ReferencedDocumentTypeDeletableError (40122). Co-Authored-By: Claude Fable 5 --- .../document/v3/document-meta.json | 46 +++- .../class_methods/try_from_schema/mod.rs | 126 +++++++++ .../src/data_contract/document_type/mod.rs | 2 + .../document_type/property/mod.rs | 19 ++ packages/rs-dpp/src/errors/consensus/codes.rs | 2 + .../errors/consensus/state/document/mod.rs | 2 + ...eferenced_document_type_deletable_error.rs | 51 ++++ ...eferenced_document_type_not_found_error.rs | 51 ++++ .../src/errors/consensus/state/state_error.rs | 28 ++ .../src/validation/meta_validators/mod.rs | 85 ++++++ .../document_reference_validation/v0/mod.rs | 90 ++++++ .../batch/tests/document/creation.rs | 257 ++++++++++++++++++ ...nce-validation-contract-permanent-doc.json | 102 +++++++ .../src/errors/consensus/consensus_error.rs | 8 + 14 files changed, 867 insertions(+), 2 deletions(-) create mode 100644 packages/rs-dpp/src/errors/consensus/state/document/referenced_document_type_deletable_error.rs create mode 100644 packages/rs-dpp/src/errors/consensus/state/document/referenced_document_type_not_found_error.rs create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc.json 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..ab0bd0e67a5 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", + "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", "contractId", "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..9c147711827 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,33 @@ fn apply_property_reference_v0( "identity" => DocumentPropertyReferenceTarget::Identity, "contract" => DocumentPropertyReferenceTarget::Contract, "token" => DocumentPropertyReferenceTarget::Token, + "permanentDocument" => { + let contract_id = refers_to_map + .get(property_names::CONTRACT_ID) + .ok_or_else(|| { + DataContractError::InvalidContractStructure( + "permanentDocument refersTo requires a contractId".to_string(), + ) + })? + .to_identifier() + .map_err(|e| DataContractError::ValueWrongType(e.to_string()))?; + + 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 +396,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 +489,104 @@ 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, + document_type_name: "note".to_string(), + } + ) + ); + } + + #[test] + fn should_reject_permanent_document_refers_to_without_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", + "documentType": "note" + } + } + }, + "required": [], + "additionalProperties": false + })) + .expect_err("should fail"); + + let message = err.to_string(); + assert!( + message.contains("permanentDocument refersTo requires a contractId"), + "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..d101e49bd12 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,16 @@ 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 { + contract_id: Identifier, + document_type_name: String, + }, } impl std::fmt::Display for DocumentPropertyReferenceTarget { @@ -69,6 +81,13 @@ 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, + document_type_name, + } => write!( + f, + "permanent document (contract {contract_id}, document type {document_type_name})" + ), } } } 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..6e2ba8a3e18 100644 --- a/packages/rs-dpp/src/validation/meta_validators/mod.rs +++ b/packages/rs-dpp/src/validation/meta_validators/mod.rs @@ -339,6 +339,91 @@ 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_reject_permanent_document_refers_to_without_contract_id() { + let schema = document_schema_with_refers_to(json!({ + "type": "permanentDocument", + "documentType": "note" + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_err(), + "expected permanentDocument refersTo without contractId to be invalid" + ); + } + + #[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..4816f1c5337 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,90 @@ fn validate_document_type_references_v0( referenced_token_info.is_some() } + DocumentPropertyReferenceTarget::PermanentDocument { + contract_id: referenced_contract_id, + document_type_name, + } => { + // The declaring contract may reference its own document types; it is + // already loaded for this transition, so no fetch is billed for it + let referenced_contract_fetch_info; + let referenced_contract = if *referenced_contract_id == contract.id() { + contract + } else { + let (fee, fetch_info) = platform.drive.get_contract_with_fetch_info_and_fee( + referenced_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( + *referenced_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( + *referenced_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( + *referenced_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..96233ab1815 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,261 @@ mod creation_tests { } ); } + + const REFERENCE_VALIDATION_PERMANENT_DOC_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc.json"; + + /// Registers the permanent-document fixture contract, creates and commits a + /// `note` document (its type has `canBeDeleted: false`), then creates a + /// `message` document mutated by the test and returns that second + /// transition's execution result. The mutator receives the committed note's + /// id so the happy path can point a reference at a document known to exist. + async fn run_permanent_document_reference_creation( + mutator: F, + ) -> StateTransitionExecutionResult + where + F: FnOnce(&mut Document, 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 contract = setup_contract( + &platform.drive, + REFERENCE_VALIDATION_PERMANENT_DOC_CONTRACT_PATH, + None, + None, + None::, + None, + None, + ); + + let 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"); + + let note_id = note_document.id(); + + let note_create_transition = + BatchTransition::new_document_creation_transition_from_document( + note_document, + note, + note_entropy.0, + &key, + 2, + 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 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, note_id); + + let documents_batch_create_transition = + BatchTransition::new_document_creation_transition_from_document( + document, + message, + entropy.0, + &key, + 3, + 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, note_id| { + document.set("noteId", 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, note_id| { + document.set("deletableNoteId", 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, note_id| { + document.set("unknownTypeNoteId", 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, note_id| { + document.set("foreignNoteId", note_id.into()); + }) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedDocumentTypeNotFoundError( + _ + )), + .. + } + ); + } } 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..8384f8d44f1 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc.json @@ -0,0 +1,102 @@ +{ + "$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", + "contractId": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "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" + } + }, + "note": { + "type": "string", + "position": 4, + "maxLength": 64 + } + }, + "required": [], + "indices": [], + "additionalProperties": false + } + } +} 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() + } } } From 8db24d2cc95587b1ab832617bb4ab74b89cd4a9f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 07:33:41 +0700 Subject: [PATCH 2/6] test(dpp): cover reference target display and permanentDocument parse error branches Co-Authored-By: Claude Fable 5 --- .../class_methods/try_from_schema/mod.rs | 59 +++++++++++++++++++ .../document_type/property/mod.rs | 23 ++++++++ 2 files changed, 82 insertions(+) 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 9c147711827..bf76a09742b 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 @@ -563,6 +563,65 @@ mod tests { ); } + #[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!({ 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 d101e49bd12..10acc8f9c87 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 @@ -7153,4 +7153,27 @@ 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, + document_type_name: "note".to_string(), + } + .to_string(), + format!("permanent document (contract {contract_id}, document type note)") + ); + } } From e46fd74c692b90a2e22f6fe50773cf09c06e675c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 09:18:31 +0700 Subject: [PATCH 3/6] test(drive-abci): cover successful cross-contract permanent document reference A second fixture contract with its own non-deletable note type is registered alongside the declaring contract; the new test references a committed document in it, exercising foreign contract resolution, the foreign document type lookup and the billed document existence check. Co-Authored-By: Claude Fable 5 --- .../batch/tests/document/creation.rs | 189 +++++++++++------- ...dation-contract-permanent-doc-foreign.json | 22 ++ ...nce-validation-contract-permanent-doc.json | 15 +- 3 files changed, 153 insertions(+), 73 deletions(-) create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-foreign.json 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 96233ab1815..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 @@ -5010,17 +5010,26 @@ 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, creates and commits a - /// `note` document (its type has `canBeDeleted: false`), then creates a - /// `message` document mutated by the test and returns that second - /// transition's execution result. The mutator receives the committed note's - /// id so the happy path can point a reference at a document known to exist. + /// 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, Identifier), + F: FnOnce(&mut Document, &PermanentReferenceTargets), { let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() @@ -5044,71 +5053,90 @@ mod creation_tests { None, ); - let note = contract - .document_type_for_name("note") - .expect("expected a note document type"); + let foreign_contract = setup_contract( + &platform.drive, + REFERENCE_VALIDATION_PERMANENT_DOC_FOREIGN_CONTRACT_PATH, + None, + None, + None::, + None, + None, + ); - let note_entropy = Bytes32::random_with_rng(&mut rng); + let mut note_ids = Vec::new(); - 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"); + 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_id = note_document.id(); + let note_entropy = Bytes32::random_with_rng(&mut rng); - let note_create_transition = - BatchTransition::new_document_creation_transition_from_document( - note_document, - note, - note_entropy.0, - &key, - 2, - 0, - None, - &signer, - platform_version, - None, - ) - .await - .expect("expect to create note batch transition"); + 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"); - let note_create_serialized_transition = note_create_transition - .serialize_to_bytes() - .expect("expected note batch serialized state transition"); + note_ids.push(note_document.id()); - let transaction = platform.drive.grove.start_transaction(); + 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 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"); + let note_create_serialized_transition = note_create_transition + .serialize_to_bytes() + .expect("expected note batch serialized state transition"); - assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); + let transaction = platform.drive.grove.start_transaction(); - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit 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") @@ -5129,7 +5157,7 @@ mod creation_tests { ) .expect("expected a random message document"); - mutator(&mut document, note_id); + mutator(&mut document, &targets); let documents_batch_create_transition = BatchTransition::new_document_creation_transition_from_document( @@ -5137,7 +5165,7 @@ mod creation_tests { message, entropy.0, &key, - 3, + 4, 0, None, &signer, @@ -5182,8 +5210,25 @@ mod creation_tests { #[tokio::test] async fn should_document_creation_succeed_when_referenced_permanent_document_exists() { - let result = run_permanent_document_reference_creation(|document, note_id| { - document.set("noteId", note_id.into()); + 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; @@ -5213,8 +5258,8 @@ mod creation_tests { 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, note_id| { - document.set("deletableNoteId", note_id.into()); + let result = run_permanent_document_reference_creation(|document, targets| { + document.set("deletableNoteId", targets.note_id.into()); }) .await; @@ -5231,8 +5276,8 @@ mod creation_tests { #[tokio::test] async fn should_document_creation_fail_when_referenced_document_type_missing() { - let result = run_permanent_document_reference_creation(|document, note_id| { - document.set("unknownTypeNoteId", note_id.into()); + let result = run_permanent_document_reference_creation(|document, targets| { + document.set("unknownTypeNoteId", targets.note_id.into()); }) .await; @@ -5249,8 +5294,8 @@ mod creation_tests { #[tokio::test] async fn should_document_creation_fail_when_referenced_document_contract_missing() { - let result = run_permanent_document_reference_creation(|document, note_id| { - document.set("foreignNoteId", note_id.into()); + let result = run_permanent_document_reference_creation(|document, targets| { + document.set("foreignNoteId", targets.note_id.into()); }) .await; 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.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc.json index 8384f8d44f1..c3d940bdbda 100644 --- 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 @@ -88,9 +88,22 @@ "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": 4, + "position": 5, "maxLength": 64 } }, From f5ad6e1d5f68b0bd1ddc2519f1afa4f097cfc711 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 15:00:00 +0700 Subject: [PATCH 4/6] feat(platform)!: optional contractId and contract-registration validation for permanentDocument references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An absent contractId in a permanentDocument refersTo declaration now means the reference targets the declaring contract itself (DocumentPropertyReferenceTarget::PermanentDocument carries an Option); documentType remains required. This also makes self references usable on contract creation, where the final contract id is derived from the owner and nonce and can not be named in the schema. Reference declarations are now validated when the contract enters the state: a new versioned data_contract_reference_validation (v0) checks every permanentDocument declaration — the referenced contract must exist (the in-flight contract itself for self references, billed fetch otherwise), the referenced document type must exist and must forbid deletion. It is called from new data_contract_create state v1 and data_contract_update state v1 modules that delegate to v0 and layer the check, selected by DRIVE_ABCI_VALIDATION_VERSIONS_V10 (unreleased PV14) bumping both contract state validations to 1. Document-write-time validation keeps performing the same declaration checks defensively before the billed document existence check. Co-Authored-By: Claude Fable 5 --- .../document/v3/document-meta.json | 4 +- .../class_methods/try_from_schema/mod.rs | 42 +++-- .../document_type/property/mod.rs | 23 ++- .../src/validation/meta_validators/mod.rs | 7 +- .../document_reference_validation/v0/mod.rs | 17 +- .../data_contract_reference_validation/mod.rs | 50 ++++++ .../v0/mod.rs | 117 ++++++++++++ .../data_contract_common/mod.rs | 2 + .../data_contract_create/mod.rs | 170 +++++++++++++++++- .../data_contract_create/state/mod.rs | 1 + .../data_contract_create/state/v1/mod.rs | 84 +++++++++ .../data_contract_update/mod.rs | 144 +++++++++++++++ .../data_contract_update/state/mod.rs | 17 +- .../data_contract_update/state/v1/mod.rs | 88 +++++++++ .../state_transition/state_transitions/mod.rs | 3 + ...-permanent-doc-registration-deletable.json | 41 +++++ ...ent-doc-registration-missing-contract.json | 29 +++ ...rmanent-doc-registration-unknown-type.json | 41 +++++ ...permanent-doc-registration-update-bad.json | 66 +++++++ ...ermanent-doc-registration-update-good.json | 66 +++++++ ...ract-permanent-doc-registration-valid.json | 54 ++++++ ...nce-validation-contract-permanent-doc.json | 1 - .../drive_abci_validation_versions/mod.rs | 4 + .../drive_abci_validation_versions/v1.rs | 1 + .../drive_abci_validation_versions/v10.rs | 5 +- .../drive_abci_validation_versions/v2.rs | 1 + .../drive_abci_validation_versions/v3.rs | 1 + .../drive_abci_validation_versions/v4.rs | 1 + .../drive_abci_validation_versions/v5.rs | 1 + .../drive_abci_validation_versions/v6.rs | 1 + .../drive_abci_validation_versions/v7.rs | 1 + .../drive_abci_validation_versions/v8.rs | 1 + .../drive_abci_validation_versions/v9.rs | 1 + 33 files changed, 1050 insertions(+), 35 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v1/mod.rs create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-deletable.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-missing-contract.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-unknown-type.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-bad.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-update-good.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-valid.json 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 ab0bd0e67a5..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 @@ -103,7 +103,7 @@ ] }, "contractId": { - "description": "The id of the data contract the referenced document lives in, as a base58 string or a 32-byte array", + "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", @@ -140,7 +140,7 @@ "required": ["type"] }, "then": { - "required": ["type", "contractId", "documentType"] + "required": ["type", "documentType"] }, "else": { "properties": { 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 bf76a09742b..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 @@ -355,15 +355,16 @@ fn apply_property_reference_v0( "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) - .ok_or_else(|| { - DataContractError::InvalidContractStructure( - "permanentDocument refersTo requires a contractId".to_string(), - ) - })? - .to_identifier() - .map_err(|e| DataContractError::ValueWrongType(e.to_string()))?; + .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) @@ -526,7 +527,7 @@ mod tests { property_type, DocumentPropertyType::IdentifierWithReference( DocumentPropertyReferenceTarget::PermanentDocument { - contract_id, + contract_id: Some(contract_id), document_type_name: "note".to_string(), } ) @@ -534,8 +535,8 @@ mod tests { } #[test] - fn should_reject_permanent_document_refers_to_without_contract_id() { - let err = try_document_type_from_schema(json!({ + 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": { @@ -554,12 +555,23 @@ mod tests { "required": [], "additionalProperties": false })) - .expect_err("should fail"); + .expect("should parse"); - let message = err.to_string(); - assert!( - message.contains("permanentDocument refersTo requires a contractId"), - "unexpected error: {message}" + 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(), + } + ) ); } 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 10acc8f9c87..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 @@ -70,7 +70,9 @@ pub enum DocumentPropertyReferenceTarget { /// guarantees a validated reference can never dangle. #[serde(rename = "permanentDocument")] PermanentDocument { - contract_id: Identifier, + /// The contract the referenced document type lives in; `None` means + /// the declaring contract itself + contract_id: Option, document_type_name: String, }, } @@ -82,12 +84,19 @@ impl std::fmt::Display for DocumentPropertyReferenceTarget { DocumentPropertyReferenceTarget::Contract => write!(f, "contract"), DocumentPropertyReferenceTarget::Token => write!(f, "token"), DocumentPropertyReferenceTarget::PermanentDocument { - contract_id, + 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})" + ), } } } @@ -7169,11 +7178,19 @@ mod tests { assert_eq!(DocumentPropertyReferenceTarget::Token.to_string(), "token"); assert_eq!( DocumentPropertyReferenceTarget::PermanentDocument { - contract_id, + 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/validation/meta_validators/mod.rs b/packages/rs-dpp/src/validation/meta_validators/mod.rs index 6e2ba8a3e18..d05da98b1e4 100644 --- a/packages/rs-dpp/src/validation/meta_validators/mod.rs +++ b/packages/rs-dpp/src/validation/meta_validators/mod.rs @@ -368,15 +368,16 @@ mod tests { } #[test] - fn should_reject_permanent_document_refers_to_without_contract_id() { + 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_err(), - "expected permanentDocument refersTo without contractId to be invalid" + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_ok(), + "expected permanentDocument refersTo without contractId to be valid" ); } 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 4816f1c5337..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 @@ -175,14 +175,17 @@ fn validate_document_type_references_v0( contract_id: referenced_contract_id, document_type_name, } => { - // The declaring contract may reference its own document types; it is - // already loaded for this transition, so no fetch is billed for it + // 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 *referenced_contract_id == contract.id() { + let referenced_contract = if effective_contract_id == contract.id() { contract } else { let (fee, fetch_info) = platform.drive.get_contract_with_fetch_info_and_fee( - referenced_contract_id.to_buffer(), + effective_contract_id.to_buffer(), Some(&block_info.epoch), false, transaction, @@ -203,7 +206,7 @@ fn validate_document_type_references_v0( // same failure: the declared document type could not be found return Ok(SimpleConsensusValidationResult::new_with_error( ReferencedDocumentTypeNotFoundError::new( - *referenced_contract_id, + effective_contract_id, document_type_name.clone(), path.to_string(), ) @@ -220,7 +223,7 @@ fn validate_document_type_references_v0( else { return Ok(SimpleConsensusValidationResult::new_with_error( ReferencedDocumentTypeNotFoundError::new( - *referenced_contract_id, + effective_contract_id, document_type_name.clone(), path.to_string(), ) @@ -235,7 +238,7 @@ fn validate_document_type_references_v0( if referenced_document_type.documents_can_be_deleted() { return Ok(SimpleConsensusValidationResult::new_with_error( ReferencedDocumentTypeDeletableError::new( - *referenced_contract_id, + effective_contract_id, document_type_name.clone(), path.to_string(), ) 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..e2d7eb33f3a --- /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,117 @@ +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::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::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`. +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 { + 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 (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 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(), + 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-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..9cd2a56591d --- /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,66 @@ +{ + "$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": 2, + "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-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..ce04e546f8d --- /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,66 @@ +{ + "$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": 2, + "refersTo": { + "type": "permanentDocument", + "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..56cdc6d1e23 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-permanent-doc-registration-valid.json @@ -0,0 +1,54 @@ +{ + "$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" + } + } + }, + "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 index c3d940bdbda..9eab77cbe0a 100644 --- 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 @@ -45,7 +45,6 @@ "position": 0, "refersTo": { "type": "permanentDocument", - "contractId": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", "documentType": "note" } }, 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, From 645bb12beed89901c9a804ac4f3dbda57573486c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 15:14:20 +0700 Subject: [PATCH 5/6] perf(drive-abci): memoize foreign contract resolution in reference declaration validation Repeated permanentDocument declarations naming the same foreign contract are now billed a single fetch (misses memoized too), and the doc comment states that stopping at the first invalid declaration is intentional. The valid registration fixture gains a second reference into the same foreign contract to exercise the memoized path. Co-Authored-By: Claude Fable 5 --- .../v0/mod.rs | 53 ++++++++++++++----- ...permanent-doc-registration-update-bad.json | 15 +++++- ...ermanent-doc-registration-update-good.json | 13 +++++ ...ract-permanent-doc-registration-valid.json | 13 +++++ 4 files changed, 79 insertions(+), 15 deletions(-) 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 index e2d7eb33f3a..a7acae81ae9 100644 --- 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 @@ -5,10 +5,14 @@ use dpp::data_contract::document_type::{DocumentPropertyReferenceTarget, Documen 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; @@ -26,7 +30,11 @@ use crate::execution::types::state_transition_execution_context::{ /// fetches are billed. /// /// The error paths name the failing declaration as -/// `documentTypeName.propertyPath`. +/// `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, @@ -35,6 +43,11 @@ pub(super) fn validate_data_contract_references_v0( 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( @@ -55,22 +68,34 @@ pub(super) fn validate_data_contract_references_v0( let referenced_contract = if effective_contract_id == contract.id() { contract } else { - 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 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", + )))?; - 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)); - // The cost is added even if the referenced contract does not exist or was cached - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + fetched_contracts.insert(effective_contract_id, fetch_info.clone()); + + fetch_info + } + }; - let Some(fetch_info) = fetch_info else { + 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( 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 index 9cd2a56591d..668bdce4543 100644 --- 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 @@ -52,11 +52,24 @@ "minItems": 32, "maxItems": 32, "contentMediaType": "application/x.dash.dpp.identifier", - "position": 2, + "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": [], 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 index ce04e546f8d..cef122385fd 100644 --- 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 @@ -47,6 +47,18 @@ } }, "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, @@ -55,6 +67,7 @@ "position": 2, "refersTo": { "type": "permanentDocument", + "contractId": "YMN9Qj5jPNp7j14VPcML1B6xGgcPWVZUGLFU3Mnyfaf", "documentType": "note" } } 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 index 56cdc6d1e23..dcd7933b80e 100644 --- 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 @@ -45,6 +45,19 @@ "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": [], From 8f140f5029ddbf6986c35841dc892efa021f4f09 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 15:18:34 +0700 Subject: [PATCH 6/6] docs(drive-abci): disambiguate the billed-fetch comment from the local memoization Co-Authored-By: Claude Fable 5 --- .../data_contract_reference_validation/v0/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index a7acae81ae9..1d319d85318 100644 --- 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 @@ -85,7 +85,8 @@ pub(super) fn validate_data_contract_references_v0( )))?; // The cost is added even if the referenced contract does not exist - // or was cached + // or was served from Drive's own contract cache; only locally + // memoized repeats above skip it execution_context .add_operation(ValidationOperation::PrecalculatedOperation(fee));