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/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 29f77e22a4a..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 @@ -714,6 +714,7 @@ fn parse_document_properties( property_value, root_schema, ctx.data_contact_config, + ctx.platform_version, ) .map_err(consensus_or_protocol_data_contract_error)?; @@ -725,6 +726,7 @@ fn parse_document_properties( property_value, root_schema, ctx.data_contact_config, + 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 3bbc576e687..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 @@ -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}; @@ -121,6 +122,7 @@ fn insert_values( property_value: &Value, root_schema: &Value, config: &DataContractConfig, + platform_version: &PlatformVersion, ) -> Result<(), DataContractError> { let mut to_visit: Vec<(Option, String, &Value)> = vec![(prefix, property_key, property_value)]; @@ -169,6 +171,8 @@ fn insert_values( } } property_type => { + let property_type = + apply_property_reference(&inner_properties, property_type, platform_version)?; document_properties.insert( prefixed_property_key, DocumentProperty { @@ -185,6 +189,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, @@ -193,6 +198,7 @@ fn insert_values_nested( property_value: &Value, root_schema: &Value, config: &DataContractConfig, + platform_version: &PlatformVersion, ) -> Result<(), DataContractError> { let mut inner_properties = property_value.to_btree_ref_string_map()?; @@ -269,6 +275,7 @@ fn insert_values_nested( object_property_value, root_schema, config, + platform_version, )?; } } @@ -278,6 +285,9 @@ fn insert_values_nested( property_type => property_type, }; + let property_type = + apply_property_reference(&inner_properties, property_type, platform_version)?; + document_properties.insert( property_key, DocumentProperty { @@ -289,3 +299,229 @@ fn insert_values_nested( Ok(()) } + +/// Folds a `refersTo` declaration into the property type: an identifier property +/// with `refersTo` becomes `IdentifierWithReference(target)`. Non-identifier +/// properties cannot carry `refersTo`. +/// +/// 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); + }; + + if !matches!( + property_type, + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) + ) { + 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::IdentifierWithReference(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 { + 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"); + + 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::IdentifierWithReference( + 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}" + ); + } + + #[test] + 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( + 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_platform_versions_predating_it() { + 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("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 7220a565d92..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,6 +246,7 @@ impl DocumentTypeV0 { property_value, &root_schema, data_contact_config, + platform_version, ) .map_err(consensus_or_protocol_data_contract_error)?; @@ -257,6 +258,7 @@ impl DocumentTypeV0 { property_value, &root_schema, data_contact_config, + platform_version, ) .map_err(consensus_or_protocol_data_contract_error)?; } 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..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 @@ -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 { @@ -73,6 +95,7 @@ pub enum DocumentPropertyType { Object(IndexMap), Array(ArrayItemType), VariableTypeArray(Vec), + IdentifierWithReference(DocumentPropertyReferenceTarget), } impl DocumentPropertyType { @@ -128,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(), @@ -166,7 +191,9 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => None, DocumentPropertyType::VariableTypeArray(_) => None, - DocumentPropertyType::Identifier => Some(32), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Some(32) + } } } @@ -211,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)) + } } } @@ -256,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)) + } } } @@ -289,7 +320,9 @@ impl DocumentPropertyType { .sum(), DocumentPropertyType::Array(_) => None, DocumentPropertyType::VariableTypeArray(_) => None, - DocumentPropertyType::Identifier => Some(32), + DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => { + Some(32) + } } } @@ -423,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()) + } } } @@ -472,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()) + } } } @@ -521,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()) + } } } @@ -697,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( @@ -916,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(); @@ -1074,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() @@ -1209,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() @@ -1330,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()) } @@ -1456,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)) @@ -2140,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()); @@ -7066,4 +7114,24 @@ mod tests { assert!(window[0] < window[1]); } } + + #[test] + fn should_serialize_reference_metadata() { + let property = DocumentProperty { + property_type: DocumentPropertyType::IdentifierWithReference( + 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!({ + "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 3fc2259f7ef..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 b1c1d0b9450..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-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..d3b026d6462 --- /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,200 @@ +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::IdentifierWithReference(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..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(_) @@ -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}" ); } 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/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 { 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() + } +}