Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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 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": {
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;

Expand All @@ -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)?;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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>, String, &Value)> =
vec![(prefix, property_key, property_value)];
Expand Down Expand Up @@ -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 {
Expand All @@ -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<String, DocumentProperty>,
known_required: &BTreeSet<String>,
Expand All @@ -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()?;

Expand Down Expand Up @@ -269,6 +275,7 @@ fn insert_values_nested(
object_property_value,
root_schema,
config,
platform_version,
)?;
}
}
Expand All @@ -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 {
Expand All @@ -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<String, &Value>,
property_type: DocumentPropertyType,
platform_version: &PlatformVersion,
) -> Result<DocumentPropertyType, DataContractError> {
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<String, &Value>,
property_type: DocumentPropertyType,
) -> Result<DocumentPropertyType, DataContractError> {
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<DocumentType, ProtocolError> {
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<DocumentType, ProtocolError> {
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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ impl DocumentTypeV0 {
property_value,
&root_schema,
data_contact_config,
platform_version,
)
.map_err(consensus_or_protocol_data_contract_error)?;

Expand All @@ -257,6 +258,7 @@ impl DocumentTypeV0 {
property_value,
&root_schema,
data_contact_config,
platform_version,
)
.map_err(consensus_or_protocol_data_contract_error)?;
}
Expand Down
Loading
Loading