From ebe90e2cc4fa0f7425c1aaac8d52f679c295ae20 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 29 May 2025 15:42:21 +0200 Subject: [PATCH 1/3] feat: add DataContractMismatch enum for detailed contract comparison --- .../data_contract/serialized_version/mod.rs | 134 ++++++++++++++---- .../v0/mod.rs | 12 +- 2 files changed, 111 insertions(+), 35 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs index d8d85fd6dfb..81f643ab481 100644 --- a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs +++ b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs @@ -1,18 +1,16 @@ -use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0; -use crate::data_contract::{ - DataContract, DefinitionName, DocumentName, GroupContractPosition, TokenContractPosition, - EMPTY_GROUPS, EMPTY_TOKENS, -}; -use crate::version::PlatformVersion; -use std::collections::BTreeMap; - use super::EMPTY_KEYWORDS; use crate::data_contract::associated_token::token_configuration::TokenConfiguration; use crate::data_contract::group::Group; +use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0; use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1; use crate::data_contract::v0::DataContractV0; use crate::data_contract::v1::DataContractV1; +use crate::data_contract::{ + DataContract, DefinitionName, DocumentName, GroupContractPosition, TokenContractPosition, + EMPTY_GROUPS, EMPTY_TOKENS, +}; use crate::validation::operations::ProtocolValidationOperation; +use crate::version::PlatformVersion; use crate::ProtocolError; use bincode::{Decode, Encode}; use derive_more::From; @@ -21,6 +19,8 @@ use platform_version::{IntoPlatformVersioned, TryFromPlatformVersioned}; use platform_versioning::PlatformVersioned; #[cfg(feature = "data-contract-serde-conversion")] use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fmt; pub(in crate::data_contract) mod v0; pub(in crate::data_contract) mod v1; @@ -34,6 +34,59 @@ pub mod property_names { pub const CONTRACT_DESERIALIZATION_LIMIT: usize = 15000; +/// Represents a field mismatch between two `DataContractInSerializationFormat::V1` +/// variants, or indicates a format version mismatch. +/// +/// Used to diagnose why two data contracts are not considered equal +/// when ignoring auto-generated fields. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum DataContractMismatch { + /// The `id` fields are not equal. + Id, + /// The `config` fields are not equal. + Config, + /// The `version` fields are not equal. + Version, + /// The `owner_id` fields are not equal. + OwnerId, + /// The `schema_defs` fields are not equal. + SchemaDefs, + /// The `document_schemas` fields are not equal. + DocumentSchemas, + /// The `groups` fields are not equal. + Groups, + /// The `tokens` fields are not equal. + Tokens, + /// The `keywords` fields are not equal. + Keywords, + /// The `description` fields are not equal. + Description, + /// The two variants are of different serialization formats (e.g., V0 vs V1). + FormatVersionMismatch, +} + +impl fmt::Display for DataContractMismatch { + /// Formats the enum into a human-readable string describing the mismatch. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let description = match self { + DataContractMismatch::Id => "ID fields differ", + DataContractMismatch::Config => "Config fields differ", + DataContractMismatch::Version => "Version fields differ", + DataContractMismatch::OwnerId => "Owner ID fields differ", + DataContractMismatch::SchemaDefs => "Schema definitions differ", + DataContractMismatch::DocumentSchemas => "Document schemas differ", + DataContractMismatch::Groups => "Groups differ", + DataContractMismatch::Tokens => "Tokens differ", + DataContractMismatch::Keywords => "Keywords differ", + DataContractMismatch::Description => "Description fields differ", + DataContractMismatch::FormatVersionMismatch => { + "Serialization format versions differ (e.g., V0 vs V1)" + } + }; + write!(f, "{}", description) + } +} + #[derive(Debug, Clone, Encode, Decode, PartialEq, PlatformVersioned, From)] #[cfg_attr( feature = "data-contract-serde-conversion", @@ -112,36 +165,59 @@ impl DataContractInSerializationFormat { } } - pub fn eq_without_auto_fields(&self, other: &Self) -> bool { + /// Compares `self` to another `DataContractInSerializationFormat` instance + /// and returns the first mismatching field, if any. + /// + /// This comparison ignores auto-generated fields and is only sensitive to + /// significant differences in contract content. For V0 formats, any difference + /// results in a generic mismatch. For differing format versions (V0 vs V1), + /// a `FormatVersionMismatch` is returned. + /// + /// # Returns + /// + /// - `None` if the contracts are equal according to the relevant fields. + /// - `Some(DataContractMismatch)` indicating the first field where they differ. + pub fn first_mismatch(&self, other: &Self) -> Option { match (self, other) { ( DataContractInSerializationFormat::V0(v0_self), DataContractInSerializationFormat::V0(v0_other), - ) => v0_self == v0_other, + ) => { + if v0_self != v0_other { + Some(DataContractMismatch::FormatVersionMismatch) + } else { + None + } + } ( DataContractInSerializationFormat::V1(v1_self), DataContractInSerializationFormat::V1(v1_other), ) => { - v1_self.id == v1_other.id - && v1_self.config == v1_other.config - && v1_self.version == v1_other.version - && v1_self.owner_id == v1_other.owner_id - && v1_self.schema_defs == v1_other.schema_defs - && v1_self.document_schemas == v1_other.document_schemas - && v1_self.groups == v1_other.groups - && v1_self.tokens == v1_other.tokens - && v1_self.keywords == v1_other.keywords - && v1_self.description == v1_other.description + if v1_self.id != v1_other.id { + Some(DataContractMismatch::Id) + } else if v1_self.config != v1_other.config { + Some(DataContractMismatch::Config) + } else if v1_self.version != v1_other.version { + Some(DataContractMismatch::Version) + } else if v1_self.owner_id != v1_other.owner_id { + Some(DataContractMismatch::OwnerId) + } else if v1_self.schema_defs != v1_other.schema_defs { + Some(DataContractMismatch::SchemaDefs) + } else if v1_self.document_schemas != v1_other.document_schemas { + Some(DataContractMismatch::DocumentSchemas) + } else if v1_self.groups != v1_other.groups { + Some(DataContractMismatch::Groups) + } else if v1_self.tokens != v1_other.tokens { + Some(DataContractMismatch::Tokens) + } else if v1_self.keywords != v1_other.keywords { + Some(DataContractMismatch::Keywords) + } else if v1_self.description != v1_other.description { + Some(DataContractMismatch::Description) + } else { + None + } } - // Cross-version comparisons return false - ( - DataContractInSerializationFormat::V0(_), - DataContractInSerializationFormat::V1(_), - ) - | ( - DataContractInSerializationFormat::V1(_), - DataContractInSerializationFormat::V0(_), - ) => false, + _ => Some(DataContractMismatch::FormatVersionMismatch), } } } diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 439419461c3..f64e731988c 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -81,10 +81,10 @@ impl Drive { .clone() .try_into_platform_versioned(platform_version)?; - if !contract_for_serialization - .eq_without_auto_fields(data_contract_create.data_contract()) + if let Some(mismatch) = + contract_for_serialization.first_mismatch(data_contract_create.data_contract()) { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after create with id {}", data_contract_create.data_contract().id())))); + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after create with id {}: {}", data_contract_create.data_contract().id(), mismatch)))); } Ok((root_hash, VerifiedDataContract(contract))) @@ -103,10 +103,10 @@ impl Drive { let contract_for_serialization: DataContractInSerializationFormat = contract .clone() .try_into_platform_versioned(platform_version)?; - if !contract_for_serialization - .eq_without_auto_fields(data_contract_update.data_contract()) + if let Some(mismatch) = + contract_for_serialization.first_mismatch(data_contract_update.data_contract()) { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after update with id {}", data_contract_update.data_contract().id())))); + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after update with id {}: {}", data_contract_update.data_contract().id(), mismatch)))); } Ok((root_hash, VerifiedDataContract(contract))) } From 417b4e09d3386015dd4692d57c863a8a7ea6b9aa Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 29 May 2025 15:45:08 +0200 Subject: [PATCH 2/3] fix --- packages/rs-dpp/src/data_contract/serialized_version/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs index 81f643ab481..7afdaf5a6ea 100644 --- a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs +++ b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs @@ -63,6 +63,8 @@ pub enum DataContractMismatch { Description, /// The two variants are of different serialization formats (e.g., V0 vs V1). FormatVersionMismatch, + /// The two variants are different in V0. + V0Mismatch, } impl fmt::Display for DataContractMismatch { @@ -82,6 +84,7 @@ impl fmt::Display for DataContractMismatch { DataContractMismatch::FormatVersionMismatch => { "Serialization format versions differ (e.g., V0 vs V1)" } + DataContractMismatch::V0Mismatch => "V0 versions differ", }; write!(f, "{}", description) } @@ -184,7 +187,7 @@ impl DataContractInSerializationFormat { DataContractInSerializationFormat::V0(v0_other), ) => { if v0_self != v0_other { - Some(DataContractMismatch::FormatVersionMismatch) + Some(DataContractMismatch::V0Mismatch) } else { None } From 42b9a63419d4596a4013f8242b010a958281239d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 29 May 2025 16:18:01 +0200 Subject: [PATCH 3/3] case insensitive compare on keywords --- .../rs-dpp/src/data_contract/serialized_version/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs index 7afdaf5a6ea..baa5d062412 100644 --- a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs +++ b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs @@ -212,7 +212,13 @@ impl DataContractInSerializationFormat { Some(DataContractMismatch::Groups) } else if v1_self.tokens != v1_other.tokens { Some(DataContractMismatch::Tokens) - } else if v1_self.keywords != v1_other.keywords { + } else if v1_self.keywords.len() != v1_other.keywords.len() + || v1_self + .keywords + .iter() + .zip(v1_other.keywords.iter()) + .any(|(a, b)| a.to_lowercase() != b.to_lowercase()) + { Some(DataContractMismatch::Keywords) } else if v1_self.description != v1_other.description { Some(DataContractMismatch::Description)