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
2 changes: 1 addition & 1 deletion book/src/contract-keywords/refers-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ The "every replace" rows exist because something the reference depends on can ch
### Transfers, purchases, deletes and restores

- A transfer or a purchase checks no reference. A reference governs writing, not holding: a new owner meets the writer gates (a `where` entry valued `"$ownerId"`, `identityProperty: "$ownerId"`, an `owner` requirement) on their first replace.
- Deleting a referring document checks nothing. Deleting a referenced document does not look for documents referring to it: a `permanentDocument` target cannot be deleted at all, a `moderatedDocument` target is removed by a moderator on the record the reference then resolves to, and a `deletableDocument` reference meets its missing target on the referring document's next replace.
- Deleting a referring document checks nothing. Deleting a referenced document does not look for documents referring to it: a `permanentDocument` target cannot be deleted at all, a `moderatedDocument` target is removed by a moderator on the record the reference then resolves to, and a `deletableDocument` reference meets its missing target on the referring document's next replace. A `deletableDocument` target can still refuse its owner's delete while documents refer to it: a [`deleteConstraints`](deletion.md#deleteconstraints) rule counting them by the target's own id, `{ "equal": [{ "countOf": ["vote", { "pollId": "$id" }] }, 0] }`, needs a countable index on the referring property, and holds the target in place until the last of them is gone.
- A document a moderator removed and later restores comes back as it was, without its references being checked again (see [Restoring Documents](../data-model/contract-moderation.md#restoring-documents)).

## The reference budget
Expand Down
2 changes: 1 addition & 1 deletion book/src/data-model/documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ The same grammar gates the owner's delete. `deleteConstraints` is a doctype keyw
}
```

The parser (`apply_delete_constraints`, generation 3, versioned with `apply_property_constraints` on `parse_property_constraints`) reads the rules with `parse_delete_constraints`, the same parse as `parse_property_constraints` under another key, checks what they read with `validate_property_constraint_reads` and, under full validation, holds them to the same `SystemLimits`, counted apart from `propertyConstraints`. On every parse it refuses the keyword on a type whose owner never deletes a stored document: `canBeDeleted: false`, `"onlyWhenConsumed"` or `indexOnly`. `validate_property_constraint_aggregates` checks their `countOf` and `sumOf` with the contract's other types, except that a delete rule may total its own contested type, since the total it reads is the stored one. `DocumentReferenceLookup::referenced_side_error` refuses a `consume` into such a type, which would delete without a delete transition. The rules are frozen on update by the same compatibility rule as `propertyConstraints` (10246).
The parser (`apply_delete_constraints_v0`, generation 3, called by `apply_property_constraints` so one dispatch on `parse_property_constraints` covers both keywords) reads the rules with `parse_delete_constraints`, the same parse as `parse_property_constraints` under another key, checks what they read with `validate_property_constraint_reads` and, under full validation, holds them to the same `SystemLimits`, counted apart from `propertyConstraints`. On every parse it refuses the keyword on a type whose owner never deletes a stored document: `canBeDeleted: false`, `"onlyWhenConsumed"` or `indexOnly`. `validate_property_constraint_aggregates` checks their `countOf` and `sumOf` with the contract's other types, except that a delete rule may total its own contested type, since the total it reads is the stored one. `DocumentReferenceLookup::referenced_side_error` refuses a `consume` into such a type, which would delete without a delete transition. The rules are frozen on update by the same compatibility rule as `propertyConstraints` (10246).

Enforcement is document delete state validation 1 (protocol version 14, `DRIVE_ABCI_VALIDATION_VERSIONS_V10`): the checks of version 0 (the base transition, the stored document, its owner), then, for a type with rules, `read_delete_constraint_aggregates` reads the totals (billed, each as it will be once the document is gone: the stored total less what the document adds to it) and `DocumentTypeV0Methods::validate_delete_constraints` judges each rule in name order on the stored document's properties and `DocumentSystemValues::of_document`. The first rule broken refuses the delete with `DocumentDeleteConstraintViolatedError` (state code 40147), naming the document, the rule and why, so the delete is paid and the nonce spent. In Rust the rules are `DocumentTypeV2Getters::delete_constraints`, and a filter value `$id` is `AggregateBinding::Id`, which `AggregateRead::filter_values` reads from the document's id.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2650,9 +2650,10 @@ pub(super) fn validate_generated_from_declarations(
///
/// Only parser generation 3 calls it, once the core parse has run the
/// meta-schema, so under full validation a malformed declaration is the
/// meta-schema's to report. Versioned on `parse_property_constraints` in the
/// platform version's document type schema versions: `None` selects the
/// behavior of the versions that predate the keyword, which ignore it
/// meta-schema's to report. Version 0 also reads `deleteConstraints`
/// ([`apply_delete_constraints_v0`]). Versioned on `parse_property_constraints`
/// in the platform version's document type schema versions: `None` selects the
/// behavior of the versions that predate the keywords, which ignore them
/// entirely.
pub(super) fn apply_property_constraints(
document_type: &mut DocumentTypeV2,
Expand Down Expand Up @@ -2715,7 +2716,14 @@ fn apply_property_constraints_v0(
}

document_type.property_constraints = constraints;
Ok(())
// The rules gating the owner's delete, in the same grammar and under the same version
apply_delete_constraints_v0(
document_type,
schema_defs,
document_type_name,
full_validation,
platform_version,
)
}

/// Checks one keyword's rules against the limits of `SystemLimits`: at most
Expand Down Expand Up @@ -2779,37 +2787,10 @@ fn validate_rule_limits(
/// `canBeDeleted: false` or `"onlyWhenConsumed"`, where there is no delete to
/// gate, and not indexOnly, whose delete carries the row's values instead of
/// naming a stored document. The keyword arrives with meta-schema v3 and
/// protocol version 14, so no stored contract breaks either rule. Versioned
/// with [`apply_property_constraints`], on `parse_property_constraints`:
/// `None` ignores the keyword, which no earlier meta-schema admits.
pub(super) fn apply_delete_constraints(
document_type: &mut DocumentTypeV2,
schema_defs: Option<&BTreeMap<String, Value>>,
document_type_name: &str,
full_validation: bool,
platform_version: &PlatformVersion,
) -> Result<(), DataContractError> {
match platform_version
.dpp
.contract_versions
.document_type_versions
.schema
.parse_property_constraints
{
None => Ok(()),
Some(0) => apply_delete_constraints_v0(
document_type,
schema_defs,
document_type_name,
full_validation,
platform_version,
),
Some(version) => Err(DataContractError::Unsupported(format!(
"parse_property_constraints version {version} is not supported"
))),
}
}

/// protocol version 14, so no stored contract breaks either rule. Called by
/// `apply_property_constraints_v0`, so one dispatch on `parse_property_constraints`
/// parses both keywords: `None` ignores this one too, which no earlier
/// meta-schema admits.
fn apply_delete_constraints_v0(
document_type: &mut DocumentTypeV2,
schema_defs: Option<&BTreeMap<String, Value>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ use super::common;
#[cfg(feature = "validation")]
use super::schema_at_path;
use super::{
apply_delete_constraints, apply_property_constraints, parse_doctype_reference,
validate_encrypted_for_declarations, validate_generated_from_declarations,
validate_list_element_sources, validate_reference_lookup_sources,
apply_property_constraints, parse_doctype_reference, validate_encrypted_for_declarations,
validate_generated_from_declarations, validate_list_element_sources,
validate_reference_lookup_sources,
};

mod ranked_prefix_overlap;
Expand Down Expand Up @@ -586,7 +586,10 @@ fn parse_generation_3(
// integer properties they read and their transient flags are known; their
// limits are checked under full validation only. The `enum`s their string
// constants are checked against are read through `$ref`s into the
// contract's `$defs` too.
// contract's `$defs` too. The `deleteConstraints` rules with them, once
// `canBeDeleted` is resolved against the contract default and `indexOnly`
// and `"onlyWhenConsumed"` are applied: only a type whose owner deletes its
// stored documents declares them.
apply_property_constraints(
&mut v2,
schema_defs,
Expand All @@ -595,17 +598,6 @@ fn parse_generation_3(
platform_version,
)
.map_err(consensus_or_protocol_data_contract_error)?;
// The `deleteConstraints` rules the same way, once `canBeDeleted` is resolved
// against the contract default and `indexOnly` and `"onlyWhenConsumed"` are
// applied: only a type whose owner deletes its stored documents declares them.
apply_delete_constraints(
&mut v2,
schema_defs,
name,
full_validation,
platform_version,
)
.map_err(consensus_or_protocol_data_contract_error)?;

// After `apply_index_only`, `apply_immutable_fields` and the parse of the
// references and generated properties: each ability is refused on an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -762,11 +762,13 @@ fn should_hold_a_constant_to_the_enum_of_a_key_declared_by_reference() {
/// create waits in its contest's storage, outside the count and sum trees, and
/// the document a contest awards is stored without the rules being judged, so
/// the rule could be passed. Another type may still total it, as it totals any
/// type whose writes it does not judge.
/// type whose writes it does not judge, and so may the type's own delete rules,
/// which read the stored total at the delete.
#[test]
fn should_refuse_a_total_of_its_own_type_with_a_contested_index() {
let contract = |name_rules: Option<serde_json::Value>,
note_rules: Option<serde_json::Value>| {
note_rules: Option<serde_json::Value>,
name_delete_rules: Option<serde_json::Value>| {
let mut name = json!({
"type": "object",
"documentsMutable": false,
Expand Down Expand Up @@ -797,6 +799,9 @@ fn should_refuse_a_total_of_its_own_type_with_a_contested_index() {
if let Some(rules) = name_rules {
name["propertyConstraints"] = rules;
}
if let Some(rules) = name_delete_rules {
name["deleteConstraints"] = rules;
}
let mut note = json!({
"type": "object",
"properties": {
Expand Down Expand Up @@ -827,10 +832,12 @@ fn should_refuse_a_total_of_its_own_type_with_a_contested_index() {
});

expect_structure_error(
contract(Some(one_per_owner.clone()), None),
"rule \"onePerOwner\" counts \"name\", its own type, which has a contested index",
contract(Some(one_per_owner.clone()), None, None),
"propertyConstraints rule \"onePerOwner\" counts \"name\", its own type, which has a \
contested index",
);
contract(None, Some(one_per_owner)).expect("another type may count it");
contract(None, Some(one_per_owner.clone()), None).expect("another type may count it");
contract(None, None, Some(one_per_owner)).expect("the type's own delete rule may count it");
}

/// A client gives no totals, and a rule reading one is not judged; consensus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1091,20 +1091,20 @@ pub trait DocumentTypeV0Methods: DocumentTypeV0Getters + DocumentTypeV0MethodsVe
}

/// Judges the owner's delete of a stored document, with id `document_id` and properties
/// `data`, against every rule of the document type's `deleteConstraints`, in name order:
/// `data` (a map), against every rule of the document type's `deleteConstraints`, in name
/// order:
/// the first rule the stored document breaks fails with
/// `DocumentDeleteConstraintViolatedError` (40147), naming the document, the rule and
/// why, as `propertyConstraints` would. `system` holds the stored document's owner and
/// its times and heights, and the `countOf` and `sumOf` totals consensus read as they
/// will be once the document is gone. A type with no rule costs nothing, and its data
/// is not copied.
/// will be once the document is gone. A type with no rule costs nothing.
///
/// Versioned with [`Self::validate_property_constraints`]: `None` before protocol
/// version 14, where no parsed document type carries a rule.
fn validate_delete_constraints(
&self,
document_id: Identifier,
data: &BTreeMap<String, Value>,
data: &Value,
system: &DocumentSystemValues,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, ProtocolError>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -906,26 +906,20 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa

/// `validate_delete_constraints` version 0: every rule of the document type's
/// `deleteConstraints` is evaluated against the stored document's `data` with `system`,
/// in name order, and the first one broken is reported. The data is copied into a map
/// value only when the type declares a rule. A rule reading a total consensus did not
/// read ([`PropertyConstraint::unread_aggregate`]) is an error.
/// in name order, and the first one broken is reported. A rule reading a total consensus
/// did not read ([`PropertyConstraint::unread_aggregate`]) is an error.
fn validate_delete_constraints_v0(
&self,
document_id: Identifier,
data: &BTreeMap<String, Value>,
data: &Value,
system: &DocumentSystemValues,
) -> Result<SimpleConsensusValidationResult, ProtocolError>
where
Self: DocumentTypeV2Getters,
{
let rules = self.delete_constraints();
if rules.is_empty() {
return Ok(SimpleConsensusValidationResult::default());
}
let data = Value::from(data.clone());
for (name, constraint) in rules {
for (name, constraint) in self.delete_constraints() {
self.expect_every_aggregate_read(name, constraint, system)?;
if let Some(violation) = constraint.violation(&data, system) {
if let Some(violation) = constraint.violation(data, system) {
return Ok(SimpleConsensusValidationResult::new_with_error(
DocumentDeleteConstraintViolatedError::new(
document_id,
Expand Down
2 changes: 1 addition & 1 deletion packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ pub struct DocumentTypeV2 {
/// protocol version 14), in the grammar of `property_constraints`: a rule
/// reads the stored document, and a `countOf` or `sumOf` total as it will
/// be once the document is gone. Empty on document types that declare
/// none. The parser (`apply_delete_constraints`) admits them only on a type
/// none. The parser (`apply_delete_constraints_v0`) admits them only on a type
/// whose owner may delete its documents, holds what they read to what a
/// `propertyConstraints` rule may read, and refuses a `refersTo` with
/// `consume` at such a type, since a consume would delete without them.
Expand Down
Loading
Loading