Skip to content

Commit 3b09052

Browse files
feat(sdk): add DataContractMismatch enum for detailed contract comparison (#2648)
1 parent ec63c33 commit 3b09052

File tree

2 files changed

+120
-35
lines changed
  • packages
    • rs-dpp/src/data_contract/serialized_version
    • rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0

2 files changed

+120
-35
lines changed

packages/rs-dpp/src/data_contract/serialized_version/mod.rs

Lines changed: 114 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0;
2-
use crate::data_contract::{
3-
DataContract, DefinitionName, DocumentName, GroupContractPosition, TokenContractPosition,
4-
EMPTY_GROUPS, EMPTY_TOKENS,
5-
};
6-
use crate::version::PlatformVersion;
7-
use std::collections::BTreeMap;
8-
91
use super::EMPTY_KEYWORDS;
102
use crate::data_contract::associated_token::token_configuration::TokenConfiguration;
113
use crate::data_contract::group::Group;
4+
use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0;
125
use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1;
136
use crate::data_contract::v0::DataContractV0;
147
use crate::data_contract::v1::DataContractV1;
8+
use crate::data_contract::{
9+
DataContract, DefinitionName, DocumentName, GroupContractPosition, TokenContractPosition,
10+
EMPTY_GROUPS, EMPTY_TOKENS,
11+
};
1512
use crate::validation::operations::ProtocolValidationOperation;
13+
use crate::version::PlatformVersion;
1614
use crate::ProtocolError;
1715
use bincode::{Decode, Encode};
1816
use derive_more::From;
@@ -21,6 +19,8 @@ use platform_version::{IntoPlatformVersioned, TryFromPlatformVersioned};
2119
use platform_versioning::PlatformVersioned;
2220
#[cfg(feature = "data-contract-serde-conversion")]
2321
use serde::{Deserialize, Serialize};
22+
use std::collections::BTreeMap;
23+
use std::fmt;
2424

2525
pub(in crate::data_contract) mod v0;
2626
pub(in crate::data_contract) mod v1;
@@ -34,6 +34,62 @@ pub mod property_names {
3434

3535
pub const CONTRACT_DESERIALIZATION_LIMIT: usize = 15000;
3636

37+
/// Represents a field mismatch between two `DataContractInSerializationFormat::V1`
38+
/// variants, or indicates a format version mismatch.
39+
///
40+
/// Used to diagnose why two data contracts are not considered equal
41+
/// when ignoring auto-generated fields.
42+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
43+
pub enum DataContractMismatch {
44+
/// The `id` fields are not equal.
45+
Id,
46+
/// The `config` fields are not equal.
47+
Config,
48+
/// The `version` fields are not equal.
49+
Version,
50+
/// The `owner_id` fields are not equal.
51+
OwnerId,
52+
/// The `schema_defs` fields are not equal.
53+
SchemaDefs,
54+
/// The `document_schemas` fields are not equal.
55+
DocumentSchemas,
56+
/// The `groups` fields are not equal.
57+
Groups,
58+
/// The `tokens` fields are not equal.
59+
Tokens,
60+
/// The `keywords` fields are not equal.
61+
Keywords,
62+
/// The `description` fields are not equal.
63+
Description,
64+
/// The two variants are of different serialization formats (e.g., V0 vs V1).
65+
FormatVersionMismatch,
66+
/// The two variants are different in V0.
67+
V0Mismatch,
68+
}
69+
70+
impl fmt::Display for DataContractMismatch {
71+
/// Formats the enum into a human-readable string describing the mismatch.
72+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73+
let description = match self {
74+
DataContractMismatch::Id => "ID fields differ",
75+
DataContractMismatch::Config => "Config fields differ",
76+
DataContractMismatch::Version => "Version fields differ",
77+
DataContractMismatch::OwnerId => "Owner ID fields differ",
78+
DataContractMismatch::SchemaDefs => "Schema definitions differ",
79+
DataContractMismatch::DocumentSchemas => "Document schemas differ",
80+
DataContractMismatch::Groups => "Groups differ",
81+
DataContractMismatch::Tokens => "Tokens differ",
82+
DataContractMismatch::Keywords => "Keywords differ",
83+
DataContractMismatch::Description => "Description fields differ",
84+
DataContractMismatch::FormatVersionMismatch => {
85+
"Serialization format versions differ (e.g., V0 vs V1)"
86+
}
87+
DataContractMismatch::V0Mismatch => "V0 versions differ",
88+
};
89+
write!(f, "{}", description)
90+
}
91+
}
92+
3793
#[derive(Debug, Clone, Encode, Decode, PartialEq, PlatformVersioned, From)]
3894
#[cfg_attr(
3995
feature = "data-contract-serde-conversion",
@@ -112,36 +168,65 @@ impl DataContractInSerializationFormat {
112168
}
113169
}
114170

115-
pub fn eq_without_auto_fields(&self, other: &Self) -> bool {
171+
/// Compares `self` to another `DataContractInSerializationFormat` instance
172+
/// and returns the first mismatching field, if any.
173+
///
174+
/// This comparison ignores auto-generated fields and is only sensitive to
175+
/// significant differences in contract content. For V0 formats, any difference
176+
/// results in a generic mismatch. For differing format versions (V0 vs V1),
177+
/// a `FormatVersionMismatch` is returned.
178+
///
179+
/// # Returns
180+
///
181+
/// - `None` if the contracts are equal according to the relevant fields.
182+
/// - `Some(DataContractMismatch)` indicating the first field where they differ.
183+
pub fn first_mismatch(&self, other: &Self) -> Option<DataContractMismatch> {
116184
match (self, other) {
117185
(
118186
DataContractInSerializationFormat::V0(v0_self),
119187
DataContractInSerializationFormat::V0(v0_other),
120-
) => v0_self == v0_other,
188+
) => {
189+
if v0_self != v0_other {
190+
Some(DataContractMismatch::V0Mismatch)
191+
} else {
192+
None
193+
}
194+
}
121195
(
122196
DataContractInSerializationFormat::V1(v1_self),
123197
DataContractInSerializationFormat::V1(v1_other),
124198
) => {
125-
v1_self.id == v1_other.id
126-
&& v1_self.config == v1_other.config
127-
&& v1_self.version == v1_other.version
128-
&& v1_self.owner_id == v1_other.owner_id
129-
&& v1_self.schema_defs == v1_other.schema_defs
130-
&& v1_self.document_schemas == v1_other.document_schemas
131-
&& v1_self.groups == v1_other.groups
132-
&& v1_self.tokens == v1_other.tokens
133-
&& v1_self.keywords == v1_other.keywords
134-
&& v1_self.description == v1_other.description
199+
if v1_self.id != v1_other.id {
200+
Some(DataContractMismatch::Id)
201+
} else if v1_self.config != v1_other.config {
202+
Some(DataContractMismatch::Config)
203+
} else if v1_self.version != v1_other.version {
204+
Some(DataContractMismatch::Version)
205+
} else if v1_self.owner_id != v1_other.owner_id {
206+
Some(DataContractMismatch::OwnerId)
207+
} else if v1_self.schema_defs != v1_other.schema_defs {
208+
Some(DataContractMismatch::SchemaDefs)
209+
} else if v1_self.document_schemas != v1_other.document_schemas {
210+
Some(DataContractMismatch::DocumentSchemas)
211+
} else if v1_self.groups != v1_other.groups {
212+
Some(DataContractMismatch::Groups)
213+
} else if v1_self.tokens != v1_other.tokens {
214+
Some(DataContractMismatch::Tokens)
215+
} else if v1_self.keywords.len() != v1_other.keywords.len()
216+
|| v1_self
217+
.keywords
218+
.iter()
219+
.zip(v1_other.keywords.iter())
220+
.any(|(a, b)| a.to_lowercase() != b.to_lowercase())
221+
{
222+
Some(DataContractMismatch::Keywords)
223+
} else if v1_self.description != v1_other.description {
224+
Some(DataContractMismatch::Description)
225+
} else {
226+
None
227+
}
135228
}
136-
// Cross-version comparisons return false
137-
(
138-
DataContractInSerializationFormat::V0(_),
139-
DataContractInSerializationFormat::V1(_),
140-
)
141-
| (
142-
DataContractInSerializationFormat::V1(_),
143-
DataContractInSerializationFormat::V0(_),
144-
) => false,
229+
_ => Some(DataContractMismatch::FormatVersionMismatch),
145230
}
146231
}
147232
}

packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ impl Drive {
8181
.clone()
8282
.try_into_platform_versioned(platform_version)?;
8383

84-
if !contract_for_serialization
85-
.eq_without_auto_fields(data_contract_create.data_contract())
84+
if let Some(mismatch) =
85+
contract_for_serialization.first_mismatch(data_contract_create.data_contract())
8686
{
87-
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()))));
87+
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))));
8888
}
8989

9090
Ok((root_hash, VerifiedDataContract(contract)))
@@ -103,10 +103,10 @@ impl Drive {
103103
let contract_for_serialization: DataContractInSerializationFormat = contract
104104
.clone()
105105
.try_into_platform_versioned(platform_version)?;
106-
if !contract_for_serialization
107-
.eq_without_auto_fields(data_contract_update.data_contract())
106+
if let Some(mismatch) =
107+
contract_for_serialization.first_mismatch(data_contract_update.data_contract())
108108
{
109-
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()))));
109+
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))));
110110
}
111111
Ok((root_hash, VerifiedDataContract(contract)))
112112
}

0 commit comments

Comments
 (0)