Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
18dda42
doc: reference validation spec
lklimek Jan 15, 2026
802d10e
chore: minor improvement
lklimek Jan 15, 2026
06b4e90
doc
lklimek Jan 15, 2026
93902c5
spec update
lklimek Jan 15, 2026
3d5c0b7
feat: add refersTo to document schema
lklimek Jan 15, 2026
dbd9616
chore(dpp): add DocumentProperty.reference
lklimek Jan 15, 2026
69bb293
chore(dpp): define ReferencedEntityNotFoundError
lklimek Jan 15, 2026
eeb4334
fix(scripts): configure_test_suite_network.sh not working
lklimek Jan 19, 2026
2eab1ca
chore: implemented?
lklimek Jan 20, 2026
fdd45af
Merge branch 'v3.0-dev' into feat/reference-validation
lklimek Jan 20, 2026
0385784
test: fix stack size
lklimek Jan 20, 2026
184d1ec
tests 1
lklimek Jan 20, 2026
f153446
test(dpp): refersTo tests
lklimek Jan 21, 2026
90a76ef
test: more tests
lklimek Jan 21, 2026
c5036a3
fix(wasm-dpp): add error variant to fix build
lklimek Jan 21, 2026
0caedb4
Merge remote-tracking branch 'origin/v3.0-dev' into feat/reference-va…
lklimek Jan 21, 2026
858ad36
fix(dpp): order matters in dpp enums
lklimek Jan 22, 2026
02125ff
fix(platform): err when no required identifier
lklimek Jan 22, 2026
1fc3406
Merge branch 'v3.0-dev' into feat/reference-validation
lklimek Jan 22, 2026
84c5600
chore: schema verification
lklimek Jan 22, 2026
d1cbd32
chore: test renames
lklimek Jan 22, 2026
364305d
Update docs/specs/reference-validation.md
lklimek Jan 22, 2026
38f2715
Merge remote-tracking branch 'origin/feat/reference-validation' into …
lklimek Jan 22, 2026
5b92420
Merge branch 'v3.1-dev' into feat/reference-validation
QuantumExplorer Jan 24, 2026
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
48 changes: 48 additions & 0 deletions docs/specs/reference-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Reference Validation (`refersTo`)

## Summary
Introduce an optional `refersTo` keyword on document properties so contracts can request existence checks for referenced identities. When present, state validation rejects documents whose referenced identity does not exist. Contracts and documents without `refersTo` are unchanged.

## Schema Changes
- Extend document meta-schema (`packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json`) to allow a `refersTo` object alongside existing keywords.
- `refersTo` structure:
- `type`: `"identity"` (current target; keep the mechanism extensible for other reference targets such as documents or contracts).
- `mustExist`: `boolean` (optional, defaults to `true`; `false` means no reference validation).
- Validation rules during schema parsing:
- Only allowed on identifier-typed properties (array byteArray=true, minItems=32, maxItems=32, `contentMediaType` identifier).
- Not allowed on non-identifier properties (reject contract).
- Mutability: `refersTo` may only be set at contract creation; contract updates cannot add or change it.
- JSON Schema compatibility rules must allow the keyword but should reject updates that attempt to add/modify it post-creation.

## Data Model / Parsing
- During `DocumentType::try_from_schema`, detect `refersTo` and store per-property reference metadata (path → `{ targetType: Identity, mustExist: bool }`).
- Expose reference metadata through DocumentType accessors and WASM/JS bindings so clients can introspect.
- Keep existing `identifier_paths`/`binary_paths` behavior (the sets of property paths already tracked for identifier and binary fields); `refersTo` is additive on top.

## Runtime Validation
- Enforce during Drive document state validation (create/replace state validators) for document create and replace transitions:
- For each property with `refersTo.mustExist == true`, fetch the referenced identity ID and fail with a consensus state error if missing.
- Support nested properties (use flattened property paths).
- Count identity fetches in execution context fee accounting.
- Implement via versioned document state validators (new v2 modules) while keeping v0/v1 behavior unchanged.
- Applied in ABCI paths: CheckTx, PrepareProposal, and ProcessProposal.
- Basic validation (DPP) only checks keyword shape/placement; no state access.

## Errors
- Add a dedicated consensus state error, e.g., `ReferencedEntityNotFoundError { path, identityId }`.
- Avoid overloading signature errors; ensure deterministic mapping to codes.

## Backward Compatibility
- Gated by platform/protocol version (and/or data contract system version). Legacy nodes reject contracts containing `refersTo`; such contracts are accepted only after activation. Post-activation, newer nodes enforce `mustExist:true` semantics.
- Existing pre-activation contracts and documents remain valid; documents are rejected only when the contract opts in with `mustExist:true` and the network is past activation.

## Implementation Notes
- Reference existence checks use the identity revision lookup (`fetch_identity_revision`) as the minimal-cost existence check.
- Reference validation dispatches through a versioned `DocumentReferenceValidation` trait; v0 rules are implemented for identity references.

## Acceptance Criteria
- Contracts containing `refersTo` validate against updated meta-schema and pass compatibility checks when added to existing identifier fields.
- Documents with `refersTo.type=identity` + `mustExist:true` are accepted only if the referenced identities exist; missing ones return the new consensus error.
- Documents without `refersTo`, or with `mustExist:false`, behave exactly as today.
- Enforcement applies to create and replace transitions (including nested fields) with proper fee accounting for identity lookups.
- WASM/JS bindings serialize/deserialize `refersTo` metadata; tests cover parsing and state validation failures.
2 changes: 1 addition & 1 deletion packages/dapi-grpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,6 @@ ignored = [
"platform-version",
"futures-core",
"tonic-prost-build",
"getrandom", # Ignore getrandom as we need it to enable `js` feature
"getrandom", # Ignore getrandom as we need it to enable `js` feature
"dash-platform-macros", # used in build.rs
]
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,25 @@
"minItems": 1,
"uniqueItems": true
},
"refersTo": {
"type": "object",
"properties": {
"type": {
"enum": [
"identity",
"contract"
]
},
"mustExist": {
"type": "boolean",
"default": true
}
},
"required": [
"type"
],
"additionalProperties": false
},
"type": {
"$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/type"
},
Expand Down Expand Up @@ -203,6 +222,33 @@
"required": [
"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"
]
}
},
"allOf": [
Expand Down Expand Up @@ -606,4 +652,4 @@
"properties",
"additionalProperties"
]
}
}
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, DocumentPropertyReference, DocumentPropertyReferenceTarget,
DocumentPropertyType, DocumentType,
};
use crate::data_contract::errors::DataContractError;
use crate::data_contract::{TokenConfiguration, TokenContractPosition};
Expand Down Expand Up @@ -113,7 +114,10 @@ fn insert_values(
let is_required = known_required.contains(&prefixed_property_key);
let is_transient = known_transient.contains(&prefixed_property_key);

match DocumentPropertyType::try_from_value_map(&inner_properties, &config.into())? {
let property_type =
DocumentPropertyType::try_from_value_map(&inner_properties, &config.into())?;

match property_type {
DocumentPropertyType::Object(_) => {
if let Some(properties_as_value) = inner_properties.get(property_names::PROPERTIES)
{
Expand All @@ -140,12 +144,14 @@ fn insert_values(
}
}
property_type => {
let reference = parse_property_reference(&inner_properties, &property_type)?;
document_properties.insert(
prefixed_property_key,
DocumentProperty {
property_type,
required: is_required,
transient: is_transient,
reference,
},
);
}
Expand Down Expand Up @@ -250,14 +256,221 @@ fn insert_values_nested(
property_type => property_type,
};

let reference = parse_property_reference(&inner_properties, &property_type)?;

document_properties.insert(
property_key,
DocumentProperty {
property_type,
required: is_required,
transient: is_transient,
reference,
},
);

Ok(())
}

fn parse_property_reference(
inner_properties: &BTreeMap<String, &Value>,
property_type: &DocumentPropertyType,
) -> Result<Option<DocumentPropertyReference>, DataContractError> {
let Some(refers_to_value) = inner_properties.get(property_names::REFERS_TO) else {
return Ok(None);
};

if !matches!(property_type, DocumentPropertyType::Identifier) {
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,
other => {
return Err(DataContractError::InvalidContractStructure(format!(
"invalid refersTo type {other}"
)))
}
};

let must_exist = refers_to_map
.get_optional_bool("mustExist")
.map_err(|e| DataContractError::ValueWrongType(e.to_string()))?
.unwrap_or(true);

Ok(Some(DocumentPropertyReference { target, must_exist }))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::data_contract::config::DataContractConfig;
use crate::data_contract::document_type::accessors::DocumentTypeV0Getters;
use platform_value::Identifier;
use platform_version::version::PlatformVersion;
use serde_json::json;
use std::collections::BTreeMap;

#[test]
fn should_parse_refers_to_on_identifier_property() {
let platform_version = PlatformVersion::latest();
let config =
DataContractConfig::default_for_version(platform_version).expect("config should build");

let 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
});

let value = platform_value::to_value(schema).expect("schema should convert");

let document_type = DocumentType::try_from_schema(
Identifier::random(),
0,
config.version(),
"msg",
value,
None,
&BTreeMap::new(),
&config,
false,
&mut vec![],
platform_version,
)
.expect("should parse");

let reference = document_type
.as_ref()
.flattened_properties()
.get("toUserId")
.and_then(|p| p.reference.clone())
.expect("reference should be present");

assert!(matches!(
reference.target,
DocumentPropertyReferenceTarget::Identity
));
assert!(reference.must_exist);
}

#[test]
fn should_reject_refers_to_on_non_identifier_property() {
let platform_version = PlatformVersion::latest();
let config =
DataContractConfig::default_for_version(platform_version).expect("config should build");

let schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"position": 0,
"refersTo": { "type": "identity" }
}
},
"required": [],
"additionalProperties": false
});

let value = platform_value::to_value(schema).expect("schema should convert");

let err = DocumentType::try_from_schema(
Identifier::random(),
0,
config.version(),
"msg",
value,
None,
&BTreeMap::new(),
&config,
false,
&mut vec![],
platform_version,
)
.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_parse_refers_to_with_must_exist_false() {
let platform_version = PlatformVersion::latest();
let config =
DataContractConfig::default_for_version(platform_version).expect("config should build");

let 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",
"mustExist": false
}
}
},
"required": [],
"additionalProperties": false
});

let value = platform_value::to_value(schema).expect("schema should convert");

let document_type = DocumentType::try_from_schema(
Identifier::random(),
0,
config.version(),
"msg",
value,
None,
&BTreeMap::new(),
&config,
false,
&mut vec![],
platform_version,
)
.expect("should parse");

let reference = document_type
.as_ref()
.flattened_properties()
.get("toUserId")
.and_then(|p| p.reference.clone())
.expect("reference should be present");

assert!(matches!(
reference.target,
DocumentPropertyReferenceTarget::Identity
));
assert!(!reference.must_exist);
}
}
Loading
Loading