diff --git a/book/src/data-model/contract-moderation.md b/book/src/data-model/contract-moderation.md index 815617b59eb..582ce7969ea 100644 --- a/book/src/data-model/contract-moderation.md +++ b/book/src/data-model/contract-moderation.md @@ -294,7 +294,7 @@ The declaration lives in `packages/rs-dpp/src/data_contract/config/moderation/el **The interim block.** The batch transformer's `contract_moderation_gate` v0 runs it before the lists: on an elected contract whose interim is `NotYetUsable`, every document transition of a moderated document type, deletions included (nothing of those types was ever written), is refused, paid, with `ContractModeratedDocumentTypeNotYetUsableError` (41200) and its contract nonce bump, in a block and in the mempool. The lists are read only for the transitions on the other types, and not at all when nothing is left. The interim moderators of the other two kinds moderate through the same transition, the same gate and the same claim as the merged kinds; a moderation transition against a `NotYetUsable` contract fails as by a non-moderator (41101). -**Referencing an elected contract.** A document type that must point at a contract of this kind says so in its reference: `"refersTo": { "type": "contract", "contractRequirements": { "moderation": "elected" } }`. `contractRequirements` holds what the referenced contract must declare beyond existing, each key an aspect of the contract with a closed set of values or a bound: `moderation: "elected"`, or `moderation: "electionOpen"`, which also requires the contract's own election delay to have passed since its creation, or the contract to declare none (the delay between a contract's creation and the first charter against it, so a team cannot be seated before anyone has seen the contract, set by each contract for itself). Both have a user in the charter contract: a charter proposal only needs the target to be `elected`, so teams can form during the notice, and the charter that opens the contest needs its election `electionOpen`; `minimumAgeSeconds`, a number of seconds the reference fixes, which requires the contract's recorded creation time to be at least that far before the block time of the write; and `minimumSecondsSinceUpdate`, the same of the later of the contract's creation and last update times (any update restarts the clock; an elected declaration can not be added by an update, so this one is for other uses than the charter). A contract created before contracts recorded their creation time never meets a duration, its own election delay included. Consensus checks them when the referring document is written, against the contract it has already fetched for the existence check and the block time, so they cost no further read; a contract that exists but does not meet a requirement refuses the write, paid, with `ReferencedContractRequirementNotMetError` (40135) naming the requirement, where a contract that does not exist is still 40120. A changed `contractRequirements` is an incompatible schema change on update, like the rest of a `refersTo`. The charter system contract's `targetContractId` is the first user. +**Referencing an elected contract.** A document type that must point at a contract of this kind says so in its reference: `"refersTo": { "type": "contract", "contractRequirements": { "moderation": "elected" } }`. `contractRequirements` holds what the referenced contract must declare beyond existing, each key an aspect of the contract with a closed set of values or a bound: `moderation: "elected"`, or `moderation: "electionOpen"`, which also requires the contract's own election delay to have passed since its creation, or the contract to declare none (the delay between a contract's creation and the first charter against it, so a team cannot be seated before anyone has seen the contract, set by each contract for itself). Both have a user in the charter contract: a charter proposal only needs the target to be `elected`, so teams can form during the notice, and the charter that opens the contest needs its election `electionOpen`; `minimumAgeSeconds`, a number of seconds the reference fixes, which requires the contract's recorded creation time to be at least that far before the block time of the write; `minimumSecondsSinceUpdate`, the same of the later of the contract's creation and last update times (any update restarts the clock; an elected declaration can not be added by an update, so this one is for other uses than the charter); `owner`, `"self"` requiring the referenced contract to be owned by the writer of the referring document (its `$ownerId`, a write gate like the `$ownerId` property agreement of a document reference) and `"other"` by anyone else (so a charter may forbid an owner from chartering its own team); `readonly: true`, requiring the referenced contract's config to be read-only, one that can never be updated again (which makes `minimumSecondsSinceUpdate` moot for the same target); `keepsHistory: true`, requiring its config to keep history (only `true` is declarable for either flag); and `ownerProtected`, requiring the contract's elected moderation declaration to protect the owner from the team (`true`) or to leave it unprotected (`false`), which implies elected moderation without the schema having to say so, a contract without an elected declaration meeting neither value. A contract created before contracts recorded their creation time never meets a duration, its own election delay included. Consensus checks them when the referring document is written, against the contract it has already fetched for the existence check and the write itself (its owner and block time), so they cost no further read; a contract that exists but does not meet a requirement refuses the write, paid, with `ReferencedContractRequirementNotMetError` (40135) naming the requirement, where a contract that does not exist is still 40120. A changed `contractRequirements` is an incompatible schema change on update, like the rest of a `refersTo`. The charter system contract's `targetContractId` is the first user. **What comes next.** The charter system contract, applications and the election (new vote poll kinds), the seated team under the contract with its per-ability powers, charter-priced moderators amounts within the maximums, and challenges and amendments. Issue #4865 holds the design. diff --git a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json index a59c93a4da6..fefdea82fee 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json @@ -133,7 +133,7 @@ "pattern": "^[a-zA-Z0-9-_]{1,64}$" }, "contractRequirements": { - "description": "contract references only: what the referenced contract must declare beyond existing, checked when the referring document is written against the contract already fetched for the existence check and the block time, so a requirement costs no further read. Each key names an aspect of the referenced contract and its value the requirement: moderation \"elected\" requires the contract to declare an elected moderation team and \"electionOpen\" one whose own electionDelay, counted from the contract's creation, has passed at the block time of the write (or which declares none); minimumAgeSeconds requires the contract's recorded creation time to be at least that many seconds before the block time of the write, and minimumSecondsSinceUpdate the later of its recorded creation and last update times (a contract without a recorded creation time never meets either). An unmet requirement refuses the write (ReferencedContractRequirementNotMetError, 40135)", + "description": "contract references only: what the referenced contract must declare beyond existing, checked when the referring document is written against the contract already fetched for the existence check and the block time, so a requirement costs no further read. Each key names an aspect of the referenced contract and its value the requirement: moderation \"elected\" requires the contract to declare an elected moderation team and \"electionOpen\" one whose own electionDelay, counted from the contract's creation, has passed at the block time of the write (or which declares none); minimumAgeSeconds requires the contract's recorded creation time to be at least that many seconds before the block time of the write, and minimumSecondsSinceUpdate the later of its recorded creation and last update times (a contract without a recorded creation time never meets either); owner \"self\" requires the contract to be owned by the writer of the referring document (its $ownerId), \"other\" by anyone else; readonly true requires the contract's config to be readonly (one that can never be updated again) and keepsHistory true its config to keep history, only true being declarable for either; ownerProtected requires the contract's elected moderation declaration to protect (true) or not protect (false) the contract owner from the team, a contract without elected moderation meeting neither. An unmet requirement refuses the write (ReferencedContractRequirementNotMetError, 40135)", "type": "object", "properties": { "moderation": { @@ -151,6 +151,21 @@ "type": "integer", "minimum": 1, "maximum": 4294967295 + }, + "owner": { + "enum": [ + "self", + "other" + ] + }, + "readonly": { + "const": true + }, + "keepsHistory": { + "const": true + }, + "ownerProtected": { + "type": "boolean" } }, "minProperties": 1, diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs index 38c0c0e3b7c..da27d502beb 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs @@ -4,8 +4,9 @@ use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::v1::DocumentTypeV1; use crate::data_contract::document_type::{ is_referenced_system_agreement_property, is_referring_system_agreement_property, - property_names, ContractReferenceModeration, ContractReferenceRequirements, DocumentProperty, - DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentType, + property_names, ContractReferenceModeration, ContractReferenceOwner, + ContractReferenceRequirements, DocumentProperty, DocumentPropertyReferenceTarget, + DocumentPropertyType, DocumentType, }; use crate::data_contract::errors::DataContractError; use crate::data_contract::{TokenConfiguration, TokenContractPosition}; @@ -526,8 +527,8 @@ fn apply_property_reference_v0( } /// The `contractRequirements` of a `contract` reference: each key an aspect of the referenced -/// contract with a closed set of values (`moderation`), or a bound on it (`minimumAgeSeconds`), -/// at least one when the object is given at all. +/// contract with a closed set of values (`moderation`, `owner`, the config flags), or a bound +/// on it (`minimumAgeSeconds`), at least one when the object is given at all. fn parse_contract_reference_requirements( refers_to_map: &BTreeMap, ) -> Result { @@ -565,6 +566,27 @@ fn parse_contract_reference_requirements( fields.minimum_seconds_since_update = Some(parse_contract_reference_seconds(&field, value)?); } + property_names::OWNER => { + let name = value.as_text().ok_or_else(|| { + DataContractError::InvalidContractStructure( + "contract refersTo contractRequirements owner must be a string".to_string(), + ) + })?; + fields.owner = Some(ContractReferenceOwner::from_wire_name(name).ok_or_else(|| { + DataContractError::InvalidContractStructure(format!( + "contract refersTo contractRequirements owner {name:?} is unknown, expected \"self\" or \"other\"" + )) + })?); + } + property_names::READONLY => { + fields.readonly = Some(parse_contract_reference_true(&field, value)?); + } + property_names::KEEPS_HISTORY => { + fields.keeps_history = Some(parse_contract_reference_true(&field, value)?); + } + property_names::OWNER_PROTECTED => { + fields.owner_protected = Some(parse_contract_reference_bool(&field, value)?); + } other => { return Err(DataContractError::InvalidContractStructure(format!( "contract refersTo contractRequirements {other:?} is unknown" @@ -591,6 +613,28 @@ fn parse_contract_reference_seconds(field: &str, value: &Value) -> Result Result { + value.as_bool().ok_or_else(|| { + DataContractError::InvalidContractStructure(format!( + "contract refersTo contractRequirements {field} must be a boolean" + )) + }) +} + +/// A flag requirement of a `contract` reference (`readonly`, `keepsHistory`): only `true` +/// requires anything, so `false` is refused rather than declared as a requirement that +/// requires nothing. +fn parse_contract_reference_true(field: &str, value: &Value) -> Result { + if parse_contract_reference_bool(field, value)? { + Ok(true) + } else { + Err(DataContractError::InvalidContractStructure(format!( + "contract refersTo contractRequirements {field} must be true" + ))) + } +} + #[cfg(test)] mod tests { use super::*; @@ -1119,6 +1163,10 @@ mod tests { moderation: Some(ContractReferenceModeration::Elected), minimum_age_seconds: None, minimum_seconds_since_update: None, + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }, } ) @@ -1138,6 +1186,10 @@ mod tests { moderation: Some(ContractReferenceModeration::ElectionOpen), minimum_age_seconds: None, minimum_seconds_since_update: None, + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }, } ) @@ -1157,6 +1209,10 @@ mod tests { moderation: None, minimum_age_seconds: Some(604_800), minimum_seconds_since_update: None, + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }, } ) @@ -1172,6 +1228,10 @@ mod tests { moderation: None, minimum_age_seconds: None, minimum_seconds_since_update: Some(86_400), + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }, } ) @@ -1191,12 +1251,152 @@ mod tests { moderation: Some(ContractReferenceModeration::Elected), minimum_age_seconds: Some(u32::MAX), minimum_seconds_since_update: Some(1), + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, + }, + } + ) + ); + } + + #[test] + fn should_parse_contract_refers_to_requiring_an_owner_relation() { + for (name, owner) in [ + ("self", ContractReferenceOwner::Writer), + ("other", ContractReferenceOwner::Other), + ] { + assert_eq!( + contract_reference_target(json!({ + "type": "contract", + "contractRequirements": { "owner": name } + })), + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: ContractReferenceRequirements { + owner: Some(owner), + ..Default::default() + }, + } + ) + ); + } + assert_eq!( + contract_reference_target(json!({ + "type": "contract", + "contractRequirements": { "moderation": "elected", "owner": "other" } + })), + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: ContractReferenceRequirements { + moderation: Some(ContractReferenceModeration::Elected), + owner: Some(ContractReferenceOwner::Other), + ..Default::default() }, } ) ); } + #[test] + fn should_parse_contract_refers_to_requiring_config_flags() { + for (requirements, expected) in [ + ( + json!({ "readonly": true }), + ContractReferenceRequirements { + readonly: Some(true), + ..Default::default() + }, + ), + ( + json!({ "keepsHistory": true }), + ContractReferenceRequirements { + keeps_history: Some(true), + ..Default::default() + }, + ), + ( + json!({ "ownerProtected": true }), + ContractReferenceRequirements { + owner_protected: Some(true), + ..Default::default() + }, + ), + ( + json!({ "ownerProtected": false }), + ContractReferenceRequirements { + owner_protected: Some(false), + ..Default::default() + }, + ), + ( + json!({ "moderation": "elected", "readonly": true, "keepsHistory": true, "ownerProtected": false }), + ContractReferenceRequirements { + moderation: Some(ContractReferenceModeration::Elected), + readonly: Some(true), + keeps_history: Some(true), + owner_protected: Some(false), + ..Default::default() + }, + ), + ] { + assert_eq!( + contract_reference_target(json!({ + "type": "contract", + "contractRequirements": requirements + })), + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: expected, + } + ), + "{requirements}" + ); + } + } + + #[test] + fn should_reject_a_config_flag_requirement_that_is_not_a_boolean_or_requires_nothing() { + for (field, value, fragment) in [ + ("readonly", json!(false), "readonly must be true"), + ("readonly", json!("true"), "readonly must be a boolean"), + ("readonly", json!(1), "readonly must be a boolean"), + ("keepsHistory", json!(false), "keepsHistory must be true"), + ( + "keepsHistory", + json!("true"), + "keepsHistory must be a boolean", + ), + ( + "ownerProtected", + json!("true"), + "ownerProtected must be a boolean", + ), + ( + "ownerProtected", + json!(0), + "ownerProtected must be a boolean", + ), + ( + "ownerProtected", + json!(null), + "ownerProtected must be a boolean", + ), + ] { + let refers_to = json!({ + "type": "contract", + "contractRequirements": { field: value } + }); + let err = try_document_type_from_schema(contract_reference_schema(refers_to.clone())) + .expect_err("should be refused"); + assert!( + err.to_string().contains(fragment), + "{refers_to}: expected {fragment:?}, got {err}" + ); + } + } + #[test] fn should_reject_a_duration_requirement_that_is_zero_negative_too_large_or_not_an_integer() { for field in ["minimumAgeSeconds", "minimumSecondsSinceUpdate"] { @@ -1241,6 +1441,14 @@ mod tests { json!({ "type": "contract", "contractRequirements": { "tokens": "any" } }), "is unknown", ), + ( + json!({ "type": "contract", "contractRequirements": { "owner": "anyone" } }), + "owner \"anyone\" is unknown, expected \"self\" or \"other\"", + ), + ( + json!({ "type": "contract", "contractRequirements": { "owner": true } }), + "owner must be a string", + ), ( json!({ "type": "identity", "contractRequirements": { "moderation": "elected" } }), "does not take contractRequirements", diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs index fb1fbf67dc1..8bbcf432ae1 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs @@ -2021,6 +2021,26 @@ mod tests { platform_value!({ "type": "contract", "contractRequirements": { "minimumSecondsSinceUpdate": 61 } }), "/properties/toUserId/refersTo/contractRequirements/minimumSecondsSinceUpdate", ), + ( + platform_value!({ "type": "contract", "contractRequirements": { "owner": "self" } }), + platform_value!({ "type": "contract", "contractRequirements": { "owner": "other" } }), + "/properties/toUserId/refersTo/contractRequirements/owner", + ), + ( + platform_value!({ "type": "contract", "contractRequirements": { "ownerProtected": true } }), + platform_value!({ "type": "contract", "contractRequirements": { "ownerProtected": false } }), + "/properties/toUserId/refersTo/contractRequirements/ownerProtected", + ), + ( + platform_value!({ "type": "contract", "contractRequirements": { "owner": "self" } }), + platform_value!({ "type": "contract", "contractRequirements": { "owner": "self", "readonly": true } }), + "/properties/toUserId/refersTo/contractRequirements/readonly", + ), + ( + platform_value!({ "type": "contract", "contractRequirements": { "owner": "self", "keepsHistory": true } }), + platform_value!({ "type": "contract", "contractRequirements": { "owner": "self" } }), + "/properties/toUserId/refersTo/contractRequirements/keepsHistory", + ), ] { let old_document_type = identifier_document_type(Some(old_fields), platform_version); diff --git a/packages/rs-dpp/src/data_contract/document_type/mod.rs b/packages/rs-dpp/src/data_contract/document_type/mod.rs index 5d19f4f47f3..3e39ec42e9c 100644 --- a/packages/rs-dpp/src/data_contract/document_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/mod.rs @@ -111,6 +111,10 @@ pub(crate) mod property_names { pub const MODERATION: &str = "moderation"; pub const MINIMUM_AGE_SECONDS: &str = "minimumAgeSeconds"; pub const MINIMUM_SECONDS_SINCE_UPDATE: &str = "minimumSecondsSinceUpdate"; + pub const OWNER: &str = "owner"; + pub const READONLY: &str = "readonly"; + pub const KEEPS_HISTORY: &str = "keepsHistory"; + pub const OWNER_PROTECTED: &str = "ownerProtected"; pub const DOCUMENTS_COUNTABLE: &str = "documentsCountable"; pub const RANGE_COUNTABLE: &str = "rangeCountable"; /// Doctype-level flag naming the property whose values are summed into diff --git a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs index 84dcfb4b84a..d71648c21e2 100644 --- a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs @@ -12,6 +12,7 @@ use platform_serialization_derive::{ use crate::consensus::basic::decode::DecodingError; use crate::data_contract::accessors::v0::DataContractV0Getters; use crate::data_contract::accessors::v1::DataContractV1Getters; +use crate::data_contract::config::v0::DataContractConfigGettersV0; use crate::data_contract::config::v1::DataContractConfigGettersV1; use crate::data_contract::config::v2::DataContractConfigGettersV2; use crate::data_contract::config::DataContractConfig; @@ -93,9 +94,9 @@ pub struct ByteArrayPropertySizes { /// Declared as `refersTo: { "type": "contract", "contractRequirements": { ... } }`: each key names an /// aspect of the referenced contract and its value the requirement on it. Consensus checks the /// requirements when the referring document is written, against the contract it has already -/// fetched for the existence check and the block time of the write, so a requirement costs no -/// further read. An unmet one refuses the write with `ReferencedContractRequirementNotMetError` -/// (40135). +/// fetched for the existence check and the write itself (its owner and block time), so a +/// requirement costs no further read. An unmet one refuses the write with +/// `ReferencedContractRequirementNotMetError` (40135). #[derive( Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize, Encode, Decode, DecodeUntrusted, )] @@ -115,6 +116,76 @@ pub struct ContractReferenceRequirements { /// recorded a creation time does not meet it. #[serde(default, skip_serializing_if = "Option::is_none")] pub minimum_seconds_since_update: Option, + /// Who must own the referenced contract, relative to the writer of the referring document. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub owner: Option, + /// Whether the referenced contract must be read-only (its config's `readonly`), a + /// contract that can never be updated again. Only `true` is declarable. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub readonly: Option, + /// Whether the referenced contract must keep its history (its config's `keepsHistory`). + /// Only `true` is declarable. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub keeps_history: Option, + /// Whether the elected moderation declaration of the referenced contract must protect + /// (`true`), or must not protect (`false`), the contract owner from the team, its + /// `ownerProtected`. Either value implies elected moderation: a contract with no + /// moderation, or with moderation of another kind, meets neither. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub owner_protected: Option, +} + +/// The write of a referring document, what a contract reference's requirements are checked +/// against beside the referenced contract itself. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct ReferringWrite { + /// The `$ownerId` of the referring document: the owner of the transition writing it. + pub owner_id: Identifier, + /// The time of the block writing it. + pub block_time_ms: TimestampMillis, +} + +/// Who a `contract` reference may require to own the referenced contract, relative to the +/// writer of the referring document. +#[derive( + Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Encode, Decode, DecodeUntrusted, +)] +pub enum ContractReferenceOwner { + /// The writer itself: the contract's owner is the `$ownerId` of the referring document, + /// a write gate like the `$ownerId` property agreement of a document reference. + #[serde(rename = "self")] + Writer, + /// Anyone but the writer. + #[serde(rename = "other")] + Other, +} + +impl ContractReferenceOwner { + /// The wire name, the value of `contractRequirements.owner`. + pub fn as_str(&self) -> &'static str { + match self { + ContractReferenceOwner::Writer => "self", + ContractReferenceOwner::Other => "other", + } + } + + /// The owner relation a wire name names, `None` for any other name. + pub fn from_wire_name(name: &str) -> Option { + match name { + "self" => Some(ContractReferenceOwner::Writer), + "other" => Some(ContractReferenceOwner::Other), + _ => None, + } + } + + /// Whether `contract`, owned as it is, meets this for a referring document owned by + /// `writer_id`. + pub fn is_met_by(&self, contract: &DataContract, writer_id: &Identifier) -> bool { + match self { + ContractReferenceOwner::Writer => contract.owner_id() == *writer_id, + ContractReferenceOwner::Other => contract.owner_id() != *writer_id, + } + } } /// The moderation a `contract` reference may require of the referenced contract. @@ -187,6 +258,10 @@ pub enum ContractReferenceRequirement { Moderation(ContractReferenceModeration), MinimumAgeSeconds(u32), MinimumSecondsSinceUpdate(u32), + Owner(ContractReferenceOwner), + Readonly(bool), + KeepsHistory(bool), + OwnerProtected(bool), } impl ContractReferenceRequirement { @@ -200,6 +275,10 @@ impl ContractReferenceRequirement { ContractReferenceRequirement::MinimumSecondsSinceUpdate(_) => { property_names::MINIMUM_SECONDS_SINCE_UPDATE } + ContractReferenceRequirement::Owner(_) => property_names::OWNER, + ContractReferenceRequirement::Readonly(_) => property_names::READONLY, + ContractReferenceRequirement::KeepsHistory(_) => property_names::KEEPS_HISTORY, + ContractReferenceRequirement::OwnerProtected(_) => property_names::OWNER_PROTECTED, } } @@ -211,22 +290,46 @@ impl ContractReferenceRequirement { | ContractReferenceRequirement::MinimumSecondsSinceUpdate(seconds) => { seconds.to_string() } + ContractReferenceRequirement::Owner(owner) => owner.as_str().to_string(), + ContractReferenceRequirement::Readonly(flag) + | ContractReferenceRequirement::KeepsHistory(flag) + | ContractReferenceRequirement::OwnerProtected(flag) => flag.to_string(), } } - /// Whether `contract` meets this requirement at `block_time_ms`, the time of the block - /// writing the referring document. - pub fn is_met_by(&self, contract: &DataContract, block_time_ms: TimestampMillis) -> bool { + /// Whether `contract` meets this requirement for `write`, the write of the referring + /// document. + pub fn is_met_by(&self, contract: &DataContract, write: ReferringWrite) -> bool { match self { ContractReferenceRequirement::Moderation(moderation) => { - moderation.is_met_by(contract, block_time_ms) + moderation.is_met_by(contract, write.block_time_ms) } ContractReferenceRequirement::MinimumAgeSeconds(seconds) => { - Self::minimum_age_is_met(contract.created_at(), *seconds, block_time_ms) + Self::minimum_age_is_met(contract.created_at(), *seconds, write.block_time_ms) } ContractReferenceRequirement::MinimumSecondsSinceUpdate(seconds) => { - Self::minimum_age_is_met(Self::last_change_time(contract), *seconds, block_time_ms) + Self::minimum_age_is_met( + Self::last_change_time(contract), + *seconds, + write.block_time_ms, + ) + } + ContractReferenceRequirement::Owner(owner) => { + owner.is_met_by(contract, &write.owner_id) + } + ContractReferenceRequirement::Readonly(required) => { + contract.config().readonly() == *required } + ContractReferenceRequirement::KeepsHistory(required) => { + contract.config().keeps_history() == *required + } + // Either value needs an elected declaration to read the flag from: a contract + // without one meets neither + ContractReferenceRequirement::OwnerProtected(required) => contract + .config() + .moderation() + .and_then(|moderation| moderation.moderators.elected()) + .is_some_and(|elected| elected.owner_protected == *required), } } @@ -262,6 +365,10 @@ impl ContractReferenceRequirements { self.moderation.is_none() && self.minimum_age_seconds.is_none() && self.minimum_seconds_since_update.is_none() + && self.owner.is_none() + && self.readonly.is_none() + && self.keeps_history.is_none() + && self.owner_protected.is_none() } /// The requirements, in declaration order. @@ -279,17 +386,37 @@ impl ContractReferenceRequirements { .into_iter() .map(ContractReferenceRequirement::MinimumSecondsSinceUpdate), ) + .chain( + self.owner + .into_iter() + .map(ContractReferenceRequirement::Owner), + ) + .chain( + self.readonly + .into_iter() + .map(ContractReferenceRequirement::Readonly), + ) + .chain( + self.keeps_history + .into_iter() + .map(ContractReferenceRequirement::KeepsHistory), + ) + .chain( + self.owner_protected + .into_iter() + .map(ContractReferenceRequirement::OwnerProtected), + ) } - /// The first requirement `contract` does not meet at `block_time_ms`, the time of the - /// block writing the referring document, `None` when it meets them all. + /// The first requirement `contract` does not meet for `write`, the write of the referring + /// document, `None` when it meets them all. pub fn first_unmet_by( &self, contract: &DataContract, - block_time_ms: TimestampMillis, + write: ReferringWrite, ) -> Option { self.requirements() - .find(|requirement| !requirement.is_met_by(contract, block_time_ms)) + .find(|requirement| !requirement.is_met_by(contract, write)) } } @@ -497,6 +624,22 @@ impl std::fmt::Display for DocumentPropertyReferenceTarget { if let Some(seconds) = contract_requirements.minimum_seconds_since_update { write!(f, " unchanged for at least {seconds} seconds")?; } + match contract_requirements.owner { + Some(ContractReferenceOwner::Writer) => write!(f, " owned by the writer")?, + Some(ContractReferenceOwner::Other) => write!(f, " not owned by the writer")?, + None => {} + } + if contract_requirements.readonly == Some(true) { + write!(f, " read-only")?; + } + if contract_requirements.keeps_history == Some(true) { + write!(f, " keeping history")?; + } + match contract_requirements.owner_protected { + Some(true) => write!(f, " with the owner protected")?, + Some(false) => write!(f, " with the owner unprotected")?, + None => {} + } Ok(()) } DocumentPropertyReferenceTarget::Token => write!(f, "token"), @@ -7778,6 +7921,10 @@ mod tests { moderation: None, minimum_age_seconds: Some(3600), minimum_seconds_since_update: Some(60), + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }; assert_eq!( requirements.requirements().collect::>(), @@ -7804,6 +7951,190 @@ mod tests { ); } + #[test] + fn should_meet_an_owner_requirement_from_the_referenced_contract_owner_and_the_writer() { + use crate::tests::fixtures::get_dashpay_contract_fixture; + + let platform_version = PlatformVersion::latest(); + let contract = get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + let owner_id = contract.owner_id(); + let someone_else = Identifier::from([0x42; 32]); + assert_ne!(owner_id, someone_else); + + assert!(ContractReferenceOwner::Writer.is_met_by(&contract, &owner_id)); + assert!(!ContractReferenceOwner::Writer.is_met_by(&contract, &someone_else)); + assert!(!ContractReferenceOwner::Other.is_met_by(&contract, &owner_id)); + assert!(ContractReferenceOwner::Other.is_met_by(&contract, &someone_else)); + + assert_eq!(ContractReferenceOwner::Writer.as_str(), "self"); + assert_eq!(ContractReferenceOwner::Other.as_str(), "other"); + assert_eq!( + ContractReferenceOwner::from_wire_name("self"), + Some(ContractReferenceOwner::Writer) + ); + assert_eq!( + ContractReferenceOwner::from_wire_name("other"), + Some(ContractReferenceOwner::Other) + ); + assert_eq!(ContractReferenceOwner::from_wire_name("owner"), None); + + let requirement = ContractReferenceRequirement::Owner(ContractReferenceOwner::Writer); + assert_eq!(requirement.field(), "owner"); + assert_eq!(requirement.required(), "self"); + assert_eq!( + ContractReferenceRequirement::Owner(ContractReferenceOwner::Other).required(), + "other" + ); + + // The first unmet requirement is reported in declaration order: an owner requirement + // is checked after the moderation and duration ones + let requirements = ContractReferenceRequirements { + owner: Some(ContractReferenceOwner::Other), + ..Default::default() + }; + assert_eq!( + requirements.requirements().collect::>(), + vec![ContractReferenceRequirement::Owner( + ContractReferenceOwner::Other + )] + ); + let by_owner = ReferringWrite { + owner_id, + block_time_ms: 0, + }; + let by_someone_else = ReferringWrite { + owner_id: someone_else, + block_time_ms: 0, + }; + assert_eq!( + requirements.first_unmet_by(&contract, by_owner), + Some(ContractReferenceRequirement::Owner( + ContractReferenceOwner::Other + )) + ); + assert_eq!( + requirements.first_unmet_by(&contract, by_someone_else), + None + ); + let both = ContractReferenceRequirements { + minimum_age_seconds: Some(1), + owner: Some(ContractReferenceOwner::Other), + ..Default::default() + }; + assert_eq!( + both.first_unmet_by(&contract, by_owner), + Some(ContractReferenceRequirement::MinimumAgeSeconds(1)) + ); + } + + #[test] + fn should_meet_a_config_flag_requirement_from_the_referenced_contract_config() { + use crate::data_contract::accessors::v0::DataContractV0Setters; + use crate::data_contract::config::moderation::{ + ContractModerationConfig, ContractModerators, ElectedModerators, InterimModerators, + ModerationAbility, DEFAULT_ELECTION_WINDOW_SECONDS, + }; + use crate::data_contract::config::v0::DataContractConfigSettersV0; + use crate::tests::fixtures::get_dashpay_contract_fixture; + use std::collections::BTreeSet; + + let platform_version = PlatformVersion::latest(); + let mut contract = get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + let write = ReferringWrite { + owner_id: Identifier::from([0x42; 32]), + block_time_ms: 0, + }; + let readonly = ContractReferenceRequirement::Readonly(true); + let keeps_history = ContractReferenceRequirement::KeepsHistory(true); + let protected = ContractReferenceRequirement::OwnerProtected(true); + let unprotected = ContractReferenceRequirement::OwnerProtected(false); + + // The fixture is neither read-only nor keeping history, and declares no moderation: + // it meets none of the flags, whichever value the owner protection requires + assert!(!readonly.is_met_by(&contract, write)); + assert!(!keeps_history.is_met_by(&contract, write)); + assert!(!protected.is_met_by(&contract, write)); + assert!(!unprotected.is_met_by(&contract, write)); + + let mut config = contract.config().clone(); + config.set_readonly(true); + config.set_keeps_history(true); + contract.set_config(config); + assert!(readonly.is_met_by(&contract, write)); + assert!(keeps_history.is_met_by(&contract, write)); + + // Appointed moderation still has no owner protection to read + contract.set_config(contract.config().clone().with_moderation(Some( + ContractModerationConfig { + banlist: true, + suspensions: false, + warnings: false, + moderators: ContractModerators::AppointedModerators(BTreeSet::from([ + Identifier::from([0x77; 32]), + ])), + }, + ))); + assert!(!protected.is_met_by(&contract, write)); + assert!(!unprotected.is_met_by(&contract, write)); + + for owner_protected in [true, false] { + contract.set_config(contract.config().clone().with_moderation(Some( + ContractModerationConfig { + banlist: true, + suspensions: false, + warnings: false, + moderators: ContractModerators::Elected(Box::new(ElectedModerators { + join_window: DEFAULT_ELECTION_WINDOW_SECONDS, + vote_window: DEFAULT_ELECTION_WINDOW_SECONDS, + challenge_cool_down: 1_209_600, + election_delay: None, + moderated_document_types: BTreeMap::from([( + "profile".to_string(), + BTreeSet::from([ModerationAbility::Ban]), + )]), + interim: InterimModerators::ContractOwner, + owner_protected, + })), + }, + ))); + assert_eq!(protected.is_met_by(&contract, write), owner_protected); + assert_eq!(unprotected.is_met_by(&contract, write), !owner_protected); + } + + assert_eq!(readonly.field(), "readonly"); + assert_eq!(readonly.required(), "true"); + assert_eq!(keeps_history.field(), "keepsHistory"); + assert_eq!(keeps_history.required(), "true"); + assert_eq!(protected.field(), "ownerProtected"); + assert_eq!(protected.required(), "true"); + assert_eq!(unprotected.required(), "false"); + + // The contract is left read-only, keeping history and with its owner unprotected: + // requiring the protection is the one requirement it does not meet + let requirements = ContractReferenceRequirements { + readonly: Some(true), + keeps_history: Some(true), + owner_protected: Some(true), + ..Default::default() + }; + assert!(!requirements.is_empty()); + assert_eq!( + requirements.requirements().collect::>(), + vec![readonly, keeps_history, protected] + ); + assert_eq!( + requirements.first_unmet_by(&contract, write), + Some(protected) + ); + let met = ContractReferenceRequirements { + owner_protected: Some(false), + ..requirements + }; + assert_eq!(met.first_unmet_by(&contract, write), None); + } + #[test] fn should_meet_election_open_when_the_contract_declares_no_delay_or_the_delay_passed() { use crate::data_contract::accessors::v0::DataContractV0Setters; @@ -7940,6 +8271,10 @@ mod tests { moderation: Some(ContractReferenceModeration::Elected), minimum_age_seconds: None, minimum_seconds_since_update: None, + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }, } .to_string(), @@ -7951,6 +8286,10 @@ mod tests { moderation: Some(ContractReferenceModeration::Elected), minimum_age_seconds: Some(604_800), minimum_seconds_since_update: Some(86_400), + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }, } .to_string(), @@ -7962,17 +8301,69 @@ mod tests { moderation: None, minimum_age_seconds: Some(1), minimum_seconds_since_update: None, + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }, } .to_string(), "contract at least 1 seconds old" ); + assert_eq!( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: ContractReferenceRequirements { + owner: Some(ContractReferenceOwner::Writer), + ..Default::default() + }, + } + .to_string(), + "contract owned by the writer" + ); + assert_eq!( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: ContractReferenceRequirements { + moderation: Some(ContractReferenceModeration::Elected), + owner: Some(ContractReferenceOwner::Other), + ..Default::default() + }, + } + .to_string(), + "contract with elected moderation not owned by the writer" + ); + assert_eq!( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: ContractReferenceRequirements { + readonly: Some(true), + keeps_history: Some(true), + owner_protected: Some(true), + ..Default::default() + }, + } + .to_string(), + "contract read-only keeping history with the owner protected" + ); + assert_eq!( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: ContractReferenceRequirements { + owner: Some(ContractReferenceOwner::Other), + owner_protected: Some(false), + ..Default::default() + }, + } + .to_string(), + "contract not owned by the writer with the owner unprotected" + ); assert_eq!( DocumentPropertyReferenceTarget::Contract { contract_requirements: ContractReferenceRequirements { moderation: Some(ContractReferenceModeration::ElectionOpen), minimum_age_seconds: None, minimum_seconds_since_update: None, + owner: None, + readonly: None, + keeps_history: None, + owner_protected: None, }, } .to_string(), diff --git a/packages/rs-dpp/src/errors/consensus/state/document/referenced_contract_requirement_not_met_error.rs b/packages/rs-dpp/src/errors/consensus/state/document/referenced_contract_requirement_not_met_error.rs index 3d6df7df369..ca08a93caaf 100644 --- a/packages/rs-dpp/src/errors/consensus/state/document/referenced_contract_requirement_not_met_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/document/referenced_contract_requirement_not_met_error.rs @@ -52,14 +52,15 @@ impl ReferencedContractRequirementNotMetError { &self.contract_id } - /// The `contractRequirements` key of the requirement: `moderation`, `minimumAgeSeconds` or - /// `minimumSecondsSinceUpdate` + /// The `contractRequirements` key of the requirement: `moderation`, `minimumAgeSeconds`, + /// `minimumSecondsSinceUpdate`, `owner`, `readonly`, `keepsHistory` or `ownerProtected` pub fn field(&self) -> &str { &self.field } /// The value the reference requires as the schema spells it, `elected` for a moderation - /// requirement, the number of seconds for a duration + /// requirement, the number of seconds for a duration, `self` or `other` for an owner, + /// `true` or `false` for a config flag pub fn required(&self) -> &str { &self.required } diff --git a/packages/rs-dpp/src/validation/meta_validators/mod.rs b/packages/rs-dpp/src/validation/meta_validators/mod.rs index 58b373b814d..097c3b3fe4c 100644 --- a/packages/rs-dpp/src/validation/meta_validators/mod.rs +++ b/packages/rs-dpp/src/validation/meta_validators/mod.rs @@ -348,6 +348,14 @@ mod tests { json!({ "minimumAgeSeconds": 4294967295u64 }), json!({ "minimumSecondsSinceUpdate": 86400 }), json!({ "moderation": "elected", "minimumAgeSeconds": 604800, "minimumSecondsSinceUpdate": 86400 }), + json!({ "owner": "self" }), + json!({ "owner": "other" }), + json!({ "moderation": "elected", "owner": "other" }), + json!({ "readonly": true }), + json!({ "keepsHistory": true }), + json!({ "ownerProtected": true }), + json!({ "ownerProtected": false }), + json!({ "moderation": "elected", "owner": "other", "readonly": true, "keepsHistory": true, "ownerProtected": false }), ] { let schema = document_schema_with_refers_to(json!({ "type": "contract", @@ -373,7 +381,19 @@ mod tests { json!({ "type": "contract", "contractRequirements": { "minimumSecondsSinceUpdate": 0 } }), json!({ "type": "contract", "contractRequirements": { "minimumSecondsSinceUpdate": "60" } }), json!({ "type": "contract", "contractRequirements": { "tokens": "any" } }), + json!({ "type": "contract", "contractRequirements": { "owner": "anyone" } }), + json!({ "type": "contract", "contractRequirements": { "owner": true } }), json!({ "type": "identity", "contractRequirements": { "minimumAgeSeconds": 3600 } }), + json!({ "type": "identity", "contractRequirements": { "owner": "self" } }), + json!({ "type": "contract", "contractRequirements": { "readonly": false } }), + json!({ "type": "contract", "contractRequirements": { "readonly": "true" } }), + json!({ "type": "contract", "contractRequirements": { "readonly": 1 } }), + json!({ "type": "contract", "contractRequirements": { "keepsHistory": false } }), + json!({ "type": "contract", "contractRequirements": { "keepsHistory": "true" } }), + json!({ "type": "contract", "contractRequirements": { "ownerProtected": "true" } }), + json!({ "type": "contract", "contractRequirements": { "ownerProtected": 1 } }), + json!({ "type": "contract", "contractRequirements": { "ownerProtected": null } }), + json!({ "type": "identity", "contractRequirements": { "readonly": true } }), ] { let schema = document_schema_with_refers_to(refers_to.clone()); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs index 7c0f91bff2b..6487aa68c26 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs @@ -12,7 +12,7 @@ use dpp::data_contract::document_type::accessors::{ use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::data_contract::document_type::{ is_referring_system_agreement_property, DocumentPropertyReferenceTarget, - DocumentPropertyType, DocumentTypeRef, + DocumentPropertyType, DocumentTypeRef, ReferringWrite, }; use dpp::data_contract::DataContract; use dpp::document::property_names::{CREATOR_ID, OWNER_ID}; @@ -304,10 +304,14 @@ fn validate_document_type_references_v0( None => false, Some(fetch_info) => { // The declaration's requirements are checked against the contract - // just fetched and the block time, so they cost no further read; - // the first unmet one refuses the write - if let Some(requirement) = contract_requirements - .first_unmet_by(&fetch_info.contract, block_info.time_ms) + // just fetched and the write itself (its owner and block time), so + // they cost no further read; the first unmet one refuses the write + let write = ReferringWrite { + owner_id, + block_time_ms: block_info.time_ms, + }; + if let Some(requirement) = + contract_requirements.first_unmet_by(&fetch_info.contract, write) { return Ok(SimpleConsensusValidationResult::new_with_error( ReferencedContractRequirementNotMetError::new( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs index 2359c05630d..30a8ba16ded 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs @@ -16,6 +16,8 @@ mod creation_tests { use dpp::fee::fee_result::refunds::FeeRefunds; use dpp::fee::fee_result::FeeResult; use dpp::data_contract::accessors::v0::DataContractV0Setters; + use dpp::data_contract::config::v0::DataContractConfigSettersV0; + use dpp::data_contract::config::DataContractConfig; use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::data_contract::document_type::restricted_creation::CreationRestrictionMode; use dpp::document::Document; @@ -5323,6 +5325,36 @@ mod creation_tests { /// The minimum age, and minimum time since the last update, the aged- and /// updated-contract-reference fixtures declare, in milliseconds const AGED_REFERENCE_MINIMUM_AGE_MS: u64 = 3_600_000; + const REFERENCE_VALIDATION_OWNER_SELF_CONTRACT_REF_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-self-contract-ref.json"; + /// The `id` of the self-owned-contract-reference fixture (`owner: "self"`): owned by an + /// identity that is not the writer, so a reference to it is unmet. + const REFERENCE_VALIDATION_OWNER_SELF_CONTRACT_REF_CONTRACT_ID: &str = + "Gb9ZqQQTAoV7JCUrZhsS4ssZozbE33Suunzhr7RVy2pD"; + const REFERENCE_VALIDATION_OWNER_OTHER_CONTRACT_REF_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-other-contract-ref.json"; + /// The `id` of the other-owned-contract-reference fixture (`owner: "other"`): owned by an + /// identity that is not the writer, so a reference to it is met. + const REFERENCE_VALIDATION_OWNER_OTHER_CONTRACT_REF_CONTRACT_ID: &str = + "EPHoATa8ifeWoviLaxTQRWfsJUHAQUCro6dEkwgjwog7"; + const REFERENCE_VALIDATION_READONLY_CONTRACT_REF_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-readonly-contract-ref.json"; + /// The `id` of the read-only-contract-reference fixture (`readonly: true`): not read-only + /// itself, so a reference to it is unmet. + const REFERENCE_VALIDATION_READONLY_CONTRACT_REF_CONTRACT_ID: &str = + "2FkduiNwoBct7PdxmCRYgLxckj4s8dhx7tXLUUpq74Qk"; + const REFERENCE_VALIDATION_KEEPS_HISTORY_CONTRACT_REF_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-keeps-history-contract-ref.json"; + /// The `id` of the history-keeping-contract-reference fixture (`keepsHistory: true`): not + /// keeping history itself, so a reference to it is unmet. + const REFERENCE_VALIDATION_KEEPS_HISTORY_CONTRACT_REF_CONTRACT_ID: &str = + "7gQQ8BdF9gem9YxBNLd3QW7AmiGYNTTuVLerkiJZ3JCx"; + const REFERENCE_VALIDATION_OWNER_PROTECTED_CONTRACT_REF_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-protected-contract-ref.json"; + /// The `id` of the owner-protected-contract-reference fixture (`ownerProtected: true`): + /// declaring no moderation at all, so a reference to it is unmet. + const REFERENCE_VALIDATION_OWNER_PROTECTED_CONTRACT_REF_CONTRACT_ID: &str = + "6BL67iVAJ5eiVne4qXJGNRGpVqB2b2TKiF1odQ7y2qjn"; const REFERENCE_VALIDATION_TOKEN_REF_CONTRACT_PATH: &str = "tests/supporting_files/contract/reference-validation/reference-validation-contract-token-ref.json"; const REFERENCE_VALIDATION_OPTIONAL_CONTRACT_PATH: &str = @@ -5347,15 +5379,16 @@ mod creation_tests { run_reference_validation_creation_with_setup_and_mutator( contract_path, BlockInfo::default(), - |_, _| Identifier::default(), + |_, _, _| Identifier::default(), |document, targets, _| mutator(document, targets), ) .await } /// Like `run_reference_validation_creation_with_mutator`, with a `setup` step that writes - /// whatever else the test needs into state before the contract, and hands the mutator an - /// id it produced (a contract's, say). The document is written in a block of `block_info`. + /// whatever else the test needs into state before the contract, given the targets already + /// in state (the writer's identity among them), and hands the mutator an id it produced + /// (a contract's, say). The document is written in a block of `block_info`. async fn run_reference_validation_creation_with_setup_and_mutator( contract_path: &str, block_info: BlockInfo, @@ -5363,7 +5396,11 @@ mod creation_tests { mutator: F, ) -> StateTransitionExecutionResult where - S: FnOnce(&mut TempPlatform, &PlatformVersion) -> Identifier, + S: FnOnce( + &mut TempPlatform, + &ReferenceTargets, + &PlatformVersion, + ) -> Identifier, F: FnOnce(&mut Document, &ReferenceTargets, Identifier), { let platform_version = PlatformVersion::latest(); @@ -5395,7 +5432,7 @@ mod creation_tests { token_id, }; - let setup_id = setup(&mut platform, platform_version); + let setup_id = setup(&mut platform, &targets, platform_version); let contract = setup_contract( &platform.drive, @@ -5624,23 +5661,25 @@ mod creation_tests { ); } - /// A contract with an elected moderation team, for a reference that requires one. It is - /// written to state directly, the way the fixtures are, so the moderated type needs no - /// list behind it. + /// A contract with an elected moderation team, protecting its owner from the team or not, + /// for a reference that requires one. It is written to state directly, the way the + /// fixtures are, so the moderated type needs no list behind it. fn insert_elected_contract( - platform: &mut TempPlatform, - platform_version: &PlatformVersion, - ) -> Identifier { - insert_elected_contract_with(None, None)(platform, platform_version) + owner_protected: bool, + ) -> impl FnOnce(&mut TempPlatform, &ReferenceTargets, &PlatformVersion) -> Identifier + { + insert_elected_contract_with(None, None, owner_protected) } - /// An elected contract with the given election delay and recorded creation time, written - /// to state directly, the way the fixtures are. + /// An elected contract with the given election delay, recorded creation time and owner + /// protection, written to state directly, the way the fixtures are. fn insert_elected_contract_with( election_delay: Option, created_at: Option, - ) -> impl FnOnce(&mut TempPlatform, &PlatformVersion) -> Identifier { - move |platform, _platform_version| { + owner_protected: bool, + ) -> impl FnOnce(&mut TempPlatform, &ReferenceTargets, &PlatformVersion) -> Identifier + { + move |platform, _targets, _platform_version| { use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::accessors::v1::DataContractV1Setters; use dpp::data_contract::config::moderation::{ @@ -5671,7 +5710,7 @@ mod creation_tests { BTreeSet::from([ModerationAbility::Ban]), )]), interim: InterimModerators::ContractOwner, - owner_protected: false, + owner_protected, })), }, ))); @@ -5683,6 +5722,33 @@ mod creation_tests { } } + /// A contract with the id `contract_id` whose config `configure` adjusts, for a reference + /// that requires something of the config. Written to state directly, the way the fixtures + /// are. + fn insert_contract_configured( + contract_id: [u8; 32], + configure: impl FnOnce(&mut DataContractConfig), + ) -> impl FnOnce(&mut TempPlatform, &ReferenceTargets, &PlatformVersion) -> Identifier + { + move |platform, _targets, _platform_version| { + use dpp::data_contract::accessors::v0::DataContractV0Getters; + + let contract = setup_contract( + &platform.drive, + REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_PATH, + Some(contract_id), + None, + Some(|contract: &mut DataContract| { + let mut config = contract.config().clone(); + configure(&mut config); + contract.set_config(config); + }), + None, + None, + ); + contract.id() + } + } #[tokio::test] async fn should_document_creation_fail_when_election_open_contract_is_not_elected() { // The fixture contract itself exists in state and declares no moderation at all @@ -5719,7 +5785,7 @@ mod creation_tests { let result = run_reference_validation_creation_with_setup_and_mutator( REFERENCE_VALIDATION_ELECTION_OPEN_CONTRACT_REF_CONTRACT_PATH, BlockInfo::default(), - insert_elected_contract_with(None, None), + insert_elected_contract_with(None, None, false), |document, _, elected_contract_id| { document.set("refContractId", elected_contract_id.into()); }, @@ -5741,6 +5807,7 @@ mod creation_tests { insert_elected_contract_with( Some(3600), Some(AGED_REFERENCE_BLOCK_TIME_MS - AGED_REFERENCE_MINIMUM_AGE_MS + 1), + false, ), |document, _, elected_contract_id| { document.set("refContractId", elected_contract_id.into()); @@ -5766,7 +5833,7 @@ mod creation_tests { let result = run_reference_validation_creation_with_setup_and_mutator( REFERENCE_VALIDATION_ELECTION_OPEN_CONTRACT_REF_CONTRACT_PATH, aged_reference_block_info(), - insert_elected_contract_with(Some(1), None), + insert_elected_contract_with(Some(1), None, false), |document, _, elected_contract_id| { document.set("refContractId", elected_contract_id.into()); }, @@ -5791,6 +5858,7 @@ mod creation_tests { insert_elected_contract_with( Some(3600), Some(AGED_REFERENCE_BLOCK_TIME_MS - AGED_REFERENCE_MINIMUM_AGE_MS), + false, ), |document, _, elected_contract_id| { document.set("refContractId", elected_contract_id.into()); @@ -5858,7 +5926,7 @@ mod creation_tests { let result = run_reference_validation_creation_with_setup_and_mutator( REFERENCE_VALIDATION_ELECTED_CONTRACT_REF_CONTRACT_PATH, BlockInfo::default(), - insert_elected_contract, + insert_elected_contract(false), |document, _, elected_contract_id| { document.set("refContractId", elected_contract_id.into()); }, @@ -5876,7 +5944,8 @@ mod creation_tests { /// create transition would have recorded. fn insert_contract_created_at( created_at: Option, - ) -> impl FnOnce(&mut TempPlatform, &PlatformVersion) -> Identifier { + ) -> impl FnOnce(&mut TempPlatform, &ReferenceTargets, &PlatformVersion) -> Identifier + { insert_contract_with_times(created_at, None) } @@ -5885,8 +5954,9 @@ mod creation_tests { fn insert_contract_with_times( created_at: Option, updated_at: Option, - ) -> impl FnOnce(&mut TempPlatform, &PlatformVersion) -> Identifier { - move |platform, _platform_version| { + ) -> impl FnOnce(&mut TempPlatform, &ReferenceTargets, &PlatformVersion) -> Identifier + { + move |platform, _targets, _platform_version| { use dpp::data_contract::accessors::v1::DataContractV1Setters; let contract = setup_contract( @@ -5952,7 +6022,7 @@ mod creation_tests { let result = run_reference_validation_creation_with_setup_and_mutator( REFERENCE_VALIDATION_AGED_CONTRACT_REF_CONTRACT_PATH, aged_reference_block_info(), - |_, _| Identifier::default(), + |_, _, _| Identifier::default(), |document, _, _| { document.set("refContractId", existing_contract_id.into()); }, @@ -6066,6 +6136,290 @@ mod creation_tests { ); } + /// A contract owned by the writer of the referring document, for a reference that requires + /// the referenced contract to be, or not to be, the writer's own. Written to state directly, + /// the way the fixtures are. + fn insert_contract_owned_by_writer( + platform: &mut TempPlatform, + targets: &ReferenceTargets, + _platform_version: &PlatformVersion, + ) -> Identifier { + let contract = setup_contract( + &platform.drive, + REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_PATH, + Some([0xB1; 32]), + Some(targets.identity_id.to_buffer()), + None::, + None, + None, + ); + contract.id() + } + + #[tokio::test] + async fn should_document_creation_succeed_when_required_self_owned_contract_is_the_writers() { + let result = run_reference_validation_creation_with_setup_and_mutator( + REFERENCE_VALIDATION_OWNER_SELF_CONTRACT_REF_CONTRACT_PATH, + BlockInfo::default(), + insert_contract_owned_by_writer, + |document, _, own_contract_id| { + document.set("refContractId", own_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_required_self_owned_contract_is_someone_elses() { + // The fixture contract itself exists in state, owned by an identity that is not the + // writer + let existing_contract_id = Identifier::from_string( + REFERENCE_VALIDATION_OWNER_SELF_CONTRACT_REF_CONTRACT_ID, + Encoding::Base58, + ) + .expect("expected a valid contract id"); + + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_OWNER_SELF_CONTRACT_REF_CONTRACT_PATH, + |document, _| { + document.set("refContractId", existing_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedContractRequirementNotMetError(ref e)), + .. + } if e.contract_id() == &existing_contract_id + && e.field() == "owner" + && e.required() == "self" + && e.path() == "refContractId" + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_required_other_owned_contract_is_someone_elses() + { + let existing_contract_id = Identifier::from_string( + REFERENCE_VALIDATION_OWNER_OTHER_CONTRACT_REF_CONTRACT_ID, + Encoding::Base58, + ) + .expect("expected a valid contract id"); + + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_OWNER_OTHER_CONTRACT_REF_CONTRACT_PATH, + |document, _| { + document.set("refContractId", existing_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_required_other_owned_contract_is_the_writers() { + let result = run_reference_validation_creation_with_setup_and_mutator( + REFERENCE_VALIDATION_OWNER_OTHER_CONTRACT_REF_CONTRACT_PATH, + BlockInfo::default(), + insert_contract_owned_by_writer, + |document, _, own_contract_id| { + document.set("refContractId", own_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedContractRequirementNotMetError(ref e)), + .. + } if e.contract_id() == &Identifier::from([0xB1; 32]) + && e.field() == "owner" + && e.required() == "other" + && e.path() == "refContractId" + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_required_readonly_contract_is_readonly() { + let result = run_reference_validation_creation_with_setup_and_mutator( + REFERENCE_VALIDATION_READONLY_CONTRACT_REF_CONTRACT_PATH, + BlockInfo::default(), + insert_contract_configured([0xC1; 32], |config| config.set_readonly(true)), + |document, _, readonly_contract_id| { + document.set("refContractId", readonly_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_required_readonly_contract_is_updatable() { + // The fixture contract itself exists in state and is not read-only + let existing_contract_id = Identifier::from_string( + REFERENCE_VALIDATION_READONLY_CONTRACT_REF_CONTRACT_ID, + Encoding::Base58, + ) + .expect("expected a valid contract id"); + + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_READONLY_CONTRACT_REF_CONTRACT_PATH, + |document, _| { + document.set("refContractId", existing_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedContractRequirementNotMetError(ref e)), + .. + } if e.contract_id() == &existing_contract_id + && e.field() == "readonly" + && e.required() == "true" + && e.path() == "refContractId" + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_required_history_keeping_contract_keeps_history() + { + let result = run_reference_validation_creation_with_setup_and_mutator( + REFERENCE_VALIDATION_KEEPS_HISTORY_CONTRACT_REF_CONTRACT_PATH, + BlockInfo::default(), + insert_contract_configured([0xC2; 32], |config| config.set_keeps_history(true)), + |document, _, history_contract_id| { + document.set("refContractId", history_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_required_history_keeping_contract_keeps_none() { + // The fixture contract itself exists in state and keeps no history + let existing_contract_id = Identifier::from_string( + REFERENCE_VALIDATION_KEEPS_HISTORY_CONTRACT_REF_CONTRACT_ID, + Encoding::Base58, + ) + .expect("expected a valid contract id"); + + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_KEEPS_HISTORY_CONTRACT_REF_CONTRACT_PATH, + |document, _| { + document.set("refContractId", existing_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedContractRequirementNotMetError(ref e)), + .. + } if e.contract_id() == &existing_contract_id + && e.field() == "keepsHistory" + && e.required() == "true" + && e.path() == "refContractId" + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_required_owner_protection_is_declared() { + let result = run_reference_validation_creation_with_setup_and_mutator( + REFERENCE_VALIDATION_OWNER_PROTECTED_CONTRACT_REF_CONTRACT_PATH, + BlockInfo::default(), + insert_elected_contract(true), + |document, _, elected_contract_id| { + document.set("refContractId", elected_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_required_owner_protection_is_not_declared() { + // Elected moderation whose declaration leaves the owner unprotected + let result = run_reference_validation_creation_with_setup_and_mutator( + REFERENCE_VALIDATION_OWNER_PROTECTED_CONTRACT_REF_CONTRACT_PATH, + BlockInfo::default(), + insert_elected_contract(false), + |document, _, elected_contract_id| { + document.set("refContractId", elected_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedContractRequirementNotMetError(ref e)), + .. + } if e.contract_id() == &Identifier::from([0xE1; 32]) + && e.field() == "ownerProtected" + && e.required() == "true" + && e.path() == "refContractId" + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_required_owner_protection_has_no_elected_moderation( + ) { + // The fixture contract itself exists in state and declares no moderation at all, so + // there is no owner protection flag to meet either value + let existing_contract_id = Identifier::from_string( + REFERENCE_VALIDATION_OWNER_PROTECTED_CONTRACT_REF_CONTRACT_ID, + Encoding::Base58, + ) + .expect("expected a valid contract id"); + + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_OWNER_PROTECTED_CONTRACT_REF_CONTRACT_PATH, + |document, _| { + document.set("refContractId", existing_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + PaidConsensusError { + error: ConsensusError::StateError(StateError::ReferencedContractRequirementNotMetError(ref e)), + .. + } if e.contract_id() == &existing_contract_id + && e.field() == "ownerProtected" + && e.required() == "true" + && e.path() == "refContractId" + ); + } + #[tokio::test] async fn should_document_creation_succeed_with_nested_and_multiple_references() { let result = run_reference_validation_creation_with_mutator( diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-keeps-history-contract-ref.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-keeps-history-contract-ref.json new file mode 100644 index 00000000000..bad217dc0a4 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-keeps-history-contract-ref.json @@ -0,0 +1,38 @@ +{ + "$formatVersion": "1", + "id": "7gQQ8BdF9gem9YxBNLd3QW7AmiGYNTTuVLerkiJZ3JCx", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "refContractId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "contract", + "contractRequirements": { + "keepsHistory": true + } + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "refContractId" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-other-contract-ref.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-other-contract-ref.json new file mode 100644 index 00000000000..dc6f2f31520 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-other-contract-ref.json @@ -0,0 +1,38 @@ +{ + "$formatVersion": "1", + "id": "EPHoATa8ifeWoviLaxTQRWfsJUHAQUCro6dEkwgjwog7", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "refContractId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "contract", + "contractRequirements": { + "owner": "other" + } + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "refContractId" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-protected-contract-ref.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-protected-contract-ref.json new file mode 100644 index 00000000000..84429f76050 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-protected-contract-ref.json @@ -0,0 +1,38 @@ +{ + "$formatVersion": "1", + "id": "6BL67iVAJ5eiVne4qXJGNRGpVqB2b2TKiF1odQ7y2qjn", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "refContractId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "contract", + "contractRequirements": { + "ownerProtected": true + } + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "refContractId" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-self-contract-ref.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-self-contract-ref.json new file mode 100644 index 00000000000..752519fde02 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-owner-self-contract-ref.json @@ -0,0 +1,38 @@ +{ + "$formatVersion": "1", + "id": "Gb9ZqQQTAoV7JCUrZhsS4ssZozbE33Suunzhr7RVy2pD", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "refContractId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "contract", + "contractRequirements": { + "owner": "self" + } + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "refContractId" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-readonly-contract-ref.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-readonly-contract-ref.json new file mode 100644 index 00000000000..ca7a468e017 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-readonly-contract-ref.json @@ -0,0 +1,38 @@ +{ + "$formatVersion": "1", + "id": "2FkduiNwoBct7PdxmCRYgLxckj4s8dhx7tXLUUpq74Qk", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "documentSchemas": { + "message": { + "type": "object", + "documentsMutable": true, + "properties": { + "refContractId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + "refersTo": { + "type": "contract", + "contractRequirements": { + "readonly": true + } + } + }, + "note": { + "type": "string", + "position": 1, + "maxLength": 64 + } + }, + "required": [ + "refContractId" + ], + "indices": [], + "additionalProperties": false + } + } +} diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 002ca42dd65..9c8c36a843d 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -529,21 +529,30 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// document id) for every resolution, where the shipped rule awarded the /// latest; DPNS contests ending from this version on follow the new rule. /// -/// 24. **Contract references may require elected moderation, a minimum age or -/// a minimum time since the last update**: a `contract` `refersTo` +/// 24. **Contract references may require elected moderation, a minimum age, a +/// minimum time since the last update, an owner relation to the writer or +/// config flags of the referenced contract**: a `contract` `refersTo` /// declaration may carry `contractRequirements`, what the referenced /// contract must declare beyond existing, with `moderation: "elected"` or /// `"electionOpen"` (elected, and the contract's own `electionDelay` since /// its creation has passed, or it declares none), /// `minimumAgeSeconds` (the contract's recorded creation time must be at -/// least that many seconds before the block time of the write) and +/// least that many seconds before the block time of the write), /// `minimumSecondsSinceUpdate` (the same of the later of its creation and /// last update times; a contract without a recorded creation time never -/// meets either) as the requirements (meta-schema v3, `apply_property_reference` 0, -/// `ContractReferenceRequirements` on `DocumentPropertyReferenceTarget::Contract`). -/// The document reference validation checks them against the contract it -/// fetched for the existence check and the block time, so they cost no -/// further read, and refuses the first unmet requirement with +/// meets either), `owner` (`"self"`: the contract is owned by the +/// `$ownerId` of the referring document, `"other"`: by anyone else), +/// `readonly: true` (its config is read-only, so it can never be updated +/// again), `keepsHistory: true` (its config keeps history) and +/// `ownerProtected` (its elected moderation declaration protects the owner +/// from the team, or does not, as the value says; a contract without +/// elected moderation meets neither value) as the requirements +/// (meta-schema v3, `apply_property_reference` 0, +/// `ContractReferenceRequirements` on +/// `DocumentPropertyReferenceTarget::Contract`). The document reference +/// validation checks them against the contract it fetched for the +/// existence check and the write itself (its owner and block time), so +/// they cost no further read, and refuses the first unmet requirement with /// `ReferencedContractRequirementNotMetError` (40135). A changed /// `contractRequirements` is an incompatible schema change on update. /// diff --git a/packages/wasm-dpp2/src/consensus_error.rs b/packages/wasm-dpp2/src/consensus_error.rs index 6dde1ce2581..d1bf70c531d 100644 --- a/packages/wasm-dpp2/src/consensus_error.rs +++ b/packages/wasm-dpp2/src/consensus_error.rs @@ -54,7 +54,9 @@ pub enum DocumentReferenceErrorCodeWasm { /// The referenced contract exists but does not meet what the reference's /// `contractRequirements` require of it: elected moderation, its election /// open, a minimum age or a minimum time since its last update at the block - /// time of the write. + /// time of the write, an owner relation to the writer of the referring + /// document, or a config flag (read-only, keeping history, the owner + /// protection of its elected moderation declaration). ReferencedContractRequirementNotMet = 40135, } diff --git a/packages/wasm-dpp2/src/data_contract/document_type_reference.rs b/packages/wasm-dpp2/src/data_contract/document_type_reference.rs index b4be3826b3f..5315e8c8e4e 100644 --- a/packages/wasm-dpp2/src/data_contract/document_type_reference.rs +++ b/packages/wasm-dpp2/src/data_contract/document_type_reference.rs @@ -44,13 +44,23 @@ export type DocumentPropertyReferenceTarget = * `minimumAgeSeconds` requires the contract's recorded creation time * to be at least that many seconds before the block time of the write, * and `minimumSecondsSinceUpdate` the same of the later of its creation - * and last update times (code 40135 when any is unmet). Absent when - * the declaration carries no requirement. + * and last update times, and `owner: 'self'` requires the contract to + * be owned by the writer of the referring document (its `$ownerId`), + * `'other'` by anyone else; `readonly: true` requires a read-only + * contract (one that can never be updated again), `keepsHistory: true` + * one keeping its history, and `ownerProtected` an elected moderation + * declaration whose owner protection flag has that value (code 40135 + * when any is unmet). Absent when the declaration carries no + * requirement. */ contractRequirements?: { moderation?: 'elected' | 'electionOpen'; minimumAgeSeconds?: number; minimumSecondsSinceUpdate?: number; + owner?: 'self' | 'other'; + readonly?: true; + keepsHistory?: true; + ownerProtected?: boolean; }; } | { type: 'token' } @@ -217,6 +227,18 @@ fn reference_to_js( path, )?; } + if let Some(owner) = contract_requirements.owner { + set_field(&fields, "owner", &JsValue::from_str(owner.as_str()), path)?; + } + for (name, flag) in [ + ("readonly", contract_requirements.readonly), + ("keepsHistory", contract_requirements.keeps_history), + ("ownerProtected", contract_requirements.owner_protected), + ] { + if let Some(flag) = flag { + set_field(&fields, name, &JsValue::from_bool(flag), path)?; + } + } set_field(&object, "contractRequirements", &fields, path)?; } }