roadmap/implementers-guide/src/runtime/disputes.md
After a backed candidate is made available, it is included and proceeds into an acceptance period during which validators are randomly selected to do (secondary) approval checks of the parablock. Any reports disputing the validity of the candidate will cause escalation, where even more validators are requested to check the block, and so on, until either the parablock is determined to be invalid or valid. Those on the wrong side of the dispute are slashed and, if the parablock is deemed invalid, the relay chain is rolled back to a point before that block was included.
However, this isn't the end of the story. We are working in a forkful blockchain environment, which carries three important considerations:
This means that in all likelihood, there is the possibility of disputes that are started on one fork of the relay chain, and as soon as the dispute resolution process starts to indicate that the parablock is indeed invalid, that fork of the relay chain will be abandoned and the dispute will never be fully resolved on that chain.
Even if this doesn't happen, there is the possibility that there are two disputes underway, and one resolves leading to a reversion of the chain before the other has concluded. In this case we want to both transplant the concluded dispute onto other forks of the chain as well as the unconcluded dispute.
We account for these requirements by having the disputes module handle two kinds of disputes.
When a local dispute concludes negatively, the chain needs to be abandoned and reverted back to a block where the state does not contain the bad parablock. We expect that due to the Approval Checking Protocol, the current executing block should not be finalized. So we do two things when a local dispute concludes negatively:
If, as is expected, the chain is unfinalized, the freeze will have no effect as no honest validator will attempt to build on the frozen chain. However, if the approval checking protocol has failed and the bad parablock is finalized, the freeze serves to put the chain into a governance-only mode.
The storage of this module is designed around tracking DisputeStates, updating them with votes, and tracking blocks included by this branch of the relay chain. It also contains a Frozen parameter designed to freeze the state of all parachains.
Storage Layout:
LastPrunedSession: Option<SessionIndex>,
// All ongoing or concluded disputes for the last several sessions.
Disputes: double_map (SessionIndex, CandidateHash) -> Option<DisputeState>,
// All included blocks on the chain, as well as the block number in this chain that
// should be reverted back to if the candidate is disputed and determined to be invalid.
Included: double_map (SessionIndex, CandidateHash) -> Option<BlockNumber>,
// Whether the chain is frozen or not. Starts as `None`. When this is `Some`,
// the chain will not accept any new parachain blocks for backing or inclusion,
// and its value indicates the last valid block number in the chain.
// It can only be set back to `None` by governance intervention.
Frozen: Option<BlockNumber>,
byzantine_thresholdrefers to the maximum numberfof validators which may be byzantine. The total number of validators isn = 3f + ewheree in { 1, 2, 3 }.
config.dispute_period + 1, nothing to do here.pruning_target = current_session - config.dispute_period - 1. We add the extra 1 because we want to keep things for config.dispute_period full sessions.
The stuff at the end of the most recent session has been around for a little over 0 sessions, not a little over 1.LastPrunedSession is None, then set LastPrunedSession to Some(pruning_target) and return.last_pruned..=pruning_target and set LastPrunedSession to Some(pruning_target).This is currently a no op.
filter_multi_dispute_data(MultiDisputeStatementSet) -> MultiDisputeStatementSet:
MultiDisputeStatementSet and filters it down to a MultiDisputeStatementSet
that satisfies all the criteria of provide_multi_dispute_data. That is, eliminating
ancient votes, duplicates and unconfirmed disputes.
This can be used by block authors to create the final submission in a block which is
guaranteed to pass the provide_multi_dispute_data checks.provide_multi_dispute_data(MultiDisputeStatementSet) -> Vec<(SessionIndex, Hash)>:
provide_dispute_data, propagating failure.provide_dispute_data(DisputeStatementSet) -> bool: Provide data to an ongoing dispute or initiate a dispute.
SessionInfo is used to check statement signatures and this function should fail if any signatures are invalid.Disputes, create a new DisputeState with blank bitfields.concluded_at is Some, and is concluded_at + config.post_conclusion_acceptance_period < now, return false.concluded_at is None, reward all statements.concluded_at is Some, reward all statements slightly less.concluded_at to Some(now) if it was None.Included map contains (session, candidate): invoke revert_and_freeze with the stored block number.disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)>: Get a list of all disputes and info about dispute state.
Disputes and collect into a vector.note_included(SessionIndex, CandidateHash, included_in: BlockNumber):
(SessionIndex, CandidateHash) to the Included map with included_in - 1 as the value.(SessionIndex, CandidateHash) that has concluded against the candidate, invoke revert_and_freeze with the stored block number.concluded_invalid(SessionIndex, CandidateHash) -> bool: Returns whether a candidate has already concluded a dispute in the negative.
is_frozen(): Load the value of Frozen from storage. Return true if Some and false if None.
last_valid_block(): Load the value of Frozen from storage and return. None indicates that all blocks in the chain are potentially valid.
revert_and_freeze(BlockNumber):
is_frozen() return.Frozen to Some(BlockNumber) to indicate a rollback to the block number.Revert(BlockNumber + 1) log to indicate a rollback of the block's child in the header chain, which is the same as a rollback to the block number.All disputes delivered to the runtime by the client are filtered before the actual import. In this context actual import means persisted in the runtime storage. The filtering has got two purposes:
Implementation note: Filtering is performed in function filter_dispute_data from Disputes pallet.
The filtering is performed on the whole statement set which is about to be imported onchain. The following filters are applied:
OLDEST_ACCEPTED parameter
it is removed from the set. OLDEST_ACCEPTED is a runtime configuration option.
Implementation note: dispute_post_conclusion_acceptance_period from
HostConfiguration is used in the current Polkadot/Kusama implementation.After the disputes are filtered the validators participating in the disputes are rewarded and more importantly the offenders are slashed. Generally there can be two types of punishments:
A dispute might be inconclusive. This means that it has timed out without being confirmed. A confirmed dispute is one containing votes more than the byzantine threshold (1/3 of the active validators). Validators participating in inconclusive disputes are not slashed. Thanks to the applied filtering (described in the previous section) one can be confident that there are no spam disputes in the runtime. So if a validator is not voting it is due to another reason (e.g. being under DoS attack). There is no reason to punish such validators with a slash.
Implementation note: Slashing is performed in process_checked_dispute_data from Disputes pallet.