diff --git a/polkadot/roadmap/implementers-guide/src/SUMMARY.md b/polkadot/roadmap/implementers-guide/src/SUMMARY.md
index 3e6a824d9098ff0b9b61b9eace0b6c39941619d6..7a1e0299926ab478affdcf32f9c4ba36f64b380b 100644
--- a/polkadot/roadmap/implementers-guide/src/SUMMARY.md
+++ b/polkadot/roadmap/implementers-guide/src/SUMMARY.md
@@ -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)
diff --git a/polkadot/roadmap/implementers-guide/src/node/approval/README.md b/polkadot/roadmap/implementers-guide/src/node/approval/README.md
index 2d0815376728a76185a18d7818b72acb12ae6191..ac636853084eea8c93d6a86565d4ad6f37681cdc 100644
--- a/polkadot/roadmap/implementers-guide/src/node/approval/README.md
+++ b/polkadot/roadmap/implementers-guide/src/node/approval/README.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.
\ No newline at end of file
+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.
diff --git a/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md b/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md
index dd6994caea790db61170eee14a71bebb2b22abd5..9bd01e036a36f7c934899f92c370a6f704830a88 100644
--- a/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md
+++ b/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md
@@ -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.
diff --git a/polkadot/roadmap/implementers-guide/src/node/approval/dispute-participation.md b/polkadot/roadmap/implementers-guide/src/node/approval/dispute-participation.md
deleted file mode 100644
index 10c278c20df114f451a536f0f357cf787b619b22..0000000000000000000000000000000000000000
--- a/polkadot/roadmap/implementers-guide/src/node/approval/dispute-participation.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Dispute Participation
-
-## Protocol
-
-## Functionality
\ No newline at end of file
diff --git a/polkadot/roadmap/implementers-guide/src/node/backing/candidate-backing.md b/polkadot/roadmap/implementers-guide/src/node/backing/candidate-backing.md
index c44f5e1f09b1a84c4b95644457fb1c5fa51d24c5..8cd51b8bedaa6a484c2600056d5db10143841f0d 100644
--- a/polkadot/roadmap/implementers-guide/src/node/backing/candidate-backing.md
+++ b/polkadot/roadmap/implementers-guide/src/node/backing/candidate-backing.md
@@ -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
diff --git a/polkadot/roadmap/implementers-guide/src/node/disputes/README.md b/polkadot/roadmap/implementers-guide/src/node/disputes/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..db4a05ccfcb722349f773c209815bdfe0696b7e4
--- /dev/null
+++ b/polkadot/roadmap/implementers-guide/src/node/disputes/README.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.
diff --git a/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-coordinator.md b/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-coordinator.md
new file mode 100644
index 0000000000000000000000000000000000000000..f566bea49411361468b28a0cbae29aea612f9f27
--- /dev/null
+++ b/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-coordinator.md
@@ -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
diff --git a/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-distribution.md b/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-distribution.md
new file mode 100644
index 0000000000000000000000000000000000000000..b186fe5895e3fcca502d088d16ee24f221c10a59
--- /dev/null
+++ b/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-distribution.md
@@ -0,0 +1,3 @@
+# Dispute Distribution
+
+TODO https://github.com/paritytech/polkadot/issues/2581
diff --git a/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-participation.md b/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-participation.md
new file mode 100644
index 0000000000000000000000000000000000000000..545f39abdee1af7cf7bc13483e8589bb044fcf66
--- /dev/null
+++ b/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-participation.md
@@ -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
diff --git a/polkadot/roadmap/implementers-guide/src/node/grandpa-voting-rule.md b/polkadot/roadmap/implementers-guide/src/node/grandpa-voting-rule.md
index 57da4e0dad6957aa05c284267478410d1c74381f..d3b54475838000f18151ad6436b62a55e43702f2 100644
--- a/polkadot/roadmap/implementers-guide/src/node/grandpa-voting-rule.md
+++ b/polkadot/roadmap/implementers-guide/src/node/grandpa-voting-rule.md
@@ -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.
diff --git a/polkadot/roadmap/implementers-guide/src/node/utility/provisioner.md b/polkadot/roadmap/implementers-guide/src/node/utility/provisioner.md
index bbaf1071a1d3d5f0ee0e32fd2958e63c99666678..0e8aa059635b04fad6cdfd6c2fe7a650ec46a0fc 100644
--- a/polkadot/roadmap/implementers-guide/src/node/utility/provisioner.md
+++ b/polkadot/roadmap/implementers-guide/src/node/utility/provisioner.md
@@ -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.
diff --git a/polkadot/roadmap/implementers-guide/src/runtime-api/candidates-included.md b/polkadot/roadmap/implementers-guide/src/runtime-api/candidates-included.md
new file mode 100644
index 0000000000000000000000000000000000000000..7692c422833d49f477584b5fe127d422138932da
--- /dev/null
+++ b/polkadot/roadmap/implementers-guide/src/runtime-api/candidates-included.md
@@ -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>;
+```
diff --git a/polkadot/roadmap/implementers-guide/src/runtime-api/disputes-info.md b/polkadot/roadmap/implementers-guide/src/runtime-api/disputes-info.md
new file mode 100644
index 0000000000000000000000000000000000000000..3548d5fb5793a2e664f4ec3d681ec5e609471919
--- /dev/null
+++ b/polkadot/roadmap/implementers-guide/src/runtime-api/disputes-info.md
@@ -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;
+```
diff --git a/polkadot/roadmap/implementers-guide/src/runtime-api/known-disputes.md b/polkadot/roadmap/implementers-guide/src/runtime-api/known-disputes.md
deleted file mode 100644
index 1e3b3cc6805f2d4859fc97305e522d180dba434e..0000000000000000000000000000000000000000
--- a/polkadot/roadmap/implementers-guide/src/runtime-api/known-disputes.md
+++ /dev/null
@@ -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)>;
-```
diff --git a/polkadot/roadmap/implementers-guide/src/runtime/disputes.md b/polkadot/roadmap/implementers-guide/src/runtime/disputes.md
index b4d5cc1dc64381d0fca6de87b664b56a591e9846..4faece7cb092347b2f7411d648d2651d9b31f8d8 100644
--- a/polkadot/roadmap/implementers-guide/src/runtime/disputes.md
+++ b/polkadot/roadmap/implementers-guide/src/runtime/disputes.md
@@ -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.
diff --git a/polkadot/roadmap/implementers-guide/src/types/disputes.md b/polkadot/roadmap/implementers-guide/src/types/disputes.md
index f6ed74e18977fd65290ded092f4ff719f11eaba5..becace642dfe3138ff8c0a5f1acfc32fdafb5620 100644
--- a/polkadot/roadmap/implementers-guide/src/types/disputes.md
+++ b/polkadot/roadmap/implementers-guide/src/types/disputes.md
@@ -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),
 }
 
diff --git a/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md b/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md
index 2eb94c42fdce6b82fb2b45ce1fd338ea3cc2d2fd..2ac0b1e124a82ecb6b299cc3cffdf760c78c47e5 100644
--- a/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md
+++ b/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md
@@ -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
diff --git a/polkadot/roadmap/implementers-guide/src/types/runtime.md b/polkadot/roadmap/implementers-guide/src/types/runtime.md
index fadc34def620eb12a063cfb6f95640be1708cfb3..324f8e6f74ae2c44565716be90af183fd7079635 100644
--- a/polkadot/roadmap/implementers-guide/src/types/runtime.md
+++ b/polkadot/roadmap/implementers-guide/src/types/runtime.md
@@ -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.