Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
645 changes: 645 additions & 0 deletions packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ use crate::data_contract::errors::DataContractError;
use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements;
use crate::identity::SecurityLevel;
#[cfg(feature = "validation")]
use crate::validation::meta_validators::DOCUMENT_META_SCHEMA_V0;
use crate::validation::meta_validators::{DOCUMENT_META_SCHEMA_V0, DOCUMENT_META_SCHEMA_V1};
use crate::validation::operations::ProtocolValidationOperation;
use crate::version::PlatformVersion;
use crate::ProtocolError;
Expand Down Expand Up @@ -132,8 +132,28 @@ impl DocumentTypeV0 {
)
})?;

// Select the appropriate document meta-schema based on platform version
let meta_schema = match platform_version
.dpp
.contract_versions
.document_type_versions
.schema
.document_type_schema
{
0 => &*DOCUMENT_META_SCHEMA_V0,
1 => &*DOCUMENT_META_SCHEMA_V1,
version => {
return Err(ProtocolError::UnknownVersionMismatch {
method: "DocumentTypeV0::try_from_schema (document_type_schema)"
.to_string(),
known_versions: vec![0, 1],
received: version,
})
}
};

// Validate against JSON Schema
DOCUMENT_META_SCHEMA_V0
meta_schema
.validate(&root_json_schema)
.map_err(|mut errs| ConsensusError::from(errs.next().unwrap()))?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ use crate::tokens::token_amount_on_contract_token::{
DocumentActionTokenCost, DocumentActionTokenEffect,
};
#[cfg(feature = "validation")]
use crate::validation::meta_validators::DOCUMENT_META_SCHEMA_V0;
use crate::validation::meta_validators::{DOCUMENT_META_SCHEMA_V0, DOCUMENT_META_SCHEMA_V1};
use crate::validation::operations::ProtocolValidationOperation;
use crate::version::PlatformVersion;
use crate::ProtocolError;
Expand Down Expand Up @@ -151,8 +151,28 @@ impl DocumentTypeV1 {
)
})?;

// Select the appropriate document meta-schema based on platform version
let meta_schema = match platform_version
.dpp
.contract_versions
.document_type_versions
.schema
.document_type_schema
{
0 => &*DOCUMENT_META_SCHEMA_V0,
1 => &*DOCUMENT_META_SCHEMA_V1,
version => {
return Err(ProtocolError::UnknownVersionMismatch {
method: "DocumentTypeV1::try_from_schema (document_type_schema)"
.to_string(),
known_versions: vec![0, 1],
received: version,
})
}
};

// Validate against JSON Schema
DOCUMENT_META_SCHEMA_V0
meta_schema
.validate(&root_json_schema)
.map_err(|mut errs| ConsensusError::from(errs.next().unwrap()))?;

Expand Down Expand Up @@ -708,6 +728,139 @@ mod tests {
use assert_matches::assert_matches;
use platform_value::platform_value;

mod document_meta_schema_version {
use super::*;

#[test]
fn v0_schema_allows_unknown_properties() {
let platform_version = PlatformVersion::first();

let schema = platform_value!({
"type": "object",
"properties": {
"test_field": {
"type": "string",
"position": 0
}
},
"additionalProperties": false,
"unknownProp": true
});

let config = DataContractConfig::default_for_version(platform_version)
.expect("should create a default config");

let result = DocumentTypeV1::try_from_schema(
Identifier::new([1; 32]),
1,
config.version(),
"test_doc",
schema,
None,
&BTreeMap::new(),
&config,
true,
&mut vec![],
platform_version,
);

assert!(
result.is_ok(),
"v0 schema should allow unknown top-level properties, got error: {:?}",
result.err()
);
}

#[test]
fn v1_schema_rejects_unknown_properties() {
let platform_version = PlatformVersion::latest();

let schema = platform_value!({
"type": "object",
"properties": {
"test_field": {
"type": "string",
"position": 0
}
},
"additionalProperties": false,
"unknownProp": true
});

let config = DataContractConfig::default_for_version(platform_version)
.expect("should create a default config");

let result = DocumentTypeV1::try_from_schema(
Identifier::new([1; 32]),
1,
config.version(),
"test_doc",
schema,
None,
&BTreeMap::new(),
&config,
true,
&mut vec![],
platform_version,
);
Comment thread
QuantumExplorer marked this conversation as resolved.

assert!(
result.is_err(),
"v1 schema should reject unknown top-level properties"
);

let err = result.unwrap_err();
let err_str = format!("{:?}", err);
let err_str_lower = err_str.to_lowercase();
assert!(
err_str_lower.contains("additional properties"),
"Error should mention additional properties, got: {}",
err_str
);
}

#[test]
fn v1_schema_accepts_known_properties() {
let platform_version = PlatformVersion::latest();

let schema = platform_value!({
"type": "object",
"properties": {
"test_field": {
"type": "string",
"position": 0
}
},
"additionalProperties": false,
"required": ["test_field"],
"$comment": "hello"
});

let config = DataContractConfig::default_for_version(platform_version)
.expect("should create a default config");

let result = DocumentTypeV1::try_from_schema(
Identifier::new([1; 32]),
1,
config.version(),
"test_doc",
schema,
None,
&BTreeMap::new(),
&config,
true,
&mut vec![],
platform_version,
);

assert!(
result.is_ok(),
"v1 schema should accept known properties like required and $comment, got error: {:?}",
result.err()
);
}
}

mod document_type_name {
use super::*;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use platform_value::Value;

/// The set of top-level property names allowed on a document type schema object
/// as defined by the v1 document meta-schema.
///
/// Any key not in this list should be stripped from document type schemas
/// during the v12 protocol upgrade migration to prevent unknown properties
/// from changing storage semantics.
pub const ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[
"type",
"$schema",
"$defs",
"indices",
"signatureSecurityLevelRequirement",
"documentsKeepHistory",
"documentsMutable",
"canBeDeleted",
"transferable",
"tradeMode",
"creationRestrictionMode",
"requiresIdentityEncryptionBoundedKey",
"requiresIdentityDecryptionBoundedKey",
"tokenCost",
"properties",
"transient",
"keywords",
"additionalProperties",
"required",
"$comment",
"description",
"minProperties",
"maxProperties",
"dependentRequired",
];
Comment thread
QuantumExplorer marked this conversation as resolved.

/// Strips any top-level key from the document type schema `Value::Map` that
/// is not in the allowed set. Returns `true` if any keys were removed.
pub fn strip_unknown_properties_from_document_schema(schema: &mut Value) -> bool {
let map = match schema {
Value::Map(map) => map,
_ => return false,
};

let before = map.len();
map.retain(|(key, _)| {
let key_str = match key {
Value::Text(s) => s.as_str(),
_ => return true, // keep non-string keys (shouldn't happen but safe)
};
ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES.contains(&key_str)
Comment thread
QuantumExplorer marked this conversation as resolved.
});
map.len() != before
}

#[cfg(test)]
mod tests {
use super::*;
use platform_value::platform_value;

#[test]
fn strips_unknown_properties() {
let mut schema = platform_value!({
"type": "object",
"properties": {},
"additionalProperties": false,
"unknownProp": true,
"anotherUnknown": 42
});

let changed = strip_unknown_properties_from_document_schema(&mut schema);
assert!(changed);

let map = schema.as_map().unwrap();
let keys: Vec<&str> = map.iter().filter_map(|(k, _)| k.as_text()).collect();
assert!(!keys.contains(&"unknownProp"));
assert!(!keys.contains(&"anotherUnknown"));
assert!(keys.contains(&"type"));
assert!(keys.contains(&"properties"));
assert!(keys.contains(&"additionalProperties"));
}

#[test]
fn no_change_when_all_properties_are_known() {
let mut schema = platform_value!({
"type": "object",
"properties": {},
"additionalProperties": false,
"required": ["foo"],
"$comment": "test"
});

let changed = strip_unknown_properties_from_document_schema(&mut schema);
assert!(!changed);
}

#[test]
fn handles_non_map_value() {
let mut schema = Value::Text("not a map".to_string());
let changed = strip_unknown_properties_from_document_schema(&mut schema);
assert!(!changed);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod allowed_top_level_properties;

mod enrich_with_base_schema;

mod find_identifier_and_binary_paths;
Expand Down
7 changes: 7 additions & 0 deletions packages/rs-dpp/src/data_contract/serialized_version/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ impl DataContractInSerializationFormat {
}
}

pub fn document_schemas_mut(&mut self) -> &mut BTreeMap<DocumentName, Value> {
match self {
DataContractInSerializationFormat::V0(v0) => &mut v0.document_schemas,
DataContractInSerializationFormat::V1(v1) => &mut v1.document_schemas,
}
}

pub fn schema_defs(&self) -> Option<&BTreeMap<DefinitionName, Value>> {
match self {
DataContractInSerializationFormat::V0(v0) => v0.schema_defs.as_ref(),
Expand Down
Loading
Loading