From 482a07442a17b6afcfb849a3eba4b7cc2b67be28 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 8 Feb 2018 21:04:43 +0100 Subject: [PATCH 01/21] reshuffle consensus libraries --- Cargo.lock | 31 +- Cargo.toml | 4 +- .../src/handle_incoming.rs | 214 ---------- .../candidate-agreement/src/round_robin.rs | 164 -------- polkadot/candidate-agreement/src/tests/mod.rs | 385 ------------------ .../Cargo.toml | 2 +- .../src/lib.rs | 0 polkadot/statement-table/Cargo.toml | 7 + .../table.rs => statement-table/src/lib.rs} | 39 +- substrate/bft/Cargo.toml | 9 + .../bft => substrate/bft/src}/accumulator.rs | 10 +- .../bft => substrate/bft/src/generic}/mod.rs | 96 +++-- .../bft/src/generic}/tests.rs | 70 +++- substrate/bft/src/lib.rs | 151 +++++++ 14 files changed, 356 insertions(+), 826 deletions(-) delete mode 100644 polkadot/candidate-agreement/src/handle_incoming.rs delete mode 100644 polkadot/candidate-agreement/src/round_robin.rs delete mode 100644 polkadot/candidate-agreement/src/tests/mod.rs rename polkadot/{candidate-agreement => consensus}/Cargo.toml (80%) rename polkadot/{candidate-agreement => consensus}/src/lib.rs (100%) create mode 100644 polkadot/statement-table/Cargo.toml rename polkadot/{candidate-agreement/src/table.rs => statement-table/src/lib.rs} (97%) create mode 100644 substrate/bft/Cargo.toml rename {polkadot/candidate-agreement/src/bft => substrate/bft/src}/accumulator.rs (98%) rename {polkadot/candidate-agreement/src/bft => substrate/bft/src/generic}/mod.rs (90%) rename {polkadot/candidate-agreement/src/bft => substrate/bft/src/generic}/tests.rs (83%) create mode 100644 substrate/bft/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b2fca77787b12..b6147eb6d0b04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,15 +958,6 @@ dependencies = [ "polkadot-network 0.1.0", ] -[[package]] -name = "polkadot-candidate-agreement" -version = "0.1.0" -dependencies = [ - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "polkadot-cli" version = "0.1.0" @@ -999,6 +990,15 @@ dependencies = [ "substrate-primitives 0.1.0", ] +[[package]] +name = "polkadot-consensus" +version = "0.1.0" +dependencies = [ + "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "polkadot-executor" version = "0.1.0" @@ -1068,6 +1068,10 @@ dependencies = [ "substrate-runtime-std 0.1.0", ] +[[package]] +name = "polkadot-statement-table" +version = "0.1.0" + [[package]] name = "polkadot-validator" version = "0.1.0" @@ -1396,6 +1400,15 @@ name = "strsim" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "substrate-bft" +version = "0.1.0" +dependencies = [ + "ed25519 0.1.0", + "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", + "substrate-primitives 0.1.0", +] + [[package]] name = "substrate-client" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 1c3d80e3c7dd7..b8f74ab1c49ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,13 +19,15 @@ members = [ "substrate/environmental", "substrate/executor", "polkadot/network", - "polkadot/candidate-agreement", "polkadot/cli", "polkadot/collator", + "polkadot/consensus", "polkadot/executor", "polkadot/runtime", + "polkadot/statement-table", "polkadot/primitives", "polkadot/validator", + "substrate/bft", "substrate/primitives", "substrate/rpc", "substrate/rpc-servers", diff --git a/polkadot/candidate-agreement/src/handle_incoming.rs b/polkadot/candidate-agreement/src/handle_incoming.rs deleted file mode 100644 index 625c950784106..0000000000000 --- a/polkadot/candidate-agreement/src/handle_incoming.rs +++ /dev/null @@ -1,214 +0,0 @@ -// 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 . - -//! A stream that handles incoming messages to the BFT agreement module and statement -//! table. It forwards as necessary, and dispatches requests for determining availability -//! and validity of candidates as necessary. - -use std::collections::HashSet; - -use futures::prelude::*; -use futures::stream::{Fuse, FuturesUnordered}; -use futures::sync::mpsc; - -use table::{self, Statement, Context as TableContext}; - -use super::{Context, CheckedMessage, SharedTable, TypeResolve}; - -enum CheckResult { - Available, - Unavailable, - Valid, - Invalid, -} - -enum Checking { - Availability(D, A), - Validity(D, V), -} - -impl Future for Checking - where - D: Clone, - A: Future, - V: Future, -{ - type Item = (D, CheckResult); - type Error = E; - - fn poll(&mut self) -> Poll { - Ok(Async::Ready(match *self { - Checking::Availability(ref digest, ref mut f) => { - match try_ready!(f.poll()) { - true => (digest.clone(), CheckResult::Available), - false => (digest.clone(), CheckResult::Unavailable), - } - } - Checking::Validity(ref digest, ref mut f) => { - match try_ready!(f.poll()) { - true => (digest.clone(), CheckResult::Valid), - false => (digest.clone(), CheckResult::Invalid), - } - } - })) - } -} - -/// Handles incoming messages to the BFT service and statement table. -/// -/// Also triggers requests for determining validity and availability of other -/// parachain candidates. -pub struct HandleIncoming { - table: SharedTable, - messages_in: Fuse, - bft_out: mpsc::UnboundedSender<::BftCommunication>, - local_id: C::AuthorityId, - requesting_about: FuturesUnordered::Future, - ::Future, - >>, - checked_validity: HashSet, - checked_availability: HashSet, -} - -impl HandleIncoming { - fn sign_and_import_statement(&self, digest: C::Digest, result: CheckResult) { - let statement = match result { - CheckResult::Valid => Statement::Valid(digest), - CheckResult::Invalid => Statement::Invalid(digest), - CheckResult::Available => Statement::Available(digest), - CheckResult::Unavailable => return, // no such statement and not provable. - }; - - // TODO: trigger broadcast to peers immediately? - self.table.sign_and_import(statement); - } - - fn import_message(&mut self, origin: C::AuthorityId, message: CheckedMessage) { - match message { - CheckedMessage::Bft(msg) => { let _ = self.bft_out.unbounded_send(msg); } - CheckedMessage::Table(table_messages) => { - // import all table messages and check for any that we - // need to produce statements for. - let msg_iter = table_messages - .into_iter() - .map(|m| (m, Some(origin.clone()))); - let summaries: Vec<_> = self.table.import_statements(msg_iter); - - for summary in summaries { - self.dispatch_on_summary(summary) - } - } - } - } - - // on new candidates in our group, begin checking validity. - // on new candidates in our availability sphere, begin checking availability. - fn dispatch_on_summary(&mut self, summary: table::Summary) { - let is_validity_member = - self.table.context().is_member_of(&self.local_id, &summary.group_id); - - let is_availability_member = - self.table.context().is_availability_guarantor_of(&self.local_id, &summary.group_id); - - let digest = &summary.candidate; - - // TODO: consider a strategy based on the number of candidate votes as well. - let checking_validity = - is_validity_member && - self.checked_validity.insert(digest.clone()) && - self.table.proposed_digest() != Some(digest.clone()); - - let checking_availability = is_availability_member && self.checked_availability.insert(digest.clone()); - - if checking_validity || checking_availability { - let context = &*self.table.context(); - let requesting_about = &mut self.requesting_about; - self.table.with_candidate(digest, |c| match c { - None => {} // TODO: handle table inconsistency somehow? - Some(candidate) => { - if checking_validity { - let future = context.check_validity(candidate).into_future(); - let checking = Checking::Validity(digest.clone(), future); - requesting_about.push(checking); - } - - if checking_availability { - let future = context.check_availability(candidate).into_future(); - let checking = Checking::Availability(digest.clone(), future); - requesting_about.push(checking); - } - } - }) - } - } -} - -impl HandleIncoming - where - C: Context, - I: Stream),Error=E>, - C::CheckAvailability: IntoFuture, - C::CheckCandidate: IntoFuture, -{ - pub fn new( - table: SharedTable, - messages_in: I, - bft_out: mpsc::UnboundedSender<::BftCommunication>, - ) -> Self { - let local_id = table.context().local_id(); - - HandleIncoming { - table, - bft_out, - local_id, - messages_in: messages_in.fuse(), - requesting_about: FuturesUnordered::new(), - checked_validity: HashSet::new(), - checked_availability: HashSet::new(), - } - } -} - -impl Future for HandleIncoming - where - C: Context, - I: Stream),Error=E>, - C::CheckAvailability: IntoFuture, - C::CheckCandidate: IntoFuture, -{ - type Item = (); - type Error = E; - - fn poll(&mut self) -> Poll<(), E> { - loop { - // FuturesUnordered is safe to poll after it has completed. - while let Async::Ready(Some((d, r))) = self.requesting_about.poll()? { - self.sign_and_import_statement(d, r); - } - - match try_ready!(self.messages_in.poll()) { - None => if self.requesting_about.is_empty() { - return Ok(Async::Ready(())) - } else { - return Ok(Async::NotReady) - }, - Some((origin, msg)) => self.import_message(origin, msg), - } - } - } -} diff --git a/polkadot/candidate-agreement/src/round_robin.rs b/polkadot/candidate-agreement/src/round_robin.rs deleted file mode 100644 index 3f98507cab89d..0000000000000 --- a/polkadot/candidate-agreement/src/round_robin.rs +++ /dev/null @@ -1,164 +0,0 @@ -// 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 . - -//! Round-robin buffer for incoming messages. -//! -//! This takes batches of messages associated with a sender as input, -//! and yields messages in a fair order by sender. - -use std::collections::{Bound, BTreeMap, VecDeque}; - -use futures::prelude::*; -use futures::stream::Fuse; - -/// Implementation of the round-robin buffer for incoming messages. -#[derive(Debug)] -pub struct RoundRobinBuffer { - buffer: BTreeMap>, - last_processed_from: Option, - stored_messages: usize, - max_messages: usize, - inner: Fuse, -} - -impl RoundRobinBuffer { - /// Create a new round-robin buffer which holds up to a maximum - /// amount of messages. - pub fn new(stream: S, buffer_size: usize) -> Self { - RoundRobinBuffer { - buffer: BTreeMap::new(), - last_processed_from: None, - stored_messages: 0, - max_messages: buffer_size, - inner: stream.fuse(), - } - } -} - -impl RoundRobinBuffer { - fn next_message(&mut self) -> Option<(V, M)> { - if self.stored_messages == 0 { - return None - } - - // first pick up from the last authority we processed a message from - let mut next = { - let lower_bound = match self.last_processed_from { - None => Bound::Unbounded, - Some(ref x) => Bound::Excluded(x.clone()), - }; - - self.buffer.range_mut((lower_bound, Bound::Unbounded)) - .filter_map(|(k, v)| v.pop_front().map(|v| (k.clone(), v))) - .next() - }; - - // but wrap around to the beginning again if we got nothing. - if next.is_none() { - next = self.buffer.iter_mut() - .filter_map(|(k, v)| v.pop_front().map(|v| (k.clone(), v))) - .next(); - } - - if let Some((ref authority, _)) = next { - self.stored_messages -= 1; - self.last_processed_from = Some(authority.clone()); - } - - next - } - - // import messages, discarding when the buffer is full. - fn import_messages(&mut self, sender: V, messages: Vec) { - let space_remaining = self.max_messages - self.stored_messages; - self.stored_messages += ::std::cmp::min(space_remaining, messages.len()); - - let v = self.buffer.entry(sender).or_insert_with(VecDeque::new); - v.extend(messages.into_iter().take(space_remaining)); - } -} - -impl Stream for RoundRobinBuffer - where S: Stream)> -{ - type Item = (V, M); - type Error = S::Error; - - fn poll(&mut self) -> Poll, S::Error> { - loop { - match self.inner.poll()? { - Async::NotReady | Async::Ready(None) => break, - Async::Ready(Some((sender, msgs))) => self.import_messages(sender, msgs), - } - } - - let done = self.inner.is_done(); - Ok(match self.next_message() { - Some(msg) => Async::Ready(Some(msg)), - None => if done { Async::Ready(None) } else { Async::NotReady }, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::stream::{self, Stream}; - - #[derive(Debug, PartialEq, Eq)] - struct UncheckedMessage { data: Vec } - - #[test] - fn is_fair_and_wraps_around() { - let stream = stream::iter_ok(vec![ - (1, vec![ - UncheckedMessage { data: vec![1, 3, 5] }, - UncheckedMessage { data: vec![3, 5, 7] }, - UncheckedMessage { data: vec![5, 7, 9] }, - ]), - (2, vec![ - UncheckedMessage { data: vec![2, 4, 6] }, - UncheckedMessage { data: vec![4, 6, 8] }, - UncheckedMessage { data: vec![6, 8, 10] }, - ]), - ]); - - let round_robin = RoundRobinBuffer::new(stream, 100); - let output = round_robin.wait().collect::, ()>>().unwrap(); - - assert_eq!(output, vec![ - (1, UncheckedMessage { data: vec![1, 3, 5] }), - (2, UncheckedMessage { data: vec![2, 4, 6] }), - (1, UncheckedMessage { data: vec![3, 5, 7] }), - - (2, UncheckedMessage { data: vec![4, 6, 8] }), - (1, UncheckedMessage { data: vec![5, 7, 9] }), - (2, UncheckedMessage { data: vec![6, 8, 10] }), - ]); - } - - #[test] - fn discards_when_full() { - let stream = stream::iter_ok(vec![ - (1, (0..200).map(|i| UncheckedMessage { data: vec![i] }).collect()) - ]); - - let round_robin = RoundRobinBuffer::new(stream, 100); - let output = round_robin.wait().collect::, ()>>().unwrap(); - - assert_eq!(output.len(), 100); - } -} diff --git a/polkadot/candidate-agreement/src/tests/mod.rs b/polkadot/candidate-agreement/src/tests/mod.rs deleted file mode 100644 index 1599a94aa69b7..0000000000000 --- a/polkadot/candidate-agreement/src/tests/mod.rs +++ /dev/null @@ -1,385 +0,0 @@ -// 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 . - -//! Tests and test helpers for the candidate agreement. - -const VALIDITY_CHECK_DELAY_MS: u64 = 100; -const AVAILABILITY_CHECK_DELAY_MS: u64 = 100; -const PROPOSAL_FORMATION_TICK_MS: u64 = 50; -const PROPAGATE_STATEMENTS_TICK_MS: u64 = 200; -const TIMER_TICK_DURATION_MS: u64 = 10; - -use std::collections::HashMap; - -use futures::prelude::*; -use futures::sync::mpsc; -use tokio_timer::Timer; - -use super::*; - -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone, Copy)] -struct AuthorityId(usize); - -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone)] -struct Digest(Vec); - -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone)] -struct GroupId(usize); - -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone)] -struct ParachainCandidate { - group: GroupId, - data: usize, -} - -#[derive(PartialEq, Eq, Debug, Clone)] -struct Proposal { - candidates: Vec, -} - -#[derive(PartialEq, Eq, Debug, Clone)] -enum Signature { - Table(AuthorityId, table::Statement), - Bft(AuthorityId, bft::Message), -} - -enum Error { - Timer(tokio_timer::TimerError), - NetOut, - NetIn, -} - -#[derive(Debug, Clone)] -struct SharedTestContext { - n_authorities: usize, - n_groups: usize, - timer: Timer, -} - -#[derive(Debug, Clone)] -struct TestContext { - shared: Arc, - local_id: AuthorityId, -} - -impl Context for TestContext { - type AuthorityId = AuthorityId; - type Digest = Digest; - type GroupId = GroupId; - type Signature = Signature; - type Proposal = Proposal; - type ParachainCandidate = ParachainCandidate; - - type CheckCandidate = Box>; - type CheckAvailability = Box>; - - type StatementBatch = VecBatch< - AuthorityId, - table::SignedStatement - >; - - fn candidate_digest(candidate: &ParachainCandidate) -> Digest { - Digest(vec![candidate.group.0, candidate.data]) - } - - fn proposal_digest(candidate: &Proposal) -> Digest { - Digest(candidate.candidates.iter().fold(Vec::new(), |mut a, c| { - a.extend(Self::candidate_digest(c).0); - a - })) - } - - fn candidate_group(candidate: &ParachainCandidate) -> GroupId { - candidate.group.clone() - } - - fn round_proposer(&self, round: usize) -> AuthorityId { - AuthorityId(round % self.shared.n_authorities) - } - - fn check_validity(&self, _candidate: &ParachainCandidate) -> Self::CheckCandidate { - let future = self.shared.timer - .sleep(::std::time::Duration::from_millis(VALIDITY_CHECK_DELAY_MS)) - .map_err(Error::Timer) - .map(|_| true); - - Box::new(future) - } - - fn check_availability(&self, _candidate: &ParachainCandidate) -> Self::CheckAvailability { - let future = self.shared.timer - .sleep(::std::time::Duration::from_millis(AVAILABILITY_CHECK_DELAY_MS)) - .map_err(Error::Timer) - .map(|_| true); - - Box::new(future) - } - - fn create_proposal(&self, candidates: Vec<&ParachainCandidate>) - -> Option - { - let t = self.shared.n_groups * 2 / 3; - if candidates.len() >= t { - Some(Proposal { - candidates: candidates.iter().map(|x| (&**x).clone()).collect() - }) - } else { - None - } - } - - fn proposal_valid(&self, proposal: &Proposal, check_candidate: F) -> bool - where F: FnMut(&ParachainCandidate) -> bool - { - if proposal.candidates.len() >= self.shared.n_groups * 2 / 3 { - proposal.candidates.iter().all(check_candidate) - } else { - false - } - } - - fn local_id(&self) -> AuthorityId { - self.local_id.clone() - } - - fn sign_table_statement( - &self, - statement: &table::Statement - ) -> Signature { - Signature::Table(self.local_id(), statement.clone()) - } - - fn sign_bft_message(&self, message: &bft::Message) -> Signature { - Signature::Bft(self.local_id(), message.clone()) - } -} - -struct TestRecovery; - -impl MessageRecovery for TestRecovery { - type UncheckedMessage = OutgoingMessage; - - fn check_message(&self, msg: Self::UncheckedMessage) -> Option> { - Some(match msg { - OutgoingMessage::Bft(c) => CheckedMessage::Bft(c), - OutgoingMessage::Table(batch) => CheckedMessage::Table(batch.items), - }) - } -} - -pub struct Network { - endpoints: Vec>, - input: mpsc::UnboundedReceiver<(usize, T)>, -} - -impl Network { - pub fn new(nodes: usize) - -> (Self, Vec>, Vec>) - { - let mut inputs = Vec::with_capacity(nodes); - let mut outputs = Vec::with_capacity(nodes); - let mut endpoints = Vec::with_capacity(nodes); - - let (in_tx, in_rx) = mpsc::unbounded(); - for _ in 0..nodes { - let (out_tx, out_rx) = mpsc::unbounded(); - inputs.push(in_tx.clone()); - outputs.push(out_rx); - endpoints.push(out_tx); - } - - let network = Network { - endpoints, - input: in_rx, - }; - - (network, inputs, outputs) - } - - pub fn route_on_thread(self) { - ::std::thread::spawn(move || { let _ = self.wait(); }); - } -} - -impl Future for Network { - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll<(), Self::Error> { - match try_ready!(self.input.poll()) { - None => Ok(Async::Ready(())), - Some((sender, item)) => { - { - let receiving_endpoints = self.endpoints - .iter() - .enumerate() - .filter(|&(i, _)| i != sender) - .map(|(_, x)| x); - - for endpoint in receiving_endpoints { - let _ = endpoint.unbounded_send(item.clone()); - } - } - - self.poll() - } - } - } -} - -#[derive(Debug, Clone)] -pub struct VecBatch { - pub max_len: usize, - pub targets: Vec, - pub items: Vec, -} - -impl ::StatementBatch for VecBatch { - fn targets(&self) -> &[V] { &self.targets } - fn is_empty(&self) -> bool { self.items.is_empty() } - fn push(&mut self, item: T) -> bool { - if self.items.len() == self.max_len { - false - } else { - self.items.push(item); - true - } - } -} - -fn make_group_assignments(n_authorities: usize, n_groups: usize) - -> HashMap> -{ - let mut map = HashMap::new(); - let threshold = (n_authorities / n_groups) / 2; - let make_blank_group = || { - GroupInfo { - validity_guarantors: HashSet::new(), - availability_guarantors: HashSet::new(), - needed_validity: threshold, - needed_availability: threshold, - } - }; - - // every authority checks validity of his ID modulo n_groups and - // guarantees availability for the group above that. - for a_id in 0..n_authorities { - let primary_group = a_id % n_groups; - let availability_groups = [ - (a_id + 1) % n_groups, - a_id.wrapping_sub(1) % n_groups, - ]; - - map.entry(GroupId(primary_group)) - .or_insert_with(&make_blank_group) - .validity_guarantors - .insert(AuthorityId(a_id)); - - for &availability_group in &availability_groups { - map.entry(GroupId(availability_group)) - .or_insert_with(&make_blank_group) - .availability_guarantors - .insert(AuthorityId(a_id)); - } - } - - map -} - -fn make_blank_batch(n_authorities: usize) -> VecBatch { - VecBatch { - max_len: 20, - targets: (0..n_authorities).map(AuthorityId).collect(), - items: Vec::new(), - } -} - -#[test] -fn consensus_completes_with_minimum_good() { - let n = 50; - let f = 16; - let n_groups = 10; - - let timer = ::tokio_timer::wheel() - .tick_duration(Duration::from_millis(TIMER_TICK_DURATION_MS)) - .num_slots(1 << 16) - .build(); - - let (network, inputs, outputs) = Network::<(AuthorityId, OutgoingMessage)>::new(n - f); - network.route_on_thread(); - - let shared_test_context = Arc::new(SharedTestContext { - n_authorities: n, - n_groups: n_groups, - timer: timer.clone(), - }); - - let groups = make_group_assignments(n, n_groups); - - let authorities = inputs.into_iter().zip(outputs).enumerate().map(|(raw_id, (input, output))| { - let id = AuthorityId(raw_id); - let context = TestContext { - shared: shared_test_context.clone(), - local_id: id, - }; - - let shared_table = SharedTable::new(context.clone(), groups.clone()); - let params = AgreementParams { - context, - timer: timer.clone(), - table: shared_table, - nodes: n, - max_faulty: f, - round_timeout_multiplier: 4, - message_buffer_size: 100, - form_proposal_interval: Duration::from_millis(PROPOSAL_FORMATION_TICK_MS), - }; - - let net_out = input - .sink_map_err(|_| Error::NetOut) - .with(move |x| Ok::<_, Error>((id.0, (id, x))) ); - - let net_in = output - .map_err(|_| Error::NetIn) - .map(move |(v, msg)| (v, vec![msg])); - - let propagate_statements = timer - .interval(Duration::from_millis(PROPAGATE_STATEMENTS_TICK_MS)) - .map(move |()| make_blank_batch(n)) - .map_err(Error::Timer); - - let local_candidate = if raw_id < n_groups { - let candidate = ParachainCandidate { - group: GroupId(raw_id), - data: raw_id, - }; - ::futures::future::Either::A(Ok::<_, Error>(candidate).into_future()) - } else { - ::futures::future::Either::B(::futures::future::empty()) - }; - - agree::<_, _, _, _, _, _, Error>( - params, - net_in, - net_out, - TestRecovery, - propagate_statements, - local_candidate - ) - }).collect::>(); - - futures::future::join_all(authorities).wait().unwrap(); -} diff --git a/polkadot/candidate-agreement/Cargo.toml b/polkadot/consensus/Cargo.toml similarity index 80% rename from polkadot/candidate-agreement/Cargo.toml rename to polkadot/consensus/Cargo.toml index 8aa2d0001b5e8..ea2a27fcfa00b 100644 --- a/polkadot/candidate-agreement/Cargo.toml +++ b/polkadot/consensus/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "polkadot-candidate-agreement" +name = "polkadot-consensus" version = "0.1.0" authors = ["Parity Technologies "] diff --git a/polkadot/candidate-agreement/src/lib.rs b/polkadot/consensus/src/lib.rs similarity index 100% rename from polkadot/candidate-agreement/src/lib.rs rename to polkadot/consensus/src/lib.rs diff --git a/polkadot/statement-table/Cargo.toml b/polkadot/statement-table/Cargo.toml new file mode 100644 index 0000000000000..a26308d272fa9 --- /dev/null +++ b/polkadot/statement-table/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "polkadot-statement-table" +version = "0.1.0" +authors = ["Parity Technologies "] + +[dependencies] +substrate-primitives = { path = "../../substrate/primitives" } diff --git a/polkadot/candidate-agreement/src/table.rs b/polkadot/statement-table/src/lib.rs similarity index 97% rename from polkadot/candidate-agreement/src/table.rs rename to polkadot/statement-table/src/lib.rs index 2909d219c6fbb..be184f0c6ea3d 100644 --- a/polkadot/candidate-agreement/src/table.rs +++ b/polkadot/statement-table/src/lib.rs @@ -32,7 +32,21 @@ use std::collections::hash_map::{HashMap, Entry}; use std::hash::Hash; use std::fmt::Debug; -use super::StatementBatch; +/// A batch of statements to send out. +pub trait StatementBatch { + /// Get the target authorities of these statements. + fn targets(&self) -> &[V]; + + /// If the batch is empty. + fn is_empty(&self) -> bool; + + /// Push a statement onto the batch. Returns false when the batch is full. + /// + /// This is meant to do work like incrementally serializing the statements + /// into a vector of bytes while making sure the length is below a certain + /// amount. + fn push(&mut self, statement: T) -> bool; +} /// Context for the statement table. pub trait Context { @@ -380,7 +394,7 @@ impl Table { &self.detected_misbehavior } - /// Fill a statement batch and note messages seen by the targets. + /// Fill a statement batch and note messages as seen by the targets. pub fn fill_batch(&mut self, batch: &mut B) where B: StatementBatch< C::AuthorityId, @@ -709,9 +723,28 @@ impl Table { #[cfg(test)] mod tests { use super::*; - use ::tests::VecBatch; use std::collections::HashMap; + #[derive(Debug, Clone)] + struct VecBatch { + pub max_len: usize, + pub targets: Vec, + pub items: Vec, + } + + impl ::StatementBatch for VecBatch { + fn targets(&self) -> &[V] { &self.targets } + fn is_empty(&self) -> bool { self.items.is_empty() } + fn push(&mut self, item: T) -> bool { + if self.items.len() == self.max_len { + false + } else { + self.items.push(item); + true + } + } + } + fn create() -> Table { Table { authority_data: HashMap::default(), diff --git a/substrate/bft/Cargo.toml b/substrate/bft/Cargo.toml new file mode 100644 index 0000000000000..1e15e098cab80 --- /dev/null +++ b/substrate/bft/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "substrate-bft" +version = "0.1.0" +authors = ["Parity Technologies "] + +[dependencies] +futures = "0.1.17" +substrate-primitives = { path = "../primitives" } +ed25519 = { path = "../ed25519" } diff --git a/polkadot/candidate-agreement/src/bft/accumulator.rs b/substrate/bft/src/accumulator.rs similarity index 98% rename from polkadot/candidate-agreement/src/bft/accumulator.rs rename to substrate/bft/src/accumulator.rs index ab035737fb84e..273fc658478e9 100644 --- a/polkadot/candidate-agreement/src/bft/accumulator.rs +++ b/substrate/bft/src/accumulator.rs @@ -1,18 +1,18 @@ // Copyright 2017 Parity Technologies (UK) Ltd. -// This file is part of Polkadot. +// This file is part of Substrate. -// Polkadot is free software: you can redistribute it and/or modify +// Substrate 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, +// Substrate 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 . +// along with Substrate. If not, see . //! Message accumulator for each round of BFT consensus. @@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet}; use std::collections::hash_map::Entry; use std::hash::Hash; -use super::{Message, LocalizedMessage}; +use generic::{Message, LocalizedMessage}; /// Justification for some state at a given round. #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/polkadot/candidate-agreement/src/bft/mod.rs b/substrate/bft/src/generic/mod.rs similarity index 90% rename from polkadot/candidate-agreement/src/bft/mod.rs rename to substrate/bft/src/generic/mod.rs index f131e44e1f8b9..e0a8f133e17b2 100644 --- a/polkadot/candidate-agreement/src/bft/mod.rs +++ b/substrate/bft/src/generic/mod.rs @@ -1,25 +1,21 @@ // Copyright 2017 Parity Technologies (UK) Ltd. -// This file is part of Polkadot. +// This file is part of Substrate. -// Polkadot is free software: you can redistribute it and/or modify +// Substrate 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, +// Substrate 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 . +// along with Substrate. If not, see . //! BFT Agreement based on a rotating proposer in different rounds. - -mod accumulator; - -#[cfg(test)] -mod tests; +//! Very general implementation. use std::collections::{HashMap, VecDeque}; use std::fmt::Debug; @@ -27,9 +23,12 @@ use std::hash::Hash; use futures::{future, Future, Stream, Sink, Poll, Async, AsyncSink}; -use self::accumulator::State; +use accumulator::State; -pub use self::accumulator::{Accumulator, Justification, PrepareJustification, UncheckedJustification}; +pub use accumulator::{Accumulator, Justification, PrepareJustification, UncheckedJustification}; + +#[cfg(test)] +mod tests; /// Messages over the proposal. /// Each message carries an associated round number. @@ -46,7 +45,8 @@ pub enum Message { } impl Message { - fn round_number(&self) -> usize { + /// Extract the round number. + pub fn round_number(&self) -> usize { match *self { Message::Propose(round, _) => round, Message::Prepare(round, _) => round, @@ -119,18 +119,14 @@ pub enum Communication { Auxiliary(PrepareJustification), } -/// Type alias for a localized message using only type parameters from `Context`. -// TODO: actual type alias when it's no longer a warning. -pub struct ContextCommunication(pub Communication); +/// Hack to get around type alias warning. +pub trait TypeResolve { + /// Communication type. + type Communication; +} -impl Clone for ContextCommunication - where - LocalizedMessage: Clone, - PrepareJustification: Clone, -{ - fn clone(&self) -> Self { - ContextCommunication(self.0.clone()) - } +impl TypeResolve for C { + type Communication = Communication; } #[derive(Debug)] @@ -326,7 +322,11 @@ impl Strategy { // rounds if necessary. // // only call within the context of a `Task`. - fn poll(&mut self, context: &C, sending: &mut Sending>) + fn poll( + &mut self, + context: &C, + sending: &mut Sending<::Communication> + ) -> Poll, E> where C::RoundTimeout: Future, @@ -359,7 +359,11 @@ impl Strategy { // perform one round of polling: attempt to broadcast messages and change the state. // if the round or internal round-state changes, this should be called again. - fn poll_once(&mut self, context: &C, sending: &mut Sending>) + fn poll_once( + &mut self, + context: &C, + sending: &mut Sending<::Communication> + ) -> Poll, E> where C::RoundTimeout: Future, @@ -412,7 +416,11 @@ impl Strategy { Ok(Async::NotReady) } - fn propose(&mut self, context: &C, sending: &mut Sending>) + fn propose( + &mut self, + context: &C, + sending: &mut Sending<::Communication> + ) -> Result<(), ::Error> { if let LocalState::Start = self.local_state { @@ -461,7 +469,7 @@ impl Strategy { // broadcast the justification along with the proposal if we are locked. if let Some(ref locked) = self.locked { sending.push( - ContextCommunication(Communication::Auxiliary(locked.justification.clone())) + Communication::Auxiliary(locked.justification.clone()) ); } @@ -472,7 +480,11 @@ impl Strategy { Ok(()) } - fn prepare(&mut self, context: &C, sending: &mut Sending>) { + fn prepare( + &mut self, + context: &C, + sending: &mut Sending<::Communication> + ) { // prepare only upon start or having proposed. match self.local_state { LocalState::Start | LocalState::Proposed => {}, @@ -511,7 +523,11 @@ impl Strategy { } } - fn commit(&mut self, context: &C, sending: &mut Sending>) { + fn commit( + &mut self, + context: &C, + sending: &mut Sending<::Communication> + ) { // commit only if we haven't voted to advance or committed already match self.local_state { LocalState::Committed | LocalState::VoteAdvance => return, @@ -538,7 +554,11 @@ impl Strategy { } } - fn vote_advance(&mut self, context: &C, sending: &mut Sending>) + fn vote_advance( + &mut self, + context: &C, + sending: &mut Sending<::Communication> + ) -> Result<(), ::Error> { // we can vote for advancement under all circumstances unless we have already. @@ -606,11 +626,11 @@ impl Strategy { &mut self, message: Message, context: &C, - sending: &mut Sending> + sending: &mut Sending<::Communication> ) { let signed_message = context.sign_local(message); self.import_message(signed_message.clone()); - sending.push(ContextCommunication(Communication::Consensus(signed_message))); + sending.push(Communication::Consensus(signed_message)); } } @@ -621,7 +641,7 @@ pub struct Agreement { input: I, output: O, concluded: Option>, - sending: Sending>, + sending: Sending<::Communication>, strategy: Strategy, } @@ -630,8 +650,8 @@ impl Future for Agreement C: Context, C::RoundTimeout: Future, C::CreateProposal: Future, - I: Stream,Error=E>, - O: Sink,SinkError=E>, + I: Stream::Communication,Error=E>, + O: Sink::Communication,SinkError=E>, E: From, { type Item = Committed; @@ -656,7 +676,7 @@ impl Future for Agreement Async::NotReady => break, }; - match message.0 { + match message { Communication::Consensus(message) => self.strategy.import_message(message), Communication::Auxiliary(lock_proof) => self.strategy.import_lock_proof(&self.context, lock_proof), @@ -705,8 +725,8 @@ pub fn agree(context: C, nodes: usize, max_faulty: usize, i C: Context, C::RoundTimeout: Future, C::CreateProposal: Future, - I: Stream,Error=E>, - O: Sink,SinkError=E>, + I: Stream::Communication,Error=E>, + O: Sink::Communication,SinkError=E>, E: From, { let strategy = Strategy::create(&context, nodes, max_faulty); diff --git a/polkadot/candidate-agreement/src/bft/tests.rs b/substrate/bft/src/generic/tests.rs similarity index 83% rename from polkadot/candidate-agreement/src/bft/tests.rs rename to substrate/bft/src/generic/tests.rs index 10ef9321242b1..41d79dcca3782 100644 --- a/polkadot/candidate-agreement/src/bft/tests.rs +++ b/substrate/bft/src/generic/tests.rs @@ -1,32 +1,90 @@ // Copyright 2017 Parity Technologies (UK) Ltd. // This file is part of Polkadot. -// Polkadot is free software: you can redistribute it and/or modify +// Substrate 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, +// Substrate 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 . +// along with Substrate. If not, see . //! Tests for the candidate agreement strategy. use super::*; -use tests::Network; - use std::sync::{Arc, Mutex}; use std::time::Duration; use futures::prelude::*; -use futures::sync::oneshot; +use futures::sync::{oneshot, mpsc}; use futures::future::FutureResult; +struct Network { + endpoints: Vec>, + input: mpsc::UnboundedReceiver<(usize, T)>, +} + +impl Network { + fn new(nodes: usize) + -> (Self, Vec>, Vec>) + { + let mut inputs = Vec::with_capacity(nodes); + let mut outputs = Vec::with_capacity(nodes); + let mut endpoints = Vec::with_capacity(nodes); + + let (in_tx, in_rx) = mpsc::unbounded(); + for _ in 0..nodes { + let (out_tx, out_rx) = mpsc::unbounded(); + inputs.push(in_tx.clone()); + outputs.push(out_rx); + endpoints.push(out_tx); + } + + let network = Network { + endpoints, + input: in_rx, + }; + + (network, inputs, outputs) + } + + fn route_on_thread(self) { + ::std::thread::spawn(move || { let _ = self.wait(); }); + } +} + +impl Future for Network { + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll<(), Self::Error> { + match try_ready!(self.input.poll()) { + None => Ok(Async::Ready(())), + Some((sender, item)) => { + { + let receiving_endpoints = self.endpoints + .iter() + .enumerate() + .filter(|&(i, _)| i != sender) + .map(|(_, x)| x); + + for endpoint in receiving_endpoints { + let _ = endpoint.unbounded_send(item.clone()); + } + } + + self.poll() + } + } + } +} + #[derive(Debug, PartialEq, Eq, Clone, Hash)] struct Candidate(usize); diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs new file mode 100644 index 0000000000000..1229f3055e278 --- /dev/null +++ b/substrate/bft/src/lib.rs @@ -0,0 +1,151 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! BFT Agreement based on a rotating proposer in different rounds. + +mod accumulator; +pub mod generic; + +#[cfg_attr(test, macro_use)] +extern crate futures; +extern crate substrate_primitives as primitives; +extern crate ed25519; + +use ed25519::Signature; +use primitives::block::HeaderHash; +use primitives::{Block, AuthorityId}; + +use futures::{Stream, Sink, Future}; + +pub use generic::InputStreamConcluded; + +/// Messages over the proposal. +/// Each message carries an associated round number. +pub type Message = generic::Message; + +/// Localized message type. +pub type LocalizedMessage = generic::LocalizedMessage< + Block, + HeaderHash, + AuthorityId, + Signature +>; + +/// Justification of some hash. +pub type Justification = generic::Justification; + +/// Justification of a prepare message. +pub type PrepareJustification = generic::PrepareJustification; + +/// Result of a committed round of BFT +pub type Committed = generic::Committed; + +/// Communication between BFT participants. +pub type Communication = generic::Communication; + +/// Context necessary for agreement. +pub trait Context { + /// A future that resolves when a round timeout is concluded. + type RoundTimeout: Future; + /// A future that resolves when a proposal is ready. + type CreateProposal: Future; + + /// Get the local authority ID. + fn local_id(&self) -> AuthorityId; + + /// Get the best proposal. + fn proposal(&self) -> Self::CreateProposal; + + /// Get the digest of a candidate. + fn candidate_digest(&self, candidate: &Block) -> HeaderHash; + + /// Sign a message using the local authority ID. + fn sign_local(&self, message: Message) -> LocalizedMessage; + + /// Get the proposer for a given round of consensus. + fn round_proposer(&self, round: usize) -> AuthorityId; + + /// Whether the candidate is valid. + fn candidate_valid(&self, candidate: &Block) -> bool; + + /// Create a round timeout. The context will determine the correct timeout + /// length, and create a future that will resolve when the timeout is + /// concluded. + fn begin_round_timeout(&self, round: usize) -> Self::RoundTimeout; +} + +impl generic::Context for T { + type Candidate = Block; + type Digest = HeaderHash; + type AuthorityId = AuthorityId; + type Signature = Signature; + type RoundTimeout = ::RoundTimeout; + type CreateProposal = ::CreateProposal; + + fn local_id(&self) -> AuthorityId { Context::local_id(self) } + + fn proposal(&self) -> Self::CreateProposal { Context::proposal(self) } + + fn candidate_digest(&self, candidate: &Block) -> HeaderHash { + Context::candidate_digest(self, candidate) + } + + fn sign_local(&self, message: Message) -> LocalizedMessage { + Context::sign_local(self, message) + } + + fn round_proposer(&self, round: usize) -> AuthorityId { + Context::round_proposer(self, round) + } + + fn candidate_valid(&self, candidate: &Block) -> bool { + Context::candidate_valid(self, candidate) + } + + fn begin_round_timeout(&self, round: usize) -> Self::RoundTimeout { + Context::begin_round_timeout(self, round) + } +} + +/// Attempt to reach BFT agreement on a candidate. +/// +/// `nodes` is the number of nodes in the system. +/// `max_faulty` is the maximum number of faulty nodes. Should be less than +/// 1/3 of `nodes`, otherwise agreement may never be reached. +/// +/// The input stream should never logically conclude. The logic here assumes +/// that messages flushed to the output stream will eventually reach other nodes. +/// +/// Note that it is possible to witness agreement being reached without ever +/// seeing the candidate. Any candidates seen will be checked for validity. +/// +/// Although technically the agreement will always complete (given the eventual +/// delivery of messages), in practice it is possible for this future to +/// conclude without having witnessed the conclusion. +/// In general, this future should be pre-empted by the import of a justification +/// set for this block height. +pub fn agree(context: C, nodes: usize, max_faulty: usize, input: I, output: O) + -> generic::Agreement + where + C: Context, + C::RoundTimeout: Future, + C::CreateProposal: Future, + I: Stream, + O: Sink, + E: From, +{ + generic::agree(context, nodes, max_faulty, input, output) +} From 917b0925dca61ac1d70f57528bf5d81844481fd4 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 8 Feb 2018 23:26:16 +0100 Subject: [PATCH 02/21] polkadot-useful type definitions for statement table --- Cargo.lock | 4 + polkadot/primitives/src/parachain.rs | 17 +- polkadot/statement-table/Cargo.toml | 1 + polkadot/statement-table/src/generic.rs | 1224 ++++++++++++++++++++++ polkadot/statement-table/src/lib.rs | 1227 ++--------------------- substrate/runtime-std/with_std.rs | 1 + substrate/runtime-std/without_std.rs | 1 + 7 files changed, 1305 insertions(+), 1170 deletions(-) create mode 100644 polkadot/statement-table/src/generic.rs diff --git a/Cargo.lock b/Cargo.lock index b6147eb6d0b04..926b1bee1c185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1071,6 +1071,10 @@ dependencies = [ [[package]] name = "polkadot-statement-table" version = "0.1.0" +dependencies = [ + "polkadot-primitives 0.1.0", + "substrate-primitives 0.1.0", +] [[package]] name = "polkadot-validator" diff --git a/polkadot/primitives/src/parachain.rs b/polkadot/primitives/src/parachain.rs index 75d5b6a326acd..7e1b93975c9bd 100644 --- a/polkadot/primitives/src/parachain.rs +++ b/polkadot/primitives/src/parachain.rs @@ -19,6 +19,7 @@ #[cfg(feature = "std")] use primitives::bytes; use primitives; +use rstd::cmp::{PartialOrd, Ord, Ordering}; use rstd::vec::Vec; /// Unique identifier of a parachain. @@ -84,6 +85,20 @@ pub struct CandidateReceipt { pub fees: u64, } +impl PartialOrd for CandidateReceipt { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CandidateReceipt { + fn cmp(&self, other: &Self) -> Ordering { + // TODO: compare signatures or something more sane + self.parachain_index.cmp(&other.parachain_index) + .then_with(|| self.head_data.cmp(&other.head_data)) + } +} + /// Parachain ingress queue message. #[derive(PartialEq, Eq, Clone)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] @@ -110,7 +125,7 @@ pub struct BlockData(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec); /// Parachain head data included in the chain. -#[derive(PartialEq, Eq, Clone)] +#[derive(PartialEq, Eq, Clone, PartialOrd, Ord)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] pub struct HeadData(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec); diff --git a/polkadot/statement-table/Cargo.toml b/polkadot/statement-table/Cargo.toml index a26308d272fa9..5c8a61e81d33c 100644 --- a/polkadot/statement-table/Cargo.toml +++ b/polkadot/statement-table/Cargo.toml @@ -5,3 +5,4 @@ authors = ["Parity Technologies "] [dependencies] substrate-primitives = { path = "../../substrate/primitives" } +polkadot-primitives = { path = "../primitives" } diff --git a/polkadot/statement-table/src/generic.rs b/polkadot/statement-table/src/generic.rs new file mode 100644 index 0000000000000..13e582a33fc6b --- /dev/null +++ b/polkadot/statement-table/src/generic.rs @@ -0,0 +1,1224 @@ +// 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: generic implementation. +//! +//! This stores messages other authorities issue about candidates. +//! +//! These messages are used to create a proposal submitted to a BFT consensus process. +//! +//! Proposals are formed of sets of candidates which have the requisite number of +//! validity and availability votes. +//! +//! Each parachain is associated with two sets of authorities: those which can +//! propose and attest to validity of candidates, and those who can only attest +//! to availability. + +use std::collections::HashSet; +use std::collections::hash_map::{HashMap, Entry}; +use std::hash::Hash; +use std::fmt::Debug; + +/// A batch of statements to send out. +pub trait StatementBatch { + /// Get the target authorities of these statements. + fn targets(&self) -> &[V]; + + /// If the batch is empty. + fn is_empty(&self) -> bool; + + /// Push a statement onto the batch. Returns false when the batch is full. + /// + /// This is meant to do work like incrementally serializing the statements + /// into a vector of bytes while making sure the length is below a certain + /// amount. + fn push(&mut self, statement: T) -> bool; +} + +/// Context for the statement table. +pub trait Context { + /// A authority ID + type AuthorityId: Debug + Hash + Eq + Clone; + /// The digest (hash or other unique attribute) of a candidate. + type Digest: Debug + Hash + Eq + Clone; + /// The group ID type + type GroupId: Debug + Hash + Ord + Eq + Clone; + /// A signature type. + type Signature: Debug + Eq + Clone; + /// Candidate type. In practice this will be a candidate receipt. + type Candidate: Debug + Ord + Eq + Clone; + + /// get the digest of a candidate. + fn candidate_digest(candidate: &Self::Candidate) -> Self::Digest; + + /// get the group of a candidate. + fn candidate_group(candidate: &Self::Candidate) -> Self::GroupId; + + /// Whether a authority is a member of a group. + /// Members are meant to submit candidates and vote on validity. + fn is_member_of(&self, authority: &Self::AuthorityId, group: &Self::GroupId) -> bool; + + /// Whether a authority 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, + authority: &Self::AuthorityId, + group: &Self::GroupId, + ) -> bool; + + // requisite number of votes for validity and availability respectively from a group. + fn requisite_votes(&self, group: &Self::GroupId) -> (usize, usize); +} + +/// Statements circulated among peers. +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum Statement { + /// Broadcast by a authority to indicate that this is his candidate for + /// inclusion. + /// + /// Broadcasting two different candidate messages per round is not allowed. + Candidate(C), + /// Broadcast by a authority to attest that the candidate with given digest + /// is valid. + Valid(D), + /// Broadcast by a authority to attest that the auxiliary data for a candidate + /// with given digest is available. + Available(D), + /// Broadcast by a authority to attest that the candidate with given digest + /// is invalid. + Invalid(D), +} + +/// A signed statement. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct SignedStatement { + /// The statement. + pub statement: Statement, + /// The signature. + pub signature: S, + /// The sender. + pub sender: V, +} + +// A unique trace for a class of valid statements issued by a authority. +// +// We keep track of which statements we have received or sent to other authorities +// in order to prevent relaying the same data multiple times. +// +// The signature of the statement is replaced by the authority because the authority +// is unique while signatures are not (at least under common schemes like +// Schnorr or ECDSA). +#[derive(Hash, PartialEq, Eq, Clone)] +enum StatementTrace { + /// The candidate proposed by the authority. + Candidate(V), + /// A validity statement from that authority about the given digest. + Valid(V, D), + /// An invalidity statement from that authority about the given digest. + Invalid(V, D), + /// An availability statement from that authority about the given digest. + Available(V, D), +} + +/// Misbehavior: voting more than one way on candidate validity. +/// +/// Since there are three possible ways to vote, a double vote is possible in +/// three possible combinations (unordered) +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum ValidityDoubleVote { + /// Implicit vote by issuing and explicity voting validity. + IssuedAndValidity((C, S), (D, S)), + /// Implicit vote by issuing and explicitly voting invalidity + IssuedAndInvalidity((C, S), (D, S)), + /// Direct votes for validity and invalidity + ValidityAndInvalidity(D, S, S), +} + +/// Misbehavior: declaring multiple candidates. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct MultipleCandidates { + /// The first candidate seen. + pub first: (C, S), + /// The second candidate seen. + pub second: (C, S), +} + +/// Misbehavior: submitted statement for wrong group. +#[derive(PartialEq, Eq, Debug, Clone)] +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. +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum Misbehavior { + /// Voted invalid and valid on validity. + ValidityDoubleVote(ValidityDoubleVote), + /// Submitted multiple candidates. + MultipleCandidates(MultipleCandidates), + /// Submitted a message withou + UnauthorizedStatement(UnauthorizedStatement), +} + +/// Fancy work-around for a type alias of context-based misbehavior +/// without producing compiler warnings. +pub trait ResolveMisbehavior { + /// The misbehavior type. + type Misbehavior; +} + +impl ResolveMisbehavior for C { + type Misbehavior = Misbehavior; +} + +// kinds of votes for validity +#[derive(Clone, PartialEq, Eq)] +enum ValidityVote { + // implicit validity vote by issuing + Issued(S), + // direct validity vote + Valid(S), + // direct invalidity vote + Invalid(S), +} + +/// A summary of import of a statement. +#[derive(Clone, PartialEq, Eq)] +pub struct Summary { + /// The digest of the candidate referenced. + pub candidate: D, + /// The group that candidate is in. + pub group_id: G, + /// How many validity votes are currently witnessed. + pub validity_votes: usize, + /// How many availability votes are currently witnessed. + pub availability_votes: usize, + /// Whether this has been signalled bad by at least one participant. + pub signalled_bad: bool, +} + +/// Stores votes and data about a candidate. +pub struct CandidateData { + group_id: C::GroupId, + candidate: C::Candidate, + validity_votes: HashMap>, + availability_votes: HashMap, + indicated_bad_by: Vec, +} + +impl CandidateData { + /// whether this has been indicated bad by anyone. + pub fn indicated_bad(&self) -> bool { + !self.indicated_bad_by.is_empty() + } + + // Candidate data can be included in a proposal + // if it has enough validity and availability votes + // and no authorities have called it bad. + fn can_be_included(&self, validity_threshold: usize, availability_threshold: usize) -> bool { + self.indicated_bad_by.is_empty() + && self.validity_votes.len() >= validity_threshold + && self.availability_votes.len() >= availability_threshold + } + + fn summary(&self, digest: C::Digest) -> Summary { + Summary { + candidate: digest, + group_id: self.group_id.clone(), + validity_votes: self.validity_votes.len() - self.indicated_bad_by.len(), + availability_votes: self.availability_votes.len(), + signalled_bad: self.indicated_bad(), + } + } +} + +// authority metadata +struct AuthorityData { + proposal: Option<(C::Digest, C::Signature)>, + known_statements: HashSet>, +} + +impl Default for AuthorityData { + fn default() -> Self { + AuthorityData { + proposal: None, + known_statements: HashSet::default(), + } + } +} + +/// Stores votes +pub struct Table { + authority_data: HashMap>, + detected_misbehavior: HashMap::Misbehavior>, + candidate_votes: HashMap>, +} + +impl Default for Table { + fn default() -> Self { + Table { + authority_data: HashMap::new(), + detected_misbehavior: HashMap::new(), + candidate_votes: HashMap::new(), + } + } +} + +impl Table { + /// Produce a set of proposed candidates. + /// + /// This will be at most one per group, consisting of the + /// best candidate for each group with requisite votes for inclusion. + /// + /// The vector is sorted in ascending order by group id. + pub fn proposed_candidates<'a>(&'a self, context: &C) -> Vec<&'a C::Candidate> { + use std::collections::BTreeMap; + use std::collections::btree_map::Entry as BTreeEntry; + + let mut best_candidates = BTreeMap::new(); + for candidate_data in self.candidate_votes.values() { + let group_id = &candidate_data.group_id; + let (validity_t, availability_t) = context.requisite_votes(group_id); + + if !candidate_data.can_be_included(validity_t, availability_t) { continue } + let candidate = &candidate_data.candidate; + match best_candidates.entry(group_id.clone()) { + BTreeEntry::Occupied(mut occ) => { + let candidate_ref = occ.get_mut(); + if *candidate_ref > candidate { + *candidate_ref = candidate; + } + } + BTreeEntry::Vacant(vacant) => { vacant.insert(candidate); }, + } + } + + best_candidates.values().cloned().collect::>() + } + + /// Whether a candidate can be included. + pub fn candidate_includable(&self, digest: &C::Digest, context: &C) -> bool { + self.candidate_votes.get(digest).map_or(false, |data| { + let (v_threshold, a_threshold) = context.requisite_votes(&data.group_id); + data.can_be_included(v_threshold, a_threshold) + }) + } + + /// Import a signed statement. Signatures should be checked for validity, and the + /// sender should be checked to actually be a authority. + /// + /// This can note the origin of the statement to indicate that he has + /// seen it already. + pub fn import_statement( + &mut self, + context: &C, + statement: SignedStatement, + from: Option + ) -> Option> { + let SignedStatement { statement, signature, sender: signer } = statement; + + let trace = match statement { + Statement::Candidate(_) => StatementTrace::Candidate(signer.clone()), + Statement::Valid(ref d) => StatementTrace::Valid(signer.clone(), d.clone()), + Statement::Invalid(ref d) => StatementTrace::Invalid(signer.clone(), d.clone()), + Statement::Available(ref d) => StatementTrace::Available(signer.clone(), d.clone()), + }; + + let (maybe_misbehavior, maybe_summary) = match statement { + Statement::Candidate(candidate) => self.import_candidate( + context, + signer.clone(), + candidate, + signature + ), + Statement::Valid(digest) => self.validity_vote( + context, + signer.clone(), + digest, + ValidityVote::Valid(signature), + ), + Statement::Invalid(digest) => self.validity_vote( + context, + signer.clone(), + digest, + ValidityVote::Invalid(signature), + ), + Statement::Available(digest) => self.availability_vote( + context, + signer.clone(), + digest, + signature, + ), + }; + + if let Some(misbehavior) = maybe_misbehavior { + // all misbehavior in agreement is provable and actively malicious. + // punishments are not cumulative. + self.detected_misbehavior.insert(signer, misbehavior); + } else { + if let Some(from) = from { + self.note_trace_seen(trace.clone(), from); + } + + self.note_trace_seen(trace, signer); + } + + maybe_summary + } + + /// Get a candidate by digest. + pub fn get_candidate(&self, digest: &C::Digest) -> Option<&C::Candidate> { + self.candidate_votes.get(digest).map(|d| &d.candidate) + } + + /// Access all witnessed misbehavior. + pub fn get_misbehavior(&self) + -> &HashMap::Misbehavior> + { + &self.detected_misbehavior + } + + /// Fill a statement batch and note messages as seen by the targets. + pub fn fill_batch(&mut self, batch: &mut B) + where B: StatementBatch< + C::AuthorityId, + SignedStatement, + > + { + // naively iterate all statements so far, taking any that + // at least one of the targets has not seen. + + // workaround for the fact that it's inconvenient to borrow multiple + // entries out of a hashmap mutably -- we just move them out and + // replace them when we're done. + struct SwappedTargetData<'a, C: 'a + Context> { + authority_data: &'a mut HashMap>, + target_data: Vec<(C::AuthorityId, AuthorityData)>, + } + + impl<'a, C: 'a + Context> Drop for SwappedTargetData<'a, C> { + fn drop(&mut self) { + for (id, data) in self.target_data.drain(..) { + self.authority_data.insert(id, data); + } + } + } + + // pre-fetch authority data for all the targets. + let mut target_data = { + let authority_data = &mut self.authority_data; + let mut target_data = Vec::with_capacity(batch.targets().len()); + for target in batch.targets() { + let active_data = match authority_data.get_mut(target) { + None => Default::default(), + Some(x) => ::std::mem::replace(x, Default::default()), + }; + + target_data.push((target.clone(), active_data)); + } + + SwappedTargetData { + authority_data, + target_data + } + }; + + let target_data = &mut target_data.target_data; + + macro_rules! attempt_send { + ($trace:expr, sender=$sender:expr, sig=$sig:expr, statement=$statement:expr) => {{ + let trace = $trace; + let can_send = target_data.iter() + .any(|t| !t.1.known_statements.contains(&trace)); + + if can_send { + let statement = SignedStatement { + statement: $statement, + signature: $sig, + sender: $sender, + }; + + if batch.push(statement) { + for target in target_data.iter_mut() { + target.1.known_statements.insert(trace.clone()); + } + } else { + return; + } + } + }} + } + + // reconstruct statements for anything whose trace passes the filter. + for (digest, candidate) in self.candidate_votes.iter() { + let issuance_iter = candidate.validity_votes.iter() + .filter(|&(_, x)| if let ValidityVote::Issued(_) = *x { true } else { false }); + + let validity_iter = candidate.validity_votes.iter() + .filter(|&(_, x)| if let ValidityVote::Issued(_) = *x { false } else { true }); + + // send issuance statements before votes. + for (sender, vote) in issuance_iter.chain(validity_iter) { + match *vote { + ValidityVote::Issued(ref sig) => { + attempt_send!( + StatementTrace::Candidate(sender.clone()), + sender = sender.clone(), + sig = sig.clone(), + statement = Statement::Candidate(candidate.candidate.clone()) + ) + } + ValidityVote::Valid(ref sig) => { + attempt_send!( + StatementTrace::Valid(sender.clone(), digest.clone()), + sender = sender.clone(), + sig = sig.clone(), + statement = Statement::Valid(digest.clone()) + ) + } + ValidityVote::Invalid(ref sig) => { + attempt_send!( + StatementTrace::Invalid(sender.clone(), digest.clone()), + sender = sender.clone(), + sig = sig.clone(), + statement = Statement::Invalid(digest.clone()) + ) + } + } + }; + + + // and lastly send availability. + for (sender, sig) in candidate.availability_votes.iter() { + attempt_send!( + StatementTrace::Available(sender.clone(), digest.clone()), + sender = sender.clone(), + sig = sig.clone(), + statement = Statement::Available(digest.clone()) + ) + } + } + + } + + fn note_trace_seen(&mut self, trace: StatementTrace, known_by: C::AuthorityId) { + self.authority_data.entry(known_by).or_insert_with(|| AuthorityData { + proposal: None, + known_statements: HashSet::default(), + }).known_statements.insert(trace); + } + + fn import_candidate( + &mut self, + context: &C, + from: C::AuthorityId, + candidate: C::Candidate, + signature: C::Signature, + ) -> (Option<::Misbehavior>, Option>) { + let group = C::candidate_group(&candidate); + if !context.is_member_of(&from, &group) { + return ( + Some(Misbehavior::UnauthorizedStatement(UnauthorizedStatement { + statement: SignedStatement { + signature, + statement: Statement::Candidate(candidate), + sender: from, + }, + })), + None, + ); + } + + // check that authority hasn't already specified another candidate. + let digest = C::candidate_digest(&candidate); + + let new_proposal = match self.authority_data.entry(from.clone()) { + Entry::Occupied(mut occ) => { + // if digest is different, fetch candidate and + // note misbehavior. + let existing = occ.get_mut(); + + if let Some((ref old_digest, ref old_sig)) = existing.proposal { + if old_digest != &digest { + const EXISTENCE_PROOF: &str = + "when proposal first received from authority, candidate \ + votes entry is created. proposal here is `Some`, therefore \ + candidate votes entry exists; qed"; + + let old_candidate = self.candidate_votes.get(old_digest) + .expect(EXISTENCE_PROOF) + .candidate + .clone(); + + return ( + Some(Misbehavior::MultipleCandidates(MultipleCandidates { + first: (old_candidate, old_sig.clone()), + second: (candidate, signature.clone()), + })), + None, + ); + } + + false + } else { + existing.proposal = Some((digest.clone(), signature.clone())); + true + } + } + Entry::Vacant(vacant) => { + vacant.insert(AuthorityData { + proposal: Some((digest.clone(), signature.clone())), + known_statements: HashSet::new(), + }); + true + } + }; + + // NOTE: altering this code may affect the existence proof above. ensure it remains + // valid. + if new_proposal { + self.candidate_votes.entry(digest.clone()).or_insert_with(move || CandidateData { + group_id: group, + candidate: candidate, + validity_votes: HashMap::new(), + availability_votes: HashMap::new(), + indicated_bad_by: Vec::new(), + }); + } + + self.validity_vote( + context, + from, + digest, + ValidityVote::Issued(signature), + ) + } + + fn validity_vote( + &mut self, + context: &C, + from: C::AuthorityId, + digest: C::Digest, + vote: ValidityVote, + ) -> (Option<::Misbehavior>, Option>) { + let votes = match self.candidate_votes.get_mut(&digest) { + None => return (None, None), // TODO: queue up but don't get DoS'ed + Some(votes) => votes, + }; + + // check that this authority actually can vote in this group. + if !context.is_member_of(&from, &votes.group_id) { + let (sig, valid) = match vote { + ValidityVote::Valid(s) => (s, true), + ValidityVote::Invalid(s) => (s, false), + ValidityVote::Issued(_) => + panic!("implicit issuance vote only cast from `import_candidate` after \ + checking group membership of issuer; qed"), + }; + + return ( + Some(Misbehavior::UnauthorizedStatement(UnauthorizedStatement { + statement: SignedStatement { + signature: sig, + sender: from, + statement: if valid { + Statement::Valid(digest) + } else { + Statement::Invalid(digest) + } + } + })), + None, + ); + } + + // check for double votes. + match votes.validity_votes.entry(from.clone()) { + Entry::Occupied(occ) => { + if occ.get() != &vote { + let double_vote_proof = match (occ.get().clone(), vote) { + (ValidityVote::Issued(iss), ValidityVote::Valid(good)) | + (ValidityVote::Valid(good), ValidityVote::Issued(iss)) => + ValidityDoubleVote::IssuedAndValidity((votes.candidate.clone(), iss), (digest, good)), + (ValidityVote::Issued(iss), ValidityVote::Invalid(bad)) | + (ValidityVote::Invalid(bad), ValidityVote::Issued(iss)) => + ValidityDoubleVote::IssuedAndInvalidity((votes.candidate.clone(), iss), (digest, bad)), + (ValidityVote::Valid(good), ValidityVote::Invalid(bad)) | + (ValidityVote::Invalid(bad), ValidityVote::Valid(good)) => + ValidityDoubleVote::ValidityAndInvalidity(digest, good, bad), + _ => { + // this would occur if two different but valid signatures + // on the same kind of vote occurred. + return (None, None); + } + }; + + return ( + Some(Misbehavior::ValidityDoubleVote(double_vote_proof)), + None, + ) + } + + return (None, None); + } + Entry::Vacant(vacant) => { + if let ValidityVote::Invalid(_) = vote { + votes.indicated_bad_by.push(from); + } + + vacant.insert(vote); + } + } + + (None, Some(votes.summary(digest))) + } + + fn availability_vote( + &mut self, + context: &C, + from: C::AuthorityId, + digest: C::Digest, + signature: C::Signature, + ) -> (Option<::Misbehavior>, Option>) { + let votes = match self.candidate_votes.get_mut(&digest) { + None => return (None, None), // TODO: queue up but don't get DoS'ed + Some(votes) => votes, + }; + + // check that this authority actually can vote in this group. + if !context.is_availability_guarantor_of(&from, &votes.group_id) { + return ( + Some(Misbehavior::UnauthorizedStatement(UnauthorizedStatement { + statement: SignedStatement { + signature: signature.clone(), + statement: Statement::Available(digest), + sender: from, + } + })), + None + ); + } + + votes.availability_votes.insert(from, signature); + (None, Some(votes.summary(digest))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[derive(Debug, Clone)] + struct VecBatch { + pub max_len: usize, + pub targets: Vec, + pub items: Vec, + } + + impl ::StatementBatch for VecBatch { + fn targets(&self) -> &[V] { &self.targets } + fn is_empty(&self) -> bool { self.items.is_empty() } + fn push(&mut self, item: T) -> bool { + if self.items.len() == self.max_len { + false + } else { + self.items.push(item); + true + } + } + } + + fn create() -> Table { + Table { + authority_data: HashMap::default(), + detected_misbehavior: HashMap::default(), + candidate_votes: HashMap::default(), + } + } + + #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] + struct AuthorityId(usize); + + #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)] + struct GroupId(usize); + + // group, body + #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)] + struct Candidate(usize, usize); + + #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] + struct Signature(usize); + + #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] + struct Digest(usize); + + #[derive(Debug, PartialEq, Eq)] + struct TestContext { + // v -> (validity, availability) + authorities: HashMap + } + + impl Context for TestContext { + type AuthorityId = AuthorityId; + type Digest = Digest; + type Candidate = Candidate; + type GroupId = GroupId; + type Signature = Signature; + + fn candidate_digest(candidate: &Candidate) -> Digest { + Digest(candidate.1) + } + + fn candidate_group(candidate: &Candidate) -> GroupId { + GroupId(candidate.0) + } + + fn is_member_of( + &self, + authority: &AuthorityId, + group: &GroupId + ) -> bool { + self.authorities.get(authority).map(|v| &v.0 == group).unwrap_or(false) + } + + fn is_availability_guarantor_of( + &self, + authority: &AuthorityId, + group: &GroupId + ) -> bool { + self.authorities.get(authority).map(|v| &v.1 == group).unwrap_or(false) + } + + fn requisite_votes(&self, _id: &GroupId) -> (usize, usize) { + (6, 34) + } + } + + #[test] + fn submitting_two_candidates_is_misbehavior() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); + map + } + }; + + let mut table = create(); + let statement_a = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + + let statement_b = SignedStatement { + statement: Statement::Candidate(Candidate(2, 999)), + signature: Signature(1), + sender: AuthorityId(1), + }; + + table.import_statement(&context, statement_a, None); + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); + + table.import_statement(&context, statement_b, None); + assert_eq!( + table.detected_misbehavior.get(&AuthorityId(1)).unwrap(), + &Misbehavior::MultipleCandidates(MultipleCandidates { + first: (Candidate(2, 100), Signature(1)), + second: (Candidate(2, 999), Signature(1)), + }) + ); + } + + #[test] + fn submitting_candidate_from_wrong_group_is_misbehavior() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + map.insert(AuthorityId(1), (GroupId(3), GroupId(455))); + map + } + }; + + let mut table = create(); + let statement = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + + table.import_statement(&context, statement, None); + + assert_eq!( + table.detected_misbehavior.get(&AuthorityId(1)).unwrap(), + &Misbehavior::UnauthorizedStatement(UnauthorizedStatement { + statement: SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }, + }) + ); + } + + #[test] + fn unauthorized_votes() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); + map.insert(AuthorityId(2), (GroupId(3), GroupId(222))); + map + } + }; + + let mut table = create(); + + let candidate_a = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + let candidate_a_digest = Digest(100); + + let candidate_b = SignedStatement { + statement: Statement::Candidate(Candidate(3, 987)), + signature: Signature(2), + sender: AuthorityId(2), + }; + let candidate_b_digest = Digest(987); + + table.import_statement(&context, candidate_a, None); + table.import_statement(&context, candidate_b, None); + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2))); + + // authority 1 votes for availability on 2's candidate. + let bad_availability_vote = SignedStatement { + statement: Statement::Available(candidate_b_digest.clone()), + signature: Signature(1), + sender: AuthorityId(1), + }; + table.import_statement(&context, bad_availability_vote, None); + + assert_eq!( + table.detected_misbehavior.get(&AuthorityId(1)).unwrap(), + &Misbehavior::UnauthorizedStatement(UnauthorizedStatement { + statement: SignedStatement { + statement: Statement::Available(candidate_b_digest), + signature: Signature(1), + sender: AuthorityId(1), + }, + }) + ); + + // authority 2 votes for validity on 1's candidate. + let bad_validity_vote = SignedStatement { + statement: Statement::Valid(candidate_a_digest.clone()), + signature: Signature(2), + sender: AuthorityId(2), + }; + table.import_statement(&context, bad_validity_vote, None); + + assert_eq!( + table.detected_misbehavior.get(&AuthorityId(2)).unwrap(), + &Misbehavior::UnauthorizedStatement(UnauthorizedStatement { + statement: SignedStatement { + statement: Statement::Valid(candidate_a_digest), + signature: Signature(2), + sender: AuthorityId(2), + }, + }) + ); + } + + #[test] + fn validity_double_vote_is_misbehavior() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); + map.insert(AuthorityId(2), (GroupId(2), GroupId(246))); + map + } + }; + + let mut table = create(); + let statement = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + let candidate_digest = Digest(100); + + table.import_statement(&context, statement, None); + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); + + let valid_statement = SignedStatement { + statement: Statement::Valid(candidate_digest.clone()), + signature: Signature(2), + sender: AuthorityId(2), + }; + + let invalid_statement = SignedStatement { + statement: Statement::Invalid(candidate_digest.clone()), + signature: Signature(2), + sender: AuthorityId(2), + }; + + table.import_statement(&context, valid_statement, None); + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2))); + + table.import_statement(&context, invalid_statement, None); + + assert_eq!( + table.detected_misbehavior.get(&AuthorityId(2)).unwrap(), + &Misbehavior::ValidityDoubleVote(ValidityDoubleVote::ValidityAndInvalidity( + candidate_digest, + Signature(2), + Signature(2), + )) + ); + } + + #[test] + fn issue_and_vote_is_misbehavior() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); + map + } + }; + + let mut table = create(); + let statement = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + let candidate_digest = Digest(100); + + table.import_statement(&context, statement, None); + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); + + let extra_vote = SignedStatement { + statement: Statement::Valid(candidate_digest.clone()), + signature: Signature(1), + sender: AuthorityId(1), + }; + + table.import_statement(&context, extra_vote, None); + assert_eq!( + table.detected_misbehavior.get(&AuthorityId(1)).unwrap(), + &Misbehavior::ValidityDoubleVote(ValidityDoubleVote::IssuedAndValidity( + (Candidate(2, 100), Signature(1)), + (Digest(100), Signature(1)), + )) + ); + } + + #[test] + fn candidate_can_be_included() { + let validity_threshold = 6; + let availability_threshold = 34; + + let mut candidate = CandidateData:: { + group_id: GroupId(4), + candidate: Candidate(4, 12345), + validity_votes: HashMap::new(), + availability_votes: HashMap::new(), + indicated_bad_by: Vec::new(), + }; + + assert!(!candidate.can_be_included(validity_threshold, availability_threshold)); + + for i in 0..validity_threshold { + candidate.validity_votes.insert(AuthorityId(i + 100), ValidityVote::Valid(Signature(i + 100))); + } + + assert!(!candidate.can_be_included(validity_threshold, availability_threshold)); + + for i in 0..availability_threshold { + candidate.availability_votes.insert(AuthorityId(i + 255), Signature(i + 255)); + } + + assert!(candidate.can_be_included(validity_threshold, availability_threshold)); + + candidate.indicated_bad_by.push(AuthorityId(1024)); + + assert!(!candidate.can_be_included(validity_threshold, availability_threshold)); + } + + #[test] + fn candidate_import_gives_summary() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); + map + } + }; + + let mut table = create(); + let statement = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + + let summary = table.import_statement(&context, statement, None) + .expect("candidate import to give summary"); + + assert_eq!(summary.candidate, Digest(100)); + assert_eq!(summary.group_id, GroupId(2)); + assert_eq!(summary.validity_votes, 1); + assert_eq!(summary.availability_votes, 0); + } + + #[test] + fn candidate_vote_gives_summary() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); + map.insert(AuthorityId(2), (GroupId(2), GroupId(455))); + map + } + }; + + let mut table = create(); + let statement = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + let candidate_digest = Digest(100); + + table.import_statement(&context, statement, None); + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); + + let vote = SignedStatement { + statement: Statement::Valid(candidate_digest.clone()), + signature: Signature(2), + sender: AuthorityId(2), + }; + + let summary = table.import_statement(&context, vote, None) + .expect("candidate vote to give summary"); + + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2))); + + assert_eq!(summary.candidate, Digest(100)); + assert_eq!(summary.group_id, GroupId(2)); + assert_eq!(summary.validity_votes, 2); + assert_eq!(summary.availability_votes, 0); + } + + #[test] + fn availability_vote_gives_summary() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); + map.insert(AuthorityId(2), (GroupId(5), GroupId(2))); + map + } + }; + + let mut table = create(); + let statement = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + let candidate_digest = Digest(100); + + table.import_statement(&context, statement, None); + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); + + let vote = SignedStatement { + statement: Statement::Available(candidate_digest.clone()), + signature: Signature(2), + sender: AuthorityId(2), + }; + + let summary = table.import_statement(&context, vote, None) + .expect("candidate vote to give summary"); + + assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2))); + + assert_eq!(summary.candidate, Digest(100)); + assert_eq!(summary.group_id, GroupId(2)); + assert_eq!(summary.validity_votes, 1); + assert_eq!(summary.availability_votes, 1); + } + + #[test] + fn filling_batch_sets_known_flag() { + let context = TestContext { + authorities: { + let mut map = HashMap::new(); + for i in 1..10 { + map.insert(AuthorityId(i), (GroupId(2), GroupId(400 + i))); + } + map + } + }; + + let mut table = create(); + let statement = SignedStatement { + statement: Statement::Candidate(Candidate(2, 100)), + signature: Signature(1), + sender: AuthorityId(1), + }; + + table.import_statement(&context, statement, None); + + for i in 2..10 { + let statement = SignedStatement { + statement: Statement::Valid(Digest(100)), + signature: Signature(i), + sender: AuthorityId(i), + }; + + table.import_statement(&context, statement, None); + } + + let mut batch = VecBatch { + max_len: 5, + targets: (1..10).map(AuthorityId).collect(), + items: Vec::new(), + }; + + // 9 statements in the table, each seen by one. + table.fill_batch(&mut batch); + assert_eq!(batch.items.len(), 5); + + // 9 statements in the table, 5 of which seen by all targets. + batch.items.clear(); + table.fill_batch(&mut batch); + assert_eq!(batch.items.len(), 4); + + batch.items.clear(); + table.fill_batch(&mut batch); + assert!(batch.items.is_empty()); + } +} diff --git a/polkadot/statement-table/src/lib.rs b/polkadot/statement-table/src/lib.rs index be184f0c6ea3d..86c2ec1952afe 100644 --- a/polkadot/statement-table/src/lib.rs +++ b/polkadot/statement-table/src/lib.rs @@ -1,16 +1,3 @@ -// 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 . @@ -27,1198 +14,100 @@ //! propose and attest to validity of candidates, and those who can only attest //! to availability. -use std::collections::HashSet; -use std::collections::hash_map::{HashMap, Entry}; -use std::hash::Hash; -use std::fmt::Debug; +extern crate substrate_primitives; +extern crate polkadot_primitives as primitives; -/// A batch of statements to send out. -pub trait StatementBatch { - /// Get the target authorities of these statements. - fn targets(&self) -> &[V]; +pub mod generic; - /// If the batch is empty. - fn is_empty(&self) -> bool; +pub use generic::Table; - /// Push a statement onto the batch. Returns false when the batch is full. - /// - /// This is meant to do work like incrementally serializing the statements - /// into a vector of bytes while making sure the length is below a certain - /// amount. - fn push(&mut self, statement: T) -> bool; -} +use primitives::parachain::{Id, CandidateReceipt}; +use primitives::{SessionKey, Hash, Signature}; -/// Context for the statement table. -pub trait Context { - /// A authority ID - type AuthorityId: Debug + Hash + Eq + Clone; - /// The digest (hash or other unique attribute) of a candidate. - type Digest: Debug + Hash + Eq + Clone; - /// The group ID type - type GroupId: Debug + Hash + Ord + Eq + Clone; - /// A signature type. - type Signature: Debug + Eq + Clone; - /// Candidate type. In practice this will be a candidate receipt. - type Candidate: Debug + Ord + Eq + Clone; +/// Statements about candidates on the network. +pub type Statement = generic::Statement; - /// get the digest of a candidate. - fn candidate_digest(candidate: &Self::Candidate) -> Self::Digest; +/// Signed statements about candidates. +pub type SignedStatement = generic::SignedStatement; + +/// Kinds of misbehavior, along with proof. +pub type Misbehavior = generic::Misbehavior; + +/// A summary of import of a statement. +pub type Summary = generic::Summary; - /// get the group of a candidate. - fn candidate_group(candidate: &Self::Candidate) -> Self::GroupId; + +/// Context necessary to construct a table. +pub trait Context { + /// get the digest of a candidate. + // TODO: remove this when hashing logic finalized. + fn candidate_digest(candidate: &CandidateReceipt) -> Hash; /// Whether a authority is a member of a group. /// Members are meant to submit candidates and vote on validity. - fn is_member_of(&self, authority: &Self::AuthorityId, group: &Self::GroupId) -> bool; + fn is_member_of(&self, authority: &SessionKey, group: &Id) -> bool; /// Whether a authority 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, - authority: &Self::AuthorityId, - group: &Self::GroupId, + authority: &SessionKey, + group: &Id, ) -> bool; // requisite number of votes for validity and availability respectively from a group. - fn requisite_votes(&self, group: &Self::GroupId) -> (usize, usize); -} - -/// Statements circulated among peers. -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum Statement { - /// Broadcast by a authority to indicate that this is his candidate for - /// inclusion. - /// - /// Broadcasting two different candidate messages per round is not allowed. - Candidate(C), - /// Broadcast by a authority to attest that the candidate with given digest - /// is valid. - Valid(D), - /// Broadcast by a authority to attest that the auxiliary data for a candidate - /// with given digest is available. - Available(D), - /// Broadcast by a authority to attest that the candidate with given digest - /// is invalid. - Invalid(D), -} - -/// A signed statement. -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct SignedStatement { - /// The statement. - pub statement: Statement, - /// The signature. - pub signature: S, - /// The sender. - pub sender: V, -} - -// A unique trace for a class of valid statements issued by a authority. -// -// We keep track of which statements we have received or sent to other authorities -// in order to prevent relaying the same data multiple times. -// -// The signature of the statement is replaced by the authority because the authority -// is unique while signatures are not (at least under common schemes like -// Schnorr or ECDSA). -#[derive(Hash, PartialEq, Eq, Clone)] -enum StatementTrace { - /// The candidate proposed by the authority. - Candidate(V), - /// A validity statement from that authority about the given digest. - Valid(V, D), - /// An invalidity statement from that authority about the given digest. - Invalid(V, D), - /// An availability statement from that authority about the given digest. - Available(V, D), -} - -/// Misbehavior: voting more than one way on candidate validity. -/// -/// Since there are three possible ways to vote, a double vote is possible in -/// three possible combinations (unordered) -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum ValidityDoubleVote { - /// Implicit vote by issuing and explicity voting validity. - IssuedAndValidity((C, S), (D, S)), - /// Implicit vote by issuing and explicitly voting invalidity - IssuedAndInvalidity((C, S), (D, S)), - /// Direct votes for validity and invalidity - ValidityAndInvalidity(D, S, S), -} - -/// Misbehavior: declaring multiple candidates. -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct MultipleCandidates { - /// The first candidate seen. - pub first: (C, S), - /// The second candidate seen. - pub second: (C, S), -} - -/// Misbehavior: submitted statement for wrong group. -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct UnauthorizedStatement { - /// A signed statement which was submitted without proper authority. - pub statement: SignedStatement, + fn requisite_votes(&self, group: &Id) -> (usize, usize); } -/// Different kinds of misbehavior. All of these kinds of malicious misbehavior -/// are easily provable and extremely disincentivized. -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum Misbehavior { - /// Voted invalid and valid on validity. - ValidityDoubleVote(ValidityDoubleVote), - /// Submitted multiple candidates. - MultipleCandidates(MultipleCandidates), - /// Submitted a message withou - UnauthorizedStatement(UnauthorizedStatement), -} - -/// Fancy work-around for a type alias of context-based misbehavior -/// without producing compiler warnings. -pub trait ResolveMisbehavior { - /// The misbehavior type. - type Misbehavior; -} - -impl ResolveMisbehavior for C { - type Misbehavior = Misbehavior; -} - -// kinds of votes for validity -#[derive(Clone, PartialEq, Eq)] -enum ValidityVote { - // implicit validity vote by issuing - Issued(S), - // direct validity vote - Valid(S), - // direct invalidity vote - Invalid(S), -} +impl generic::Context for C { + type AuthorityId = SessionKey; + type Digest = Hash; + type GroupId = Id; + type Signature = Signature; + type Candidate = CandidateReceipt; -/// A summary of import of a statement. -#[derive(Clone, PartialEq, Eq)] -pub struct Summary { - /// The digest of the candidate referenced. - pub candidate: D, - /// The group that candidate is in. - pub group_id: G, - /// How many validity votes are currently witnessed. - pub validity_votes: usize, - /// How many availability votes are currently witnessed. - pub availability_votes: usize, - /// Whether this has been signalled bad by at least one participant. - pub signalled_bad: bool, -} - -/// Stores votes and data about a candidate. -pub struct CandidateData { - group_id: C::GroupId, - candidate: C::Candidate, - validity_votes: HashMap>, - availability_votes: HashMap, - indicated_bad_by: Vec, -} - -impl CandidateData { - /// whether this has been indicated bad by anyone. - pub fn indicated_bad(&self) -> bool { - !self.indicated_bad_by.is_empty() + fn candidate_digest(candidate: &CandidateReceipt) -> Hash { + ::candidate_digest(candidate) } - // Candidate data can be included in a proposal - // if it has enough validity and availability votes - // and no authorities have called it bad. - fn can_be_included(&self, validity_threshold: usize, availability_threshold: usize) -> bool { - self.indicated_bad_by.is_empty() - && self.validity_votes.len() >= validity_threshold - && self.availability_votes.len() >= availability_threshold + fn candidate_group(candidate: &CandidateReceipt) -> Id { + candidate.parachain_index.clone() } - fn summary(&self, digest: C::Digest) -> Summary { - Summary { - candidate: digest, - group_id: self.group_id.clone(), - validity_votes: self.validity_votes.len() - self.indicated_bad_by.len(), - availability_votes: self.availability_votes.len(), - signalled_bad: self.indicated_bad(), - } + fn is_member_of(&self, authority: &SessionKey, group: &Id) -> bool { + Context::is_member_of(self, authority, group) } -} -// authority metadata -struct AuthorityData { - proposal: Option<(C::Digest, C::Signature)>, - known_statements: HashSet>, -} - -impl Default for AuthorityData { - fn default() -> Self { - AuthorityData { - proposal: None, - known_statements: HashSet::default(), - } + fn is_availability_guarantor_of(&self, authority: &SessionKey, group: &Id) -> bool { + Context::is_availability_guarantor_of(self, authority, group) } -} - -/// Stores votes -pub struct Table { - authority_data: HashMap>, - detected_misbehavior: HashMap::Misbehavior>, - candidate_votes: HashMap>, -} -impl Default for Table { - fn default() -> Self { - Table { - authority_data: HashMap::new(), - detected_misbehavior: HashMap::new(), - candidate_votes: HashMap::new(), - } + fn requisite_votes(&self, group: &Id) -> (usize, usize) { + Context::requisite_votes(self, group) } } -impl Table { - /// Produce a set of proposed candidates. - /// - /// This will be at most one per group, consisting of the - /// best candidate for each group with requisite votes for inclusion. - /// - /// The vector is sorted in ascending order by group id. - pub fn proposed_candidates<'a>(&'a self, context: &C) -> Vec<&'a C::Candidate> { - use std::collections::BTreeMap; - use std::collections::btree_map::Entry as BTreeEntry; - - let mut best_candidates = BTreeMap::new(); - for candidate_data in self.candidate_votes.values() { - let group_id = &candidate_data.group_id; - let (validity_t, availability_t) = context.requisite_votes(group_id); - - if !candidate_data.can_be_included(validity_t, availability_t) { continue } - let candidate = &candidate_data.candidate; - match best_candidates.entry(group_id.clone()) { - BTreeEntry::Occupied(mut occ) => { - let candidate_ref = occ.get_mut(); - if *candidate_ref > candidate { - *candidate_ref = candidate; - } - } - BTreeEntry::Vacant(vacant) => { vacant.insert(candidate); }, - } - } - - best_candidates.values().cloned().collect::>() - } +/// A batch of statements to send out. +pub trait StatementBatch { + /// Get the target authorities of these statements. + fn targets(&self) -> &[SessionKey]; - /// Whether a candidate can be included. - pub fn candidate_includable(&self, digest: &C::Digest, context: &C) -> bool { - self.candidate_votes.get(digest).map_or(false, |data| { - let (v_threshold, a_threshold) = context.requisite_votes(&data.group_id); - data.can_be_included(v_threshold, a_threshold) - }) - } + /// If the batch is empty. + fn is_empty(&self) -> bool; - /// Import a signed statement. Signatures should be checked for validity, and the - /// sender should be checked to actually be a authority. + /// Push a statement onto the batch. Returns false when the batch is full. /// - /// This can note the origin of the statement to indicate that he has - /// seen it already. - pub fn import_statement( - &mut self, - context: &C, - statement: SignedStatement, - from: Option - ) -> Option> { - let SignedStatement { statement, signature, sender: signer } = statement; - - let trace = match statement { - Statement::Candidate(_) => StatementTrace::Candidate(signer.clone()), - Statement::Valid(ref d) => StatementTrace::Valid(signer.clone(), d.clone()), - Statement::Invalid(ref d) => StatementTrace::Invalid(signer.clone(), d.clone()), - Statement::Available(ref d) => StatementTrace::Available(signer.clone(), d.clone()), - }; - - let (maybe_misbehavior, maybe_summary) = match statement { - Statement::Candidate(candidate) => self.import_candidate( - context, - signer.clone(), - candidate, - signature - ), - Statement::Valid(digest) => self.validity_vote( - context, - signer.clone(), - digest, - ValidityVote::Valid(signature), - ), - Statement::Invalid(digest) => self.validity_vote( - context, - signer.clone(), - digest, - ValidityVote::Invalid(signature), - ), - Statement::Available(digest) => self.availability_vote( - context, - signer.clone(), - digest, - signature, - ), - }; - - if let Some(misbehavior) = maybe_misbehavior { - // all misbehavior in agreement is provable and actively malicious. - // punishments are not cumulative. - self.detected_misbehavior.insert(signer, misbehavior); - } else { - if let Some(from) = from { - self.note_trace_seen(trace.clone(), from); - } - - self.note_trace_seen(trace, signer); - } - - maybe_summary - } - - /// Get a candidate by digest. - pub fn get_candidate(&self, digest: &C::Digest) -> Option<&C::Candidate> { - self.candidate_votes.get(digest).map(|d| &d.candidate) - } - - /// Access all witnessed misbehavior. - pub fn get_misbehavior(&self) - -> &HashMap::Misbehavior> - { - &self.detected_misbehavior - } - - /// Fill a statement batch and note messages as seen by the targets. - pub fn fill_batch(&mut self, batch: &mut B) - where B: StatementBatch< - C::AuthorityId, - SignedStatement, - > - { - // naively iterate all statements so far, taking any that - // at least one of the targets has not seen. - - // workaround for the fact that it's inconvenient to borrow multiple - // entries out of a hashmap mutably -- we just move them out and - // replace them when we're done. - struct SwappedTargetData<'a, C: 'a + Context> { - authority_data: &'a mut HashMap>, - target_data: Vec<(C::AuthorityId, AuthorityData)>, - } - - impl<'a, C: 'a + Context> Drop for SwappedTargetData<'a, C> { - fn drop(&mut self) { - for (id, data) in self.target_data.drain(..) { - self.authority_data.insert(id, data); - } - } - } - - // pre-fetch authority data for all the targets. - let mut target_data = { - let authority_data = &mut self.authority_data; - let mut target_data = Vec::with_capacity(batch.targets().len()); - for target in batch.targets() { - let active_data = match authority_data.get_mut(target) { - None => Default::default(), - Some(x) => ::std::mem::replace(x, Default::default()), - }; - - target_data.push((target.clone(), active_data)); - } - - SwappedTargetData { - authority_data, - target_data - } - }; - - let target_data = &mut target_data.target_data; - - macro_rules! attempt_send { - ($trace:expr, sender=$sender:expr, sig=$sig:expr, statement=$statement:expr) => {{ - let trace = $trace; - let can_send = target_data.iter() - .any(|t| !t.1.known_statements.contains(&trace)); - - if can_send { - let statement = SignedStatement { - statement: $statement, - signature: $sig, - sender: $sender, - }; - - if batch.push(statement) { - for target in target_data.iter_mut() { - target.1.known_statements.insert(trace.clone()); - } - } else { - return; - } - } - }} - } - - // reconstruct statements for anything whose trace passes the filter. - for (digest, candidate) in self.candidate_votes.iter() { - let issuance_iter = candidate.validity_votes.iter() - .filter(|&(_, x)| if let ValidityVote::Issued(_) = *x { true } else { false }); - - let validity_iter = candidate.validity_votes.iter() - .filter(|&(_, x)| if let ValidityVote::Issued(_) = *x { false } else { true }); - - // send issuance statements before votes. - for (sender, vote) in issuance_iter.chain(validity_iter) { - match *vote { - ValidityVote::Issued(ref sig) => { - attempt_send!( - StatementTrace::Candidate(sender.clone()), - sender = sender.clone(), - sig = sig.clone(), - statement = Statement::Candidate(candidate.candidate.clone()) - ) - } - ValidityVote::Valid(ref sig) => { - attempt_send!( - StatementTrace::Valid(sender.clone(), digest.clone()), - sender = sender.clone(), - sig = sig.clone(), - statement = Statement::Valid(digest.clone()) - ) - } - ValidityVote::Invalid(ref sig) => { - attempt_send!( - StatementTrace::Invalid(sender.clone(), digest.clone()), - sender = sender.clone(), - sig = sig.clone(), - statement = Statement::Invalid(digest.clone()) - ) - } - } - }; - - - // and lastly send availability. - for (sender, sig) in candidate.availability_votes.iter() { - attempt_send!( - StatementTrace::Available(sender.clone(), digest.clone()), - sender = sender.clone(), - sig = sig.clone(), - statement = Statement::Available(digest.clone()) - ) - } - } - - } - - fn note_trace_seen(&mut self, trace: StatementTrace, known_by: C::AuthorityId) { - self.authority_data.entry(known_by).or_insert_with(|| AuthorityData { - proposal: None, - known_statements: HashSet::default(), - }).known_statements.insert(trace); - } - - fn import_candidate( - &mut self, - context: &C, - from: C::AuthorityId, - candidate: C::Candidate, - signature: C::Signature, - ) -> (Option<::Misbehavior>, Option>) { - let group = C::candidate_group(&candidate); - if !context.is_member_of(&from, &group) { - return ( - Some(Misbehavior::UnauthorizedStatement(UnauthorizedStatement { - statement: SignedStatement { - signature, - statement: Statement::Candidate(candidate), - sender: from, - }, - })), - None, - ); - } - - // check that authority hasn't already specified another candidate. - let digest = C::candidate_digest(&candidate); - - let new_proposal = match self.authority_data.entry(from.clone()) { - Entry::Occupied(mut occ) => { - // if digest is different, fetch candidate and - // note misbehavior. - let existing = occ.get_mut(); - - if let Some((ref old_digest, ref old_sig)) = existing.proposal { - if old_digest != &digest { - const EXISTENCE_PROOF: &str = - "when proposal first received from authority, candidate \ - votes entry is created. proposal here is `Some`, therefore \ - candidate votes entry exists; qed"; - - let old_candidate = self.candidate_votes.get(old_digest) - .expect(EXISTENCE_PROOF) - .candidate - .clone(); - - return ( - Some(Misbehavior::MultipleCandidates(MultipleCandidates { - first: (old_candidate, old_sig.clone()), - second: (candidate, signature.clone()), - })), - None, - ); - } - - false - } else { - existing.proposal = Some((digest.clone(), signature.clone())); - true - } - } - Entry::Vacant(vacant) => { - vacant.insert(AuthorityData { - proposal: Some((digest.clone(), signature.clone())), - known_statements: HashSet::new(), - }); - true - } - }; - - // NOTE: altering this code may affect the existence proof above. ensure it remains - // valid. - if new_proposal { - self.candidate_votes.entry(digest.clone()).or_insert_with(move || CandidateData { - group_id: group, - candidate: candidate, - validity_votes: HashMap::new(), - availability_votes: HashMap::new(), - indicated_bad_by: Vec::new(), - }); - } - - self.validity_vote( - context, - from, - digest, - ValidityVote::Issued(signature), - ) - } - - fn validity_vote( - &mut self, - context: &C, - from: C::AuthorityId, - digest: C::Digest, - vote: ValidityVote, - ) -> (Option<::Misbehavior>, Option>) { - let votes = match self.candidate_votes.get_mut(&digest) { - None => return (None, None), // TODO: queue up but don't get DoS'ed - Some(votes) => votes, - }; - - // check that this authority actually can vote in this group. - if !context.is_member_of(&from, &votes.group_id) { - let (sig, valid) = match vote { - ValidityVote::Valid(s) => (s, true), - ValidityVote::Invalid(s) => (s, false), - ValidityVote::Issued(_) => - panic!("implicit issuance vote only cast from `import_candidate` after \ - checking group membership of issuer; qed"), - }; - - return ( - Some(Misbehavior::UnauthorizedStatement(UnauthorizedStatement { - statement: SignedStatement { - signature: sig, - sender: from, - statement: if valid { - Statement::Valid(digest) - } else { - Statement::Invalid(digest) - } - } - })), - None, - ); - } - - // check for double votes. - match votes.validity_votes.entry(from.clone()) { - Entry::Occupied(occ) => { - if occ.get() != &vote { - let double_vote_proof = match (occ.get().clone(), vote) { - (ValidityVote::Issued(iss), ValidityVote::Valid(good)) | - (ValidityVote::Valid(good), ValidityVote::Issued(iss)) => - ValidityDoubleVote::IssuedAndValidity((votes.candidate.clone(), iss), (digest, good)), - (ValidityVote::Issued(iss), ValidityVote::Invalid(bad)) | - (ValidityVote::Invalid(bad), ValidityVote::Issued(iss)) => - ValidityDoubleVote::IssuedAndInvalidity((votes.candidate.clone(), iss), (digest, bad)), - (ValidityVote::Valid(good), ValidityVote::Invalid(bad)) | - (ValidityVote::Invalid(bad), ValidityVote::Valid(good)) => - ValidityDoubleVote::ValidityAndInvalidity(digest, good, bad), - _ => { - // this would occur if two different but valid signatures - // on the same kind of vote occurred. - return (None, None); - } - }; - - return ( - Some(Misbehavior::ValidityDoubleVote(double_vote_proof)), - None, - ) - } - - return (None, None); - } - Entry::Vacant(vacant) => { - if let ValidityVote::Invalid(_) = vote { - votes.indicated_bad_by.push(from); - } - - vacant.insert(vote); - } - } - - (None, Some(votes.summary(digest))) - } - - fn availability_vote( - &mut self, - context: &C, - from: C::AuthorityId, - digest: C::Digest, - signature: C::Signature, - ) -> (Option<::Misbehavior>, Option>) { - let votes = match self.candidate_votes.get_mut(&digest) { - None => return (None, None), // TODO: queue up but don't get DoS'ed - Some(votes) => votes, - }; - - // check that this authority actually can vote in this group. - if !context.is_availability_guarantor_of(&from, &votes.group_id) { - return ( - Some(Misbehavior::UnauthorizedStatement(UnauthorizedStatement { - statement: SignedStatement { - signature: signature.clone(), - statement: Statement::Available(digest), - sender: from, - } - })), - None - ); - } - - votes.availability_votes.insert(from, signature); - (None, Some(votes.summary(digest))) - } + /// This is meant to do work like incrementally serializing the statements + /// into a vector of bytes while making sure the length is below a certain + /// amount. + fn push(&mut self, statement: SignedStatement) -> bool; } -#[cfg(test)] -mod tests { - use super::*; - use std::collections::HashMap; - - #[derive(Debug, Clone)] - struct VecBatch { - pub max_len: usize, - pub targets: Vec, - pub items: Vec, - } - - impl ::StatementBatch for VecBatch { - fn targets(&self) -> &[V] { &self.targets } - fn is_empty(&self) -> bool { self.items.is_empty() } - fn push(&mut self, item: T) -> bool { - if self.items.len() == self.max_len { - false - } else { - self.items.push(item); - true - } - } - } - - fn create() -> Table { - Table { - authority_data: HashMap::default(), - detected_misbehavior: HashMap::default(), - candidate_votes: HashMap::default(), - } - } - - #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] - struct AuthorityId(usize); - - #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)] - struct GroupId(usize); - - // group, body - #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)] - struct Candidate(usize, usize); - - #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] - struct Signature(usize); - - #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] - struct Digest(usize); - - #[derive(Debug, PartialEq, Eq)] - struct TestContext { - // v -> (validity, availability) - authorities: HashMap - } - - impl Context for TestContext { - type AuthorityId = AuthorityId; - type Digest = Digest; - type Candidate = Candidate; - type GroupId = GroupId; - type Signature = Signature; - - fn candidate_digest(candidate: &Candidate) -> Digest { - Digest(candidate.1) - } - - fn candidate_group(candidate: &Candidate) -> GroupId { - GroupId(candidate.0) - } - - fn is_member_of( - &self, - authority: &AuthorityId, - group: &GroupId - ) -> bool { - self.authorities.get(authority).map(|v| &v.0 == group).unwrap_or(false) - } - - fn is_availability_guarantor_of( - &self, - authority: &AuthorityId, - group: &GroupId - ) -> bool { - self.authorities.get(authority).map(|v| &v.1 == group).unwrap_or(false) - } - - fn requisite_votes(&self, _id: &GroupId) -> (usize, usize) { - (6, 34) - } - } - - #[test] - fn submitting_two_candidates_is_misbehavior() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); - map - } - }; - - let mut table = create(); - let statement_a = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - - let statement_b = SignedStatement { - statement: Statement::Candidate(Candidate(2, 999)), - signature: Signature(1), - sender: AuthorityId(1), - }; - - table.import_statement(&context, statement_a, None); - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); - - table.import_statement(&context, statement_b, None); - assert_eq!( - table.detected_misbehavior.get(&AuthorityId(1)).unwrap(), - &Misbehavior::MultipleCandidates(MultipleCandidates { - first: (Candidate(2, 100), Signature(1)), - second: (Candidate(2, 999), Signature(1)), - }) - ); - } - - #[test] - fn submitting_candidate_from_wrong_group_is_misbehavior() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - map.insert(AuthorityId(1), (GroupId(3), GroupId(455))); - map - } - }; - - let mut table = create(); - let statement = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - - table.import_statement(&context, statement, None); - - assert_eq!( - table.detected_misbehavior.get(&AuthorityId(1)).unwrap(), - &Misbehavior::UnauthorizedStatement(UnauthorizedStatement { - statement: SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }, - }) - ); - } - - #[test] - fn unauthorized_votes() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); - map.insert(AuthorityId(2), (GroupId(3), GroupId(222))); - map - } - }; - - let mut table = create(); - - let candidate_a = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - let candidate_a_digest = Digest(100); - - let candidate_b = SignedStatement { - statement: Statement::Candidate(Candidate(3, 987)), - signature: Signature(2), - sender: AuthorityId(2), - }; - let candidate_b_digest = Digest(987); - - table.import_statement(&context, candidate_a, None); - table.import_statement(&context, candidate_b, None); - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2))); - - // authority 1 votes for availability on 2's candidate. - let bad_availability_vote = SignedStatement { - statement: Statement::Available(candidate_b_digest.clone()), - signature: Signature(1), - sender: AuthorityId(1), - }; - table.import_statement(&context, bad_availability_vote, None); - - assert_eq!( - table.detected_misbehavior.get(&AuthorityId(1)).unwrap(), - &Misbehavior::UnauthorizedStatement(UnauthorizedStatement { - statement: SignedStatement { - statement: Statement::Available(candidate_b_digest), - signature: Signature(1), - sender: AuthorityId(1), - }, - }) - ); - - // authority 2 votes for validity on 1's candidate. - let bad_validity_vote = SignedStatement { - statement: Statement::Valid(candidate_a_digest.clone()), - signature: Signature(2), - sender: AuthorityId(2), - }; - table.import_statement(&context, bad_validity_vote, None); - - assert_eq!( - table.detected_misbehavior.get(&AuthorityId(2)).unwrap(), - &Misbehavior::UnauthorizedStatement(UnauthorizedStatement { - statement: SignedStatement { - statement: Statement::Valid(candidate_a_digest), - signature: Signature(2), - sender: AuthorityId(2), - }, - }) - ); - } - - #[test] - fn validity_double_vote_is_misbehavior() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); - map.insert(AuthorityId(2), (GroupId(2), GroupId(246))); - map - } - }; - - let mut table = create(); - let statement = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - let candidate_digest = Digest(100); - - table.import_statement(&context, statement, None); - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); - - let valid_statement = SignedStatement { - statement: Statement::Valid(candidate_digest.clone()), - signature: Signature(2), - sender: AuthorityId(2), - }; - - let invalid_statement = SignedStatement { - statement: Statement::Invalid(candidate_digest.clone()), - signature: Signature(2), - sender: AuthorityId(2), - }; - - table.import_statement(&context, valid_statement, None); - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2))); - - table.import_statement(&context, invalid_statement, None); - - assert_eq!( - table.detected_misbehavior.get(&AuthorityId(2)).unwrap(), - &Misbehavior::ValidityDoubleVote(ValidityDoubleVote::ValidityAndInvalidity( - candidate_digest, - Signature(2), - Signature(2), - )) - ); - } - - #[test] - fn issue_and_vote_is_misbehavior() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); - map - } - }; - - let mut table = create(); - let statement = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - let candidate_digest = Digest(100); - - table.import_statement(&context, statement, None); - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); - - let extra_vote = SignedStatement { - statement: Statement::Valid(candidate_digest.clone()), - signature: Signature(1), - sender: AuthorityId(1), - }; - - table.import_statement(&context, extra_vote, None); - assert_eq!( - table.detected_misbehavior.get(&AuthorityId(1)).unwrap(), - &Misbehavior::ValidityDoubleVote(ValidityDoubleVote::IssuedAndValidity( - (Candidate(2, 100), Signature(1)), - (Digest(100), Signature(1)), - )) - ); - } - - #[test] - fn candidate_can_be_included() { - let validity_threshold = 6; - let availability_threshold = 34; - - let mut candidate = CandidateData:: { - group_id: GroupId(4), - candidate: Candidate(4, 12345), - validity_votes: HashMap::new(), - availability_votes: HashMap::new(), - indicated_bad_by: Vec::new(), - }; - - assert!(!candidate.can_be_included(validity_threshold, availability_threshold)); - - for i in 0..validity_threshold { - candidate.validity_votes.insert(AuthorityId(i + 100), ValidityVote::Valid(Signature(i + 100))); - } - - assert!(!candidate.can_be_included(validity_threshold, availability_threshold)); - - for i in 0..availability_threshold { - candidate.availability_votes.insert(AuthorityId(i + 255), Signature(i + 255)); - } - - assert!(candidate.can_be_included(validity_threshold, availability_threshold)); - - candidate.indicated_bad_by.push(AuthorityId(1024)); - - assert!(!candidate.can_be_included(validity_threshold, availability_threshold)); - } - - #[test] - fn candidate_import_gives_summary() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); - map - } - }; - - let mut table = create(); - let statement = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - - let summary = table.import_statement(&context, statement, None) - .expect("candidate import to give summary"); - - assert_eq!(summary.candidate, Digest(100)); - assert_eq!(summary.group_id, GroupId(2)); - assert_eq!(summary.validity_votes, 1); - assert_eq!(summary.availability_votes, 0); - } - - #[test] - fn candidate_vote_gives_summary() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); - map.insert(AuthorityId(2), (GroupId(2), GroupId(455))); - map - } - }; - - let mut table = create(); - let statement = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - let candidate_digest = Digest(100); - - table.import_statement(&context, statement, None); - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); - - let vote = SignedStatement { - statement: Statement::Valid(candidate_digest.clone()), - signature: Signature(2), - sender: AuthorityId(2), - }; - - let summary = table.import_statement(&context, vote, None) - .expect("candidate vote to give summary"); - - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2))); - - assert_eq!(summary.candidate, Digest(100)); - assert_eq!(summary.group_id, GroupId(2)); - assert_eq!(summary.validity_votes, 2); - assert_eq!(summary.availability_votes, 0); - } - - #[test] - fn availability_vote_gives_summary() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - map.insert(AuthorityId(1), (GroupId(2), GroupId(455))); - map.insert(AuthorityId(2), (GroupId(5), GroupId(2))); - map - } - }; - - let mut table = create(); - let statement = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - let candidate_digest = Digest(100); - - table.import_statement(&context, statement, None); - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1))); - - let vote = SignedStatement { - statement: Statement::Available(candidate_digest.clone()), - signature: Signature(2), - sender: AuthorityId(2), - }; - - let summary = table.import_statement(&context, vote, None) - .expect("candidate vote to give summary"); - - assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2))); - - assert_eq!(summary.candidate, Digest(100)); - assert_eq!(summary.group_id, GroupId(2)); - assert_eq!(summary.validity_votes, 1); - assert_eq!(summary.availability_votes, 1); - } - - #[test] - fn filling_batch_sets_known_flag() { - let context = TestContext { - authorities: { - let mut map = HashMap::new(); - for i in 1..10 { - map.insert(AuthorityId(i), (GroupId(2), GroupId(400 + i))); - } - map - } - }; - - let mut table = create(); - let statement = SignedStatement { - statement: Statement::Candidate(Candidate(2, 100)), - signature: Signature(1), - sender: AuthorityId(1), - }; - - table.import_statement(&context, statement, None); - - for i in 2..10 { - let statement = SignedStatement { - statement: Statement::Valid(Digest(100)), - signature: Signature(i), - sender: AuthorityId(i), - }; - - table.import_statement(&context, statement, None); - } - - let mut batch = VecBatch { - max_len: 5, - targets: (1..10).map(AuthorityId).collect(), - items: Vec::new(), - }; - - // 9 statements in the table, each seen by one. - table.fill_batch(&mut batch); - assert_eq!(batch.items.len(), 5); - - // 9 statements in the table, 5 of which seen by all targets. - batch.items.clear(); - table.fill_batch(&mut batch); - assert_eq!(batch.items.len(), 4); - - batch.items.clear(); - table.fill_batch(&mut batch); - assert!(batch.items.is_empty()); +impl generic::StatementBatch for T { + fn targets(&self) -> &[SessionKey] { StatementBatch::targets(self ) } + fn is_empty(&self) -> bool { StatementBatch::is_empty(self) } + fn push(&mut self, statement: SignedStatement) -> bool { + StatementBatch::push(self, statement) } } diff --git a/substrate/runtime-std/with_std.rs b/substrate/runtime-std/with_std.rs index 01212679fc43e..7a7918f8fe316 100644 --- a/substrate/runtime-std/with_std.rs +++ b/substrate/runtime-std/with_std.rs @@ -20,6 +20,7 @@ pub use std::cell; pub use std::boxed; pub use std::slice; pub use std::mem; +pub use std::cmp; pub use std::ops; pub use std::iter; pub use std::ptr; diff --git a/substrate/runtime-std/without_std.rs b/substrate/runtime-std/without_std.rs index 4aff3153ca20e..e089b49b7776c 100644 --- a/substrate/runtime-std/without_std.rs +++ b/substrate/runtime-std/without_std.rs @@ -24,6 +24,7 @@ extern crate pwasm_alloc; pub use alloc::vec; pub use alloc::boxed; pub use alloc::rc; +pub use core::cmp; pub use core::mem; pub use core::slice; pub use core::cell; From 8e2fd3cadef780ddce0a77f3015f6ab3d4c311bb Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 10 Feb 2018 18:47:54 +0100 Subject: [PATCH 03/21] begin BftService --- Cargo.lock | 2 + polkadot/consensus/src/lib.rs | 91 ------------------- substrate/bft/Cargo.toml | 2 + substrate/bft/src/lib.rs | 144 +++++++++++++++--------------- substrate/primitives/src/bft.rs | 54 +++++++++++ substrate/primitives/src/block.rs | 5 ++ 6 files changed, 136 insertions(+), 162 deletions(-) create mode 100644 substrate/primitives/src/bft.rs diff --git a/Cargo.lock b/Cargo.lock index 926b1bee1c185..79e7d7b5e18a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1410,7 +1410,9 @@ version = "0.1.0" dependencies = [ "ed25519 0.1.0", "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", + "substrate-client 0.1.0", "substrate-primitives 0.1.0", + "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/polkadot/consensus/src/lib.rs b/polkadot/consensus/src/lib.rs index 2cf4be5c54715..3e634789cfb7f 100644 --- a/polkadot/consensus/src/lib.rs +++ b/polkadot/consensus/src/lib.rs @@ -371,81 +371,6 @@ impl SharedTable { } } -/// Errors that can occur during agreement. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Error { - IoTerminated, - FaultyTimer, - CannotPropose, -} - -impl From for Error { - fn from(_: bft::InputStreamConcluded) -> Error { - Error::IoTerminated - } -} - -/// Context owned by the BFT future necessary to execute the logic. -pub struct BftContext { - context: C, - table: SharedTable, - timer: Timer, - round_timeout_multiplier: u64, -} - -impl bft::Context for BftContext - where C::Proposal: 'static, -{ - type AuthorityId = C::AuthorityId; - type Digest = C::Digest; - type Signature = C::Signature; - type Candidate = C::Proposal; - type RoundTimeout = Box>; - type CreateProposal = Box>; - - fn local_id(&self) -> Self::AuthorityId { - self.context.local_id() - } - - fn proposal(&self) -> Self::CreateProposal { - Box::new(self.table.get_proposal().map_err(|_| Error::CannotPropose)) - } - - fn candidate_digest(&self, candidate: &Self::Candidate) -> Self::Digest { - C::proposal_digest(candidate) - } - - fn sign_local(&self, message: bft::Message) - -> bft::LocalizedMessage - { - let sender = self.local_id(); - let signature = self.context.sign_bft_message(&message); - bft::LocalizedMessage { - message, - sender, - signature, - } - } - - fn round_proposer(&self, round: usize) -> Self::AuthorityId { - self.context.round_proposer(round) - } - - fn candidate_valid(&self, proposal: &Self::Candidate) -> bool { - self.table.proposal_valid(proposal) - } - - fn begin_round_timeout(&self, round: usize) -> Self::RoundTimeout { - let round = ::std::cmp::min(63, round) as u32; - let timeout = 1u64.checked_shl(round) - .unwrap_or_else(u64::max_value) - .saturating_mul(self.round_timeout_multiplier); - - Box::new(self.timer.sleep(Duration::from_secs(timeout)) - .map_err(|_| Error::FaultyTimer)) - } -} - /// Parameters necessary for agreement. pub struct AgreementParams { @@ -477,22 +402,6 @@ pub trait MessageRecovery { fn check_message(&self, Self::UncheckedMessage) -> Option>; } -/// A batch of statements to send out. -pub trait StatementBatch { - /// Get the target authorities of these statements. - fn targets(&self) -> &[V]; - - /// If the batch is empty. - fn is_empty(&self) -> bool; - - /// Push a statement onto the batch. Returns false when the batch is full. - /// - /// This is meant to do work like incrementally serializing the statements - /// into a vector of bytes while making sure the length is below a certain - /// amount. - fn push(&mut self, statement: T) -> bool; -} - /// Recovered and fully checked messages. pub enum CheckedMessage { /// Messages meant for the BFT agreement logic. diff --git a/substrate/bft/Cargo.toml b/substrate/bft/Cargo.toml index 1e15e098cab80..4332e3354d52e 100644 --- a/substrate/bft/Cargo.toml +++ b/substrate/bft/Cargo.toml @@ -5,5 +5,7 @@ authors = ["Parity Technologies "] [dependencies] futures = "0.1.17" +substrate-client = { path = "../client" } substrate-primitives = { path = "../primitives" } ed25519 = { path = "../ed25519" } +tokio-timer = "0.1.2" diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index 1229f3055e278..2841070a4cc76 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -21,14 +21,18 @@ pub mod generic; #[cfg_attr(test, macro_use)] extern crate futures; +extern crate substrate_client as client; extern crate substrate_primitives as primitives; extern crate ed25519; +extern crate tokio_timer; +use client::Client; use ed25519::Signature; -use primitives::block::HeaderHash; -use primitives::{Block, AuthorityId}; +use primitives::block::{Block, Header, HeaderHash}; +use primitives::AuthorityId; use futures::{Stream, Sink, Future}; +use tokio_timer::Timer; pub use generic::InputStreamConcluded; @@ -56,96 +60,94 @@ pub type Committed = generic::Committed; /// Communication between BFT participants. pub type Communication = generic::Communication; -/// Context necessary for agreement. -pub trait Context { - /// A future that resolves when a round timeout is concluded. - type RoundTimeout: Future; - /// A future that resolves when a proposal is ready. - type CreateProposal: Future; - - /// Get the local authority ID. - fn local_id(&self) -> AuthorityId; - - /// Get the best proposal. - fn proposal(&self) -> Self::CreateProposal; - - /// Get the digest of a candidate. - fn candidate_digest(&self, candidate: &Block) -> HeaderHash; - - /// Sign a message using the local authority ID. - fn sign_local(&self, message: Message) -> LocalizedMessage; +/// Errors that can occur during agreement. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Error { + /// Io streams terminated. + IoTerminated, + /// Timer failed to fire. + FaultyTimer, + /// Unable to propose for some reason. + CannotPropose, +} - /// Get the proposer for a given round of consensus. - fn round_proposer(&self, round: usize) -> AuthorityId; +impl From for Error { + fn from(_: InputStreamConcluded) -> Error { + Error::IoTerminated; + } +} - /// Whether the candidate is valid. - fn candidate_valid(&self, candidate: &Block) -> bool; +/// Logic for a proposer. +/// +/// This will encapsulate creation and evaluation of proposals at a specific +/// block. +pub trait Proposer: Sized { + type CreateProposal: IntoFuture; + + /// Initialize the proposal logic on top of a specific header. + // TODO: provide state context explicitly? + fn init(parent_header: &Header, sign_with: ed25519::Pair) -> Self; + + /// Create a proposal. + fn propose(&self) -> Self::CreateProposal; + /// Evaluate proposal. True means valid. + // TODO: change this to a future. + fn evaluate(&self, proposal: &Block) -> bool; +} - /// Create a round timeout. The context will determine the correct timeout - /// length, and create a future that will resolve when the timeout is - /// concluded. - fn begin_round_timeout(&self, round: usize) -> Self::RoundTimeout; +/// Instance of BFT agreement. +pub struct BftInstance

{ + key: ed25519::Pair, // TODO (now): key changing over time. + authorities: Vec, + parent_hash: HeaderHash, + timer: Timer, + round_timeout_multiplier: u64, + proposer: P, } -impl generic::Context for T { - type Candidate = Block; - type Digest = HeaderHash; +impl generic::Context for BftInstance

{ type AuthorityId = AuthorityId; + type Digest = HeaderHash; type Signature = Signature; - type RoundTimeout = ::RoundTimeout; - type CreateProposal = ::CreateProposal; + type Candidate = Block; + type RoundTimeout = Box>; + type CreateProposal = P::CreateProposal; - fn local_id(&self) -> AuthorityId { Context::local_id(self) } + fn local_id(&self) -> AuthorityId { + self.key.public() + } - fn proposal(&self) -> Self::CreateProposal { Context::proposal(self) } + fn proposal(&self) -> P::CreateProposal { + self.proposer.propose() + } - fn candidate_digest(&self, candidate: &Block) -> HeaderHash { - Context::candidate_digest(self, candidate) + fn candidate_digest(&self, proposal: &Block) -> HeaderHash { + unimplemented!() // TODO: calculate header hash. } fn sign_local(&self, message: Message) -> LocalizedMessage { - Context::sign_local(self, message) + unimplemented!() // TODO: figure out message encoding. } fn round_proposer(&self, round: usize) -> AuthorityId { - Context::round_proposer(self, round) + // repeat blake2_256 on parent hash round + 1 times. + // use as index into authorities vec. + unimplemented!() } - fn candidate_valid(&self, candidate: &Block) -> bool { - Context::candidate_valid(self, candidate) + fn candidate_valid(&self, proposal: &Block) -> bool { + self.proposer.evaluate(proposal) } fn begin_round_timeout(&self, round: usize) -> Self::RoundTimeout { - Context::begin_round_timeout(self, round) + let round = ::std::cmp::min(63, round) as u32; + let timeout = 1u64.checked_shl(round) + .unwrap_or_else(u64::max_value) + .saturating_mul(self.round_timeout_multiplier); + + Box::new(self.timer.sleep(Duration::from_secs(timeout)) + .map_err(|_| Error::FaultyTimer)) } } -/// Attempt to reach BFT agreement on a candidate. -/// -/// `nodes` is the number of nodes in the system. -/// `max_faulty` is the maximum number of faulty nodes. Should be less than -/// 1/3 of `nodes`, otherwise agreement may never be reached. -/// -/// The input stream should never logically conclude. The logic here assumes -/// that messages flushed to the output stream will eventually reach other nodes. -/// -/// Note that it is possible to witness agreement being reached without ever -/// seeing the candidate. Any candidates seen will be checked for validity. -/// -/// Although technically the agreement will always complete (given the eventual -/// delivery of messages), in practice it is possible for this future to -/// conclude without having witnessed the conclusion. -/// In general, this future should be pre-empted by the import of a justification -/// set for this block height. -pub fn agree(context: C, nodes: usize, max_faulty: usize, input: I, output: O) - -> generic::Agreement - where - C: Context, - C::RoundTimeout: Future, - C::CreateProposal: Future, - I: Stream, - O: Sink, - E: From, -{ - generic::agree(context, nodes, max_faulty, input, output) -} +/// Bft service built around diff --git a/substrate/primitives/src/bft.rs b/substrate/primitives/src/bft.rs new file mode 100644 index 0000000000000..6cc1e39b46b9d --- /dev/null +++ b/substrate/primitives/src/bft.rs @@ -0,0 +1,54 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! Message formats for the BFT consensus layer. + +use block::{Block, HeaderHash}; +use codec::{Slicable, Input}; + +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +#[repr(u8)] +enum ActionKind { + Propose = 1, + Prepare = 2, + Commit = 3, + AdvanceRound = 4, +} + +/// Actions which can be taken during the BFT process. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub enum Action { + /// Proposal of a block candidate. + Propose(usize, Block), + /// Preparation to commit for a candidate. + Prepare(usize, HeaderHash), + /// Vote to commit to a candidate. + Commit(usize, HeaderHash), + /// Vote to advance round after inactive primary. + AdvanceRound(usize), +} + +/// Messages exchanged between participants in the BFT consensus. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Message { + /// The parent header hash this action is relative to. + pub parent: HeaderHash, + /// The action being broadcasted. + pub action: Action, +} diff --git a/substrate/primitives/src/block.rs b/substrate/primitives/src/block.rs index 4ac11f9c461f2..4ab0238869541 100644 --- a/substrate/primitives/src/block.rs +++ b/substrate/primitives/src/block.rs @@ -149,6 +149,11 @@ impl Header { digest: Default::default(), } } + + /// Get the blake2-256 hash of this header. + pub fn hash(&self) -> HeaderHash { + hashing::blake2_256(Slicable::to_vec(self).as_slice()) + } } impl Slicable for Header { From 6abfed425715e52dbb6494e82f236337eb17f160 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 12 Feb 2018 14:37:07 +0100 Subject: [PATCH 04/21] primary selection logic --- Cargo.lock | 1 + substrate/bft/Cargo.toml | 1 + substrate/bft/src/lib.rs | 57 ++++++++++++++++++++----- substrate/ed25519/src/lib.rs | 2 +- substrate/primitives/src/bft.rs | 75 +++++++++++++++++++++++++++++++-- substrate/primitives/src/lib.rs | 5 ++- 6 files changed, 123 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c9647e6b7a58..073170c862087 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1391,6 +1391,7 @@ dependencies = [ "ed25519 0.1.0", "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", "substrate-client 0.1.0", + "substrate-codec 0.1.0", "substrate-primitives 0.1.0", "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/substrate/bft/Cargo.toml b/substrate/bft/Cargo.toml index 4332e3354d52e..e92c5bd9290fd 100644 --- a/substrate/bft/Cargo.toml +++ b/substrate/bft/Cargo.toml @@ -6,6 +6,7 @@ authors = ["Parity Technologies "] [dependencies] futures = "0.1.17" substrate-client = { path = "../client" } +substrate-codec = { path = "../codec" } substrate-primitives = { path = "../primitives" } ed25519 = { path = "../ed25519" } tokio-timer = "0.1.2" diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index 2841070a4cc76..2952fc615d477 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -19,19 +19,22 @@ mod accumulator; pub mod generic; -#[cfg_attr(test, macro_use)] -extern crate futures; +extern crate substrate_codec as codec; extern crate substrate_client as client; extern crate substrate_primitives as primitives; extern crate ed25519; extern crate tokio_timer; +#[cfg_attr(test, macro_use)] +extern crate futures; + use client::Client; +use codec::Slicable; use ed25519::Signature; use primitives::block::{Block, Header, HeaderHash}; use primitives::AuthorityId; -use futures::{Stream, Sink, Future}; +use futures::{Stream, Future}; use tokio_timer::Timer; pub use generic::InputStreamConcluded; @@ -73,7 +76,7 @@ pub enum Error { impl From for Error { fn from(_: InputStreamConcluded) -> Error { - Error::IoTerminated; + Error::IoTerminated } } @@ -82,7 +85,7 @@ impl From for Error { /// This will encapsulate creation and evaluation of proposals at a specific /// block. pub trait Proposer: Sized { - type CreateProposal: IntoFuture; + type CreateProposal: Future; /// Initialize the proposal logic on top of a specific header. // TODO: provide state context explicitly? @@ -114,7 +117,7 @@ impl generic::Context for BftInstance

{ type CreateProposal = P::CreateProposal; fn local_id(&self) -> AuthorityId { - self.key.public() + self.key.public().0 } fn proposal(&self) -> P::CreateProposal { @@ -122,17 +125,49 @@ impl generic::Context for BftInstance

{ } fn candidate_digest(&self, proposal: &Block) -> HeaderHash { - unimplemented!() // TODO: calculate header hash. + proposal.header.hash() } fn sign_local(&self, message: Message) -> LocalizedMessage { - unimplemented!() // TODO: figure out message encoding. + use primitives::bft::{Message as PrimitiveMessage, Action as PrimitiveAction}; + + let action = match message.clone() { + ::generic::Message::Propose(r, p) => PrimitiveAction::Propose(r as u32, p), + ::generic::Message::Prepare(r, h) => PrimitiveAction::Prepare(r as u32, h), + ::generic::Message::Commit(r, h) => PrimitiveAction::Commit(r as u32, h), + ::generic::Message::AdvanceRound(r) => PrimitiveAction::AdvanceRound(r as u32), + }; + + let primitive = PrimitiveMessage { + parent: self.parent_hash, + action, + }; + + let to_sign = Slicable::encode(&primitive); + let signature = self.key.sign(&to_sign); + + LocalizedMessage { + message, + signature, + sender: self.key.public().0 + } } fn round_proposer(&self, round: usize) -> AuthorityId { + use primitives::hashing::blake2_256; + // repeat blake2_256 on parent hash round + 1 times. // use as index into authorities vec. - unimplemented!() + // TODO: parent hash is really insecure as a randomness beacon as + // the prior can easily influence the block hash. + let hashed = (0..round + 1).fold(self.parent_hash.0, |a, _| { + blake2_256(&a[..]) + }); + + let index = u32::decode(&mut &hashed[..]) + .expect("there are more than 4 bytes in a 32 byte hash; qed"); + + self.authorities[(index as usize) % self.authorities.len()] } fn candidate_valid(&self, proposal: &Block) -> bool { @@ -140,6 +175,8 @@ impl generic::Context for BftInstance

{ } fn begin_round_timeout(&self, round: usize) -> Self::RoundTimeout { + use std::time::Duration; + let round = ::std::cmp::min(63, round) as u32; let timeout = 1u64.checked_shl(round) .unwrap_or_else(u64::max_value) @@ -149,5 +186,3 @@ impl generic::Context for BftInstance

{ .map_err(|_| Error::FaultyTimer)) } } - -/// Bft service built around diff --git a/substrate/ed25519/src/lib.rs b/substrate/ed25519/src/lib.rs index 287d83e36d981..a55531f5d480e 100644 --- a/substrate/ed25519/src/lib.rs +++ b/substrate/ed25519/src/lib.rs @@ -42,7 +42,7 @@ pub fn verify(sig: &[u8], message: &[u8], public: &[u8]) -> bool { /// A public key. #[derive(PartialEq, Clone, Debug)] -pub struct Public ([u8; 32]); +pub struct Public(pub [u8; 32]); /// A key pair. pub struct Pair(signature::Ed25519KeyPair); diff --git a/substrate/primitives/src/bft.rs b/substrate/primitives/src/bft.rs index 6cc1e39b46b9d..277c4214d5cbd 100644 --- a/substrate/primitives/src/bft.rs +++ b/substrate/primitives/src/bft.rs @@ -34,13 +34,65 @@ enum ActionKind { #[cfg_attr(feature = "std", derive(Debug))] pub enum Action { /// Proposal of a block candidate. - Propose(usize, Block), + Propose(u32, Block), /// Preparation to commit for a candidate. - Prepare(usize, HeaderHash), + Prepare(u32, HeaderHash), /// Vote to commit to a candidate. - Commit(usize, HeaderHash), + Commit(u32, HeaderHash), /// Vote to advance round after inactive primary. - AdvanceRound(usize), + AdvanceRound(u32), +} + +impl Slicable for Action { + fn encode(&self) -> Vec { + let mut v = Vec::new(); + match *self { + Action::Propose(ref round, ref block) => { + v.push(ActionKind::Propose as u8); + round.using_encoded(|s| v.extend(s)); + block.using_encoded(|s| v.extend(s)); + } + Action::Prepare(ref round, ref hash) => { + v.push(ActionKind::Prepare as u8); + round.using_encoded(|s| v.extend(s)); + hash.using_encoded(|s| v.extend(s)); + } + Action::Commit(ref round, ref hash) => { + v.push(ActionKind::Commit as u8); + round.using_encoded(|s| v.extend(s)); + hash.using_encoded(|s| v.extend(s)); + } + Action::AdvanceRound(ref round) => { + v.push(ActionKind::AdvanceRound as u8); + round.using_encoded(|s| v.extend(s)); + } + } + + v + } + + fn decode(value: &mut I) -> Option { + match u8::decode(value) { + Some(x) if x == ActionKind::Propose as u8 => { + let (round, block) = try_opt!(Slicable::decode(value)); + Some(Action::Propose(round, block)) + } + Some(x) if x == ActionKind::Prepare as u8 => { + let (round, hash) = try_opt!(Slicable::decode(value)); + + Some(Action::Prepare(round, hash)) + } + Some(x) if x == ActionKind::Commit as u8 => { + let (round, hash) = try_opt!(Slicable::decode(value)); + + Some(Action::Commit(round, hash)) + } + Some(x) if x == ActionKind::AdvanceRound as u8 => { + Slicable::decode(value).map(Action::AdvanceRound) + } + _ => None, + } + } } /// Messages exchanged between participants in the BFT consensus. @@ -52,3 +104,18 @@ pub struct Message { /// The action being broadcasted. pub action: Action, } + +impl Slicable for Message { + fn encode(&self) -> Vec { + let mut v = self.parent.encode(); + self.action.using_encoded(|s| v.extend(s)); + v + } + + fn decode(value: &mut I) -> Option { + Some(Message { + parent: try_opt!(Slicable::decode(value)), + action: try_opt!(Slicable::decode(value)), + }) + } +} diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index 1c282fe42e92e..9563b4fad76b1 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -80,10 +80,11 @@ pub use hashing::{blake2_256, twox_128, twox_256}; #[cfg(feature = "std")] pub mod hexdisplay; -pub mod storage; +pub mod bft; +pub mod block; pub mod hash; +pub mod storage; pub mod uint; -pub mod block; #[cfg(test)] mod tests; From fc18524404cc9b952422caa7c62a96dbeec921df Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 12 Feb 2018 16:01:11 +0100 Subject: [PATCH 05/21] bft service implementation without I/O --- Cargo.lock | 2 + substrate/bft/Cargo.toml | 2 + substrate/bft/src/lib.rs | 183 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 182 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 073170c862087..bebc794549182 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1390,9 +1390,11 @@ version = "0.1.0" dependencies = [ "ed25519 0.1.0", "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "substrate-client 0.1.0", "substrate-codec 0.1.0", "substrate-primitives 0.1.0", + "substrate-state-machine 0.1.0", "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/substrate/bft/Cargo.toml b/substrate/bft/Cargo.toml index e92c5bd9290fd..59b3ff3f3cd61 100644 --- a/substrate/bft/Cargo.toml +++ b/substrate/bft/Cargo.toml @@ -8,5 +8,7 @@ futures = "0.1.17" substrate-client = { path = "../client" } substrate-codec = { path = "../codec" } substrate-primitives = { path = "../primitives" } +substrate-state-machine = { path = "../state-machine" } ed25519 = { path = "../ed25519" } tokio-timer = "0.1.2" +parking_lot = "0.4" diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index 2952fc615d477..7f2dad132fd83 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -22,20 +22,31 @@ pub mod generic; extern crate substrate_codec as codec; extern crate substrate_client as client; extern crate substrate_primitives as primitives; +extern crate substrate_state_machine as state_machine; extern crate ed25519; extern crate tokio_timer; +extern crate parking_lot; -#[cfg_attr(test, macro_use)] +#[macro_use] extern crate futures; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + use client::Client; +use client::backend::Backend; use codec::Slicable; use ed25519::Signature; use primitives::block::{Block, Header, HeaderHash}; use primitives::AuthorityId; +use state_machine::CodeExecutor; -use futures::{Stream, Future}; +use futures::{stream, task, Async, Sink, Future}; +use futures::future::{Executor, ExecuteErrorKind}; +use futures::sync::oneshot; use tokio_timer::Timer; +use parking_lot::Mutex; pub use generic::InputStreamConcluded; @@ -89,7 +100,7 @@ pub trait Proposer: Sized { /// Initialize the proposal logic on top of a specific header. // TODO: provide state context explicitly? - fn init(parent_header: &Header, sign_with: ed25519::Pair) -> Self; + fn init(parent_header: &Header, sign_with: Arc) -> Self; /// Create a proposal. fn propose(&self) -> Self::CreateProposal; @@ -99,8 +110,8 @@ pub trait Proposer: Sized { } /// Instance of BFT agreement. -pub struct BftInstance

{ - key: ed25519::Pair, // TODO (now): key changing over time. +struct BftInstance

{ + key: Arc, authorities: Vec, parent_hash: HeaderHash, timer: Timer, @@ -186,3 +197,165 @@ impl generic::Context for BftInstance

{ .map_err(|_| Error::FaultyTimer)) } } + +type Input = stream::Empty; + +// "black hole" output sink. +struct Output; + +impl Sink for Output { + type SinkItem = Communication; + type SinkError = Error; + + fn start_send(&mut self, _item: Communication) -> ::futures::StartSend { + Ok(::futures::AsyncSink::Ready) + } + + fn poll_complete(&mut self) -> ::futures::Poll<(), Error> { + Ok(Async::Ready(())) + } +} + +/// A future that resolves either when canceled (witnessing a block from the network at same height) +/// or when agreement completes. +pub struct BftFuture { + inner: generic::Agreement, Input, Output>, + cancel: Arc, + send_task: Option>, + client: Arc>, +} + +impl Future for BftFuture + where + P: Proposer, + B: Backend, + X: CodeExecutor, + client::error::Error: From<::Error>, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> ::futures::Poll<(), ()> { + // send the task to the bft service so this can be cancelled. + if let Some(sender) = self.send_task.take() { + let _ = sender.send(task::current()); + } + + // service has canceled the future. bail + if self.cancel.load(Ordering::Acquire) { + return Ok(Async::Ready(())) + } + + // TODO: handle this error, at least by logging. + let committed = try_ready!(self.inner.poll().map_err(|_| ())); + + // If we didn't see the justification (very unlikely), + // we will get the block from the network later. + if let Some(justified_block) = committed.candidate { + // TODO: import justification alongside.xw + let _ = self.client.import_block( + justified_block.header, + Some(justified_block.transactions), + ); + } + + Ok(Async::Ready(())) + } +} + +struct AgreementHandle { + cancel: Arc, + task: Option>, +} + +impl Drop for AgreementHandle { + fn drop(&mut self) { + let task = match self.task.take() { + Some(t) => t, + None => return, + }; + + // if this fails, the task is definitely not live anyway. + if let Ok(task) = task.wait() { + self.cancel.store(true, Ordering::Release); + task.notify(); + } + } +} + +/// The BftService kicks off the agreement process on top of any blocks it +/// is notified of. +pub struct BftService { + client: Arc>, + executor: E, + live_agreements: Mutex>, + timer: Timer, + round_timeout_multiplier: u64, + key: Arc, // TODO: key changing over time. + _marker: ::std::marker::PhantomData

, +} + +impl BftService + where + P: Proposer, + E: Executor>, + B: Backend, + X: CodeExecutor, + client::error::Error: From<::Error>, +{ + /// Signal that a valid block with the given header has been imported. + /// + /// This will begin the consensus process to build a block on top of it. + /// If the executor fails to run the future, an error will be returned. + pub fn build_upon(&self, header: &Header) -> Result<(), ExecuteErrorKind> { + let parent_hash = header.parent_hash.clone(); + let hash = header.hash(); + let mut _preempted_consensus = None; + + let proposer = P::init(header, self.key.clone()); + + // TODO: check key is one of the authorities. + let authorities = Vec::new(); + let n = authorities.len(); + let max_faulty = n.saturating_sub(1) / 3; + + let bft_instance = BftInstance { + proposer, + parent_hash, + round_timeout_multiplier: self.round_timeout_multiplier, + timer: self.timer.clone(), + key: self.key.clone(), + authorities: authorities, + }; + + let agreement = generic::agree( + bft_instance, + n, + max_faulty, + stream::empty(), + Output, + ); + + let cancel = Arc::new(AtomicBool::new(false)); + let (tx, rx) = oneshot::channel(); + + self.executor.execute(BftFuture { + inner: agreement, + cancel: cancel.clone(), + send_task: Some(tx), + client: self.client.clone(), + }).map_err(|e| e.kind())?; + + { + let mut live = self.live_agreements.lock(); + live.insert(hash, AgreementHandle { + task: Some(rx), + cancel, + }); + + _preempted_consensus = live.remove(&parent_hash); + } + + Ok(()) + } +} From 017fd515ed7c323e032fc12007c336ee72c09e63 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 12 Feb 2018 16:10:21 +0100 Subject: [PATCH 06/21] extract out `BlockImport` trait --- substrate/bft/src/lib.rs | 53 +++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index 7f2dad132fd83..89026ea0023cd 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -109,6 +109,25 @@ pub trait Proposer: Sized { fn evaluate(&self, proposal: &Block) -> bool; } +/// Block import trait. +pub trait BlockImport { + /// Import a block alongside its corresponding justification. + fn import_block(&self, block: Block, justification: Justification); +} + +impl BlockImport for Client + where + B: Backend, + E: CodeExecutor, + client::error::Error: From<::Error> +{ + fn import_block(&self, block: Block, _justification: Justification) { + // TODO: use justification. + let _ = self.import_block(block.header, Some(block.transactions)); + } +} + + /// Instance of BFT agreement. struct BftInstance

{ key: Arc, @@ -218,20 +237,14 @@ impl Sink for Output { /// A future that resolves either when canceled (witnessing a block from the network at same height) /// or when agreement completes. -pub struct BftFuture { +pub struct BftFuture { inner: generic::Agreement, Input, Output>, cancel: Arc, send_task: Option>, - client: Arc>, + import: Arc, } -impl Future for BftFuture - where - P: Proposer, - B: Backend, - X: CodeExecutor, - client::error::Error: From<::Error>, -{ +impl Future for BftFuture { type Item = (); type Error = (); @@ -249,14 +262,10 @@ impl Future for BftFuture // TODO: handle this error, at least by logging. let committed = try_ready!(self.inner.poll().map_err(|_| ())); - // If we didn't see the justification (very unlikely), + // If we didn't see the proposal (very unlikely), // we will get the block from the network later. if let Some(justified_block) = committed.candidate { - // TODO: import justification alongside.xw - let _ = self.client.import_block( - justified_block.header, - Some(justified_block.transactions), - ); + self.import.import_block(justified_block, committed.justification) } Ok(Async::Ready(())) @@ -285,8 +294,8 @@ impl Drop for AgreementHandle { /// The BftService kicks off the agreement process on top of any blocks it /// is notified of. -pub struct BftService { - client: Arc>, +pub struct BftService { + import: Arc, executor: E, live_agreements: Mutex>, timer: Timer, @@ -295,13 +304,11 @@ pub struct BftService { _marker: ::std::marker::PhantomData

, } -impl BftService +impl BftService where P: Proposer, - E: Executor>, - B: Backend, - X: CodeExecutor, - client::error::Error: From<::Error>, + E: Executor>, + I: BlockImport, { /// Signal that a valid block with the given header has been imported. /// @@ -343,7 +350,7 @@ impl BftService inner: agreement, cancel: cancel.clone(), send_task: Some(tx), - client: self.client.clone(), + import: self.import.clone(), }).map_err(|e| e.kind())?; { From c33c3ff8c54484e7e73c6f737c71033dd7cdfff9 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 12 Feb 2018 17:18:26 +0100 Subject: [PATCH 07/21] allow bft primitives to compile on wasm --- Cargo.lock | 1 + .../release/polkadot_runtime.compact.wasm | Bin 75751 -> 80217 bytes .../release/polkadot_runtime.wasm | Bin 75800 -> 80296 bytes substrate/bft/Cargo.toml | 1 + substrate/bft/src/error.rs | 57 +++++++++++++++++ .../bft/src/{ => generic}/accumulator.rs | 0 substrate/bft/src/generic/mod.rs | 6 +- substrate/bft/src/lib.rs | 58 ++++++++++-------- .../release/runtime_test.compact.wasm | Bin 14152 -> 13451 bytes .../release/runtime_test.wasm | Bin 14248 -> 13576 bytes substrate/primitives/src/bft.rs | 1 + substrate/primitives/src/block.rs | 1 + .../substrate_test_runtime.compact.wasm | Bin 31904 -> 32856 bytes .../release/substrate_test_runtime.wasm | Bin 31984 -> 32936 bytes 14 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 substrate/bft/src/error.rs rename substrate/bft/src/{ => generic}/accumulator.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 2c50f7590bb18..46e35cd42d6ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1391,6 +1391,7 @@ name = "substrate-bft" version = "0.1.0" dependencies = [ "ed25519 0.1.0", + "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "substrate-client 0.1.0", diff --git a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm index b4b77e5ec9e4c2a74a57d38ded342bd8de036607..c6fc9b035c57be18a2c7c8e8686935e994fc47dc 100644 GIT binary patch literal 80217 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

1pYlHczCYNfqIV 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

Pp* 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 Ol^6!hl&e2}h4eqwXm@)6 literal 75751 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 diff --git a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.wasm b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.wasm index 2b5124161a1f76f50f483c1c8bf74aa9942dd88a..9f4e4cc836056cee61e8be0cab4c4a64044b6789 100644 GIT binary patch literal 80296 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 PT!CT0Ou70KmrMTx)ZdN4 literal 75800 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 diff --git a/substrate/bft/Cargo.toml b/substrate/bft/Cargo.toml index 59b3ff3f3cd61..873fcad4530de 100644 --- a/substrate/bft/Cargo.toml +++ b/substrate/bft/Cargo.toml @@ -12,3 +12,4 @@ substrate-state-machine = { path = "../state-machine" } ed25519 = { path = "../ed25519" } tokio-timer = "0.1.2" parking_lot = "0.4" +error-chain = "0.11" diff --git a/substrate/bft/src/error.rs b/substrate/bft/src/error.rs new file mode 100644 index 0000000000000..1f5286f5e665e --- /dev/null +++ b/substrate/bft/src/error.rs @@ -0,0 +1,57 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! Error types in the BFT service. + +error_chain! { + errors { + /// Missing state at block with given Id. + StateUnavailable(b: ::client::BlockId) { + description("State missing at given block."), + display("State unavailable at block {:?}", b), + } + + /// I/O terminated unexpectedly + IoTerminated { + description("I/O terminated unexpectedly."), + display("I/O terminated unexpectedly."), + } + + /// Unable to schedule wakeup. + FaultyTimer { + description("Faulty timer: unable to schedule wakeup"), + display("Faulty timer: unable to schedule wakeup"), + } + + /// Unable to propose a block. + CannotPropose { + description("Unable to create block proposal."), + display("Unable to create block proposal."), + } + + /// Error dispatching the agreement future onto the executor. + Executor(e: ::futures::future::ExecuteErrorKind) { + description("Unable to dispatch agreement future"), + display("Unable to dispatch agreement future: {:?}", e), + } + } +} + +impl From<::generic::InputStreamConcluded> for Error { + fn from(_: ::generic::InputStreamConcluded) -> Error { + ErrorKind::IoTerminated.into() + } +} diff --git a/substrate/bft/src/accumulator.rs b/substrate/bft/src/generic/accumulator.rs similarity index 100% rename from substrate/bft/src/accumulator.rs rename to substrate/bft/src/generic/accumulator.rs diff --git a/substrate/bft/src/generic/mod.rs b/substrate/bft/src/generic/mod.rs index e0a8f133e17b2..f5cd34b8b69fd 100644 --- a/substrate/bft/src/generic/mod.rs +++ b/substrate/bft/src/generic/mod.rs @@ -23,9 +23,11 @@ use std::hash::Hash; use futures::{future, Future, Stream, Sink, Poll, Async, AsyncSink}; -use accumulator::State; +use self::accumulator::State; -pub use accumulator::{Accumulator, Justification, PrepareJustification, UncheckedJustification}; +pub use self::accumulator::{Accumulator, Justification, PrepareJustification, UncheckedJustification}; + +mod accumulator; #[cfg(test)] mod tests; diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index 89026ea0023cd..0264cc934d4d6 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -16,7 +16,7 @@ //! BFT Agreement based on a rotating proposer in different rounds. -mod accumulator; +pub mod error; pub mod generic; extern crate substrate_codec as codec; @@ -30,11 +30,14 @@ extern crate parking_lot; #[macro_use] extern crate futures; +#[macro_use] +extern crate error_chain; + use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use client::Client; +use client::{BlockId, Client}; use client::backend::Backend; use codec::Slicable; use ed25519::Signature; @@ -43,12 +46,13 @@ use primitives::AuthorityId; use state_machine::CodeExecutor; use futures::{stream, task, Async, Sink, Future}; -use futures::future::{Executor, ExecuteErrorKind}; +use futures::future::Executor; use futures::sync::oneshot; use tokio_timer::Timer; use parking_lot::Mutex; pub use generic::InputStreamConcluded; +pub use error::{Error, ErrorKind}; /// Messages over the proposal. /// Each message carries an associated round number. @@ -74,23 +78,6 @@ pub type Committed = generic::Committed; /// Communication between BFT participants. pub type Communication = generic::Communication; -/// Errors that can occur during agreement. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Error { - /// Io streams terminated. - IoTerminated, - /// Timer failed to fire. - FaultyTimer, - /// Unable to propose for some reason. - CannotPropose, -} - -impl From for Error { - fn from(_: InputStreamConcluded) -> Error { - Error::IoTerminated - } -} - /// Logic for a proposer. /// /// This will encapsulate creation and evaluation of proposals at a specific @@ -115,6 +102,12 @@ pub trait BlockImport { fn import_block(&self, block: Block, justification: Justification); } +/// Trait for getting the authorities at a given block. +pub trait Authorities { + /// Get the authorities at the given block. + fn authorities(&self, at: &BlockId) -> Result, Error>; +} + impl BlockImport for Client where B: Backend, @@ -127,6 +120,17 @@ impl BlockImport for Client } } +impl Authorities for Client + where + B: Backend, + E: CodeExecutor, + client::error::Error: From<::Error> +{ + fn authorities(&self, at: &BlockId) -> Result, Error> { + self.authorities_at(at).map_err(|_| ErrorKind::StateUnavailable(*at).into()) + } +} + /// Instance of BFT agreement. struct BftInstance

{ @@ -213,7 +217,7 @@ impl generic::Context for BftInstance

{ .saturating_mul(self.round_timeout_multiplier); Box::new(self.timer.sleep(Duration::from_secs(timeout)) - .map_err(|_| Error::FaultyTimer)) + .map_err(|_| ErrorKind::FaultyTimer.into())) } } @@ -295,7 +299,7 @@ impl Drop for AgreementHandle { /// The BftService kicks off the agreement process on top of any blocks it /// is notified of. pub struct BftService { - import: Arc, + client: Arc, executor: E, live_agreements: Mutex>, timer: Timer, @@ -308,13 +312,13 @@ impl BftService where P: Proposer, E: Executor>, - I: BlockImport, + I: BlockImport + Authorities, { /// Signal that a valid block with the given header has been imported. /// /// This will begin the consensus process to build a block on top of it. /// If the executor fails to run the future, an error will be returned. - pub fn build_upon(&self, header: &Header) -> Result<(), ExecuteErrorKind> { + pub fn build_upon(&self, header: &Header) -> Result<(), Error> { let parent_hash = header.parent_hash.clone(); let hash = header.hash(); let mut _preempted_consensus = None; @@ -322,7 +326,7 @@ impl BftService let proposer = P::init(header, self.key.clone()); // TODO: check key is one of the authorities. - let authorities = Vec::new(); + let authorities = self.client.authorities(&BlockId::Hash(hash))?; let n = authorities.len(); let max_faulty = n.saturating_sub(1) / 3; @@ -350,8 +354,8 @@ impl BftService inner: agreement, cancel: cancel.clone(), send_task: Some(tx), - import: self.import.clone(), - }).map_err(|e| e.kind())?; + import: self.client.clone(), + }).map_err(|e| e.kind()).map_err(ErrorKind::Executor)?; { let mut live = self.live_agreements.lock(); diff --git a/substrate/executor/wasm/target/wasm32-unknown-unknown/release/runtime_test.compact.wasm b/substrate/executor/wasm/target/wasm32-unknown-unknown/release/runtime_test.compact.wasm index 39b6f737efc2312f442206f6204d317487855cba..d5fd4dfb021bc0d9dc3253288694113321c46df3 100644 GIT binary patch literal 13451 zcmdU0dvsjId7t~fcjvBjCEJp0%l52e@ERLy_1ay@25THF+t}C+93BCJB-?AFwJpi3 zmBDF?Rz@V@ZBiOIBq2^qcm;|{^C}_W^ZLt&T0N=i1Tcl2k!5idv~=0 z6OxnmkFL({-1%nan{U4N%v`a7(LEYtOgo$R8{^~S+IYXNeyPIxP5cr}m8@R_NTa%T z4WoXnpQ%#2MxWGuBR90KCU{9cNLN3QFs?MhB&Rnr?VE69fZEC=1akwzB zGshUU$EbaeYGn2lAh&1R$o{ZLoO+BD28W7$n3OrxNl_fa#)><#%q3tgbsWW1YA(3FFuy;nU*%9*|ue9tj_s?7Pt50 z_6!&H>&!3aMvHyh2Z{rIgG0=(Q`Ov_k>dWoLasPg7-I35f{hFe4Q^vK4XUzjcxd}z zad3EOV0T~G5Y(!MDhbrZ6-1FuG}c$uX&EzSsP=b=XXfk>dMeq>sw<3v*sZypgF}5| zLstzBZD$R3#P}vt32e6KYfLh3Cz}_3V9{x@|JpF`Nln+s^=l+c>ZS~KB@D@gk=9MU zM`&>-{RH|aG%f?l16MXnOE94Yo=iz6a0Pm&btbdY5Jq4obYUcQbdP$k1Xx$Sk1*+B zxD!|j2Fz?t9J2@-D~#1nMmK@!d4~4I!syi9ghd5QC@QG09vGYmlR+M|32U|Zm@p@6 zIb+;pt2J)1pq6o$$=|VH0ppkHTJ@TA4Am=Z5~!wiSLmy?jDZH}ONM!Mniuq{d1*j2 z7f8cwm`mQS5$?NmAU7h09b>x^)3H}L2K+3;L|7zg(iJQf*4h*atUCmrEx=JcihO`s zm#{2RA&;=UY|0Bmk+_7V5@rMp^1_UON$@G`>E?wME@tYf(y{%DGrzc@5_8enM78U+L7XO#L;s4Kh^SD-}l&SYon;=w_ zrbsn{d=R7MH!0N+c?l_pnlbgXgaz-At&qXHWJ~%OWb{QO)jWQJP_)TVv?3C1>RpmO zVCrVYq)OVZgasY2DmFEx^%f|C!od}g{=>c$Ajo3^qYB{Wq0nXopu$B*XjHk&Yg3O$ zryi4eP+jhVN>g3iVFvJ@h9ux!^oD=w zVCTg(P>4CRd2|AK(GUeo5e#%z!;%DhwhD%4K#o*F@C-EnyM?j$x59F5?Brh zA!n)R!H=5{v^CvqSZd^6$+_wR#HaNUI7%&}7t!Ygrn2Nu;Z!t8-0IY?PS{k8Bpec? zENM#Qj<6$VC*Zf|FbZ9P`;e53>9k~#TfPK@#M30Jrh!2?jaGM|WvSh;4V z^<@A)1n-hWc^G91<(E-rr4O==NJIvCA)eHiK|KtCVH79aq>j}PAf}#N>XA&ulu%6? zVaoB1ar6f!SD0{%Zd~(qF13V<@MUq~iWpiOp_W{BfuK*}AwTtTj*yDDGM?Tu*qu!OKI541(yG88msqQ}mQ8 zP(cjCpSxi$h=Lan426vgPndaO3nyrt15#HiQY|7iTSY2p*(EF@H6|=&Ng_t1xIGVy@W}mx@e_G&`5C^LqZy(3MfH;{5gt7S9qY2(9j}dM5Co> zgn_Xh!KqjWiV@|ugthajmS&fRz$-AykOnHy`T$~B!Y)bps2&Rgolm|3z%7Y*1VsFb zN5FN7ng|HTSEGRBW~Nmt-KhnUvy8BZW}zCihKhcO+EF@#oB~;kp&}qwuQZQTt{?Ox z{%sGZ#4iw12;`mmm4vKQ?@z=@^~+Wh7k+3pFcSH)$`a%eEjXR=Ij|lm8~vC_m-L4N zM;Q1)#~3VCXpp?kZ9E26SQz|9P1DB1e5@)=KxJSoNCLvFNtcKMKCGE6L?;SSL5@R} z=E@UV$rwoD~F>%kY;dgQQZFnWY1y*$}9`FJ!8!9p1f=ES4pyYsL> zOCATzh9~(;=r-&~-frYuG^jIqmuuiX0sRd6PH%G19d^Xz$#1>dvH{551p3FJ2ggAZ z@E?cykt-4muBZarL}o-?kr$sEnH?qpCQvm5ctkB#PcbV-Z6pj%IHtTbvQ{>YK3{bj zC&be`rSjd-K>*=cJ5c!KkUd!{Jq18kE*8!%j3M0xF^)>afq5_4h$ok#3+C3qOmbLE z>if{v%~sI>O5Wk`?Mw_iIa%+-nb!ILigePJ@PpSWAo!RL}3#-H6U*$35Y$uHkh?~@@JY-XONEr~~@b}5uz4BOy!%#R(;w|htA|Frt5(bfPDDvT5 z%jCnhl_s5P1vX&;C7^veA*ZC>_9K<6-zQHfVx|$lS=B)6bmGB)GGLRW^Z?EtbWW)N z44)JuK`O(H9=IP5DV8#NXa_<)1e#AkxhN%IyjoS@11p+DN=ayJL`TvwlLo_AA|s0s z+ZDvBjWjhvtOGH^GK)k>f>CmwqWq5%j70$!Bd4H9eR=J~j8jBC-(`v@Wc?L!gT z6n#Dn2)w5)ytYIqpJ7rULhLXpIw7k!RKBmE$Q2Y6iGKv8?Q<3>irPgZ8CYT<(MBq% z5I`ODpn>sU=$aJ;Ae@xgnB6YNA5w2!g&kcv(O%`gl!qxRg@FJ9YxbjFp&{Es?`A$3 zGL{Sm>&g&-7*A!DjjLLf5&LAsmHwfoR1c^r515Xj%7lFh6DCceVP#G6r_EZcYE`CG zA)}17hD?d%gaZYv%_nm)Lw^eE!eZ!53Eq%`1|Ho|LP#&bEc$`mDwab&SSP8YAgSBY z>5I;Xs*YQb4{2ybCIQ1)q6ff;f@F>Y4igAx=84JZL#HH7LZb~3!C%H^H2_pYZWy*> z11$j1X+uSDoWCH+_}|aIXy!jT`#NqBF~4y%CsH^%X@Wad84p+vf0ZDE)Z_#?4q^(O z-Ia7of{Y%tAi~iG0nt0Zj4}D-__=Y)^d*u3?A?I?0lXK%)CiLz3*jCC2w@k=3K<^t zpq-Paw)x~zh;1N2A`_ZR$s4i;c?7Nw6_=E{`VZNp7CJEGc>%~F2SC8kFer71=madJ zQ&(x&6s0%PCGAnVOks@SUep#U>mhIZcu;#7pj2B1SkOuf$tii zk~5F;*(R(gJC#VQ3e*WLwC*s)Rl8eNK}%GDI<7>AXWG5!BbmX*PJd1O!QTPY93%(~jPbM$_h2 z+C^c|m;!7q9DD+U4Gbn_gqG7lHyQ!uQ6mtGig1c2*xo`osDO2ZZW7y3E?;-(Tosf8 zyNNc^IWRq3#M?`qofM+cEIRnFglcP zBsf$cMRmyF$(LD0)UQuHhEj(GM=F5+Q6-Z4FEIs49Z-_`XWX*X>7-go9dncgPp(G^ zKCN$2@+VEWL_Jk$!cmVj;SH|Er-DrlEW9QPbuKx&B)nf-d06s(mGDz-gVUcO3zbyI z!O$2Nag_nV{K$<%JVoX4(L@ZAg*qtpqD_9s3i8yk$(7sC3xubeSwoJ5Jl?g3SS}f%Sgj@phk)c#U^B0 zg_H;x&_?Giq(**$juEP9(S3)U8U`mkoC8oNB%P$meg!}2GUOcM_rk2ii}1(Eh4|Re zkrtjBw}M8v-71||yrDtyC^hgZELszXk-*XJ5=sCbs-}%&TK5(J~-;wQR*pQ^aYlItYWr!2Zfw>9z%W zMSBaE#6tT*xKUuX$&cH@P#!`Un{n!-3p)xi8{^0vZ8gvg`Hcw@ua1ONt$_%6Z8{{W*Q87k{3gi?loN}-^e%7g-_GEGEs2&3MgI5ie4 z3`|EV5p8lw?WZ=mic^$VVG6a?MFN0)4#bhurhOQlR-jP30Xq!%T)>G?+{OabZc+^n z)6g2)Pf{96_87v|UJ~%CG?Z%ZNJFRkhiPc_fEp1EC?||@1$ajqN*EM;Dh;hhQd=G~ z9ZA)Wj?z%5*ir4i+;Xc7sq+c*DqEaB+fH@%@ z_15EH#*vTeo0TI`_l-Dl!nFY-By$aam z1Og>d>(J3hHLFW??_@P`h^GFd*o+)m-hr&z1pN`4kJe3Rv-du1{-?V4Vl%Z&WAlmn zKZnh9V5Hdm+N?i`&4^G@-dJJtYqJz(5IG&ZYI_m9;F2BrDRq-o#qoo`Tbq{$e+PF? z8vf>u#k_p;=4D~+bN8e6xu&r4!#7c>37_9SNdmw7L`+3K`M{UP@^VSo^v}29Ifko6 znmY2rg`Eg7ky*l^AAYEN%)|1gEn-@@gR*nSI!uc2`&ib^TSjD_$|8`!!GMMcKTUjtOC6 zh(5wY5)SYyM{ACZt)CmARd;n^BLCYmTIiV6a41`Lf<(8*UokYJ z>JFfC0%TGquQGF!3SVHuK-+HGH_-J4w*KK+1D2$GEY1h8F#{~z*P6I$0|X~VEPy%4k0_vuut;Jq>lA{s} zG8AxysZfKCxErP!Xx{P#w<526r(HF%Iz9noVc0~#5vDSC)ag`(2GNrlMhz3k(E!VX z*e0P(U3^9dG}QN$vnKO&Wsc^lwW2GyK^p{2BXQn9$f_r%gEE>pJg--Zr4H~YbpoT5 zc(nwcUyD&P8_ZQFduI7$Ps5u#0&)qOd9nKLO9PA4S&t^`RN@lKlq4Bmd}(q~L|0oOY6cql;H{VT#=+T__=}geT4A=#3iPzuE~0R#(QL_HGJb;T9Ib z2|F+f%ux-wHiqHPQrEDiuyGA*BFV+|jXcyymyB|_x#yP)}zz2rjfFfgLG@ zUjy>NF=pc&ZeS??WK$11a`PAu7~X*3y!yR5+03oMp^>p7lPT_$-?o!kZtNHyW^yGr zwhnxh-eut4j=`b9(JOP?5wMWbMy{sm}H{kFB0*{RrwzcdY+`7Cl zN-t{hrhdj)4BwHcT!3;uzD(Y(y}NZG8cfytkjBlA<_e(Xu*Qwyp&Y}T1l+*Obd=BY zx@`j^1KS3R`^7L`YuvGW_$r25S>AL6gJE#&1imxy{f^FVCV<*P)_5A}YaD^{FnY4}FDd!OM=PD&;Yx?RZ%)ceNNE zD~jPAV(akO(Du=8QM@uIcISq~;HY4-Wbk^i6&(l~c31G*4gRc_N~Ky;ZK?KDN2)WG zPGwTrR99=NwY9aawY{~YwX-$dnrY3pcD1G2THD&%+S@wXI@{81nYL_OS9_|xwY{yq zy}hHovpwCOY0tKIb)-64JK8$hJ32Z#JJKDQj%-I)XR5Qcv#qnev!k=KGu@f#%yxFA zQ|Z=pTe>~nk?u^V)0uQO-IYmYS~G2#_Dn~nGn39_GTBU5HkED7wq@J19of!oI-AL6 zvt3;vu?y380d*IKbpgb1Rn3D#kc$mg9Y#q)XSj>zK?${)sMLXrmV%ZnJ`+;gHeATH zjP4%XmTTEFyj_Xy5H8b09zjJczOc;M!obkZoY*m35UM{h8w}oAyb=t4*@7hicLQ(> zlyNo3aX`y6H%`rL^M(*ZILl~ppimqY19YZv$mVe+s2qeCf)3kU>xXLE+`4S}^5q}F zdqFmDqE*0(SqeWiX$t6-R^jN7?^aJl3cmC~qxw{r)Q7%8wY+y9aPANGf$2ItI0oY) z{eIXjo5kx6Z#o78T?%LbD#>^@W5QV|vDYdIaj9+Vi)wxA>tPOa%PF8{Jd z#L`Eo-}fe%`n_MD_gfU1w8<)4Ny8I)3UwL!#=38$2 zS${F=H{bRExuq z^~BGvz3yYz-+0rVhwi!e{s$gI=pLyZM^_w)FGG&Hl$uJyakd8ftgjbe|PKdtnvb>Ofu z$3F0TQF;hX3*<)gOrz+oG-{2N z8P`3-K5(+G$!)1S(-1+Twsezu;EvgR*2gb38fKe8>6ccEwZhSBn!M73>7rly-W=a5 zowQ0{wHEoGekS%WOJ_~qY-GHN%V(Cp?v}p3s?&_wS-02q?INFVTxx#UD_z^v9Bc4; zW2GDHFW%!fnysHVC%%8W<6Bng3w0BFf~%8f*c;8#&1UHdW3Ew)y;i9XFP+(%h9&FJ zY&~XhJ<#gRdTU1gOnsI<$7r_ZnTxc%u}gnke@TDYf5rQr{;K|c?U?mr{Vj3A{F(OO z8mz32e@gQguQ+?t=3DQ+`*XJArO!J1qF)?+$(-4A(Z%2FeBs4o^&h|fmb<5G>88!w zbC+KJ)vrxn_gOD<@V5J%*ow1<``-HLrCaa%^y1`&pFQ;Xzq{wY`yYPn@uzI=&zjf0 zYR!l4z3=(&9&%>SS$Nvh&zRx^r=6Z`&vvgncjJ}|F1(2Jt$$l?$F9+<|7!e(dmen` z@R6e}tB{%0^B?P1Ec^@0fIM=h*0Q{GV4l%h+iWiK=37gxbIh7EN)OtL%tdC>?ciGJ z;6%o2h`FUZyNqqFmukSPm2)kvCu?r7lBQvLuIKcK#isA2jc#j>gJaq)>oe`~cE_IR zUobJT$(&#N!J64l%s$6o?2YkN@(g>06|+BNYgS-br5mqUxXz80?!98+8XmLbGqP?h zJ;#76ndra3*CyCH_kwv{6Haezp0Rd)#;9>)2kyQ0t{r2%^wbTHPqy6p(t)*~|HlJe z&KaiOe|oGpmb7MEJF$WWY_$#GXmu#?NrapOSrLCD4T5~(}iJE2RcK@PS+_>C4 zt7~3-ndw0@?b2P>zQPG%(Z6u1Z-Q<+>CQjjH_tMfXEr6Z+QyH~hEt!#mu^EEnAT&aCCR$e<=zBBgrNGVt5D+ynhoC&VI3PVD}jE&VvoSt{er1)c}=%9B$wZ4od2jk)gTfb4sG^Thzo!n1h_?+qC`aMHL1YWV&}1WTIuym`BPgdQwuc%| gnGGLlJaxWXAB+x<6}IKh8yFcG9NKx|`5W2)0KqN8t^fc4 literal 14152 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 Iau@V}0qPGAUH||9 diff --git a/substrate/executor/wasm/target/wasm32-unknown-unknown/release/runtime_test.wasm b/substrate/executor/wasm/target/wasm32-unknown-unknown/release/runtime_test.wasm index 7a21c422087bd00293fd1508f1542997922d3c44..2623dc31b8a59c862375e4abce3c9f6bf086602b 100644 GIT binary patch literal 13576 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 literal 14248 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 HeaderHash { ::hashing::blake2_256(Slicable::encode(self).as_slice()).into() } diff --git a/substrate/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.compact.wasm b/substrate/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.compact.wasm index 7713a89ab36efbd5f52bf99e39100af9b34272c4..2f9b66ebfe8a8f3fc3852e05336ce2aee7b90701 100644 GIT binary patch literal 32856 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 DiK}pl literal 31904 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 OvwDB-GR^YcivJ6?m)|x3 diff --git a/substrate/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.wasm b/substrate/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.wasm index ec5638e1c183ce98875d9e6fe4db58fd09e01f70..160eaf04215b29354fc6aaece97eeebb9b003093 100644 GIT binary patch literal 32936 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 literal 31984 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 Date: Mon, 12 Feb 2018 18:14:13 +0100 Subject: [PATCH 08/21] Block builder (substrate) --- substrate/client/src/block_builder.rs | 89 +++++++++++++++++++++++++++ substrate/client/src/lib.rs | 17 ++++- substrate/primitives/src/block.rs | 11 ++-- substrate/primitives/src/lib.rs | 4 +- 4 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 substrate/client/src/block_builder.rs diff --git a/substrate/client/src/block_builder.rs b/substrate/client/src/block_builder.rs new file mode 100644 index 0000000000000..ebd8ec6c47fe3 --- /dev/null +++ b/substrate/client/src/block_builder.rs @@ -0,0 +1,89 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! Utility struct to build a block. + +use std::vec::Vec; +use codec::{Joiner, Slicable}; +use state_machine::{self, CodeExecutor}; +use primitives::{Header, Block}; +use primitives::block::Transaction; +use {backend, error, BlockId, BlockStatus, Client}; +use triehash::ordered_trie_root; + +/// Utility for building new (valid) blocks from a stream of transactions. +pub struct BlockBuilder where + B: backend::Backend, + E: CodeExecutor + Clone, + error::Error: From<<::State as state_machine::backend::Backend>::Error>, +{ + header: Header, + transactions: Vec, + executor: E, + state: B::State, + changes: state_machine::OverlayedChanges, +} + +impl BlockBuilder where + B: backend::Backend, + E: CodeExecutor + Clone, + error::Error: From<<::State as state_machine::backend::Backend>::Error>, +{ + /// Create a new instance of builder from the given client. + pub fn new(client: &Client) -> error::Result { + let best = (client.info().map(|i| i.chain.best_number)?..1) + .find(|&n| if let Ok(BlockStatus::InChain) = client.block_status(&BlockId::Number(n)) + { true } else { false }) + .unwrap_or(0); + + Ok(BlockBuilder { + header: Header { + number: best + 1, + parent_hash: client.block_hash(best)?.expect("We already ascertained this is InChain before; qed"), + state_root: Default::default(), + transaction_root: Default::default(), + digest: Default::default(), + }, + transactions: Default::default(), + executor: client.clone_executor(), + state: client.state_at(&BlockId::Number(best))?, + changes: Default::default(), + }) + } + + /// Push a transaction onto the block's list of transactions. This will ensure the transaction + /// can be validly executed (by executing it); if it is invalid, it'll be returned along with + /// the error. Otherwise, it will return a mutable reference to self (in order to chain). + pub fn push(&mut self, tx: Transaction) -> error::Result<()> { + let output = state_machine::execute(&self.state, &mut self.changes, &self.executor, "execute_transaction", + &vec![].and(&self.header).and(&tx))?; + self.header = Header::decode(&mut &output[..]).expect("Header came straight out of runtime do must be valid"); + self.transactions.push(tx); + Ok(()) + } + + /// Consume the builder to return a valid `Block` containing all pushed transactions. + pub fn bake(mut self) -> error::Result { + self.header.transaction_root = ordered_trie_root(self.transactions.iter().map(Slicable::encode)).0.into(); + let output = state_machine::execute(&self.state, &mut self.changes, &self.executor, "finalise_block", + &self.header.encode())?; + self.header = Header::decode(&mut &output[..]).expect("Header came straight out of runtime do must be valid"); + Ok(Block { + header: self.header, + transactions: self.transactions, + }) + } +} diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index 530288ca29edb..1a7d4ceb622f2 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -39,9 +39,11 @@ pub mod blockchain; pub mod backend; pub mod in_mem; pub mod genesis; +pub mod block_builder; pub use blockchain::Info as ChainInfo; pub use blockchain::BlockId; +pub use block_builder::BlockBuilder; use primitives::{block, AuthorityId}; use primitives::storage::{StorageKey, StorageData}; @@ -59,6 +61,7 @@ pub struct Client where B: backend::Backend { } /// Client info +// TODO: split queue info from chain info and amalgamate into single struct. #[derive(Debug)] pub struct ClientInfo { /// Best block hash. @@ -167,6 +170,11 @@ impl Client where self.storage(id, &StorageKey(b":code".to_vec())).map(|data| data.0) } + /// Clone a new instance of Executor. + pub fn clone_executor(&self) -> E where E: Clone { + self.executor.clone() + } + /// Get the current set of authorities from storage. pub fn authorities_at(&self, id: &BlockId) -> error::Result> { let state = self.state_at(id)?; @@ -183,10 +191,8 @@ impl Client where /// No changes are made. pub fn call(&self, id: &BlockId, method: &str, call_data: &[u8]) -> error::Result { let mut changes = state_machine::OverlayedChanges::default(); - let state = self.state_at(id)?; - let return_data = state_machine::execute( - &state, + &self.state_at(id)?, &mut changes, &self.executor, method, @@ -195,6 +201,11 @@ impl Client where Ok(CallResult { return_data, changes }) } + /// Create a new block, built on the head of the chain. + pub fn new_block(&self) -> error::Result> where E: Clone { + BlockBuilder::new(self) + } + /// Queue a block for import. pub fn import_block(&self, header: block::Header, body: Option) -> error::Result { // TODO: import lock diff --git a/substrate/primitives/src/block.rs b/substrate/primitives/src/block.rs index ba34dd085f08a..90882f7fc3f71 100644 --- a/substrate/primitives/src/block.rs +++ b/substrate/primitives/src/block.rs @@ -17,19 +17,18 @@ //! Block and header type definitions. #[cfg(feature = "std")] -use bytes; use rstd::vec::Vec; +use {bytes, Hash}; use codec::{Input, Slicable}; -use hash::H256; /// Used to refer to a block number. pub type Number = u64; /// Hash used to refer to a block hash. -pub type HeaderHash = H256; +pub type HeaderHash = Hash; /// Hash used to refer to a transaction hash. -pub type TransactionHash = H256; +pub type TransactionHash = Hash; /// Simple generic transaction type. #[derive(PartialEq, Eq, Clone)] @@ -127,9 +126,9 @@ pub struct Header { /// Block number. pub number: Number, /// State root after this transition. - pub state_root: H256, + pub state_root: Hash, /// The root of the trie that represents this block's transactions, indexed by a 32-byte integer. - pub transaction_root: H256, + pub transaction_root: Hash, /// The digest of activity on the block. pub digest: Digest, } diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index 1c282fe42e92e..211bfce8f5a7f 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -90,8 +90,10 @@ mod tests; pub use self::hash::{H160, H256}; pub use self::uint::{U256, U512}; - pub use block::{Block, Header}; +/// General hash type. +pub type Hash = H256; + /// An identifier for an authority in the consensus algorithm. The same as ed25519::Public. pub type AuthorityId = [u8; 32]; From 1830fa730756b4bc37b54e830149ca05f99837b2 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 12 Feb 2018 19:52:27 +0100 Subject: [PATCH 09/21] take polkadot-consensus down to the core. --- Cargo.lock | 6 + polkadot/consensus/Cargo.toml | 6 + polkadot/consensus/src/lib.rs | 780 ++++++++----------------- polkadot/primitives/src/parachain.rs | 104 ++++ polkadot/primitives/src/transaction.rs | 3 +- polkadot/statement-table/src/lib.rs | 7 +- substrate/codec/src/slicable.rs | 10 +- 7 files changed, 373 insertions(+), 543 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46e35cd42d6ba..a2191936c5f24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1006,8 +1006,14 @@ dependencies = [ name = "polkadot-consensus" version = "0.1.0" dependencies = [ + "ed25519 0.1.0", "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "polkadot-primitives 0.1.0", + "polkadot-statement-table 0.1.0", + "substrate-bft 0.1.0", + "substrate-codec 0.1.0", + "substrate-primitives 0.1.0", "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/polkadot/consensus/Cargo.toml b/polkadot/consensus/Cargo.toml index ea2a27fcfa00b..aeda287f80cac 100644 --- a/polkadot/consensus/Cargo.toml +++ b/polkadot/consensus/Cargo.toml @@ -7,3 +7,9 @@ authors = ["Parity Technologies "] futures = "0.1.17" parking_lot = "0.4" tokio-timer = "0.1.2" +ed25519 = { path = "../../substrate/ed25519" } +polkadot-primitives = { path = "../primitives" } +polkadot-statement-table = { path = "../statement-table" } +substrate-bft = { path = "../../substrate/bft" } +substrate-codec = { path = "../../substrate/codec" } +substrate-primitives = { path = "../../substrate/primitives" } diff --git a/polkadot/consensus/src/lib.rs b/polkadot/consensus/src/lib.rs index e3c5a034cd346..26d8cfae9e275 100644 --- a/polkadot/consensus/src/lib.rs +++ b/polkadot/consensus/src/lib.rs @@ -1,534 +1,246 @@ -// // 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. -// //! -// //! Authorities are split into groups by parachain, and each authority might come -// //! up its own candidate for their parachain. Within groups, authorities pass around -// //! their candidates and produce statements of validity. -// //! -// //! Any candidate that receives majority approval by the authorities in a group -// //! may be subject to inclusion, unless any authorities flag that candidate as invalid. -// //! -// //! Wrongly flagging as invalid should be strongly disincentivized, so that in the -// //! equilibrium state it is not expected to happen. Likewise with the submission -// //! of invalid blocks. -// //! -// //! Groups themselves may be compromised by malicious authorities. - -// #[macro_use] -// extern crate futures; -// extern crate parking_lot; -// extern crate tokio_timer; - -// use std::collections::{HashMap, HashSet}; -// use std::fmt::Debug; -// use std::hash::Hash; -// use std::sync::Arc; -// use std::time::Duration; - -// use futures::prelude::*; -// use futures::sync::{mpsc, oneshot}; -// use parking_lot::Mutex; -// use tokio_timer::Timer; - -// use table::Table; - -// mod bft; -// mod handle_incoming; -// mod round_robin; -// mod table; - -// #[cfg(test)] -// pub mod tests; - -// /// Context necessary for agreement. -// pub trait Context: Send + Clone { -// /// A authority ID -// type AuthorityId: Debug + Hash + Eq + Clone + Ord; -// /// The digest (hash or other unique attribute) of a candidate. -// type Digest: Debug + Hash + Eq + Clone; -// /// The group ID type -// type GroupId: Debug + Hash + Ord + Eq + Clone; -// /// A signature type. -// type Signature: Debug + Eq + Clone; -// /// Candidate type. In practice this will be a candidate receipt. -// type ParachainCandidate: Debug + Ord + Eq + Clone; -// /// The actual block proposal type. This is what is agreed upon, and -// /// is composed of multiple candidates. -// type Proposal: Debug + Eq + Clone; - -// /// A future that resolves when a candidate is checked for validity. -// /// -// /// In Polkadot, this will involve fetching the corresponding block data, -// /// producing the necessary ingress, and running the parachain validity function. -// type CheckCandidate: IntoFuture; - -// /// A future that resolves when availability of a candidate's external -// /// data is checked. -// type CheckAvailability: IntoFuture; - -// /// The statement batch type. -// type StatementBatch: StatementBatch< -// Self::AuthorityId, -// table::SignedStatement, -// >; - -// /// Get the digest of a candidate. -// fn candidate_digest(candidate: &Self::ParachainCandidate) -> Self::Digest; - -// /// Get the digest of a proposal. -// fn proposal_digest(proposal: &Self::Proposal) -> Self::Digest; - -// /// Get the group of a candidate. -// fn candidate_group(candidate: &Self::ParachainCandidate) -> Self::GroupId; - -// /// Get the primary for a given round. -// fn round_proposer(&self, round: usize) -> Self::AuthorityId; - -// /// Check a candidate for validity. -// fn check_validity(&self, candidate: &Self::ParachainCandidate) -> Self::CheckCandidate; - -// /// Check availability of candidate data. -// fn check_availability(&self, candidate: &Self::ParachainCandidate) -> Self::CheckAvailability; - -// /// Attempt to combine a set of parachain candidates into a proposal. -// /// -// /// This may arbitrarily return `None`, but the intent is for `Some` -// /// to only be returned when candidates from enough groups are known. -// /// -// /// "enough" may be subjective as well. -// fn create_proposal(&self, candidates: Vec<&Self::ParachainCandidate>) -// -> Option; - -// /// Check validity of a proposal. This should call out to the `check_candidate` -// /// function for all parachain candidates contained within it, as well as -// /// checking other validity constraints of the proposal. -// fn proposal_valid(&self, proposal: &Self::Proposal, check_candidate: F) -> bool -// where F: FnMut(&Self::ParachainCandidate) -> bool; - -// /// Get the local authority ID. -// fn local_id(&self) -> Self::AuthorityId; - -// /// Sign a table validity statement with the local key. -// fn sign_table_statement( -// &self, -// statement: &table::Statement -// ) -> Self::Signature; - -// /// Sign a BFT agreement message. -// fn sign_bft_message(&self, &bft::Message) -> Self::Signature; -// } - -// /// Helper for type resolution for contexts until type aliases apply bounds. -// pub trait TypeResolve { -// type SignedTableStatement; -// type BftCommunication; -// type BftCommitted; -// type Misbehavior; -// } - -// impl TypeResolve for C { -// type SignedTableStatement = table::SignedStatement; -// type BftCommunication = bft::Communication; -// type BftCommitted = bft::Committed; -// type Misbehavior = table::Misbehavior; -// } - -// /// Information about a specific group. -// #[derive(Debug, Clone)] -// pub struct GroupInfo { -// /// Authorities meant to check validity of candidates. -// pub validity_guarantors: HashSet, -// /// Authorities meant to check availability of candidate data. -// pub availability_guarantors: HashSet, -// /// Number of votes needed for validity. -// pub needed_validity: usize, -// /// Number of votes needed for availability. -// pub needed_availability: usize, -// } - -// struct TableContext { -// context: C, -// groups: HashMap>, -// } - -// impl ::std::ops::Deref for TableContext { -// type Target = C; - -// fn deref(&self) -> &C { -// &self.context -// } -// } - -// impl table::Context for TableContext { -// type AuthorityId = C::AuthorityId; -// type Digest = C::Digest; -// type GroupId = C::GroupId; -// type Signature = C::Signature; -// type Candidate = C::ParachainCandidate; - -// fn candidate_digest(candidate: &Self::Candidate) -> Self::Digest { -// C::candidate_digest(candidate) -// } - -// fn candidate_group(candidate: &Self::Candidate) -> Self::GroupId { -// C::candidate_group(candidate) -// } - -// fn is_member_of(&self, authority: &Self::AuthorityId, group: &Self::GroupId) -> bool { -// self.groups.get(group).map_or(false, |g| g.validity_guarantors.contains(authority)) -// } - -// fn is_availability_guarantor_of(&self, authority: &Self::AuthorityId, group: &Self::GroupId) -> bool { -// self.groups.get(group).map_or(false, |g| g.availability_guarantors.contains(authority)) -// } - -// fn requisite_votes(&self, group: &Self::GroupId) -> (usize, usize) { -// self.groups.get(group).map_or( -// (usize::max_value(), usize::max_value()), -// |g| (g.needed_validity, g.needed_availability), -// ) -// } -// } - -// // A shared table object. -// struct SharedTableInner { -// table: Table>, -// proposed_digest: Option, -// awaiting_proposal: Vec>, -// } - -// impl SharedTableInner { -// fn import_statement( -// &mut self, -// context: &TableContext, -// statement: ::SignedTableStatement, -// received_from: Option -// ) -> Option> { -// self.table.import_statement(context, statement, received_from) -// } - -// fn update_proposal(&mut self, context: &TableContext) { -// if self.awaiting_proposal.is_empty() { return } -// let proposal_candidates = self.table.proposed_candidates(context); -// if let Some(proposal) = context.context.create_proposal(proposal_candidates) { -// for sender in self.awaiting_proposal.drain(..) { -// let _ = sender.send(proposal.clone()); -// } -// } -// } - -// fn get_proposal(&mut self, context: &TableContext) -> oneshot::Receiver { -// let (tx, rx) = oneshot::channel(); -// self.awaiting_proposal.push(tx); -// self.update_proposal(context); -// rx -// } - -// fn proposal_valid(&mut self, context: &TableContext, proposal: &C::Proposal) -> bool { -// context.context.proposal_valid(proposal, |contained_candidate| { -// // check that the candidate is valid (has enough votes) -// let digest = C::candidate_digest(contained_candidate); -// self.table.candidate_includable(&digest, context) -// }) -// } -// } - -// /// A shared table object. -// pub struct SharedTable { -// context: Arc>, -// inner: Arc>>, -// } - -// impl Clone for SharedTable { -// fn clone(&self) -> Self { -// SharedTable { -// context: self.context.clone(), -// inner: self.inner.clone() -// } -// } -// } - -// impl SharedTable { -// /// Create a new shared table. -// pub fn new(context: C, groups: HashMap>) -> Self { -// SharedTable { -// context: Arc::new(TableContext { context, groups }), -// inner: Arc::new(Mutex::new(SharedTableInner { -// table: Table::default(), -// awaiting_proposal: Vec::new(), -// proposed_digest: None, -// })) -// } -// } - -// /// Import a single statement. -// pub fn import_statement( -// &self, -// statement: ::SignedTableStatement, -// received_from: Option, -// ) -> Option> { -// self.inner.lock().import_statement(&*self.context, statement, received_from) -// } - -// /// Sign and import a local statement. -// pub fn sign_and_import( -// &self, -// statement: table::Statement, -// ) -> Option> { -// let proposed_digest = match statement { -// table::Statement::Candidate(ref c) => Some(C::candidate_digest(c)), -// _ => None, -// }; - -// let signed_statement = table::SignedStatement { -// signature: self.context.sign_table_statement(&statement), -// sender: self.context.local_id(), -// statement, -// }; - -// let mut inner = self.inner.lock(); -// if proposed_digest.is_some() { -// inner.proposed_digest = proposed_digest; -// } - -// inner.import_statement(&*self.context, signed_statement, None) -// } - -// /// Import many statements at once. -// /// -// /// Provide an iterator yielding pairs of (statement, received_from). -// pub fn import_statements(&self, iterable: I) -> U -// where -// I: IntoIterator::SignedTableStatement, Option)>, -// U: ::std::iter::FromIterator>, -// { -// let mut inner = self.inner.lock(); - -// iterable.into_iter().filter_map(move |(statement, received_from)| { -// inner.import_statement(&*self.context, statement, received_from) -// }).collect() -// } - -// /// Update the proposal sealing. -// pub fn update_proposal(&self) { -// self.inner.lock().update_proposal(&*self.context) -// } - -// /// Register interest in receiving a proposal when ready. -// /// If one is ready immediately, it will be provided. -// pub fn get_proposal(&self) -> oneshot::Receiver { -// self.inner.lock().get_proposal(&*self.context) -// } - -// /// Check if a proposal is valid. -// pub fn proposal_valid(&self, proposal: &C::Proposal) -> bool { -// self.inner.lock().proposal_valid(&*self.context, proposal) -// } - -// /// Execute a closure using a specific candidate. -// /// -// /// Deadlocks if called recursively. -// pub fn with_candidate(&self, digest: &C::Digest, f: F) -> U -// where F: FnOnce(Option<&C::ParachainCandidate>) -> U -// { -// let inner = self.inner.lock(); -// f(inner.table.get_candidate(digest)) -// } - -// /// Get all witnessed misbehavior. -// pub fn get_misbehavior(&self) -> HashMap::Misbehavior> { -// self.inner.lock().table.get_misbehavior().clone() -// } - -// /// Fill a statement batch. -// pub fn fill_batch(&self, batch: &mut C::StatementBatch) { -// self.inner.lock().table.fill_batch(batch); -// } - -// /// Get the local proposed candidate digest. -// pub fn proposed_digest(&self) -> Option { -// self.inner.lock().proposed_digest.clone() -// } - -// // Get a handle to the table context. -// fn context(&self) -> &TableContext { -// &*self.context -// } -// } - - -// /// Parameters necessary for agreement. -// pub struct AgreementParams { -// /// The context itself. -// pub context: C, -// /// For scheduling timeouts. -// pub timer: Timer, -// /// The statement table. -// pub table: SharedTable, -// /// The number of nodes. -// pub nodes: usize, -// /// The maximum number of faulty nodes. -// pub max_faulty: usize, -// /// The round timeout multiplier: 2^round_number is multiplied by this. -// pub round_timeout_multiplier: u64, -// /// The maximum amount of messages to queue. -// pub message_buffer_size: usize, -// /// Interval to attempt forming proposals over. -// pub form_proposal_interval: Duration, -// } - -// /// Recovery for messages -// pub trait MessageRecovery { -// /// The unchecked message type. This implies that work hasn't been done -// /// to decode the payload and check and authenticate a signature. -// type UncheckedMessage; - -// /// Attempt to transform a checked message into an unchecked. -// fn check_message(&self, Self::UncheckedMessage) -> Option>; -// } - -// /// Recovered and fully checked messages. -// pub enum CheckedMessage { -// /// Messages meant for the BFT agreement logic. -// Bft(::BftCommunication), -// /// Statements circulating about the table. -// Table(Vec<::SignedTableStatement>), -// } - -// /// Outgoing messages to the network. -// #[derive(Debug, Clone)] -// pub enum OutgoingMessage { -// /// Messages meant for BFT agreement peers. -// Bft(::BftCommunication), -// /// Batches of table statements. -// Table(C::StatementBatch), -// } - -// /// Create an agreement future, and I/O streams. -// // TODO: kill 'static bounds and use impl Future. -// pub fn agree< -// Context, -// NetIn, -// NetOut, -// Recovery, -// PropagateStatements, -// LocalCandidate, -// Err, -// >( -// params: AgreementParams, -// net_in: NetIn, -// net_out: NetOut, -// recovery: Recovery, -// propagate_statements: PropagateStatements, -// local_candidate: LocalCandidate, -// ) -// -> Box::BftCommitted,Error=Error>> -// where -// Context: ::Context + 'static, -// Context::CheckCandidate: IntoFuture, -// Context::CheckAvailability: IntoFuture, -// NetIn: Stream),Error=Err> + 'static, -// NetOut: Sink> + 'static, -// Recovery: MessageRecovery + 'static, -// PropagateStatements: Stream + 'static, -// LocalCandidate: IntoFuture + 'static -// { -// let (bft_in_in, bft_in_out) = mpsc::unbounded(); -// let (bft_out_in, bft_out_out) = mpsc::unbounded(); - -// let agreement = { -// let bft_context = BftContext { -// context: params.context, -// table: params.table.clone(), -// timer: params.timer.clone(), -// round_timeout_multiplier: params.round_timeout_multiplier, -// }; - -// bft::agree( -// bft_context, -// params.nodes, -// params.max_faulty, -// bft_in_out.map(bft::ContextCommunication).map_err(|_| Error::IoTerminated), -// bft_out_in.sink_map_err(|_| Error::IoTerminated), -// ) -// }; - -// let route_messages_in = { -// let round_robin = round_robin::RoundRobinBuffer::new(net_in, params.message_buffer_size); - -// let round_robin_recovered = round_robin -// .filter_map(move |(sender, msg)| recovery.check_message(msg).map(move |x| (sender, x))); - -// handle_incoming::HandleIncoming::new( -// params.table.clone(), -// round_robin_recovered, -// bft_in_in, -// ).map_err(|_| Error::IoTerminated) -// }; - -// let route_messages_out = { -// let table = params.table.clone(); -// let periodic_table_statements = propagate_statements -// .or_else(|_| ::futures::future::empty()) // halt the stream instead of error. -// .map(move |mut batch| { table.fill_batch(&mut batch); batch }) -// .filter(|b| !b.is_empty()) -// .map(OutgoingMessage::Table); - -// let complete_out_stream = bft_out_out -// .map_err(|_| Error::IoTerminated) -// .map(|bft::ContextCommunication(x)| x) -// .map(OutgoingMessage::Bft) -// .select(periodic_table_statements); - -// net_out.sink_map_err(|_| Error::IoTerminated).send_all(complete_out_stream) -// }; - -// let import_local_candidate = { -// let table = params.table.clone(); -// local_candidate -// .into_future() -// .map(table::Statement::Candidate) -// .map(Some) -// .or_else(|_| Ok(None)) -// .map(move |s| if let Some(s) = s { -// table.sign_and_import(s); -// }) -// }; - -// let create_proposal_on_interval = { -// let table = params.table; -// params.timer.interval(params.form_proposal_interval) -// .map_err(|_| Error::FaultyTimer) -// .for_each(move |_| { table.update_proposal(); Ok(()) }) -// }; - -// // if these auxiliary futures terminate before the agreement, then -// // that is an error. -// let auxiliary_futures = route_messages_in.join4( -// create_proposal_on_interval, -// route_messages_out, -// import_local_candidate, -// ).and_then(|_| Err(Error::IoTerminated)); - -// let future = agreement -// .select(auxiliary_futures) -// .map(|(committed, _)| committed) -// .map_err(|(e, _)| e); - -// Box::new(future) -// } +// 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. +//! +//! Authorities are split into groups by parachain, and each authority might come +//! up its own candidate for their parachain. Within groups, authorities pass around +//! their candidates and produce statements of validity. +//! +//! Any candidate that receives majority approval by the authorities in a group +//! may be subject to inclusion, unless any authorities flag that candidate as invalid. +//! +//! Wrongly flagging as invalid should be strongly disincentivized, so that in the +//! equilibrium state it is not expected to happen. Likewise with the submission +//! of invalid blocks. +//! +//! Groups themselves may be compromised by malicious authorities. + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use codec::Slicable; +use table::Table; +use table::generic::Statement as GenericStatement; +use polkadot_primitives::Hash; +use polkadot_primitives::parachain::{Id as ParaId, CandidateReceipt}; +use primitives::block::Block as SubstrateBlock; +use primitives::AuthorityId; + +use parking_lot::Mutex; + +extern crate futures; +extern crate ed25519; +extern crate parking_lot; +extern crate tokio_timer; +extern crate polkadot_statement_table as table; +extern crate polkadot_primitives; +extern crate substrate_bft as bft; +extern crate substrate_codec as codec; +extern crate substrate_primitives as primitives; + +#[cfg(test)] +pub mod tests; + +/// Information about a specific group. +#[derive(Debug, Clone)] +pub struct GroupInfo { + /// Authorities meant to check validity of candidates. + pub validity_guarantors: HashSet, + /// Authorities meant to check availability of candidate data. + pub availability_guarantors: HashSet, + /// Number of votes needed for validity. + pub needed_validity: usize, + /// Number of votes needed for availability. + pub needed_availability: usize, +} + +struct TableContext { + parent_hash: Hash, + key: Arc, + groups: HashMap, +} + +impl table::Context for TableContext { + fn is_member_of(&self, authority: &AuthorityId, group: &ParaId) -> bool { + self.groups.get(group).map_or(false, |g| g.validity_guarantors.contains(authority)) + } + + fn is_availability_guarantor_of(&self, authority: &AuthorityId, group: &ParaId) -> bool { + self.groups.get(group).map_or(false, |g| g.availability_guarantors.contains(authority)) + } + + fn requisite_votes(&self, group: &ParaId) -> (usize, usize) { + self.groups.get(group).map_or( + (usize::max_value(), usize::max_value()), + |g| (g.needed_validity, g.needed_availability), + ) + } +} + +impl TableContext { + fn sign_statement(&self, statement: table::Statement) -> table::SignedStatement { + let signature = sign_table_statement(&statement, &self.key, &self.parent_hash); + let local_id = self.key.public().0; + + table::SignedStatement { + statement, + signature, + sender: local_id, + } + } +} + +/// Sign a table statement against a parent hash. +/// The actual message signed is the encoded statement concatenated with the +/// parent hash. +pub fn sign_table_statement(statement: &table::Statement, key: &ed25519::Pair, parent_hash: &Hash) -> ed25519::Signature { + use polkadot_primitives::parachain::Statement as RawStatement; + + let raw = match *statement { + GenericStatement::Candidate(ref c) => RawStatement::Candidate(c.clone()), + GenericStatement::Valid(h) => RawStatement::Valid(h), + GenericStatement::Invalid(h) => RawStatement::Invalid(h), + GenericStatement::Available(h) => RawStatement::Available(h), + }; + + let mut encoded = raw.encode(); + encoded.extend(&parent_hash.0); + + key.sign(&encoded) +} + +// A shared table object. +struct SharedTableInner { + table: Table, + proposed_digest: Option, +} + +impl SharedTableInner { + fn import_statement( + &mut self, + context: &TableContext, + statement: ::table::SignedStatement, + received_from: Option, + ) -> Option { + self.table.import_statement(context, statement, received_from) + } +} + +/// A shared table object. +pub struct SharedTable { + context: Arc, + inner: Arc>, +} + +impl Clone for SharedTable { + fn clone(&self) -> Self { + SharedTable { + context: self.context.clone(), + inner: self.inner.clone() + } + } +} + +impl SharedTable { + /// Create a new shared table. + /// + /// Provide the key to sign with, and the parent hash of the relay chain + /// block being built. + pub fn new(groups: HashMap, key: Arc, parent_hash: Hash) -> Self { + SharedTable { + context: Arc::new(TableContext { groups, key, parent_hash }), + inner: Arc::new(Mutex::new(SharedTableInner { + table: Table::default(), + proposed_digest: None, + })) + } + } + + /// Import a single statement. + pub fn import_statement( + &self, + statement: table::SignedStatement, + received_from: Option, + ) -> Option { + self.inner.lock().import_statement(&*self.context, statement, received_from) + } + + /// Sign and import a local statement. + pub fn sign_and_import( + &self, + statement: table::Statement, + ) -> Option { + let proposed_digest = match statement { + GenericStatement::Candidate(ref c) => Some(c.hash()), + _ => None, + }; + + let signed_statement = self.context.sign_statement(statement); + + let mut inner = self.inner.lock(); + if proposed_digest.is_some() { + inner.proposed_digest = proposed_digest; + } + + inner.import_statement(&*self.context, signed_statement, None) + } + + /// Import many statements at once. + /// + /// Provide an iterator yielding pairs of (statement, received_from). + pub fn import_statements(&self, iterable: I) -> U + where + I: IntoIterator)>, + U: ::std::iter::FromIterator, + { + let mut inner = self.inner.lock(); + + iterable.into_iter().filter_map(move |(statement, received_from)| { + inner.import_statement(&*self.context, statement, received_from) + }).collect() + } + + /// Check if a proposal is valid. + pub fn proposal_valid(&self, _proposal: &SubstrateBlock) -> bool { + false // TODO + } + + /// Execute a closure using a specific candidate. + /// + /// Deadlocks if called recursively. + pub fn with_candidate(&self, digest: &Hash, f: F) -> U + where F: FnOnce(Option<&CandidateReceipt>) -> U + { + let inner = self.inner.lock(); + f(inner.table.get_candidate(digest)) + } + + /// Get all witnessed misbehavior. + pub fn get_misbehavior(&self) -> HashMap { + self.inner.lock().table.get_misbehavior().clone() + } + + /// Fill a statement batch. + pub fn fill_batch(&self, batch: &mut B) { + self.inner.lock().table.fill_batch(batch); + } + + /// Get the local proposed block's hash. + pub fn proposed_hash(&self) -> Option { + self.inner.lock().proposed_digest.clone() + } +} diff --git a/polkadot/primitives/src/parachain.rs b/polkadot/primitives/src/parachain.rs index a334d026b20c6..a39904f44ec53 100644 --- a/polkadot/primitives/src/parachain.rs +++ b/polkadot/primitives/src/parachain.rs @@ -22,6 +22,7 @@ use primitives; use codec::{Input, Slicable, NonTrivialSlicable}; use rstd::cmp::{PartialOrd, Ord, Ordering}; use rstd::vec::Vec; +use ::Hash; /// Unique identifier of a parachain. #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -160,6 +161,41 @@ pub struct CandidateReceipt { pub fees: u64, } +impl Slicable for CandidateReceipt { + fn encode(&self) -> Vec { + let mut v = Vec::new(); + + self.parachain_index.using_encoded(|s| v.extend(s)); + self.collator.using_encoded(|s| v.extend(s)); + self.head_data.0.using_encoded(|s| v.extend(s)); + self.balance_uploads.using_encoded(|s| v.extend(s)); + self.egress_queue_roots.using_encoded(|s| v.extend(s)); + self.fees.using_encoded(|s| v.extend(s)); + + v + } + + fn decode(input: &mut I) -> Option { + Some(CandidateReceipt { + parachain_index: try_opt!(Slicable::decode(input)), + collator: try_opt!(Slicable::decode(input)), + head_data: try_opt!(Slicable::decode(input).map(HeadData)), + balance_uploads: try_opt!(Slicable::decode(input)), + egress_queue_roots: try_opt!(Slicable::decode(input)), + fees: try_opt!(Slicable::decode(input)), + }) + } +} + +impl CandidateReceipt { + /// Get the blake2_256 hash + #[cfg(feature = "std")] + pub fn hash(&self) -> Hash { + let encoded = self.encode(); + primitives::hashing::blake2_256(&encoded).into() + } +} + impl PartialOrd for CandidateReceipt { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -224,6 +260,74 @@ impl Slicable for Activity { } } +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +#[repr(u8)] +enum StatementKind { + Candidate = 1, + Valid = 2, + Invalid = 3, + Available = 4, +} + +/// Statements which can be made about parachain candidates. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub enum Statement { + /// Proposal of a parachain candidate. + Candidate(CandidateReceipt), + /// State that a parachain candidate is valid. + Valid(Hash), + /// Vote to commit to a candidate. + Invalid(Hash), + /// Vote to advance round after inactive primary. + Available(Hash), +} + +impl Slicable for Statement { + fn encode(&self) -> Vec { + let mut v = Vec::new(); + match *self { + Statement::Candidate(ref candidate) => { + v.push(StatementKind::Candidate as u8); + candidate.using_encoded(|s| v.extend(s)); + } + Statement::Valid(ref hash) => { + v.push(StatementKind::Valid as u8); + hash.using_encoded(|s| v.extend(s)); + } + Statement::Invalid(ref hash) => { + v.push(StatementKind::Invalid as u8); + hash.using_encoded(|s| v.extend(s)); + } + Statement::Available(ref hash) => { + v.push(StatementKind::Available as u8); + hash.using_encoded(|s| v.extend(s)); + } + } + + v + } + + fn decode(value: &mut I) -> Option { + match u8::decode(value) { + Some(x) if x == StatementKind::Candidate as u8 => { + Slicable::decode(value).map(Statement::Candidate) + } + Some(x) if x == StatementKind::Valid as u8 => { + Slicable::decode(value).map(Statement::Valid) + } + Some(x) if x == StatementKind::Invalid as u8 => { + Slicable::decode(value).map(Statement::Invalid) + } + Some(x) if x == StatementKind::Available as u8 => { + Slicable::decode(value).map(Statement::Available) + } + _ => None, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/polkadot/primitives/src/transaction.rs b/polkadot/primitives/src/transaction.rs index 24a3ae4ee3634..6279a2d0aa078 100644 --- a/polkadot/primitives/src/transaction.rs +++ b/polkadot/primitives/src/transaction.rs @@ -152,7 +152,6 @@ impl Slicable for Proposal { } } - /// Public functions that can be dispatched to. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] @@ -314,7 +313,7 @@ pub struct UncheckedTransaction { impl Slicable for UncheckedTransaction { fn decode(input: &mut I) -> Option { - // This is a little more complicated than usua since the binary format must be compatible + // This is a little more complicated than usual since the binary format must be compatible // with substrate's generic `Vec` type. Basically this just means accepting that there // will be a prefix of u32, which has the total number of bytes following (we don't need // to use this). diff --git a/polkadot/statement-table/src/lib.rs b/polkadot/statement-table/src/lib.rs index 86c2ec1952afe..e3abf95686900 100644 --- a/polkadot/statement-table/src/lib.rs +++ b/polkadot/statement-table/src/lib.rs @@ -36,13 +36,8 @@ pub type Misbehavior = generic::Misbehavior; - /// Context necessary to construct a table. pub trait Context { - /// get the digest of a candidate. - // TODO: remove this when hashing logic finalized. - fn candidate_digest(candidate: &CandidateReceipt) -> Hash; - /// Whether a authority is a member of a group. /// Members are meant to submit candidates and vote on validity. fn is_member_of(&self, authority: &SessionKey, group: &Id) -> bool; @@ -68,7 +63,7 @@ impl generic::Context for C { type Candidate = CandidateReceipt; fn candidate_digest(candidate: &CandidateReceipt) -> Hash { - ::candidate_digest(candidate) + candidate.hash() } fn candidate_group(candidate: &CandidateReceipt) -> Id { diff --git a/substrate/codec/src/slicable.rs b/substrate/codec/src/slicable.rs index fcce7806c1ad7..b2aeb08035bfa 100644 --- a/substrate/codec/src/slicable.rs +++ b/substrate/codec/src/slicable.rs @@ -53,6 +53,7 @@ pub trait Slicable: Sized { } /// Trait to mark that a type is not trivially (essentially "in place") serialisable. +// TODO: under specialization, remove this and simply specialize in place serializable types. pub trait NonTrivialSlicable: Slicable {} impl Slicable for T { @@ -213,6 +214,8 @@ macro_rules! tuple_impl { self.0.using_encoded(f) } } + + impl<$one: NonTrivialSlicable> NonTrivialSlicable for ($one,) { } }; ($first:ident, $($rest:ident,)+) => { impl<$first: Slicable, $($rest: Slicable),+> @@ -248,6 +251,11 @@ macro_rules! tuple_impl { } } + impl<$first: Slicable, $($rest: Slicable),+> + NonTrivialSlicable + for ($first, $($rest),+) + { } + tuple_impl!($($rest,)+); } } @@ -256,7 +264,7 @@ macro_rules! tuple_impl { mod inner_tuple_impl { use rstd::vec::Vec; - use super::{Input, Slicable}; + use super::{Input, Slicable, NonTrivialSlicable}; tuple_impl!(A, B, C, D, E, F, G, H, I, J, K,); } From 767a9d95508e2e159e1149e922115d2d7f47f40f Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 12 Feb 2018 20:40:59 +0100 Subject: [PATCH 10/21] test for preemption --- Cargo.lock | 3 + substrate/bft/Cargo.toml | 5 ++ substrate/bft/src/lib.rs | 119 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2191936c5f24..0af4fe42e80e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1402,8 +1402,11 @@ dependencies = [ "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "substrate-client 0.1.0", "substrate-codec 0.1.0", + "substrate-executor 0.1.0", + "substrate-keyring 0.1.0", "substrate-primitives 0.1.0", "substrate-state-machine 0.1.0", + "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/substrate/bft/Cargo.toml b/substrate/bft/Cargo.toml index 873fcad4530de..f1af1cda411ce 100644 --- a/substrate/bft/Cargo.toml +++ b/substrate/bft/Cargo.toml @@ -13,3 +13,8 @@ ed25519 = { path = "../ed25519" } tokio-timer = "0.1.2" parking_lot = "0.4" error-chain = "0.11" + +[dev-dependencies] +substrate-keyring = { path = "../keyring" } +substrate-executor = { path = "../executor" } +tokio-core = "0.1.12" diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index 0264cc934d4d6..f4b1f72a0112d 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -45,7 +45,7 @@ use primitives::block::{Block, Header, HeaderHash}; use primitives::AuthorityId; use state_machine::CodeExecutor; -use futures::{stream, task, Async, Sink, Future}; +use futures::{stream, task, Async, Sink, Future, IntoFuture}; use futures::future::Executor; use futures::sync::oneshot; use tokio_timer::Timer; @@ -83,7 +83,7 @@ pub type Communication = generic::Communication; + type CreateProposal: IntoFuture; /// Initialize the proposal logic on top of a specific header. // TODO: provide state context explicitly? @@ -147,15 +147,15 @@ impl generic::Context for BftInstance

{ type Digest = HeaderHash; type Signature = Signature; type Candidate = Block; - type RoundTimeout = Box>; - type CreateProposal = P::CreateProposal; + type RoundTimeout = Box + Send>; + type CreateProposal = ::Future; fn local_id(&self) -> AuthorityId { self.key.public().0 } - fn proposal(&self) -> P::CreateProposal { - self.proposer.propose() + fn proposal(&self) -> Self::CreateProposal { + self.proposer.propose().into_future() } fn candidate_digest(&self, proposal: &Block) -> HeaderHash { @@ -319,7 +319,6 @@ impl BftService /// This will begin the consensus process to build a block on top of it. /// If the executor fails to run the future, an error will be returned. pub fn build_upon(&self, header: &Header) -> Result<(), Error> { - let parent_hash = header.parent_hash.clone(); let hash = header.hash(); let mut _preempted_consensus = None; @@ -332,7 +331,7 @@ impl BftService let bft_instance = BftInstance { proposer, - parent_hash, + parent_hash: hash, round_timeout_multiplier: self.round_timeout_multiplier, timer: self.timer.clone(), key: self.key.clone(), @@ -364,9 +363,111 @@ impl BftService cancel, }); - _preempted_consensus = live.remove(&parent_hash); + // cancel any agreements attempted to build upon this block's parent + // as clearly agreement has already been reached. + _preempted_consensus = live.remove(&header.parent_hash); } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use primitives::block; + use self::tokio_core::reactor::{Core, Handle}; + use self::keyring::Keyring; + + extern crate substrate_keyring as keyring; + extern crate tokio_core; + + struct FakeClient { + authorities: Vec, + imported_heights: Mutex> + } + + impl BlockImport for FakeClient { + fn import_block(&self, block: Block, _justification: Justification) { + assert!(self.imported_heights.lock().insert(block.header.number)) + } + } + + impl Authorities for FakeClient { + fn authorities(&self, _at: &BlockId) -> Result, Error> { + Ok(self.authorities.clone()) + } + } + + struct DummyProposer(block::Number); + + impl Proposer for DummyProposer { + type CreateProposal = Result; + + fn init(parent_header: &Header, _sign_with: Arc) -> Self { + DummyProposer(parent_header.number + 1) + } + + fn propose(&self) -> Result { + Ok(Block { + header: Header::from_block_number(self.0), + transactions: Default::default() + }) + } + + fn evaluate(&self, proposal: &Block) -> bool { + proposal.header.number == self.0 + } + } + + fn make_service(client: FakeClient, handle: Handle) + -> BftService + { + BftService { + client: Arc::new(client), + executor: handle, + live_agreements: Mutex::new(HashMap::new()), + timer: Timer::default(), + round_timeout_multiplier: 4, + key: Arc::new(Keyring::One.into()), + _marker: Default::default(), + } + } + + #[test] + fn future_gets_preempted() { + let client = FakeClient { + authorities: vec![ + Keyring::One.to_raw_public(), + Keyring::Two.to_raw_public(), + Keyring::Alice.to_raw_public(), + Keyring::Eve.to_raw_public(), + ], + imported_heights: Mutex::new(HashSet::new()), + }; + + let mut core = Core::new().unwrap(); + + let service = make_service(client, core.handle()); + + let first = Header::from_block_number(2); + let first_hash = first.hash(); + + let mut second = Header::from_block_number(3); + second.parent_hash = first_hash; + let second_hash = second.hash(); + + service.build_upon(&first).unwrap(); + assert!(service.live_agreements.lock().contains_key(&first_hash)); + + // turn the core so the future gets polled and sends its task to the + // service. otherwise it deadlocks. + core.turn(Some(::std::time::Duration::from_millis(100))); + service.build_upon(&second).unwrap(); + assert!(!service.live_agreements.lock().contains_key(&first_hash)); + assert!(service.live_agreements.lock().contains_key(&second_hash)); + + core.turn(Some(::std::time::Duration::from_millis(100))); + } +} From 7fc4b4d62ff0002dbb9d7e39c9f7d6c2cf64ef74 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 12 Feb 2018 21:01:51 +0100 Subject: [PATCH 11/21] fix test build --- polkadot/consensus/src/lib.rs | 3 --- polkadot/statement-table/src/generic.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/polkadot/consensus/src/lib.rs b/polkadot/consensus/src/lib.rs index 26d8cfae9e275..f3e62ba1d56d2 100644 --- a/polkadot/consensus/src/lib.rs +++ b/polkadot/consensus/src/lib.rs @@ -52,9 +52,6 @@ extern crate substrate_bft as bft; extern crate substrate_codec as codec; extern crate substrate_primitives as primitives; -#[cfg(test)] -pub mod tests; - /// Information about a specific group. #[derive(Debug, Clone)] pub struct GroupInfo { diff --git a/polkadot/statement-table/src/generic.rs b/polkadot/statement-table/src/generic.rs index 13e582a33fc6b..11665fe114636 100644 --- a/polkadot/statement-table/src/generic.rs +++ b/polkadot/statement-table/src/generic.rs @@ -732,7 +732,7 @@ mod tests { pub items: Vec, } - impl ::StatementBatch for VecBatch { + impl ::generic::StatementBatch for VecBatch { fn targets(&self) -> &[V] { &self.targets } fn is_empty(&self) -> bool { self.items.is_empty() } fn push(&mut self, item: T) -> bool { From 9acd3f99abd7f592e44a738b645d7fc67ca9cb16 Mon Sep 17 00:00:00 2001 From: Gav Date: Mon, 12 Feb 2018 23:39:13 +0100 Subject: [PATCH 12/21] Fix wasm build --- .../release/polkadot_runtime.compact.wasm | Bin 75751 -> 75751 bytes .../release/polkadot_runtime.wasm | Bin 75800 -> 75800 bytes substrate/primitives/src/block.rs | 5 +++-- .../substrate_test_runtime.compact.wasm | Bin 31904 -> 31904 bytes .../release/substrate_test_runtime.wasm | Bin 31984 -> 31984 bytes 5 files changed, 3 insertions(+), 2 deletions(-) diff --git a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm index b4b77e5ec9e4c2a74a57d38ded342bd8de036607..84e52dbc50cfa544c0a8899aa70d307a4b0562d5 100644 GIT binary patch delta 120 zcmaEUp5^&@mJQNOOdMRBWtr}KFa~Y@>m|ZZ1uiXJic6JT+qz z4`c9VkJ9;!jQ*2fl;r`%!pmb>8T~hJt?^+3N-@>XRoPtq(2H;L{KZds7y~!AZcq>a VQp*m^<^W1NowStLeDuAN1pwW1EGqy2 delta 120 zcmaEUp5^&@mJQNOOk5nBWtr}KFa~b^>m|ZZ1uiXJqu>JT+qz z4`a|~kJ9;!jKPy%l;r`%!pmb>8G|=(t?^+3N-@>XRoVP<<5M2SfX(OkDF^_m59elc V0A+fvTS{!6zu1d!^U?Q876A6IEpz|? diff --git a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.wasm b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.wasm index 2b5124161a1f76f50f483c1c8bf74aa9942dd88a..a92614dcc2d6fc281b868d3754d5c972ece62333 100644 GIT binary patch delta 115 zcmbPnfn~-8mJKpYOdMRB<(T+A8G|?LdCM~aDL-E)DaL@!6Vpu?83Q*T%h<%j7_zyt zbUq_vz+}ntJfK)dc`PeX#j_e8CZLpY{alsJYaV*>ZN9MhDGy`N=4~4k1c21_1G71R P(#0n&B{qM1uVeuLwbv@f delta 114 zcmbPnfn~-8mJKpYOk5nB<(T+A8G|6^V%=|nCUB0x; zyu_T$;?(%0oc!c$2EEPtOsiBFjV9kxdjTX5tG@w~8#FS3q?BenqxR$~%^g6}SWAad zdvk(T594N4ul4+se?)(q{6^b}QGc_(&PGN?t;r8{BNz=fTj}Yt0d;1W*aOKGChvjd z3{#M~jb`~k@}rpvkW@3bWHj2GV4lj&s6Y9bQz4_)W(#LKW}sQ+ZtIvgzl(Xp&1ks! fcE)8U#vPOUqYO8*MSWu2ye`{OfYEjGhw2Rg!;Vyy delta 236 zcmZ4RlX1aM#toYoIk~yH*;!ecxY;M~X7tg}5>Bm1O)f1-jV~!m%qvbzF3HT#W6-N|is5sZ49@7w9J0X6Wt+5<^n*Y`lu)(vEwntMKw zoaSxTlkjcA07Ox@<=QMz_fysy6@tBrZ|L diff --git a/substrate/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.wasm b/substrate/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.wasm index ec5638e1c183ce98875d9e6fe4db58fd09e01f70..3329885da9a60eb0dbabb75aadba95405ccefe2a 100644 GIT binary patch delta 241 zcmezHlkvk(#tp|9IoY{^fSr|6^V%=|nC1HQD( zyu_T$;?(%0oc!c$2E)ynOb1jLO($!qzW|cL8gCd)CuwE^nOU0gjCzw#Y3=}$&00E) zdYd(ECn1N;;cU#B2*)H}CH>1gB f?aa$ej5{XZk22gG7WIj7^S^9I0Y=x!4mBG9@V`_w delta 235 zcmezHlkvk(#tp|9Ik~yH*;!ecxY;M4XDrgt6;7>4O)f1-jV~!m%qvbzF3HT#W68P8}k`IP1kAla;? z!)UU3gH{jYIll4X7d3)gDOBb$t&cd)+{$mAU5w z$!G2+KvLSnlF@i`s7ER{qv_<`!G(+ln{`9%n1N>Hgs)@XY@@TDfAg(O6{g9rt-o!S bihje*XtbF<{W8<$f7y-#jBb-1YBm4>k&#gr From ca5900fe5dd576c59be2131e8ca021b632507420 Mon Sep 17 00:00:00 2001 From: Gav Date: Tue, 13 Feb 2018 10:18:28 +0100 Subject: [PATCH 13/21] Bulid on any block --- substrate/client/src/block_builder.rs | 13 +++++++++---- substrate/client/src/lib.rs | 26 +++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/substrate/client/src/block_builder.rs b/substrate/client/src/block_builder.rs index ebd8ec6c47fe3..8c2d0c06288ae 100644 --- a/substrate/client/src/block_builder.rs +++ b/substrate/client/src/block_builder.rs @@ -42,24 +42,29 @@ impl BlockBuilder where E: CodeExecutor + Clone, error::Error: From<<::State as state_machine::backend::Backend>::Error>, { - /// Create a new instance of builder from the given client. + /// Create a new instance of builder from the given client, building on the latest block. pub fn new(client: &Client) -> error::Result { let best = (client.info().map(|i| i.chain.best_number)?..1) .find(|&n| if let Ok(BlockStatus::InChain) = client.block_status(&BlockId::Number(n)) { true } else { false }) .unwrap_or(0); + Self::at_block(&BlockId::Number(best), client) + } + /// Create a new instance of builder from the given client using a particular block's ID to + /// build upon. + pub fn at_block(block_id: &BlockId, client: &Client) -> error::Result { Ok(BlockBuilder { header: Header { - number: best + 1, - parent_hash: client.block_hash(best)?.expect("We already ascertained this is InChain before; qed"), + number: client.block_number_from_id(block_id)?.ok_or(error::ErrorKind::UnknownBlock(*block_id))? + 1, + parent_hash: client.block_hash_from_id(block_id)?.ok_or(error::ErrorKind::UnknownBlock(*block_id))?, state_root: Default::default(), transaction_root: Default::default(), digest: Default::default(), }, transactions: Default::default(), executor: client.clone_executor(), - state: client.state_at(&BlockId::Number(best))?, + state: client.state_at(block_id)?, changes: Default::default(), }) } diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index 1a7d4ceb622f2..f347dda3cb3d1 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -43,7 +43,6 @@ pub mod block_builder; pub use blockchain::Info as ChainInfo; pub use blockchain::BlockId; -pub use block_builder::BlockBuilder; use primitives::{block, AuthorityId}; use primitives::storage::{StorageKey, StorageData}; @@ -202,8 +201,13 @@ impl Client where } /// Create a new block, built on the head of the chain. - pub fn new_block(&self) -> error::Result> where E: Clone { - BlockBuilder::new(self) + pub fn new_block(&self) -> error::Result> where E: Clone { + block_builder::BlockBuilder::new(self) + } + + /// Create a new block, built on top of `parent`. + pub fn new_block_at(&self, parent: &BlockId) -> error::Result> where E: Clone { + block_builder::BlockBuilder::at_block(parent, &self) } /// Queue a block for import. @@ -249,6 +253,22 @@ impl Client where self.backend.blockchain().hash(block_number) } + /// Convert an arbitrary block ID into a block hash. + pub fn block_hash_from_id(&self, id: &BlockId) -> error::Result> { + match *id { + BlockId::Hash(h) => Ok(Some(h)), + BlockId::Number(n) => self.block_hash(n), + } + } + + /// Convert an arbitrary block ID into a block hash. + pub fn block_number_from_id(&self, id: &BlockId) -> error::Result> { + match *id { + BlockId::Hash(_) => Ok(self.header(id)?.map(|h| h.number)), + BlockId::Number(n) => Ok(Some(n)), + } + } + /// Get block header by id. pub fn header(&self, id: &BlockId) -> error::Result> { self.backend.blockchain().header(*id) From d11cfe1a87797059212cbfe5bf2dfb910cb330c5 Mon Sep 17 00:00:00 2001 From: Gav Date: Tue, 13 Feb 2018 11:54:24 +0100 Subject: [PATCH 14/21] Test for block builder. --- substrate/client/src/lib.rs | 24 +++++++++++++++++++++++ substrate/executor/src/native_executor.rs | 8 +++++++- substrate/executor/src/wasm_executor.rs | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index f347dda3cb3d1..046d84de5f421 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -306,10 +306,34 @@ mod tests { }; let client = new_in_mem(Executor::new(), prepare_genesis).unwrap(); + assert_eq!(client.info().unwrap().chain.best_number, 0); assert_eq!(client.authorities_at(&BlockId::Number(0)).unwrap(), vec![ Keyring::Alice.to_raw_public(), Keyring::Bob.to_raw_public(), Keyring::Charlie.to_raw_public() ]); } + + #[test] + fn block_builder_works() { + let genesis_config = GenesisConfig::new_simple(vec![ + Keyring::Alice.to_raw_public(), + Keyring::Bob.to_raw_public(), + Keyring::Charlie.to_raw_public() + ], 1000); + + let prepare_genesis = || { + let mut storage = genesis_config.genesis_map(); + let block = genesis::construct_genesis_block(&storage); + storage.extend(additional_storage_with_genesis(&block)); + (primitives::block::Header::decode(&mut block.header.encode().as_ref()).expect("to_vec() always gives a valid serialisation; qed"), storage.into_iter().collect()) + }; + let client = new_in_mem(Executor::new(), prepare_genesis).unwrap(); + + let builder = client.new_block().unwrap(); + let block = builder.bake().unwrap(); + client.import_block(block.header, Some(block.transactions)).unwrap(); + + assert_eq!(client.info().unwrap().chain.best_number, 1); + } } diff --git a/substrate/executor/src/native_executor.rs b/substrate/executor/src/native_executor.rs index 99773b9f761ba..a759c046c662b 100644 --- a/substrate/executor/src/native_executor.rs +++ b/substrate/executor/src/native_executor.rs @@ -45,12 +45,18 @@ pub trait NativeExecutionDispatch { /// A generic `CodeExecutor` implementation that uses a delegate to determine wasm code equivalence /// and dispatch to native code when possible, falling back on `WasmExecutor` when not. -#[derive(Default)] +#[derive(Debug, Default)] pub struct NativeExecutor { /// Dummy field to avoid the compiler complaining about us not using `D`. pub _dummy: ::std::marker::PhantomData, } +impl Clone for NativeExecutor { + fn clone(&self) -> Self { + NativeExecutor { _dummy: Default::default() } + } +} + impl CodeExecutor for NativeExecutor { type Error = Error; diff --git a/substrate/executor/src/wasm_executor.rs b/substrate/executor/src/wasm_executor.rs index eef0df976d54e..00bdb55865644 100644 --- a/substrate/executor/src/wasm_executor.rs +++ b/substrate/executor/src/wasm_executor.rs @@ -276,7 +276,7 @@ impl_function_executor!(this: FunctionExecutor<'e, E>, /// Wasm rust executor for contracts. /// /// Executes the provided code in a sandboxed wasm runtime. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct WasmExecutor; impl CodeExecutor for WasmExecutor { From b973cccdb7dc3f86383943365b64ece1f9d0aeda Mon Sep 17 00:00:00 2001 From: Gav Date: Tue, 13 Feb 2018 15:25:42 +0100 Subject: [PATCH 15/21] Block import tests for client. --- Cargo.lock | 2 +- substrate/client/Cargo.toml | 2 +- substrate/client/src/backend.rs | 14 +-- substrate/client/src/in_mem.rs | 35 ++++--- substrate/client/src/lib.rs | 124 ++++++++++++++++++++--- substrate/state-machine/src/backend.rs | 2 +- substrate/state-machine/src/lib.rs | 29 +++--- substrate/test-runtime/src/genesismap.rs | 5 +- 8 files changed, 159 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e54ac6c5e8e2..cda0cd782e20b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1391,8 +1391,8 @@ dependencies = [ "substrate-executor 0.1.0", "substrate-keyring 0.1.0", "substrate-primitives 0.1.0", + "substrate-runtime-io 0.1.0", "substrate-runtime-support 0.1.0", - "substrate-serializer 0.1.0", "substrate-state-machine 0.1.0", "substrate-test-runtime 0.1.0", "triehash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index b9e0135b9e97a..8d3b4199910ea 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -13,8 +13,8 @@ ed25519 = { path = "../ed25519" } substrate-codec = { path = "../codec" } substrate-executor = { path = "../executor" } substrate-primitives = { path = "../primitives" } +substrate-runtime-io = { path = "../runtime-io" } substrate-runtime-support = { path = "../runtime-support" } -substrate-serializer = { path = "../serializer" } substrate-state-machine = { path = "../state-machine" } substrate-test-runtime = { path = "../test-runtime" } substrate-keyring = { path = "../../substrate/keyring" } diff --git a/substrate/client/src/backend.rs b/substrate/client/src/backend.rs index fad0f203e8c64..4421c82b3742f 100644 --- a/substrate/client/src/backend.rs +++ b/substrate/client/src/backend.rs @@ -21,22 +21,24 @@ use error; use primitives::block; use blockchain::{self, BlockId}; -/// Block insertion transction. Keeps hold if the inserted block state and data. +/// Block insertion operation. Keeps hold if the inserted block state and data. pub trait BlockImportOperation { /// Associated state backend type. type State: state_machine::backend::Backend; /// Returns pending state. - fn state(&self) -> error::Result; + fn state(&self) -> error::Result<&Self::State>; /// Append block data to the transaction. - fn import_block(&mut self, header: block::Header, body: Option, is_new_best: bool) -> error::Result<()>; + fn set_block_data(&mut self, header: block::Header, body: Option, is_new_best: bool) -> error::Result<()>; + /// Inject storage data into the database. + fn set_storage, Vec)>>(&mut self, iter: I) -> error::Result<()>; /// Inject storage data into the database. fn reset_storage, Vec)>>(&mut self, iter: I) -> error::Result<()>; } /// Client backend. Manages the data layer. pub trait Backend { - /// Associated block insertion transaction type. + /// Associated block insertion operation type. type BlockImportOperation: BlockImportOperation; /// Associated blockchain backend type. type Blockchain: blockchain::Backend; @@ -44,9 +46,9 @@ pub trait Backend { type State: state_machine::backend::Backend; /// Begin a new block insertion transaction with given parent block id. - fn begin_transaction(&self, block: BlockId) -> error::Result; + fn begin_operation(&self, block: BlockId) -> error::Result; /// Commit block insertion. - fn commit_transaction(&self, transaction: Self::BlockImportOperation) -> error::Result<()>; + fn commit_operation(&self, transaction: Self::BlockImportOperation) -> error::Result<()>; /// Returns reference to blockchain backend. fn blockchain(&self) -> &Self::Blockchain; /// Returns state backend for specified block. diff --git a/substrate/client/src/in_mem.rs b/substrate/client/src/in_mem.rs index ce1aaf3db5d71..69afbb10b6383 100644 --- a/substrate/client/src/in_mem.rs +++ b/substrate/client/src/in_mem.rs @@ -21,13 +21,13 @@ use parking_lot::RwLock; use state_machine; use error; use backend; -use primitives; -use ser; +use runtime_support::Hashable; use primitives::block::{self, HeaderHash}; use blockchain::{self, BlockId, BlockStatus}; +use state_machine::backend::Backend as StateBackend; fn header_hash(header: &block::Header) -> block::HeaderHash { - primitives::hashing::blake2_256(&ser::encode(header)).into() + header.blake2_256().into() } struct PendingBlock { @@ -41,7 +41,7 @@ struct Block { body: Option, } -/// In-memory transaction. +/// In-memory operation. pub struct BlockImportOperation { pending_block: Option, pending_state: state_machine::backend::InMemory, @@ -156,12 +156,12 @@ impl blockchain::Backend for Blockchain { impl backend::BlockImportOperation for BlockImportOperation { type State = state_machine::backend::InMemory; - fn state(&self) -> error::Result { - Ok(self.pending_state.clone()) + fn state(&self) -> error::Result<&Self::State> { + Ok(&self.pending_state) } - fn import_block(&mut self, header: block::Header, body: Option, is_new_best: bool) -> error::Result<()> { - assert!(self.pending_block.is_none(), "Only one block per transaction is allowed"); + fn set_block_data(&mut self, header: block::Header, body: Option, is_new_best: bool) -> error::Result<()> { + assert!(self.pending_block.is_none(), "Only one block per operation is allowed"); self.pending_block = Some(PendingBlock { block: Block { header: header, @@ -172,6 +172,11 @@ impl backend::BlockImportOperation for BlockImportOperation { Ok(()) } + fn set_storage, Vec)>>(&mut self, changes: I) -> error::Result<()> { + self.pending_state.commit(changes); + Ok(()) + } + fn reset_storage, Vec)>>(&mut self, iter: I) -> error::Result<()> { self.pending_state = state_machine::backend::InMemory::from(iter.collect()); Ok(()) @@ -214,10 +219,10 @@ impl Backend { }; edit_header(&mut header); - let mut tx = self.begin_transaction(BlockId::Hash(best_hash)).expect("In-memory backend does not fail"); + let mut tx = self.begin_operation(BlockId::Hash(best_hash)).expect("In-memory backend does not fail"); best_hash = header_hash(&header); - tx.import_block(header, None, true).expect("In-memory backend does not fail"); - self.commit_transaction(tx).expect("In-memory backend does not fail"); + tx.set_block_data(header, Some(vec![]), true).expect("In-memory backend does not fail"); + self.commit_operation(tx).expect("In-memory backend does not fail"); } } @@ -232,7 +237,7 @@ impl backend::Backend for Backend { type Blockchain = Blockchain; type State = state_machine::backend::InMemory; - fn begin_transaction(&self, block: BlockId) -> error::Result { + fn begin_operation(&self, block: BlockId) -> error::Result { let state = match block { BlockId::Hash(h) if h.is_zero() => Self::State::default(), _ => self.state_at(block)?, @@ -244,10 +249,10 @@ impl backend::Backend for Backend { }) } - fn commit_transaction(&self, transaction: Self::BlockImportOperation) -> error::Result<()> { - if let Some(pending_block) = transaction.pending_block { + fn commit_operation(&self, operation: Self::BlockImportOperation) -> error::Result<()> { + if let Some(pending_block) = operation.pending_block { let hash = header_hash(&pending_block.block.header); - self.states.write().insert(hash, transaction.pending_state); + self.states.write().insert(hash, operation.pending_state); self.blockchain.insert(hash, pending_block.block.header, pending_block.block.body, pending_block.is_best); } Ok(()) diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index 046d84de5f421..62df5390654a1 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -18,13 +18,13 @@ #![warn(missing_docs)] +extern crate substrate_runtime_support as runtime_support; +extern crate substrate_runtime_io as runtime_io; extern crate substrate_primitives as primitives; extern crate substrate_state_machine as state_machine; -extern crate substrate_serializer as ser; extern crate substrate_codec as codec; #[cfg(test)] #[macro_use] extern crate substrate_executor as executor; extern crate ed25519; -#[cfg(test)] extern crate substrate_runtime_support as runtime_support; #[cfg(test)] extern crate substrate_test_runtime as test_runtime; #[cfg(test)] extern crate substrate_keyring as keyring; @@ -51,6 +51,7 @@ use codec::{KeyedVec, Slicable}; use blockchain::Backend as BlockchainBackend; use backend::BlockImportOperation; use state_machine::backend::Backend as StateBackend; +use state_machine::{Ext, OverlayedChanges}; /// Polkadot Client #[derive(Debug)] @@ -136,10 +137,10 @@ impl Client where if backend.blockchain().header(BlockId::Number(0))?.is_none() { trace!("Empty database, writing genesis block"); let (genesis_header, genesis_store) = build_genesis(); - let mut tx = backend.begin_transaction(BlockId::Hash(block::HeaderHash::default()))?; - tx.reset_storage(genesis_store.into_iter())?; - tx.import_block(genesis_header, None, true)?; - backend.commit_transaction(tx)?; + let mut op = backend.begin_operation(BlockId::Hash(block::HeaderHash::default()))?; + op.reset_storage(genesis_store.into_iter())?; + op.set_block_data(genesis_header, Some(vec![]), true)?; + backend.commit_operation(op)?; } Ok(Client { backend, @@ -200,6 +201,23 @@ impl Client where Ok(CallResult { return_data, changes }) } + /// Set up the native execution environment to call into a native runtime code. + pub fn using_environment T, T>( + &self, f: F + ) -> error::Result { + self.using_environment_at(&BlockId::Number(self.info()?.chain.best_number), &mut Default::default(), f) + } + + /// Set up the native execution environment to call into a native runtime code. + pub fn using_environment_at T, T>( + &self, + id: &BlockId, + overlay: &mut OverlayedChanges, + f: F + ) -> error::Result { + Ok(runtime_io::with_externalities(&mut Ext { backend: &self.state_at(id)?, overlay }, f)) + } + /// Create a new block, built on the head of the chain. pub fn new_block(&self) -> error::Result> where E: Clone { block_builder::BlockBuilder::new(self) @@ -219,13 +237,21 @@ impl Client where blockchain::BlockStatus::Unknown => return Ok(ImportResult::UnknownParent), } - let mut transaction = self.backend.begin_transaction(BlockId::Hash(header.parent_hash))?; - let mut _state = transaction.state()?; - // TODO: execute block on _state + let mut transaction = self.backend.begin_operation(BlockId::Hash(header.parent_hash))?; + let mut overlay = OverlayedChanges::default(); + + state_machine::execute( + transaction.state()?, + &mut overlay, + &self.executor, + "execute_block", + &block::Block { header: header.clone(), transactions: body.clone().unwrap_or_default().clone() }.encode() + )?; let is_new_best = header.number == self.backend.blockchain().info()?.best_number + 1; - transaction.import_block(header, body, is_new_best)?; - self.backend.commit_transaction(transaction)?; + transaction.set_block_data(header, body, is_new_best)?; + transaction.set_storage(overlay.drain())?; + self.backend.commit_operation(transaction)?; Ok(ImportResult::Queued) } @@ -284,12 +310,37 @@ impl Client where mod tests { use super::*; use codec::Slicable; + use runtime_support::Hashable; use keyring::Keyring; + use primitives::block::Transaction as PrimitiveTransaction; use test_runtime::genesismap::{GenesisConfig, additional_storage_with_genesis}; + use test_runtime::{UncheckedTransaction, Transaction}; use test_runtime; native_executor_instance!(Executor, test_runtime::api::dispatch, include_bytes!("../../test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.compact.wasm")); + fn genesis_config() -> GenesisConfig { + GenesisConfig::new_simple(vec![ + Keyring::Alice.to_raw_public(), + Keyring::Bob.to_raw_public(), + Keyring::Charlie.to_raw_public() + ], 1000) + } + + fn prepare_genesis() -> (primitives::block::Header, Vec<(Vec, Vec)>) { + let mut storage = genesis_config().genesis_map(); + let block = genesis::construct_genesis_block(&storage); + storage.extend(additional_storage_with_genesis(&block)); + (primitives::block::Header::decode(&mut block.header.encode().as_ref()).expect("to_vec() always gives a valid serialisation; qed"), storage.into_iter().collect()) + } + + #[test] + fn client_initialises_from_genesis_ok() { + let client = new_in_mem(Executor::new(), prepare_genesis).unwrap(); + + assert_eq!(client.block_hash_from_id(&BlockId::Number(0)).unwrap().unwrap().0, prepare_genesis().0.blake2_256()); + } + #[test] fn authorities_call_works() { let genesis_config = GenesisConfig::new_simple(vec![ @@ -315,7 +366,7 @@ mod tests { } #[test] - fn block_builder_works() { + fn block_builder_works_with_no_transactions() { let genesis_config = GenesisConfig::new_simple(vec![ Keyring::Alice.to_raw_public(), Keyring::Bob.to_raw_public(), @@ -330,10 +381,59 @@ mod tests { }; let client = new_in_mem(Executor::new(), prepare_genesis).unwrap(); + let genesis_hash = genesis::construct_genesis_block(&genesis_config.genesis_map()).header.blake2_256(); + assert_eq!(client.using_environment(|| test_runtime::system::latest_block_hash()).unwrap().0, genesis_hash); + let builder = client.new_block().unwrap(); let block = builder.bake().unwrap(); + + assert_eq!(block.header.parent_hash.0, genesis_hash); + + client.import_block(block.header, Some(block.transactions)).unwrap(); + + assert_eq!(client.info().unwrap().chain.best_number, 1); + } + + trait Signable { + fn signed(self) -> PrimitiveTransaction; + } + impl Signable for Transaction { + fn signed(self) -> PrimitiveTransaction { + let signature = Keyring::from_raw_public(self.from.clone()).unwrap().sign(&self.encode()); + PrimitiveTransaction::decode(&mut UncheckedTransaction { signature, tx: self }.encode().as_ref()).unwrap() + } + } + + #[test] + fn block_builder_works_with_transactions() { + let genesis_config = GenesisConfig::new_simple(vec![ + Keyring::Alice.to_raw_public(), + Keyring::Bob.to_raw_public(), + Keyring::Charlie.to_raw_public() + ], 1000); + + let prepare_genesis = || { + let mut storage = genesis_config.genesis_map(); + let block = genesis::construct_genesis_block(&storage); + storage.extend(additional_storage_with_genesis(&block)); + (primitives::block::Header::decode(&mut block.header.encode().as_ref()).expect("to_vec() always gives a valid serialisation; qed"), storage.into_iter().collect()) + }; + let client = new_in_mem(Executor::new(), prepare_genesis).unwrap(); + + let mut builder = client.new_block().unwrap(); + + builder.push(Transaction { + from: Keyring::Alice.to_raw_public(), + to: Keyring::Ferdie.to_raw_public(), + amount: 42, + nonce: 0 + }.signed()).unwrap(); + let block = builder.bake().unwrap(); client.import_block(block.header, Some(block.transactions)).unwrap(); assert_eq!(client.info().unwrap().chain.best_number, 1); + assert!(client.state_at(&BlockId::Number(1)).unwrap() != client.state_at(&BlockId::Number(0)).unwrap()); + assert_eq!(client.using_environment(|| test_runtime::system::balance_of(Keyring::Alice.to_raw_public())).unwrap(), 958); + assert_eq!(client.using_environment(|| test_runtime::system::balance_of(Keyring::Ferdie.to_raw_public())).unwrap(), 42); } } diff --git a/substrate/state-machine/src/backend.rs b/substrate/state-machine/src/backend.rs index a1dec6dc3a575..f77e9a59d811d 100644 --- a/substrate/state-machine/src/backend.rs +++ b/substrate/state-machine/src/backend.rs @@ -57,7 +57,7 @@ impl error::Error for Void { /// In-memory backend. Fully recomputes tries on each commit but useful for /// tests. -#[derive(Default, Clone)] +#[derive(Debug, PartialEq, Default, Clone)] pub struct InMemory { inner: MemoryState, // keeps all the state in memory. } diff --git a/substrate/state-machine/src/lib.rs b/substrate/state-machine/src/lib.rs index 33dd346c1cd25..a3f2e3dc4b898 100644 --- a/substrate/state-machine/src/lib.rs +++ b/substrate/state-machine/src/lib.rs @@ -42,13 +42,10 @@ pub use testing::TestExternalities; pub use ext::Ext; /// Updates to be committed to the state. -pub enum Update { - /// Set storage of object at given key -- empty is deletion. - Storage(Vec, Vec), -} +pub type Update = (Vec, Vec); // in-memory section of the state. -#[derive(Default, Clone)] +#[derive(Debug, PartialEq, Default, Clone)] struct MemoryState { storage: HashMap, Vec>, } @@ -63,15 +60,11 @@ impl MemoryState { } fn update(&mut self, changes: I) where I: IntoIterator { - for update in changes { - match update { - Update::Storage(key, val) => { - if val.is_empty() { - self.storage.remove(&key); - } else { - self.storage.insert(key, val); - } - } + for (key, val) in changes { + if val.is_empty() { + self.storage.remove(&key); + } else { + self.storage.insert(key, val); } } } @@ -105,10 +98,12 @@ impl OverlayedChanges { /// Commit prospective changes to state. pub fn commit_prospective(&mut self) { - let storage_updates = self.prospective.storage.drain() - .map(|(key, value)| Update::Storage(key, value)); + self.committed.update(self.prospective.storage.drain()); + } - self.committed.update(storage_updates); + /// Drain prospective changes to an iterator. + pub fn drain(&mut self) -> ::std::collections::hash_map::Drain, std::vec::Vec> { + self.committed.storage.drain() } } diff --git a/substrate/test-runtime/src/genesismap.rs b/substrate/test-runtime/src/genesismap.rs index 0dc01c0abce03..1bc24d265763e 100644 --- a/substrate/test-runtime/src/genesismap.rs +++ b/substrate/test-runtime/src/genesismap.rs @@ -62,7 +62,10 @@ macro_rules! map { pub fn additional_storage_with_genesis(genesis_block: &Block) -> HashMap, Vec> { use codec::Slicable; + use primitives::hexdisplay::HexDisplay; + println!("genesis hash {}", HexDisplay::from(&genesis_block.header.blake2_256())); + println!("genesis {}", HexDisplay::from(&genesis_block.header.encode())); map![ - twox_128(&b"latest"[..]).encode() => genesis_block.header.blake2_256().encode() + twox_128(&b"latest"[..]).to_vec() => genesis_block.header.blake2_256().to_vec() ] } From ec618654248bb6567425e53458a78841e10cfaa7 Mon Sep 17 00:00:00 2001 From: Gav Date: Tue, 13 Feb 2018 15:31:05 +0100 Subject: [PATCH 16/21] Tidy ups --- substrate/client/src/lib.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index 62df5390654a1..947732ee8070e 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -310,7 +310,6 @@ impl Client where mod tests { use super::*; use codec::Slicable; - use runtime_support::Hashable; use keyring::Keyring; use primitives::block::Transaction as PrimitiveTransaction; use test_runtime::genesismap::{GenesisConfig, additional_storage_with_genesis}; @@ -337,8 +336,11 @@ mod tests { #[test] fn client_initialises_from_genesis_ok() { let client = new_in_mem(Executor::new(), prepare_genesis).unwrap(); + let genesis_hash = client.block_hash(0).unwrap().unwrap(); - assert_eq!(client.block_hash_from_id(&BlockId::Number(0)).unwrap().unwrap().0, prepare_genesis().0.blake2_256()); + assert_eq!(client.using_environment(|| test_runtime::system::latest_block_hash()).unwrap(), genesis_hash); + assert_eq!(client.using_environment(|| test_runtime::system::balance_of(Keyring::Alice.to_raw_public())).unwrap(), 1000); + assert_eq!(client.using_environment(|| test_runtime::system::balance_of(Keyring::Ferdie.to_raw_public())).unwrap(), 0); } #[test] @@ -381,17 +383,13 @@ mod tests { }; let client = new_in_mem(Executor::new(), prepare_genesis).unwrap(); - let genesis_hash = genesis::construct_genesis_block(&genesis_config.genesis_map()).header.blake2_256(); - assert_eq!(client.using_environment(|| test_runtime::system::latest_block_hash()).unwrap().0, genesis_hash); - let builder = client.new_block().unwrap(); let block = builder.bake().unwrap(); - assert_eq!(block.header.parent_hash.0, genesis_hash); - client.import_block(block.header, Some(block.transactions)).unwrap(); assert_eq!(client.info().unwrap().chain.best_number, 1); + assert_eq!(client.using_environment(|| test_runtime::system::latest_block_hash()).unwrap(), client.block_hash(1).unwrap().unwrap()); } trait Signable { From 23638cd05f249e0089253b13f40a85af7cb0b450 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 15 Feb 2018 03:19:55 +0100 Subject: [PATCH 17/21] clean up block builder instantiation --- substrate/client/src/block_builder.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/substrate/client/src/block_builder.rs b/substrate/client/src/block_builder.rs index 8c2d0c06288ae..323fb61c29fc1 100644 --- a/substrate/client/src/block_builder.rs +++ b/substrate/client/src/block_builder.rs @@ -21,7 +21,7 @@ use codec::{Joiner, Slicable}; use state_machine::{self, CodeExecutor}; use primitives::{Header, Block}; use primitives::block::Transaction; -use {backend, error, BlockId, BlockStatus, Client}; +use {backend, error, BlockId, Client}; use triehash::ordered_trie_root; /// Utility for building new (valid) blocks from a stream of transactions. @@ -44,11 +44,7 @@ impl BlockBuilder where { /// Create a new instance of builder from the given client, building on the latest block. pub fn new(client: &Client) -> error::Result { - let best = (client.info().map(|i| i.chain.best_number)?..1) - .find(|&n| if let Ok(BlockStatus::InChain) = client.block_status(&BlockId::Number(n)) - { true } else { false }) - .unwrap_or(0); - Self::at_block(&BlockId::Number(best), client) + client.info().and_then(|i| Self::at_block(&BlockId::Hash(i.chain.best_hash), client)) } /// Create a new instance of builder from the given client using a particular block's ID to @@ -75,7 +71,7 @@ impl BlockBuilder where pub fn push(&mut self, tx: Transaction) -> error::Result<()> { let output = state_machine::execute(&self.state, &mut self.changes, &self.executor, "execute_transaction", &vec![].and(&self.header).and(&tx))?; - self.header = Header::decode(&mut &output[..]).expect("Header came straight out of runtime do must be valid"); + self.header = Header::decode(&mut &output[..]).expect("Header came straight out of runtime so must be valid"); self.transactions.push(tx); Ok(()) } @@ -85,7 +81,7 @@ impl BlockBuilder where self.header.transaction_root = ordered_trie_root(self.transactions.iter().map(Slicable::encode)).0.into(); let output = state_machine::execute(&self.state, &mut self.changes, &self.executor, "finalise_block", &self.header.encode())?; - self.header = Header::decode(&mut &output[..]).expect("Header came straight out of runtime do must be valid"); + self.header = Header::decode(&mut &output[..]).expect("Header came straight out of runtime so must be valid"); Ok(Block { header: self.header, transactions: self.transactions, From 340ce39a66915acfca42fa54e21bf5b1ea63d04a Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 15 Feb 2018 04:03:57 +0100 Subject: [PATCH 18/21] justification verification logic --- substrate/bft/src/lib.rs | 89 +++++++++++++++++++++++++++++++----- substrate/ed25519/src/lib.rs | 19 +++++++- 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index f4b1f72a0112d..5a13224665497 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -40,7 +40,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use client::{BlockId, Client}; use client::backend::Backend; use codec::Slicable; -use ed25519::Signature; +use ed25519::LocalizedSignature; +use primitives::bft::{Message as PrimitiveMessage, Action as PrimitiveAction}; use primitives::block::{Block, Header, HeaderHash}; use primitives::AuthorityId; use state_machine::CodeExecutor; @@ -63,20 +64,23 @@ pub type LocalizedMessage = generic::LocalizedMessage< Block, HeaderHash, AuthorityId, - Signature + LocalizedSignature >; /// Justification of some hash. -pub type Justification = generic::Justification; +pub type Justification = generic::Justification; /// Justification of a prepare message. -pub type PrepareJustification = generic::PrepareJustification; +pub type PrepareJustification = generic::PrepareJustification; + +/// Unchecked justification. +pub type UncheckedJustification = generic::UncheckedJustification; /// Result of a committed round of BFT -pub type Committed = generic::Committed; +pub type Committed = generic::Committed; /// Communication between BFT participants. -pub type Communication = generic::Communication; +pub type Communication = generic::Communication; /// Logic for a proposer. /// @@ -131,7 +135,6 @@ impl Authorities for Client } } - /// Instance of BFT agreement. struct BftInstance

{ key: Arc, @@ -145,7 +148,7 @@ struct BftInstance

{ impl generic::Context for BftInstance

{ type AuthorityId = AuthorityId; type Digest = HeaderHash; - type Signature = Signature; + type Signature = LocalizedSignature; type Candidate = Block; type RoundTimeout = Box + Send>; type CreateProposal = ::Future; @@ -163,8 +166,6 @@ impl generic::Context for BftInstance

{ } fn sign_local(&self, message: Message) -> LocalizedMessage { - use primitives::bft::{Message as PrimitiveMessage, Action as PrimitiveAction}; - let action = match message.clone() { ::generic::Message::Propose(r, p) => PrimitiveAction::Propose(r as u32, p), ::generic::Message::Prepare(r, h) => PrimitiveAction::Prepare(r as u32, h), @@ -178,7 +179,10 @@ impl generic::Context for BftInstance

{ }; let to_sign = Slicable::encode(&primitive); - let signature = self.key.sign(&to_sign); + let signature = LocalizedSignature { + signer: self.key.public(), + signature: self.key.sign(&to_sign), + }; LocalizedMessage { message, @@ -327,7 +331,7 @@ impl BftService // TODO: check key is one of the authorities. let authorities = self.client.authorities(&BlockId::Hash(hash))?; let n = authorities.len(); - let max_faulty = n.saturating_sub(1) / 3; + let max_faulty = max_faulty_of(n); let bft_instance = BftInstance { proposer, @@ -372,6 +376,57 @@ impl BftService } } +/// Given a total number of authorities, yield the maximum faulty that would be allowed. +/// This will always be under 1/3. +pub fn max_faulty_of(n: usize) -> usize { + n.saturating_sub(1) / 3 +} + +fn check_justification_signed_message(authorities: &[AuthorityId], message: &[u8], just: UncheckedJustification) + -> Result +{ + just.check(authorities.len() - max_faulty_of(authorities.len()), |_, _, sig| { + let auth_id = sig.signer.0; + if !authorities.contains(&auth_id) { return None } + + if ed25519::verify_strong(&sig.signature, message, &sig.signer) { + Some(sig.signer.0) + } else { + None + } + }) +} + +/// Check a full justification for a header hash. +/// Provide all valid authorities. +/// +/// On failure, returns the justification back. +pub fn check_justification(authorities: &[AuthorityId], parent: HeaderHash, just: UncheckedJustification) + -> Result +{ + let message = Slicable::encode(&PrimitiveMessage { + parent, + action: PrimitiveAction::Commit(just.round_number as u32, just.digest), + }); + + check_justification_signed_message(authorities, &message[..], just) +} + +/// Check a prepare justification for a header hash. +/// Provide all valid authorities. +/// +/// On failure, returns the justification back. +pub fn check_prepare_justification(authorities: &[AuthorityId], parent: HeaderHash, just: UncheckedJustification) + -> Result +{ + let message = Slicable::encode(&PrimitiveMessage { + parent, + action: PrimitiveAction::Prepare(just.round_number as u32, just.digest), + }); + + check_justification_signed_message(authorities, &message[..], just) +} + #[cfg(test)] mod tests { use super::*; @@ -470,4 +525,14 @@ mod tests { core.turn(Some(::std::time::Duration::from_millis(100))); } + + #[test] + fn max_faulty() { + assert_eq!(max_faulty_of(3), 0); + assert_eq!(max_faulty_of(4), 1); + assert_eq!(max_faulty_of(100), 33); + assert_eq!(max_faulty_of(0), 0); + assert_eq!(max_faulty_of(11), 3); + assert_eq!(max_faulty_of(99), 32); + } } diff --git a/substrate/ed25519/src/lib.rs b/substrate/ed25519/src/lib.rs index 30496adefff22..3d5e53937fedd 100644 --- a/substrate/ed25519/src/lib.rs +++ b/substrate/ed25519/src/lib.rs @@ -24,9 +24,18 @@ extern crate untrusted; use ring::{rand, signature}; use primitives::hash::H512; -/// Alias to 520-bit hash when used in the context of a signature on the relay chain. +/// Alias to 512-bit hash when used in the context of a signature on the relay chain. pub type Signature = H512; +/// A localized signature also contains sender information. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct LocalizedSignature { + /// The signer of the signature. + pub signer: Public, + /// The signature itself. + pub signature: Signature, +} + /// Verify a message without type checking the parameters' types for the right size. pub fn verify(sig: &[u8], message: &[u8], public: &[u8]) -> bool { let public_key = untrusted::Input::from(public); @@ -40,7 +49,7 @@ pub fn verify(sig: &[u8], message: &[u8], public: &[u8]) -> bool { } /// A public key. -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Eq, Clone, Debug)] pub struct Public(pub [u8; 32]); /// A key pair. @@ -152,6 +161,12 @@ impl Verifiable for Signature { } } +impl Verifiable for LocalizedSignature { + fn verify(&self, message: &[u8], pubkey: &Public) -> bool { + pubkey == &self.signer && self.signature.verify(message, pubkey) + } +} + #[cfg(test)] mod test { use super::*; From 170b0d19251c5f803fafe2c4731cbc6fd0b56e52 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 15 Feb 2018 09:51:39 +0100 Subject: [PATCH 19/21] JustifiedHeader and import --- Cargo.lock | 3 +- polkadot/api/src/lib.rs | 2 +- polkadot/primitives/src/block.rs | 2 + substrate/bft/Cargo.toml | 2 - substrate/bft/src/error.rs | 2 +- substrate/bft/src/lib.rs | 158 +++++++++++++++++--------- substrate/client/Cargo.toml | 1 + substrate/client/src/backend.rs | 5 +- substrate/client/src/block_builder.rs | 4 +- substrate/client/src/blockchain.rs | 20 +--- substrate/client/src/error.rs | 9 +- substrate/client/src/in_mem.rs | 4 +- substrate/client/src/lib.rs | 119 +++++++++++++++---- substrate/ed25519/src/lib.rs | 5 + substrate/network/src/blocks.rs | 1 + substrate/network/src/chain.rs | 15 ++- substrate/network/src/message.rs | 5 + substrate/network/src/protocol.rs | 7 +- substrate/network/src/sync.rs | 14 ++- substrate/network/src/test/mod.rs | 4 +- substrate/primitives/src/bft.rs | 32 ++++++ substrate/primitives/src/block.rs | 20 ++++ substrate/primitives/src/lib.rs | 3 + substrate/rpc/src/chain/mod.rs | 2 +- substrate/rpc/src/state/mod.rs | 6 +- substrate/runtime-std/with_std.rs | 1 + substrate/runtime-std/without_std.rs | 1 + 27 files changed, 321 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b05937f464e54..c8f137d6fbf4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1400,12 +1400,10 @@ dependencies = [ "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "substrate-client 0.1.0", "substrate-codec 0.1.0", "substrate-executor 0.1.0", "substrate-keyring 0.1.0", "substrate-primitives 0.1.0", - "substrate-state-machine 0.1.0", "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1419,6 +1417,7 @@ dependencies = [ "hex-literal 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "substrate-bft 0.1.0", "substrate-codec 0.1.0", "substrate-executor 0.1.0", "substrate-keyring 0.1.0", diff --git a/polkadot/api/src/lib.rs b/polkadot/api/src/lib.rs index a16781eb69b28..efe0f45220099 100644 --- a/polkadot/api/src/lib.rs +++ b/polkadot/api/src/lib.rs @@ -28,12 +28,12 @@ extern crate substrate_state_machine as state_machine; extern crate error_chain; use client::backend::Backend; -use client::blockchain::BlockId; use client::Client; use polkadot_runtime::runtime; use polkadot_executor::Executor as LocalDispatch; use substrate_executor::{NativeExecutionDispatch, NativeExecutor}; use primitives::{AccountId, SessionKey}; +use primitives::block::Id as BlockId; use primitives::parachain::DutyRoster; error_chain! { diff --git a/polkadot/primitives/src/block.rs b/polkadot/primitives/src/block.rs index d71fa82e1c287..987744baa61bc 100644 --- a/polkadot/primitives/src/block.rs +++ b/polkadot/primitives/src/block.rs @@ -23,6 +23,8 @@ use rstd::vec::Vec; use codec::{Input, Slicable}; use transaction::UncheckedTransaction; +pub use primitives::block::Id; + /// Used to refer to a block number. pub type Number = u64; diff --git a/substrate/bft/Cargo.toml b/substrate/bft/Cargo.toml index f1af1cda411ce..5d9fa23c63406 100644 --- a/substrate/bft/Cargo.toml +++ b/substrate/bft/Cargo.toml @@ -5,10 +5,8 @@ authors = ["Parity Technologies "] [dependencies] futures = "0.1.17" -substrate-client = { path = "../client" } substrate-codec = { path = "../codec" } substrate-primitives = { path = "../primitives" } -substrate-state-machine = { path = "../state-machine" } ed25519 = { path = "../ed25519" } tokio-timer = "0.1.2" parking_lot = "0.4" diff --git a/substrate/bft/src/error.rs b/substrate/bft/src/error.rs index 1f5286f5e665e..38f560c2cc78d 100644 --- a/substrate/bft/src/error.rs +++ b/substrate/bft/src/error.rs @@ -19,7 +19,7 @@ error_chain! { errors { /// Missing state at block with given Id. - StateUnavailable(b: ::client::BlockId) { + StateUnavailable(b: ::primitives::block::Id) { description("State missing at given block."), display("State unavailable at block {:?}", b), } diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index 5a13224665497..9d84953278823 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -20,9 +20,7 @@ pub mod error; pub mod generic; extern crate substrate_codec as codec; -extern crate substrate_client as client; extern crate substrate_primitives as primitives; -extern crate substrate_state_machine as state_machine; extern crate ed25519; extern crate tokio_timer; extern crate parking_lot; @@ -37,14 +35,11 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use client::{BlockId, Client}; -use client::backend::Backend; use codec::Slicable; use ed25519::LocalizedSignature; -use primitives::bft::{Message as PrimitiveMessage, Action as PrimitiveAction}; -use primitives::block::{Block, Header, HeaderHash}; +use primitives::bft::{Message as PrimitiveMessage, Action as PrimitiveAction, Justification as PrimitiveJustification}; +use primitives::block::{Block, Id as BlockId, Header, HeaderHash}; use primitives::AuthorityId; -use state_machine::CodeExecutor; use futures::{stream, task, Async, Sink, Future, IntoFuture}; use futures::future::Executor; @@ -76,6 +71,19 @@ pub type PrepareJustification = generic::PrepareJustification; +impl From for UncheckedJustification { + fn from(just: PrimitiveJustification) -> Self { + UncheckedJustification { + round_number: just.round_number as usize, + digest: just.hash, + signatures: just.signatures.into_iter().map(|(from, sig)| LocalizedSignature { + signer: ed25519::Public(from), + signature: sig, + }).collect(), + } + } +} + /// Result of a committed round of BFT pub type Committed = generic::Committed; @@ -112,29 +120,6 @@ pub trait Authorities { fn authorities(&self, at: &BlockId) -> Result, Error>; } -impl BlockImport for Client - where - B: Backend, - E: CodeExecutor, - client::error::Error: From<::Error> -{ - fn import_block(&self, block: Block, _justification: Justification) { - // TODO: use justification. - let _ = self.import_block(block.header, Some(block.transactions)); - } -} - -impl Authorities for Client - where - B: Backend, - E: CodeExecutor, - client::error::Error: From<::Error> -{ - fn authorities(&self, at: &BlockId) -> Result, Error> { - self.authorities_at(at).map_err(|_| ErrorKind::StateUnavailable(*at).into()) - } -} - /// Instance of BFT agreement. struct BftInstance

{ key: Arc, @@ -166,29 +151,7 @@ impl generic::Context for BftInstance

{ } fn sign_local(&self, message: Message) -> LocalizedMessage { - let action = match message.clone() { - ::generic::Message::Propose(r, p) => PrimitiveAction::Propose(r as u32, p), - ::generic::Message::Prepare(r, h) => PrimitiveAction::Prepare(r as u32, h), - ::generic::Message::Commit(r, h) => PrimitiveAction::Commit(r as u32, h), - ::generic::Message::AdvanceRound(r) => PrimitiveAction::AdvanceRound(r as u32), - }; - - let primitive = PrimitiveMessage { - parent: self.parent_hash, - action, - }; - - let to_sign = Slicable::encode(&primitive); - let signature = LocalizedSignature { - signer: self.key.public(), - signature: self.key.sign(&to_sign), - }; - - LocalizedMessage { - message, - signature, - sender: self.key.public().0 - } + sign_message(message, &*self.key, self.parent_hash.clone()) } fn round_proposer(&self, round: usize) -> AuthorityId { @@ -427,6 +390,33 @@ pub fn check_prepare_justification(authorities: &[AuthorityId], parent: HeaderHa check_justification_signed_message(authorities, &message[..], just) } +/// Sign a BFT message with the given key. +pub fn sign_message(message: Message, key: &ed25519::Pair, parent_hash: HeaderHash) -> LocalizedMessage { + let action = match message.clone() { + ::generic::Message::Propose(r, p) => PrimitiveAction::Propose(r as u32, p), + ::generic::Message::Prepare(r, h) => PrimitiveAction::Prepare(r as u32, h), + ::generic::Message::Commit(r, h) => PrimitiveAction::Commit(r as u32, h), + ::generic::Message::AdvanceRound(r) => PrimitiveAction::AdvanceRound(r as u32), + }; + + let primitive = PrimitiveMessage { + parent: parent_hash, + action, + }; + + let to_sign = Slicable::encode(&primitive); + let signature = LocalizedSignature { + signer: key.public(), + signature: key.sign(&to_sign), + }; + + LocalizedMessage { + message, + signature, + sender: key.public().0 + } +} + #[cfg(test)] mod tests { use super::*; @@ -535,4 +525,66 @@ mod tests { assert_eq!(max_faulty_of(11), 3); assert_eq!(max_faulty_of(99), 32); } + + #[test] + fn justification_check_works() { + let parent_hash = Default::default(); + let hash = [0xff; 32].into(); + + let authorities = vec![ + Keyring::One.to_raw_public(), + Keyring::Two.to_raw_public(), + Keyring::Alice.to_raw_public(), + Keyring::Eve.to_raw_public(), + ]; + + let authorities_keys = vec![ + Keyring::One.into(), + Keyring::Two.into(), + Keyring::Alice.into(), + Keyring::Eve.into(), + ]; + + let unchecked = UncheckedJustification { + digest: hash, + round_number: 1, + signatures: authorities_keys.iter().take(3).map(|key| { + sign_message(generic::Message::Commit(1, hash), key, parent_hash).signature + }).collect(), + }; + + assert!(check_justification(&authorities, parent_hash, unchecked).is_ok()); + + let unchecked = UncheckedJustification { + digest: hash, + round_number: 0, // wrong round number (vs. the signatures) + signatures: authorities_keys.iter().take(3).map(|key| { + sign_message(generic::Message::Commit(1, hash), key, parent_hash).signature + }).collect(), + }; + + assert!(check_justification(&authorities, parent_hash, unchecked).is_err()); + + // not enough signatures. + let unchecked = UncheckedJustification { + digest: hash, + round_number: 1, + signatures: authorities_keys.iter().take(2).map(|key| { + sign_message(generic::Message::Commit(1, hash), key, parent_hash).signature + }).collect(), + }; + + assert!(check_justification(&authorities, parent_hash, unchecked).is_err()); + + // wrong hash. + let unchecked = UncheckedJustification { + digest: [0xfe; 32].into(), + round_number: 1, + signatures: authorities_keys.iter().take(3).map(|key| { + sign_message(generic::Message::Commit(1, hash), key, parent_hash).signature + }).collect(), + }; + + assert!(check_justification(&authorities, parent_hash, unchecked).is_err()); + } } diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index 8d3b4199910ea..c2af60e7223b4 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -10,6 +10,7 @@ parking_lot = "0.4" triehash = "0.1" hex-literal = "0.1" ed25519 = { path = "../ed25519" } +substrate-bft = { path = "../bft" } substrate-codec = { path = "../codec" } substrate-executor = { path = "../executor" } substrate-primitives = { path = "../primitives" } diff --git a/substrate/client/src/backend.rs b/substrate/client/src/backend.rs index 4421c82b3742f..54b72145adeff 100644 --- a/substrate/client/src/backend.rs +++ b/substrate/client/src/backend.rs @@ -18,8 +18,7 @@ use state_machine; use error; -use primitives::block; -use blockchain::{self, BlockId}; +use primitives::block::{self, Id as BlockId}; /// Block insertion operation. Keeps hold if the inserted block state and data. pub trait BlockImportOperation { @@ -41,7 +40,7 @@ pub trait Backend { /// Associated block insertion operation type. type BlockImportOperation: BlockImportOperation; /// Associated blockchain backend type. - type Blockchain: blockchain::Backend; + type Blockchain: ::blockchain::Backend; /// Associated state backend type. type State: state_machine::backend::Backend; diff --git a/substrate/client/src/block_builder.rs b/substrate/client/src/block_builder.rs index 323fb61c29fc1..54fcd8df7ebf0 100644 --- a/substrate/client/src/block_builder.rs +++ b/substrate/client/src/block_builder.rs @@ -20,8 +20,8 @@ use std::vec::Vec; use codec::{Joiner, Slicable}; use state_machine::{self, CodeExecutor}; use primitives::{Header, Block}; -use primitives::block::Transaction; -use {backend, error, BlockId, Client}; +use primitives::block::{Id as BlockId, Transaction}; +use {backend, error, Client}; use triehash::ordered_trie_root; /// Utility for building new (valid) blocks from a stream of transactions. diff --git a/substrate/client/src/blockchain.rs b/substrate/client/src/blockchain.rs index 6eb9061eb7dfa..a6918a07c2cfb 100644 --- a/substrate/client/src/blockchain.rs +++ b/substrate/client/src/blockchain.rs @@ -16,27 +16,9 @@ //! Polkadot blockchain trait -use std::fmt::{Display, Formatter, Error as FmtError}; -use primitives::block; +use primitives::block::{self, Id as BlockId}; use error::Result; -/// Block indentification. -#[derive(Debug, Clone, Copy)] -pub enum BlockId { - /// Identify by block header hash. - Hash(block::HeaderHash), - /// Identify by block number. - Number(block::Number), -} - -impl Display for BlockId { - fn fmt(&self, f: &mut Formatter) -> ::std::result::Result<(), FmtError> { - match *self { - BlockId::Hash(h) => h.fmt(f), - BlockId::Number(n) => n.fmt(f), - } - } -} /// Blockchain database backend. Does not perform any validation. pub trait Backend: Send + Sync { diff --git a/substrate/client/src/error.rs b/substrate/client/src/error.rs index 1c97387e817c9..2ae07c870c59f 100644 --- a/substrate/client/src/error.rs +++ b/substrate/client/src/error.rs @@ -18,7 +18,6 @@ use std; use state_machine; -use blockchain; error_chain! { errors { @@ -29,7 +28,7 @@ error_chain! { } /// Unknown block. - UnknownBlock(h: blockchain::BlockId) { + UnknownBlock(h: ::primitives::block::Id) { description("unknown block"), display("UnknownBlock: {}", h), } @@ -46,6 +45,12 @@ error_chain! { display("Blockchain: {}", e), } + /// Bad justification for header. + BadJustification(h: ::primitives::block::Id) { + description("bad justification for header"), + display("bad justification for header: {}", h), + } + /// Invalid state data. AuthLen { description("authority count state error"), diff --git a/substrate/client/src/in_mem.rs b/substrate/client/src/in_mem.rs index 69afbb10b6383..844dab1ddd474 100644 --- a/substrate/client/src/in_mem.rs +++ b/substrate/client/src/in_mem.rs @@ -22,8 +22,8 @@ use state_machine; use error; use backend; use runtime_support::Hashable; -use primitives::block::{self, HeaderHash}; -use blockchain::{self, BlockId, BlockStatus}; +use primitives::block::{self, Id as BlockId, HeaderHash}; +use blockchain::{self, BlockStatus}; use state_machine::backend::Backend as StateBackend; fn header_hash(header: &block::Header) -> block::HeaderHash { diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index 947732ee8070e..0b9d80813191a 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -18,6 +18,7 @@ #![warn(missing_docs)] +extern crate substrate_bft as bft; extern crate substrate_runtime_support as runtime_support; extern crate substrate_runtime_io as runtime_io; extern crate substrate_primitives as primitives; @@ -42,9 +43,9 @@ pub mod genesis; pub mod block_builder; pub use blockchain::Info as ChainInfo; -pub use blockchain::BlockId; use primitives::{block, AuthorityId}; +use primitives::block::Id as BlockId; use primitives::storage::{StorageKey, StorageData}; use codec::{KeyedVec, Slicable}; @@ -108,6 +109,20 @@ pub enum BlockStatus { Unknown, } +/// A header paired with a justification which has already been checked. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct JustifiedHeader { + header: block::Header, + justification: bft::Justification, +} + +impl JustifiedHeader { + /// Deconstruct the justified header into parts. + pub fn into_inner(self) -> (block::Header, bft::Justification) { + (self.header, self.justification) + } +} + /// Create an instance of in-memory client. pub fn new_in_mem( executor: E, @@ -228,10 +243,31 @@ impl Client where block_builder::BlockBuilder::at_block(parent, &self) } + /// Check a header's justification. + pub fn check_justification( + &self, + header: block::Header, + justification: bft::UncheckedJustification, + ) -> error::Result { + let authorities = self.authorities_at(&BlockId::Hash(header.parent_hash))?; + let just = bft::check_justification(&authorities[..], header.parent_hash, justification) + .map_err(|_| error::ErrorKind::BadJustification(BlockId::Hash(header.hash())))?; + Ok(JustifiedHeader { + header, + justification: just, + }) + } + /// Queue a block for import. - pub fn import_block(&self, header: block::Header, body: Option) -> error::Result { + pub fn import_block( + &self, + header: JustifiedHeader, + body: Option, + ) -> error::Result { // TODO: import lock // TODO: validate block + // TODO: import justification. + let (header, _) = header.into_inner(); match self.backend.blockchain().status(BlockId::Hash(header.parent_hash))? { blockchain::BlockStatus::InChain => (), blockchain::BlockStatus::Unknown => return Ok(ImportResult::UnknownParent), @@ -306,6 +342,33 @@ impl Client where } } +impl bft::BlockImport for Client + where + B: backend::Backend, + E: state_machine::CodeExecutor, + error::Error: From<::Error> +{ + fn import_block(&self, block: block::Block, justification: bft::Justification) { + let justified_header = JustifiedHeader { + header: block.header, + justification, + }; + + let _ = self.import_block(justified_header, Some(block.transactions)); + } +} + +impl bft::Authorities for Client + where + B: backend::Backend, + E: state_machine::CodeExecutor, + error::Error: From<::Error> +{ + fn authorities(&self, at: &BlockId) -> Result, bft::Error> { + self.authorities_at(at).map_err(|_| bft::ErrorKind::StateUnavailable(*at).into()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -333,6 +396,31 @@ mod tests { (primitives::block::Header::decode(&mut block.header.encode().as_ref()).expect("to_vec() always gives a valid serialisation; qed"), storage.into_iter().collect()) } + // since we are in the client module we can create falsely justified + // headers. + // TODO: remove this in favor of custom verification pipelines for the + // client + fn justify(header: &block::Header) -> bft::UncheckedJustification { + let hash = header.hash(); + let authorities = vec![ + Keyring::Alice.into(), + Keyring::Bob.into(), + Keyring::Charlie.into(), + ]; + + bft::UncheckedJustification { + digest: hash, + signatures: authorities.iter().map(|key| { + bft::sign_message( + bft::generic::Message::Commit(1, hash), + key, + header.parent_hash + ).signature + }).collect(), + round_number: 1, + } + } + #[test] fn client_initialises_from_genesis_ok() { let client = new_in_mem(Executor::new(), prepare_genesis).unwrap(); @@ -345,11 +433,7 @@ mod tests { #[test] fn authorities_call_works() { - let genesis_config = GenesisConfig::new_simple(vec![ - Keyring::Alice.to_raw_public(), - Keyring::Bob.to_raw_public(), - Keyring::Charlie.to_raw_public() - ], 1000); + let genesis_config = genesis_config(); let prepare_genesis = || { let mut storage = genesis_config.genesis_map(); @@ -369,11 +453,7 @@ mod tests { #[test] fn block_builder_works_with_no_transactions() { - let genesis_config = GenesisConfig::new_simple(vec![ - Keyring::Alice.to_raw_public(), - Keyring::Bob.to_raw_public(), - Keyring::Charlie.to_raw_public() - ], 1000); + let genesis_config = genesis_config(); let prepare_genesis = || { let mut storage = genesis_config.genesis_map(); @@ -386,7 +466,9 @@ mod tests { let builder = client.new_block().unwrap(); let block = builder.bake().unwrap(); - client.import_block(block.header, Some(block.transactions)).unwrap(); + let justification = justify(&block.header); + let justified = client.check_justification(block.header, justification).unwrap(); + client.import_block(justified, Some(block.transactions)).unwrap(); assert_eq!(client.info().unwrap().chain.best_number, 1); assert_eq!(client.using_environment(|| test_runtime::system::latest_block_hash()).unwrap(), client.block_hash(1).unwrap().unwrap()); @@ -404,11 +486,7 @@ mod tests { #[test] fn block_builder_works_with_transactions() { - let genesis_config = GenesisConfig::new_simple(vec![ - Keyring::Alice.to_raw_public(), - Keyring::Bob.to_raw_public(), - Keyring::Charlie.to_raw_public() - ], 1000); + let genesis_config = genesis_config(); let prepare_genesis = || { let mut storage = genesis_config.genesis_map(); @@ -427,7 +505,10 @@ mod tests { nonce: 0 }.signed()).unwrap(); let block = builder.bake().unwrap(); - client.import_block(block.header, Some(block.transactions)).unwrap(); + + let justification = justify(&block.header); + let justified = client.check_justification(block.header, justification).unwrap(); + client.import_block(justified, Some(block.transactions)).unwrap(); assert_eq!(client.info().unwrap().chain.best_number, 1); assert!(client.state_at(&BlockId::Number(1)).unwrap() != client.state_at(&BlockId::Number(0)).unwrap()); diff --git a/substrate/ed25519/src/lib.rs b/substrate/ed25519/src/lib.rs index 3d5e53937fedd..e353d7f979e88 100644 --- a/substrate/ed25519/src/lib.rs +++ b/substrate/ed25519/src/lib.rs @@ -171,6 +171,11 @@ impl Verifiable for LocalizedSignature { mod test { use super::*; + fn _test_primitives_signature_and_local_the_same() { + fn takes_two(_: T, _: T) { } + takes_two(Signature::default(), primitives::Signature::default()) + } + #[test] fn test_vector_should_work() { let pair: Pair = Pair::from_seed(&hex!("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")); diff --git a/substrate/network/src/blocks.rs b/substrate/network/src/blocks.rs index 7cb472ec549f3..f111d431049fb 100644 --- a/substrate/network/src/blocks.rs +++ b/substrate/network/src/blocks.rs @@ -205,6 +205,7 @@ mod test { body: None, message_queue: None, receipt: None, + justification: None, }).collect() } diff --git a/substrate/network/src/chain.rs b/substrate/network/src/chain.rs index 709a988060cd3..0c7a2ade30ea5 100644 --- a/substrate/network/src/chain.rs +++ b/substrate/network/src/chain.rs @@ -16,14 +16,15 @@ //! Blockchain access trait -use client::{self, Client as PolkadotClient, ImportResult, ClientInfo, BlockStatus, BlockId}; +use client::{self, Client as PolkadotClient, ImportResult, ClientInfo, BlockStatus}; use client::error::Error; use state_machine; -use primitives::block; +use primitives::block::{self, Id as BlockId}; +use primitives::bft::Justification; -pub trait Client : Send + Sync { +pub trait Client: Send + Sync { /// Given a hash return a header - fn import(&self, header: block::Header, body: Option) -> Result; + fn import(&self, header: block::Header, justification: Justification, body: Option) -> Result; /// Get blockchain info. fn info(&self) -> Result; @@ -46,8 +47,10 @@ impl Client for PolkadotClient where E: state_machine::CodeExecutor + Send + Sync + 'static, Error: From<<::State as state_machine::backend::Backend>::Error>, { - fn import(&self, header: block::Header, body: Option) -> Result { - (self as &PolkadotClient).import_block(header, body) + fn import(&self, header: block::Header, justification: Justification, body: Option) -> Result { + // TODO: defer justification check. + let justified_header = self.check_justification(header, justification.into())?; + (self as &PolkadotClient).import_block(justified_header, body) } fn info(&self) -> Result { diff --git a/substrate/network/src/message.rs b/substrate/network/src/message.rs index 63c4691cfe7d6..793daa444f79e 100644 --- a/substrate/network/src/message.rs +++ b/substrate/network/src/message.rs @@ -19,6 +19,7 @@ use std::borrow::Borrow; use primitives::AuthorityId; use primitives::block::{Number as BlockNumber, HeaderHash, Header, Body}; +use primitives::bft::Justification; use service::Role as RoleFlags; pub type RequestId = u64; @@ -85,6 +86,8 @@ pub enum BlockAttribute { Receipt, /// Include block message queue. MessageQueue, + /// Include a justification for the block. + Justification, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] @@ -100,6 +103,8 @@ pub struct BlockData { pub receipt: Option, /// Block message queue if requested. pub message_queue: Option, + /// Justification if requested. + pub justification: Option, } #[serde(untagged)] diff --git a/substrate/network/src/protocol.rs b/substrate/network/src/protocol.rs index 006ed39c3ccde..5a2f9732b1494 100644 --- a/substrate/network/src/protocol.rs +++ b/substrate/network/src/protocol.rs @@ -17,10 +17,10 @@ use std::collections::{HashMap, HashSet, BTreeMap}; use std::{mem, cmp}; use std::sync::Arc; +use std::time; use parking_lot::RwLock; use serde_json; -use std::time; -use primitives::block::{HeaderHash, TransactionHash, Number as BlockNumber, Header}; +use primitives::block::{HeaderHash, TransactionHash, Number as BlockNumber, Header, Id as BlockId}; use network::{PeerId, NodeId}; use message::{self, Message}; @@ -30,7 +30,6 @@ use config::ProtocolConfig; use chain::Client; use io::SyncIo; use error; -use client::BlockId; use super::header_hash; const REQUEST_TIMEOUT_SEC: u64 = 15; @@ -230,6 +229,7 @@ impl Protocol { message::BlockAttribute::Body => get_body = true, message::BlockAttribute::Receipt => unimplemented!(), message::BlockAttribute::MessageQueue => unimplemented!(), + message::BlockAttribute::Justification => unimplemented!(), } } while let Some(header) = self.chain.header(&id).unwrap_or(None) { @@ -244,6 +244,7 @@ impl Protocol { body: if get_body { self.chain.body(&BlockId::Hash(hash)).unwrap_or(None) } else { None }, receipt: None, message_queue: None, + justification: None, }; blocks.push(block_data); match request.direction { diff --git a/substrate/network/src/sync.rs b/substrate/network/src/sync.rs index 465bffcc3164e..3070a4f7bdc00 100644 --- a/substrate/network/src/sync.rs +++ b/substrate/network/src/sync.rs @@ -18,8 +18,8 @@ use std::collections::HashMap; use io::SyncIo; use protocol::Protocol; use network::PeerId; -use client::{ImportResult, BlockStatus, ClientInfo, BlockId}; -use primitives::block::{HeaderHash, Number as BlockNumber, Header}; +use client::{ImportResult, BlockStatus, ClientInfo}; +use primitives::block::{HeaderHash, Number as BlockNumber, Header, Id as BlockId}; use blocks::{self, BlockCollection}; use message::{self, Message}; use super::header_hash; @@ -80,7 +80,7 @@ impl ChainSync { blocks: BlockCollection::new(), best_queued_hash: info.best_queued_hash.unwrap_or(info.chain.best_hash), best_queued_number: info.best_queued_number.unwrap_or(info.chain.best_number), - required_block_attributes: vec![message::BlockAttribute::Header, message::BlockAttribute::Body], + required_block_attributes: vec![message::BlockAttribute::Header, message::BlockAttribute::Body, message::BlockAttribute::Justification], } } @@ -219,7 +219,11 @@ impl ChainSync { let number = header.number; let hash = header_hash(&header); let parent = header.parent_hash; - let result = protocol.chain().import(header, block.body); + let result = protocol.chain().import( + header, + block.justification.expect("always fetches justification while syncing; qed"), + block.body + ); match result { Ok(ImportResult::AlreadyInChain) => { trace!(target: "sync", "Block already in chain {}: {:?}", number, hash); @@ -398,7 +402,7 @@ impl ChainSync { fn request_ancestry(io: &mut SyncIo, protocol: &Protocol, peer_id: PeerId, block: BlockNumber) { let request = message::BlockRequest { id: 0, - fields: vec![message::BlockAttribute::Header], + fields: vec![message::BlockAttribute::Header, message::BlockAttribute::Justification], from: message::FromBlock::Number(block), to: None, direction: message::Direction::Ascending, diff --git a/substrate/network/src/test/mod.rs b/substrate/network/src/test/mod.rs index d5010ed2bf45a..c41c1d0931790 100644 --- a/substrate/network/src/test/mod.rs +++ b/substrate/network/src/test/mod.rs @@ -19,8 +19,8 @@ mod sync; use std::collections::{VecDeque, HashSet, HashMap}; use std::sync::Arc; use parking_lot::RwLock; -use client::{self, BlockId}; -use primitives::block; +use client; +use primitives::block::{self, Id as BlockId}; use substrate_executor as executor; use io::SyncIo; use protocol::Protocol; diff --git a/substrate/primitives/src/bft.rs b/substrate/primitives/src/bft.rs index f6be42e2490c6..49e14075285bf 100644 --- a/substrate/primitives/src/bft.rs +++ b/substrate/primitives/src/bft.rs @@ -120,3 +120,35 @@ impl Slicable for Message { }) } } + +/// Justification of a block. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +pub struct Justification { + /// The round consensus was reached in. + pub round_number: u32, + /// The hash of the header justified. + pub hash: HeaderHash, + /// The signatures and signers of the hash. + pub signatures: Vec<(::AuthorityId, ::Signature)> +} + +impl Slicable for Justification { + fn encode(&self) -> Vec { + let mut v = Vec::new(); + + self.round_number.using_encoded(|s| v.extend(s)); + self.hash.using_encoded(|s| v.extend(s)); + self.signatures.using_encoded(|s| v.extend(s)); + + v + } + + fn decode(value: &mut I) -> Option { + Some(Justification { + round_number: try_opt!(Slicable::decode(value)), + hash: try_opt!(Slicable::decode(value)), + signatures: try_opt!(Slicable::decode(value)), + }) + } +} diff --git a/substrate/primitives/src/block.rs b/substrate/primitives/src/block.rs index d949e9a453d04..c0d2b5d053f95 100644 --- a/substrate/primitives/src/block.rs +++ b/substrate/primitives/src/block.rs @@ -16,6 +16,7 @@ //! Block and header type definitions. +use rstd::fmt; use rstd::vec::Vec; #[cfg(feature = "std")] use bytes; @@ -177,6 +178,25 @@ impl Slicable for Header { } } +/// Block indentification. +#[derive(Clone, Copy)] +#[cfg_attr(feature = "std", derive(Debug))] +pub enum Id { + /// Identify by block header hash. + Hash(HeaderHash), + /// Identify by block number. + Number(Number), +} + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Id::Hash(h) => h.fmt(f), + Id::Number(n) => n.fmt(f), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index fe42a997ca385..ecf187cf910f3 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -98,3 +98,6 @@ pub type Hash = H256; /// An identifier for an authority in the consensus algorithm. The same as ed25519::Public. pub type AuthorityId = [u8; 32]; + +/// A 512-bit value interpreted as a signature. +pub type Signature = hash::H512; diff --git a/substrate/rpc/src/chain/mod.rs b/substrate/rpc/src/chain/mod.rs index ef827696145d8..2d668826c40dd 100644 --- a/substrate/rpc/src/chain/mod.rs +++ b/substrate/rpc/src/chain/mod.rs @@ -42,6 +42,6 @@ impl ChainApi for client::Client where client::error::Error: From<<::State as state_machine::backend::Backend>::Error>, { fn header(&self, hash: block::HeaderHash) -> Result> { - client::Client::header(self, &client::BlockId::Hash(hash)).chain_err(|| "Blockchain error") + client::Client::header(self, &block::Id::Hash(hash)).chain_err(|| "Blockchain error") } } diff --git a/substrate/rpc/src/state/mod.rs b/substrate/rpc/src/state/mod.rs index de643de7c7290..b8977f30859ae 100644 --- a/substrate/rpc/src/state/mod.rs +++ b/substrate/rpc/src/state/mod.rs @@ -21,7 +21,7 @@ mod error; #[cfg(test)] mod tests; -use client::{self, Client, BlockId}; +use client::{self, Client}; use primitives::block; use primitives::storage::{StorageKey, StorageData}; use state_machine; @@ -47,10 +47,10 @@ impl StateApi for Client where client::error::Error: From<<::State as state_machine::backend::Backend>::Error>, { fn storage(&self, key: StorageKey, block: block::HeaderHash) -> Result { - Ok(self.storage(&BlockId::Hash(block), &key)?) + Ok(self.storage(&block::Id::Hash(block), &key)?) } fn call(&self, method: String, data: Vec, block: block::HeaderHash) -> Result> { - Ok(self.call(&BlockId::Hash(block), &method, &data)?.return_data) + Ok(self.call(&block::Id::Hash(block), &method, &data)?.return_data) } } diff --git a/substrate/runtime-std/with_std.rs b/substrate/runtime-std/with_std.rs index 960871665e2c4..32124b04c80f7 100644 --- a/substrate/runtime-std/with_std.rs +++ b/substrate/runtime-std/with_std.rs @@ -17,6 +17,7 @@ pub use std::boxed; pub use std::cell; pub use std::cmp; +pub use std::fmt; pub use std::iter; pub use std::mem; pub use std::ops; diff --git a/substrate/runtime-std/without_std.rs b/substrate/runtime-std/without_std.rs index b27db9bc7d217..b3e50eb2adf08 100644 --- a/substrate/runtime-std/without_std.rs +++ b/substrate/runtime-std/without_std.rs @@ -33,3 +33,4 @@ pub use core::mem; pub use core::ops; pub use core::ptr; pub use core::slice; +pub use core::fmt; From 6a1a851a680d6650f2ad1152e3195b067bd86d44 Mon Sep 17 00:00:00 2001 From: arkpar Date: Thu, 15 Feb 2018 12:59:08 +0100 Subject: [PATCH 20/21] Propert block generation for tests --- Cargo.lock | 5 ++ substrate/bft/src/generic/accumulator.rs | 7 ++ substrate/bft/src/lib.rs | 10 +++ substrate/client/Cargo.toml | 4 +- substrate/client/src/backend.rs | 3 +- substrate/client/src/blockchain.rs | 3 + substrate/client/src/in_mem.rs | 47 +++-------- substrate/client/src/lib.rs | 13 ++- substrate/ed25519/src/lib.rs | 6 ++ substrate/network/Cargo.toml | 5 ++ substrate/network/src/chain.rs | 7 ++ substrate/network/src/lib.rs | 19 +++-- substrate/network/src/protocol.rs | 6 +- substrate/network/src/sync.rs | 88 +++++++++++--------- substrate/network/src/test/mod.rs | 100 +++++++++++++++++++---- substrate/network/src/test/sync.rs | 48 +++++------ 16 files changed, 239 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8f137d6fbf4c..0b548f6a0535c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1481,11 +1481,16 @@ dependencies = [ "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "substrate-bft 0.1.0", "substrate-client 0.1.0", + "substrate-codec 0.1.0", "substrate-executor 0.1.0", + "substrate-keyring 0.1.0", "substrate-primitives 0.1.0", + "substrate-runtime-support 0.1.0", "substrate-serializer 0.1.0", "substrate-state-machine 0.1.0", + "substrate-test-runtime 0.1.0", ] [[package]] diff --git a/substrate/bft/src/generic/accumulator.rs b/substrate/bft/src/generic/accumulator.rs index 273fc658478e9..a7b2076bba5e6 100644 --- a/substrate/bft/src/generic/accumulator.rs +++ b/substrate/bft/src/generic/accumulator.rs @@ -83,6 +83,13 @@ impl UncheckedJustification { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Justification(UncheckedJustification); +impl Justification { + /// Convert this justification back to unchecked. + pub fn uncheck(self) -> UncheckedJustification { + self.0 + } +} + impl ::std::ops::Deref for Justification { type Target = UncheckedJustification; diff --git a/substrate/bft/src/lib.rs b/substrate/bft/src/lib.rs index 9d84953278823..1f81b23cac0e5 100644 --- a/substrate/bft/src/lib.rs +++ b/substrate/bft/src/lib.rs @@ -84,6 +84,16 @@ impl From for UncheckedJustification { } } +impl From for PrimitiveJustification { + fn from(just: UncheckedJustification) -> Self { + PrimitiveJustification { + round_number: just.round_number as u32, + hash: just.digest, + signatures: just.signatures.into_iter().map(|s| (s.signer.into(), s.signature)).collect(), + } + } +} + /// Result of a committed round of BFT pub type Committed = generic::Committed; diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index c2af60e7223b4..879e05e3c9442 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -17,5 +17,7 @@ substrate-primitives = { path = "../primitives" } substrate-runtime-io = { path = "../runtime-io" } substrate-runtime-support = { path = "../runtime-support" } substrate-state-machine = { path = "../state-machine" } -substrate-test-runtime = { path = "../test-runtime" } substrate-keyring = { path = "../../substrate/keyring" } + +[dev-dependencies] +substrate-test-runtime = { path = "../test-runtime" } diff --git a/substrate/client/src/backend.rs b/substrate/client/src/backend.rs index 54b72145adeff..eef904679173c 100644 --- a/substrate/client/src/backend.rs +++ b/substrate/client/src/backend.rs @@ -19,6 +19,7 @@ use state_machine; use error; use primitives::block::{self, Id as BlockId}; +use primitives; /// Block insertion operation. Keeps hold if the inserted block state and data. pub trait BlockImportOperation { @@ -28,7 +29,7 @@ pub trait BlockImportOperation { /// Returns pending state. fn state(&self) -> error::Result<&Self::State>; /// Append block data to the transaction. - fn set_block_data(&mut self, header: block::Header, body: Option, is_new_best: bool) -> error::Result<()>; + fn set_block_data(&mut self, header: block::Header, body: Option, justification: Option, is_new_best: bool) -> error::Result<()>; /// Inject storage data into the database. fn set_storage, Vec)>>(&mut self, iter: I) -> error::Result<()>; /// Inject storage data into the database. diff --git a/substrate/client/src/blockchain.rs b/substrate/client/src/blockchain.rs index a6918a07c2cfb..095cbe3cbee8a 100644 --- a/substrate/client/src/blockchain.rs +++ b/substrate/client/src/blockchain.rs @@ -17,6 +17,7 @@ //! Polkadot blockchain trait use primitives::block::{self, Id as BlockId}; +use primitives; use error::Result; @@ -26,6 +27,8 @@ pub trait Backend: Send + Sync { fn header(&self, id: BlockId) -> Result>; /// Get block body. Returns `None` if block is not found. fn body(&self, id: BlockId) -> Result>; + /// Get block justification. Returns `None` if justification does not exist. + fn justification(&self, id: BlockId) -> Result>; /// Get blockchain info. fn info(&self) -> Result; /// Get block status. diff --git a/substrate/client/src/in_mem.rs b/substrate/client/src/in_mem.rs index 844dab1ddd474..62ef3b971552b 100644 --- a/substrate/client/src/in_mem.rs +++ b/substrate/client/src/in_mem.rs @@ -22,6 +22,7 @@ use state_machine; use error; use backend; use runtime_support::Hashable; +use primitives; use primitives::block::{self, Id as BlockId, HeaderHash}; use blockchain::{self, BlockStatus}; use state_machine::backend::Backend as StateBackend; @@ -38,6 +39,7 @@ struct PendingBlock { #[derive(PartialEq, Eq, Clone)] struct Block { header: block::Header, + justification: Option, body: Option, } @@ -90,12 +92,13 @@ impl Blockchain { } } - fn insert(&self, hash: HeaderHash, header: block::Header, body: Option, is_new_best: bool) { + fn insert(&self, hash: HeaderHash, header: block::Header, justification: Option, body: Option, is_new_best: bool) { let number = header.number; let mut storage = self.storage.write(); storage.blocks.insert(hash, Block { header: header, body: body, + justification: justification, }); storage.hashes.insert(number, hash); if is_new_best { @@ -132,6 +135,10 @@ impl blockchain::Backend for Blockchain { Ok(self.id(id).and_then(|hash| self.storage.read().blocks.get(&hash).and_then(|b| b.body.clone()))) } + fn justification(&self, id: BlockId) -> error::Result> { + Ok(self.id(id).and_then(|hash| self.storage.read().blocks.get(&hash).and_then(|b| b.justification.clone()))) + } + fn info(&self) -> error::Result { let storage = self.storage.read(); Ok(blockchain::Info { @@ -160,12 +167,13 @@ impl backend::BlockImportOperation for BlockImportOperation { Ok(&self.pending_state) } - fn set_block_data(&mut self, header: block::Header, body: Option, is_new_best: bool) -> error::Result<()> { + fn set_block_data(&mut self, header: block::Header, body: Option, justification: Option, is_new_best: bool) -> error::Result<()> { assert!(self.pending_block.is_none(), "Only one block per operation is allowed"); self.pending_block = Some(PendingBlock { block: Block { header: header, body: body, + justification: justification, }, is_best: is_new_best, }); @@ -197,39 +205,6 @@ impl Backend { blockchain: Blockchain::new(), } } - - /// Generate and import a sequence of blocks. A user supplied function is allowed to modify each block header. Useful for testing. - pub fn generate_blocks(&self, count: usize, edit_header: F) where F: Fn(&mut block::Header) { - use backend::{Backend, BlockImportOperation}; - let info = blockchain::Backend::info(&self.blockchain).expect("In-memory backend never fails"); - let mut best_num = info.best_number; - let mut best_hash = info.best_hash; - let state_root = blockchain::Backend::header(&self.blockchain, BlockId::Hash(best_hash)) - .expect("In-memory backend never fails") - .expect("Best header always exists in the blockchain") - .state_root; - for _ in 0 .. count { - best_num = best_num + 1; - let mut header = block::Header { - parent_hash: best_hash, - number: best_num, - state_root: state_root, - transaction_root: Default::default(), - digest: Default::default(), - }; - edit_header(&mut header); - - let mut tx = self.begin_operation(BlockId::Hash(best_hash)).expect("In-memory backend does not fail"); - best_hash = header_hash(&header); - tx.set_block_data(header, Some(vec![]), true).expect("In-memory backend does not fail"); - self.commit_operation(tx).expect("In-memory backend does not fail"); - } - } - - /// Generate and import a sequence of blocks. Useful for testing. - pub fn push_blocks(&self, count: usize) { - self.generate_blocks(count, |_| {}) - } } impl backend::Backend for Backend { @@ -253,7 +228,7 @@ impl backend::Backend for Backend { if let Some(pending_block) = operation.pending_block { let hash = header_hash(&pending_block.block.header); self.states.write().insert(hash, operation.pending_state); - self.blockchain.insert(hash, pending_block.block.header, pending_block.block.body, pending_block.is_best); + self.blockchain.insert(hash, pending_block.block.header, pending_block.block.justification, pending_block.block.body, pending_block.is_best); } Ok(()) } diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index 0b9d80813191a..c86c06afef917 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -53,6 +53,7 @@ use blockchain::Backend as BlockchainBackend; use backend::BlockImportOperation; use state_machine::backend::Backend as StateBackend; use state_machine::{Ext, OverlayedChanges}; +use runtime_support::Hashable; /// Polkadot Client #[derive(Debug)] @@ -154,7 +155,7 @@ impl Client where let (genesis_header, genesis_store) = build_genesis(); let mut op = backend.begin_operation(BlockId::Hash(block::HeaderHash::default()))?; op.reset_storage(genesis_store.into_iter())?; - op.set_block_data(genesis_header, Some(vec![]), true)?; + op.set_block_data(genesis_header, Some(vec![]), None, true)?; backend.commit_operation(op)?; } Ok(Client { @@ -267,7 +268,7 @@ impl Client where // TODO: import lock // TODO: validate block // TODO: import justification. - let (header, _) = header.into_inner(); + let (header, justification) = header.into_inner(); match self.backend.blockchain().status(BlockId::Hash(header.parent_hash))? { blockchain::BlockStatus::InChain => (), blockchain::BlockStatus::Unknown => return Ok(ImportResult::UnknownParent), @@ -285,7 +286,8 @@ impl Client where )?; let is_new_best = header.number == self.backend.blockchain().info()?.best_number + 1; - transaction.set_block_data(header, body, is_new_best)?; + trace!("Imported {}, (#{}), best={}", block::HeaderHash::from(header.blake2_256()), header.number, is_new_best); + transaction.set_block_data(header, body, Some(justification.uncheck().into()), is_new_best)?; transaction.set_storage(overlay.drain())?; self.backend.commit_operation(transaction)?; Ok(ImportResult::Queued) @@ -340,6 +342,11 @@ impl Client where pub fn body(&self, id: &BlockId) -> error::Result> { self.backend.blockchain().body(*id) } + + /// Get block justification set by id. + pub fn justification(&self, id: &BlockId) -> error::Result> { + self.backend.blockchain().justification(*id) + } } impl bft::BlockImport for Client diff --git a/substrate/ed25519/src/lib.rs b/substrate/ed25519/src/lib.rs index e353d7f979e88..4d8117186bc61 100644 --- a/substrate/ed25519/src/lib.rs +++ b/substrate/ed25519/src/lib.rs @@ -98,6 +98,12 @@ impl AsRef<[u8]> for Public { } } +impl Into<[u8; 32]> for Public { + fn into(self) -> [u8; 32] { + self.0 + } +} + impl Pair { /// Generate new secure (random) key pair. pub fn new() -> Pair { diff --git a/substrate/network/Cargo.toml b/substrate/network/Cargo.toml index 55edfb7d5487e..2c07934052605 100644 --- a/substrate/network/Cargo.toml +++ b/substrate/network/Cargo.toml @@ -22,7 +22,12 @@ substrate-primitives = { path = "../../substrate/primitives" } substrate-client = { path = "../../substrate/client" } substrate-state-machine = { path = "../../substrate/state-machine" } substrate-serializer = { path = "../../substrate/serializer" } +substrate-runtime-support = { path = "../../substrate/runtime-support" } [dev-dependencies] +substrate-test-runtime = { path = "../test-runtime" } substrate-executor = { path = "../../substrate/executor" } +substrate-keyring = { path = "../../substrate/keyring" } +substrate-codec = { path = "../../substrate/codec" } +substrate-bft = { path = "../bft" } env_logger = "0.4" diff --git a/substrate/network/src/chain.rs b/substrate/network/src/chain.rs index 0c7a2ade30ea5..1a51797f10b37 100644 --- a/substrate/network/src/chain.rs +++ b/substrate/network/src/chain.rs @@ -40,6 +40,9 @@ pub trait Client: Send + Sync { /// Get block body. fn body(&self, id: &BlockId) -> Result, Error>; + + /// Get block justification. + fn justification(&self, id: &BlockId) -> Result, Error>; } impl Client for PolkadotClient where @@ -72,4 +75,8 @@ impl Client for PolkadotClient where fn body(&self, id: &BlockId) -> Result, Error> { (self as &PolkadotClient).body(id) } + + fn justification(&self, id: &BlockId) -> Result, Error> { + (self as &PolkadotClient).justification(id) + } } diff --git a/substrate/network/src/lib.rs b/substrate/network/src/lib.rs index d7053272cebad..71e6ab755f2e3 100644 --- a/substrate/network/src/lib.rs +++ b/substrate/network/src/lib.rs @@ -27,6 +27,7 @@ extern crate substrate_primitives as primitives; extern crate substrate_state_machine as state_machine; extern crate substrate_serializer as ser; extern crate substrate_client as client; +extern crate substrate_runtime_support as runtime_support; extern crate serde; extern crate serde_json; #[macro_use] extern crate serde_derive; @@ -34,6 +35,13 @@ extern crate serde_json; #[macro_use] extern crate bitflags; #[macro_use] extern crate error_chain; +#[cfg(test)] extern crate env_logger; +#[cfg(test)] extern crate substrate_test_runtime as test_runtime; +#[cfg(test)] extern crate substrate_keyring as keyring; +#[cfg(test)] #[macro_use] extern crate substrate_executor as executor; +#[cfg(test)] extern crate substrate_codec as codec; +#[cfg(test)] extern crate substrate_bft as bft; + mod service; mod sync; mod protocol; @@ -44,13 +52,7 @@ mod config; mod chain; mod blocks; -#[cfg(test)] -mod test; - -#[cfg(test)] -extern crate substrate_executor; -#[cfg(test)] -extern crate env_logger; +#[cfg(test)] mod test; pub use service::Service; pub use protocol::{ProtocolStatus}; @@ -59,5 +61,6 @@ pub use network::{NonReservedPeerMode, ConnectionFilter, ConnectionDirection, Ne // TODO: move it elsewhere fn header_hash(header: &primitives::Header) -> primitives::block::HeaderHash { - primitives::hashing::blake2_256(&ser::encode(header)).into() + use runtime_support::Hashable; + header.blake2_256().into() } diff --git a/substrate/network/src/protocol.rs b/substrate/network/src/protocol.rs index 5a2f9732b1494..980bf441f5c2a 100644 --- a/substrate/network/src/protocol.rs +++ b/substrate/network/src/protocol.rs @@ -222,14 +222,14 @@ impl Protocol { }; let max = cmp::min(request.max.unwrap_or(u32::max_value()), MAX_BLOCK_DATA_RESPONSE) as usize; // TODO: receipts, etc. - let (mut get_header, mut get_body) = (false, false); + let (mut get_header, mut get_body, mut get_justification) = (false, false, false); for a in request.fields { match a { message::BlockAttribute::Header => get_header = true, message::BlockAttribute::Body => get_body = true, message::BlockAttribute::Receipt => unimplemented!(), message::BlockAttribute::MessageQueue => unimplemented!(), - message::BlockAttribute::Justification => unimplemented!(), + message::BlockAttribute::Justification => get_justification = true, } } while let Some(header) = self.chain.header(&id).unwrap_or(None) { @@ -244,7 +244,7 @@ impl Protocol { body: if get_body { self.chain.body(&BlockId::Hash(hash)).unwrap_or(None) } else { None }, receipt: None, message_queue: None, - justification: None, + justification: if get_justification { self.chain.justification(&BlockId::Hash(hash)).unwrap_or(None) } else { None }, }; blocks.push(block_data); match request.direction { diff --git a/substrate/network/src/sync.rs b/substrate/network/src/sync.rs index 3070a4f7bdc00..c952bd5f2e44a 100644 --- a/substrate/network/src/sync.rs +++ b/substrate/network/src/sync.rs @@ -215,45 +215,57 @@ impl ChainSync { for block in new_blocks { let origin = block.origin; let block = block.block; - if let Some(header) = block.header { - let number = header.number; - let hash = header_hash(&header); - let parent = header.parent_hash; - let result = protocol.chain().import( - header, - block.justification.expect("always fetches justification while syncing; qed"), - block.body - ); - match result { - Ok(ImportResult::AlreadyInChain) => { - trace!(target: "sync", "Block already in chain {}: {:?}", number, hash); - self.block_imported(&hash, number); - }, - Ok(ImportResult::AlreadyQueued) => { - trace!(target: "sync", "Block already queued {}: {:?}", number, hash); - self.block_imported(&hash, number); - }, - Ok(ImportResult::Queued) => { - trace!(target: "sync", "Block queued {}: {:?}", number, hash); - self.block_imported(&hash, number); - imported = imported + 1; - }, - Ok(ImportResult::UnknownParent) => { - debug!(target: "sync", "Block with unknown parent {}: {:?}, parent: {:?}", number, hash, parent); - self.restart(io, protocol); - return; - }, - Ok(ImportResult::KnownBad) => { - debug!(target: "sync", "Bad block {}: {:?}", number, hash); - io.disable_peer(origin); //TODO: use persistent ID - self.restart(io, protocol); - return; - } - Err(e) => { - debug!(target: "sync", "Error importing block {}: {:?}: {:?}", number, hash, e); - self.restart(io, protocol); - return; + match (block.header, block.justification) { + (Some(header), Some(justification)) => { + let number = header.number; + let hash = header_hash(&header); + let parent = header.parent_hash; + let result = protocol.chain().import( + header, + justification, + block.body + ); + match result { + Ok(ImportResult::AlreadyInChain) => { + trace!(target: "sync", "Block already in chain {}: {:?}", number, hash); + self.block_imported(&hash, number); + }, + Ok(ImportResult::AlreadyQueued) => { + trace!(target: "sync", "Block already queued {}: {:?}", number, hash); + self.block_imported(&hash, number); + }, + Ok(ImportResult::Queued) => { + trace!(target: "sync", "Block queued {}: {:?}", number, hash); + self.block_imported(&hash, number); + imported = imported + 1; + }, + Ok(ImportResult::UnknownParent) => { + debug!(target: "sync", "Block with unknown parent {}: {:?}, parent: {:?}", number, hash, parent); + self.restart(io, protocol); + return; + }, + Ok(ImportResult::KnownBad) => { + debug!(target: "sync", "Bad block {}: {:?}", number, hash); + io.disable_peer(origin); //TODO: use persistent ID + self.restart(io, protocol); + return; + } + Err(e) => { + debug!(target: "sync", "Error importing block {}: {:?}: {:?}", number, hash, e); + self.restart(io, protocol); + return; + } } + }, + (None, _) => { + debug!(target: "sync", "Header {} was not provided by {} ", block.hash, origin); + io.disable_peer(origin); //TODO: use persistent ID + return; + }, + (_, None) => { + debug!(target: "sync", "Justification set for block {} was not provided by {} ", block.hash, origin); + io.disable_peer(origin); //TODO: use persistent ID + return; } } } diff --git a/substrate/network/src/test/mod.rs b/substrate/network/src/test/mod.rs index c41c1d0931790..4cddc1bf8af8e 100644 --- a/substrate/network/src/test/mod.rs +++ b/substrate/network/src/test/mod.rs @@ -19,13 +19,23 @@ mod sync; use std::collections::{VecDeque, HashSet, HashMap}; use std::sync::Arc; use parking_lot::RwLock; -use client; -use primitives::block::{self, Id as BlockId}; -use substrate_executor as executor; +use client::{self, genesis}; +use client::block_builder::BlockBuilder; +use primitives::block::Id as BlockId; +use primitives; +use executor; use io::SyncIo; use protocol::Protocol; use config::ProtocolConfig; use network::{PeerId, SessionInfo, Error as NetworkError}; +use test_runtime::genesismap::{GenesisConfig, additional_storage_with_genesis}; +use runtime_support::Hashable; +use test_runtime; +use keyring::Keyring; +use codec::Slicable; +use bft; + +native_executor_instance!(Executor, test_runtime::api::dispatch, include_bytes!("../../../test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.compact.wasm")); pub struct TestIo<'p> { pub queue: &'p RwLock>, @@ -92,7 +102,7 @@ pub struct TestPacket { } pub struct Peer { - pub chain: Arc>, + client: Arc>>, pub sync: Protocol, pub queue: RwLock>, } @@ -100,9 +110,9 @@ pub struct Peer { impl Peer { /// Called after blockchain has been populated to updated current state. fn start(&self) { - // Update the sync state to the lates chain state. - let info = self.chain.info().expect("In-mem chain does not fail"); - let header = self.chain.header(&BlockId::Hash(info.chain.best_hash)).unwrap().unwrap(); + // Update the sync state to the latest chain state. + let info = self.client.info().expect("In-mem client does not fail"); + let header = self.client.header(&BlockId::Hash(info.chain.best_hash)).unwrap().unwrap(); self.sync.on_block_imported(&header); } @@ -149,6 +159,55 @@ impl Peer { fn flush(&self) { } + + fn justify(header: &primitives::block::Header) -> bft::UncheckedJustification { + let hash = header.hash(); + let authorities = vec![ Keyring::Alice.into() ]; + + bft::UncheckedJustification { + digest: hash, + signatures: authorities.iter().map(|key| { + bft::sign_message( + bft::generic::Message::Commit(1, hash), + key, + header.parent_hash + ).signature + }).collect(), + round_number: 1, + } + } + + fn generate_blocks(&self, count: usize, mut edit_block: F) where F: FnMut(&mut BlockBuilder>) { + for _ in 0 .. count { + let mut builder = self.client.new_block().unwrap(); + edit_block(&mut builder); + let block = builder.bake().unwrap(); + trace!("Generating {}, (#{})", primitives::block::HeaderHash::from(block.header.blake2_256()), block.header.number); + let justification = Self::justify(&block.header); + let justified = self.client.check_justification(block.header, justification).unwrap(); + self.client.import_block(justified, Some(block.transactions)).unwrap(); + } + } + + fn push_blocks(&self, count: usize, with_tx: bool) { + let mut nonce = 0; + if with_tx { + self.generate_blocks(count, |builder| { + let tx = test_runtime::Transaction { + from: Keyring::Alice.to_raw_public(), + to: Keyring::Alice.to_raw_public(), + amount: 1, + nonce: nonce, + }; + let signature = Keyring::from_raw_public(tx.from.clone()).unwrap().sign(&tx.encode()); + let tx = primitives::block::Transaction::decode(&mut test_runtime::UncheckedTransaction { signature, tx: tx }.encode().as_ref()).unwrap(); + builder.push(tx).unwrap(); + nonce = nonce + 1; + }); + } else { + self.generate_blocks(count, |_| ()); + } + } } pub struct TestNet { @@ -158,6 +217,19 @@ pub struct TestNet { } impl TestNet { + fn genesis_config() -> GenesisConfig { + GenesisConfig::new_simple(vec![ + Keyring::Alice.to_raw_public(), + ], 1000) + } + + fn prepare_genesis() -> (primitives::block::Header, Vec<(Vec, Vec)>) { + let mut storage = Self::genesis_config().genesis_map(); + let block = genesis::construct_genesis_block(&storage); + storage.extend(additional_storage_with_genesis(&block)); + (primitives::block::Header::decode(&mut block.header.encode().as_ref()).expect("to_vec() always gives a valid serialisation; qed"), storage.into_iter().collect()) + } + pub fn new(n: usize) -> Self { Self::new_with_config(n, ProtocolConfig::default()) } @@ -168,21 +240,13 @@ impl TestNet { started: false, disconnect_events: Vec::new(), }; - let test_genesis_block = block::Header { - parent_hash: 0.into(), - number: 0, - state_root: 0.into(), - transaction_root: Default::default(), - digest: Default::default(), - }; for _ in 0..n { - let chain = Arc::new(client::new_in_mem(executor::WasmExecutor, - || (test_genesis_block.clone(), vec![])).unwrap()); - let sync = Protocol::new(config.clone(), chain.clone()).unwrap(); + let client = Arc::new(client::new_in_mem(Executor::new(), Self::prepare_genesis).unwrap()); + let sync = Protocol::new(config.clone(), client.clone()).unwrap(); net.peers.push(Arc::new(Peer { sync: sync, - chain: chain, + client: client, queue: RwLock::new(VecDeque::new()), })); } diff --git a/substrate/network/src/test/sync.rs b/substrate/network/src/test/sync.rs index 54167a2c5e394..e5cc1ebfad082 100644 --- a/substrate/network/src/test/sync.rs +++ b/substrate/network/src/test/sync.rs @@ -22,10 +22,10 @@ use super::*; fn sync_from_two_peers_works() { ::env_logger::init().ok(); let mut net = TestNet::new(3); - net.peer(1).chain.backend().push_blocks(100); - net.peer(2).chain.backend().push_blocks(100); + net.peer(1).push_blocks(100, false); + net.peer(2).push_blocks(100, false); net.sync(); - assert!(net.peer(0).chain.backend().blockchain().equals_to(net.peer(1).chain.backend().blockchain())); + assert!(net.peer(0).client.backend().blockchain().equals_to(net.peer(1).client.backend().blockchain())); let status = net.peer(0).sync.status(); assert_eq!(status.sync.state, SyncState::Idle); } @@ -34,54 +34,54 @@ fn sync_from_two_peers_works() { fn sync_from_two_peers_with_ancestry_search_works() { ::env_logger::init().ok(); let mut net = TestNet::new(3); - net.peer(0).chain.backend().generate_blocks(10, |header| header.state_root = 42.into()); - net.peer(1).chain.backend().push_blocks(100); - net.peer(2).chain.backend().push_blocks(100); + net.peer(0).push_blocks(10, true); + net.peer(1).push_blocks(100, false); + net.peer(2).push_blocks(100, false); net.restart_peer(0); net.sync(); - assert!(net.peer(0).chain.backend().blockchain().canon_equals_to(net.peer(1).chain.backend().blockchain())); + assert!(net.peer(0).client.backend().blockchain().canon_equals_to(net.peer(1).client.backend().blockchain())); } #[test] fn sync_long_chain_works() { let mut net = TestNet::new(2); - net.peer(1).chain.backend().push_blocks(5000); + net.peer(1).push_blocks(500, false); net.sync_steps(3); assert_eq!(net.peer(0).sync.status().sync.state, SyncState::Downloading); net.sync(); - assert!(net.peer(0).chain.backend().blockchain().equals_to(net.peer(1).chain.backend().blockchain())); + assert!(net.peer(0).client.backend().blockchain().equals_to(net.peer(1).client.backend().blockchain())); } #[test] fn sync_no_common_longer_chain_fails() { ::env_logger::init().ok(); let mut net = TestNet::new(3); - net.peer(0).chain.backend().generate_blocks(200, |header| header.state_root = 42.into()); - net.peer(1).chain.backend().push_blocks(200); + net.peer(0).push_blocks(20, true); + net.peer(1).push_blocks(20, false); net.sync(); - assert!(!net.peer(0).chain.backend().blockchain().canon_equals_to(net.peer(1).chain.backend().blockchain())); + assert!(!net.peer(0).client.backend().blockchain().canon_equals_to(net.peer(1).client.backend().blockchain())); } #[test] fn sync_after_fork_works() { ::env_logger::init().ok(); let mut net = TestNet::new(3); - net.peer(0).chain.backend().push_blocks(30); - net.peer(1).chain.backend().push_blocks(30); - net.peer(2).chain.backend().push_blocks(30); + net.peer(0).push_blocks(30, false); + net.peer(1).push_blocks(30, false); + net.peer(2).push_blocks(30, false); - net.peer(0).chain.backend().generate_blocks(10, |header| header.state_root = 42.into()); // fork - net.peer(1).chain.backend().push_blocks(20); - net.peer(2).chain.backend().push_blocks(20); + net.peer(0).push_blocks(10, true); + net.peer(1).push_blocks(20, false); + net.peer(2).push_blocks(20, false); - net.peer(1).chain.backend().generate_blocks(10, |header| header.state_root = 42.into()); // second fork between 1 and 2 - net.peer(2).chain.backend().push_blocks(1); + net.peer(1).push_blocks(10, true); + net.peer(2).push_blocks(1, false); // peer 1 has the best chain - let peer1_chain = net.peer(1).chain.backend().blockchain().clone(); + let peer1_chain = net.peer(1).client.backend().blockchain().clone(); net.sync(); - assert!(net.peer(0).chain.backend().blockchain().canon_equals_to(&peer1_chain)); - assert!(net.peer(1).chain.backend().blockchain().canon_equals_to(&peer1_chain)); - assert!(net.peer(2).chain.backend().blockchain().canon_equals_to(&peer1_chain)); + assert!(net.peer(0).client.backend().blockchain().canon_equals_to(&peer1_chain)); + assert!(net.peer(1).client.backend().blockchain().canon_equals_to(&peer1_chain)); + assert!(net.peer(2).client.backend().blockchain().canon_equals_to(&peer1_chain)); } From a1247bd0608e9ee40932cbbf48257080ab6532fe Mon Sep 17 00:00:00 2001 From: arkpar Date: Thu, 15 Feb 2018 15:17:33 +0100 Subject: [PATCH 21/21] Fixed rpc tests --- Cargo.lock | 1 + substrate/rpc/Cargo.toml | 1 + substrate/rpc/src/chain/tests.rs | 3 ++- substrate/rpc/src/lib.rs | 2 ++ substrate/rpc/src/state/tests.rs | 5 +++-- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b548f6a0535c..62e3721f8c60a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1523,6 +1523,7 @@ dependencies = [ "substrate-client 0.1.0", "substrate-executor 0.1.0", "substrate-primitives 0.1.0", + "substrate-runtime-support 0.1.0", "substrate-state-machine 0.1.0", ] diff --git a/substrate/rpc/Cargo.toml b/substrate/rpc/Cargo.toml index 149199546ef13..485e08708c79d 100644 --- a/substrate/rpc/Cargo.toml +++ b/substrate/rpc/Cargo.toml @@ -15,3 +15,4 @@ substrate-executor = { path = "../executor" } [dev-dependencies] assert_matches = "1.1" substrate-executor = { path = "../executor" } +substrate-runtime-support = { path = "../runtime-support" } diff --git a/substrate/rpc/src/chain/tests.rs b/substrate/rpc/src/chain/tests.rs index c62981094c160..f462d759ecdb4 100644 --- a/substrate/rpc/src/chain/tests.rs +++ b/substrate/rpc/src/chain/tests.rs @@ -16,6 +16,7 @@ use substrate_executor as executor; use client; +use runtime_support::Hashable; use super::*; #[test] @@ -31,7 +32,7 @@ fn should_return_header() { let client = client::new_in_mem(executor::WasmExecutor, || (test_genesis_block.clone(), vec![])).unwrap(); assert_matches!( - ChainApi::header(&client, "af65e54217fb213853703d57b80fc5b2bb834bf923046294d7a49bff62f0a8b2".into()), + ChainApi::header(&client, test_genesis_block.blake2_256().into()), Ok(Some(ref x)) if x == &block::Header { parent_hash: 0.into(), number: 0, diff --git a/substrate/rpc/src/lib.rs b/substrate/rpc/src/lib.rs index 05d3d94685100..513bc42d873f8 100644 --- a/substrate/rpc/src/lib.rs +++ b/substrate/rpc/src/lib.rs @@ -33,6 +33,8 @@ extern crate substrate_executor; #[cfg(test)] #[macro_use] extern crate assert_matches; +#[cfg(test)] +extern crate substrate_runtime_support as runtime_support; pub mod chain; pub mod state; diff --git a/substrate/rpc/src/state/tests.rs b/substrate/rpc/src/state/tests.rs index 687c851c5c029..469b794b05899 100644 --- a/substrate/rpc/src/state/tests.rs +++ b/substrate/rpc/src/state/tests.rs @@ -17,6 +17,7 @@ use super::*; use substrate_executor as executor; use self::error::{Error, ErrorKind}; +use runtime_support::Hashable; use client; #[test] @@ -30,7 +31,7 @@ fn should_return_storage() { }; let client = client::new_in_mem(executor::WasmExecutor, || (test_genesis_block.clone(), vec![])).unwrap(); - let genesis_hash = "af65e54217fb213853703d57b80fc5b2bb834bf923046294d7a49bff62f0a8b2".into(); + let genesis_hash = test_genesis_block.blake2_256().into(); assert_matches!( StateApi::storage(&client, StorageKey(vec![10]), genesis_hash), @@ -51,7 +52,7 @@ fn should_call_contract() { }; let client = client::new_in_mem(executor::WasmExecutor, || (test_genesis_block.clone(), vec![])).unwrap(); - let genesis_hash = "af65e54217fb213853703d57b80fc5b2bb834bf923046294d7a49bff62f0a8b2".into(); + let genesis_hash = test_genesis_block.blake2_256().into(); assert_matches!( StateApi::call(&client, "balanceOf".into(), vec![1,2,3], genesis_hash),