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
27 changes: 27 additions & 0 deletions book/src/data-model/contested-documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ document's owner may not be joined by the same identity twice.

The index's `contested.resolution` says how the contest is decided.

From protocol version 14, an identifier property among the index values is written as an
identifier in the poll (`Index::extract_contested_values`), whether the document gave it as an
identifier, as 32 bytes or as an array of byte values. The index keys store all of these alike,
but a poll is hashed from its values, and that hash keys the contest's prefunded balance and end
date: two contenders writing the same identifier two ways would otherwise split one contest into
two polls. Before 14 the values are taken as given.

## Resolution 0: masternode vote

The DPNS rule. The choices are a contender, abstain, or **lock**, which gives the value to nobody.
Expand All @@ -33,6 +40,26 @@ poll duration, which opens the vote window. `getVotePollsByEndDate` shows whiche

The moderation charters contract uses this resolution to elect moderation teams.

## Moderation elections

An `electedCharter` contest of the moderation charters contract (protocol version 14), keyed by
the target contract id, is a **moderation election** and does not take the generic parameters:

- Its join window and vote window are the `joinWindow` and `voteWindow` of the target contract's
elected moderation declaration (one day to four weeks each, one week by default), on every
network. A single applicant wins when the join window closes; a second applicant moves the end
to the join window plus the vote window. A late applicant is refused with
`DocumentContestNotJoinableError` naming the target's join window.
- Each application prefunds the votes with the moderation fund, 0.5 Dash
(`moderation_vote_resolution_fund_required_amount`), instead of the contested document fund.

The target's declaration is read, and billed, when an application opens the contest and when a
later one joins it; it is frozen at the target's creation, so both reads agree. Nothing at the
end of a contest reads the target: the end date was written when the contest opened or was
joined. A target that is missing or declares something else leaves a contest on the generic
windows instead of failing, and the application's own reference validation refuses it. Every
other contest, DPNS included, keeps the generic windows and fund.

## Ties

From protocol version 14, a tie among the top contenders goes to the **earliest** contender:
Expand Down
18 changes: 18 additions & 0 deletions book/src/fees/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,24 @@ to prevent namespace squatting:

Before protocol version 9, all registration fees were zero.

### Contest Funds

A document create that opens or joins a contest (a contested unique index)
prefunds the masternode votes: the amount leaves the contender's balance for the
contest's prefunded balance, each vote takes a fixed cost from it, and what is
left when the contest ends is released as processing fees. The amounts are
`VoteResolutionFundFees` in the fee version:

| Component | Protocol versions 1 to 13 | Protocol version 14 |
|---|---|---|
| Contested document fund (DPNS and every other contest) | 0.2 Dash | 0.1 Dash |
| Moderation election fund (an `electedCharter` application) | none exist | 0.5 Dash |
| One vote | 0.0001 Dash | 0.00002 Dash |

`required_vote_resolution_fund` in `rs-dpp` picks between the two funds; the
schedules before 14 carry the contested document amount in the moderation
field, so the choice changes nothing there.

## User Fee Increase

Every state transition carries a `user_fee_increase` field (a `UserFeeIncrease`
Expand Down
11 changes: 9 additions & 2 deletions docs/protocol/moderation-charters.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,15 @@ masternodes (weight 1) and evonodes (weight 4) vote for a contender or abstain,
with no Lock choice, so the contest always ends with a winner, a tie goes to
the earliest contender, and a contest with a single contender at the end of the
join window is awarded at once. An elected charter create opens or joins that
contest for its target contract. Reading the join window, the vote window and
the fund from the target contract comes in a later pull request.
contest for its target contract.

The contest runs on the target contract's own windows, on every network: the
join window is the target's `joinWindow`, and a second applicant moves the end
to `joinWindow` plus `voteWindow`. An application prefunds the masternode
votes with 0.5 Dash (`moderation_vote_resolution_fund_required_amount`), not
the 0.1 Dash of other contests; what the votes leave is released as processing
fees when the contest is cleaned up. The target's declaration is read when an
application opens or joins the contest, never when it ends.

## Seating

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
mod v0;

use crate::data_contract::document_type::property::DocumentProperty;
use crate::data_contract::document_type::Index;
use crate::ProtocolError;
use indexmap::IndexMap;
use platform_value::Value;
use platform_version::version::PlatformVersion;
use std::collections::BTreeMap;

impl Index {
/// The values a contest on this index names its resource by: those of
/// [`Self::extract_values`], with every identifier property written as
/// `Value::Identifier` from protocol version 14. Validation also accepts an identifier as
/// bytes or as an array of 32 byte values, and the index keys store all of them alike,
/// but a contest's poll is hashed from these values: two contenders writing the same
/// identifier in two forms would otherwise name one contest with two polls, each with
/// its own prefunded balance and end date. `document_properties` are the document type's
/// flattened properties. Before 14 the values are taken as given.
///
/// # Parameters
/// * `data`: the document's properties.
/// * `document_properties`: the document type's flattened properties.
/// * `platform_version`: the platform version.
///
/// # Returns
/// The index values of the contest, one per index property, in index order.
pub fn extract_contested_values(
&self,
data: &BTreeMap<String, Value>,
document_properties: &IndexMap<String, DocumentProperty>,
platform_version: &PlatformVersion,
) -> Result<Vec<Value>, ProtocolError> {
match platform_version
.dpp
.contract_versions
.document_type_versions
.methods
.canonical_contested_index_values
{
None => Ok(self.extract_values(data)),
Some(0) => Ok(self.extract_contested_values_v0(data, document_properties)),
Some(version) => Err(ProtocolError::UnknownVersionMismatch {
method: "Index::extract_contested_values".to_string(),
known_versions: vec![0],
received: version,
}),
}
}
}

/// The identifier a contest's index value names, in every form validation accepts for an
/// identifier property: `Value::Identifier`, 32 bytes, or an array of 32 byte values. `None`
/// for any other value, a base58 string included.
pub fn contested_index_identifier(value: &Value) -> Option<[u8; 32]> {
match value {
Value::Identifier(bytes) | Value::Bytes32(bytes) => Some(*bytes),
Value::Bytes(bytes) => <[u8; 32]>::try_from(bytes.as_slice()).ok(),
Value::Array(items) if items.len() == 32 => items
.iter()
.map(|item| item.to_integer::<u8>().ok())
.collect::<Option<Vec<u8>>>()
.and_then(|bytes| <[u8; 32]>::try_from(bytes).ok()),
_ => None,
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::data_contract::document_type::index::tests::make_index;
use crate::data_contract::document_type::property::{
ByteArrayPropertySizes, DocumentPropertyType,
};

/// From protocol version 14 an identifier index value in any accepted form is written as
/// `Value::Identifier`, so every contender of one contest names it with the same poll; a
/// byte array property keeps its bytes. Before 14 the values are taken as given.
#[test]
fn should_write_identifier_contest_values_as_identifiers_from_version_14() {
let index = make_index(
"byTargetContract",
vec![("targetContractId", true), ("salt", true)],
true,
);
let property = |property_type| DocumentProperty {
property_type,
required: true,
transient: false,
required_since: None,
distinct_from: None,
encrypted_for: None,
};
let document_properties = IndexMap::from([
(
"targetContractId".to_string(),
property(DocumentPropertyType::Identifier),
),
(
"salt".to_string(),
property(DocumentPropertyType::ByteArray(ByteArrayPropertySizes {
min_size: Some(32),
max_size: Some(32),
})),
),
]);
let latest = PlatformVersion::latest();
let before_14 = PlatformVersion::get(13).expect("protocol version 13");

for target in [
Value::Identifier([0x7A; 32]),
Value::Bytes32([0x7A; 32]),
Value::Bytes(vec![0x7A; 32]),
Value::Array(vec![Value::U8(0x7A); 32]),
] {
let data = BTreeMap::from([
("targetContractId".to_string(), target.clone()),
("salt".to_string(), Value::Bytes(vec![0x01; 32])),
]);
assert_eq!(
index
.extract_contested_values(&data, &document_properties, latest)
.expect("values"),
vec![Value::Identifier([0x7A; 32]), Value::Bytes(vec![0x01; 32])],
"{target:?}"
);
assert_eq!(
index
.extract_contested_values(&data, &document_properties, before_14)
.expect("values"),
vec![target, Value::Bytes(vec![0x01; 32])]
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use super::contested_index_identifier;
use crate::data_contract::document_type::property::{DocumentProperty, DocumentPropertyType};
use crate::data_contract::document_type::Index;
use indexmap::IndexMap;
use platform_value::Value;
use std::collections::BTreeMap;

impl Index {
/// The index values of `data` with every identifier property written as
/// `Value::Identifier`.
#[inline(always)]
pub(super) fn extract_contested_values_v0(
&self,
data: &BTreeMap<String, Value>,
document_properties: &IndexMap<String, DocumentProperty>,
) -> Vec<Value> {
self.properties
.iter()
.zip(self.extract_values(data))
.map(|(index_property, value)| {
let is_identifier = matches!(
document_properties
.get(&index_property.name)
.map(|property| &property.property_type),
Some(
DocumentPropertyType::Identifier
| DocumentPropertyType::IdentifierWithReference(_)
)
);
if is_identifier {
canonical_identifier_value(value)
} else {
value
}
})
.collect()
}
}

/// An identifier value in any accepted form as `Value::Identifier`; any other value as it is.
fn canonical_identifier_value(value: Value) -> Value {
contested_index_identifier(&value).map_or(value, Value::Identifier)
}
4 changes: 3 additions & 1 deletion packages/rs-dpp/src/data_contract/document_type/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ use std::cmp::Ordering;
use std::sync::OnceLock;
use std::{collections::BTreeMap, convert::TryFrom};

mod extract_contested_values;
pub mod preallocation;
pub mod random_index;
pub mod time_range;

pub use extract_contested_values::contested_index_identifier;
pub use preallocation::{PreallocatedKeySource, PreallocationBinding};
pub use time_range::TimeRangeTransform;

Expand Down Expand Up @@ -2637,7 +2639,7 @@ mod tests {
}
}

fn make_index(name: &str, properties: Vec<(&str, bool)>, unique: bool) -> Index {
pub(super) fn make_index(name: &str, properties: Vec<(&str, bool)>, unique: bool) -> Index {
Index {
name: name.to_string(),
properties: properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ pub trait DocumentTypeV0Methods: DocumentTypeV0Getters + DocumentTypeV0MethodsVe
.methods
.contested_vote_poll_for_document
{
0 => Ok(self.contested_vote_poll_for_document_v0(document)),
0 => self.contested_vote_poll_for_document_v0(document, platform_version),
version => Err(ProtocolError::UnknownVersionMismatch {
method: "contested_vote_poll_for_document".to_string(),
known_versions: vec![0],
Expand All @@ -632,7 +632,10 @@ pub trait DocumentTypeV0Methods: DocumentTypeV0Getters + DocumentTypeV0MethodsVe
.methods
.contested_vote_poll_for_document
{
0 => Ok(self.contested_vote_poll_for_document_properties_v0(document_properties)),
0 => self.contested_vote_poll_for_document_properties_v0(
document_properties,
platform_version,
),
version => Err(ProtocolError::UnknownVersionMismatch {
method: "contested_vote_poll_for_document_properties".to_string(),
known_versions: vec![0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use crate::fee::Credits;
use crate::identity::TimestampMillis;
use crate::prelude::{BlockHeight, CoreBlockHeight};
use crate::validation::SimpleConsensusValidationResult;
use crate::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll;
use crate::voting::vote_polls::contested_document_resource_vote_poll::{
required_vote_resolution_fund, ContestedDocumentResourceVotePoll,
};
use crate::voting::vote_polls::VotePoll;
use crate::ProtocolError;
use chrono::Utc;
Expand Down Expand Up @@ -361,14 +363,19 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa
}

/// Figures out the prefunded voting balance (v0) for a document in a document type
fn contested_vote_poll_for_document_v0(&self, document: &Document) -> Option<VotePoll> {
self.contested_vote_poll_for_document_properties_v0(document.properties())
fn contested_vote_poll_for_document_v0(
&self,
document: &Document,
platform_version: &PlatformVersion,
) -> Result<Option<VotePoll>, ProtocolError> {
self.contested_vote_poll_for_document_properties_v0(document.properties(), platform_version)
}

fn contested_vote_poll_for_document_properties_v0(
&self,
document_properties: &BTreeMap<String, Value>,
) -> Option<VotePoll> {
platform_version: &PlatformVersion,
) -> Result<Option<VotePoll>, ProtocolError> {
self.indexes()
.values()
.find(|index| {
Expand All @@ -392,14 +399,24 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa
}
})
.map(|index| {
let index_values = index.extract_values(document_properties);
VotePoll::ContestedDocumentResourceVotePoll(ContestedDocumentResourceVotePoll {
contract_id: self.data_contract_id(),
document_type_name: self.name().clone(),
index_name: index.name.clone(),
index_values,
})
// Identifier values are written one way from protocol version 14, so every
// contender of a contest names it with the same poll; before 14 they are taken
// as given, as they always were
let index_values = index.extract_contested_values(
document_properties,
self.flattened_properties(),
platform_version,
)?;
Ok(VotePoll::ContestedDocumentResourceVotePoll(
ContestedDocumentResourceVotePoll {
contract_id: self.data_contract_id(),
document_type_name: self.name().clone(),
index_name: index.name.clone(),
index_values,
},
))
})
.transpose()
}

fn index_for_types_v0(
Expand Down Expand Up @@ -674,12 +691,16 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa
}
})
.map(|index| {
// A moderation election is prefunded with the moderation fund. Every schedule
// before protocol version 14 carries the contested document fund there, so
// the amount is unchanged wherever this ran before
(
index.name.clone(),
platform_version
.fee_version
.vote_resolution_fund_fees
.contested_document_vote_resolution_fund_required_amount,
required_vote_resolution_fund(
&self.data_contract_id(),
self.name(),
platform_version,
),
)
})
}
Expand Down
Loading
Loading