Unverified Commit 8348cc4c authored by asynchronous rob's avatar asynchronous rob Committed by GitHub
Browse files

Implement the Statement Distribution Subsystem (#1326)

* set up data types and control flow for statement distribution

* add some set-like methods to View

* implement sending to peers

* start fixing equivocation handling

* Add a section to the statement distribution subsystem on equivocations and flood protection

* fix typo and amend wording

* implement flood protection

* have peer knowledge tracker follow when peer first learns about a candidate

* send dependents after circulating

* add another TODO

* trigger send in one more place

* refactors from review

* send new statements to candidate backing

* instantiate active head data with runtime API values

* track our view changes and peer view changes

* apply a benefit to peers who send us statements we want

* remove unneeded TODO

* add some comments and improve Hash implementation

* start tests and fix `note_statement`

* test active_head seconding logic

* test that the per-peer tracking logic works

* test per-peer knowledge tracker

* test that peer view updates lead to messages being sent

* test statement circulation

* address review comments

* have view set methods return references
parent 0499212f
Pipeline #99573 passed with stages
in 21 minutes and 55 seconds
......@@ -4831,6 +4831,28 @@ dependencies = [
"westend-runtime",
]
[[package]]
name = "polkadot-statement-distribution"
version = "0.1.0"
dependencies = [
"arrayvec 0.5.1",
"assert_matches",
"futures 0.3.5",
"futures-timer 3.0.2",
"indexmap",
"log 0.4.8",
"parity-scale-codec",
"parking_lot 0.10.2",
"polkadot-node-primitives",
"polkadot-node-subsystem",
"polkadot-primitives",
"polkadot-subsystem-test-helpers",
"sp-keyring",
"sp-runtime",
"sp-staking",
"streamunordered",
]
[[package]]
name = "polkadot-statement-table"
version = "0.8.14"
......
......@@ -45,6 +45,7 @@ members = [
"node/core/proposer",
"node/network/bridge",
"node/network/statement-distribution",
"node/overseer",
"node/primitives",
"node/service",
......
[package]
name = "polkadot-statement-distribution"
version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>"]
description = "Statement Distribution Subsystem"
edition = "2018"
[dependencies]
futures = "0.3.5"
log = "0.4.8"
futures-timer = "3.0.2"
streamunordered = "0.5.1"
polkadot-primitives = { path = "../../../primitives" }
node-primitives = { package = "polkadot-node-primitives", path = "../../primitives" }
parity-scale-codec = "1.3.0"
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-staking = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
polkadot-subsystem = { package = "polkadot-node-subsystem", path = "../../subsystem" }
arrayvec = "0.5.1"
indexmap = "1.4.0"
[dev-dependencies]
parking_lot = "0.10.0"
subsystem-test = { package = "polkadot-subsystem-test-helpers", path = "../../test-helpers/subsystem" }
assert_matches = "1.3.0"
sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "master" }
This diff is collapsed.
......@@ -29,7 +29,7 @@ use polkadot_primitives::{Hash,
};
/// A statement, where the candidate receipt is included in the `Seconded` variant.
#[derive(Debug, Clone, PartialEq, Encode, Decode)]
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub enum Statement {
/// A statement that a validator seconds a candidate.
#[codec(index = "1")]
......@@ -42,16 +42,19 @@ pub enum Statement {
Invalid(Hash),
}
impl Statement {
pub fn to_compact(&self) -> CompactStatement {
match *self {
Statement::Seconded(ref c) => CompactStatement::Candidate(c.hash()),
Statement::Valid(hash) => CompactStatement::Valid(hash),
Statement::Invalid(hash) => CompactStatement::Invalid(hash),
}
}
}
impl EncodeAs<CompactStatement> for Statement {
fn encode_as(&self) -> Vec<u8> {
let statement = match *self {
Statement::Seconded(ref c) => {
polkadot_primitives::parachain::CompactStatement::Candidate(c.hash())
}
Statement::Valid(hash) => polkadot_primitives::parachain::CompactStatement::Valid(hash),
Statement::Invalid(hash) => polkadot_primitives::parachain::CompactStatement::Invalid(hash),
};
statement.encode()
self.to_compact().encode()
}
}
......@@ -87,5 +90,22 @@ pub type ProtocolId = [u8; 4];
/// A succinct representation of a peer's view. This consists of a bounded amount of chain heads.
///
/// Up to `N` (5?) chain heads.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct View(pub Vec<Hash>);
impl View {
/// Returns an iterator of the hashes present in `Self` but not in `other`.
pub fn difference<'a>(&'a self, other: &'a View) -> impl Iterator<Item = &'a Hash> + 'a {
self.0.iter().filter(move |h| !other.contains(h))
}
/// An iterator containing hashes present in both `Self` and in `other`.
pub fn intersection<'a>(&'a self, other: &'a View) -> impl Iterator<Item = &'a Hash> + 'a {
self.0.iter().filter(move |h| other.contains(h))
}
/// Whether the view contains a given hash.
pub fn contains(&self, hash: &Hash) -> bool {
self.0.contains(hash)
}
}
......@@ -24,7 +24,6 @@
use futures::channel::{mpsc, oneshot};
use sc_network::{ObservedRole, ReputationChange, PeerId};
use polkadot_primitives::{BlockNumber, Hash, Signature};
use polkadot_primitives::parachain::{
AbridgedCandidateReceipt, PoVBlock, ErasureChunk, BackedCandidate, Id as ParaId,
......@@ -34,6 +33,8 @@ use polkadot_node_primitives::{
MisbehaviorReport, SignedFullStatement, View, ProtocolId,
};
pub use sc_network::{ObservedRole, ReputationChange, PeerId};
/// A notification of a new backed candidate.
#[derive(Debug)]
pub struct NewBackedCandidate(pub BackedCandidate);
......@@ -234,4 +235,6 @@ pub enum AllMessages {
RuntimeApi(RuntimeApiMessage),
/// Message for the availability store subsystem.
AvailabilityStore(AvailabilityStoreMessage),
/// Message for the network bridge subsystem.
NetworkBridge(NetworkBridgeMessage),
}
......@@ -595,7 +595,7 @@ pub struct Activity(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec<u8
/// Statements that can be made about parachain candidates. These are the
/// actual values that are signed.
#[derive(Clone, PartialEq, Eq, Encode, Decode)]
#[derive(Clone, PartialEq, Eq, Encode, Decode, Hash)]
#[cfg_attr(feature = "std", derive(Debug))]
pub enum CompactStatement {
/// Proposal of a parachain candidate.
......@@ -609,6 +609,18 @@ pub enum CompactStatement {
Invalid(Hash),
}
impl CompactStatement {
/// Get the underlying candidate hash this references.
pub fn candidate_hash(&self) -> &Hash {
match *self {
CompactStatement::Candidate(ref h)
| CompactStatement::Valid(ref h)
| CompactStatement::Invalid(ref h)
=> h
}
}
}
/// A signed compact statement, suitable to be sent to the chain.
pub type SignedStatement = Signed<CompactStatement>;
......
......@@ -24,14 +24,14 @@ Statement Distribution is the only backing subsystem which has any notion of pee
It is responsible for distributing signed statements that we have generated and forwarding them, and for detecting a variety of Validator misbehaviors for reporting to [Misbehavior Arbitration](../utility/misbehavior-arbitration.md). During the Backing stage of the inclusion pipeline, it's the main point of contact with peer nodes. On receiving a signed statement from a peer, assuming the peer receipt state machine is in an appropriate state, it sends the Candidate Receipt to the [Candidate Backing subsystem](candidate-backing.md) to handle the validator's statement.
Track equivocating validators and stop accepting information from them. Forward double-vote proofs to the double-vote reporting system. Establish a data-dependency order:
Track equivocating validators and stop accepting information from them. Establish a data-dependency order:
- In order to receive a `Seconded` message we have the on corresponding chain head in our view
- In order to receive an `Invalid` or `Valid` message we must have received the corresponding `Seconded` message.
And respect this data-dependency order from our peers by respecting their views. This subsystem is responsible for checking message signatures.
The Statement Distribution subsystem sends statements to peer nodes and detects double-voting by validators. When validators conflict with each other or themselves, the Misbehavior Arbitration system is notified.
The Statement Distribution subsystem sends statements to peer nodes.
## Peer Receipt State Machine
......@@ -53,4 +53,21 @@ This system implies a certain level of duplication of messages--we received X's
And respect this data-dependency order from our peers. This subsystem is responsible for checking message signatures.
No jobs, `StartWork` and `StopWork` pulses are used to control neighbor packets and what we are currently accepting.
No jobs. We follow view changes from the [`NetworkBridge`](../utility/network-bridge.md), which in turn is updated by the overseer.
## Equivocations and Flood Protection
An equivocation is a double-vote by a validator. The [Candidate Backing](candidate-backing.md) Subsystem is better-suited than this one to detect equivocations as it adds votes to quorum trackers.
At this level, we are primarily concerned about flood-protection, and to some extent, detecting equivocations is a part of that. In particular, we are interested in detecting equivocations of `Seconded` statements. Since every other statement is dependent on `Seconded` statements, ensuring that we only ever hold a bounded number of `Seconded` statements is sufficient for flood-protection.
The simple approach is to say that we only receive up to two `Seconded` statements per validator per chain head. However, the marginal cost of equivocation, conditional on having already equivocated, is close to 0, since a single double-vote offence is counted as all double-vote offences for a particular chain-head. Even if it were not, there is some amount of equivocations that can be done such that the marginal cost of issuing further equivocations is close to 0, as there would be an amount of equivocations necessary to be completely and totally obliterated by the slashing algorithm. We fear the validator with nothing left to lose.
With that in mind, this simple approach has a caveat worth digging deeper into.
First: We may be aware of two equivocated `Seconded` statements issued by a validator. A totally honest peer of ours can also be aware of one or two different `Seconded` statements issued by the same validator. And yet another peer may be aware of one or two _more_ `Seconded` statements. And so on. This interacts badly with pre-emptive sending logic. Upon sending a `Seconded` statement to a peer, we will want to pre-emptively follow up with all statements relative to that candidate. Waiting for acknowledgement introduces latency at every hop, so that is best avoided. What can happen is that upon receipt of the `Seconded` statement, the peer will discard it as it falls beyond the bound of 2 that it is allowed to store. It cannot store anything in memory about discarded candidates as that would introduce a DoS vector. Then, the peer would receive from us all of the statements pertaining to that candidate, which, from its perspective, would be undesired - they are data-dependent on the `Seconded` statement we sent them, but they have erased all record of that from their memory. Upon receiving a potential flood of undesired statements, this 100% honest peer may choose to disconnect from us. In this way, an adversary may be able to partition the network with careful distribution of equivocated `Seconded` statements.
The fix is to track, per-peer, the hashes of up to 4 candidates per validator (per relay-parent) that the peer is aware of. It is 4 because we may send them 2 and they may send us 2 different ones. We track the data that they are aware of as the union of things we have sent them and things they have sent us. If we receive a 1st or 2nd `Seconded` statement from a peer, we note it in the peer's known candidates even if we do disregard the data locally. And then, upon receipt of any data dependent on that statement, we do not reduce that peer's standing in our eyes, as the data was not undesired.
There is another caveat to the fix: we don't want to allow the peer to flood us because it has set things up in a way that it knows we will drop all of its traffic.
We also track how many statements we have received per peer, per candidate, and per chain-head. This is any statement concerning a particular candidate: `Seconded`, `Valid`, or `Invalid`. If we ever receive a statement from a peer which would push any of these counters beyond twice the amount of validators at the chain-head, we begin to lower the peer's standing and eventually disconnect. This bound is a massive overestimate and could be reduced to twice the number of validators in the corresponding validator group. It is worth noting that the goal at the time of writing is to ensure any finite bound on the amount of stored data, as any equivocation results in a large slash.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment