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
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
- [Contract Groups](data-model/contract-groups.md)
- [Contract Moderation](data-model/contract-moderation.md)
- [Documents](data-model/documents.md)
- [Contested Documents](data-model/contested-documents.md)
- [Identities](data-model/identities.md)
- [Key Budgets and Expiry](data-model/key-limits.md)

Expand Down
49 changes: 49 additions & 0 deletions book/src/data-model/contested-documents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Contested Documents

A unique index may be declared `contested`. A document whose index values fall in the contested
range is not stored outright: it opens or joins a **contest**, a `ContestedDocumentResourceVotePoll`
that names the contract, the document type, the index and the index values, and masternodes and
evonodes decide who gets the value. A masternode's vote counts once, an evonode's four times. Every
vote is a `MasternodeVote` state transition carrying the poll and a `ResourceVoteChoice`.

The contest is funded by the contenders' prefunded voting balances, and each vote costs a fixed
amount from that balance. Contenders may join for the **join window** (one week on mainnet) after
the first document; the contest runs for the **poll duration** (two weeks on mainnet). The first
document's owner may not be joined by the same identity twice.

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

## Resolution 0: masternode vote

The DPNS rule. The choices are a contender, abstain, or **lock**, which gives the value to nobody.
The contender with the most votes wins unless the lock tally exceeds it, in which case the value is
locked and may be contested again later. The contest always runs the full poll duration, even with
a single contender, so the masternodes may lock the value.

## Resolution 1: masternode vote without locking

`ContestedIndexResolution::MasternodeVoteNoLocking`, meta-schema v3 (protocol version 14). The
choices are a contender or abstain. A Lock vote is refused with `VoteChoiceNotAllowedForVotePollError`
(40307). The contest always ends with a winner: the contender with the most votes, no minimum.

A contest without locking ends when its join window closes while it still has a single contender,
so that contender is awarded the value without a vote window. Its end-date entry is written at the
end of the join window when the contest opens; the first additional contender moves it to the full
poll duration, which opens the vote window. `getVotePollsByEndDate` shows whichever end applies.

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

## Ties

From protocol version 14, a tie among the top contenders goes to the **earliest** contender:
creation time, then block height, then core block height, then document id. This holds for both
resolutions; contests ending before version 14 awarded the latest contender.

## Storage

A contest's state lives under `votes / contested_resource / active_polls`, laid out like the
contested index it decides: the contenders' documents, one votes sum tree per contender, and the
abstain and lock tallies. The masternodes' vote references live under
`votes / contested_resource / identity_votes`, and the end dates under `votes / end_date_queries`.
Once the contest ends, the winning document is awarded, the losers are removed, and the stored
result stays for the `getContestedResourceVoteState` query.
2 changes: 1 addition & 1 deletion book/src/error-handling/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ The fee category currently has a single code. The 30000 range is reserved for fu
| 40000-40009 | Data Contract | `DataContractAlreadyPresentError` (40000), `DataContractIsReadonlyError` (40001), `DataContractNotFoundError` (40008) |
| 40100-40134 | Documents | `DocumentAlreadyPresentError` (40100), `DocumentNotFoundError` (40101), `DuplicateUniqueIndexError` (40105), `DocumentActionFeeAgreementNotSetError` (40132), `DocumentActionFeeAgreementMismatchError` (40133), `DocumentActionFeeMultiplierNotToleratedError` (40134) |
| 40200-40217 | Identity | `IdentityAlreadyExistsError` (40200), `InvalidIdentityRevisionError` (40203), `IdentityInsufficientBalanceError` (40210) |
| 40300-40306 | Voting | `MasternodeNotFoundError` (40300), `MasternodeVoteAlreadyPresentError` (40304) |
| 40300-40307 | Voting | `MasternodeNotFoundError` (40300), `MasternodeVoteAlreadyPresentError` (40304), `VoteChoiceNotAllowedForVotePollError` (40307) |
| 40400-40401 | Prefunded Balances | `PrefundedSpecializedBalanceInsufficientError` (40400) |
| 40500-40502 | Data Triggers | `DataTriggerConditionError` (40500), `DataTriggerExecutionError` (40501) |
| 40600-40603 | Addresses | `AddressDoesNotExistError` (40600), `AddressNotEnoughFundsError` (40601) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -587,9 +587,10 @@
"resolution": {
"type": "integer",
"enum": [
0
0,
1
],
"description": "Resolution. 0 - Masternode Vote"
"description": "Resolution. 0 - Masternode Vote. 1 - Masternode Vote without a Lock choice: 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"
},
"description": {
"type": "string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ pub(super) struct ParserGeneration {
/// `rangeCountable` has always implied `documentsCountable`. Forwarded to
/// [`Index::try_from_value_map`] exactly like the admissions above.
pub admit_range_countable_implies_countable: bool,
/// Whether a contested index may declare `"resolution": 1`, the masternode
/// vote without a Lock choice. Forwarded to [`Index::try_from_value_map`]
/// exactly like the admissions above.
pub admit_index_no_locking_resolution: bool,
}

/// Reject a document type whose name is not a non-empty ASCII
Expand Down Expand Up @@ -869,6 +873,7 @@ fn parse_indices(
range_countable_implies_countable: ctx
.generation
.admit_range_countable_implies_countable,
no_locking_resolution: ctx.generation.admit_index_no_locking_resolution,
},
)
.map_err(consensus_or_protocol_data_contract_error)?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ impl DocumentTypeV1 {
// the parser demands an explicit countable `countable`, as the frozen
// v1 and v2 meta-schemas do.
admit_range_countable_implies_countable: false,
// NO LOCKING RESOLUTION: a generation-3 value; not in this grammar.
admit_index_no_locking_resolution: false,
},
platform_version,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ fn try_from_schema_generation_3(
3,
)
.range_countable_implies_countable,
// NO LOCKING RESOLUTION: a contested index resolved without a Lock
// choice, a generation-3 value from the same shared mapping.
admit_index_no_locking_resolution: IndexGrammarAdmissions::for_schema_generation(3)
.no_locking_resolution,
},
platform_version,
)?;
Expand Down
Loading
Loading