From 73ec7acd3ec350e3ea8b9ccad2daaa6b9c5353bb Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 02:27:25 +0700 Subject: [PATCH 1/4] feat(platform)!: reference validation for documents (refersTo) Implements the refersTo document property keyword: data contracts can declare that an identifier property references an identity, a contract, or a token, and document create/replace state validation rejects transitions whose referenced entity does not exist on Platform. All three targets are permanent once created, so an existence check at write time holds forever. Replace transitions only validate references on changed fields, and each existence check is billed through the execution context (identity revision fetch, contract fetch with fee, token contract info fetch with cost). The reference lives in the property type itself: DocumentPropertyType::Identifier(Option), mirroring how String and ByteArray carry their metadata in-variant and making a reference on a non-identifier property unrepresentable. The parsed document type structs are never consensus-serialized (contracts store raw document schemas and re-derive types on load), so reshaping the variant is wire-invisible. Ported from the original v3.0-era branch (PR #2993) onto v4.2-dev: * Gated by the in-development protocol version 14 instead of a new protocol version: DRIVE_ABCI_VALIDATION_VERSIONS_V10 (new in the unreleased v14) is amended to bump document create state validation to 2 and replace to 1, and to introduce the document_reference_validation feature version. * refersTo is admitted only by the v3 document meta-schema (the PV14 generation), so contracts from earlier generations can never carry it; the meta-schema also constrains refersTo to identifier-shaped properties via dependentSchemas. * The original branch's mustExist flag was dropped: refersTo present always means the referent must exist, and its non-enforcing variant can be reintroduced compatibly later if ever needed. * ReferencedEntityNotFoundError is a state error only (the original branch also added an unused BasicError variant) with code 40120, appended per the frozen-discriminant rules. * JSON-schema compatibility rules reject adding, removing, or modifying refersTo on contract updates. * wasm-dpp exposes the new consensus error. Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Co-Authored-By: Claude Fable 5 --- .../document/v3/document-meta.json | 45 +- .../class_methods/try_from_schema/mod.rs | 132 +++++- .../methods/validate_update/common/mod.rs | 98 ++++ .../src/data_contract/document_type/mod.rs | 1 + .../document_type/property/mod.rs | 124 +++-- .../v0/mod.rs | 2 +- .../document_type/v0/random_document_type.rs | 2 +- packages/rs-dpp/src/errors/consensus/codes.rs | 1 + .../errors/consensus/state/document/mod.rs | 1 + .../referenced_entity_not_found_error.rs | 56 +++ .../src/errors/consensus/state/state_error.rs | 14 + .../src/validation/meta_validators/mod.rs | 97 ++++ .../document_create_transition_action/mod.rs | 13 +- .../state_v2/mod.rs | 66 +++ .../document_reference_validation/mod.rs | 70 +++ .../document_reference_validation/v0/mod.rs | 199 ++++++++ .../document_replace_transition_action/mod.rs | 14 +- .../state_v1/mod.rs | 66 +++ .../batch/action_validation/document/mod.rs | 1 + .../batch/tests/document/creation.rs | 323 +++++++++++++ .../batch/tests/document/replacement.rs | 424 ++++++++++++++++++ ...ence-validation-contract-contract-ref.json | 35 ++ .../reference-validation-contract-nested.json | 64 +++ ...eference-validation-contract-optional.json | 35 ++ ...ference-validation-contract-token-ref.json | 35 ++ .../reference-validation-contract.json | 35 ++ packages/rs-drive/src/query/conditions.rs | 12 +- .../src/rules/rule_set.rs | 39 ++ .../tests/rules.rs | 47 ++ .../drive_abci_validation_versions/mod.rs | 1 + .../drive_abci_validation_versions/v1.rs | 1 + .../drive_abci_validation_versions/v10.rs | 11 +- .../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 + .../rs-platform-version/src/version/v14.rs | 11 +- .../src/errors/consensus/consensus_error.rs | 4 + .../errors/consensus/state/document/mod.rs | 2 + .../referenced_entity_not_found_error.rs | 44 ++ 44 files changed, 2077 insertions(+), 56 deletions(-) create mode 100644 packages/rs-dpp/src/errors/consensus/state/document/referenced_entity_not_found_error.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/state_v1/mod.rs create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-contract-ref.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-nested.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-optional.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-token-ref.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract.json create mode 100644 packages/wasm-dpp/src/errors/consensus/state/document/referenced_entity_not_found_error.rs 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 b52d3ffc160..baefe8f7f47 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 @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json", - "$comment": "EDITABLE UNTIL THE RELEASE CARRYING PROTOCOL V14 SHIPS — FROZEN AFTER. This v3 document meta-schema activates with protocol v14 (CONTRACT_VERSIONS_V6). It is v2 plus the ranked index keywords (rankedCountable, rankedSummable, rankedAverageable), and admits every v14+ contract written to disk. v2 stays in place for protocol v13, where those keys still fail an index entry's `additionalProperties: false`. Once the release carrying protocol v14 ships, mutating it would change historical validation results and break consensus replay. After release, any new top-level property or rule MUST go in a newer meta-schema version (v4+). The $id above deliberately still names the v1 path: v1, v2 and v3 all share that identity, and it is the exact string `enrich_with_base_schema` injects as every PV12+ document schema's `$schema`, so bumping it here would be a wire-visible change rather than a documentation fix.", + "$comment": "EDITABLE UNTIL THE RELEASE CARRYING PROTOCOL V14 SHIPS — FROZEN AFTER. This v3 document meta-schema activates with protocol v14 (CONTRACT_VERSIONS_V6). It is v2 plus the ranked index keywords (rankedCountable, rankedSummable, rankedAverageable) and the refersTo reference keyword on identifier properties, and admits every v14+ contract written to disk. v2 stays in place for protocol v13, where those keys still fail an index entry's `additionalProperties: false`. Once the release carrying protocol v14 ships, mutating it would change historical validation results and break consensus replay. After release, any new top-level property or rule MUST go in a newer meta-schema version (v4+). The $id above deliberately still names the v1 path: v1, v2 and v3 all share that identity, and it is the exact string `enrich_with_base_schema` injects as every PV12+ document schema's `$schema`, so bumping it here would be a wire-visible change rather than a documentation fix.", "type": "object", "$defs": { "documentProperties": { @@ -91,6 +91,22 @@ "uniqueItems": { "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/uniqueItems" }, + "refersTo": { + "type": "object", + "properties": { + "type": { + "enum": [ + "identity", + "contract", + "token" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, "contains": { "$ref": "https://json-schema.org/draft/2020-12/meta/applicator#/properties/contains" }, @@ -192,6 +208,33 @@ "maxLength" ] }, + "refersTo": { + "description": "refersTo is only allowed on identifier properties", + "properties": { + "type": { + "const": "array" + }, + "byteArray": { + "const": true + }, + "contentMediaType": { + "const": "application/x.dash.dpp.identifier" + }, + "minItems": { + "const": 32 + }, + "maxItems": { + "const": 32 + } + }, + "required": [ + "type", + "byteArray", + "contentMediaType", + "minItems", + "maxItems" + ] + }, "format": { "description": "prevent slow format validation of large strings", "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 3bbc576e687..c78e4e1c7cd 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 @@ -2,7 +2,8 @@ use crate::data_contract::config::DataContractConfig; use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::v1::DocumentTypeV1; use crate::data_contract::document_type::{ - property_names, DocumentProperty, DocumentPropertyType, DocumentType, + property_names, DocumentProperty, DocumentPropertyReferenceTarget, DocumentPropertyType, + DocumentType, }; use crate::data_contract::errors::DataContractError; use crate::data_contract::{TokenConfiguration, TokenContractPosition}; @@ -169,6 +170,7 @@ fn insert_values( } } property_type => { + let property_type = apply_property_reference(&inner_properties, property_type)?; document_properties.insert( prefixed_property_key, DocumentProperty { @@ -278,6 +280,8 @@ fn insert_values_nested( property_type => property_type, }; + let property_type = apply_property_reference(&inner_properties, property_type)?; + document_properties.insert( property_key, DocumentProperty { @@ -289,3 +293,129 @@ fn insert_values_nested( Ok(()) } + +/// Folds a `refersTo` declaration into the property type: an identifier property +/// with `refersTo` becomes `Identifier(Some(target))`. Non-identifier properties +/// cannot carry `refersTo`. +fn apply_property_reference( + inner_properties: &BTreeMap, + property_type: DocumentPropertyType, +) -> Result { + let Some(refers_to_value) = inner_properties.get(property_names::REFERS_TO) else { + return Ok(property_type); + }; + + if !matches!(property_type, DocumentPropertyType::Identifier(_)) { + return Err(DataContractError::InvalidContractStructure( + "refersTo is only allowed on identifier properties".to_string(), + )); + } + + let refers_to_map = refers_to_value.to_btree_ref_string_map()?; + + let target = match refers_to_map + .get_str(property_names::TYPE) + .map_err(|e| DataContractError::ValueWrongType(e.to_string()))? + { + "identity" => DocumentPropertyReferenceTarget::Identity, + "contract" => DocumentPropertyReferenceTarget::Contract, + "token" => DocumentPropertyReferenceTarget::Token, + other => { + return Err(DataContractError::InvalidContractStructure(format!( + "invalid refersTo type {other}" + ))) + } + }; + + Ok(DocumentPropertyType::Identifier(Some(target))) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::config::DataContractConfig; + use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; + use serde_json::json; + + fn try_document_type_from_schema( + schema: serde_json::Value, + ) -> Result { + let platform_version = PlatformVersion::latest(); + let config = + DataContractConfig::default_for_version(platform_version).expect("config should build"); + + let value = platform_value::to_value(schema).expect("schema should convert"); + + DocumentType::try_from_schema( + Identifier::random(), + 0, + config.version(), + "msg", + value, + None, + &BTreeMap::new(), + &config, + false, + &mut vec![], + platform_version, + ) + } + + #[test] + fn should_parse_refers_to_on_identifier_property() { + let document_type = try_document_type_from_schema(json!({ + "type": "object", + "properties": { + "toUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "identity" + } + } + }, + "required": [], + "additionalProperties": false + })) + .expect("should parse"); + + let property_type = document_type + .as_ref() + .flattened_properties() + .get("toUserId") + .map(|p| p.property_type.clone()) + .expect("property should be present"); + + assert!(matches!( + property_type, + DocumentPropertyType::Identifier(Some(DocumentPropertyReferenceTarget::Identity)) + )); + } + + #[test] + fn should_reject_refers_to_on_non_identifier_property() { + let err = try_document_type_from_schema(json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "position": 0, + "refersTo": { "type": "identity" } + } + }, + "required": [], + "additionalProperties": false + })) + .expect_err("should fail"); + + let message = err.to_string(); + assert!( + message.contains("refersTo is only allowed on identifier properties"), + "unexpected error: {message}" + ); + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs index 2117ce8dff1..cae38edefce 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs @@ -1570,6 +1570,104 @@ mod tests { )] if e.operation() == "replace" && e.property_path() == "/properties/test/type" ); } + + fn identifier_document_type( + refers_to: Option, + platform_version: &PlatformVersion, + ) -> DocumentType { + let mut to_user_id = platform_value!({ + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0 + }); + + if let Some(refers_to) = refers_to { + to_user_id + .insert("refersTo".to_string(), refers_to) + .expect("should insert refersTo"); + } + + let schema = platform_value!({ + "type": "object", + "properties": { + "toUserId": to_user_id + }, + "signatureSecurityLevelRequirement": 0, + "additionalProperties": false, + }); + + let config = DataContractConfig::default_for_version(platform_version) + .expect("should create a default config"); + + DocumentType::try_from_schema( + Identifier::random(), + 1, + config.version(), + "test", + schema, + None, + &BTreeMap::new(), + &config, + false, + &mut Vec::new(), + platform_version, + ) + .expect("failed to create document type") + } + + #[test] + fn should_return_invalid_result_when_refers_to_is_added() { + let platform_version = PlatformVersion::latest(); + + let old_document_type = identifier_document_type(None, platform_version); + let new_document_type = identifier_document_type( + Some(platform_value!({ "type": "identity" })), + platform_version, + ); + + let result = old_document_type + .as_ref() + .validate_schema(new_document_type.as_ref(), platform_version) + .expect("failed to validate schema compatibility"); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::IncompatibleDocumentTypeSchemaError(e) + )] if e.operation() == "add" + && e.property_path() == "/properties/toUserId/refersTo" + ); + } + + #[test] + fn should_return_invalid_result_when_refers_to_is_modified() { + let platform_version = PlatformVersion::latest(); + + let old_document_type = identifier_document_type( + Some(platform_value!({ "type": "identity" })), + platform_version, + ); + let new_document_type = identifier_document_type( + Some(platform_value!({ "type": "contract" })), + platform_version, + ); + + let result = old_document_type + .as_ref() + .validate_schema(new_document_type.as_ref(), platform_version) + .expect("failed to validate schema compatibility"); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::IncompatibleDocumentTypeSchemaError(e) + )] if e.operation() == "replace" + && e.property_path() == "/properties/toUserId/refersTo/type" + ); + } } mod validate_byte_array_encoding { 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 cb886cffd67..7a77778410c 100644 --- a/packages/rs-dpp/src/data_contract/document_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/mod.rs @@ -79,6 +79,7 @@ pub(crate) mod property_names { pub const CONTENT_MEDIA_TYPE: &str = "contentMediaType"; pub const ENCRYPTION_KEY_REQUIREMENTS: &str = "encryptionKeyReqs"; pub const DECRYPTION_KEY_REQUIREMENTS: &str = "decryptionKeyReqs"; + pub const REFERS_TO: &str = "refersTo"; 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 1b37b3ca8e3..80f5513cf15 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 @@ -4,6 +4,8 @@ use std::convert::TryInto; use std::io::{BufReader, Cursor, Read}; use crate::data_contract::errors::DataContractError; +use bincode::{Decode, Encode}; +use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; use crate::consensus::basic::decode::DecodingError; use crate::data_contract::config::v1::DataContractConfigGettersV1; @@ -51,6 +53,26 @@ pub struct ByteArrayPropertySizes { pub max_size: Option, } +#[derive( + Debug, PartialEq, Eq, Clone, Serialize, Encode, Decode, PlatformSerialize, PlatformDeserialize, +)] +#[serde(rename_all = "lowercase")] +pub enum DocumentPropertyReferenceTarget { + Identity, + Contract, + Token, +} + +impl std::fmt::Display for DocumentPropertyReferenceTarget { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DocumentPropertyReferenceTarget::Identity => write!(f, "identity"), + DocumentPropertyReferenceTarget::Contract => write!(f, "contract"), + DocumentPropertyReferenceTarget::Token => write!(f, "token"), + } + } +} + // @append_only #[derive(Debug, PartialEq, Clone, Serialize)] pub enum DocumentPropertyType { @@ -67,7 +89,7 @@ pub enum DocumentPropertyType { F64, String(StringPropertySizes), ByteArray(ByteArrayPropertySizes), - Identifier, + Identifier(Option), Boolean, Date, Object(IndexMap), @@ -92,7 +114,7 @@ impl DocumentPropertyType { "f64" | "number" => Ok(DocumentPropertyType::F64), "boolean" => Ok(DocumentPropertyType::Boolean), "date" => Ok(DocumentPropertyType::Date), - "identifier" => Ok(DocumentPropertyType::Identifier), + "identifier" => Ok(DocumentPropertyType::Identifier(None)), "string" => Ok(DocumentPropertyType::String(StringPropertySizes { min_length: None, max_length: None, @@ -128,7 +150,7 @@ impl DocumentPropertyType { DocumentPropertyType::F64 => "f64".to_string(), DocumentPropertyType::String(_) => "string".to_string(), DocumentPropertyType::ByteArray(_) => "byteArray".to_string(), - DocumentPropertyType::Identifier => "identifier".to_string(), + DocumentPropertyType::Identifier(_) => "identifier".to_string(), DocumentPropertyType::Boolean => "boolean".to_string(), DocumentPropertyType::Date => "date".to_string(), DocumentPropertyType::Object(_) => "object".to_string(), @@ -166,7 +188,7 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => None, DocumentPropertyType::VariableTypeArray(_) => None, - DocumentPropertyType::Identifier => Some(32), + DocumentPropertyType::Identifier(_) => Some(32), } } @@ -211,7 +233,7 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => Ok(None), DocumentPropertyType::VariableTypeArray(_) => Ok(None), - DocumentPropertyType::Identifier => Ok(Some(32)), + DocumentPropertyType::Identifier(_) => Ok(Some(32)), } } @@ -256,7 +278,7 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => Ok(None), DocumentPropertyType::VariableTypeArray(_) => Ok(None), - DocumentPropertyType::Identifier => Ok(Some(32)), + DocumentPropertyType::Identifier(_) => Ok(Some(32)), } } @@ -289,7 +311,7 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => None, DocumentPropertyType::VariableTypeArray(_) => None, - DocumentPropertyType::Identifier => Some(32), + DocumentPropertyType::Identifier(_) => Some(32), } } @@ -423,7 +445,7 @@ impl DocumentPropertyType { } DocumentPropertyType::Array(_) => Value::Null, DocumentPropertyType::VariableTypeArray(_) => Value::Null, - DocumentPropertyType::Identifier => Value::Identifier(rng.gen()), + DocumentPropertyType::Identifier(_) => Value::Identifier(rng.gen()), } } @@ -472,7 +494,7 @@ impl DocumentPropertyType { } DocumentPropertyType::Array(_) => Value::Null, DocumentPropertyType::VariableTypeArray(_) => Value::Null, - DocumentPropertyType::Identifier => Value::Identifier(rng.gen()), + DocumentPropertyType::Identifier(_) => Value::Identifier(rng.gen()), } } @@ -521,7 +543,7 @@ impl DocumentPropertyType { } DocumentPropertyType::Array(_) => Value::Null, DocumentPropertyType::VariableTypeArray(_) => Value::Null, - DocumentPropertyType::Identifier => Value::Identifier(rng.gen()), + DocumentPropertyType::Identifier(_) => Value::Identifier(rng.gen()), } } @@ -697,7 +719,7 @@ impl DocumentPropertyType { } } } - DocumentPropertyType::Identifier => { + DocumentPropertyType::Identifier(_) => { let mut id = [0; 32]; buf.read_exact(&mut id).map_err(|_| { DataContractError::DecodingContractError(DecodingError::new( @@ -916,7 +938,7 @@ impl DocumentPropertyType { r_vec.append(&mut bytes); Ok(r_vec) } - DocumentPropertyType::Identifier => { + DocumentPropertyType::Identifier(_) => { let mut bytes = value.into_identifier_bytes()?; let mut r_vec = bytes.len().encode_var_vec(); @@ -1074,7 +1096,7 @@ impl DocumentPropertyType { Ok(r_vec) } }, - DocumentPropertyType::Identifier => Ok(value.to_identifier_bytes()?), + DocumentPropertyType::Identifier(_) => Ok(value.to_identifier_bytes()?), DocumentPropertyType::Boolean => { let value_as_boolean = value .as_bool() @@ -1209,7 +1231,7 @@ impl DocumentPropertyType { DocumentPropertyType::ByteArray(_) => { value.to_binary_bytes().map_err(ProtocolError::ValueError) } - DocumentPropertyType::Identifier => value + DocumentPropertyType::Identifier(_) => value .to_identifier_bytes() .map_err(ProtocolError::ValueError), DocumentPropertyType::Boolean => { @@ -1330,7 +1352,7 @@ impl DocumentPropertyType { Ok(Value::Float(float)) } DocumentPropertyType::ByteArray(_) => Ok(Value::Bytes(value.to_vec())), - DocumentPropertyType::Identifier => { + DocumentPropertyType::Identifier(_) => { let identifier = Identifier::from_bytes(value)?; Ok(identifier.into()) } @@ -1456,7 +1478,7 @@ impl DocumentPropertyType { DataContractError::ValueDecodingError("could not parse hex bytes".to_string()) })?)) } - DocumentPropertyType::Identifier => Ok(Value::Identifier( + DocumentPropertyType::Identifier(_) => Ok(Value::Identifier( Value::Text(str.to_owned()) .to_identifier() .map_err(|e| DataContractError::ValueDecodingError(format!("{:?}", e)))? @@ -2140,7 +2162,7 @@ impl DocumentPropertyType { } // Convert hex or base58 strings to identifiers for Identifier fields - (DocumentPropertyType::Identifier, Value::Text(str_value)) => { + (DocumentPropertyType::Identifier(_), Value::Text(str_value)) => { // First try base58 decoding (most common for identifiers) if let Ok(id) = Identifier::from_string_unknown_encoding(&str_value) { *value = Value::Identifier(id.into_buffer()); @@ -2394,7 +2416,9 @@ impl DocumentPropertyType { } match value_map.get_optional_str(property_names::CONTENT_MEDIA_TYPE)? { - Some("application/x.dash.dpp.identifier") => DocumentPropertyType::Identifier, + Some("application/x.dash.dpp.identifier") => { + DocumentPropertyType::Identifier(None) + } Some(_) | None => DocumentPropertyType::ByteArray(ByteArrayPropertySizes { min_size: value_map.get_optional_integer(property_names::MIN_ITEMS)?, max_size: value_map.get_optional_integer(property_names::MAX_ITEMS)?, @@ -2557,7 +2581,7 @@ mod tests { }), "byteArray", ), - (DocumentPropertyType::Identifier, "identifier"), + (DocumentPropertyType::Identifier(None), "identifier"), (DocumentPropertyType::Boolean, "boolean"), (DocumentPropertyType::Date, "date"), (DocumentPropertyType::Object(IndexMap::new()), "object"), @@ -2646,7 +2670,7 @@ mod tests { ); assert_eq!( DocumentPropertyType::try_from_name("identifier").unwrap(), - DocumentPropertyType::Identifier + DocumentPropertyType::Identifier(None) ); assert!(DocumentPropertyType::try_from_name("string").is_ok()); assert!(DocumentPropertyType::try_from_name("byteArray").is_ok()); @@ -2684,7 +2708,7 @@ mod tests { assert_eq!(DocumentPropertyType::F64.min_size(), Some(8)); assert_eq!(DocumentPropertyType::Boolean.min_size(), Some(1)); assert_eq!(DocumentPropertyType::Date.min_size(), Some(8)); - assert_eq!(DocumentPropertyType::Identifier.min_size(), Some(32)); + assert_eq!(DocumentPropertyType::Identifier(None).min_size(), Some(32)); } #[test] @@ -2768,7 +2792,7 @@ mod tests { assert_eq!(DocumentPropertyType::F64.max_size(), Some(8)); assert_eq!(DocumentPropertyType::Boolean.max_size(), Some(1)); assert_eq!(DocumentPropertyType::Date.max_size(), Some(8)); - assert_eq!(DocumentPropertyType::Identifier.max_size(), Some(32)); + assert_eq!(DocumentPropertyType::Identifier(None).max_size(), Some(32)); } #[test] @@ -2863,7 +2887,9 @@ mod tests { Some(8) ); assert_eq!( - DocumentPropertyType::Identifier.min_byte_size(pv).unwrap(), + DocumentPropertyType::Identifier(None) + .min_byte_size(pv) + .unwrap(), Some(32) ); } @@ -3046,7 +3072,7 @@ mod tests { assert!(!DocumentPropertyType::F64.is_integer()); assert!(!DocumentPropertyType::Boolean.is_integer()); assert!(!DocumentPropertyType::Date.is_integer()); - assert!(!DocumentPropertyType::Identifier.is_integer()); + assert!(!DocumentPropertyType::Identifier(None).is_integer()); assert!(!DocumentPropertyType::U128.is_integer()); assert!(!DocumentPropertyType::I128.is_integer()); assert!(!DocumentPropertyType::String(StringPropertySizes { @@ -3477,7 +3503,7 @@ mod tests { #[test] fn test_tree_keys_roundtrip_identifier() { - let prop = DocumentPropertyType::Identifier; + let prop = DocumentPropertyType::Identifier(None); let id_bytes: [u8; 32] = [42u8; 32]; let val = Value::Identifier(id_bytes); let enc = prop.encode_value_for_tree_keys(&val).unwrap(); @@ -3597,7 +3623,7 @@ mod tests { #[test] fn test_encode_value_with_size_identifier() { - let prop = DocumentPropertyType::Identifier; + let prop = DocumentPropertyType::Identifier(None); let id_bytes = [1u8; 32]; let result = prop .encode_value_with_size(Value::Identifier(id_bytes), true) @@ -3862,7 +3888,7 @@ mod tests { #[test] fn test_encode_value_ref_with_size_identifier() { - let prop = DocumentPropertyType::Identifier; + let prop = DocumentPropertyType::Identifier(None); let id_bytes = [5u8; 32]; let val = Value::Identifier(id_bytes); let result = prop.encode_value_ref_with_size(&val, true).unwrap(); @@ -4179,7 +4205,7 @@ mod tests { #[test] fn test_read_optionally_from_identifier_required() { use std::io::BufReader; - let prop = DocumentPropertyType::Identifier; + let prop = DocumentPropertyType::Identifier(None); let id_bytes = [7u8; 32]; let mut reader = BufReader::new(id_bytes.as_slice()); let (value, _) = prop.read_optionally_from(&mut reader, true).unwrap(); @@ -4581,7 +4607,7 @@ mod tests { map.insert("contentMediaType".to_string(), &media_type_val); let options = DocumentPropertyTypeParsingOptions::default(); let result = DocumentPropertyType::try_from_value_map(&map, &options).unwrap(); - assert_eq!(result, DocumentPropertyType::Identifier); + assert_eq!(result, DocumentPropertyType::Identifier(None)); } #[test] @@ -5256,7 +5282,7 @@ mod tests { #[test] fn test_encode_value_for_tree_keys_identifier() { - let prop = DocumentPropertyType::Identifier; + let prop = DocumentPropertyType::Identifier(None); let id = [7u8; 32]; let result = prop .encode_value_for_tree_keys(&Value::Identifier(id)) @@ -5361,7 +5387,7 @@ mod tests { #[test] fn test_decode_value_for_tree_keys_identifier() { - let prop = DocumentPropertyType::Identifier; + let prop = DocumentPropertyType::Identifier(None); let id = [7u8; 32]; let decoded = prop.decode_value_for_tree_keys(&id).unwrap(); if let Value::Identifier(decoded_id) = decoded { @@ -5640,7 +5666,9 @@ mod tests { fn test_min_byte_size_identifier() { let pv = PlatformVersion::latest(); assert_eq!( - DocumentPropertyType::Identifier.min_byte_size(pv).unwrap(), + DocumentPropertyType::Identifier(None) + .min_byte_size(pv) + .unwrap(), Some(32) ); } @@ -5649,7 +5677,9 @@ mod tests { fn test_max_byte_size_identifier() { let pv = PlatformVersion::latest(); assert_eq!( - DocumentPropertyType::Identifier.max_byte_size(pv).unwrap(), + DocumentPropertyType::Identifier(None) + .max_byte_size(pv) + .unwrap(), Some(32) ); } @@ -5782,7 +5812,7 @@ mod tests { Value::Float(_) )); assert!(matches!( - DocumentPropertyType::Identifier.random_value(&mut rng), + DocumentPropertyType::Identifier(None).random_value(&mut rng), Value::Identifier(_) )); } @@ -6019,7 +6049,7 @@ mod tests { fn test_random_sub_filled_value_identifier() { let mut rng = StdRng::seed_from_u64(15); assert!(matches!( - DocumentPropertyType::Identifier.random_sub_filled_value(&mut rng), + DocumentPropertyType::Identifier(None).random_sub_filled_value(&mut rng), Value::Identifier(_) )); } @@ -6142,7 +6172,7 @@ mod tests { Value::Float(_) )); assert!(matches!( - DocumentPropertyType::Identifier.random_filled_value(&mut rng), + DocumentPropertyType::Identifier(None).random_filled_value(&mut rng), Value::Identifier(_) )); assert_eq!( @@ -6280,7 +6310,7 @@ mod tests { #[test] fn test_read_optionally_from_identifier_truncated_returns_error() { - let prop = DocumentPropertyType::Identifier; + let prop = DocumentPropertyType::Identifier(None); // Only 16 bytes but identifier needs 32 let data = [1u8; 16]; let mut reader = BufReader::new(data.as_slice()); @@ -7066,4 +7096,24 @@ mod tests { assert!(window[0] < window[1]); } } + + #[test] + fn should_serialize_reference_metadata() { + let property = DocumentProperty { + property_type: DocumentPropertyType::Identifier(Some( + DocumentPropertyReferenceTarget::Identity, + )), + required: false, + transient: false, + }; + + let value = serde_json::to_value(&property).expect("serialization should succeed"); + + assert_eq!( + value.get("property_type"), + Some(&serde_json::json!({ + "Identifier": "identity" + })) + ); + } } diff --git a/packages/rs-dpp/src/data_contract/document_type/schema/find_identifier_and_binary_paths/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/schema/find_identifier_and_binary_paths/v0/mod.rs index 3fc2259f7ef..61f31d70abb 100644 --- a/packages/rs-dpp/src/data_contract/document_type/schema/find_identifier_and_binary_paths/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/schema/find_identifier_and_binary_paths/v0/mod.rs @@ -29,7 +29,7 @@ impl DocumentTypeV0 { }; match &value.property_type { - DocumentPropertyType::Identifier => { + DocumentPropertyType::Identifier(_) => { identifier_paths.insert(new_path); } DocumentPropertyType::ByteArray(_) => { diff --git a/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs b/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs index b1c1d0b9450..d35297a9ab8 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs @@ -325,7 +325,7 @@ impl DocumentTypeV0 { schema.insert("byteArray".to_string(), serde_json::Value::Bool(true)); serde_json::Value::Object(schema) }, - DocumentPropertyType::Identifier => { + DocumentPropertyType::Identifier(_) => { json!({ "type": "array", "items": { diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index c3de95ae6b3..fceb51d471f 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -311,6 +311,7 @@ impl ErrorWithCode for StateError { Self::IdentityTryingToPayWithWrongTokenError(_) => 40117, Self::DocumentContestIndexMismatchError(_) => 40118, Self::DocumentContestNotRequiredError(_) => 40119, + Self::ReferencedEntityNotFoundError(_) => 40120, // 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 a52e83df43f..bc684d57f1f 100644 --- a/packages/rs-dpp/src/errors/consensus/state/document/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/document/mod.rs @@ -15,3 +15,4 @@ 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_entity_not_found_error; diff --git a/packages/rs-dpp/src/errors/consensus/state/document/referenced_entity_not_found_error.rs b/packages/rs-dpp/src/errors/consensus/state/document/referenced_entity_not_found_error.rs new file mode 100644 index 00000000000..7628c155ae9 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/document/referenced_entity_not_found_error.rs @@ -0,0 +1,56 @@ +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::data_contract::document_type::DocumentPropertyReferenceTarget; +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 {entity_type} {entity_id} not found for path {path}")] +#[platform_serialize(unversioned)] +pub struct ReferencedEntityNotFoundError { + /* + + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + + */ + entity_id: Identifier, + entity_type: DocumentPropertyReferenceTarget, + path: String, +} + +impl ReferencedEntityNotFoundError { + pub fn new( + entity_id: Identifier, + entity_type: DocumentPropertyReferenceTarget, + path: String, + ) -> Self { + Self { + entity_id, + entity_type, + path, + } + } + + pub fn entity_id(&self) -> &Identifier { + &self.entity_id + } + + pub fn entity_type(&self) -> &DocumentPropertyReferenceTarget { + &self.entity_type + } + + pub fn path(&self) -> &str { + &self.path + } +} + +impl From for ConsensusError { + fn from(err: ReferencedEntityNotFoundError) -> Self { + Self::StateError(StateError::ReferencedEntityNotFoundError(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 225080349a9..4b7aec58651 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,7 @@ 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_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; use crate::consensus::state::group::{GroupActionAlreadyCompletedError, GroupActionAlreadySignedByIdentityError, GroupActionDoesNotExistError, IdentityMemberOfGroupNotFoundError, IdentityNotMemberOfGroupError, ModificationOfGroupActionMainParametersNotPermittedError}; @@ -362,6 +363,9 @@ pub enum StateError { #[error(transparent)] DocumentContestNotRequiredError(DocumentContestNotRequiredError), + + #[error(transparent)] + ReferencedEntityNotFoundError(ReferencedEntityNotFoundError), } impl From for ConsensusError { @@ -428,5 +432,15 @@ mod tests { )), 92 ); + assert_eq!( + discriminant_of(StateError::ReferencedEntityNotFoundError( + ReferencedEntityNotFoundError::new( + Identifier::from([1; 32]), + crate::data_contract::document_type::DocumentPropertyReferenceTarget::Identity, + "toUserId".to_string(), + ) + )), + 93 + ); } } diff --git a/packages/rs-dpp/src/validation/meta_validators/mod.rs b/packages/rs-dpp/src/validation/meta_validators/mod.rs index cfb737411a0..82b3c38f2c3 100644 --- a/packages/rs-dpp/src/validation/meta_validators/mod.rs +++ b/packages/rs-dpp/src/validation/meta_validators/mod.rs @@ -300,3 +300,100 @@ lazy_static! { .expect("Invalid data contract schema"); } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn document_schema_with_refers_to(refers_to: serde_json::Value) -> serde_json::Value { + json!({ + "$schema": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json", + "type": "object", + "properties": { + "toUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": refers_to + } + }, + "additionalProperties": false + }) + } + + #[test] + fn should_accept_refers_to_in_v3_document_schema() { + for target in ["identity", "contract", "token"] { + let schema = document_schema_with_refers_to(json!({ + "type": target + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_ok(), + "expected schema with {target} target to be valid" + ); + } + } + + #[test] + fn should_reject_refers_to_with_unknown_properties() { + let schema = document_schema_with_refers_to(json!({ + "type": "identity", + "mustExist": false + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_err(), + "expected unknown refersTo properties to be rejected" + ); + } + + #[test] + fn should_reject_refers_to_with_unknown_type() { + let schema = document_schema_with_refers_to(json!({ + "type": "unknown" + })); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_err(), + "expected schema to be invalid" + ); + } + + #[test] + fn should_reject_refers_to_on_non_identifier_property() { + let schema = json!({ + "$schema": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json", + "type": "object", + "properties": { + "name": { + "type": "string", + "position": 0, + "refersTo": { "type": "identity" } + } + }, + "additionalProperties": false + }); + + assert!( + DOCUMENT_META_SCHEMA_V3.validate(&schema).is_err(), + "expected refersTo on a non-identifier property to be invalid" + ); + } + + #[test] + fn should_reject_refers_to_in_v2_document_schema() { + let schema = document_schema_with_refers_to(json!({ + "type": "identity" + })); + + assert!( + DOCUMENT_META_SCHEMA_V2.validate(&schema).is_err(), + "expected refersTo to be rejected by the v2 meta schema" + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/mod.rs index cd28fee88d8..6d67acc456c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/mod.rs @@ -10,6 +10,7 @@ use crate::error::execution::ExecutionError; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v0::DocumentCreateTransitionActionStateValidationV0; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v1::DocumentCreateTransitionActionStateValidationV1; +use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v2::DocumentCreateTransitionActionStateValidationV2; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::advanced_structure_v0::DocumentCreateTransitionActionStructureValidationV0; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::advanced_structure_v1::DocumentCreateTransitionActionStructureValidationV1; use crate::platform_types::platform::PlatformStateRef; @@ -18,6 +19,7 @@ mod advanced_structure_v0; mod advanced_structure_v1; mod state_v0; mod state_v1; +mod state_v2; pub trait DocumentCreateTransitionActionValidation { fn validate_structure( @@ -100,9 +102,18 @@ impl DocumentCreateTransitionActionValidation for DocumentCreateTransitionAction transaction, platform_version, ), + // V2 introduces document reference validation (`refersTo`) on top of V1 + 2 => self.validate_state_v2( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "DocumentCreateTransitionAction::validate_state".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs new file mode 100644 index 00000000000..451b7620710 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs @@ -0,0 +1,66 @@ +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::{ + DocumentCreateTransitionAction, DocumentCreateTransitionActionAccessorsV0, +}; + +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v1::DocumentCreateTransitionActionStateValidationV1; +use crate::execution::validation::state_transition::batch::action_validation::document::document_reference_validation::DocumentReferenceValidation; +use crate::platform_types::platform::PlatformStateRef; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentCreateTransitionActionStateValidationV2 +{ + fn validate_state_v2( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentCreateTransitionActionStateValidationV2 for DocumentCreateTransitionAction { + fn validate_state_v2( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.validate_state_v1( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let reference_result = self.base().validate_document_references( + self.data(), + None, + platform, + block_info, + transaction, + execution_context, + platform_version, + )?; + if !reference_result.is_valid() { + return Ok(reference_result); + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/mod.rs new file mode 100644 index 00000000000..74cc62a04cc --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/mod.rs @@ -0,0 +1,70 @@ +pub mod v0; + +use std::collections::{BTreeMap, BTreeSet}; + +use dpp::block::block_info::BlockInfo; +use dpp::platform_value::Value; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_reference_validation::v0::DocumentReferenceValidationV0; +use crate::platform_types::platform::PlatformStateRef; + +pub(crate) trait DocumentReferenceValidation { + /// Validates the document's `refersTo` references against platform state. + /// + /// When `changed_fields` is provided (replace transitions), only references on + /// those fields are validated. + #[allow(clippy::too_many_arguments)] + fn validate_document_references( + &self, + document_data: &BTreeMap, + changed_fields: Option<&BTreeSet>, + platform: &PlatformStateRef, + block_info: &BlockInfo, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentReferenceValidation for DocumentBaseTransitionAction { + fn validate_document_references( + &self, + document_data: &BTreeMap, + changed_fields: Option<&BTreeSet>, + platform: &PlatformStateRef, + block_info: &BlockInfo, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .document_reference_validation + { + 0 => self.validate_document_references_v0( + document_data, + changed_fields, + platform, + block_info, + transaction, + execution_context, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "DocumentBaseTransitionAction::validate_document_references".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs new file mode 100644 index 00000000000..152973718fe --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs @@ -0,0 +1,199 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use dpp::block::block_info::BlockInfo; +use dpp::consensus::basic::document::InvalidDocumentTypeError; +use dpp::consensus::basic::invalid_identifier_error::InvalidIdentifierError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::{ + DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentTypeRef, +}; +use dpp::errors::consensus::state::document::referenced_entity_not_found_error::ReferencedEntityNotFoundError; +use dpp::identifier::Identifier; +use dpp::platform_value::btreemap_extensions::BTreeValueMapPathHelper; +use dpp::platform_value::Value; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::execution_operation::{RetrieveIdentityInfo, ValidationOperation}; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; +use crate::platform_types::platform::PlatformStateRef; + +/// Versioned, stateful validation of document references using the v0 rules. +/// +/// This performs existence checks for the supported reference targets (identity, +/// contract and token) and can be limited to changed fields for replace +/// transitions. It is intended to be called via the higher-level +/// `DocumentReferenceValidation` dispatcher that selects the version. +pub(crate) trait DocumentReferenceValidationV0 { + #[allow(clippy::too_many_arguments)] + fn validate_document_references_v0( + &self, + document_data: &BTreeMap, + changed_fields: Option<&BTreeSet>, + platform: &PlatformStateRef, + block_info: &BlockInfo, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentReferenceValidationV0 for DocumentBaseTransitionAction { + fn validate_document_references_v0( + &self, + document_data: &BTreeMap, + changed_fields: Option<&BTreeSet>, + platform: &PlatformStateRef, + block_info: &BlockInfo, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result { + let contract_fetch_info = self.data_contract_fetch_info(); + let contract = &contract_fetch_info.contract; + let document_type_name = self.document_type_name(); + + let Some(document_type) = contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTypeError::new(document_type_name.clone(), contract.id()).into(), + )); + }; + + validate_document_type_references_v0( + document_type, + document_data, + changed_fields, + platform, + block_info, + transaction, + execution_context, + platform_version, + ) + } +} + +#[allow(clippy::too_many_arguments)] +fn validate_document_type_references_v0( + document_type: DocumentTypeRef<'_>, + document_data: &BTreeMap, + changed_fields: Option<&BTreeSet>, + platform: &PlatformStateRef, + block_info: &BlockInfo, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, +) -> Result { + for (path, property) in document_type.flattened_properties() { + if let Some(changed) = changed_fields { + if !is_changed_field(changed, path) { + continue; + } + } + + let DocumentPropertyType::Identifier(Some(reference_target)) = &property.property_type + else { + continue; + }; + + let referenced_id = match document_data.get_optional_identifier_at_path(path) { + Ok(Some(referenced_id)) => referenced_id, + // A reference property that is not set is not validated; whether it may be + // absent at all is enforced by the document type's required fields + Ok(None) => continue, + Err(err) => { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidIdentifierError::new(path.to_string(), err.to_string()).into(), + )) + } + }; + + let exists = match reference_target { + DocumentPropertyReferenceTarget::Identity => { + execution_context.add_operation(ValidationOperation::RetrieveIdentity( + RetrieveIdentityInfo::only_revision(), + )); + + platform + .drive + .fetch_identity_revision(referenced_id, true, transaction, platform_version)? + .is_some() + } + DocumentPropertyReferenceTarget::Contract => { + let (fee, referenced_contract) = + platform.drive.get_contract_with_fetch_info_and_fee( + referenced_id, + 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)); + + referenced_contract.is_some() + } + DocumentPropertyReferenceTarget::Token => { + // Token contract info is written for every token when its contract is + // inserted and is never deleted, so it serves as the existence record + let (referenced_token_info, fee) = + platform.drive.fetch_token_contract_info_with_costs( + referenced_id, + block_info, + true, + transaction, + platform_version, + )?; + + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + + referenced_token_info.is_some() + } + }; + + if !exists { + let missing_id = + Identifier::from_bytes(&referenced_id).map_err(|e| Error::Protocol(e.into()))?; + + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::ReferencedEntityNotFoundError( + ReferencedEntityNotFoundError::new( + missing_id, + reference_target.clone(), + path.to_string(), + ), + )), + )); + } + } + + Ok(SimpleConsensusValidationResult::new()) +} + +/// A flattened property path counts as changed when the replace transition changed +/// the path itself or any of its ancestors: `changed_data_fields` holds top-level +/// document keys, so a changed object key replaces its entire subtree, including +/// any nested reference properties under it. +fn is_changed_field(changed_fields: &BTreeSet, path: &str) -> bool { + changed_fields.iter().any(|field| { + path == field + || path + .strip_prefix(field.as_str()) + .is_some_and(|rest| rest.starts_with('.')) + }) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/mod.rs index de32e19b21c..9f964c616ef 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/mod.rs @@ -9,11 +9,13 @@ use crate::error::Error; use crate::error::execution::ExecutionError; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::state_v0::DocumentReplaceTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::state_v1::DocumentReplaceTransitionActionStateValidationV1; use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::advanced_structure_v0::DocumentReplaceTransitionActionStructureValidationV0; use crate::platform_types::platform::PlatformStateRef; mod advanced_structure_v0; mod state_v0; +mod state_v1; pub trait DocumentReplaceTransitionActionValidation { fn validate_structure( @@ -77,9 +79,19 @@ impl DocumentReplaceTransitionActionValidation for DocumentReplaceTransitionActi transaction, platform_version, ), + // V1 introduces document reference validation (`refersTo`) on top of V0, + // limited to the fields changed by the replace transition + 1 => self.validate_state_v1( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "DocumentReplaceTransitionAction::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/batch/action_validation/document/document_replace_transition_action/state_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/state_v1/mod.rs new file mode 100644 index 00000000000..d1122c02bee --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/state_v1/mod.rs @@ -0,0 +1,66 @@ +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::{ + DocumentReplaceTransitionAction, DocumentReplaceTransitionActionAccessorsV0, +}; + +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_reference_validation::DocumentReferenceValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::state_v0::DocumentReplaceTransitionActionStateValidationV0; +use crate::platform_types::platform::PlatformStateRef; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentReplaceTransitionActionStateValidationV1 +{ + fn validate_state_v1( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentReplaceTransitionActionStateValidationV1 for DocumentReplaceTransitionAction { + fn validate_state_v1( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let reference_result = self.base().validate_document_references( + self.data(), + Some(self.changed_data_fields()), + platform, + block_info, + transaction, + execution_context, + platform_version, + )?; + if !reference_result.is_valid() { + return Ok(reference_result); + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs index ac80aafead6..ee1fb8389f2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs @@ -2,6 +2,7 @@ mod document_base_transaction_action; pub(crate) mod document_create_transition_action; pub(crate) mod document_delete_transition_action; pub(crate) mod document_purchase_transition_action; +pub(crate) mod document_reference_validation; pub(crate) mod document_replace_transition_action; pub(crate) mod document_transfer_transition_action; pub(crate) mod document_update_price_transition_action; 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 a0a19039af8..a221eba8be8 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 @@ -4684,4 +4684,327 @@ mod creation_tests { // He was paid 5 assert_eq!(token_balance, Some(5)); } + + const REFERENCE_VALIDATION_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract.json"; + const REFERENCE_VALIDATION_NESTED_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-nested.json"; + const REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-contract-ref.json"; + /// The `id` of the contract-reference fixture contract; the happy-path test + /// references it since it is the one contract known to exist in state. + const REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_ID: &str = + "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd"; + const REFERENCE_VALIDATION_TOKEN_REF_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-token-ref.json"; + const REFERENCE_VALIDATION_OPTIONAL_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-optional.json"; + + /// References the mutator can point document fields at: the two identities + /// existing in state and the id of a token that exists in state. + struct ReferenceTargets { + identity_id: Identifier, + other_identity_id: Identifier, + token_id: Identifier, + } + + // Helper to run document creation with custom reference mutations. + async fn run_reference_validation_creation_with_mutator( + contract_path: &str, + mutator: F, + ) -> StateTransitionExecutionResult + where + F: FnOnce(&mut Document, &ReferenceTargets), + { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(433); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + let (other_identity, ..) = setup_identity(&mut platform, 959, dash_to_credits!(0.1)); + + let (_token_contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + other_identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + let targets = ReferenceTargets { + identity_id: identity.id(), + other_identity_id: other_identity.id(), + token_id, + }; + + let contract = setup_contract( + &platform.drive, + contract_path, + None, + None, + None::, + None, + None, + ); + + let message = contract + .document_type_for_name("message") + .expect("expected a message document type"); + + let entropy = Bytes32::random_with_rng(&mut rng); + + let mut document = message + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random document"); + + mutator(&mut document, &targets); + + let documents_batch_create_transition = + BatchTransition::new_document_creation_transition_from_document( + document, + message, + entropy.0, + &key, + 2, + 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_fail_when_referenced_identity_missing() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_CONTRACT_PATH, + |document, _| { + document.set("toUserId", Identifier::random().into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_referenced_identity_exists() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_CONTRACT_PATH, + |document, targets| { + document.set("toUserId", targets.identity_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_referenced_contract_missing() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_PATH, + |document, _| { + document.set("refContractId", Identifier::random().into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_optional_reference_not_set() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_OPTIONAL_CONTRACT_PATH, + |document, _| { + document.remove("optionalUserId"); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_optional_reference_set_to_missing_identity() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_OPTIONAL_CONTRACT_PATH, + |document, _| { + document.set("optionalUserId", Identifier::random().into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_referenced_token_missing() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_TOKEN_REF_CONTRACT_PATH, + |document, _| { + document.set("refTokenId", Identifier::random().into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_referenced_token_exists() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_TOKEN_REF_CONTRACT_PATH, + |document, targets| { + document.set("refTokenId", targets.token_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_referenced_contract_exists() { + let existing_contract_id = Identifier::from_string( + REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_ID, + Encoding::Base58, + ) + .expect("expected a valid contract id"); + + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_PATH, + |document, _| { + document.set("refContractId", existing_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_with_nested_and_multiple_references() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_NESTED_CONTRACT_PATH, + |document, targets| { + document.set("toUserId", targets.identity_id.into()); + document.set("otherUserId", targets.other_identity_id.into()); + document.set("meta.nestedUserId", targets.identity_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_nested_reference_missing() { + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_NESTED_CONTRACT_PATH, + |document, targets| { + document.set("toUserId", targets.identity_id.into()); + document.set("otherUserId", targets.other_identity_id.into()); + document.set("meta.nestedUserId", Identifier::random().into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index ec5a3d79634..e0294cd1482 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -3,12 +3,332 @@ use super::*; mod replacement_tests { use super::*; use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; + use dpp::data_contract::DataContract; + use dpp::document::Document; + use dpp::fee::fee_result::FeeResult; use dpp::identifier::Identifier; use dpp::prelude::IdentityNonce; use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; use dpp::tokens::token_payment_info::TokenPaymentInfo; + use drive::util::test_helpers::setup_contract; use std::collections::BTreeMap; + const REFERENCE_VALIDATION_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract.json"; + const REFERENCE_VALIDATION_NESTED_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-nested.json"; + const REFERENCE_VALIDATION_OPTIONAL_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-optional.json"; + + /// Creates a document from `contract_path`'s message type, applies `create_setup` + /// to it, processes the creation (asserting success), then applies `replace_mutation` + /// and processes the replacement, returning its execution result. + async fn run_reference_validation_create_then_replace( + contract_path: &str, + create_setup: C, + replace_mutation: R, + ) -> StateTransitionExecutionResult + where + C: FnOnce(&mut Document, Identifier, Identifier), + R: FnOnce(&mut Document, Identifier, Identifier), + { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(433); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + let (other_identity, ..) = setup_identity(&mut platform, 959, dash_to_credits!(0.1)); + + let contract = setup_contract( + &platform.drive, + contract_path, + None, + None, + None::, + None, + None, + ); + + let message = contract + .document_type_for_name("message") + .expect("expected a message document type"); + + let entropy = Bytes32::random_with_rng(&mut rng); + + let mut document = message + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random document"); + + create_setup(&mut document, identity.id(), other_identity.id()); + + let documents_batch_create_transition = + BatchTransition::new_document_creation_transition_from_document( + document.clone(), + message, + entropy.0, + &key, + 2, + 0, + None, + &signer, + platform_version, + None, + ) + .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"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + document.increment_revision().unwrap(); + replace_mutation(&mut document, identity.id(), other_identity.id()); + + let documents_batch_replace_transition = + BatchTransition::new_document_replacement_transition_from_document( + document, + message, + &key, + 3, + 0, + None, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create documents batch transition"); + + let documents_batch_replace_serialized_transition = documents_batch_replace_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[documents_batch_replace_serialized_transition], + &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() + } + + async fn run_reference_validation_replace_with_contract( + contract_path: &str, + to_user_id: F, + change_note: bool, + ) -> (StateTransitionExecutionResult, FeeResult) + where + F: FnOnce(Identifier, Identifier) -> Identifier, + { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(433); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + let (other_identity, ..) = setup_identity(&mut platform, 959, dash_to_credits!(0.1)); + + let contract = setup_contract( + &platform.drive, + contract_path, + None, + None, + None::, + None, + None, + ); + + let message = contract + .document_type_for_name("message") + .expect("expected a message document type"); + + let entropy = Bytes32::random_with_rng(&mut rng); + + let mut document = message + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random document"); + + document.set("toUserId", identity.id().into()); + document.set("note", "before".into()); + + let documents_batch_create_transition = + BatchTransition::new_document_creation_transition_from_document( + document.clone(), + message, + entropy.0, + &key, + 2, + 0, + None, + &signer, + platform_version, + None, + ) + .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"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + document.increment_revision().unwrap(); + if change_note { + document.set("note", "after".into()); + } + document.set( + "toUserId", + to_user_id(identity.id(), other_identity.id()).into(), + ); + + let documents_batch_replace_transition = + BatchTransition::new_document_replacement_transition_from_document( + document, + message, + &key, + 3, + 0, + None, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create documents batch transition"); + + let documents_batch_replace_serialized_transition = documents_batch_replace_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[documents_batch_replace_serialized_transition], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let result = processing_result + .execution_results() + .first() + .expect("expected one execution result") + .clone(); + + (result, processing_result.aggregated_fees().clone()) + } + #[tokio::test] async fn test_document_replace_on_document_type_that_is_mutable() { run_document_replace_on_document_type_that_is_mutable_at_protocol_version( @@ -2537,4 +2857,108 @@ mod replacement_tests { // He had 5, but spent 2 assert_eq!(token_balance, Some(3)); } + + #[tokio::test] + async fn should_document_replace_fail_when_referenced_identity_missing() { + let (result, _) = run_reference_validation_replace_with_contract( + REFERENCE_VALIDATION_CONTRACT_PATH, + |_, _| Identifier::random(), + false, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } + + #[tokio::test] + async fn should_document_replace_validate_only_changed_fields() { + let (_, fee_without_reference) = run_reference_validation_replace_with_contract( + REFERENCE_VALIDATION_CONTRACT_PATH, + |identity_id, _| identity_id, + true, + ) + .await; + + let (_, fee_with_reference) = run_reference_validation_replace_with_contract( + REFERENCE_VALIDATION_CONTRACT_PATH, + |_, other_id| other_id, + true, + ) + .await; + + assert!( + fee_with_reference.processing_fee > fee_without_reference.processing_fee, + "expected identity reference validation to increase processing fee" + ); + } + + #[tokio::test] + async fn should_document_replace_fail_when_nested_reference_changed_to_missing_identity() { + // Regression: changed_data_fields holds top-level keys ("meta"), while + // reference properties are tracked by flattened path ("meta.nestedUserId"); + // a nested reference under a changed object must still be validated. + let result = run_reference_validation_create_then_replace( + REFERENCE_VALIDATION_NESTED_CONTRACT_PATH, + |document, owner_id, other_id| { + document.set("toUserId", owner_id.into()); + document.set("otherUserId", other_id.into()); + document.set("meta.nestedUserId", owner_id.into()); + }, + |document, _, _| { + document.set("meta.nestedUserId", Identifier::random().into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } + + #[tokio::test] + async fn should_document_replace_succeed_when_optional_reference_removed() { + let result = run_reference_validation_create_then_replace( + REFERENCE_VALIDATION_OPTIONAL_CONTRACT_PATH, + |document, owner_id, _| { + document.set("optionalUserId", owner_id.into()); + }, + |document, _, _| { + document.remove("optionalUserId"); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_replace_fail_when_reference_field_changed_to_missing_identity() { + let (result, _) = run_reference_validation_replace_with_contract( + REFERENCE_VALIDATION_CONTRACT_PATH, + |_, _| Identifier::random(), + true, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(_)), + .. + } + ); + } } diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-contract-ref.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-contract-ref.json new file mode 100644 index 00000000000..569484c157c --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-contract-ref.json @@ -0,0 +1,35 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "refContractId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "contract" + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "refContractId" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-nested.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-nested.json new file mode 100644 index 00000000000..f4e2544380d --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-nested.json @@ -0,0 +1,64 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "toUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "identity" + } + }, + "otherUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 1, + "refersTo": { + "type": "identity" + } + }, + "meta": { + "type": "object", + "position": 2, + "properties": { + "nestedUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "identity" + } + } + }, + "required": [ + "nestedUserId" + ], + "additionalProperties": false + } + }, + "required": [ + "toUserId", + "otherUserId", + "meta" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-optional.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-optional.json new file mode 100644 index 00000000000..5814ccbfab6 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-optional.json @@ -0,0 +1,35 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "optionalUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "identity" + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "note" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-token-ref.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-token-ref.json new file mode 100644 index 00000000000..22fa245fbbf --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-token-ref.json @@ -0,0 +1,35 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "refTokenId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "token" + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "refTokenId" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract.json new file mode 100644 index 00000000000..f13b224836a --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract.json @@ -0,0 +1,35 @@ +{ + "$formatVersion": "1", + "id": "4Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVd", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "toUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "identity" + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "toUserId" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive/src/query/conditions.rs b/packages/rs-drive/src/query/conditions.rs index 6a062b512d3..7a87ee015b5 100644 --- a/packages/rs-drive/src/query/conditions.rs +++ b/packages/rs-drive/src/query/conditions.rs @@ -1476,7 +1476,7 @@ impl<'a> WhereClause { use DocumentPropertyType as T; match prop_ty { T::String(_) => matches!(v, Value::Text(_)), - T::Identifier => matches!(v, Value::Identifier(_)), + T::Identifier(_) => matches!(v, Value::Identifier(_)), T::Boolean => matches!(v, Value::Bool(_)), T::ByteArray(_) => matches!(v, Value::Bytes(_)), T::F64 => matches!(v, Value::Float(_)), @@ -1547,7 +1547,7 @@ impl<'a> WhereClause { | Value::I8(_) ), T::String(_) => matches!(self.value, Value::Text(_)), - T::Identifier => matches!(self.value, Value::Identifier(_)), + T::Identifier(_) => matches!(self.value, Value::Identifier(_)), T::ByteArray(_) => matches!(self.value, Value::Bytes(_)), T::Boolean => matches!(self.value, Value::Bool(_)), // Not applicable for object/array/variable arrays @@ -1642,7 +1642,7 @@ pub fn allowed_ops_for_type(property_type: &DocumentPropertyType) -> &'static [W BetweenExcludeLeft, BetweenExcludeRight, ], - DocumentPropertyType::Identifier => &[Equal, In], + DocumentPropertyType::Identifier(_) => &[Equal, In], DocumentPropertyType::ByteArray(_) => &[Equal, In], DocumentPropertyType::Boolean => &[Equal], DocumentPropertyType::Object(_) @@ -1675,7 +1675,7 @@ fn meta_field_property_type(field: &str) -> Option { match field { // Identifiers "$id" | "$ownerId" | "$dataContractId" | "$creatorId" => { - Some(DocumentPropertyType::Identifier) + Some(DocumentPropertyType::Identifier(None)) } // Dates (millis since epoch) "$createdAt" | "$updatedAt" | "$transferredAt" => Some(DocumentPropertyType::Date), @@ -3440,7 +3440,7 @@ mod tests { for field in ["$id", "$ownerId", "$dataContractId", "$creatorId"] { let pt = meta_field_property_type(field); assert!( - matches!(pt, Some(DocumentPropertyType::Identifier)), + matches!(pt, Some(DocumentPropertyType::Identifier(_))), "expected Identifier for {field}" ); } @@ -3587,7 +3587,7 @@ mod tests { use super::allowed_ops_for_type; use dpp::data_contract::document_type::DocumentPropertyType; - let ops = allowed_ops_for_type(&DocumentPropertyType::Identifier); + let ops = allowed_ops_for_type(&DocumentPropertyType::Identifier(None)); assert_eq!(ops, &[Equal, In]); } diff --git a/packages/rs-json-schema-compatibility-validator/src/rules/rule_set.rs b/packages/rs-json-schema-compatibility-validator/src/rules/rule_set.rs index 2962395889a..6718805e19f 100644 --- a/packages/rs-json-schema-compatibility-validator/src/rules/rule_set.rs +++ b/packages/rs-json-schema-compatibility-validator/src/rules/rule_set.rs @@ -1160,6 +1160,45 @@ pub static KEYWORD_COMPATIBILITY_RULES: Lazy = Laz ], }, ), + ( + "refersTo", + CompatibilityRules { + allow_addition: false, + allow_removal: false, + allow_replacement_callback: FALSE_CALLBACK.clone(), + subschema_levels_depth: None, + inner: None, + #[cfg(any(test, feature = "examples"))] + examples: vec![ + ( + json!({}), + json!({ "refersTo": { "type": "identity" } }), + Some(JsonSchemaChange::Add(AddOperation { + path: "/refersTo".to_string(), + value: json!({ "type": "identity" }), + })), + ) + .into(), + ( + json!({ "refersTo": { "type": "identity" } }), + json!({}), + Some(JsonSchemaChange::Remove(RemoveOperation { + path: "/refersTo".to_string(), + })), + ) + .into(), + ( + json!({ "refersTo": { "type": "identity" } }), + json!({ "refersTo": { "type": "contract" } }), + Some(JsonSchemaChange::Replace(ReplaceOperation { + path: "/refersTo/type".to_string(), + value: json!("contract"), + })), + ) + .into(), + ], + }, + ), ( "byteArray", CompatibilityRules { diff --git a/packages/rs-json-schema-compatibility-validator/tests/rules.rs b/packages/rs-json-schema-compatibility-validator/tests/rules.rs index f7e69758ff7..32d87c90fbf 100644 --- a/packages/rs-json-schema-compatibility-validator/tests/rules.rs +++ b/packages/rs-json-schema-compatibility-validator/tests/rules.rs @@ -1,6 +1,7 @@ use json_schema_compatibility_validator::{ validate_schemas_compatibility, CompatibilityRuleExample, Options, KEYWORD_COMPATIBILITY_RULES, }; +use serde_json::json; #[test] fn test_schema_keyword_rules() { @@ -49,3 +50,49 @@ To: {:?}", } } } + +#[test] +fn should_reject_refers_to_addition_as_incompatible() { + let options = Options::default(); + let original_schema = json!({ + "type": "object", + "properties": { + "toUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0 + } + }, + "additionalProperties": false + }); + + let new_schema = json!({ + "type": "object", + "properties": { + "toUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { "type": "identity" } + } + }, + "additionalProperties": false + }); + + let result = validate_schemas_compatibility(&original_schema, &new_schema, &options) + .expect("compatibility validation failed"); + + assert!(!result.is_compatible(), "expected incompatibility"); + assert!( + result.incompatible_changes().iter().any( + |change| change.name() == "add" && change.path() == "/properties/toUserId/refersTo" + ), + "expected add of /properties/toUserId/refersTo to be incompatible" + ); +} 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 ffce6d06ed1..57405878d69 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 @@ -212,6 +212,7 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { pub document_transfer_transition_state_validation: FeatureVersion, pub document_purchase_transition_state_validation: FeatureVersion, pub document_update_price_transition_state_validation: FeatureVersion, + pub document_reference_validation: FeatureVersion, pub token_mint_transition_structure_validation: FeatureVersion, pub token_burn_transition_structure_validation: FeatureVersion, pub token_transfer_transition_structure_validation: FeatureVersion, 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 c772961c230..241823540b6 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 @@ -136,6 +136,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 899f366268d..08a99d7c2fa 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 @@ -9,7 +9,11 @@ use crate::version::drive_abci_versions::drive_abci_validation_versions::{ // PROTOCOL_VERSION_14: bump `document_create_transition_structure_validation` to // 1, which cross-checks the index named by a document create transition's // prefunded voting balance against the contested index the document itself -// resolves to. v9 remains unchanged for PROTOCOL_VERSION_13 chain replay. +// resolves to. Also bump document create state validation to 2 and document +// replace state validation to 1, adding `refersTo` document reference +// validation (referenced identities and contracts must exist), and introduce +// the `document_reference_validation` feature version. +// v9 remains unchanged for PROTOCOL_VERSION_13 chain replay. pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = DriveAbciValidationVersions { state_transitions: DriveAbciStateTransitionValidationVersions { @@ -182,12 +186,13 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = document_purchase_transition_structure_validation: 0, document_update_price_transition_structure_validation: 0, document_base_transition_state_validation: 0, - document_create_transition_state_validation: 1, + document_create_transition_state_validation: 2, document_delete_transition_state_validation: 0, - document_replace_transition_state_validation: 0, + document_replace_transition_state_validation: 1, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 f4de9851115..58332887853 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 @@ -136,6 +136,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 f517f8f9670..1579c41caca 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 @@ -136,6 +136,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 93bfd9d25a7..e1c030a65c8 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 @@ -139,6 +139,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 afeade81a8a..cc45526c0d4 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 @@ -140,6 +140,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 bb107a9bb96..80c2fb7091e 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 @@ -143,6 +143,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 883c33ede3f..952e6063fc7 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 @@ -137,6 +137,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 69c237d8683..fe3cd4b6bb0 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 @@ -191,6 +191,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 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 d2404d18557..d278784555c 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 @@ -187,6 +187,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, document_update_price_transition_state_validation: 0, + document_reference_validation: 0, token_mint_transition_structure_validation: 0, token_burn_transition_structure_validation: 0, token_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 984a7e46d64..21000350ca7 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -107,9 +107,12 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// `document_create_transition_structure_validation` 0 → 1, requiring a /// contested create transition's prefunded voting balance to name the /// same vote poll the document itself resolves to, and rejecting one on a -/// document that resolves to no contested index. v13 keeps the v9 table -/// and therefore keeps accepting both, so replay of pre-upgrade blocks is -/// unchanged. +/// document that resolves to no contested index. It also bumps document +/// create state validation to 2 and document replace state validation to +/// 1, enforcing `refersTo` document references: a document whose +/// reference property names an identity or contract that does not exist +/// is rejected. v13 keeps the v9 table and therefore keeps +/// accepting all of these, so replay of pre-upgrade blocks is unchanged. /// /// The wire surface is deliberately unchanged: `GetDocumentsRequestV1` /// already carries `selects` / `group_by` / `order_by` / `limit` / @@ -121,7 +124,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V1, methods: DRIVE_ABCI_METHOD_VERSIONS_V9, - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested create transitions must name the contested index they resolve to + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V2, query: DRIVE_ABCI_QUERY_VERSIONS_V2, // changed: ranked HAVING routing gate checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 9701a5407d2..85aaf45d489 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -138,6 +138,7 @@ use crate::errors::consensus::state::document::{ DocumentAlreadyPresentErrorWasm, DocumentNotFoundErrorWasm, DocumentOwnerIdMismatchErrorWasm, DocumentTimestampWindowViolationErrorWasm, DocumentTimestampsMismatchErrorWasm, DuplicateUniqueIndexErrorWasm, InvalidDocumentRevisionErrorWasm, + ReferencedEntityNotFoundErrorWasm, }; use crate::errors::consensus::state::identity::{ IdentityAlreadyExistsErrorWasm, IdentityPublicKeyIsDisabledErrorWasm, @@ -471,6 +472,9 @@ pub fn from_state_error(state_error: &StateError) -> JsValue { StateError::InsufficientShieldedFeeError(e) => { generic_consensus_error!(InsufficientShieldedFeeError, e).into() } + StateError::ReferencedEntityNotFoundError(e) => { + ReferencedEntityNotFoundErrorWasm::from(e).into() + } } } diff --git a/packages/wasm-dpp/src/errors/consensus/state/document/mod.rs b/packages/wasm-dpp/src/errors/consensus/state/document/mod.rs index ac977aac21e..953c755b1e1 100644 --- a/packages/wasm-dpp/src/errors/consensus/state/document/mod.rs +++ b/packages/wasm-dpp/src/errors/consensus/state/document/mod.rs @@ -6,6 +6,7 @@ mod document_timestamps_are_equal_error; mod document_timestamps_mismatch_error; mod duplicate_unique_index_error; mod invalid_document_revision_error; +mod referenced_entity_not_found_error; pub use document_already_present_error::*; pub use document_not_found_error::*; @@ -15,3 +16,4 @@ pub use document_timestamps_are_equal_error::*; pub use document_timestamps_mismatch_error::*; pub use duplicate_unique_index_error::*; pub use invalid_document_revision_error::*; +pub use referenced_entity_not_found_error::*; diff --git a/packages/wasm-dpp/src/errors/consensus/state/document/referenced_entity_not_found_error.rs b/packages/wasm-dpp/src/errors/consensus/state/document/referenced_entity_not_found_error.rs new file mode 100644 index 00000000000..e56bb5d59c7 --- /dev/null +++ b/packages/wasm-dpp/src/errors/consensus/state/document/referenced_entity_not_found_error.rs @@ -0,0 +1,44 @@ +use crate::buffer::Buffer; +use dpp::consensus::codes::ErrorWithCode; +use dpp::consensus::state::document::referenced_entity_not_found_error::ReferencedEntityNotFoundError; +use dpp::consensus::ConsensusError; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name=ReferencedEntityNotFoundError)] +pub struct ReferencedEntityNotFoundErrorWasm { + inner: ReferencedEntityNotFoundError, +} + +impl From<&ReferencedEntityNotFoundError> for ReferencedEntityNotFoundErrorWasm { + fn from(e: &ReferencedEntityNotFoundError) -> Self { + Self { inner: e.clone() } + } +} + +#[wasm_bindgen(js_class=ReferencedEntityNotFoundError)] +impl ReferencedEntityNotFoundErrorWasm { + #[wasm_bindgen(js_name=getEntityId)] + pub fn entity_id(&self) -> Buffer { + Buffer::from_bytes(self.inner.entity_id().as_bytes()) + } + + #[wasm_bindgen(js_name=getEntityType)] + pub fn entity_type(&self) -> String { + self.inner.entity_type().to_string() + } + + #[wasm_bindgen(js_name=getPath)] + pub fn path(&self) -> String { + self.inner.path().to_string() + } + + #[wasm_bindgen(js_name=getCode)] + pub fn get_code(&self) -> u32 { + ConsensusError::from(self.inner.clone()).code() + } + + #[wasm_bindgen(getter)] + pub fn message(&self) -> String { + self.inner.to_string() + } +} From 57de4f3d90cd6e2606444b21ea78cbac59c6beb3 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 04:49:09 +0700 Subject: [PATCH 2/4] refactor(dpp): keep DocumentPropertyType append-only by splitting the reference variant The append-only CI gate rejects any change to existing variants of DocumentPropertyType, so Identifier(Option) is split back into the original unit Identifier variant plus a new IdentifierWithReference(DocumentPropertyReferenceTarget) variant appended at the end of the enum. The reference still lives in the property type itself; a plain identifier and a referencing identifier are now separate variants instead of None/Some. Co-Authored-By: Claude Fable 5 --- .../class_methods/try_from_schema/mod.rs | 15 +- .../document_type/property/mod.rs | 128 ++++++++++-------- .../v0/mod.rs | 3 +- .../document_type/v0/random_document_type.rs | 2 +- .../document_reference_validation/v0/mod.rs | 3 +- packages/rs-drive/src/query/conditions.rs | 22 ++- 6 files changed, 104 insertions(+), 69 deletions(-) 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 c78e4e1c7cd..a644de69053 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 @@ -295,8 +295,8 @@ fn insert_values_nested( } /// Folds a `refersTo` declaration into the property type: an identifier property -/// with `refersTo` becomes `Identifier(Some(target))`. Non-identifier properties -/// cannot carry `refersTo`. +/// with `refersTo` becomes `IdentifierWithReference(target)`. Non-identifier +/// properties cannot carry `refersTo`. fn apply_property_reference( inner_properties: &BTreeMap, property_type: DocumentPropertyType, @@ -305,7 +305,10 @@ fn apply_property_reference( return Ok(property_type); }; - if !matches!(property_type, DocumentPropertyType::Identifier(_)) { + if !matches!( + property_type, + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) + ) { return Err(DataContractError::InvalidContractStructure( "refersTo is only allowed on identifier properties".to_string(), )); @@ -327,7 +330,7 @@ fn apply_property_reference( } }; - Ok(DocumentPropertyType::Identifier(Some(target))) + Ok(DocumentPropertyType::IdentifierWithReference(target)) } #[cfg(test)] @@ -392,7 +395,9 @@ mod tests { assert!(matches!( property_type, - DocumentPropertyType::Identifier(Some(DocumentPropertyReferenceTarget::Identity)) + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::Identity + ) )); } 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 80f5513cf15..779c82a37ed 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 @@ -89,12 +89,13 @@ pub enum DocumentPropertyType { F64, String(StringPropertySizes), ByteArray(ByteArrayPropertySizes), - Identifier(Option), + Identifier, Boolean, Date, Object(IndexMap), Array(ArrayItemType), VariableTypeArray(Vec), + IdentifierWithReference(DocumentPropertyReferenceTarget), } impl DocumentPropertyType { @@ -114,7 +115,7 @@ impl DocumentPropertyType { "f64" | "number" => Ok(DocumentPropertyType::F64), "boolean" => Ok(DocumentPropertyType::Boolean), "date" => Ok(DocumentPropertyType::Date), - "identifier" => Ok(DocumentPropertyType::Identifier(None)), + "identifier" => Ok(DocumentPropertyType::Identifier), "string" => Ok(DocumentPropertyType::String(StringPropertySizes { min_length: None, max_length: None, @@ -150,7 +151,9 @@ impl DocumentPropertyType { DocumentPropertyType::F64 => "f64".to_string(), DocumentPropertyType::String(_) => "string".to_string(), DocumentPropertyType::ByteArray(_) => "byteArray".to_string(), - DocumentPropertyType::Identifier(_) => "identifier".to_string(), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + "identifier".to_string() + } DocumentPropertyType::Boolean => "boolean".to_string(), DocumentPropertyType::Date => "date".to_string(), DocumentPropertyType::Object(_) => "object".to_string(), @@ -188,7 +191,9 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => None, DocumentPropertyType::VariableTypeArray(_) => None, - DocumentPropertyType::Identifier(_) => Some(32), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Some(32) + } } } @@ -233,7 +238,9 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => Ok(None), DocumentPropertyType::VariableTypeArray(_) => Ok(None), - DocumentPropertyType::Identifier(_) => Ok(Some(32)), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Ok(Some(32)) + } } } @@ -278,7 +285,9 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => Ok(None), DocumentPropertyType::VariableTypeArray(_) => Ok(None), - DocumentPropertyType::Identifier(_) => Ok(Some(32)), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Ok(Some(32)) + } } } @@ -311,7 +320,9 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => None, DocumentPropertyType::VariableTypeArray(_) => None, - DocumentPropertyType::Identifier(_) => Some(32), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Some(32) + } } } @@ -445,7 +456,9 @@ impl DocumentPropertyType { } DocumentPropertyType::Array(_) => Value::Null, DocumentPropertyType::VariableTypeArray(_) => Value::Null, - DocumentPropertyType::Identifier(_) => Value::Identifier(rng.gen()), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Value::Identifier(rng.gen()) + } } } @@ -494,7 +507,9 @@ impl DocumentPropertyType { } DocumentPropertyType::Array(_) => Value::Null, DocumentPropertyType::VariableTypeArray(_) => Value::Null, - DocumentPropertyType::Identifier(_) => Value::Identifier(rng.gen()), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Value::Identifier(rng.gen()) + } } } @@ -543,7 +558,9 @@ impl DocumentPropertyType { } DocumentPropertyType::Array(_) => Value::Null, DocumentPropertyType::VariableTypeArray(_) => Value::Null, - DocumentPropertyType::Identifier(_) => Value::Identifier(rng.gen()), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Value::Identifier(rng.gen()) + } } } @@ -719,7 +736,7 @@ impl DocumentPropertyType { } } } - DocumentPropertyType::Identifier(_) => { + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { let mut id = [0; 32]; buf.read_exact(&mut id).map_err(|_| { DataContractError::DecodingContractError(DecodingError::new( @@ -938,7 +955,7 @@ impl DocumentPropertyType { r_vec.append(&mut bytes); Ok(r_vec) } - DocumentPropertyType::Identifier(_) => { + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { let mut bytes = value.into_identifier_bytes()?; let mut r_vec = bytes.len().encode_var_vec(); @@ -1096,7 +1113,9 @@ impl DocumentPropertyType { Ok(r_vec) } }, - DocumentPropertyType::Identifier(_) => Ok(value.to_identifier_bytes()?), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Ok(value.to_identifier_bytes()?) + } DocumentPropertyType::Boolean => { let value_as_boolean = value .as_bool() @@ -1231,9 +1250,11 @@ impl DocumentPropertyType { DocumentPropertyType::ByteArray(_) => { value.to_binary_bytes().map_err(ProtocolError::ValueError) } - DocumentPropertyType::Identifier(_) => value - .to_identifier_bytes() - .map_err(ProtocolError::ValueError), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + value + .to_identifier_bytes() + .map_err(ProtocolError::ValueError) + } DocumentPropertyType::Boolean => { let value_as_boolean = value .as_bool() @@ -1352,7 +1373,7 @@ impl DocumentPropertyType { Ok(Value::Float(float)) } DocumentPropertyType::ByteArray(_) => Ok(Value::Bytes(value.to_vec())), - DocumentPropertyType::Identifier(_) => { + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { let identifier = Identifier::from_bytes(value)?; Ok(identifier.into()) } @@ -1478,12 +1499,14 @@ impl DocumentPropertyType { DataContractError::ValueDecodingError("could not parse hex bytes".to_string()) })?)) } - DocumentPropertyType::Identifier(_) => Ok(Value::Identifier( - Value::Text(str.to_owned()) - .to_identifier() - .map_err(|e| DataContractError::ValueDecodingError(format!("{:?}", e)))? - .into_buffer(), - )), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Ok(Value::Identifier( + Value::Text(str.to_owned()) + .to_identifier() + .map_err(|e| DataContractError::ValueDecodingError(format!("{:?}", e)))? + .into_buffer(), + )) + } DocumentPropertyType::Boolean => { if str.to_lowercase().as_str() == "true" { Ok(Value::Bool(true)) @@ -2162,7 +2185,10 @@ impl DocumentPropertyType { } // Convert hex or base58 strings to identifiers for Identifier fields - (DocumentPropertyType::Identifier(_), Value::Text(str_value)) => { + ( + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_), + Value::Text(str_value), + ) => { // First try base58 decoding (most common for identifiers) if let Ok(id) = Identifier::from_string_unknown_encoding(&str_value) { *value = Value::Identifier(id.into_buffer()); @@ -2416,9 +2442,7 @@ impl DocumentPropertyType { } match value_map.get_optional_str(property_names::CONTENT_MEDIA_TYPE)? { - Some("application/x.dash.dpp.identifier") => { - DocumentPropertyType::Identifier(None) - } + Some("application/x.dash.dpp.identifier") => DocumentPropertyType::Identifier, Some(_) | None => DocumentPropertyType::ByteArray(ByteArrayPropertySizes { min_size: value_map.get_optional_integer(property_names::MIN_ITEMS)?, max_size: value_map.get_optional_integer(property_names::MAX_ITEMS)?, @@ -2581,7 +2605,7 @@ mod tests { }), "byteArray", ), - (DocumentPropertyType::Identifier(None), "identifier"), + (DocumentPropertyType::Identifier, "identifier"), (DocumentPropertyType::Boolean, "boolean"), (DocumentPropertyType::Date, "date"), (DocumentPropertyType::Object(IndexMap::new()), "object"), @@ -2670,7 +2694,7 @@ mod tests { ); assert_eq!( DocumentPropertyType::try_from_name("identifier").unwrap(), - DocumentPropertyType::Identifier(None) + DocumentPropertyType::Identifier ); assert!(DocumentPropertyType::try_from_name("string").is_ok()); assert!(DocumentPropertyType::try_from_name("byteArray").is_ok()); @@ -2708,7 +2732,7 @@ mod tests { assert_eq!(DocumentPropertyType::F64.min_size(), Some(8)); assert_eq!(DocumentPropertyType::Boolean.min_size(), Some(1)); assert_eq!(DocumentPropertyType::Date.min_size(), Some(8)); - assert_eq!(DocumentPropertyType::Identifier(None).min_size(), Some(32)); + assert_eq!(DocumentPropertyType::Identifier.min_size(), Some(32)); } #[test] @@ -2792,7 +2816,7 @@ mod tests { assert_eq!(DocumentPropertyType::F64.max_size(), Some(8)); assert_eq!(DocumentPropertyType::Boolean.max_size(), Some(1)); assert_eq!(DocumentPropertyType::Date.max_size(), Some(8)); - assert_eq!(DocumentPropertyType::Identifier(None).max_size(), Some(32)); + assert_eq!(DocumentPropertyType::Identifier.max_size(), Some(32)); } #[test] @@ -2887,9 +2911,7 @@ mod tests { Some(8) ); assert_eq!( - DocumentPropertyType::Identifier(None) - .min_byte_size(pv) - .unwrap(), + DocumentPropertyType::Identifier.min_byte_size(pv).unwrap(), Some(32) ); } @@ -3072,7 +3094,7 @@ mod tests { assert!(!DocumentPropertyType::F64.is_integer()); assert!(!DocumentPropertyType::Boolean.is_integer()); assert!(!DocumentPropertyType::Date.is_integer()); - assert!(!DocumentPropertyType::Identifier(None).is_integer()); + assert!(!DocumentPropertyType::Identifier.is_integer()); assert!(!DocumentPropertyType::U128.is_integer()); assert!(!DocumentPropertyType::I128.is_integer()); assert!(!DocumentPropertyType::String(StringPropertySizes { @@ -3503,7 +3525,7 @@ mod tests { #[test] fn test_tree_keys_roundtrip_identifier() { - let prop = DocumentPropertyType::Identifier(None); + let prop = DocumentPropertyType::Identifier; let id_bytes: [u8; 32] = [42u8; 32]; let val = Value::Identifier(id_bytes); let enc = prop.encode_value_for_tree_keys(&val).unwrap(); @@ -3623,7 +3645,7 @@ mod tests { #[test] fn test_encode_value_with_size_identifier() { - let prop = DocumentPropertyType::Identifier(None); + let prop = DocumentPropertyType::Identifier; let id_bytes = [1u8; 32]; let result = prop .encode_value_with_size(Value::Identifier(id_bytes), true) @@ -3888,7 +3910,7 @@ mod tests { #[test] fn test_encode_value_ref_with_size_identifier() { - let prop = DocumentPropertyType::Identifier(None); + let prop = DocumentPropertyType::Identifier; let id_bytes = [5u8; 32]; let val = Value::Identifier(id_bytes); let result = prop.encode_value_ref_with_size(&val, true).unwrap(); @@ -4205,7 +4227,7 @@ mod tests { #[test] fn test_read_optionally_from_identifier_required() { use std::io::BufReader; - let prop = DocumentPropertyType::Identifier(None); + let prop = DocumentPropertyType::Identifier; let id_bytes = [7u8; 32]; let mut reader = BufReader::new(id_bytes.as_slice()); let (value, _) = prop.read_optionally_from(&mut reader, true).unwrap(); @@ -4607,7 +4629,7 @@ mod tests { map.insert("contentMediaType".to_string(), &media_type_val); let options = DocumentPropertyTypeParsingOptions::default(); let result = DocumentPropertyType::try_from_value_map(&map, &options).unwrap(); - assert_eq!(result, DocumentPropertyType::Identifier(None)); + assert_eq!(result, DocumentPropertyType::Identifier); } #[test] @@ -5282,7 +5304,7 @@ mod tests { #[test] fn test_encode_value_for_tree_keys_identifier() { - let prop = DocumentPropertyType::Identifier(None); + let prop = DocumentPropertyType::Identifier; let id = [7u8; 32]; let result = prop .encode_value_for_tree_keys(&Value::Identifier(id)) @@ -5387,7 +5409,7 @@ mod tests { #[test] fn test_decode_value_for_tree_keys_identifier() { - let prop = DocumentPropertyType::Identifier(None); + let prop = DocumentPropertyType::Identifier; let id = [7u8; 32]; let decoded = prop.decode_value_for_tree_keys(&id).unwrap(); if let Value::Identifier(decoded_id) = decoded { @@ -5666,9 +5688,7 @@ mod tests { fn test_min_byte_size_identifier() { let pv = PlatformVersion::latest(); assert_eq!( - DocumentPropertyType::Identifier(None) - .min_byte_size(pv) - .unwrap(), + DocumentPropertyType::Identifier.min_byte_size(pv).unwrap(), Some(32) ); } @@ -5677,9 +5697,7 @@ mod tests { fn test_max_byte_size_identifier() { let pv = PlatformVersion::latest(); assert_eq!( - DocumentPropertyType::Identifier(None) - .max_byte_size(pv) - .unwrap(), + DocumentPropertyType::Identifier.max_byte_size(pv).unwrap(), Some(32) ); } @@ -5812,7 +5830,7 @@ mod tests { Value::Float(_) )); assert!(matches!( - DocumentPropertyType::Identifier(None).random_value(&mut rng), + DocumentPropertyType::Identifier.random_value(&mut rng), Value::Identifier(_) )); } @@ -6049,7 +6067,7 @@ mod tests { fn test_random_sub_filled_value_identifier() { let mut rng = StdRng::seed_from_u64(15); assert!(matches!( - DocumentPropertyType::Identifier(None).random_sub_filled_value(&mut rng), + DocumentPropertyType::Identifier.random_sub_filled_value(&mut rng), Value::Identifier(_) )); } @@ -6172,7 +6190,7 @@ mod tests { Value::Float(_) )); assert!(matches!( - DocumentPropertyType::Identifier(None).random_filled_value(&mut rng), + DocumentPropertyType::Identifier.random_filled_value(&mut rng), Value::Identifier(_) )); assert_eq!( @@ -6310,7 +6328,7 @@ mod tests { #[test] fn test_read_optionally_from_identifier_truncated_returns_error() { - let prop = DocumentPropertyType::Identifier(None); + let prop = DocumentPropertyType::Identifier; // Only 16 bytes but identifier needs 32 let data = [1u8; 16]; let mut reader = BufReader::new(data.as_slice()); @@ -7100,9 +7118,9 @@ mod tests { #[test] fn should_serialize_reference_metadata() { let property = DocumentProperty { - property_type: DocumentPropertyType::Identifier(Some( + property_type: DocumentPropertyType::IdentifierWithReference( DocumentPropertyReferenceTarget::Identity, - )), + ), required: false, transient: false, }; @@ -7112,7 +7130,7 @@ mod tests { assert_eq!( value.get("property_type"), Some(&serde_json::json!({ - "Identifier": "identity" + "IdentifierWithReference": "identity" })) ); } diff --git a/packages/rs-dpp/src/data_contract/document_type/schema/find_identifier_and_binary_paths/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/schema/find_identifier_and_binary_paths/v0/mod.rs index 61f31d70abb..a887c8a35ff 100644 --- a/packages/rs-dpp/src/data_contract/document_type/schema/find_identifier_and_binary_paths/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/schema/find_identifier_and_binary_paths/v0/mod.rs @@ -29,7 +29,8 @@ impl DocumentTypeV0 { }; match &value.property_type { - DocumentPropertyType::Identifier(_) => { + DocumentPropertyType::Identifier + | DocumentPropertyType::IdentifierWithReference(_) => { identifier_paths.insert(new_path); } DocumentPropertyType::ByteArray(_) => { diff --git a/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs b/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs index d35297a9ab8..17fdc0fe3a4 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs @@ -325,7 +325,7 @@ impl DocumentTypeV0 { schema.insert("byteArray".to_string(), serde_json::Value::Bool(true)); serde_json::Value::Object(schema) }, - DocumentPropertyType::Identifier(_) => { + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { json!({ "type": "array", "items": { 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 152973718fe..d3b026d6462 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 @@ -101,7 +101,8 @@ fn validate_document_type_references_v0( } } - let DocumentPropertyType::Identifier(Some(reference_target)) = &property.property_type + let DocumentPropertyType::IdentifierWithReference(reference_target) = + &property.property_type else { continue; }; diff --git a/packages/rs-drive/src/query/conditions.rs b/packages/rs-drive/src/query/conditions.rs index 7a87ee015b5..ef7f928608a 100644 --- a/packages/rs-drive/src/query/conditions.rs +++ b/packages/rs-drive/src/query/conditions.rs @@ -1476,7 +1476,7 @@ impl<'a> WhereClause { use DocumentPropertyType as T; match prop_ty { T::String(_) => matches!(v, Value::Text(_)), - T::Identifier(_) => matches!(v, Value::Identifier(_)), + T::Identifier | T::IdentifierWithReference(_) => matches!(v, Value::Identifier(_)), T::Boolean => matches!(v, Value::Bool(_)), T::ByteArray(_) => matches!(v, Value::Bytes(_)), T::F64 => matches!(v, Value::Float(_)), @@ -1547,7 +1547,9 @@ impl<'a> WhereClause { | Value::I8(_) ), T::String(_) => matches!(self.value, Value::Text(_)), - T::Identifier(_) => matches!(self.value, Value::Identifier(_)), + T::Identifier | T::IdentifierWithReference(_) => { + matches!(self.value, Value::Identifier(_)) + } T::ByteArray(_) => matches!(self.value, Value::Bytes(_)), T::Boolean => matches!(self.value, Value::Bool(_)), // Not applicable for object/array/variable arrays @@ -1642,7 +1644,9 @@ pub fn allowed_ops_for_type(property_type: &DocumentPropertyType) -> &'static [W BetweenExcludeLeft, BetweenExcludeRight, ], - DocumentPropertyType::Identifier(_) => &[Equal, In], + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + &[Equal, In] + } DocumentPropertyType::ByteArray(_) => &[Equal, In], DocumentPropertyType::Boolean => &[Equal], DocumentPropertyType::Object(_) @@ -1675,7 +1679,7 @@ fn meta_field_property_type(field: &str) -> Option { match field { // Identifiers "$id" | "$ownerId" | "$dataContractId" | "$creatorId" => { - Some(DocumentPropertyType::Identifier(None)) + Some(DocumentPropertyType::Identifier) } // Dates (millis since epoch) "$createdAt" | "$updatedAt" | "$transferredAt" => Some(DocumentPropertyType::Date), @@ -3440,7 +3444,13 @@ mod tests { for field in ["$id", "$ownerId", "$dataContractId", "$creatorId"] { let pt = meta_field_property_type(field); assert!( - matches!(pt, Some(DocumentPropertyType::Identifier(_))), + matches!( + pt, + Some( + DocumentPropertyType::Identifier + | DocumentPropertyType::IdentifierWithReference(_) + ) + ), "expected Identifier for {field}" ); } @@ -3587,7 +3597,7 @@ mod tests { use super::allowed_ops_for_type; use dpp::data_contract::document_type::DocumentPropertyType; - let ops = allowed_ops_for_type(&DocumentPropertyType::Identifier(None)); + let ops = allowed_ops_for_type(&DocumentPropertyType::Identifier); assert_eq!(ops, &[Equal, In]); } From c9ab42b1fc0dcfa49ae2b16f820f5aab8c2f9836 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 05:20:25 +0700 Subject: [PATCH 3/4] refactor(dpp): gate refersTo folding to parser generation 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apply_property_reference ran unconditionally in the shared insert_values helpers, so every try_from_schema generation folded (or hard-rejected) the refersTo keyword. Pre-generation-3 parses must stay byte-for-byte identical to what they produced before the keyword existed: their meta-schemas reject refersTo under full validation, but the non-validating parse path would have started folding it or erroring on it. The fold is now gated by a new admit_property_references flag on ParserGeneration, set by each generation from its own constants — false for generations 0-2 (keyword ignored entirely), true for generation 3. Regression tests pin the PV13 parse: refersTo is ignored on identifier properties and no longer errors on non-identifier ones. Co-Authored-By: Claude Fable 5 --- .../try_from_schema/common/mod.rs | 9 ++ .../class_methods/try_from_schema/mod.rs | 92 ++++++++++++++++++- .../class_methods/try_from_schema/v0/mod.rs | 3 + .../class_methods/try_from_schema/v1/mod.rs | 2 + .../class_methods/try_from_schema/v3/mod.rs | 4 + 5 files changed, 107 insertions(+), 3 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 29f77e22a4a..9491f2e2ff9 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -154,6 +154,13 @@ pub(super) struct ParserGeneration { /// exist from protocol v12 onward, so the driver passes `false` below /// that boundary and the index is rejected with `UnsupportedFeatureError`. pub admit_count_indexes: bool, + /// Whether the `refersTo` reference keyword is part of this generation's + /// grammar. When `false` the keyword is ignored entirely, exactly as a + /// node that predated it did — meta-schemas v0–v2 already reject it when + /// full validation runs, so this flag keeps the non-validating parse of + /// pre-generation-3 contracts byte-for-byte identical to what those + /// generations produced. + pub admit_property_references: bool, /// Method name reported by the `UnknownVersionMismatch` raised for an /// unknown `document_type_schema`. Differs per generation, so it is a /// parameter rather than a constant. @@ -714,6 +721,7 @@ fn parse_document_properties( property_value, root_schema, ctx.data_contact_config, + ctx.generation.admit_property_references, ) .map_err(consensus_or_protocol_data_contract_error)?; @@ -725,6 +733,7 @@ fn parse_document_properties( property_value, root_schema, ctx.data_contact_config, + ctx.generation.admit_property_references, ) .map_err(consensus_or_protocol_data_contract_error)?; } 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 a644de69053..e30d42a8443 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 @@ -122,6 +122,7 @@ fn insert_values( property_value: &Value, root_schema: &Value, config: &DataContractConfig, + admit_property_references: bool, ) -> Result<(), DataContractError> { let mut to_visit: Vec<(Option, String, &Value)> = vec![(prefix, property_key, property_value)]; @@ -170,7 +171,11 @@ fn insert_values( } } property_type => { - let property_type = apply_property_reference(&inner_properties, property_type)?; + let property_type = if admit_property_references { + apply_property_reference(&inner_properties, property_type)? + } else { + property_type + }; document_properties.insert( prefixed_property_key, DocumentProperty { @@ -187,6 +192,7 @@ fn insert_values( } // TODO: This is quite big +#[allow(clippy::too_many_arguments)] fn insert_values_nested( document_properties: &mut IndexMap, known_required: &BTreeSet, @@ -195,6 +201,7 @@ fn insert_values_nested( property_value: &Value, root_schema: &Value, config: &DataContractConfig, + admit_property_references: bool, ) -> Result<(), DataContractError> { let mut inner_properties = property_value.to_btree_ref_string_map()?; @@ -271,6 +278,7 @@ fn insert_values_nested( object_property_value, root_schema, config, + admit_property_references, )?; } } @@ -280,7 +288,11 @@ fn insert_values_nested( property_type => property_type, }; - let property_type = apply_property_reference(&inner_properties, property_type)?; + let property_type = if admit_property_references { + apply_property_reference(&inner_properties, property_type)? + } else { + property_type + }; document_properties.insert( property_key, @@ -297,6 +309,11 @@ fn insert_values_nested( /// Folds a `refersTo` declaration into the property type: an identifier property /// with `refersTo` becomes `IdentifierWithReference(target)`. Non-identifier /// properties cannot carry `refersTo`. +/// +/// Only generation 3 admits the keyword (`admit_property_references`); the +/// callers above skip this fold entirely for earlier generations, which parsed +/// before the keyword existed and must keep producing exactly what they always +/// produced. fn apply_property_reference( inner_properties: &BTreeMap, property_type: DocumentPropertyType, @@ -343,7 +360,13 @@ mod tests { fn try_document_type_from_schema( schema: serde_json::Value, ) -> Result { - let platform_version = PlatformVersion::latest(); + try_document_type_from_schema_on_version(schema, PlatformVersion::latest()) + } + + fn try_document_type_from_schema_on_version( + schema: serde_json::Value, + platform_version: &PlatformVersion, + ) -> Result { let config = DataContractConfig::default_for_version(platform_version).expect("config should build"); @@ -423,4 +446,67 @@ mod tests { "unexpected error: {message}" ); } + + #[test] + fn should_ignore_refers_to_on_pre_generation_3_parse() { + // Generations before 3 predate the `refersTo` keyword: even if it + // appears in a schema they parse (only possible without full + // validation — their meta-schemas reject it), they must ignore it + // and keep producing the plain identifier type they always produced. + let platform_version = PlatformVersion::get(13).expect("platform version 13 should exist"); + + let document_type = try_document_type_from_schema_on_version( + json!({ + "type": "object", + "properties": { + "toUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "identity" + } + } + }, + "required": [], + "additionalProperties": false + }), + platform_version, + ) + .expect("should parse"); + + let property_type = document_type + .as_ref() + .flattened_properties() + .get("toUserId") + .map(|p| p.property_type.clone()) + .expect("property should be present"); + + assert!(matches!(property_type, DocumentPropertyType::Identifier)); + } + + #[test] + fn should_not_reject_refers_to_on_non_identifier_property_on_pre_generation_3_parse() { + let platform_version = PlatformVersion::get(13).expect("platform version 13 should exist"); + + try_document_type_from_schema_on_version( + json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "position": 0, + "refersTo": { "type": "identity" } + } + }, + "required": [], + "additionalProperties": false + }), + platform_version, + ) + .expect("pre-generation-3 parse should ignore refersTo entirely"); + } } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs index 7220a565d92..4c29edfa4ae 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs @@ -246,6 +246,8 @@ impl DocumentTypeV0 { property_value, &root_schema, data_contact_config, + // Generation 0 predates the `refersTo` keyword + false, ) .map_err(consensus_or_protocol_data_contract_error)?; @@ -257,6 +259,7 @@ impl DocumentTypeV0 { property_value, &root_schema, data_contact_config, + false, ) .map_err(consensus_or_protocol_data_contract_error)?; } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs index 41a56c52db9..deecc77ba97 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs @@ -93,6 +93,8 @@ impl DocumentTypeV1 { document_type_schema_version, admit_history: document_type_schema_version >= 2, admit_count_indexes: platform_version.protocol_version >= 12, + // The `refersTo` reference keyword arrived with generation 3 + admit_property_references: false, meta_schema_method_name: "DocumentTypeV1::try_from_schema (document_type_schema)", // RANKED: generation 1 predates the ranked aggregates entirely // — its index grammar has no `ranked*` keywords, and it diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs index 30ae2b94db9..5e7ef3ee8a9 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs @@ -242,6 +242,10 @@ fn try_from_schema_generation_3( // Count indexes arrived at PV12; every version selecting this // generation is far past that boundary. admit_count_indexes: true, + // Meta-schema v3 introduces the `refersTo` reference keyword, so + // this generation folds it into the property type; earlier + // generations ignore it, exactly as they did before it existed. + admit_property_references: true, meta_schema_method_name: "DocumentType::try_from_schema_v3 (document_type_schema)", // RANKED: the constants that make this generation 3. admit_ranked: true, From f0c03d793346d2d56896196911b1e205a31e10e2 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 05:34:40 +0700 Subject: [PATCH 4/4] refactor(dpp): version apply_property_reference through the platform version tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the admit_property_references flag on ParserGeneration with the standard versioned-method pattern: DocumentTypeSchemaVersions gains an apply_property_reference OptionalFeatureVersion (None in contract versions 1-5, Some(0) in CONTRACT_VERSIONS_V6), insert_values and insert_values_nested take the platform version, and apply_property_reference dispatches on the table value — None ignores the keyword exactly as versions predating it did, Some(0) folds it via apply_property_reference_v0. Co-Authored-By: Claude Fable 5 --- .../try_from_schema/common/mod.rs | 11 +--- .../class_methods/try_from_schema/mod.rs | 63 ++++++++++++------- .../class_methods/try_from_schema/v0/mod.rs | 5 +- .../class_methods/try_from_schema/v1/mod.rs | 2 - .../class_methods/try_from_schema/v3/mod.rs | 4 -- .../dpp_versions/dpp_contract_versions/mod.rs | 6 +- .../dpp_versions/dpp_contract_versions/v1.rs | 2 + .../dpp_versions/dpp_contract_versions/v2.rs | 2 + .../dpp_versions/dpp_contract_versions/v3.rs | 2 + .../dpp_versions/dpp_contract_versions/v4.rs | 2 + .../dpp_versions/dpp_contract_versions/v5.rs | 2 + .../dpp_versions/dpp_contract_versions/v6.rs | 1 + 12 files changed, 59 insertions(+), 43 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 9491f2e2ff9..d54aac7b187 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -154,13 +154,6 @@ pub(super) struct ParserGeneration { /// exist from protocol v12 onward, so the driver passes `false` below /// that boundary and the index is rejected with `UnsupportedFeatureError`. pub admit_count_indexes: bool, - /// Whether the `refersTo` reference keyword is part of this generation's - /// grammar. When `false` the keyword is ignored entirely, exactly as a - /// node that predated it did — meta-schemas v0–v2 already reject it when - /// full validation runs, so this flag keeps the non-validating parse of - /// pre-generation-3 contracts byte-for-byte identical to what those - /// generations produced. - pub admit_property_references: bool, /// Method name reported by the `UnknownVersionMismatch` raised for an /// unknown `document_type_schema`. Differs per generation, so it is a /// parameter rather than a constant. @@ -721,7 +714,7 @@ fn parse_document_properties( property_value, root_schema, ctx.data_contact_config, - ctx.generation.admit_property_references, + ctx.platform_version, ) .map_err(consensus_or_protocol_data_contract_error)?; @@ -733,7 +726,7 @@ fn parse_document_properties( property_value, root_schema, ctx.data_contact_config, - ctx.generation.admit_property_references, + ctx.platform_version, ) .map_err(consensus_or_protocol_data_contract_error)?; } 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 e30d42a8443..63abaf8496c 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 @@ -122,7 +122,7 @@ fn insert_values( property_value: &Value, root_schema: &Value, config: &DataContractConfig, - admit_property_references: bool, + platform_version: &PlatformVersion, ) -> Result<(), DataContractError> { let mut to_visit: Vec<(Option, String, &Value)> = vec![(prefix, property_key, property_value)]; @@ -171,11 +171,8 @@ fn insert_values( } } property_type => { - let property_type = if admit_property_references { - apply_property_reference(&inner_properties, property_type)? - } else { - property_type - }; + let property_type = + apply_property_reference(&inner_properties, property_type, platform_version)?; document_properties.insert( prefixed_property_key, DocumentProperty { @@ -201,7 +198,7 @@ fn insert_values_nested( property_value: &Value, root_schema: &Value, config: &DataContractConfig, - admit_property_references: bool, + platform_version: &PlatformVersion, ) -> Result<(), DataContractError> { let mut inner_properties = property_value.to_btree_ref_string_map()?; @@ -278,7 +275,7 @@ fn insert_values_nested( object_property_value, root_schema, config, - admit_property_references, + platform_version, )?; } } @@ -288,11 +285,8 @@ fn insert_values_nested( property_type => property_type, }; - let property_type = if admit_property_references { - apply_property_reference(&inner_properties, property_type)? - } else { - property_type - }; + let property_type = + apply_property_reference(&inner_properties, property_type, platform_version)?; document_properties.insert( property_key, @@ -310,13 +304,33 @@ fn insert_values_nested( /// with `refersTo` becomes `IdentifierWithReference(target)`. Non-identifier /// properties cannot carry `refersTo`. /// -/// Only generation 3 admits the keyword (`admit_property_references`); the -/// callers above skip this fold entirely for earlier generations, which parsed -/// before the keyword existed and must keep producing exactly what they always -/// produced. +/// Versioned on `apply_property_reference` in the platform version's document +/// type schema versions. `None` selects the behavior of the versions that +/// predate the keyword: it is ignored entirely, so their parses stay +/// byte-for-byte identical to what they always produced. fn apply_property_reference( inner_properties: &BTreeMap, property_type: DocumentPropertyType, + platform_version: &PlatformVersion, +) -> Result { + match platform_version + .dpp + .contract_versions + .document_type_versions + .schema + .apply_property_reference + { + None => Ok(property_type), + Some(0) => apply_property_reference_v0(inner_properties, property_type), + Some(version) => Err(DataContractError::Unsupported(format!( + "apply_property_reference version {version} is not supported" + ))), + } +} + +fn apply_property_reference_v0( + inner_properties: &BTreeMap, + property_type: DocumentPropertyType, ) -> Result { let Some(refers_to_value) = inner_properties.get(property_names::REFERS_TO) else { return Ok(property_type); @@ -448,11 +462,12 @@ mod tests { } #[test] - fn should_ignore_refers_to_on_pre_generation_3_parse() { - // Generations before 3 predate the `refersTo` keyword: even if it - // appears in a schema they parse (only possible without full - // validation — their meta-schemas reject it), they must ignore it - // and keep producing the plain identifier type they always produced. + fn should_ignore_refers_to_on_platform_versions_predating_it() { + // Platform versions whose tables carry `apply_property_reference: None` + // predate the `refersTo` keyword: even if it appears in a schema they + // parse (only possible without full validation — their meta-schemas + // reject it), they must ignore it and keep producing the plain + // identifier type they always produced. let platform_version = PlatformVersion::get(13).expect("platform version 13 should exist"); let document_type = try_document_type_from_schema_on_version( @@ -489,7 +504,7 @@ mod tests { } #[test] - fn should_not_reject_refers_to_on_non_identifier_property_on_pre_generation_3_parse() { + fn should_not_reject_refers_to_on_non_identifier_property_on_platform_versions_predating_it() { let platform_version = PlatformVersion::get(13).expect("platform version 13 should exist"); try_document_type_from_schema_on_version( @@ -507,6 +522,6 @@ mod tests { }), platform_version, ) - .expect("pre-generation-3 parse should ignore refersTo entirely"); + .expect("a parse predating refersTo should ignore the keyword entirely"); } } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs index 4c29edfa4ae..686b8a42e76 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs @@ -246,8 +246,7 @@ impl DocumentTypeV0 { property_value, &root_schema, data_contact_config, - // Generation 0 predates the `refersTo` keyword - false, + platform_version, ) .map_err(consensus_or_protocol_data_contract_error)?; @@ -259,7 +258,7 @@ impl DocumentTypeV0 { property_value, &root_schema, data_contact_config, - false, + platform_version, ) .map_err(consensus_or_protocol_data_contract_error)?; } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs index deecc77ba97..41a56c52db9 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs @@ -93,8 +93,6 @@ impl DocumentTypeV1 { document_type_schema_version, admit_history: document_type_schema_version >= 2, admit_count_indexes: platform_version.protocol_version >= 12, - // The `refersTo` reference keyword arrived with generation 3 - admit_property_references: false, meta_schema_method_name: "DocumentTypeV1::try_from_schema (document_type_schema)", // RANKED: generation 1 predates the ranked aggregates entirely // — its index grammar has no `ranked*` keywords, and it diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs index 5e7ef3ee8a9..30ae2b94db9 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs @@ -242,10 +242,6 @@ fn try_from_schema_generation_3( // Count indexes arrived at PV12; every version selecting this // generation is far past that boundary. admit_count_indexes: true, - // Meta-schema v3 introduces the `refersTo` reference keyword, so - // this generation folds it into the property type; earlier - // generations ignore it, exactly as they did before it existed. - admit_property_references: true, meta_schema_method_name: "DocumentType::try_from_schema_v3 (document_type_schema)", // RANKED: the constants that make this generation 3. admit_ranked: true, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs index 13825ccf8f3..e66bb564083 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs @@ -1,4 +1,4 @@ -use versioned_feature_core::{FeatureVersion, FeatureVersionBounds}; +use versioned_feature_core::{FeatureVersion, FeatureVersionBounds, OptionalFeatureVersion}; pub mod v1; pub mod v2; pub mod v3; @@ -78,6 +78,10 @@ pub struct DocumentTypeSchemaVersions { pub should_add_creator_id: FeatureVersion, pub enrich_with_base_schema: FeatureVersion, pub find_identifier_and_binary_paths: FeatureVersion, + /// Folds the `refersTo` reference keyword into the parsed property type. + /// `None` on versions that predate the keyword: they ignore it entirely, + /// exactly as they parsed before it existed. + pub apply_property_reference: OptionalFeatureVersion, pub validate_max_depth: FeatureVersion, pub max_depth: u16, pub recursive_schema_validator_versions: RecursiveSchemaValidatorVersions, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs index e4761c569c9..156436fbd24 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs @@ -41,6 +41,8 @@ pub const CONTRACT_VERSIONS_V1: DPPContractVersions = DPPContractVersions { should_add_creator_id: 0, enrich_with_base_schema: 0, find_identifier_and_binary_paths: 0, + // This version predates the `refersTo` reference keyword + apply_property_reference: None, validate_max_depth: 0, max_depth: 256, recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs index 1928ac74b0c..45a29d6b353 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs @@ -41,6 +41,8 @@ pub const CONTRACT_VERSIONS_V2: DPPContractVersions = DPPContractVersions { should_add_creator_id: 0, enrich_with_base_schema: 0, find_identifier_and_binary_paths: 0, + // This version predates the `refersTo` reference keyword + apply_property_reference: None, validate_max_depth: 0, max_depth: 256, recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs index 9e50775c78a..143f2719c93 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs @@ -43,6 +43,8 @@ pub const CONTRACT_VERSIONS_V3: DPPContractVersions = DPPContractVersions { should_add_creator_id: 1, //changed enrich_with_base_schema: 0, find_identifier_and_binary_paths: 0, + // This version predates the `refersTo` reference keyword + apply_property_reference: None, validate_max_depth: 0, max_depth: 256, recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs index eaba292f1a3..fd12634279e 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs @@ -43,6 +43,8 @@ pub const CONTRACT_VERSIONS_V4: DPPContractVersions = DPPContractVersions { should_add_creator_id: 1, enrich_with_base_schema: 1, // changed: inject v1 schema URI find_identifier_and_binary_paths: 0, + // This version predates the `refersTo` reference keyword + apply_property_reference: None, validate_max_depth: 0, max_depth: 256, recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs index 41ebed1f4fd..1ca91eedd68 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs @@ -45,6 +45,8 @@ pub const CONTRACT_VERSIONS_V5: DPPContractVersions = DPPContractVersions { should_add_creator_id: 1, enrich_with_base_schema: 1, find_identifier_and_binary_paths: 0, + // This version predates the `refersTo` reference keyword + apply_property_reference: None, validate_max_depth: 0, max_depth: 256, recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs index 712faccc3de..f61a7db2c4b 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs @@ -68,6 +68,7 @@ pub const CONTRACT_VERSIONS_V6: DPPContractVersions = DPPContractVersions { should_add_creator_id: 1, enrich_with_base_schema: 1, find_identifier_and_binary_paths: 0, + apply_property_reference: Some(0), // changed: the meta-schema v3 `refersTo` keyword is folded into the parsed property type; None before this version means the keyword is ignored, as it was before it existed validate_max_depth: 0, max_depth: 256, recursive_schema_validator_versions: RecursiveSchemaValidatorVersions {