* disputes module skeleton and storage * implement dispute module initialization logic * implement disputes session change logic * provide dispute skeletons * deduplication & ancient check * fix a couple of warnings * begin provide_dispute_data impl * flesh out statement set import somewhat * move ApprovalVote to shared primitives * add a signing-payload API to explicit dispute statements * implement statement signature checking * some bitflags glue for observing changes in disputes * implement dispute vote import logic * flesh out everything except slashing * guide: tweaks * declare and use punishment trait * punish validators for inconclusive disputes * guide: tiny fix * guide: update docs * add disputes getter fn * guide: small change to spam slots handling * improve spam slots handling and fix some bugs * finish API of disputes runtime * define and deposit `RevertTo` log * begin integrating disputes into para_inherent * use precomputed slash_for/against * return candidate hash from process_bitfields * implement inclusion::collect_disputed * finish integration into rest of runtime * add Disputes to initializer * address suggestions * use pallet macro * fix typo * Update runtime/parachains/src/disputes.rs * add test: fix pruning * document specific behavior * deposit events on dispute changes * add an allow(unused) on fn disputes * add a dummy PunishValidators implementation * add disputes module to Rococo * add disputes module to westend runtime * add disputes module to test runtime * add disputes module to kusama runtime * guide: prepare for runtime API for checking frozenness * remove revert digests in favor of state variable * merge reversions * Update runtime/parachains/src/disputes.rs Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com> * Update runtime/parachains/src/disputes.rs Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com> * Update runtime/parachains/src/disputes.rs Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com> * add byzantine_threshold and supermajority_threshold utilities to primitives * use primitive helpers * deposit revert event when freezing chain * deposit revert log when freezing chain * test revert event and log are generated when freezing * add trait to decouple disputes handling from paras inherent handling * runtime: fix compilation and setup dispute handler * disputes: add hook for filtering out dispute statements * disputes: add initializer hooks to DisputesHandler * runtime: remove disputes pallet from all runtimes * tag TODOs * don't import any dispute statements just yet... * address grumbles * fix spellcheck, hopefully * maybe now? * last spellcheck round * fix runtime tests * fix test-runtime Co-authored-by: thiolliere <gui.thiolliere@gmail.com> Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com> Co-authored-by: André Silva <andrerfosilva@gmail.com>
9.6 KiB
Disputes Module
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:
- For security, validators that misbehave shouldn't only be slashed on one fork, but on all possible forks. Validators that misbehave shouldn't be able to create a new fork of the chain when caught and get away with their misbehavior.
- It is possible (and likely) that the parablock being contested has not appeared on all forks.
- If a block author believes that there is a disputed parablock on a specific fork that will resolve to a reversion of the fork, that block author has more incentive to build on a different fork which does not include that parablock.
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.
- Local disputes: those contesting the validity of the current fork by disputing a parablock included within it.
- Remote disputes: a dispute that has partially or fully resolved on another fork which is transplanted to the local fork for completion and eventual slashing.
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:
- Freeze the state of parachains so nothing further is backed or included.
- Issue a digest in the header of the block that signals to nodes that this branch of the chain is to be abandoned.
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
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>,
// Maps session indices to a vector indicating the number of potentially-spam disputes
// each validator is participating in. Potentially-spam disputes are remote disputes which have
// fewer than `byzantine_threshold + 1` validators.
//
// The i'th entry of the vector corresponds to the i'th validator in the session.
SpamSlots: map SessionIndex -> Option<Vec<u32>>,
// 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 }.
Session Change
- If the current session is not greater than
config.dispute_period + 1, nothing to do here. - Set
pruning_target = current_session - config.dispute_period - 1. We add the extra1because we want to keep things forconfig.dispute_periodfull 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. - If
LastPrunedSessionisNone, then setLastPrunedSessiontoSome(pruning_target)and return. - Otherwise, clear out all disputes, included candidates, and
SpamSlotsentries in the rangelast_pruned..=pruning_targetand setLastPrunedSessiontoSome(pruning_target).
Block Initialization
- Iterate through all disputes. If any have not concluded and started more than
config.dispute_conclusion_by_timeout_periodblocks ago, set them toConcludedand mildly punish all validators associated, as they have failed to distribute available data. If theIncludedmap does not contain the candidate and there are fewer thanbyzantine_threshold + 1participating validators, reduceSpamSlotsfor all participating validators.
Routines
-
provide_multi_dispute_data(MultiDisputeStatementSet) -> Vec<(SessionIndex, Hash)>:- Pass on each dispute statement set to
provide_dispute_data, propagating failure. - Return a list of all candidates who just had disputes initiated.
- Pass on each dispute statement set to
-
provide_dispute_data(DisputeStatementSet) -> bool: Provide data to an ongoing dispute or initiate a dispute.- All statements must be issued under the correct session for the correct candidate.
SessionInfois used to check statement signatures and this function should fail if any signatures are invalid.- If there is no dispute under
Disputes, create a newDisputeStatewith blank bitfields. - If
concluded_atisSome, and isconcluded_at + config.post_conclusion_acceptance_period < now, return false. - If the overlap of the validators in the
DisputeStatementSetand those already present in theDisputeStateis fewer in number thanbyzantine_threshold + 1and the candidate is not present in theIncludedmap- increment
SpamSlotsfor each validator in theDisputeStatementSetwhich is not already in theDisputeState. Initialize theSpamSlotsto a zeroed vector first, if necessary. do not incrementSpamSlotsif the candidate is local. - If the value for any spam slot exceeds
config.dispute_max_spam_slots, return false.
- increment
- If the overlap of the validators in the
DisputeStatementSetand those already present in theDisputeStateis at leastbyzantine_threshold + 1, theDisputeStatehas fewer thanbyzantine_threshold + 1validators, and the candidate is not present in theIncludedmap, then decreaseSpamSlotsby 1 for each validator in theDisputeState. - Import all statements into the dispute. This should fail if any statements are duplicate or if the corresponding bit for the corresponding validator is set in the dispute already.
- If
concluded_atisNone, reward all statements. - If
concluded_atisSome, reward all statements slightly less. - If either side now has supermajority and did not previously, slash the other side. This may be both sides, and we support this possibility in code, but note that this requires validators to participate on both sides which has negative expected value. Set
concluded_attoSome(now)if it wasNone. - If just concluded against the candidate and the
Includedmap contains(session, candidate): invokerevert_and_freezewith the stored block number. - Return true if just initiated, false otherwise.
-
disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)>: Get a list of all disputes and info about dispute state.- Iterate over all disputes in
Disputesand collect into a vector.
- Iterate over all disputes in
-
note_included(SessionIndex, CandidateHash, included_in: BlockNumber):- Add
(SessionIndex, CandidateHash)to theIncludedmap withincluded_in - 1as the value. - If there is a dispute under
(Sessionindex, CandidateHash)with fewer thanbyzantine_threshold + 1participating validators, decreaseSpamSlotsby 1 for each validator in theDisputeState. - If there is a dispute under
(SessionIndex, CandidateHash)that has concluded against the candidate, invokerevert_and_freezewith the stored block number.
- Add
-
could_be_invalid(SessionIndex, CandidateHash) -> bool: Returns whether a candidate has a live dispute ongoing or a dispute which has already concluded in the negative. -
is_frozen(): Load the value ofFrozenfrom storage. Return true ifSomeand false ifNone. -
last_valid_block(): Load the value ofFrozenfrom storage and return. None indicates that all blocks in the chain are potentially valid. -
revert_and_freeze(BlockNumber):- If
is_frozen()return. - Set
FrozentoSome(BlockNumber)to indicate a rollback to the given block number is necessary.
- If