mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 02:51:01 +00:00
Node-side subsystems for Disputes (#2566)
* dispute subsystem files * rename * fix linkcheck * flesh out section README * coordinator schema * DisputeCoordinatorMessage * stub & coordinator protocol * dispute coordinator * add some more message fields * move links to bottom * dispute participation * Cleen It Up ! * runtime: store candidate receipts in dispute state yeah, this is a little heavier. why are you reading this? * Revert "runtime: store candidate receipts in dispute state" This reverts commit 51c10bfd4d866e287e6bd88f317ed57ed987eaee. * add dispute availability statement type and prepare for availability * add 'spam slots' to disputes runtmie * return Spam Slots info from runtime * rework `ImportStatement` to `ImportStatements` * some more methods for dispute coordinator * candidates-included runtime API * algo for providing disputes to runtime. * handle signing with coordinator * dispute coordinator chain ops * remove dead file * remove keystore from dispute participation * adjust ApprovedAncestor to return the necssary data * discuss how approved ancestor and determine undisputed chain are used together * add TODO * initiate disputes from approval voting * route statements from candidate backing and approval voting * fix guide build
This commit is contained in:
committed by
GitHub
parent
caebf642dd
commit
ccfabaa0c6
@@ -30,7 +30,8 @@
|
||||
- [Validation Code](runtime-api/validation-code.md)
|
||||
- [Candidate Pending Availability](runtime-api/candidate-pending-availability.md)
|
||||
- [Candidate Events](runtime-api/candidate-events.md)
|
||||
- [Known Disputes](runtime-api/known-disputes.md)
|
||||
- [Disputes Info](runtime-api/disputes-info.md)
|
||||
- [Candidates Included](runtime-api/candidates-included.md)
|
||||
- [Node Architecture](node/README.md)
|
||||
- [Subsystems and Jobs](node/subsystems-and-jobs.md)
|
||||
- [Overseer](node/overseer.md)
|
||||
@@ -51,7 +52,10 @@
|
||||
- [Approval Subsystems](node/approval/README.md)
|
||||
- [Approval Voting](node/approval/approval-voting.md)
|
||||
- [Approval Distribution](node/approval/approval-distribution.md)
|
||||
- [Dispute Participation](node/approval/dispute-participation.md)
|
||||
- [Disputes Subsystems](node/disputes/README.md)
|
||||
- [Dispute Coordinator](node/disputes/dispute-coordinator.md)
|
||||
- [Dispute Participation](node/disputes/dispute-participation.md)
|
||||
- [Dispute Distribution](node/disputes/dispute-distribution.md)
|
||||
- [Utility Subsystems](node/utility/README.md)
|
||||
- [Availability Store](node/utility/availability-store.md)
|
||||
- [Candidate Validation](node/utility/candidate-validation.md)
|
||||
|
||||
@@ -4,4 +4,4 @@ The approval subsystems implement the node-side of the [Approval Protocol](../..
|
||||
|
||||
We make a divide between the [assignment/voting logic](approval-voting.md) and the [distribution logic](approval-distribution.md) that distributes assignment certifications and approval votes. The logic in the assignment and voting also informs the GRANDPA voting rule on how to vote.
|
||||
|
||||
This category of subsystems also contains a module for [participating in live disputes](dispute-participation.md) and tracks all observed votes (backing or approval) by all validators on all candidates.
|
||||
These subsystems are intended to flag issues and begin [participating in live disputes](../disputes/dispute-participation.md). Dispute subsystems also track all observed votes (backing, approval, and dispute-specific) by all validators on all candidates.
|
||||
|
||||
@@ -134,7 +134,7 @@ struct State {
|
||||
earliest_session: SessionIndex,
|
||||
session_info: Vec<SessionInfo>,
|
||||
babe_epoch: Option<BabeEpoch>, // information about a cached BABE epoch.
|
||||
keystore: KeyStorePtr,
|
||||
keystore: KeyStore,
|
||||
|
||||
// A scheduler which keeps at most one wakeup per hash, candidate hash pair and
|
||||
// maps such pairs to `Tick`s.
|
||||
@@ -215,16 +215,18 @@ On receiving a `CheckAndImportApproval(indirect_approval_vote, response_channel)
|
||||
* Fetch the `CandidateEntry` from the indirect approval vote's `candidate_index`. If the block did not trigger inclusion of enough candidates, return `ApprovalCheckResult::Bad`.
|
||||
* Construct a `SignedApprovalVote` using the candidate hash and check against the validator's approval key, based on the session info of the block. If invalid or no such validator, return `ApprovalCheckResult::Bad`.
|
||||
* Send `ApprovalCheckResult::Accepted`
|
||||
* Dispatch a [`DisputeCoordinatorMessage::ImportStatement`](../../types/overseer-protocol.md#dispute-coordinator-message) with the approval statement.
|
||||
* [Import the checked approval vote](#import-checked-approval)
|
||||
|
||||
#### `ApprovalVotingMessage::ApprovedAncestor`
|
||||
|
||||
On receiving an `ApprovedAncestor(Hash, BlockNumber, response_channel)`:
|
||||
* Iterate over the ancestry of the hash all the way back to block number given, starting from the provided block hash.
|
||||
* Keep track of an `all_approved_max: Option<(Hash, BlockNumber)>`.
|
||||
* Iterate over the ancestry of the hash all the way back to block number given, starting from the provided block hash. Load the `CandidateHash`es from each block entry.
|
||||
* Keep track of an `all_approved_max: Option<(Hash, BlockNumber, Vec<(Hash, Vec<CandidateHash>))>`.
|
||||
* For each block hash encountered, load the `BlockEntry` associated. If any are not found, return `None` on the response channel and conclude.
|
||||
* If the block entry's `approval_bitfield` has all bits set to 1 and `all_approved_max == None`, set `all_approved_max = Some((current_hash, current_number))`.
|
||||
* If the block entry's `approval_bitfield` has any 0 bits, set `all_approved_max = None`.
|
||||
* If `all_approved_max` is `Some`, push the current block hash and candidate hashes onto the list of blocks and candidates `all_approved_max`.
|
||||
* After iterating all ancestry, return `all_approved_max`.
|
||||
|
||||
### Updates and Auxiliary Logic
|
||||
@@ -278,7 +280,9 @@ On receiving an `ApprovedAncestor(Hash, BlockNumber, response_channel)`:
|
||||
* Wait for the available data
|
||||
* Issue a `CandidateValidationMessage::ValidateFromExhaustive` message
|
||||
* Wait for the result of validation
|
||||
* Check that the result of validation, if valid, matches the commitments in the receipt.
|
||||
* If valid, issue a message on `background_tx` detailing the request.
|
||||
* If any of the data, the candidate, or the commitments are invalid, issue on `background_tx` a [`DisputeCoordinatorMessage::IssueLocalStatement`](../../types/overseer-protocol.md#dispute-coordinator-message) with `valid = false` to initiate a dispute.
|
||||
|
||||
#### Issue Approval Vote
|
||||
* Fetch the block entry and candidate entry. Ignore if `None` - we've probably just lost a race with finality.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Dispute Participation
|
||||
|
||||
## Protocol
|
||||
|
||||
## Functionality
|
||||
@@ -94,6 +94,8 @@ match msg {
|
||||
Add `Seconded` statements and `Valid` statements to a quorum. If quorum reaches validator-group majority, send a [`ProvisionerMessage`][PM]`::ProvisionableData(ProvisionableData::BackedCandidate(CandidateReceipt))` message.
|
||||
`Invalid` statements that conflict with already witnessed `Seconded` and `Valid` statements for the given candidate, statements that are double-votes, self-contradictions and so on, should result in issuing a [`ProvisionerMessage`][PM]`::MisbehaviorReport` message for each newly detected case of this kind.
|
||||
|
||||
On each incoming statement, [`DisputeCoordinatorMessage::ImportStatement`][DCM] should be issued.
|
||||
|
||||
### Validating Candidates.
|
||||
|
||||
```rust
|
||||
@@ -137,6 +139,7 @@ Dispatch a [`StatementDistributionMessage`][PDM]`::Share(relay_parent, SignedFul
|
||||
[CBM]: ../../types/overseer-protocol.md#candidate-backing-message
|
||||
[ADM]: ../../types/overseer-protocol.md#availability-distribution-message
|
||||
[SDM]: ../../types/overseer-protocol.md#statement-distribution-message
|
||||
[DCM]: ../../types/overseer-protocol.md#dispute-coordinator-message
|
||||
|
||||
[CS]: candidate-selection.md
|
||||
[CV]: ../utility/candidate-validation.md
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Disputes Subsystems
|
||||
|
||||
This section is for the node-side subsystems that lead to participation in disputes. There are five major roles that validator nodes must participate in when it comes to disputes
|
||||
* Detection. Detect bad parablocks, either during candidate backing or approval checking, and initiate a dispute.
|
||||
* Participation. Participate in active disputes. When a node becomes aware of a dispute, it should recover the data for the disputed block, check the validity of the parablock, and issue a statement regarding the validity of the parablock.
|
||||
* Distribution. Validators should notify each other of active disputes and relevant statements over the network.
|
||||
* Submission. When authoring a block, a validator should inspect the state of the parent block and provide any information about disputes that the chain needs as part of the ParaInherent. This should initialize new disputes on-chain as necessary.
|
||||
* Fork-choice and Finality. When observing a block issuing a DisputeRollback digest in the header, that branch of the relay chain should be abandoned all the way back to the indicated block. When voting on chains in GRANDPA, no chains that contain blocks that are or have been disputed should be voted on.
|
||||
|
||||
## Components
|
||||
|
||||
### Dispute Coordinator
|
||||
|
||||
This component is responsible for coordinating other subsystems around disputes.
|
||||
|
||||
This component should track all statements received from all validators over some window of sessions. This includes backing statements, approval votes, and statements made specifically for disputes. This will be used to identify disagreements or instances where validators conflict with themselves.
|
||||
|
||||
This is responsible for tracking and initiating disputes. Disputes will be initiated either externally by another subsystem which has identified an issue with a parablock or internally upon observing two statements which conflict with each other on the validity of a parablock.
|
||||
|
||||
No more than one statement by each validator on each side of the dispute needs to be stored. That is, validators are allowed to participate on both sides of the dispute, although we won't write code to do so. Such behavior has negative EV in the runtime.
|
||||
|
||||
This will notify the dispute participation subsystem of a new dispute if the local validator has not issued any statements on the disputed candidate already.
|
||||
|
||||
Locally authored statements related to disputes will be forwarded to the dispute distribution subsystem.
|
||||
|
||||
This subsystem also provides two further behaviors for the interactions between disputes and fork-choice
|
||||
- Enhancing the finality voting rule. Given description of a chain and candidates included at different heights in that chain, it returns the BlockHash corresponding to the highest BlockNumber that there are no disputes before. I expect that we will slightly change ApprovalVoting::ApprovedAncestor to return this set and then the target block to vote on will be further constrained by this function.
|
||||
- Chain roll-backs. Whenever importing new blocks, the header should be scanned for a roll-back digest. If there is one, the chain should be rolled back according to the digest. I expect this would be implemented with a ChainApi function and possibly an ApprovalVoting function to clean up the approval voting DB.
|
||||
|
||||
### Dispute Participation
|
||||
|
||||
This subsystem ties together the dispute tracker, availability recovery, candidate validation, and dispute distribution subsystems. When notified of a new dispute by the Dispute Tracker, the data should be recovered, the validation code loaded from the relay chain, and the candidate is executed.
|
||||
|
||||
A statement depending on the outcome of the execution is produced, signed, and imported to the dispute tracker.
|
||||
|
||||
### Dispute Distribution
|
||||
|
||||
This is a networking component by which validators notify each other of live disputes and statements on those disputes.
|
||||
|
||||
Validators will in the future distribute votes to each other via the network, but at the moment learn of disputes just from watching the chain.
|
||||
@@ -0,0 +1,131 @@
|
||||
# Dispute Coordinator
|
||||
|
||||
This is the central subsystem of the node-side components which participate in disputes. This subsystem wraps a database which tracks all statements observed by all validators over some window of sessions. Votes older than this session window are pruned.
|
||||
|
||||
This subsystem will be the point which produce dispute votes, eiuther positive or negative, based on locally-observed validation results as well as a sink for votes received by other subsystems. When importing a dispute vote from another node, this will trigger the [dispute participation](dispute-participation.md) subsystem to recover and validate the block and call back to this subsystem.
|
||||
|
||||
## Database Schema
|
||||
|
||||
We use an underlying Key-Value database where we assume we have the following operations available:
|
||||
* `write(key, value)`
|
||||
* `read(key) -> Option<value>`
|
||||
* `iter_with_prefix(prefix) -> Iterator<(key, value)>` - gives all keys and values in lexicographical order where the key starts with `prefix`.
|
||||
|
||||
We use this database to encode the following schema:
|
||||
|
||||
```rust
|
||||
(SessionIndex, "candidate-votes", CandidateHash) -> Option<CandidateVotes>
|
||||
"active-disputes" -> ActiveDisputes
|
||||
"earliest-session" -> Option<SessionIndex>
|
||||
```
|
||||
|
||||
The meta information that we track per-candidate is defined as the `CandidateVotes` struct.
|
||||
This draws on the [dispute statement types][DisputeTypes]
|
||||
|
||||
```rust
|
||||
struct CandidateVotes {
|
||||
// The receipt of the candidate itself.
|
||||
candidate_receipt: CandidateReceipt,
|
||||
// Sorted by validator index.
|
||||
valid: Vec<(ValidDisputeStatementKind, ValidatorIndex, ValidatorSignature)>,
|
||||
// Sorted by validator index.
|
||||
invalid: Vec<(InvalidDisputeStatementKind, ValidatorIndex, ValidatorSignature)>,
|
||||
}
|
||||
|
||||
struct ActiveDisputes {
|
||||
// sorted by session index and then by candidate hash.
|
||||
disputed: Vec<(SessionIndex, CandidateHash)>,
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol
|
||||
|
||||
Input: [`DisputeCoordinatorMessage`][DisputeCoordinatorMessage]
|
||||
|
||||
Output:
|
||||
- [`RuntimeApiMessage`][RuntimeApiMessage]
|
||||
- [`DisputeParticipationMessage`][DisputeParticipationMessage]
|
||||
|
||||
## Functionality
|
||||
|
||||
This assumes a constant `DISPUTE_WINDOW: SessionIndex`. This should correspond to at least 1 day.
|
||||
|
||||
Ephemeral in-memory state:
|
||||
|
||||
```rust
|
||||
struct State {
|
||||
keystore: KeyStore,
|
||||
// An in-memory overlay used as a write-cache.
|
||||
overlay: Map<(SessionIndex, CandidateReceipt), CandidateVotes>,
|
||||
highest_session: SessionIndex,
|
||||
}
|
||||
```
|
||||
|
||||
### On `OverseerSignal::ActiveLeavesUpdate`
|
||||
|
||||
For each leaf in the leaves update:
|
||||
* Fetch the session index for the child of the block with a [`RuntimeApiMessage::SessionIndexForChild`][RuntimeApiMessage].
|
||||
* If the session index is higher than `state.highest_session`:
|
||||
* update `state.highest_session`
|
||||
* remove everything with session index less than `state.highest_session - DISPUTE_WINDOW` from the overlay and from the `"active-disputes"` in the DB.
|
||||
* Use `iter_with_prefix` to remove everything from `"earliest-session"` up to `state.highest_session - DISPUTE_WINDOW` from the DB under `"candidate-votes"`.
|
||||
* Update `"earliest-session"` to be equal to `state.highest_session - DISPUTE_WINDOW`.
|
||||
* For each new block, explicitly or implicitly, under the new leaf, scan for a dispute digest which indicates a rollback. If a rollback is detected, use the ChainApi subsystem to blacklist the chain.
|
||||
|
||||
### On `OverseerSignal::Conclude`
|
||||
|
||||
Flush the overlay to DB and conclude.
|
||||
|
||||
### On `OverseerSignal::BlockFinalized`
|
||||
|
||||
Do nothing.
|
||||
|
||||
### On `DisputeCoordinatorMessage::ImportStatement`
|
||||
|
||||
* Deconstruct into parts `{ candidate_hash, candidate_receipt, session, statements }`.
|
||||
* If the session is earlier than `state.highest_session - DISPUTE_WINDOW`, return.
|
||||
* If there is an entry in the `state.overlay`, load that. Otherwise, load from underlying DB by querying `(session, "candidate-votes", candidate_hash). If that does not exist, create fresh with the given candidate receipt.
|
||||
* If candidate votes is empty and the statements only contain dispute-specific votes, return.
|
||||
* Otherwise, if there is already an entry from the validator in the respective `valid` or `invalid` field of the `CandidateVotes`, return.
|
||||
* Add an entry to the respective `valid` or `invalid` list of the `CandidateVotes` for each statement in `statements`.
|
||||
* Write the `CandidateVotes` to the `state.overlay`.
|
||||
* If the both `valid` and `invalid` lists now have non-zero length where previously one or both had zero length, the candidate is now freshly disputed.
|
||||
* If freshly disputed, load `"active-disputes"` and add the candidate hash and session index. Also issue a [`DisputeParticipationMessage::Participate`][DisputeParticipationMessage].
|
||||
* If the dispute now has supermajority votes in the "valid" direction, according to the `SessionInfo` of the dispute candidate's session, remove from `"active-disputes"`.
|
||||
* If the dispute now has supermajority votes in the "invalid" direction, there is no need to do anything explicitly. The actual rollback will be handled during the active leaves update by observing digests from the runtime.
|
||||
* Write `"active-disputes"`
|
||||
|
||||
### On `DisputeCoordinatorMessage::ActiveDisputes`
|
||||
|
||||
* Load `"active-disputes"` and return the data contained within.
|
||||
|
||||
### On `DisputeCoordinatorMessage::QueryCandidateVotes`
|
||||
|
||||
* Load from the `state.overlay`, and return the data if `Some`.
|
||||
* Otherwise, load `"candidate-votes"` and return the data within or `None` if missing.
|
||||
|
||||
### On `DisputeCoordinatorMessage::IssueLocalStatement`
|
||||
|
||||
* Deconstruct into parts `{ session_index, candidate_hash, candidate_receipt, is_valid }`.
|
||||
* Construct a [`DisputeStatement`][DisputeStatement] based on `Valid` or `Invalid`, depending on the parameterization of this routine.
|
||||
* Sign the statement with each key in the `SessionInfo`'s list of parachain validation keys which is present in the keystore, except those whose indices appear in `voted_indices`. This will typically just be one key, but this does provide some future-proofing for situations where the same node may run on behalf multiple validators. At the time of writing, this is not a use-case we support as other subsystems do not invariably provide this guarantee.
|
||||
|
||||
### On `DisputeCoordinatorMessage::DetermineUndisputedChain`
|
||||
|
||||
* Load `"active-disputes"`.
|
||||
* Deconstruct into parts `{ base_number, block_descriptions, rx }`
|
||||
* Starting from the beginning of `block_descriptions`:
|
||||
1. Check the `ActiveDisputes` for a dispute of each candidate in the block description.
|
||||
1. If there is a dispute, exit the loop.
|
||||
* For the highest index `i` reached in the `block_descriptions`, send `(base_number + i + 1, block_hash)` on the channel, unless `i` is 0, in which case `None` should be sent. The `block_hash` is determined by inspecting `block_descriptions[i]`.
|
||||
|
||||
### Periodically
|
||||
|
||||
* Flush the `state.overlay` to the DB, writing all entries within
|
||||
* Clear `state.overlay`.
|
||||
|
||||
[DisputeTypes]: ../../types/disputes.md
|
||||
[DisputeStatement]: ../../types/disputes.md#disputestatement
|
||||
[DisputeCoordinatorMessage]: ../../types/overseer-protocol.md#dispute-coordinator-message
|
||||
[RuntimeApiMessage]: ../../types/overseer-protocol.md#runtime-api-message
|
||||
[DisputeParticipationMessage]: ../../types/overseer-protocol.md#dispute-participation-message
|
||||
@@ -0,0 +1,3 @@
|
||||
# Dispute Distribution
|
||||
|
||||
TODO https://github.com/paritytech/polkadot/issues/2581
|
||||
@@ -0,0 +1,70 @@
|
||||
# Dispute Participation
|
||||
|
||||
This subsystem is responsible for actually participating in disputes: when notified of a dispute, we need to recover the candidate data, validate the candidate, and cast our vote in the dispute.
|
||||
|
||||
Fortunately, most of that work is handled by other subsystems; this subsystem is just a small glue component for tying other subsystems together and issuing statements based on their validity.
|
||||
|
||||
## Protocol
|
||||
|
||||
Input: [DisputeParticipationMessage][DisputeParticipationMessage]
|
||||
|
||||
Output:
|
||||
- [RuntimeApiMessage][RuntimeApiMessage]
|
||||
- [CandidateValidationMessage][CandidateValidationMessage]
|
||||
- [AvailabilityRecoveryMessage][AvailabilityRecoveryMessage]
|
||||
- [ChainApiMessage][ChainApiMessage]
|
||||
|
||||
## Functionality
|
||||
|
||||
In-memory state:
|
||||
|
||||
```rust
|
||||
struct State {
|
||||
recent_block_hash: Hash
|
||||
}
|
||||
```
|
||||
|
||||
### On `OverseerSignal::ActiveLeavesUpdate`
|
||||
|
||||
Do nothing.
|
||||
|
||||
### On `OverseerSignal::BlockFinalized`
|
||||
|
||||
Do nothing.
|
||||
|
||||
### On `OverseerSignal::Conclude`
|
||||
|
||||
Conclude.
|
||||
|
||||
### On `DisputeParticipationMessage::Participate`
|
||||
|
||||
> TODO: this validation code fetching procedure is not helpful for disputed blocks that are in chains we do not know. After https://github.com/paritytech/polkadot/issues/2457 we should use the `ValidationCodeByHash` runtime API using the code hash in the candidate receipt.
|
||||
|
||||
* Decompose into parts: `{ candidate_hash, candidate_receipt, session, voted_indices }`
|
||||
* Issue an [`AvailabilityRecoveryMessage::RecoverAvailableData`][AvailabilityRecoveryMessage]
|
||||
* If the result is `Unavailable`, return.
|
||||
* If the result is `Invalid`, [cast invalid votes](#cast-votes) and return.
|
||||
* Fetch the block number of `candidate_receipt.descriptor.relay_parent` using a [`ChainApiMessage::BlockNumber`][ChainApiMessage].
|
||||
* If the data is recovered, dispatch a [`RuntimeApiMessage::HistoricalValidationCode`][RuntimeApiMessage] with the parameters `(candidate_receipt.descriptor.para_id, relay_parent_number)`.
|
||||
* Dispatch a [`AvailabilityStoreMessage::StoreAvailableData`][AvailabilityStoreMessage] with the data.
|
||||
* If the code is not fetched from the chain, return. This should be impossible with correct relay chain configuration after the TODO above is addressed and is unlikely before then, at least if chain synchronization is working correctly.
|
||||
* Dispatch a [`CandidateValidationMessage::ValidateFromExhaustive`][CandidateValidationMessage] with the available data and the validation code.
|
||||
* If the validation result is `Invalid`, [cast invalid votes](#cast-votes) and return.
|
||||
* If the validation fails, [cast invalid votes](#cast-votes) and return.
|
||||
* If the validation succeeds, compute the `CandidateCommitments` based on the validation result and compare against the candidate receipt's `commitments_hash`. If they match, [cast valid votes](#cast-votes) and if not, [cast invalid votes](#cast-votes).
|
||||
|
||||
### Cast Votes
|
||||
|
||||
This requires the parameters `{ candidate_receipt, candidate_hash, session, voted_indices }` as well as a choice of either `Valid` or `Invalid`.
|
||||
|
||||
Invoke [`DisputeCoordinatorMessage::IssueLocalStatement`][DisputeCoordinatorMessage] with `is_valid` according to the parameterization.
|
||||
|
||||
Invoke [`DisputeCoordinatorMessage::ImportStatements`][DisputeCoordinatorMessage] with each signed statement.
|
||||
|
||||
[RuntimeApiMessage]: ../../types/overseer-protocol.md#runtime-api-message
|
||||
[DisputeParticipationMessage]: ../../types/overseer-protocol.md#dispute-participation-message
|
||||
[DisputeCoordinatorMessage]: ../../types/overseer-protocol.md#dispute-coordinator-message
|
||||
[CandidateValidationMessage]: ../../types/overseer-protocol.md#candidate-validation-message
|
||||
[AvailabilityRecoveryMessage]: ../../types/overseer-protocol.md#availability-recovery-message
|
||||
[ChainApiMessage]: ../../types/overseer-protocol.md#chain-api-message
|
||||
[AvailabilityStoreMessage]: ../../types/overseer-protocol.md#availability-store-message
|
||||
@@ -6,6 +6,6 @@ One broad goal of finality, which applies across many different blockchains, is
|
||||
|
||||
GRANDPA's regular voting rule is for each validator to select the longest chain they are aware of. GRANDPA proceeds in rounds, collecting information from all online validators and determines the blocks that a supermajority of validators all have in common with each other.
|
||||
|
||||
For parachains, we extend the security guarantee of finality to be such that no invalid parachain candidate may be included in a finalized block. Candidates may be included in some fork of the relay chain with only a few backing votes behind them. After that point, we run the [Approvals Protocol](../protocol-approval.md), which is implemented as the [Approval Voting](approval/approval-voting.md) subsystem. This system involves validators self-selecting to re-check candidates included in all observed forks of the relay chain as well as an algorithm for observing validators' statements about assignment and approval in order to determine which candidates, and thus blocks, are with high probability valid. The highest approved ancestor of a given block can be determined by querying the Approval Voting subsystem via the [`ApprovalVotingMessage::ApprovedAncestor`](../types/overseer-protocol.md#approval-voting) message.
|
||||
For parachains, we extend the security guarantee of finality to be such that no invalid parachain candidate may be included in a finalized block. Candidates may be included in some fork of the relay chain with only a few backing votes behind them. After that point, we run the [Approvals Protocol](../protocol-approval.md), which is implemented as the [Approval Voting](approval/approval-voting.md) subsystem. This system involves validators self-selecting to re-check candidates included in all observed forks of the relay chain as well as an algorithm for observing validators' statements about assignment and approval in order to determine which candidates, and thus blocks, are with high probability valid. The highest approved ancestor of a given block can be determined by querying the Approval Voting subsystem via the [`ApprovalVotingMessage::ApprovedAncestor`](../types/overseer-protocol.md#approval-voting) message. If the response of `ApprovedAncestor` is `Some`, we further constrain the voting rule to avoid unfinalized blocks. The list of block hashes and candidates should be reversed, and passed to the [`DisputeCoordinatorMessage::DetermineUndisputedChain`](../types/overseer-protocol.md#dispute-coordinator-message) for a final result.
|
||||
|
||||
Lastly, we refuse to finalize any block including a candidate for which we are aware of an ongoing dispute or of a dispute resolving against the candidate. The exact means of doing this has not been determined yet.
|
||||
|
||||
@@ -71,6 +71,30 @@ To determine availability:
|
||||
|
||||
The end result of this process is a vector of `BackedCandidate`s, sorted in order of their core index. Furthermore, this process should select at maximum one candidate which upgrades the runtime validation code.
|
||||
|
||||
### Dispute Statement Selection
|
||||
|
||||
This is the point at which the block author provides further votes to active disputes or initiates new disputes in the runtime state.
|
||||
|
||||
We must take care not to overwhelm the "spam slots" of the chain. That is, to avoid too many votes from the same validators being placed into the chain, which would trigger the anti-spam protection functionality of the [disputes module](../../runtime/disputes.md).
|
||||
|
||||
To select disputes:
|
||||
|
||||
- Make a `DisputesInfo` runtime API call and decompose into `{ spam_slots, disputes }`. Bind `disputes` to `onchain_disputes`.
|
||||
- Issue a `DisputeCoordinatorMessage::ActiveDisputes` message and wait for the response. Assign the value to `offchain_disputes`.
|
||||
- Make a `CandidatesIncluded` runtime API call for each dispute in `offchain_disputes` and tag each offchain dispute as local if the result for it is `true`.
|
||||
- Initialize `NewSpamSlots: Map<(SessionIndex, ValidatorIndex), u32>` as an empty map.
|
||||
- For each dispute in `offchain_disputes`:
|
||||
1. Make a `RuntimeApiRequest::SessionInfo` against the parent hash for the session of the dispute. If `None`, continue - this chain is in the past relative to the session the dispute belongs to and we can import it when it reaches that session.
|
||||
1. Load the spam slots from `spam_slots` for the given session. If it isn't present, treat as though all zeros.
|
||||
1. construct a `DisputeStatementSet` of all offchain votes we are aware of that the onchain doesn't already have a `valid` or `invalid` bit set for, respectively.
|
||||
1. If the `onchain_disputes` contains an entry for the dispute, load that. Otherwise, treat as empty.
|
||||
1. If the offchain dispute is local or the `DisputeStatementSet` and the onchain dispute together have at least `byzantine_threshold + 1` validators in it, continue to the next offchain dispute.
|
||||
1. Otherwise
|
||||
1. Filter out all votes from the `DisputeStatementSet` where the amount of spam slots occupied on-chain by the validator, plus the `NewSpamSlots` value, plus 1, is greater than `spam_slots.max_spam_slots`.
|
||||
1. After filtering, if either the `valid` or `invalid` lists in the combination of the `DisputeStatementSet` and the onchain dispute is empty, skip this dispute.
|
||||
1. Add 1 to the `NewSpamSlots` value for each validator in the `DisputeStatementSet`.
|
||||
- Construct a `MultiDisputeStatementSet` for each `DisputeStatement` and return that.
|
||||
|
||||
### Determining Bitfield Availability
|
||||
|
||||
An occupied core has a `CoreAvailability` bitfield. We also have a list of `SignedAvailabilityBitfield`s. We need to determine from these whether or not a core at a particular index has become available.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Candidates Included
|
||||
|
||||
This runtime API is for checking which candidates have been included within the chain, locally.
|
||||
|
||||
```rust
|
||||
/// Input and output have the same length.
|
||||
fn candidates_included(Vec<(SessionIndex, CandidateHash)>) -> Vec<bool>;
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
# Disputes Info
|
||||
|
||||
Get information about all disputes known by the chain as well as information about which validators the disputes subsystem will accept disputes from. These disputes may be either live or concluded. The [`DisputeState`](../types/disputes.md#disputestate) can be used to determine whether the dispute still accepts votes, as well as which validators' votes may be included.
|
||||
|
||||
```rust
|
||||
struct Dispute {
|
||||
session: SessionIndex,
|
||||
candidate: CandidateHash,
|
||||
dispute_state: DisputeState,
|
||||
local: bool,
|
||||
}
|
||||
|
||||
struct SpamSlotsInfo {
|
||||
max_spam_slots: u32,
|
||||
session_spam_slots: Vec<(SessionIndex, Vec<u32>)>,
|
||||
}
|
||||
|
||||
struct DisputesInfo {
|
||||
disputes: Vec<Dispute>,
|
||||
spam_slots: SpamSlotsInfo,
|
||||
}
|
||||
|
||||
fn disputes_info() -> DisputesInfo;
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
# Known Disputes
|
||||
|
||||
Get all disputes known by the chain. These disputes may be either live or concluded. The [`DisputeState`](../types/disputes.md#disputestate) can be used to determine whether the dispute still accepts votes, as well as which validators' votes may be included.
|
||||
|
||||
```rust
|
||||
fn known_disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)>;
|
||||
```
|
||||
@@ -37,38 +37,35 @@ 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 -> Vec<u32>,
|
||||
// Whether the chain is frozen or not. Starts as `false`. When this is `true`,
|
||||
// the chain will not accept any new parachain blocks for backing or inclusion.
|
||||
// It can only be set back to `false` by governance intervention.
|
||||
Frozen: bool,
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
```rust
|
||||
/// How many sessions before the current that disputes should be accepted for.
|
||||
DisputePeriod: SessionIndex;
|
||||
/// How long after conclusion to accept statements.
|
||||
PostConclusionAcceptancePeriod: BlockNumber;
|
||||
/// How long is takes for a dispute to conclude by time-out, if no supermajority is reached.
|
||||
ConclusionByTimeOutPeriod: BlockNumber;
|
||||
```
|
||||
> `byzantine_threshold` refers to the maximum number `f` of validators which may be byzantine. The total number of validators is `n = 3f + e` where `e in { 1, 2, 3 }`.
|
||||
|
||||
## Session Change
|
||||
|
||||
1. If the current session is not greater than `dispute_period + 1`, nothing to do here.
|
||||
1. Set `pruning_target = current_session - dispute_period - 1`. We add the extra `1` because we want to keep things for `dispute_period` _full_ sessions. The stuff at the end of the most recent session has been around for ~0 sessions, not ~1.
|
||||
1. If the current session is not greater than `config.dispute_period + 1`, nothing to do here.
|
||||
1. Set `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 ~0 sessions, not ~1.
|
||||
1. If `LastPrunedSession` is `None`, then set `LastPrunedSession` to `Some(pruning_target)` and return.
|
||||
1. Otherwise, clear out all disputes and included candidates in the range `last_pruned..=pruning_target` and set `LastPrunedSession` to `Some(pruning_target)`.
|
||||
1. Otherwise, clear out all disputes, included candidates, and `SpamSlots` entries in the range `last_pruned..=pruning_target` and set `LastPrunedSession` to `Some(pruning_target)`.
|
||||
|
||||
## Block Initialization
|
||||
|
||||
1. Iterate through all disputes. If any have not concluded and started more than `ConclusionByTimeOutPeriod` blocks ago, set them to `Concluded` and mildly punish all validators associated, as they have failed to distribute available data.
|
||||
1. Iterate through all disputes. If any have not concluded and started more than `config.dispute_conclusion_by_timeout_period` blocks ago, set them to `Concluded` and mildly punish all validators associated, as they have failed to distribute available data. If the `Included` map does not contain the candidate and there are fewer than `byzantine_threshold + 1` participating validators, reduce `SpamSlots` for all participating validators.
|
||||
|
||||
## Routines
|
||||
|
||||
* `provide_multi_dispute_data(MultiDisputeStatementSet) -> Vec<(SessionIndex, Hash)>`:
|
||||
1. Fail if any disputes in the set are duplicate or concluded before the `PostConclusionAcceptancePeriod` window relative to now.
|
||||
1. Fail if any disputes in the set are duplicate or concluded before the `config.dispute_post_conclusion_acceptance_period` window relative to now.
|
||||
1. Pass on each dispute statement set to `provide_dispute_data`, propagating failure.
|
||||
1. Return a list of all candidates who just had disputes initiated.
|
||||
|
||||
@@ -76,8 +73,12 @@ ConclusionByTimeOutPeriod: BlockNumber;
|
||||
1. All statements must be issued under the correct session for the correct candidate.
|
||||
1. `SessionInfo` is used to check statement signatures and this function should fail if any signatures are invalid.
|
||||
1. If there is no dispute under `Disputes`, create a new `DisputeState` with blank bitfields.
|
||||
1. If `concluded_at` is `Some`, and is `concluded_at + PostConclusionAcceptancePeriod < now`, return false.
|
||||
1. Import all statements into the dispute. This should fail if any disputes are duplicate; if the corresponding bit for the corresponding validator is set in the dispute already.
|
||||
1. If `concluded_at` is `Some`, and is `concluded_at + config.post_conclusion_acceptance_period < now`, return false.
|
||||
1. If the overlap of the validators in the `DisputeStatementSet` and those already present in the `DisputeState` is fewer in number than `byzantine_threshold + 1` and the candidate is not present in the `Included` map
|
||||
1. increment `SpamSlots` for each validator in the `DisputeStatementSet` which is not already in the `DisputeState`. Initialize the `SpamSlots` to a zeroed vector first, if necessary.
|
||||
1. If the value for any spam slot exceeds `config.dispute_max_spam_slots`, return false.
|
||||
1. If the overlap of the validators in the `DisputeStatementSet` and those already present in the `DisputeState` is at least `byzantine_threshold + 1`, the `DisputeState` has fewer than `byzantine_threshold + 1` validators, and the candidate is not present in the `Included` map, decrement `SpamSlots` for each validator in the `DisputeState`.
|
||||
1. Import all statements into the dispute. This should fail if any statements are duplicate; if the corresponding bit for the corresponding validator is set in the dispute already.
|
||||
1. If `concluded_at` is `None`, reward all statements slightly less.
|
||||
1. If `concluded_at` is `Some`, reward all statements slightly less.
|
||||
1. If either side now has supermajority, 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_at` to `Some(now)`.
|
||||
@@ -89,6 +90,7 @@ ConclusionByTimeOutPeriod: BlockNumber;
|
||||
|
||||
* `note_included(SessionIndex, CandidateHash, included_in: BlockNumber)`:
|
||||
1. Add `(SessionIndex, CandidateHash)` to the `Included` map with `included_in - 1` as the value.
|
||||
1. If there is a dispute under `(Sessionindex, CandidateHash)` with fewer than `byzantine_threshold + 1` participating validators, decrement `SpamSlots` for each validator in the `DisputeState`.
|
||||
1. If there is a dispute under `(SessionIndex, CandidateHash)` that has concluded against the candidate, invoke `revert_and_freeze` with the stored block number.
|
||||
|
||||
* `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.
|
||||
|
||||
@@ -20,6 +20,7 @@ struct DisputeStatementSet {
|
||||
enum DisputeStatement {
|
||||
/// A valid statement, of the given kind
|
||||
Valid(ValidDisputeStatementKind),
|
||||
/// An invalid statement, of the given kind.
|
||||
Invalid(InvalidDisputeStatementKind),
|
||||
}
|
||||
|
||||
|
||||
@@ -75,13 +75,25 @@ enum ApprovalVotingMessage {
|
||||
ResponseChannel<ApprovalCheckResult>,
|
||||
),
|
||||
/// Returns the highest possible ancestor hash of the provided block hash which is
|
||||
/// acceptable to vote on finality for.
|
||||
/// acceptable to vote on finality for. Along with that, return the lists of candidate hashes
|
||||
/// which appear in every block from the (non-inclusive) base number up to (inclusive) the specified
|
||||
/// approved ancestor.
|
||||
/// This list starts from the highest block (the approved ancestor itself) and moves backwards
|
||||
/// towards the base number.
|
||||
///
|
||||
/// The base number is typically the number of the last finalized block, but in GRANDPA it is
|
||||
/// possible for the base to be slightly higher than the last finalized block.
|
||||
///
|
||||
/// The `BlockNumber` provided is the number of the block's ancestor which is the
|
||||
/// earliest possible vote.
|
||||
///
|
||||
/// It can also return the same block hash, if that is acceptable to vote upon.
|
||||
/// Return `None` if the input hash is unrecognized.
|
||||
ApprovedAncestor(Hash, BlockNumber, ResponseChannel<Option<(Hash, BlockNumber)>>),
|
||||
ApprovedAncestor {
|
||||
target_hash: Hash,
|
||||
base_number: BlockNumber,
|
||||
rx: ResponseChannel<Option<(Hash, BlockNumber, Vec<(Hash, Vec<CandidateHash>)>)>>
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -324,6 +336,82 @@ enum CollatorProtocolMessage {
|
||||
}
|
||||
```
|
||||
|
||||
## Dispute Coordinator Message
|
||||
|
||||
Messages received by the [Dispute Coordinator subsystem](../node/disputes/dispute-coordinator.md)
|
||||
|
||||
This subsystem coordinates participation in disputes, tracks live disputes, and observed statements of validators from subsystems.
|
||||
|
||||
```rust
|
||||
enum DisputeCoordinatorMessage {
|
||||
/// Import a statement by a validator about a candidate.
|
||||
///
|
||||
/// The subsystem will silently discard ancient statements or sets of only dispute-specific statements for
|
||||
/// candidates that are previously unknown to the subsystem. The former is simply because ancient
|
||||
/// data is not relevant and the latter is as a DoS prevention mechanism. Both backing and approval
|
||||
/// statements already undergo anti-DoS procedures in their respective subsystems, but statements
|
||||
/// cast specifically for disputes are not necessarily relevant to any candidate the system is
|
||||
/// already aware of and thus present a DoS vector. Our expectation is that nodes will notify each
|
||||
/// other of disputes over the network by providing (at least) 2 conflicting statements, of which one is either
|
||||
/// a backing or validation statement.
|
||||
///
|
||||
/// This does not do any checking of the message signature.
|
||||
ImportStatements {
|
||||
/// The hash of the candidate.
|
||||
candidate_hash: CandidateHash,
|
||||
/// The candidate receipt itself.
|
||||
candidate_receipt: CandidateReceipt,
|
||||
/// The session the candidate appears in.
|
||||
session: SessionIndex,
|
||||
/// Triples containing the following:
|
||||
/// - A statement, either indicating validity or invalidity of the candidate.
|
||||
/// - The validator index (within the session of the candidate) of the validator casting the vote.
|
||||
/// - The signature of the validator casting the vote.
|
||||
statements: Vec<(DisputeStatement, ValidatorIndex, ValidatorSignature)>,
|
||||
},
|
||||
/// Fetch a list of all active disputes that the co-ordinator is aware of.
|
||||
ActiveDisputes(ResponseChannel<Vec<(SessionIndex, CandidateHash)>>),
|
||||
/// Get candidate votes for a candidate.
|
||||
QueryCandidateVotes(SessionIndex, CandidateHash, ResponseChannel<Option<CandidateVotes>>),
|
||||
/// Sign and issue local dispute votes. A value of `true` indicates validity, and `false` invalidity.
|
||||
IssueLocalStatement(SessionIndex, CandidateHash, CandidateReceipt, bool),
|
||||
/// Determine the highest undisputed block within the given chain, based on where candidates
|
||||
/// were included. If even the base block should not be finalized due to a dispute,
|
||||
/// then `None` should be returned on the channel.
|
||||
///
|
||||
/// The block descriptions begin counting upwards from the block after the given `base_number`. The `base_number`
|
||||
/// is typically the number of the last finalized block but may be slightly higher. This block
|
||||
/// is inevitably going to be finalized so it is not accounted for by this function.
|
||||
DetermineUndisputedChain {
|
||||
base_number: BlockNumber,
|
||||
block_descriptions: Vec<(BlockHash, SessionIndex, Vec<CandidateHash>)>,
|
||||
rx: ResponseSender<Option<(BlockNumber, BlockHash)>>,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dispute Participation Message
|
||||
|
||||
Messages received by the [Dispute Participation subsystem](../node/disputes/dispute-participation.md)
|
||||
|
||||
This subsystem simply executes requests to evaluate a candidate.
|
||||
|
||||
```rust
|
||||
enum DisputeParticipationMessage {
|
||||
/// Validate a candidate for the purposes of participating in a dispute.
|
||||
Participate {
|
||||
/// The hash of the candidate
|
||||
candidate_hash: CandidateHash,
|
||||
/// The candidate receipt itself.
|
||||
candidate_receipt: CandidateReceipt,
|
||||
/// The session the candidate appears in.
|
||||
session: SessionIndex,
|
||||
/// The indices of validators who have already voted on this candidate.
|
||||
voted_indices: Vec<ValidatorIndex>,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Network Bridge Message
|
||||
|
||||
Messages received by the network bridge. This subsystem is invoked by others to manipulate access
|
||||
|
||||
@@ -40,6 +40,12 @@ struct HostConfiguration {
|
||||
pub max_validators: Option<u32>,
|
||||
/// The amount of sessions to keep for disputes.
|
||||
pub dispute_period: SessionIndex,
|
||||
/// How long after dispute conclusion to accept statements.
|
||||
pub dispute_post_conclusion_acceptance_period: BlockNumber,
|
||||
/// The maximum number of dispute spam slots
|
||||
pub dispute_max_spam_slots: u32,
|
||||
/// How long it takes for a dispute to conclude by time-out, if no supermajority is reached.
|
||||
pub dispute_conclusion_by_time_out_period: BlockNumber,
|
||||
/// The amount of consensus slots that must pass between submitting an assignment and
|
||||
/// submitting an approval vote before a validator is considered a no-show.
|
||||
/// Must be at least 1.
|
||||
|
||||
Reference in New Issue
Block a user