Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,56 @@
"enum": [
"identity",
"contract",
"token"
"token",
"permanentDocument"
]
},
"contractId": {
"description": "The id of the data contract the referenced document lives in, as a base58 string or a 32-byte array; when absent the reference targets the declaring contract itself",
"oneOf": [
{
"type": "string",
"minLength": 32,
"maxLength": 44,
"pattern": "^[123456789A-HJ-NP-Za-km-z]{32,44}$"
},
{
"type": "array",
"minItems": 32,
"maxItems": 32,
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
}
}
]
},
"documentType": {
"description": "The name of the referenced document type; it must forbid deletion (canBeDeleted: false)",
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[a-zA-Z0-9-_]{1,64}$"
}
},
"required": [
"type"
],
"additionalProperties": false
"additionalProperties": false,
"if": {
"properties": { "type": { "const": "permanentDocument" } },
"required": ["type"]
},
"then": {
"required": ["type", "documentType"]
},
"else": {
"properties": {
"contractId": false,
"documentType": false
}
}
},
"contains": {
"$ref": "https://json-schema.org/draft/2020-12/meta/applicator#/properties/contains"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,34 @@ fn apply_property_reference_v0(
"identity" => DocumentPropertyReferenceTarget::Identity,
"contract" => DocumentPropertyReferenceTarget::Contract,
"token" => DocumentPropertyReferenceTarget::Token,
"permanentDocument" => {
// An absent contractId means the reference targets a document
// type of the declaring contract itself
let contract_id = refers_to_map
.get(property_names::CONTRACT_ID)
.map(|value| {
value
.to_identifier()
.map_err(|e| DataContractError::ValueWrongType(e.to_string()))
})
.transpose()?;

let document_type_name = refers_to_map
.get_str(property_names::DOCUMENT_TYPE)
.map_err(|e| DataContractError::ValueWrongType(e.to_string()))?;

if document_type_name.is_empty() || document_type_name.len() > 64 {
return Err(DataContractError::InvalidContractStructure(
"permanentDocument refersTo documentType must be between 1 and 64 characters"
.to_string(),
));
}

DocumentPropertyReferenceTarget::PermanentDocument {
contract_id,
document_type_name: document_type_name.to_string(),
}
}
other => {
return Err(DataContractError::InvalidContractStructure(format!(
"invalid refersTo type {other}"
Expand All @@ -369,6 +397,7 @@ mod tests {
use super::*;
use crate::data_contract::config::DataContractConfig;
use crate::data_contract::document_type::accessors::DocumentTypeV0Getters;
use platform_value::string_encoding::Encoding;
use serde_json::json;

fn try_document_type_from_schema(
Expand Down Expand Up @@ -461,6 +490,174 @@ mod tests {
);
}

#[test]
fn should_parse_permanent_document_refers_to() {
let contract_id = Identifier::from([7u8; 32]);

let document_type = try_document_type_from_schema(json!({
"type": "object",
"properties": {
"parentNoteId": {
"type": "array",
"byteArray": true,
"minItems": 32,
"maxItems": 32,
"contentMediaType": "application/x.dash.dpp.identifier",
"position": 0,
"refersTo": {
"type": "permanentDocument",
"contractId": contract_id.to_string(Encoding::Base58),
"documentType": "note"
}
}
},
"required": [],
"additionalProperties": false
}))
.expect("should parse");

let property_type = document_type
.as_ref()
.flattened_properties()
.get("parentNoteId")
.map(|p| p.property_type.clone())
.expect("property should be present");

assert_eq!(
property_type,
DocumentPropertyType::IdentifierWithReference(
DocumentPropertyReferenceTarget::PermanentDocument {
contract_id: Some(contract_id),
document_type_name: "note".to_string(),
}
)
);
}

#[test]
fn should_parse_permanent_document_refers_to_without_contract_id_as_own_contract() {
let document_type = try_document_type_from_schema(json!({
"type": "object",
"properties": {
"parentNoteId": {
"type": "array",
"byteArray": true,
"minItems": 32,
"maxItems": 32,
"contentMediaType": "application/x.dash.dpp.identifier",
"position": 0,
"refersTo": {
"type": "permanentDocument",
"documentType": "note"
}
}
},
"required": [],
"additionalProperties": false
}))
.expect("should parse");

let property_type = document_type
.as_ref()
.flattened_properties()
.get("parentNoteId")
.map(|p| p.property_type.clone())
.expect("property should be present");

assert_eq!(
property_type,
DocumentPropertyType::IdentifierWithReference(
DocumentPropertyReferenceTarget::PermanentDocument {
contract_id: None,
document_type_name: "note".to_string(),
}
)
);
}

#[test]
fn should_reject_permanent_document_refers_to_with_invalid_contract_id() {
let err = try_document_type_from_schema(json!({
"type": "object",
"properties": {
"parentNoteId": {
"type": "array",
"byteArray": true,
"minItems": 32,
"maxItems": 32,
"contentMediaType": "application/x.dash.dpp.identifier",
"position": 0,
"refersTo": {
"type": "permanentDocument",
"contractId": "not-a-valid-identifier",
"documentType": "note"
}
}
},
"required": [],
"additionalProperties": false
}))
.expect_err("should fail");

let message = err.to_string();
assert!(message.contains("base 58"), "unexpected error: {message}");
}

#[test]
fn should_reject_permanent_document_refers_to_with_oversized_document_type_name() {
let err = try_document_type_from_schema(json!({
"type": "object",
"properties": {
"parentNoteId": {
"type": "array",
"byteArray": true,
"minItems": 32,
"maxItems": 32,
"contentMediaType": "application/x.dash.dpp.identifier",
"position": 0,
"refersTo": {
"type": "permanentDocument",
"contractId": Identifier::from([7u8; 32]).to_string(Encoding::Base58),
"documentType": "a".repeat(65)
}
}
},
"required": [],
"additionalProperties": false
}))
.expect_err("should fail");

let message = err.to_string();
assert!(
message.contains("between 1 and 64 characters"),
"unexpected error: {message}"
);
}

#[test]
fn should_reject_permanent_document_refers_to_without_document_type() {
try_document_type_from_schema(json!({
"type": "object",
"properties": {
"parentNoteId": {
"type": "array",
"byteArray": true,
"minItems": 32,
"maxItems": 32,
"contentMediaType": "application/x.dash.dpp.identifier",
"position": 0,
"refersTo": {
"type": "permanentDocument",
"contractId": Identifier::from([7u8; 32]).to_string(Encoding::Base58)
}
}
},
"required": [],
"additionalProperties": false
}))
.expect_err("should fail");
}

#[test]
fn should_ignore_refers_to_on_platform_versions_predating_it() {
// Platform versions whose tables carry `apply_property_reference: None`
Expand Down
2 changes: 2 additions & 0 deletions packages/rs-dpp/src/data_contract/document_type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ pub(crate) mod property_names {
pub const ENCRYPTION_KEY_REQUIREMENTS: &str = "encryptionKeyReqs";
pub const DECRYPTION_KEY_REQUIREMENTS: &str = "decryptionKeyReqs";
pub const REFERS_TO: &str = "refersTo";
pub const CONTRACT_ID: &str = "contractId";
pub const DOCUMENT_TYPE: &str = "documentType";
pub const DOCUMENTS_COUNTABLE: &str = "documentsCountable";
pub const RANGE_COUNTABLE: &str = "rangeCountable";
/// Doctype-level flag naming the property whose values are summed into
Expand Down
59 changes: 59 additions & 0 deletions packages/rs-dpp/src/data_contract/document_type/property/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub struct ByteArrayPropertySizes {
pub max_size: Option<u16>,
}

// This enum is embedded in consensus errors, so it is consensus-serialized.
// @append_only
#[derive(
Debug, PartialEq, Eq, Clone, Serialize, Encode, Decode, PlatformSerialize, PlatformDeserialize,
)]
Expand All @@ -61,6 +63,18 @@ pub enum DocumentPropertyReferenceTarget {
Identity,
Contract,
Token,
/// A document of a document type whose documents can never be deleted
/// (`canBeDeleted: false`). Only such document types may be referenced:
/// together with document types being non-removable and the
/// `canBeDeleted` flag being immutable on contract updates, this
/// guarantees a validated reference can never dangle.
#[serde(rename = "permanentDocument")]
PermanentDocument {
/// The contract the referenced document type lives in; `None` means
/// the declaring contract itself
contract_id: Option<Identifier>,
document_type_name: String,
},
}

impl std::fmt::Display for DocumentPropertyReferenceTarget {
Expand All @@ -69,6 +83,20 @@ impl std::fmt::Display for DocumentPropertyReferenceTarget {
DocumentPropertyReferenceTarget::Identity => write!(f, "identity"),
DocumentPropertyReferenceTarget::Contract => write!(f, "contract"),
DocumentPropertyReferenceTarget::Token => write!(f, "token"),
DocumentPropertyReferenceTarget::PermanentDocument {
contract_id: Some(contract_id),
document_type_name,
} => write!(
f,
"permanent document (contract {contract_id}, document type {document_type_name})"
),
DocumentPropertyReferenceTarget::PermanentDocument {
contract_id: None,
document_type_name,
} => write!(
f,
"permanent document (own contract, document type {document_type_name})"
),
}
}
}
Expand Down Expand Up @@ -7134,4 +7162,35 @@ mod tests {
}))
);
}

#[test]
fn should_display_reference_targets() {
let contract_id = Identifier::from([7u8; 32]);

assert_eq!(
DocumentPropertyReferenceTarget::Identity.to_string(),
"identity"
);
assert_eq!(
DocumentPropertyReferenceTarget::Contract.to_string(),
"contract"
);
assert_eq!(DocumentPropertyReferenceTarget::Token.to_string(), "token");
assert_eq!(
DocumentPropertyReferenceTarget::PermanentDocument {
contract_id: Some(contract_id),
document_type_name: "note".to_string(),
}
.to_string(),
format!("permanent document (contract {contract_id}, document type note)")
);
assert_eq!(
DocumentPropertyReferenceTarget::PermanentDocument {
contract_id: None,
document_type_name: "note".to_string(),
}
.to_string(),
"permanent document (own contract, document type note)"
);
}
}
2 changes: 2 additions & 0 deletions packages/rs-dpp/src/errors/consensus/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ impl ErrorWithCode for StateError {
Self::DocumentContestIndexMismatchError(_) => 40118,
Self::DocumentContestNotRequiredError(_) => 40119,
Self::ReferencedEntityNotFoundError(_) => 40120,
Self::ReferencedDocumentTypeNotFoundError(_) => 40121,
Self::ReferencedDocumentTypeDeletableError(_) => 40122,

// Identity Errors: 40200-40299
Self::IdentityAlreadyExistsError(_) => 40200,
Expand Down
2 changes: 2 additions & 0 deletions packages/rs-dpp/src/errors/consensus/state/document/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ pub mod document_timestamps_are_equal_error;
pub mod document_timestamps_mismatch_error;
pub mod duplicate_unique_index_error;
pub mod invalid_document_revision_error;
pub mod referenced_document_type_deletable_error;
pub mod referenced_document_type_not_found_error;
pub mod referenced_entity_not_found_error;
Loading
Loading