Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book/src/data-model/contract-moderation.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,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 (`moderation: "elected"` is the first). Consensus checks it when the referring document is written, against the contract it has already fetched for the existence check, so it costs no further read; a contract that exists but does not declare an elected team refuses the write, paid, with `ReferencedContractRequirementNotMetError` (40135), 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"`; `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.

**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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,23 @@
"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, 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. 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; 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"
]
},
"minimumAgeSeconds": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"minimumSecondsSinceUpdate": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
}
},
"minProperties": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,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, at least one when the object is given at all.
/// contract with a closed set of values (`moderation`), 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<String, &Value>,
) -> Result<ContractReferenceRequirements, DataContractError> {
Expand Down Expand Up @@ -556,6 +557,13 @@ fn parse_contract_reference_requirements(
))
})?);
}
property_names::MINIMUM_AGE_SECONDS => {
fields.minimum_age_seconds = Some(parse_contract_reference_seconds(&field, value)?);
}
property_names::MINIMUM_SECONDS_SINCE_UPDATE => {
fields.minimum_seconds_since_update =
Some(parse_contract_reference_seconds(&field, value)?);
}
other => {
return Err(DataContractError::InvalidContractStructure(format!(
"contract refersTo contractRequirements {other:?} is unknown"
Expand All @@ -566,6 +574,22 @@ fn parse_contract_reference_requirements(
Ok(fields)
}

/// A duration requirement of a `contract` reference (`minimumAgeSeconds`,
/// `minimumSecondsSinceUpdate`): a whole number of seconds from 1 to `u32::MAX`.
fn parse_contract_reference_seconds(field: &str, value: &Value) -> Result<u32, DataContractError> {
let seconds: u32 = value.to_integer().map_err(|_| {
DataContractError::InvalidContractStructure(format!(
"contract refersTo contractRequirements {field} must be an integer from 1 to 4294967295"
))
})?;
if seconds == 0 {
return Err(DataContractError::InvalidContractStructure(format!(
"contract refersTo contractRequirements {field} must be at least 1"
)));
}
Ok(seconds)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1092,10 +1116,90 @@ mod tests {
DocumentPropertyReferenceTarget::Contract {
contract_requirements: ContractReferenceRequirements {
moderation: Some(ContractReferenceModeration::Elected),
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!(
contract_reference_target(json!({
"type": "contract",
"contractRequirements": { "minimumAgeSeconds": 604800 }
})),
DocumentPropertyType::IdentifierWithReference(
DocumentPropertyReferenceTarget::Contract {
contract_requirements: ContractReferenceRequirements {
moderation: None,
minimum_age_seconds: Some(604_800),
minimum_seconds_since_update: None,
},
}
)
);
assert_eq!(
contract_reference_target(json!({
"type": "contract",
"contractRequirements": { "minimumSecondsSinceUpdate": 86400 }
})),
DocumentPropertyType::IdentifierWithReference(
DocumentPropertyReferenceTarget::Contract {
contract_requirements: ContractReferenceRequirements {
moderation: None,
minimum_age_seconds: None,
minimum_seconds_since_update: Some(86_400),
},
}
)
);
assert_eq!(
contract_reference_target(json!({
"type": "contract",
"contractRequirements": {
"moderation": "elected",
"minimumAgeSeconds": u32::MAX,
"minimumSecondsSinceUpdate": 1
}
})),
DocumentPropertyType::IdentifierWithReference(
DocumentPropertyReferenceTarget::Contract {
contract_requirements: ContractReferenceRequirements {
moderation: Some(ContractReferenceModeration::Elected),
minimum_age_seconds: Some(u32::MAX),
minimum_seconds_since_update: Some(1),
},
}
)
);
}

#[test]
fn should_reject_a_duration_requirement_that_is_zero_negative_too_large_or_not_an_integer() {
for field in ["minimumAgeSeconds", "minimumSecondsSinceUpdate"] {
for (seconds, fragment) in [
(json!(0), "must be at least 1"),
(json!(-1), "must be an integer"),
(json!(u64::from(u32::MAX) + 1), "must be an integer"),
(json!(1.5), "must be an integer"),
(json!("3600"), "must be an integer"),
] {
let refers_to = json!({
"type": "contract",
"contractRequirements": { field: seconds }
});
let err =
try_document_type_from_schema(contract_reference_schema(refers_to.clone()))
.expect_err("should be refused");
assert!(
err.to_string().contains(fragment) && err.to_string().contains(field),
"{refers_to}: expected {fragment:?} naming {field}, got {err}"
);
}
}
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2011,6 +2011,16 @@ mod tests {
platform_value!({ "type": "contract" }),
"/properties/toUserId/refersTo/contractRequirements",
),
(
platform_value!({ "type": "contract", "contractRequirements": { "minimumAgeSeconds": 3600 } }),
platform_value!({ "type": "contract", "contractRequirements": { "minimumAgeSeconds": 7200 } }),
"/properties/toUserId/refersTo/contractRequirements/minimumAgeSeconds",
),
(
platform_value!({ "type": "contract", "contractRequirements": { "minimumSecondsSinceUpdate": 60 } }),
platform_value!({ "type": "contract", "contractRequirements": { "minimumSecondsSinceUpdate": 61 } }),
"/properties/toUserId/refersTo/contractRequirements/minimumSecondsSinceUpdate",
),
] {
let old_document_type =
identifier_document_type(Some(old_fields), platform_version);
Expand Down
2 changes: 2 additions & 0 deletions packages/rs-dpp/src/data_contract/document_type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ pub(crate) mod property_names {
pub const PROPERTY_AGREEMENT: &str = "propertyAgreement";
pub const CONTRACT_REQUIREMENTS: &str = "contractRequirements";
pub const MODERATION: &str = "moderation";
pub const MINIMUM_AGE_SECONDS: &str = "minimumAgeSeconds";
pub const MINIMUM_SECONDS_SINCE_UPDATE: &str = "minimumSecondsSinceUpdate";
pub const DOCUMENTS_COUNTABLE: &str = "documentsCountable";
pub const RANGE_COUNTABLE: &str = "rangeCountable";
/// Doctype-level flag naming the property whose values are summed into
Expand Down
Loading
Loading