From 3fe58b66671226e93791735fd5f47aa5b2ca5b76 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 22 Sep 2026 22:51:08 +0700 Subject: [PATCH 1/2] feat(platform)!: elected contracts declare their own election delay, read by the electionOpen reference requirement The elected moderation declaration gains an optional, unbounded `electionDelay`: seconds after the contract's creation before the first charter may be filed against it. Since config update v2 refuses declaring elected moderation after creation, the creation is the declaration's own time and nothing new is recorded. Frozen with the rest of the declaration, absent from the wire when not declared. The `moderation` reference requirement's closed set gains `"electionOpen"`: the contract declares elected moderation and its own delay has passed at the block time of the write, or it declares none. The charter's `targetContractId` declares this and carries no number. Unmet is the existing 40135 with field `moderation`, required `electionOpen`. The book sentence from #4913 that motivated `minimumSecondsSinceUpdate` with an old contract updated to declare elected moderation was wrong, since that update is refused; corrected. Co-Authored-By: Claude Fable 5.1 --- book/src/data-model/contract-moderation.md | 7 +- .../document/v3/document-meta.json | 5 +- .../config/methods/validate_update/v2/mod.rs | 4 +- .../config/moderation/elected.rs | 76 ++++++- .../data_contract/config/moderation/mod.rs | 17 +- .../class_methods/try_from_schema/mod.rs | 22 +- .../document_type/property/mod.rs | 133 ++++++++++- .../src/validation/meta_validators/mod.rs | 1 + .../batch/tests/document/creation.rs | 211 +++++++++++++++--- .../v0/contract_moderation_gate/mod.rs | 1 + .../contract_user_moderation/tests.rs | 1 + ...n-contract-election-open-contract-ref.json | 38 ++++ .../rs-platform-version/src/version/v14.rs | 9 +- packages/wasm-dpp2/src/consensus_error.rs | 5 +- .../data_contract/document_type_reference.rs | 6 +- packages/wasm-dpp2/src/data_contract/model.rs | 6 + 16 files changed, 482 insertions(+), 60 deletions(-) create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-election-open-contract-ref.json diff --git a/book/src/data-model/contract-moderation.md b/book/src/data-model/contract-moderation.md index 5ce4e5b6e56..065f6fd0bb4 100644 --- a/book/src/data-model/contract-moderation.md +++ b/book/src/data-model/contract-moderation.md @@ -274,15 +274,16 @@ pub struct ElectedModerators { pub join_window: u32, // seconds; 1 day to 4 weeks, 1 week by default pub vote_window: u32, // the same pub challenge_cool_down: u32, // seconds; 2 weeks to 3 years, always declared + pub election_delay: Option, // seconds after creation before the first charter; unbounded, none = at once pub moderated_document_types: BTreeMap>, // per type: DeleteDocuments, Ban, Suspend, Warn pub interim: InterimModerators, // ContractOwner, AppointedModerators(set), NotYetUsable, NoModeration pub owner_protected: bool, // false by default } ``` -The declaration lives in `packages/rs-dpp/src/data_contract/config/moderation/elected.rs`. On the wire it is the third `$type` of the moderators, flat: `{"$type": "elected", "challengeCoolDown": 1209600, "moderatedDocumentTypes": {"post": ["ban", "deleteDocuments"]}, "interim": {"$type": "notYetUsable"}}`, with `joinWindow`, `voteWindow` and `ownerProtected` optional. Its parts: +The declaration lives in `packages/rs-dpp/src/data_contract/config/moderation/elected.rs`. On the wire it is the third `$type` of the moderators, flat: `{"$type": "elected", "challengeCoolDown": 1209600, "moderatedDocumentTypes": {"post": ["ban", "deleteDocuments"]}, "interim": {"$type": "notYetUsable"}}`, with `joinWindow`, `voteWindow`, `electionDelay` and `ownerProtected` optional. Its parts: -- **The election parameters** are fixed once set (`SystemLimits`: `min_contract_moderation_election_window_seconds` and `max_contract_moderation_election_window_seconds` bound both windows, `min_contract_moderation_challenge_cool_down_seconds` and `max_contract_moderation_challenge_cool_down_seconds` the cool-down). The join window is how long applicants may join an election once the first one applied, the vote window how long masternodes then vote, and the cool-down how long a seated team is safe from a challenge after a seat change. Nothing reads them yet. +- **The election parameters** are fixed once set (`SystemLimits`: `min_contract_moderation_election_window_seconds` and `max_contract_moderation_election_window_seconds` bound both windows, `min_contract_moderation_challenge_cool_down_seconds` and `max_contract_moderation_challenge_cool_down_seconds` the cool-down). The join window is how long applicants may join an election once the first one applied, the vote window how long masternodes then vote, and the cool-down how long a seated team is safe from a challenge after a seat change. Nothing reads them yet. The **election delay** is the one parameter the contract sets freely: how many seconds after its creation the first charter may be filed against it, the notice the contract gives before its first election can be called. It is optional and unbounded; left out, the election may be called at once. Because the declaration is made at the contract's creation and never changes, the creation is the declaration's own time. The charter contract's `targetContractId` reads it through the `moderation: "electionOpen"` requirement below. - **The moderated set** is the document types the team moderates, each with the abilities a charter may claim on it: non-empty, each type a document type of the contract, each ability set non-empty and backed by the contract (`ban` needs the banlist, `suspend` the suspension list, `warn` the warning list, `deleteDocuments` the type itself flagged `canBeDeletedByModerators`, so deletions reach only flagged types, within their window). The charter of a team will say how those types are moderated, never which. The lists stay contract-wide: an ability on a type is what a team may do over the documents of that type. The set also bounds the interim block. A charter does not price the moderators part of an action: a type's own `actionFees.moderators` amount is the most a team may charge, a charter charges a share of it (the charter contract's business, not the declaration's), and the owner part stays what the type declares, immutable as before. - **The interim** says who moderates until a team is seated. `ContractOwner` and `AppointedModerators(set)` are the merged kinds, with their authority, their limit and their existence check (41110 at create): they moderate, they are protected, and they are the team that claims the moderators pot. `NotYetUsable` names nobody: nobody moderates, nobody claims the pot (it accumulates for the team to come, `ContractFeeClaimNotAllowedError` for everyone), and the moderated document types can not be used. A contract that never attracts a team keeps those types unusable for good; the other types work as on an unmoderated contract. `NoModeration` names nobody too, with the moderated types usable meanwhile: nobody moderates and nobody claims the pot, and every type works as on an unmoderated contract until a team is seated. - **The owner flag** says whether the contract owner is protected from the team once one is seated, as the owner and the moderators of the merged kinds are (41102 on a ban, a suspension or a deletion of its documents). Not protected by default. During the interim the owner is protected whenever it moderates, flag or not: `ContractModerationConfig::protects` is what the moderation transition checks, and it is `may_moderate` or the flag for the owner. @@ -293,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"`; `minimumAgeSeconds`, which requires the contract's recorded creation time to be at least that many seconds before the block time of the write (a delay between a contract's creation and the first charter against it, so a team cannot be seated before anyone has seen the contract); and `minimumSecondsSinceUpdate`, the same of the later of the contract's creation and last update times (so an old contract updated to declare elected moderation gets the same notice before its first charter; any update restarts the clock). A contract created before contracts recorded their creation time never meets either duration. 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); `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. **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 54b86552727..a59c93a4da6 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,12 +133,13 @@ "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; 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). An unmet requirement refuses the write (ReferencedContractRequirementNotMetError, 40135)", "type": "object", "properties": { "moderation": { "enum": [ - "elected" + "elected", + "electionOpen" ] }, "minimumAgeSeconds": { diff --git a/packages/rs-dpp/src/data_contract/config/methods/validate_update/v2/mod.rs b/packages/rs-dpp/src/data_contract/config/methods/validate_update/v2/mod.rs index adc8ebabecf..befbeb0636b 100644 --- a/packages/rs-dpp/src/data_contract/config/methods/validate_update/v2/mod.rs +++ b/packages/rs-dpp/src/data_contract/config/methods/validate_update/v2/mod.rs @@ -215,6 +215,7 @@ mod tests { BTreeSet::from([ModerationAbility::Ban]), )]), interim: InterimModerators::ContractOwner, + election_delay: None, owner_protected: false, }; modify(&mut declaration); @@ -238,10 +239,11 @@ mod tests { assert!(kept.is_valid(), "{:?}", kept.errors); type Change = (&'static str, fn(&mut ElectedModerators)); - let changes: [Change; 7] = [ + let changes: [Change; 8] = [ ("join window", |d| d.join_window += 1), ("vote window", |d| d.vote_window += 1), ("challenge cool-down", |d| d.challenge_cool_down += 1), + ("election delay", |d| d.election_delay = Some(1)), ("moderated set", |d| { d.moderated_document_types .insert("like".to_string(), BTreeSet::from([ModerationAbility::Ban])); diff --git a/packages/rs-dpp/src/data_contract/config/moderation/elected.rs b/packages/rs-dpp/src/data_contract/config/moderation/elected.rs index 6c5bbb545a8..7afb9aa7a8d 100644 --- a/packages/rs-dpp/src/data_contract/config/moderation/elected.rs +++ b/packages/rs-dpp/src/data_contract/config/moderation/elected.rs @@ -15,6 +15,7 @@ use crate::data_contract::config::moderation::{ document_schema_lets_moderators_delete, ContractModerationConfig, }; use crate::data_contract::DocumentName; +use crate::prelude::TimestampMillis; #[cfg(feature = "json-conversion")] use crate::serialization::JsonSafeFields; use bincode::{Decode, DecodeUntrusted, Encode}; @@ -35,6 +36,8 @@ pub mod property_names { pub const VOTE_WINDOW: &str = "voteWindow"; /// The challenge cool-down, in seconds pub const CHALLENGE_COOL_DOWN: &str = "challengeCoolDown"; + /// The election delay, in seconds after the contract's creation + pub const ELECTION_DELAY: &str = "electionDelay"; /// The moderated document types, each with the abilities a charter may claim on it pub const MODERATED_DOCUMENT_TYPES: &str = "moderatedDocumentTypes"; /// The interim moderators @@ -314,6 +317,12 @@ pub struct ElectedModerators { /// `SystemLimits::max_contract_moderation_challenge_cool_down_seconds` (two weeks to /// three years), always declared. pub challenge_cool_down: u32, + /// How long, in seconds after the contract's creation, before the first charter may be + /// filed against the contract: the notice the contract gives before its first election + /// can be called. Unbounded, and `None` when the declaration leaves it out, in which + /// case the election may be called at once. A reference declaring + /// `contractRequirements: { "moderation": "electionOpen" }` is what reads it. + pub election_delay: Option, /// The document types the team moderates, each with the abilities a charter may claim /// on it: non-empty, each type a document type of the contract, each ability set /// non-empty and backed by the contract (`Ban`, `Suspend` and `Warn` by the list the @@ -334,6 +343,26 @@ pub struct ElectedModerators { } impl ElectedModerators { + /// Whether the first election may be called at `block_time_ms` on a contract created at + /// `contract_created_at`: the declaration has no election delay, or the delay has passed + /// since the creation. An elected declaration is made at the contract's creation and + /// never changes, so the creation is the declaration's own time. A contract without a + /// recorded creation time and with a delay is of unknown age, and its election is not + /// open. + pub fn election_is_open( + &self, + contract_created_at: Option, + block_time_ms: TimestampMillis, + ) -> bool { + match self.election_delay { + None => true, + Some(delay) => contract_created_at.is_some_and(|created_at| { + block_time_ms + >= created_at.saturating_add(TimestampMillis::from(delay).saturating_mul(1000)) + }), + } + } + /// Whether the team moderates the document type pub fn moderates_document_type(&self, document_type_name: &str) -> bool { self.moderated_document_types @@ -440,7 +469,14 @@ impl fmt::Display for ElectedModerators { f, "an elected moderation team, in its interim moderated by {}", self.interim - ) + )?; + if let Some(delay) = self.election_delay { + write!( + f, + ", its first election open {delay} seconds after the contract's creation" + )?; + } + Ok(()) } } @@ -485,6 +521,7 @@ mod tests { moderated(&[ModerationAbility::Ban, ModerationAbility::Suspend]), )]), interim: InterimModerators::ContractOwner, + election_delay: None, owner_protected: false, } } @@ -714,11 +751,13 @@ mod tests { let mut declaration = elected(); declaration.interim = InterimModerators::AppointedModerators(set(&[1, 2])); declaration.owner_protected = true; + declaration.election_delay = Some(86_400); let moderators = ContractModerators::Elected(Box::new(declaration)); let json = serde_json::to_value(&moderators).expect("serialize"); assert_eq!(json["$type"], "elected"); assert_eq!(json["joinWindow"], 604_800); + assert_eq!(json["electionDelay"], 86_400); assert_eq!( json["moderatedDocumentTypes"], serde_json::json!({ "post": ["ban", "suspend"] }) @@ -758,6 +797,12 @@ mod tests { let parsed: ContractModerators = serde_json::from_value(minimal).expect("deserialize"); let elected = parsed.elected().expect("elected"); assert_eq!(elected.join_window, DEFAULT_ELECTION_WINDOW_SECONDS); + assert_eq!(elected.election_delay, None); + let json = serde_json::to_value(&parsed).expect("serialize"); + assert!( + json.get("electionDelay").is_none(), + "a declaration without a delay serializes none: {json}" + ); assert_eq!(elected.vote_window, DEFAULT_ELECTION_WINDOW_SECONDS); assert!(!elected.owner_protected); assert_eq!(elected.interim, InterimModerators::NotYetUsable); @@ -839,6 +884,28 @@ mod tests { } } + #[test] + fn should_open_the_election_after_the_delay_from_the_contract_creation() { + let created_at: TimestampMillis = 1_700_000_000_000; + let mut declaration = elected(); + + // No delay: open at once, whether or not the creation time is recorded + assert!(declaration.election_is_open(Some(created_at), created_at)); + assert!(declaration.election_is_open(None, 0)); + + declaration.election_delay = Some(3600); + assert!(!declaration.election_is_open(Some(created_at), created_at + 3_599_999)); + assert!(declaration.election_is_open(Some(created_at), created_at + 3_600_000)); + assert!(declaration.election_is_open(Some(created_at), TimestampMillis::MAX)); + // A delay on a contract of unknown age never opens + assert!(!declaration.election_is_open(None, TimestampMillis::MAX)); + // The bound saturates rather than wrapping around into the past + declaration.election_delay = Some(u32::MAX); + assert!( + !declaration.election_is_open(Some(TimestampMillis::MAX - 1), TimestampMillis::MAX - 1) + ); + } + #[test] fn should_describe_itself() { let mut declaration = elected(); @@ -846,6 +913,13 @@ mod tests { ContractModerators::Elected(Box::new(declaration.clone())).to_string(), "an elected moderation team, in its interim moderated by the contract owner" ); + declaration.election_delay = Some(86_400); + assert_eq!( + declaration.to_string(), + "an elected moderation team, in its interim moderated by the contract owner, its \ + first election open 86400 seconds after the contract's creation" + ); + declaration.election_delay = None; declaration.interim = InterimModerators::NotYetUsable; assert_eq!( declaration.to_string(), diff --git a/packages/rs-dpp/src/data_contract/config/moderation/mod.rs b/packages/rs-dpp/src/data_contract/config/moderation/mod.rs index 77307cfde0f..30262d0941e 100644 --- a/packages/rs-dpp/src/data_contract/config/moderation/mod.rs +++ b/packages/rs-dpp/src/data_contract/config/moderation/mod.rs @@ -171,7 +171,8 @@ impl Serialize for ContractModerators { m.end() } ContractModerators::Elected(elected) => { - let mut m = serializer.serialize_map(Some(7))?; + let entries = 7 + usize::from(elected.election_delay.is_some()); + let mut m = serializer.serialize_map(Some(entries))?; m.serialize_entry("$type", "elected")?; m.serialize_entry(elected_names::JOIN_WINDOW, &elected.join_window)?; m.serialize_entry(elected_names::VOTE_WINDOW, &elected.vote_window)?; @@ -179,6 +180,11 @@ impl Serialize for ContractModerators { elected_names::CHALLENGE_COOL_DOWN, &elected.challenge_cool_down, )?; + // Absent, not null, when the declaration has no delay: the wire form of a + // declaration that left it out is unchanged + if let Some(delay) = elected.election_delay { + m.serialize_entry(elected_names::ELECTION_DELAY, &delay)?; + } m.serialize_entry( elected_names::MODERATED_DOCUMENT_TYPES, &elected.moderated_document_types, @@ -202,6 +208,7 @@ impl<'de> Deserialize<'de> for ContractModerators { elected_names::JOIN_WINDOW, elected_names::VOTE_WINDOW, elected_names::CHALLENGE_COOL_DOWN, + elected_names::ELECTION_DELAY, elected_names::MODERATED_DOCUMENT_TYPES, elected_names::INTERIM, elected_names::OWNER_PROTECTED, @@ -213,6 +220,7 @@ impl<'de> Deserialize<'de> for ContractModerators { join_window: Option, vote_window: Option, challenge_cool_down: Option, + election_delay: Option, moderated_document_types: Option>>, interim: Option, owner_protected: Option, @@ -223,6 +231,7 @@ impl<'de> Deserialize<'de> for ContractModerators { self.join_window.is_some() || self.vote_window.is_some() || self.challenge_cool_down.is_some() + || self.election_delay.is_some() || self.moderated_document_types.is_some() || self.interim.is_some() || self.owner_protected.is_some() @@ -282,6 +291,11 @@ impl<'de> Deserialize<'de> for ContractModerators { elected_names::CHALLENGE_COOL_DOWN, &mut elected.challenge_cool_down, )?, + elected_names::ELECTION_DELAY => read_once( + &mut map, + elected_names::ELECTION_DELAY, + &mut elected.election_delay, + )?, elected_names::MODERATED_DOCUMENT_TYPES => read_once( &mut map, elected_names::MODERATED_DOCUMENT_TYPES, @@ -339,6 +353,7 @@ impl<'de> Deserialize<'de> for ContractModerators { challenge_cool_down: elected .challenge_cool_down .ok_or_else(required(elected_names::CHALLENGE_COOL_DOWN))?, + election_delay: elected.election_delay, moderated_document_types: elected .moderated_document_types .ok_or_else(required(elected_names::MODERATED_DOCUMENT_TYPES))?, 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 672b4feb1ae..38c0c0e3b7c 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 @@ -553,7 +553,8 @@ fn parse_contract_reference_requirements( })?; fields.moderation = Some(ContractReferenceModeration::from_wire_name(name).ok_or_else(|| { DataContractError::InvalidContractStructure(format!( - "contract refersTo contractRequirements moderation {name:?} is unknown, expected \"elected\"" + "contract refersTo contractRequirements moderation {name:?} is unknown, expected one of {:?}", + ContractReferenceModeration::WIRE_NAMES )) })?); } @@ -1124,6 +1125,25 @@ mod tests { ); } + #[test] + fn should_parse_contract_refers_to_requiring_the_moderation_election_open() { + assert_eq!( + contract_reference_target(json!({ + "type": "contract", + "contractRequirements": { "moderation": "electionOpen" } + })), + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: ContractReferenceRequirements { + moderation: Some(ContractReferenceModeration::ElectionOpen), + minimum_age_seconds: None, + minimum_seconds_since_update: None, + }, + } + ) + ); + } + #[test] fn should_parse_contract_refers_to_requiring_a_minimum_age_or_time_since_update() { assert_eq!( 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 12ef6abbe3f..4bc87c802c2 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,7 +12,6 @@ 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::moderation::ContractModerators; use crate::data_contract::config::v1::DataContractConfigGettersV1; use crate::data_contract::config::v2::DataContractConfigGettersV2; use crate::data_contract::config::DataContractConfig; @@ -127,6 +126,11 @@ pub enum ContractReferenceModeration { /// The contract declares an elected moderation team (`ContractModerators::Elected`), /// whatever its interim and whether a team is seated yet. Elected, + /// The contract declares an elected moderation team whose own `electionDelay`, counted + /// from the contract's creation, has passed at the block time of the write, or which + /// declares no delay. The delay is the contract's, not the reference's: the charter + /// contract's `targetContractId` declares this and carries no number. + ElectionOpen, } impl ContractReferenceModeration { @@ -134,25 +138,42 @@ impl ContractReferenceModeration { pub fn as_str(&self) -> &'static str { match self { ContractReferenceModeration::Elected => "elected", + ContractReferenceModeration::ElectionOpen => "electionOpen", } } + /// The wire names, for the message that refuses another. + pub const WIRE_NAMES: &'static [&'static str] = &["elected", "electionOpen"]; + /// The moderation a wire name names, `None` for any other name. pub fn from_wire_name(name: &str) -> Option { match name { "elected" => Some(ContractReferenceModeration::Elected), + "electionOpen" => Some(ContractReferenceModeration::ElectionOpen), _ => None, } } - /// Whether `contract` declares what this requires. - pub fn is_met_by(&self, contract: &DataContract) -> bool { + /// How the requirement reads after "a contract with". + pub fn describe(&self) -> &'static str { match self { - ContractReferenceModeration::Elected => { - contract.config().moderation().is_some_and(|moderation| { - matches!(moderation.moderators, ContractModerators::Elected(_)) - }) - } + ContractReferenceModeration::Elected => "elected moderation", + ContractReferenceModeration::ElectionOpen => "its moderation election open", + } + } + + /// Whether `contract` declares what this requires 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 { + let elected = contract + .config() + .moderation() + .and_then(|moderation| moderation.moderators.elected()); + match self { + ContractReferenceModeration::Elected => elected.is_some(), + ContractReferenceModeration::ElectionOpen => elected.is_some_and(|elected| { + elected.election_is_open(contract.created_at(), block_time_ms) + }), } } } @@ -195,7 +216,9 @@ impl ContractReferenceRequirement { /// writing the referring document. pub fn is_met_by(&self, contract: &DataContract, block_time_ms: TimestampMillis) -> bool { match self { - ContractReferenceRequirement::Moderation(moderation) => moderation.is_met_by(contract), + ContractReferenceRequirement::Moderation(moderation) => { + moderation.is_met_by(contract, block_time_ms) + } ContractReferenceRequirement::MinimumAgeSeconds(seconds) => { Self::minimum_age_is_met(contract.created_at(), *seconds, block_time_ms) } @@ -464,7 +487,7 @@ impl std::fmt::Display for DocumentPropertyReferenceTarget { } => { write!(f, "contract")?; if let Some(moderation) = contract_requirements.moderation { - write!(f, " with {} moderation", moderation.as_str())?; + write!(f, " with {}", moderation.describe())?; } if let Some(seconds) = contract_requirements.minimum_age_seconds { write!(f, " at least {seconds} seconds old")?; @@ -7779,6 +7802,85 @@ mod tests { ); } + #[test] + fn should_meet_election_open_when_the_contract_declares_no_delay_or_the_delay_passed() { + use crate::data_contract::accessors::v0::DataContractV0Setters; + use crate::data_contract::accessors::v1::DataContractV1Setters; + use crate::data_contract::config::moderation::{ + ContractModerationConfig, ContractModerators, ElectedModerators, InterimModerators, + ModerationAbility, DEFAULT_ELECTION_WINDOW_SECONDS, + }; + 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 created_at: TimestampMillis = 1_700_000_000_000; + contract.set_created_at(Some(created_at)); + + let elected = ContractReferenceModeration::Elected; + let open = ContractReferenceModeration::ElectionOpen; + + // No moderation at all: neither is met + assert!(!elected.is_met_by(&contract, created_at)); + assert!(!open.is_met_by(&contract, created_at)); + + let declare = |contract: &mut DataContract, election_delay: Option| { + let 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, + moderated_document_types: BTreeMap::from([( + "profile".to_string(), + BTreeSet::from([ModerationAbility::Ban]), + )]), + interim: InterimModerators::ContractOwner, + owner_protected: false, + })), + })); + contract.set_config(config); + }; + + // Elected without a delay: open at once + declare(&mut contract, None); + assert!(elected.is_met_by(&contract, created_at)); + assert!(open.is_met_by(&contract, created_at)); + + // Elected with a delay: elected at once, open once the delay passed + declare(&mut contract, Some(3600)); + assert!(elected.is_met_by(&contract, created_at)); + assert!(!open.is_met_by(&contract, created_at + 3_599_999)); + assert!(open.is_met_by(&contract, created_at + 3_600_000)); + + // A delay on a contract of unknown age never opens + contract.set_created_at(None); + assert!(!open.is_met_by(&contract, TimestampMillis::MAX)); + + assert_eq!( + ContractReferenceModeration::from_wire_name("electionOpen"), + Some(ContractReferenceModeration::ElectionOpen) + ); + assert_eq!( + ContractReferenceModeration::ElectionOpen.as_str(), + "electionOpen" + ); + assert_eq!( + ContractReferenceRequirement::Moderation(ContractReferenceModeration::ElectionOpen) + .required(), + "electionOpen" + ); + } + #[test] fn should_take_the_last_change_time_from_the_later_of_creation_and_update() { use crate::data_contract::accessors::v1::DataContractV1Setters; @@ -7863,6 +7965,17 @@ mod tests { .to_string(), "contract at least 1 seconds old" ); + assert_eq!( + DocumentPropertyReferenceTarget::Contract { + contract_requirements: ContractReferenceRequirements { + moderation: Some(ContractReferenceModeration::ElectionOpen), + minimum_age_seconds: None, + minimum_seconds_since_update: None, + }, + } + .to_string(), + "contract with its moderation election open" + ); assert_eq!(DocumentPropertyReferenceTarget::Token.to_string(), "token"); assert_eq!( DocumentPropertyReferenceTarget::PermanentDocument { diff --git a/packages/rs-dpp/src/validation/meta_validators/mod.rs b/packages/rs-dpp/src/validation/meta_validators/mod.rs index 01f257f877b..58b373b814d 100644 --- a/packages/rs-dpp/src/validation/meta_validators/mod.rs +++ b/packages/rs-dpp/src/validation/meta_validators/mod.rs @@ -343,6 +343,7 @@ mod tests { fn should_accept_contract_requirements_on_a_contract_refers_to_in_v3_document_schema() { for requirements in [ json!({ "moderation": "elected" }), + json!({ "moderation": "electionOpen" }), json!({ "minimumAgeSeconds": 1 }), json!({ "minimumAgeSeconds": 4294967295u64 }), json!({ "minimumSecondsSinceUpdate": 86400 }), 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 ffafa84cb55..2359c05630d 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 @@ -5303,6 +5303,12 @@ mod creation_tests { /// tests, and one that declares no moderation, so a reference to it is unmet. const REFERENCE_VALIDATION_ELECTED_CONTRACT_REF_CONTRACT_ID: &str = "9k3RE6kHNTsDmyXFwEPpiFQ3ipXfp5FuXGXpQ1rDHDJb"; + const REFERENCE_VALIDATION_ELECTION_OPEN_CONTRACT_REF_CONTRACT_PATH: &str = + "tests/supporting_files/contract/reference-validation/reference-validation-contract-election-open-contract-ref.json"; + /// The `id` of the election-open-contract-reference fixture: in state in its tests, and + /// one that declares no moderation, so a reference to it is unmet. + const REFERENCE_VALIDATION_ELECTION_OPEN_CONTRACT_REF_CONTRACT_ID: &str = + "FutvNUuQYthkyfNtaEsdJEFsnCQshBohD9GP7NrSUwR"; const REFERENCE_VALIDATION_AGED_CONTRACT_REF_CONTRACT_PATH: &str = "tests/supporting_files/contract/reference-validation/reference-validation-contract-aged-contract-ref.json"; /// The `id` of the aged-contract-reference fixture (`minimumAgeSeconds: 3600`): written to @@ -5623,44 +5629,179 @@ mod creation_tests { /// list behind it. fn insert_elected_contract( platform: &mut TempPlatform, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, ) -> Identifier { - use dpp::data_contract::accessors::v0::DataContractV0Getters; - use dpp::data_contract::config::moderation::{ - ContractModerationConfig, ContractModerators, ElectedModerators, InterimModerators, - ModerationAbility, DEFAULT_ELECTION_WINDOW_SECONDS, - }; - use std::collections::{BTreeMap, BTreeSet}; + insert_elected_contract_with(None, None)(platform, platform_version) + } - let contract = setup_contract( - &platform.drive, - REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_PATH, - Some([0xE1; 32]), - None, - Some(|contract: &mut DataContract| { - contract.set_config(contract.config().clone().with_moderation(Some( - ContractModerationConfig { - banlist: true, - suspensions: true, - 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, - moderated_document_types: BTreeMap::from([( - "message".to_string(), - BTreeSet::from([ModerationAbility::Ban]), - )]), - interim: InterimModerators::ContractOwner, - owner_protected: false, - })), - }, - ))); - }), - None, - None, + /// An elected contract with the given election delay and recorded creation time, 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| { + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::accessors::v1::DataContractV1Setters; + use dpp::data_contract::config::moderation::{ + ContractModerationConfig, ContractModerators, ElectedModerators, InterimModerators, + ModerationAbility, DEFAULT_ELECTION_WINDOW_SECONDS, + }; + use std::collections::{BTreeMap, BTreeSet}; + + let contract = setup_contract( + &platform.drive, + REFERENCE_VALIDATION_CONTRACT_REF_CONTRACT_PATH, + Some([0xE1; 32]), + None, + Some(|contract: &mut DataContract| { + contract.set_created_at(created_at); + contract.set_config(contract.config().clone().with_moderation(Some( + ContractModerationConfig { + banlist: true, + suspensions: true, + 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, + moderated_document_types: BTreeMap::from([( + "message".to_string(), + BTreeSet::from([ModerationAbility::Ban]), + )]), + interim: InterimModerators::ContractOwner, + owner_protected: false, + })), + }, + ))); + }), + 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 + let existing_contract_id = Identifier::from_string( + REFERENCE_VALIDATION_ELECTION_OPEN_CONTRACT_REF_CONTRACT_ID, + Encoding::Base58, + ) + .expect("expected a valid contract id"); + + let result = run_reference_validation_creation_with_mutator( + REFERENCE_VALIDATION_ELECTION_OPEN_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() == "moderation" + && e.required() == "electionOpen" + && e.path() == "refContractId" + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_elected_contract_declares_no_election_delay() { + // No delay: the election is open from the contract's creation, whether or not the + // creation time is recorded + 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), + |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_the_election_delay_has_not_passed() { + // Created one millisecond less than its own delay before the block + 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(3600), + Some(AGED_REFERENCE_BLOCK_TIME_MS - AGED_REFERENCE_MINIMUM_AGE_MS + 1), + ), + |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() == "moderation" + && e.required() == "electionOpen" + && e.path() == "refContractId" + ); + } + + #[tokio::test] + async fn should_document_creation_fail_when_a_delayed_contract_has_no_creation_time() { + // A delay on a contract of unknown age never opens + 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), + |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.field() == "moderation" && e.required() == "electionOpen" + ); + } + + #[tokio::test] + async fn should_document_creation_succeed_when_the_election_delay_has_passed() { + // Created exactly its own delay before the block + 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(3600), + Some(AGED_REFERENCE_BLOCK_TIME_MS - AGED_REFERENCE_MINIMUM_AGE_MS), + ), + |document, _, elected_contract_id| { + document.set("refContractId", elected_contract_id.into()); + }, + ) + .await; + + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. } ); - contract.id() } #[tokio::test] diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/contract_moderation_gate/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/contract_moderation_gate/mod.rs index 345ae197d42..7a93976d9cc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/contract_moderation_gate/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/contract_moderation_gate/mod.rs @@ -443,6 +443,7 @@ mod tests { BTreeSet::from([ModerationAbility::Ban]), )]), interim: InterimModerators::NotYetUsable, + election_delay: None, owner_protected: false, })), }, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/contract_user_moderation/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/contract_user_moderation/tests.rs index a21d62d7431..516572d3816 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/contract_user_moderation/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/contract_user_moderation/tests.rs @@ -3057,6 +3057,7 @@ fn elected(interim: InterimModerators, moderated: &[&str]) -> ContractModeration }) .collect(), interim, + election_delay: None, owner_protected: false, })), } diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-election-open-contract-ref.json b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-election-open-contract-ref.json new file mode 100644 index 00000000000..fd24dc7d0f7 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-election-open-contract-ref.json @@ -0,0 +1,38 @@ +{ + "$formatVersion": "1", + "id": "FutvNUuQYthkyfNtaEsdJEFsnCQshBohD9GP7NrSUwR", + "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": { + "moderation": "electionOpen" + } + } + }, + "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 530e2ff4eaa..002ca42dd65 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -496,7 +496,10 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// beside the owner and an appointed set, in the same config V2). The /// declaration is frozen: the join and vote windows (one day to four weeks, /// one week by default) and the challenge cool-down (two weeks to three -/// years), all in seconds and bounded by `SYSTEM_LIMITS_V4`; the document +/// years), all in seconds and bounded by `SYSTEM_LIMITS_V4`; an optional, +/// unbounded election delay in seconds after the contract's creation +/// before the first charter may be filed (`electionDelay`, read by the +/// `moderation: "electionOpen"` reference requirement of item 24); the document /// types the team moderates, each with the abilities a charter may claim on /// it; who moderates until the first team is seated (the owner, an /// appointed set, or nobody, with the moderated types not yet usable or @@ -529,7 +532,9 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// 24. **Contract references may require elected moderation, a minimum age or /// a minimum time since the last update**: a `contract` `refersTo` /// declaration may carry `contractRequirements`, what the referenced -/// contract must declare beyond existing, with `moderation: "elected"`, +/// 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 /// `minimumSecondsSinceUpdate` (the same of the later of its creation and diff --git a/packages/wasm-dpp2/src/consensus_error.rs b/packages/wasm-dpp2/src/consensus_error.rs index 00e207bef1d..6dde1ce2581 100644 --- a/packages/wasm-dpp2/src/consensus_error.rs +++ b/packages/wasm-dpp2/src/consensus_error.rs @@ -52,8 +52,9 @@ pub enum DocumentReferenceErrorCodeWasm { /// declaring `canBeDeleted: false`. ReferencedDocumentTypeNotDeletable = 40131, /// The referenced contract exists but does not meet what the reference's - /// `contractRequirements` require of it: elected moderation, a minimum age - /// or a minimum time since its last update at the block time of the write. + /// `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. 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 d5f8e8c411d..b4be3826b3f 100644 --- a/packages/wasm-dpp2/src/data_contract/document_type_reference.rs +++ b/packages/wasm-dpp2/src/data_contract/document_type_reference.rs @@ -38,7 +38,9 @@ export type DocumentPropertyReferenceTarget = * What the referenced contract must declare beyond existing, checked * by consensus when the referring document is written against the * contract fetched for the existence check and the block time: - * `moderation: 'elected'` requires an elected moderation team, + * `moderation: 'elected'` requires an elected moderation team and + * `'electionOpen'` one whose own `electionDelay` has passed since the + * contract's creation (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 same of the later of its creation @@ -46,7 +48,7 @@ export type DocumentPropertyReferenceTarget = * the declaration carries no requirement. */ contractRequirements?: { - moderation?: 'elected'; + moderation?: 'elected' | 'electionOpen'; minimumAgeSeconds?: number; minimumSecondsSinceUpdate?: number; }; diff --git a/packages/wasm-dpp2/src/data_contract/model.rs b/packages/wasm-dpp2/src/data_contract/model.rs index 3e7d5d20d8b..7d01a65bd15 100644 --- a/packages/wasm-dpp2/src/data_contract/model.rs +++ b/packages/wasm-dpp2/src/data_contract/model.rs @@ -140,6 +140,12 @@ export type ContractModerators = joinWindow?: number; voteWindow?: number; challengeCoolDown: number; + /** + * Seconds after the contract's creation before the first charter may be filed + * against it, unbounded; the election may be called at once when left out. A + * reference requiring `moderation: 'electionOpen'` reads it. + */ + electionDelay?: number; /** * The moderated document types of the contract, each with the non-empty abilities a * charter may claim on it: `ban`, `suspend` and `warn` need the list the contract From b1c9b94d6dc37eb4d01ccd4445db986518e9ccad Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 22 Sep 2026 23:03:30 +0700 Subject: [PATCH 2/2] docs(dpp): name the user of each moderation requirement value A charter proposal needs the target elected so teams can form during the notice; the charter that opens the contest needs the election open. Co-Authored-By: Claude Fable 5.1 --- book/src/data-model/contract-moderation.md | 2 +- .../rs-dpp/src/data_contract/document_type/property/mod.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/book/src/data-model/contract-moderation.md b/book/src/data-model/contract-moderation.md index 065f6fd0bb4..815617b59eb 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); `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; 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. **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/src/data_contract/document_type/property/mod.rs b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs index 4bc87c802c2..84dcfb4b84a 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 @@ -124,12 +124,14 @@ pub struct ContractReferenceRequirements { #[serde(rename_all = "camelCase")] pub enum ContractReferenceModeration { /// The contract declares an elected moderation team (`ContractModerators::Elected`), - /// whatever its interim and whether a team is seated yet. + /// whatever its interim, whether a team is seated yet and whether its election delay + /// has passed. A charter proposal declares this, so teams can form during the notice + /// the contract gives before its first election. Elected, /// The contract declares an elected moderation team whose own `electionDelay`, counted /// from the contract's creation, has passed at the block time of the write, or which /// declares no delay. The delay is the contract's, not the reference's: the charter - /// contract's `targetContractId` declares this and carries no number. + /// that opens the contest declares this and carries no number. ElectionOpen, }