diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 2956ffe63d..9b23da8a94 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -612,6 +612,14 @@ dependencies = [ "polkadot-cli 0.1.0", ] +[[package]] +name = "polkadot-candidate-agreement" +version = "0.1.0" +dependencies = [ + "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "polkadot-primitives 0.1.0", +] + [[package]] name = "polkadot-cli" version = "0.1.0" diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml index 84e987c436..c2c7ef9e36 100644 --- a/substrate/Cargo.toml +++ b/substrate/Cargo.toml @@ -9,6 +9,7 @@ polkadot-cli = { path = "cli", version = "0.1" } [workspace] members = [ + "candidate-agreement", "client", "contracts", "primitives", diff --git a/substrate/candidate-agreement/Cargo.toml b/substrate/candidate-agreement/Cargo.toml new file mode 100644 index 0000000000..842127d116 --- /dev/null +++ b/substrate/candidate-agreement/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "polkadot-candidate-agreement" +version = "0.1.0" +authors = ["Robert Habermeier "] + +[dependencies] +futures = "0.1" +polkadot-primitives = { path = "../primitives" } diff --git a/substrate/candidate-agreement/src/lib.rs b/substrate/candidate-agreement/src/lib.rs new file mode 100644 index 0000000000..0e5c398442 --- /dev/null +++ b/substrate/candidate-agreement/src/lib.rs @@ -0,0 +1,44 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Propagation and agreement of candidates. +//! +//! This is parameterized by 3 numbers: +//! N: the number of validators total +//! P: the number of parachains +//! F: the number of faulty nodes (s.t. 3F + 1 <= N) +//! We also define G as the number of validators per parachain (N/P) +//! +//! Validators are split into groups by parachain, and each validator might come +//! up its own candidate for their parachain. Within groups, validators pass around +//! their candidates and produce statements of validity. +//! +//! Any candidate that receives majority approval by the validators in a group +//! may be subject to inclusion. + +extern crate futures; +extern crate polkadot_primitives as primitives; + +use primitives::parachain; + +mod table; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + } +} diff --git a/substrate/candidate-agreement/src/table.rs b/substrate/candidate-agreement/src/table.rs new file mode 100644 index 0000000000..894cb0cfbd --- /dev/null +++ b/substrate/candidate-agreement/src/table.rs @@ -0,0 +1,234 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! The statement table. +//! +//! This stores messages other validators issue about candidates. +//! +//! These messages are used to create a proposal submitted to a BFT consensus process. +//! +//! Proposals can either be sets of candidates for inclusion or a special value indicating +//! that some unspecified misbehavior has occurred. +//! +//! Each parachain is associated with two sets of validators: those which can +//! propose and attest to validity of candidates, and those who can only attest +//! to availability. + +use std::collections::{HashSet, HashMap}; +use std::hash::Hash; + +/// Statements circulated among peers. +pub enum Statement { + /// Broadcast by a validator to indicate that this is his candidate for + /// inclusion. + /// + /// Broadcasting two different candidate messages per round is not allowed. + Candidate(C::Candidate), + /// Broadcast by a validator to attest that the candidate with given digest + /// is valid. + Valid(C::Digest), + /// Broadcast by a validator to attest that the auxiliary data for a candidate + /// with given digest is available. + Available(C::Digest), + /// Broadcast by a validator to attest that the candidate with given digest + /// is invalid. + Invalid(C::Digest), +} + +/// A signed statement. +pub struct SignedStatement { + /// The statement. + pub statement: Statement, + /// The signature. + pub signature: C::Signature, +} + +/// Context for the statement table. +pub trait Context { + /// A validator ID + type ValidatorId: Hash + Eq + Clone; + /// The digest (hash or other unique attribute) of a candidate. + type Digest: Hash + Eq + Clone; + /// Candidate type. + type Candidate: Ord + Clone; + /// The group ID type + type GroupId: Hash + Eq + Clone; + /// A signature type. + type Signature: Clone; + + /// get the digest of a candidate. + fn candidate_digest(&self, candidate: &Self::Candidate) -> Self::Digest; + + /// get the group of a candidate. + fn candidate_group(&self, candidate: &Self::Candidate) -> Self::GroupId; + + /// Whether a validator is a member of a group. + /// Members are meant to submit candidates and vote on validity. + fn is_member_of(&self, validator: &Self::ValidatorId, group: &Self::GroupId) -> bool; + + /// Whether a validator is an availability guarantor of a group. + /// Guarantors are meant to vote on availability for candidates submitted + /// in a group. + fn is_availability_guarantor_of( + &self, + validator: &Self::ValidatorId, + group: &Self::GroupId, + ) -> bool; + + // recover signer of statement. + fn statement_signer( + &self, + statement: &SignedStatement, + ) -> Option; + + // sign a statement with the local key. + fn sign_statement( + &self, + statement: Statement, + ) -> SignedStatement; +} + +/// Misbehavior: voting both ways on candidate validity. +pub struct ValidityDoubleVote { + /// The candidate digest + pub digest: C::Digest, + /// The signature on the true vote. + pub t_signature: C::Signature, + /// The signature on the false vote. + pub f_signature: C::Signature, +} + +/// Misbehavior: declaring multiple candidates. +pub struct MultipleCandidates { + /// The first candidate seen. + pub first: (C::Candidate, C::Signature), + /// The second candidate seen. + pub second: (C::Candidate, C::Signature), +} + +/// Misbehavior: submitted statement for wrong group. +pub struct UnauthorizedStatement { + /// A signed statement which was submitted without proper authority. + pub statement: SignedStatement, +} + +/// Different kinds of misbehavior. All of these kinds of malicious misbehavior +/// are easily provable and extremely disincentivized. +pub enum Misbehavior { + /// Voted invalid and valid on validity. + ValidityDoubleVote(ValidityDoubleVote), + /// Submitted multiple candidates. + MultipleCandidates(MultipleCandidates), + /// Submitted a message withou + UnauthorizedStatement(UnauthorizedStatement), +} + +// Votes on a specific candidate. +struct CandidateData { + group_id: C::GroupId, + candidate: C::Candidate, + validity_votes: HashMap, + indicated_bad_by: Vec, +} + +/// Stores votes +pub struct Table { + proposed_candidates: HashMap, + detected_misbehavior: HashMap>, + candidate_votes: HashMap>, +} + +impl Table { + /// Import a signed statement + pub fn import_statement(&mut self, context: &C, statement: SignedStatement) { + let signer = match context.statement_signer(&statement) { + None => return, + Some(signer) => signer, + }; + + let maybe_misbehavior = match statement.statement { + Statement::Candidate(candidate) => self.import_candidate( + context, + signer.clone(), + candidate, + statement.signature + ), + Statement::Valid(digest) => unimplemented!(), + _ => unimplemented!(), + }; + + if let Some(misbehavior) = maybe_misbehavior { + // all misbehavior in agreement is provable and actively malicious. + // punishments are not cumulative. + self.detected_misbehavior.insert(validator, misbehavior); + } + } + + fn import_candidate( + &mut self, + context: &C, + from: C::ValidatorId, + candidate: C::Candidate, + signature: C::Signature, + ) -> Option> { + use std::collections::hash_map::Entry; + + let group = context.candidate_group(&candidate); + if !context.is_member_of(&from, &group) { + return Some(Misbehavior::UnauthorizedStatement(UnauthorizedStatement { + statement: SignedStatement { + signature, + statement: Statement::Candidate(candidate), + }, + })); + } + + // check that validator hasn't already specified another candidate. + let digest = context.candidate_digest(&candidate); + + match self.proposed_candidates.entry(from.clone()) { + Entry::Occupied(occ) => { + // if digest is different, fetch candidate and + // note misbehavior. + let old_digest = &occ.get().0; + if old_digest != &digest { + let old_candidate = self.candidate_votes.get(old_digest) + .expect("proposed digest implies existence of votes entry; qed") + .candidate + .clone(); + + return Some(Misbehavior::MultipleCandidates(MultipleCandidates { + first: (old_candidate, occ.get().1.clone()), + second: (candidate, signature), + })); + } + } + Entry::Vacant(vacant) => { + vacant.insert((digest.clone(), signature)); + + // TODO: seed validity votes with issuer here? + self.candidate_votes.entry(digest).or_insert_with(move || CandidateData { + group_id: group, + candidate: candidate, + validity_votes: HashMap::new(), + indicated_bad_by: Vec::new(), + }); + } + } + + None + } +}