Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -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 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.",
"$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), the refersTo reference keyword on identifier properties, and the requiredSince property keyword (the contract version a property is required from), 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": {
Expand Down Expand Up @@ -224,6 +224,11 @@
"position": {
"type": "integer",
"minimum": 0
},
"requiredSince": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
}
},
"dependentSchemas": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ fn parse_document_properties(
&mut document_properties,
&required_fields,
&transient_fields,
true,
property_key,
property_value,
root_schema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ fn insert_values(
vec![(prefix, property_key, property_value)];

while let Some((prefix, property_key, property_value)) = to_visit.pop() {
let is_top_level = prefix.is_none();
let prefixed_property_key = match prefix {
None => property_key,
Some(prefix) => [prefix, property_key].join(".").to_owned(),
Expand All @@ -143,6 +144,12 @@ fn insert_values(

let is_required = known_required.contains(&prefixed_property_key);
let is_transient = known_transient.contains(&prefixed_property_key);
let required_since = apply_required_since(
&inner_properties,
is_required,
is_top_level,
platform_version,
)?;

match DocumentPropertyType::try_from_value_map(&inner_properties, &config.into())? {
DocumentPropertyType::Object(_) => {
Expand Down Expand Up @@ -179,6 +186,7 @@ fn insert_values(
property_type,
required: is_required,
transient: is_transient,
required_since,
},
);
}
Expand All @@ -194,6 +202,7 @@ fn insert_values_nested(
document_properties: &mut IndexMap<String, DocumentProperty>,
known_required: &BTreeSet<String>,
known_transient: &BTreeSet<String>,
is_top_level: bool,
property_key: String,
property_value: &Value,
root_schema: &Value,
Expand All @@ -212,6 +221,13 @@ fn insert_values_nested(

let is_transient = known_transient.contains(&property_key);

let required_since = apply_required_since(
&inner_properties,
is_required,
is_top_level,
platform_version,
)?;

let property_type =
match DocumentPropertyType::try_from_value_map(&inner_properties, &config.into())? {
DocumentPropertyType::Object(_) => {
Expand Down Expand Up @@ -271,6 +287,7 @@ fn insert_values_nested(
&mut nested_properties,
&stripped_required,
&stripped_transient,
false,
object_property_string,
object_property_value,
root_schema,
Expand All @@ -294,12 +311,79 @@ fn insert_values_nested(
property_type,
required: is_required,
transient: is_transient,
required_since,
},
);

Ok(())
}

/// Parses the `requiredSince` keyword: the contract version from which the
/// property is required. Only meaningful on top-level required properties —
/// the document wire format encodes a required property without a presence
/// flag, so requiredness that varies by contract version must be resolvable
/// per property from the current schema alone (see the per-document contract
/// version stamp in document serialization format 3).
///
/// Versioned on `apply_required_since` 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_required_since(
inner_properties: &BTreeMap<String, &Value>,
is_required: bool,
is_top_level: bool,
platform_version: &PlatformVersion,
) -> Result<Option<u32>, DataContractError> {
match platform_version
.dpp
.contract_versions
.document_type_versions
.schema
.apply_required_since
{
None => Ok(None),
Some(0) => apply_required_since_v0(inner_properties, is_required, is_top_level),
Some(version) => Err(DataContractError::Unsupported(format!(
"apply_required_since version {version} is not supported"
))),
}
}

fn apply_required_since_v0(
inner_properties: &BTreeMap<String, &Value>,
is_required: bool,
is_top_level: bool,
) -> Result<Option<u32>, DataContractError> {
let Some(required_since_value) = inner_properties.get(property_names::REQUIRED_SINCE) else {
return Ok(None);
};

if !is_top_level {
return Err(DataContractError::InvalidContractStructure(
"requiredSince is only allowed on top-level properties".to_string(),
));
}

if !is_required {
return Err(DataContractError::InvalidContractStructure(
"requiredSince is only allowed on properties listed in required".to_string(),
));
}

let required_since: u32 = required_since_value
.to_integer()
.map_err(|e| DataContractError::ValueWrongType(e.to_string()))?;

if required_since == 0 {
return Err(DataContractError::InvalidContractStructure(
"requiredSince must be a contract version of at least 1".to_string(),
));
}

Ok(Some(required_since))
}

/// Folds a `refersTo` declaration into the property type: an identifier property
/// with `refersTo` becomes `IdentifierWithReference(target)`. Non-identifier
/// properties cannot carry `refersTo`.
Expand Down Expand Up @@ -804,4 +888,113 @@ mod tests {
)
.expect("a parse predating refersTo should ignore the keyword entirely");
}

// ================================================================
// requiredSince
// ================================================================

#[test]
fn should_parse_required_since_on_top_level_required_property() {
let document_type = try_document_type_from_schema(json!({
"type": "object",
"properties": {
"a": {"type": "string", "position": 0, "maxLength": 60},
"b": {"type": "string", "position": 1, "maxLength": 60, "requiredSince": 3},
},
"required": ["a", "b"],
"additionalProperties": false
}))
.expect("should parse");

let properties = document_type.as_ref().flattened_properties().clone();
assert_eq!(properties.get("a").unwrap().required_since, None);
assert_eq!(properties.get("b").unwrap().required_since, Some(3));
assert!(properties.get("b").unwrap().required);
}

#[test]
fn should_reject_required_since_on_optional_property() {
let result = try_document_type_from_schema(json!({
"type": "object",
"properties": {
"a": {"type": "string", "position": 0, "maxLength": 60, "requiredSince": 2},
},
"required": [],
"additionalProperties": false
}));

assert!(
result.is_err(),
"requiredSince on a property not listed in required must be rejected"
);
}

#[test]
fn should_reject_required_since_on_nested_property() {
let result = try_document_type_from_schema(json!({
"type": "object",
"properties": {
"outer": {
"type": "object",
"position": 0,
"properties": {
"inner": {"type": "string", "position": 0, "maxLength": 60, "requiredSince": 2},
},
"required": ["inner"],
"additionalProperties": false
},
},
"required": [],
"additionalProperties": false
}));

assert!(
result.is_err(),
"requiredSince on a nested property must be rejected"
);
}

#[test]
fn should_reject_required_since_of_zero() {
let result = try_document_type_from_schema(json!({
"type": "object",
"properties": {
"a": {"type": "string", "position": 0, "maxLength": 60, "requiredSince": 0},
},
"required": ["a"],
"additionalProperties": false
}));

assert!(
result.is_err(),
"requiredSince of 0 must be rejected (contract versions start at 1)"
);
}

#[test]
fn should_ignore_required_since_on_platform_versions_predating_it() {
// Platform versions whose tables carry `apply_required_since: None`
// predate the 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 required
// property 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": {
"a": {"type": "string", "position": 0, "maxLength": 60, "requiredSince": 3},
},
"required": ["a"],
"additionalProperties": false
}),
platform_version,
)
.expect("a parse predating requiredSince should ignore the keyword entirely");

let properties = document_type.as_ref().flattened_properties().clone();
assert_eq!(properties.get("a").unwrap().required_since, None);
assert!(properties.get("a").unwrap().required);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ impl DocumentTypeV0 {
&mut document_properties,
&required_fields,
&transient_fields,
true,
property_key,
property_value,
&root_schema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1712,7 +1712,7 @@ mod tests {
let old = document_type_with_byte_array(old_ba, platform_version);
let new = document_type_with_byte_array(new_ba, platform_version);
old.as_ref()
.validate_update(new.as_ref(), platform_version)
.validate_update(new.as_ref(), 2, platform_version)
.expect("validate_update should not error")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ mod v1;

impl DocumentTypeRef<'_> {
/// Verify that the update to the document type is valid.
/// We assume that new document type is valid
/// We assume that new document type is valid.
/// `new_contract_version` is the version the updated contract will have
/// (already validated to be the old version + 1): a newly added required
/// property must carry `requiredSince` equal to exactly that version.
pub fn validate_update(
&self,
new_document_type: DocumentTypeRef,
new_contract_version: u32,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, ProtocolError> {
match platform_version
Expand All @@ -22,7 +26,7 @@ impl DocumentTypeRef<'_> {
.validate_update
{
0 => self.validate_update_v0(new_document_type, platform_version),
1 => self.validate_update_v1(new_document_type, platform_version),
1 => self.validate_update_v1(new_document_type, new_contract_version, platform_version),
version => Err(ProtocolError::UnknownVersionMismatch {
method: "validate_update".to_string(),
known_versions: vec![0, 1],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ mod tests {

let early_result = old
.as_ref()
.validate_update(new_early_name.as_ref(), platform_version)
.validate_update(new_early_name.as_ref(), 2, platform_version)
.expect("early-name addition should produce a validation result");

assert_matches!(
Expand All @@ -147,7 +147,7 @@ mod tests {
// check ("schema keyword 'indices' ... is not supported").
let late_error = old
.as_ref()
.validate_update(new_late_name.as_ref(), platform_version)
.validate_update(new_late_name.as_ref(), 2, platform_version)
.expect_err("late-name addition should error in schema compatibility");

assert_matches!(
Expand Down
Loading
Loading