Skip to content
Closed
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
3 changes: 2 additions & 1 deletion book/src/contract-keywords/property-constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
- **Name order, first failure.** Rules are checked in the order of their names, and the first rule the document breaks refuses the transition with `DocumentPropertyConstraintViolatedError` (10422). The error names the document type, the rule, and why it failed (below).
- **Transfer and purchase.** These change only the owner and the transfer's time and heights. Rules that read `$ownerId`, `$transferredAt…` or a total that depends on the owner are judged again, against the stored document with its new owner and transfer values; other rules are not, since nothing they read changed. A transfer or purchase that would break such a rule is refused with 10422.
- **Price updates** change only the update's time and heights, so the rules that read `$updatedAt…` are judged again the same way; other rules are not.
- **Moderator restores.** A document a [moderator restores](../data-model/contract-moderation.md#restoring-documents) is judged as a create: every rule, with its owner and the times and heights it comes back with, and every total as it will be once the document is back. Other documents may have filled a cap or a budget while it was gone; a restore that would break a rule is refused with 10422.
- **Immutable properties.** The same grammar is the condition of an [`immutable`](mutability.md#immutable) entry, which freezes a property while it holds. Only such a condition may read the stored document, through `$old.<path>`; a rule judges creates too, which have none.
- **Deletes** are not judged, with one exception: a delete of an [index-only](index-only.md) document carries the row's values, which are validated like a create's, rules included. The delete carries neither the owner nor any time or height, which is why an index-only type may not have a rule reading `$ownerId` or a system time or height. What may be deleted is the job of [`deleteConstraints`](deletion.md#deleteconstraints), rules in this grammar the stored document must meet for its owner to delete it.
- **State and fees.** A rule reads the document, its owner and its times and heights, and a `countOf` or `sumOf` reads a total from state. Each such total is a state read billed with the write; nothing else a rule does adds a fee, and it changes nothing stored. The limits below bound its cost. SDKs that validate a document before sending it apply the same rules, except those reading a total, which they cannot read.
Expand Down Expand Up @@ -199,7 +200,7 @@ A filter maps each key, a property of the counted type or `$ownerId`, to the val
```

- **As it will be after the write.** The total is the stored one with the write applied. When the counted type is the rule's own, a create adds the document, a replace swaps its stored version for the new one, and a transfer or purchase moves it to its new owner. So `atMostTenListings`, declared on `listing`, keeps every owner at ten or fewer, and a replace of one of ten is allowed.
- **Judged when the rule's own type is written.** A rule is never judged on writes of the type it counts. On its own type it holds for good, since every write that could raise the total is judged; a type with a contested index cannot total its own documents, since a document a contest awards is stored without any rule judged. On another type it is only checked when its own type is written, and can go stale later: deleting a `profile` does not undo a `post` that needed one. Deletes are not judged, so a lower bound can be broken by deleting documents, unless the counted type's [`deleteConstraints`](deletion.md#deleteconstraints) hold it.
- **Judged when the rule's own type is written.** A rule is never judged on writes of the type it counts. On its own type it holds for good, since every write that could raise the total is judged, a moderator's restore included; a type with a contested index cannot total its own documents, since a document a contest awards is stored without any rule judged. On another type it is only checked when its own type is written, and can go stale later: deleting a `profile` does not undo a `post` that needed one. Deletes are not judged, so a lower bound can be broken by deleting documents, unless the counted type's [`deleteConstraints`](deletion.md#deleteconstraints) hold it.
- **Transfers, purchases and price updates.** A total that depends on the owner (a filter value of `$ownerId`, or a `$ownerId` key on the rule's own type) is read again for a transfer or purchase, the document counted toward its new owner. A rule a price update judges, one reading `$updatedAt…`, reads its totals too.
- **Billed.** Each total is a state read billed with the write. A total two rules read alike is read once.
- **Every earlier write counts.** A document batch carries one transition, and each state transition of a block is applied before the next is validated, so a total includes every write before it.
Expand Down
2 changes: 1 addition & 1 deletion book/src/data-model/contract-moderation.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ ContractUserModerationAction::RestoreDocument {
}
```

It names no identity and no id: the document is inside the bytes, and its id and owner with it. The transform checks, in order and each refusal paid: the document type exists (10406) and carries `moderatorAbilities.delete` (41115, only such a type keeps records), the signer is the owner or a moderator (41101, whoever deleted), the bytes decode under the type (a basic decoding error, never an execution error: the bytes are the signer's), the document has a removal record (`ContractDocumentRemovalNotFoundError`, 41119) that is not marked restored (`ContractDocumentAlreadyRestoredError`, 41122: the document is live), block time is within `SystemLimits::contract_document_restore_window_ms` of the removal, a week, the last millisecond included (`DocumentRestoreWindowElapsedError`, 41120), the bytes hash to what the record holds (`DocumentRestoreHashMismatchError`, 41121: the document as it was, not an edit of it), and no other document of the type holds a value of one of its unique indexes (`DuplicateUniqueIndexError`, 40105: the value was free while the document was gone, and someone may have taken it). The record read is billed; the hash pins everything else, so the owner, the revision, the timestamps, the references and the schema are not checked again. Whether the document's owner is banned is not asked either: a ban stops writes, it never removed documents.
It names no identity and no id: the document is inside the bytes, and its id and owner with it. The transform checks, in order and each refusal paid: the document type exists (10406) and carries `moderatorAbilities.delete` (41115, only such a type keeps records), the signer is the owner or a moderator (41101, whoever deleted), the bytes decode under the type (a basic decoding error, never an execution error: the bytes are the signer's), the document has a removal record (`ContractDocumentRemovalNotFoundError`, 41119) that is not marked restored (`ContractDocumentAlreadyRestoredError`, 41122: the document is live), block time is within `SystemLimits::contract_document_restore_window_ms` of the removal, a week, the last millisecond included (`DocumentRestoreWindowElapsedError`, 41120), the bytes hash to what the record holds (`DocumentRestoreHashMismatchError`, 41121: the document as it was, not an edit of it), the document meets every rule of the type's [`propertyConstraints`](../contract-keywords/property-constraints.md) as a create would, its `countOf` and `sumOf` totals read as they will be once it is back (`DocumentPropertyConstraintViolatedError`, 10422: the document re-enters every count and sum its deletion took it out of, and other documents may have filled a cap or a budget meanwhile), and no other document of the type holds a value of one of its unique indexes (`DuplicateUniqueIndexError`, 40105: the value was free while the document was gone, and someone may have taken it). The record read and the totals read are billed; the hash pins everything else, so the owner, the revision, the timestamps, the references and the schema are not checked again. Whether the document's owner is banned is not asked either: a ban stops writes, it never removed documents.

The action carries the contract, the decoded document and the record marked restored by the signer at the block's time, so Drive puts the document back through the ordinary insert (`DocumentOperationType::AddDocument`, every index and aggregate of the type included) and replaces the record in place, without reading again. The restored document's storage flags name its owner, as they did before the deletion: the signer pays for the bytes, and the refund of a later deletion is the owner's, as it always was. Nothing the document type prices is charged, neither its creation token cost nor its `actionFees` creation fee, and no fee agreement is asked: a moderator undoes a moderation, it does not create content. The restore is proved by the record, now marked restored and holding the hash of the bytes the transition carried; the verifier reads the document's id out of those bytes under the contract's document type, so it needs the contract, which the SDKs register with their context provider before broadcasting.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ mod transformer;
// document's own deletion does, billed the same.
pub(in crate::execution::validation::state_transition) use state::v0::fetch_documents::fetch_document_with_id;
// A moderator's field change judges the changed document by its type's `propertyConstraints`
// as a replace does, with the totals they read.
pub(in crate::execution::validation::state_transition) use transformer::v0::property_constraint_aggregates::read_property_constraint_aggregates_for_moderator_change;
// as a replace does, and a moderator's restore judges the restored document as a create does,
// with the totals they read.
pub(in crate::execution::validation::state_transition) use transformer::v0::property_constraint_aggregates::{
read_property_constraint_aggregates_for_moderator_change,
read_property_constraint_aggregates_for_restore,
};

#[cfg(test)]
mod tests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,55 @@ pub(crate) fn read_property_constraint_aggregates_for_moderator_change(
)
}

/// Reads the `countOf` and `sumOf` totals the `propertyConstraints` rules of the document type
/// `document_type_name` read for a moderator's restore of `restored`, whose properties are
/// `restored_data` (one map value, which the caller judges the rules on too), each as it will
/// be once the document is back: the total the count or sum tree keeps now, plus what
/// `restored` adds to it, as for a create under its own owner. A filter reads its values (its
/// id, its owner, its properties) from `restored`. The reads are billed to
/// `execution_context`.
///
/// Only the restore transform of `ContractUserModeration` calls it (protocol version 14); a
/// type whose rules read no total reads nothing. Every state transition of a block is applied
/// before the next is validated, so no earlier write of the block is missing from the totals.
#[allow(clippy::too_many_arguments)]
pub(crate) fn read_property_constraint_aggregates_for_restore(
drive: &Drive,
contract: &DataContract,
document_type_name: &str,
restored: &Document,
restored_data: &Value,
block_info: &BlockInfo,
execution_context: &mut StateTransitionExecutionContext,
transaction: TransactionArg,
platform_version: &PlatformVersion,
) -> Result<BTreeMap<AggregateRead, i128>, Error> {
let Some(document_type) = contract.document_type_optional_for_name(document_type_name) else {
// The restore refuses a document type the contract lacks
return Ok(BTreeMap::new());
};
let reads = document_type
.property_constraints()
.values()
.flat_map(|rule| rule.aggregate_reads())
.collect::<BTreeSet<_>>();
read_aggregates(
drive,
contract,
reads,
Some(VersionData {
id: restored.id(),
data: restored_data,
owner_id: restored.owner_id(),
}),
None,
block_info,
execution_context,
transaction,
platform_version,
)
}

/// Reads from state the `countOf` and `sumOf` totals the `propertyConstraints` rules of
/// the document type `document_type_name` read, for a write storing `written` in place of
/// `stored` (`None` for a create), each as it will be once the write is done: the total the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::execution::validation::state_transition::common::validate_document_no
use crate::execution::validation::state_transition::common::validate_identity_exists::validate_identity_exists;
use crate::execution::validation::state_transition::state_transitions::batch::{
fetch_document_with_id, read_property_constraint_aggregates_for_moderator_change,
read_property_constraint_aggregates_for_restore,
};
use crate::platform_types::platform::PlatformRef;
use crate::rpc::core::CoreRPCLike;
Expand Down Expand Up @@ -1665,8 +1666,11 @@ fn transform_document_fields_change_v0<C: CoreRPCLike>(
/// under the type, the type keeps removal records and the document has one that is not yet
/// restored, block time is within the restore window after the removal, the bytes hash to what
/// the record holds, the removal is no deletion the seated team approved together (those
/// stand), and no other document holds a value of one of the type's unique indexes. Every refusal is paid
/// for by bumping the signer's contract nonce.
/// stand), the document meets the type's `propertyConstraints` as a create would, with the
/// `countOf` and `sumOf` totals as they will be once it is back
/// (`DocumentPropertyConstraintViolatedError`, 10422), and no other document holds a value of
/// one of the type's unique indexes. Every refusal is paid for by bumping the signer's contract
/// nonce.
///
/// The action carries the contract, the decoded document and the record marked restored, so
/// Drive puts the document back and marks the record without reading again. Nothing the
Expand Down Expand Up @@ -1902,7 +1906,43 @@ fn transform_document_restore_v0<C: CoreRPCLike>(
return refuse(error);
}

// What the hash does not pin: another document may have taken a value of one of the
// The hash does not pin the `countOf` and `sumOf` totals the type's `propertyConstraints` read:
// the restore puts the document back into every count and sum its deletion took it out
// of, and other documents may have taken the room it left meanwhile. So the document is
// judged as a create judges one, by every rule, under its own owner and with the times and
// heights it comes back with, the totals read, billed, as they will be once it is back.
// The rules not reading a total held when the document was last written and still do: it
// comes back byte for byte, and the rules are fixed with the type.
if !document_type.property_constraints().is_empty() {
let data = Value::from(document.properties().clone());
let aggregates = read_property_constraint_aggregates_for_restore(
platform.drive,
contract,
document_type_name,
&document,
&data,
block_info,
execution_context,
tx,
platform_version,
)?;
let result = document_type.validate_property_constraints(
&data,
&DocumentSystemValues {
aggregates: Some(aggregates),
..DocumentSystemValues::of_document(&document)
},
platform_version,
)?;
if !result.is_valid() {
return Ok(ConsensusValidationResult::new_with_data_and_errors(
bump_action(),
result.errors,
));
}
}

// What the hash does not pin either: another document may have taken a value of one of the
// type's unique indexes while the document was gone, and would clash with it.
if document_type.indexes().values().any(|index| index.unique) {
let uniqueness = platform.drive.validate_moderated_document_uniqueness(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ mod duplicate_keys;
mod moderated_document_reference;
mod moderator_fields;
mod preallocated_through_moderated_reference;
mod restore_property_constraints;
mod retraction;
mod seated_team;

Expand Down
Loading
Loading