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:
Robert Habermeier
2021-04-01 03:00:46 +02:00
committed by GitHub
parent caebf642dd
commit ccfabaa0c6
18 changed files with 434 additions and 38 deletions
@@ -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