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
19 changes: 19 additions & 0 deletions book/src/data-model/documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,25 @@ pub const INITIAL_REVISION: u64 = 1;

Revision 0 is never used for active documents. This allows `0` to serve as a sentinel value meaning "no revision" in some contexts.

## Document References (`refersTo`)

From protocol version 14 a property of a document type can declare what it points at, and consensus refuses a create or replace whose target does not exist when the document is written (the reference is a write-time constraint only; nothing resolves it for a reader). The keyword is `refersTo` on the property, its `type` one of `identity`, `contract` (optionally with `contractRequirements`, see [Contract Moderation](contract-moderation.md)), `token`, `permanentDocument`, `deletableDocument` and `identityPublicKey`. Every form sits on an identifier property, with one exception below. The parsed shape is `DocumentPropertyType::IdentifierWithReference(target)`, and any change to a declaration on contract update is an incompatible schema change.

An `identityPublicKey` reference names one key of one identity, and comes in two forms that differ in which property carries what:

- **On the identity property.** The identifier property carries the identity id and `keyIdProperty` names the sibling integer property carrying the key id: `"refersTo": { "type": "identityPublicKey", "keyIdProperty": "toKeyIndex" }`. Consensus fetches the named key of that identity.
- **On the key id property.** The integer property carries the key id and `identityProperty` names whose key it is: `"refersTo": { "type": "identityPublicKey", "identityProperty": "$ownerId" }`. The property must declare exactly the range of a `KeyID` (`"type": "integer", "minimum": 0, "maximum": 4294967295`) and the declaration takes no `keyIdProperty`. `identityProperty` is `$ownerId` (the writer), `$creatorId` (the document's creator, only on a document type that records creator ids: a transferable or tradeable type of a format-1 contract) or the path of an identifier property of the same document type (which must exist, be an identifier and not carry an `identityPublicKey` reference of its own); the last two are checked at contract registration. `keyRequirements` sit on this form exactly as on the identifier form. The parsed shape is `DocumentPropertyType::KeyIdWithReference(KeyIdReference)`, the identity source plus the requirements, sized, encoded and queried exactly as a plain `u32`.

```json
"senderKeyId": {
"type": "integer", "minimum": 0, "maximum": 4294967295,
"refersTo": { "type": "identityPublicKey", "identityProperty": "$ownerId" },
"position": 3
}
```

Both forms share the state check (`validate_referenced_identity_key_v0` in the document reference validation): the key must exist and not be disabled, else the write is refused, paid, with `ReferencedIdentityKeyNotFoundError` (40123) or `ReferencedIdentityKeyDisabledError` (40124). Identity keys can be disabled but never removed, so a validated reference never dangles. The owner form's identity is the transition's signer, which the transition already proved exists, so the key fetch is its only read; a key id of some other identity's key is meaningless by construction, there is no property to name another identity. On replace the identity form is re-validated when either the identity property or its key id property changed. For the key id form it depends on where the identity comes from. `$ownerId` is the writer, transition metadata that never appears among the changed fields, and the document may have changed hands since the key id was written, so the reference is re-validated on every replace, touched or not, as the `$ownerId` writer gate is: after a transfer the new owner has to repoint the key id at one of its own keys. `$creatorId` never changes, so it is re-validated when the key id changed. A property path is re-validated when the key id or that property changed, and a key id set while the property is not is refused (`ReferencedKeyIdPropertyInvalidError`, 40125). A transfer itself is not checked in any form, so the reference governs writing, not holding. The declaring property must carry exactly the key id range in its schema, whatever the contract's integer sizing setting, and a `keyIdProperty` of the identity form may not name a property that carries this form, nor may a path name an identifier carrying an `identityPublicKey` reference: one pair is declared once (40125 at registration). The charter contract's `joinRequest.senderKeyId`, the owner's encryption key a shared secret is derived from, is the first user.

## Immutable Properties on Mutable Document Types

A document type either allows replaces (`documentsMutable: true`, the default) or freezes its documents entirely. Protocol version 14 adds a middle ground: the doctype-level `immutable` keyword lists top-level properties that are frozen at creation while the rest of the document stays replaceable.
Expand Down
111 changes: 85 additions & 26 deletions packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json
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 4.2 (PROTOCOL V14) IS LIVE ON MAINNET; 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, the requiredSince property keyword (the contract version a property is required from), the timeRange index transform, and typed arrays (an array property whose items schema names one scalar element type instead of byteArray, stored inline as an element count followed by the elements), refuses `-` in property and document type names (word characters only; a census of every contract on mainnet and testnet found none), 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 4.2 is live on mainnet, mutating it would change historical validation results and break consensus replay, and 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 4.2 (PROTOCOL V14) IS LIVE ON MAINNET; 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, for an identityPublicKey reference with identityProperty, on the key id integer property), the requiredSince property keyword (the contract version a property is required from), the timeRange index transform, and typed arrays (an array property whose items schema names one scalar element type instead of byteArray, stored inline as an element count followed by the elements), refuses `-` in property and document type names (word characters only; a census of every contract on mainnet and testnet found none), 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 4.2 is live on mainnet, mutating it would change historical validation results and break consensus replay, and 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 @@ -209,12 +209,29 @@
"additionalProperties": false
},
"keyIdProperty": {
"description": "The property of the same document type whose value carries the referenced key id; the reference property's value carries the identity id",
"description": "identityPublicKey references on an identifier property: the property of the same document type whose value carries the referenced key id; the reference property's value carries the identity id. Takes the place of identityProperty",
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[a-zA-Z0-9_]{1,64}(\\.[a-zA-Z0-9_]{1,64})*$"
},
"identityProperty": {
"description": "identityPublicKey references on the key id property itself, the inverse form: the declaring property is an integer from 0 to 4294967295 (a key id) whose value must be the id of a key of the named identity that exists and is not disabled when the document is written (ReferencedIdentityKeyNotFoundError, 40123; ReferencedIdentityKeyDisabledError, 40124). \"$ownerId\" names the writer, the document's owner, and \"$creatorId\" its creator (only on a document type that records creator ids, checked at contract registration), whose existence is already proven, so only the key is fetched; any other value is the path of an identifier property of the same document type (it must exist, be an identifier and not carry an identityPublicKey reference of its own, checked at registration), whose value is the identity, and a key id set while it is not is refused (ReferencedKeyIdPropertyInvalidError, 40125). Takes the place of keyIdProperty",
"anyOf": [
{
"enum": [
"$ownerId",
"$creatorId"
]
},
{
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[a-zA-Z0-9_]{1,64}(\\.[a-zA-Z0-9_]{1,64})*$"
}
]
},
"keyRequirements": {
"description": "identityPublicKey references only: what the referenced key must be beyond existing and not being disabled, checked when the referring document is written against the key already fetched for the existence check, so a requirement costs no further read. Each key names an aspect of the referenced key and its value the requirement: purpose requires the key's purpose to be the named one (any but system); boundTo names a document type of the declaring contract and requires the key's contract bounds to be exactly the declaring contract and that document type (a whole-contract or contract group bound never meets it). At registration boundTo must name a document type the contract has, and one a key of the required purpose can be bound to: only authentication, encryption and decryption keys carry a document type bound, and an encryption or decryption key only where the type declares requiresIdentityEncryptionBoundedKey or requiresIdentityDecryptionBoundedKey. An unmet requirement refuses the write (ReferencedIdentityKeyRequirementNotMetError, 40136)",
"type": "object",
Expand Down Expand Up @@ -300,11 +317,26 @@
"required": ["type"]
},
"then": {
"required": ["type", "keyIdProperty"]
"oneOf": [
{
"required": ["type", "keyIdProperty"],
"properties": {
"identityProperty": false
}
},
{
"required": ["type", "identityProperty"],
"properties": {
"keyIdProperty": false,
"propertyAgreement": false
}
}
]
},
"else": {
"properties": {
"keyIdProperty": false,
"identityProperty": false,
"keyRequirements": false
}
}
Expand Down Expand Up @@ -477,31 +509,58 @@
]
},
"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
"description": "refersTo is only allowed on identifier properties, except an identityPublicKey reference with identityProperty, which sits on the key id property: an integer with minimum 0 and maximum 4294967295, the range of a key id",
"if": {
"properties": {
"refersTo": {
"required": ["identityProperty"]
}
}
},
"required": [
"type",
"byteArray",
"contentMediaType",
"minItems",
"maxItems"
]
"then": {
"properties": {
"type": {
"const": "integer"
},
"minimum": {
"const": 0
},
"maximum": {
"const": 4294967295
}
},
"required": [
"type",
"minimum",
"maximum"
]
},
"else": {
"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"
]
}
},
"encryptedFor": {
"description": "encryptedFor is only allowed on byte array properties that are not identifiers",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,17 @@ impl DocumentType {

for (name, document_type) in &contract_document_types {
for (path, property) in document_type.as_ref().flattened_properties() {
let DocumentPropertyType::IdentifierWithReference(
DocumentPropertyReferenceTarget::IdentityPublicKey {
key_requirements, ..
},
) = &property.property_type
else {
continue;
// Both forms of the key reference carry the same requirements
let key_requirements = match &property.property_type {
DocumentPropertyType::IdentifierWithReference(
DocumentPropertyReferenceTarget::IdentityPublicKey {
key_requirements, ..
},
) => key_requirements,
DocumentPropertyType::KeyIdWithReference(reference) => {
&reference.key_requirements
}
_ => continue,
};
let Some(bound_to) = &key_requirements.bound_to else {
continue;
Expand Down Expand Up @@ -148,7 +152,9 @@ mod tests {
use crate::consensus::ConsensusError;
use crate::data_contract::accessors::v0::DataContractV0Getters;
use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0;
use crate::data_contract::document_type::IdentityKeyReferenceRequirements;
use crate::data_contract::document_type::{
IdentityKeyReferenceRequirements, KeyIdReference, KeyReferenceIdentityProperty,
};
use crate::data_contract::DataContract;
use crate::identity::Purpose;
use crate::serialization::{
Expand Down Expand Up @@ -303,6 +309,69 @@ mod tests {
);
}

/// [`contract_value`] with the reference moved onto the key id property: `recipientKeyId`
/// carries `refers_to` (an `identityProperty` form) and `recipientId` is a plain identifier.
fn contract_value_with_key_id_reference(refers_to: Value) -> Value {
let mut value = contract_value(platform_value!({ "type": "identity" }));
let recipient_id = value
.get_mut_value_at_path("documentSchemas.joinRequest.properties.recipientId")
.expect("the recipientId schema");
recipient_id
.remove("refersTo")
.expect("the identity reference removes");
let recipient_key_id = value
.get_mut_value_at_path("documentSchemas.joinRequest.properties.recipientKeyId")
.expect("the recipientKeyId schema");
recipient_key_id
.insert("maximum".to_string(), Value::U64(u64::from(u32::MAX)))
.expect("the maximum inserts");
recipient_key_id
.insert("refersTo".to_string(), refers_to)
.expect("the reference inserts");
value
}

/// The bound check reads the key id form's requirements too.
#[test]
fn should_check_bound_to_on_the_key_id_form() {
let platform_version = PlatformVersion::latest();

let contract = DataContract::from_value(
contract_value_with_key_id_reference(platform_value!({
"type": "identityPublicKey",
"identityProperty": "$ownerId",
"keyRequirements": { "purpose": "decryption", "boundTo": "submittedCharter" }
})),
true,
platform_version,
)
.expect("the contract should parse");
assert_eq!(
contract
.document_type_for_name("joinRequest")
.expect("the joinRequest document type")
.flattened_properties()
.get("recipientKeyId")
.map(|p| p.property_type.clone()),
Some(DocumentPropertyType::KeyIdWithReference(KeyIdReference {
identity_property: KeyReferenceIdentityProperty::OwnerId,
key_requirements: IdentityKeyReferenceRequirements {
purpose: Some(Purpose::DECRYPTION),
bound_to: Some("submittedCharter".to_string()),
},
}))
);

assert_eq!(
refusal_message(contract_value_with_key_id_reference(platform_value!({
"type": "identityPublicKey",
"identityProperty": "$ownerId",
"keyRequirements": { "purpose": "decryption", "boundTo": "electedCharter" }
}))),
"joinRequest.recipientKeyId refersTo keyRequirements boundTo \"electedCharter\" names no document type of this contract"
);
}

#[test]
fn should_reject_a_bound_to_no_key_could_ever_carry() {
// Only authentication, encryption and decryption keys carry a document type bound
Expand Down
Loading
Loading