From b255f8f98711c398eb532041bf4394ead2e2739b Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 4 Oct 2018 17:54:56 -0400 Subject: [PATCH 01/36] grandpa-compatible digest items --- substrate/core/finality-grandpa/src/lib.rs | 19 ++++++++++++++++++- substrate/core/sr-primitives/src/traits.rs | 4 ++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 969ebc197a..3321e9edad 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -43,7 +43,9 @@ use futures::stream::Fuse; use futures::sync::mpsc; use client::{Client, ImportNotifications, backend::Backend, CallExecutor}; use codec::{Encode, Decode}; -use runtime_primitives::traits::{As, NumberFor, Block as BlockT, Header as HeaderT}; +use runtime_primitives::traits::{ + As, NumberFor, Block as BlockT, Header as HeaderT, DigestItemFor, +}; use runtime_primitives::generic::BlockId; use substrate_primitives::{ed25519, AuthorityId, Blake2Hasher}; use tokio::timer::Interval; @@ -390,6 +392,7 @@ impl grandpa::Chain for Environment: As, + DigestItemFor: CompatibleDigestItem>, { fn ancestry(&self, base: Block::Hash, block: Block::Hash) -> Result, GrandpaError> { let tree_route_res = ::client::blockchain::tree_route( @@ -434,6 +437,16 @@ impl grandpa::Chain for Environment { + /// If this digest item notes a GRANDPA set change, return the number of + /// blocks the change should occur after. + fn scheduled_change_in(&self) -> Option { None } +} + impl voter::Environment for Environment where Block: 'static, B: Backend + 'static, @@ -441,6 +454,7 @@ impl voter::Environment for Environment: As, + DigestItemFor: CompatibleDigestItem>, { type Timer = Box>; type Id = AuthorityId; @@ -555,6 +569,7 @@ pub fn run_grandpa( N: Network + 'static, N::In: 'static, NumberFor: As, + DigestItemFor: CompatibleDigestItem>, { let chain_info = client.info()?; let genesis_hash = chain_info.chain.genesis_hash; @@ -597,6 +612,8 @@ mod tests { use keyring::Keyring; use client::BlockchainEvents; + impl CompatibleDigestItem> for DigestItemFor { } + #[derive(Clone)] struct TestGrandpaNetwork { inner: Arc>, diff --git a/substrate/core/sr-primitives/src/traits.rs b/substrate/core/sr-primitives/src/traits.rs index 24dda09345..072dffe3f3 100644 --- a/substrate/core/sr-primitives/src/traits.rs +++ b/substrate/core/sr-primitives/src/traits.rs @@ -447,6 +447,10 @@ pub trait Block: Clone + Send + Sync + Codec + Eq + MaybeSerializeDebug + 'stati pub type HashFor = <::Header as Header>::Hashing; /// Extract the number type for a block. pub type NumberFor = <::Header as Header>::Number; +/// Extract the digest type for a block. +pub type DigestFor = <::Header as Header>::Digest; +/// Extract the digest item type for a block. +pub type DigestItemFor = as Digest>::Item; /// A "checkable" piece of information, used by the standard Substrate Executive in order to /// check the validity of a piece of extrinsic information, usually by verifying the signature. From 1c75f8059a0bcd4962fbb39f01aa08de72629c4a Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Wed, 17 Oct 2018 14:18:29 -0700 Subject: [PATCH 02/36] round-localized signatures on GRANDPA messages --- substrate/core/finality-grandpa/src/lib.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 3321e9edad..8296502058 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -298,9 +298,15 @@ impl Drop for ClearOnDrop { } } +fn round_localized_payload(round: u64, message: &E) -> Vec { + let mut v = message.encode(); + round.using_encoded(|s| v.extend(s)); + v +} + // converts a message stream into a stream of signed messages. // the output stream checks signatures also. -fn checked_message_stream(inner: S, voters: Vec) +fn checked_message_stream(inner: S, round: u64, voters: Vec) -> impl Stream,Error=Error> where S: Stream,Error=()> { @@ -320,7 +326,7 @@ fn checked_message_stream(inner: S, voters: Vec) } let as_public = ::ed25519::Public::from_raw(msg.id.0); - let encoded_raw = msg.message.encode(); + let encoded_raw = round_localized_payload(round, &msg.message); if ::ed25519::verify_strong(&msg.signature, &encoded_raw, as_public) { Ok(Some(msg)) } else { @@ -349,9 +355,9 @@ fn outgoing_messages( let (tx, rx) = mpsc::unbounded(); let rx = rx .map(move |msg: Message| { - // when locals exist. sign messages on import + // when locals exist, sign messages on import if let Some((ref pair, local_id)) = locals { - let encoded = msg.encode(); + let encoded = round_localized_payload(round, &msg); let signature = pair.sign(&encoded[..]); let signed = SignedMessage:: { message: msg, @@ -477,6 +483,7 @@ impl voter::Environment for Environment( self.network.messages_for(round), + round, self.config.voters.clone(), ); From e7a50ff52ab007ad5282e525fb77c3dfd1a3c59a Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Wed, 17 Oct 2018 15:56:20 -0700 Subject: [PATCH 03/36] generalize some import_queue params --- substrate/core/client/src/client.rs | 4 +- substrate/core/network/src/import_queue.rs | 235 ++++++++++----------- substrate/core/network/src/service.rs | 19 +- substrate/core/network/src/test/mod.rs | 11 +- 4 files changed, 134 insertions(+), 135 deletions(-) diff --git a/substrate/core/client/src/client.rs b/substrate/core/client/src/client.rs index 81b59e2f7a..9122e1692c 100644 --- a/substrate/core/client/src/client.rs +++ b/substrate/core/client/src/client.rs @@ -892,7 +892,9 @@ impl Client where /// TODO [snd] possibly implement this on blockchain::Backend and just redirect here /// Returns `Ok(None)` if `target_hash` is not found in search space. /// TODO [snd] write down time complexity - pub fn best_containing(&self, target_hash: Block::Hash, maybe_max_number: Option>) -> error::Result> { + pub fn best_containing(&self, target_hash: Block::Hash, maybe_max_number: Option>) + -> error::Result> + { let target_header = { match self.backend.blockchain().header(BlockId::Hash(target_hash))? { Some(x) => x, diff --git a/substrate/core/network/src/import_queue.rs b/substrate/core/network/src/import_queue.rs index 26ad83e852..79cf19ecc0 100644 --- a/substrate/core/network/src/import_queue.rs +++ b/substrate/core/network/src/import_queue.rs @@ -65,15 +65,9 @@ pub trait ImportQueue: Send + Sync { /// /// This is called automatically by the network service when synchronization /// begins. - - fn start( - &self, - _sync: Weak>>, - _service: Weak, - _chain: Weak> - ) -> Result<(), Error> where + fn start(&self, _link: L) -> Result<(), Error> where Self: Sized, - E: 'static + ExecuteInContext, + L: 'static + Link, { Ok(()) } @@ -138,18 +132,16 @@ impl AsyncImportQueueData { } impl> ImportQueue for BasicQueue { - fn start>( + fn start>( &self, - sync: Weak>>, - service: Weak, - chain: Weak> + link: L, ) -> Result<(), Error> { debug_assert!(self.handle.lock().is_none()); let qdata = self.data.clone(); let verifier = self.verifier.clone(); *self.handle.lock() = Some(::std::thread::Builder::new().name("ImportQueue".into()).spawn(move || { - import_thread(sync, service, chain, qdata, verifier) + import_thread(link, qdata, verifier) }).map_err(|err| Error::from(ErrorKind::Io(err)))?); Ok(()) } @@ -211,10 +203,8 @@ impl> Drop for BasicQueue { } /// Blocks import thread. -fn import_thread, V: Verifier>( - sync: Weak>>, - service: Weak, - chain: Weak>, +fn import_thread, V: Verifier>( + link: L, qdata: Arc>, verifier: Arc ) { @@ -236,91 +226,89 @@ fn import_thread, V: Verifier>( } }; - match (sync.upgrade(), service.upgrade(), chain.upgrade()) { - (Some(sync), Some(service), Some(chain)) => { - let blocks_hashes: Vec = new_blocks.1.iter().map(|b| b.block.hash.clone()).collect(); - if !import_many_blocks( - &mut SyncLink{chain: &sync, client: &*chain, context: &*service}, - Some(&*qdata), - new_blocks, - verifier.clone(), - ) { - break; - } + let blocks_hashes: Vec = new_blocks.1.iter().map(|b| b.block.hash.clone()).collect(); + if !import_many_blocks( + &link, + Some(&*qdata), + new_blocks, + verifier.clone(), + ) { + break; + } - let mut queue_blocks = qdata.queue_blocks.write(); - for blocks_hash in blocks_hashes { - queue_blocks.remove(&blocks_hash); - } - }, - _ => break, + let mut queue_blocks = qdata.queue_blocks.write(); + for blocks_hash in blocks_hashes { + queue_blocks.remove(&blocks_hash); } } trace!(target: "sync", "Stopping import thread"); } -/// ChainSync link trait. -trait SyncLinkApi { + +/// Hooks that the verification queue can use to influence the synchronization +/// algorithm. +pub trait Link: Send { /// Get chain reference. fn chain(&self) -> &Client; /// Block imported. - fn block_imported(&mut self, hash: &B::Hash, number: NumberFor); + fn block_imported(&self, hash: &B::Hash, number: NumberFor); /// Maintain sync. - fn maintain_sync(&mut self); + fn maintain_sync(&self); /// Disconnect from peer. - fn useless_peer(&mut self, who: NodeIndex, reason: &str); + fn useless_peer(&self, who: NodeIndex, reason: &str); /// Disconnect from peer and restart sync. - fn note_useless_and_restart_sync(&mut self, who: NodeIndex, reason: &str); + fn note_useless_and_restart_sync(&self, who: NodeIndex, reason: &str); /// Restart sync. - fn restart(&mut self); + fn restart(&self); } - -/// Link with the ChainSync service. -struct SyncLink<'a, B: 'a + BlockT, E: 'a + ExecuteInContext> { - pub chain: &'a RwLock>, - pub client: &'a Client, - pub context: &'a E, +/// A link implementation that connects to the network. +pub struct NetworkLink> { + /// The client handle. + pub(crate) client: Arc>, + /// The chain-sync handle + pub(crate) sync: Weak>>, + /// Network context. + pub(crate) context: Weak, } -impl<'a, B: 'static + BlockT, E: 'a + ExecuteInContext> SyncLink<'a, B, E> { - /// Execute closure with locked ChainSync. - fn with_sync, &mut Context)>(&mut self, closure: F) { - let service = self.context; - let sync = self.chain; - service.execute_in_context(move |protocol| { - let mut sync = sync.write(); - closure(&mut *sync, protocol) - }); +impl> NetworkLink { + /// Execute closure with locked ChainSync. + fn with_sync, &mut Context)>(&self, closure: F) { + if let (Some(sync), Some(service)) = (self.sync.upgrade(), self.context.upgrade()) { + service.execute_in_context(move |protocol| { + let mut sync = sync.write(); + closure(&mut *sync, protocol) + }); + } } } -impl<'a, B: 'static + BlockT, E: 'a + ExecuteInContext> SyncLinkApi for SyncLink<'a, B, E> { - +impl> Link for NetworkLink { fn chain(&self) -> &Client { - self.client + &*self.client } - fn block_imported(&mut self, hash: &B::Hash, number: NumberFor) { + fn block_imported(&self, hash: &B::Hash, number: NumberFor) { self.with_sync(|sync, _| sync.block_imported(&hash, number)) } - fn maintain_sync(&mut self) { + fn maintain_sync(&self) { self.with_sync(|sync, protocol| sync.maintain_sync(protocol)) } - fn useless_peer(&mut self, who: NodeIndex, reason: &str) { + fn useless_peer(&self, who: NodeIndex, reason: &str) { self.with_sync(|_, protocol| protocol.report_peer(who, Severity::Useless(reason))) } - fn note_useless_and_restart_sync(&mut self, who: NodeIndex, reason: &str) { + fn note_useless_and_restart_sync(&self, who: NodeIndex, reason: &str) { self.with_sync(|sync, protocol| { protocol.report_peer(who, Severity::Useless(reason)); // is this actually malign or just useless? sync.restart(protocol); }) } - fn restart(&mut self) { + fn restart(&self) { self.with_sync(|sync, protocol| sync.restart(protocol)) } } @@ -352,8 +340,8 @@ enum BlockImportError { } /// Import a bunch of blocks. -fn import_many_blocks<'a, B: BlockT, V: Verifier>( - link: &mut SyncLinkApi, +fn import_many_blocks<'a, B: BlockT, L: Link, V: Verifier>( + link: &L, qdata: Option<&AsyncImportQueueData>, blocks: (BlockOrigin, Vec>), verifier: Arc @@ -473,7 +461,7 @@ fn import_single_block>( /// Process single block import result. fn process_import_result<'a, B: BlockT>( - link: &mut SyncLinkApi, + link: &Link, result: Result::Header as HeaderT>::Number>, BlockImportError> ) -> usize { @@ -545,7 +533,7 @@ unsafe impl Sync for ImportCB {} #[cfg(any(test, feature = "test-helpers"))] /// A Verifier that accepts all blocks and passes them on with the configured -/// finality to be imported. +/// finality to be imported. pub struct PassThroughVerifier(pub bool); #[cfg(any(test, feature = "test-helpers"))] @@ -585,25 +573,19 @@ impl> SyncImportQueue { #[cfg(any(test, feature = "test-helpers"))] impl> ImportQueue for SyncImportQueue { - fn start>( + fn start>( &self, - sync: Weak>>, - service: Weak, - chain: Weak> + link: L, ) -> Result<(), Error> { let v = self.0.clone(); - self.1.set(Box::new(move | origin, new_blocks | { + self.1.set(Box::new(move |origin, new_blocks| { let verifier = v.clone(); - match (sync.upgrade(), service.upgrade(), chain.upgrade()) { - (Some(sync), Some(service), Some(chain)) => - import_many_blocks( - &mut SyncLink{chain: &sync, client: &*chain, context: &*service}, - None, - (origin, new_blocks), - verifier, - ), - _ => false - } + import_many_blocks( + &link, + None, + (origin, new_blocks), + verifier, + ) })); Ok(()) } @@ -635,40 +617,51 @@ pub mod tests { use test_client::runtime::{Block, Hash}; use on_demand::tests::DummyExecutor; use runtime_primitives::generic::BlockId; + use std::cell::Cell; use super::*; - struct TestLink { chain: Arc>, - imported: usize, - maintains: usize, - disconnects: usize, - restarts: usize, + imported: Cell, + maintains: Cell, + disconnects: Cell, + restarts: Cell, } impl TestLink { fn new() -> TestLink { TestLink { chain: Arc::new(test_client::new()), - imported: 0, - maintains: 0, - disconnects: 0, - restarts: 0, + imported: Cell::new(0), + maintains: Cell::new(0), + disconnects: Cell::new(0), + restarts: Cell::new(0), } } fn total(&self) -> usize { - self.imported + self.maintains + self.disconnects + self.restarts + self.imported.get() + self.maintains.get() + self.disconnects.get() + self.restarts.get() } } - impl SyncLinkApi for TestLink { + impl Link for TestLink { fn chain(&self) -> &Client { &*self.chain } - fn block_imported(&mut self, _hash: &Hash, _number: NumberFor) { self.imported += 1; } - fn maintain_sync(&mut self) { self.maintains += 1; } - fn useless_peer(&mut self, _: NodeIndex, _: &str) { self.disconnects += 1; } - fn note_useless_and_restart_sync(&mut self, _: NodeIndex, _: &str) { self.disconnects += 1; self.restarts += 1; } - fn restart(&mut self) { self.restarts += 1; } + fn block_imported(&self, _hash: &Hash, _number: NumberFor) { + self.imported.set(self.imported.get() + 1); + } + fn maintain_sync(&self) { + self.maintains.set(self.maintains.get() + 1); + } + fn useless_peer(&self, _: NodeIndex, _: &str) { + self.disconnects.set(self.disconnects.get() + 1); + } + fn note_useless_and_restart_sync(&self, id: NodeIndex, r: &str) { + self.useless_peer(id, r); + self.restart(); + } + fn restart(&self) { + self.restarts.set(self.restarts.get() + 1); + } } fn prepare_good_block() -> (client::Client, Hash, u64, BlockData) { @@ -729,39 +722,39 @@ pub mod tests { #[test] fn process_import_result_works() { - let mut link = TestLink::new(); - assert_eq!(process_import_result::(&mut link, Ok(BlockImportResult::ImportedKnown(Default::default(), 0))), 1); + let link = TestLink::new(); + assert_eq!(process_import_result::(&link, Ok(BlockImportResult::ImportedKnown(Default::default(), 0))), 1); assert_eq!(link.total(), 1); - let mut link = TestLink::new(); - assert_eq!(process_import_result::(&mut link, Ok(BlockImportResult::ImportedKnown(Default::default(), 0))), 1); + let link = TestLink::new(); + assert_eq!(process_import_result::(&link, Ok(BlockImportResult::ImportedKnown(Default::default(), 0))), 1); assert_eq!(link.total(), 1); - assert_eq!(link.imported, 1); + assert_eq!(link.imported.get(), 1); - let mut link = TestLink::new(); - assert_eq!(process_import_result::(&mut link, Ok(BlockImportResult::ImportedUnknown(Default::default(), 0))), 1); + let link = TestLink::new(); + assert_eq!(process_import_result::(&link, Ok(BlockImportResult::ImportedUnknown(Default::default(), 0))), 1); assert_eq!(link.total(), 1); - assert_eq!(link.imported, 1); + assert_eq!(link.imported.get(), 1); - let mut link = TestLink::new(); - assert_eq!(process_import_result::(&mut link, Err(BlockImportError::IncompleteHeader(Some(0)))), 0); + let link = TestLink::new(); + assert_eq!(process_import_result::(&link, Err(BlockImportError::IncompleteHeader(Some(0)))), 0); assert_eq!(link.total(), 1); - assert_eq!(link.disconnects, 1); + assert_eq!(link.disconnects.get(), 1); - let mut link = TestLink::new(); - assert_eq!(process_import_result::(&mut link, Err(BlockImportError::IncompleteJustification(Some(0)))), 0); + let link = TestLink::new(); + assert_eq!(process_import_result::(&link, Err(BlockImportError::IncompleteJustification(Some(0)))), 0); assert_eq!(link.total(), 1); - assert_eq!(link.disconnects, 1); + assert_eq!(link.disconnects.get(), 1); - let mut link = TestLink::new(); - assert_eq!(process_import_result::(&mut link, Err(BlockImportError::UnknownParent)), 0); + let link = TestLink::new(); + assert_eq!(process_import_result::(&link, Err(BlockImportError::UnknownParent)), 0); assert_eq!(link.total(), 1); - assert_eq!(link.restarts, 1); + assert_eq!(link.restarts.get(), 1); - let mut link = TestLink::new(); - assert_eq!(process_import_result::(&mut link, Err(BlockImportError::Error)), 0); + let link = TestLink::new(); + assert_eq!(process_import_result::(&link, Err(BlockImportError::Error)), 0); assert_eq!(link.total(), 1); - assert_eq!(link.restarts, 1); + assert_eq!(link.restarts.get(), 1); } #[test] @@ -782,9 +775,7 @@ pub mod tests { fn async_import_queue_drops() { let verifier = Arc::new(PassThroughVerifier(true)); let queue = BasicQueue::new(verifier); - let service = Arc::new(DummyExecutor); - let chain = Arc::new(test_client::new()); - queue.start(Weak::new(), Arc::downgrade(&service), Arc::downgrade(&chain) as Weak>).unwrap(); + queue.start(TestLink::new()).unwrap(); drop(queue); } } diff --git a/substrate/core/network/src/service.rs b/substrate/core/network/src/service.rs index 5fd479c167..52db914d6e 100644 --- a/substrate/core/network/src/service.rs +++ b/substrate/core/network/src/service.rs @@ -129,7 +129,7 @@ impl, H: ExHashT> Service { params: Params, protocol_id: ProtocolId, import_queue: I, - ) -> Result>, Error> { + ) -> Result>, Error> { let chain = params.chain.clone(); let import_queue = Arc::new(import_queue); let handler = Arc::new(Protocol::new( @@ -144,20 +144,23 @@ impl, H: ExHashT> Service { let registered = RegisteredProtocol::new(protocol_id, &versions[..]); let (thread, network) = start_thread(params.network_config, handler.clone(), registered)?; - let sync = Arc::new(Service { + let service = Arc::new(Service { network, protocol_id, handler, bg_thread: Some(thread), }); - import_queue.start( - Arc::downgrade(sync.handler.sync()), - Arc::downgrade(&sync), - Arc::downgrade(&chain) - )?; + // connect the import-queue to the network service. + let link = ::import_queue::NetworkLink { + client: chain, + sync: Arc::downgrade(service.handler.sync()), + context: Arc::downgrade(&service), + }; - Ok(sync) + import_queue.start(link)?; + + Ok(service) } /// Called when a new block is imported by the client. diff --git a/substrate/core/network/src/test/mod.rs b/substrate/core/network/src/test/mod.rs index 243e63e2b3..50e644c261 100644 --- a/substrate/core/network/src/test/mod.rs +++ b/substrate/core/network/src/test/mod.rs @@ -158,10 +158,13 @@ impl Peer { // 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.import_queue.start( - Arc::downgrade(&self.sync.sync()), - Arc::downgrade(&self.executor), - Arc::downgrade(&self.sync.context_data().chain)).expect("Test ImportQueue always starts"); + let network_link = ::import_queue::NetworkLink { + client: self.sync.context_data().chain.clone(), + sync: Arc::downgrade(self.sync.sync()), + context: Arc::downgrade(&self.executor), + }; + + self.import_queue.start(network_link).expect("Test ImportQueue always starts"); self.sync.on_block_imported(&mut TestIo::new(&self.queue, None), info.chain.best_hash, &header); } From fa62c8e9df3a0e758470f4de077b879adfc1618c Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Wed, 17 Oct 2018 16:09:05 -0700 Subject: [PATCH 04/36] start import queue in chain_ops as well --- substrate/core/network/src/import_queue.rs | 10 +++++----- substrate/core/service/src/chain_ops.rs | 12 ++++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/substrate/core/network/src/import_queue.rs b/substrate/core/network/src/import_queue.rs index 79cf19ecc0..328dc478dc 100644 --- a/substrate/core/network/src/import_queue.rs +++ b/substrate/core/network/src/import_queue.rs @@ -251,15 +251,15 @@ pub trait Link: Send { /// Get chain reference. fn chain(&self) -> &Client; /// Block imported. - fn block_imported(&self, hash: &B::Hash, number: NumberFor); + fn block_imported(&self, _hash: &B::Hash, _number: NumberFor) { } /// Maintain sync. - fn maintain_sync(&self); + fn maintain_sync(&self) { } /// Disconnect from peer. - fn useless_peer(&self, who: NodeIndex, reason: &str); + fn useless_peer(&self, _who: NodeIndex, _reason: &str) { } /// Disconnect from peer and restart sync. - fn note_useless_and_restart_sync(&self, who: NodeIndex, reason: &str); + fn note_useless_and_restart_sync(&self, _who: NodeIndex, _reason: &str) { } /// Restart sync. - fn restart(&self); + fn restart(&self) { } } /// A link implementation that connects to the network. diff --git a/substrate/core/service/src/chain_ops.rs b/substrate/core/service/src/chain_ops.rs index 38aabe7d68..a3cd5bb770 100644 --- a/substrate/core/service/src/chain_ops.rs +++ b/substrate/core/service/src/chain_ops.rs @@ -23,7 +23,7 @@ use serde_json; use client::BlockOrigin; use runtime_primitives::generic::{SignedBlock, BlockId}; use runtime_primitives::traits::{As, Block, Header}; -use network::import_queue::{ImportQueue, BlockData}; +use network::import_queue::{ImportQueue, Link, BlockData}; use network::message; use components::{self, Components, ServiceFactory, FactoryFullConfiguration, FactoryBlockNumber, RuntimeGenesis}; use new_client; @@ -87,8 +87,16 @@ pub fn export_blocks(config: FactoryFullConfiguration, exit: E, mut pub fn import_blocks(config: FactoryFullConfiguration, exit: E, mut input: R) -> error::Result<()> where F: ServiceFactory, E: Future + Send + 'static, R: Read, { + use network::ClientHandle; + + struct DummyLink(::std::sync::Arc); + impl> Link for DummyLink { + fn chain(&self) -> &ClientHandle { &*self.0 } + } + let client = new_client::(&config)?; let queue = components::FullComponents::::build_import_queue(&config, client.clone())?; + queue.start(DummyLink(client.clone()))?; let (exit_send, exit_recv) = std::sync::mpsc::channel(); ::std::thread::spawn(move || { @@ -98,7 +106,7 @@ pub fn import_blocks(config: FactoryFullConfiguration, exit: E, mut let count: u32 = Decode::decode(&mut input).ok_or("Error reading file")?; info!("Importing {} blocks", count); - let mut block_count = 0; + let mut block_count = 0; for b in 0 .. count { if exit_recv.try_recv().is_ok() { break; From 515153fa855e3163c78ad515873eabf3f34da805 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 21 Oct 2018 11:11:53 +0200 Subject: [PATCH 05/36] localize messages to set-index and add pending changes --- substrate/Cargo.lock | 1 + substrate/core/finality-grandpa/Cargo.toml | 2 + .../core/finality-grandpa/src/authorities.rs | 98 +++++++++++++++++++ substrate/core/finality-grandpa/src/lib.rs | 41 ++++++-- 4 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 substrate/core/finality-grandpa/src/authorities.rs diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 34c1f606c9..6377a2a15c 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -3027,6 +3027,7 @@ dependencies = [ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-codec-derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 0.1.0", "substrate-client 0.1.0", diff --git a/substrate/core/finality-grandpa/Cargo.toml b/substrate/core/finality-grandpa/Cargo.toml index b507ffd6d7..88b11d7c75 100644 --- a/substrate/core/finality-grandpa/Cargo.toml +++ b/substrate/core/finality-grandpa/Cargo.toml @@ -6,9 +6,11 @@ authors = ["Parity Technologies "] [dependencies] futures = "0.1.17" parity-codec = "2.1" +parity-codec-derive = "2.0" sr-primitives = { path = "../sr-primitives" } substrate-primitives = { path = "../primitives" } substrate-client = { path = "../client" } +substrate-network = { path = "../network" } log = "0.4" tokio = "0.1.7" diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs new file mode 100644 index 0000000000..31d61ef5e1 --- /dev/null +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -0,0 +1,98 @@ +// Copyright 2018 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 . + +//! Utilities for dealing with authorities, authority sets, and handoffs. + +use substrate_primitives::AuthorityId; +use std::ops::Add; + +/// A shared authority set. +pub(crate) struct SharedAuthoritySet { + inner: RwLock>, +} + +impl SharedAuthoritySet { + /// The genesis authority set. + pub(crate) fn genesis(initial: Vec<(AuthorityId, usize)>) -> Self { + SharedAuthoritySet { + inner: RwLock::new(AuthoritySet { + current_authorities: initial, + set_id: 0, + pending_changes: Vec::new(), + }) + } + } + + /// Execute some work using the inner authority set. + pub(crate) fn with(&self, f: F) -> U + where F: FnOnce(&AuthoritySet) -> U + { + f(&*self.inner.read()) + } + +impl SharedAuthoritySet { + /// Note an upcoming pending transition. + pub(crate) fn add_pending_change(&self, pending: PendingChange) { + let idx = self.pending_changes + .binary_search_by_key(|change| change.effective_number()) + .unwrap_or_else(|i| i); + + self.pending_changes.insert(idx); + } + + /// Get the earliest limit-block number, if any. + pub(crate) fn current_limit(&self) -> Option<&N> { + self.pending_changes.get(0).map(|change| &change.effective_number()); + } +} + +impl From> for SharedAuthoritySet { + fn from(set: AuthoritySet) -> Self { + SharedAuthoritySet { inner: RwLock::new(set) } + } +} + +/// A set of authorities. +#[derive(Encode, Decode)] +pub(crate) struct AuthoritySet { + current_authorities: Vec<(AuthorityId, usize)>, + set_id: u64, + pending_changes: Vec>, +} + +/// A pending change to the authority set. +/// +/// This will be applied when the announcing block is at some depth within +/// the finalized chain. +#[derive(Encode, Decode)] +pub(crate) struct PendingChange { + /// The new authorities and weights to apply. + pub(crate) next_authorities: Vec<(AuthorityId, usize)>, + /// How deep in the finalized chain the announcing block must be + /// before the change is applied. + pub(crate) finalization_depth: N, + /// The announcing block's height. + pub(crate) canon_height: N, + /// The announcing block's hash. + pub(crate) canon_hash: H, +} + +impl PendingChange { + /// Returns the effective number. + fn effective_number(&self) -> N { + self.canon_height + self.finalization_depth + } +} diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 8296502058..2a6e7814e4 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -23,6 +23,7 @@ extern crate futures; extern crate substrate_client as client; extern crate sr_primitives as runtime_primitives; extern crate substrate_primitives; +extern crate substrate_network as network; extern crate tokio; extern crate parity_codec as codec; @@ -38,6 +39,9 @@ extern crate parking_lot; #[cfg(test)] extern crate substrate_keyring as keyring; +#[macro_use] +extern crate parity_codec_derive; + use futures::prelude::*; use futures::stream::Fuse; use futures::sync::mpsc; @@ -57,7 +61,13 @@ use std::collections::{VecDeque, HashMap}; use std::sync::Arc; use std::time::{Instant, Duration}; +mod authorities; + const LAST_COMPLETED_KEY: &[u8] = b"grandpa_completed_round"; +const AUTHORITY_SET_KEY: &[u8] = b"grandpa_voters"; + +/// round-number, round-state, set indicator +type LastCompleted = (u64, RoundState, u64); /// A GRANDPA message for a substrate chain. pub type Message = grandpa::Message<::Hash>; @@ -69,8 +79,7 @@ pub struct Config { /// The expected duration for a message to be gossiped across the network. pub gossip_duration: Duration, /// The voters. - // TODO: make dynamic - pub voters: Vec, + pub genesis_voters: Vec, /// The local signing key. pub local_key: Option>, } @@ -298,15 +307,23 @@ impl Drop for ClearOnDrop { } } -fn round_localized_payload(round: u64, message: &E) -> Vec { +fn localized_payload(round: u64, set_id: u64, message: &E) -> Vec { let mut v = message.encode(); + round.using_encoded(|s| v.extend(s)); + set_id.using_encoded(|s| v.extend(s)); + v } // converts a message stream into a stream of signed messages. // the output stream checks signatures also. -fn checked_message_stream(inner: S, round: u64, voters: Vec) +fn checked_message_stream( + round: u64, + set_id: u64, + inner: S, + voters: Vec, +) -> impl Stream,Error=Error> where S: Stream,Error=()> { @@ -326,7 +343,7 @@ fn checked_message_stream(inner: S, round: u64, voters: Vec(inner: S, round: u64, voters: Vec( + round: u64, + set_id: u64, local_key: Option>, voters: Vec, - round: u64, network: N, ) -> ( impl Stream,Error=Error>, @@ -357,7 +375,7 @@ fn outgoing_messages( .map(move |msg: Message| { // when locals exist, sign messages on import if let Some((ref pair, local_id)) = locals { - let encoded = round_localized_payload(round, &msg); + let encoded = localized_payload(round, set_id, &msg); let signature = pair.sign(&encoded[..]); let signed = SignedMessage:: { message: msg, @@ -480,17 +498,20 @@ impl voter::Environment for Environment( - self.network.messages_for(round), round, - self.config.voters.clone(), + set_id, + self.network.messages_for(round), + self.config.genesis_voters.clone(), ); let (out_rx, outgoing) = outgoing_messages::( + round, + set_id, self.config.local_key.clone(), self.config.voters.clone(), - round, self.network.clone(), ); From 08890f2bc13a0bbfac881ca7407a1c5c49eda3f8 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 25 Oct 2018 16:48:51 +0200 Subject: [PATCH 06/36] note future changes when importing block --- substrate/Cargo.lock | 10 +- substrate/core/consensus/common/Cargo.toml | 5 +- substrate/core/consensus/common/src/lib.rs | 11 ++- .../consensus/common/src/offline_tracker.rs | 14 +-- substrate/core/finality-grandpa/Cargo.toml | 5 +- .../core/finality-grandpa/src/authorities.rs | 42 +++++--- substrate/core/finality-grandpa/src/lib.rs | 95 +++++++++++++++++-- 7 files changed, 138 insertions(+), 44 deletions(-) diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index e3e4b66e00..0bbfc82ad0 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -580,8 +580,8 @@ dependencies = [ [[package]] name = "finality-grandpa" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +version = "0.3.0" +source = "git+https://github.com/paritytech/finality-grandpa#fffce2dc450e72ff8b3f43818f8aceb5cfacc2d7" dependencies = [ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2965,6 +2965,7 @@ dependencies = [ name = "substrate-consensus-common" version = "0.1.0" dependencies = [ + "sr-primitives 0.1.0", "substrate-primitives 0.1.0", ] @@ -3019,7 +3020,7 @@ dependencies = [ name = "substrate-finality-grandpa" version = "0.1.0" dependencies = [ - "finality-grandpa 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "finality-grandpa 0.3.0 (git+https://github.com/paritytech/finality-grandpa)", "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3027,6 +3028,7 @@ dependencies = [ "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 0.1.0", "substrate-client 0.1.0", + "substrate-consensus-common 0.1.0", "substrate-keyring 0.1.0", "substrate-network 0.1.0", "substrate-primitives 0.1.0", @@ -4179,7 +4181,7 @@ dependencies = [ "checksum failure_derive 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "946d0e98a50d9831f5d589038d2ca7f8f455b1c21028c0db0e84116a12696426" "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" "checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa" -"checksum finality-grandpa 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "20d8cf871510f0d57630e75f9e65f87cba29581ccab1f78666d8b2e422d0baa6" +"checksum finality-grandpa 0.3.0 (git+https://github.com/paritytech/finality-grandpa)" = "" "checksum fixed-hash 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d5ec8112f00ea8a483e04748a85522184418fd1cf02890b626d8fc28683f7de" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" diff --git a/substrate/core/consensus/common/Cargo.toml b/substrate/core/consensus/common/Cargo.toml index ff5ebae0f8..ed39cf8fd4 100644 --- a/substrate/core/consensus/common/Cargo.toml +++ b/substrate/core/consensus/common/Cargo.toml @@ -4,5 +4,6 @@ version = "0.1.0" authors = ["Parity Technologies "] description = "Common utilities for substrate consensus" -[dev-dependencies] -substrate-primitives = { path= "../../primitives"} \ No newline at end of file +[dependencies] +substrate-primitives = { path = "../../primitives"} +sr-primitives = { path = "../../sr-primitives" } diff --git a/substrate/core/consensus/common/src/lib.rs b/substrate/core/consensus/common/src/lib.rs index a5bf6d218a..390e7beade 100644 --- a/substrate/core/consensus/common/src/lib.rs +++ b/substrate/core/consensus/common/src/lib.rs @@ -17,12 +17,13 @@ //! Tracks offline validators. #![allow(dead_code)] -#![cfg(feature="rhd")] - extern crate substrate_primitives as primitives; +extern crate sr_primitives; -use primitives::{generic::BlockId, Justification}; -use primitives::traits::{Block, Header}; +use sr_primitives::{generic::BlockId}; +use sr_primitives::traits::{Block, Header}; +use sr_primitives::Justification; +use primitives::AuthorityId; /// Block import trait. pub trait BlockImport { @@ -30,4 +31,4 @@ pub trait BlockImport { fn import_block(&self, block: B, justification: Justification, authorities: &[AuthorityId]) -> bool; } -pub mod offline_tracker; \ No newline at end of file +pub mod offline_tracker; diff --git a/substrate/core/consensus/common/src/offline_tracker.rs b/substrate/core/consensus/common/src/offline_tracker.rs index 18845dd68b..bd8eab8b1b 100644 --- a/substrate/core/consensus/common/src/offline_tracker.rs +++ b/substrate/core/consensus/common/src/offline_tracker.rs @@ -16,7 +16,7 @@ //! Tracks offline validators. -use node_primitives::AccountId; +use primitives::AuthorityId; use std::collections::HashMap; use std::time::{Instant, Duration}; @@ -56,7 +56,7 @@ impl Observed { /// Tracks offline validators and can issue a report for those offline. pub struct OfflineTracker { - observed: HashMap, + observed: HashMap, } impl OfflineTracker { @@ -66,7 +66,7 @@ impl OfflineTracker { } /// Note new consensus is starting with the given set of validators. - pub fn note_new_block(&mut self, validators: &[AccountId]) { + pub fn note_new_block(&mut self, validators: &[AuthorityId]) { use std::collections::HashSet; let set: HashSet<_> = validators.iter().cloned().collect(); @@ -74,14 +74,14 @@ impl OfflineTracker { } /// Note that a round has ended. - pub fn note_round_end(&mut self, validator: AccountId, was_online: bool) { + pub fn note_round_end(&mut self, validator: AuthorityId, was_online: bool) { self.observed.entry(validator) .or_insert_with(Observed::new) .note_round_end(was_online); } /// Generate a vector of indices for offline account IDs. - pub fn reports(&self, validators: &[AccountId]) -> Vec { + pub fn reports(&self, validators: &[AuthorityId]) -> Vec { validators.iter() .enumerate() .filter_map(|(i, v)| if self.is_online(v) { @@ -93,7 +93,7 @@ impl OfflineTracker { } /// Whether reports on a validator set are consistent with our view of things. - pub fn check_consistency(&self, validators: &[AccountId], reports: &[u32]) -> bool { + pub fn check_consistency(&self, validators: &[AuthorityId], reports: &[u32]) -> bool { reports.iter().cloned().all(|r| { let v = match validators.get(r as usize) { Some(v) => v, @@ -106,7 +106,7 @@ impl OfflineTracker { }) } - fn is_online(&self, v: &AccountId) -> bool { + fn is_online(&self, v: &AuthorityId) -> bool { self.observed.get(v).map(Observed::is_active).unwrap_or(true) } } diff --git a/substrate/core/finality-grandpa/Cargo.toml b/substrate/core/finality-grandpa/Cargo.toml index 88b11d7c75..34c0e99b63 100644 --- a/substrate/core/finality-grandpa/Cargo.toml +++ b/substrate/core/finality-grandpa/Cargo.toml @@ -8,14 +8,17 @@ futures = "0.1.17" parity-codec = "2.1" parity-codec-derive = "2.0" sr-primitives = { path = "../sr-primitives" } +substrate-consensus-common = { path = "../consensus/common" } substrate-primitives = { path = "../primitives" } substrate-client = { path = "../client" } substrate-network = { path = "../network" } log = "0.4" +parking_lot = "0.4" tokio = "0.1.7" [dependencies.finality-grandpa] -version = "0.2.0" +#version = "0.3.0" +git = "https://github.com/paritytech/finality-grandpa" features = ["derive-codec"] [dev-dependencies] diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index 31d61ef5e1..0ed2bcb36c 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -16,23 +16,33 @@ //! Utilities for dealing with authorities, authority sets, and handoffs. +use parking_lot::RwLock; use substrate_primitives::AuthorityId; + +use std::cmp::Ord; use std::ops::Add; +use std::sync::Arc; /// A shared authority set. pub(crate) struct SharedAuthoritySet { - inner: RwLock>, + inner: Arc>>, +} + +impl Clone for SharedAuthoritySet { + fn clone(&self) -> Self { + SharedAuthoritySet { inner: self.inner.clone() } + } } impl SharedAuthoritySet { /// The genesis authority set. - pub(crate) fn genesis(initial: Vec<(AuthorityId, usize)>) -> Self { + pub(crate) fn genesis(initial: Vec<(AuthorityId, u64)>) -> Self { SharedAuthoritySet { - inner: RwLock::new(AuthoritySet { + inner: Arc::new(RwLock::new(AuthoritySet { current_authorities: initial, set_id: 0, pending_changes: Vec::new(), - }) + })) } } @@ -42,33 +52,35 @@ impl SharedAuthoritySet { { f(&*self.inner.read()) } +} -impl SharedAuthoritySet { +impl + Ord + Clone> SharedAuthoritySet { /// Note an upcoming pending transition. pub(crate) fn add_pending_change(&self, pending: PendingChange) { - let idx = self.pending_changes - .binary_search_by_key(|change| change.effective_number()) + let mut inner = self.inner.write(); + let idx = inner.pending_changes + .binary_search_by_key(&pending.effective_number(), |change| change.effective_number()) .unwrap_or_else(|i| i); - self.pending_changes.insert(idx); + inner.pending_changes.insert(idx, pending); } /// Get the earliest limit-block number, if any. - pub(crate) fn current_limit(&self) -> Option<&N> { - self.pending_changes.get(0).map(|change| &change.effective_number()); + pub(crate) fn current_limit(&self) -> Option { + self.inner.read().pending_changes.get(0).map(|change| change.effective_number().clone()) } } impl From> for SharedAuthoritySet { fn from(set: AuthoritySet) -> Self { - SharedAuthoritySet { inner: RwLock::new(set) } + SharedAuthoritySet { inner: Arc::new(RwLock::new(set)) } } } /// A set of authorities. #[derive(Encode, Decode)] pub(crate) struct AuthoritySet { - current_authorities: Vec<(AuthorityId, usize)>, + current_authorities: Vec<(AuthorityId, u64)>, set_id: u64, pending_changes: Vec>, } @@ -80,7 +92,7 @@ pub(crate) struct AuthoritySet { #[derive(Encode, Decode)] pub(crate) struct PendingChange { /// The new authorities and weights to apply. - pub(crate) next_authorities: Vec<(AuthorityId, usize)>, + pub(crate) next_authorities: Vec<(AuthorityId, u64)>, /// How deep in the finalized chain the announcing block must be /// before the change is applied. pub(crate) finalization_depth: N, @@ -90,9 +102,9 @@ pub(crate) struct PendingChange { pub(crate) canon_hash: H, } -impl PendingChange { +impl + Clone> PendingChange { /// Returns the effective number. fn effective_number(&self) -> N { - self.canon_height + self.finalization_depth + self.canon_height.clone() + self.finalization_depth.clone() } } diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 2a6e7814e4..6411fee6ad 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -22,9 +22,11 @@ extern crate finality_grandpa as grandpa; extern crate futures; extern crate substrate_client as client; extern crate sr_primitives as runtime_primitives; +extern crate substrate_consensus_common as consensus_common; extern crate substrate_primitives; extern crate substrate_network as network; extern crate tokio; +extern crate parking_lot; extern crate parity_codec as codec; #[macro_use] @@ -47,10 +49,11 @@ use futures::stream::Fuse; use futures::sync::mpsc; use client::{Client, ImportNotifications, backend::Backend, CallExecutor}; use codec::{Encode, Decode}; +use consensus_common::BlockImport; use runtime_primitives::traits::{ As, NumberFor, Block as BlockT, Header as HeaderT, DigestItemFor, }; -use runtime_primitives::generic::BlockId; +use runtime_primitives::{generic::BlockId, Justification}; use substrate_primitives::{ed25519, AuthorityId, Blake2Hasher}; use tokio::timer::Interval; @@ -61,6 +64,8 @@ use std::collections::{VecDeque, HashMap}; use std::sync::Arc; use std::time::{Instant, Duration}; +use authorities::SharedAuthoritySet; + mod authorities; const LAST_COMPLETED_KEY: &[u8] = b"grandpa_completed_round"; @@ -404,8 +409,9 @@ fn outgoing_messages( /// The environment we run GRANDPA in. pub struct Environment { inner: Arc>, - voters: HashMap, + voters: HashMap, config: Config, + authority_set: SharedAuthoritySet>, network: N, } @@ -461,14 +467,23 @@ impl grandpa::Chain for Environment { + /// The new authorities after the change, along with their respective weights. + pub next_authorities: Vec<(AuthorityId, u64)>, + /// The number of blocks to delay. + pub delay: N, +} + /// A GRANDPA-compatible DigestItem. This can describe when GRANDPA set changes /// are scheduled. // TODO: with specialization, do a blanket implementation so this trait // doesn't have to be implemented by users. pub trait CompatibleDigestItem { - /// If this digest item notes a GRANDPA set change, return the number of - /// blocks the change should occur after. - fn scheduled_change_in(&self) -> Option { None } + /// If this digest item notes a GRANDPA set change, return information about + /// the scheduled change. + fn scheduled_change(&self) -> Option> { None } } impl voter::Environment for Environment where @@ -487,6 +502,7 @@ impl voter::Environment for Environment, SinkError = Self::Error>>; type Error = Error; + #[allow(unreachable_code)] fn round_data( &self, round: u64 @@ -498,7 +514,9 @@ impl voter::Environment for Environment( round, @@ -511,7 +529,7 @@ impl voter::Environment for Environment voter::Environment for Environment { + inner: Arc>, + authority_set: SharedAuthoritySet>, +} + +impl BlockImport for GrandpaBlockImport where + B: Backend + 'static, + E: CallExecutor + 'static, + DigestItemFor: CompatibleDigestItem>, +{ + fn import_block(&self, block: Block, _justification: Justification, _authorities: &[AuthorityId]) -> bool { + use runtime_primitives::traits::Digest; + use authorities::PendingChange; + + let maybe_event = block.header().digest().logs().iter() + .filter_map(|log| log.scheduled_change()) + .next() + .map(|change| (block.header().hash(), *block.header().number(), change)); + + // TODO [now]: use import-block trait for client when implemented + let result = self.inner.import_block(unimplemented!(), unimplemented!()).is_ok(); + if let (true, Some((hash, number, change))) = (result, maybe_event) { + self.authority_set.add_pending_change(PendingChange { + next_authorities: change.next_authorities, + finalization_depth: number + change.delay, + canon_height: number, + canon_hash: hash, + }); + + // TODO [now]: write to DB, and what to do on failure? + } + result + } +} + +/// Run a GRANDPA voter as a task. This returns two pieces of data: a task to run, +/// and a `BlockImport` implementation. pub fn run_grandpa( config: Config, client: Arc>, - voters: HashMap, + voters: HashMap, network: N, -) -> Result,client::error::Error> where +) -> ::client::error::Result<( + impl Future, + GrandpaBlockImport, +)> where Block::Hash: Ord, B: Backend + 'static, E: CallExecutor + 'static, @@ -614,11 +676,22 @@ pub fn run_grandpa( ))? }; + // TODO: attempt to load from disk. + let authority_set = SharedAuthoritySet::genesis( + voters.iter().map(|(&id, &weight)| (id, weight)).collect(), + ); + + let block_import = GrandpaBlockImport { + inner: client.clone(), + authority_set: authority_set.clone(), + }; + let environment = Arc::new(Environment { inner: client, config, voters, network, + authority_set, }); let voter = voter::Voter::new( @@ -628,7 +701,9 @@ pub fn run_grandpa( last_finalized, ); - Ok(voter.map_err(|e| warn!("GRANDPA Voter failed: {:?}", e))) + let work = voter.map_err(|e| warn!("GRANDPA Voter failed: {:?}", e)); + + Ok((work, block_import)) } #[cfg(test)] From a422a14b527c2c2c1bcf4acfd70b4a379b5943e0 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 25 Oct 2018 18:00:46 +0200 Subject: [PATCH 07/36] use generic block number in GRANDPA --- substrate/Cargo.lock | 3 +- substrate/core/finality-grandpa/src/lib.rs | 64 ++++++++++++++-------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 0bbfc82ad0..df76975816 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -581,10 +581,11 @@ dependencies = [ [[package]] name = "finality-grandpa" version = "0.3.0" -source = "git+https://github.com/paritytech/finality-grandpa#fffce2dc450e72ff8b3f43818f8aceb5cfacc2d7" +source = "git+https://github.com/paritytech/finality-grandpa#f1ad8d7ca020e5db0e51e32cd62e8cd3c578d121" dependencies = [ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-codec-derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 6411fee6ad..faf1611e1a 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -58,7 +58,7 @@ use substrate_primitives::{ed25519, AuthorityId, Blake2Hasher}; use tokio::timer::Interval; use grandpa::Error as GrandpaError; -use grandpa::{voter, round::State as RoundState, Prevote, Precommit, Equivocation}; +use grandpa::{voter, round::State as RoundState, Equivocation, BlockNumberOps}; use std::collections::{VecDeque, HashMap}; use std::sync::Arc; @@ -72,12 +72,21 @@ const LAST_COMPLETED_KEY: &[u8] = b"grandpa_completed_round"; const AUTHORITY_SET_KEY: &[u8] = b"grandpa_voters"; /// round-number, round-state, set indicator -type LastCompleted = (u64, RoundState, u64); +type LastCompleted = (u64, RoundState, u64); /// A GRANDPA message for a substrate chain. -pub type Message = grandpa::Message<::Hash>; +pub type Message = grandpa::Message<::Hash, NumberFor>; /// A signed message. -pub type SignedMessage = grandpa::SignedMessage<::Hash, ed25519::Signature, AuthorityId>; +pub type SignedMessage = grandpa::SignedMessage< + ::Hash, + NumberFor, + ed25519::Signature, + AuthorityId, +>; +/// A prevote message for this chain's block type. +pub type Prevote = grandpa::Prevote<::Hash, NumberFor>; +/// A precommit message for this chain's block type. +pub type Precommit = grandpa::Precommit<::Hash, NumberFor>; /// Configuration for the GRANDPA service. pub struct Config { @@ -134,18 +143,17 @@ pub trait BlockStatus { /// Return `Ok(Some(number))` or `Ok(None)` depending on whether the block /// is definitely known and has been imported. /// If an unexpected error occurs, return that. - fn block_number(&self, hash: Block::Hash) -> Result, Error>; + fn block_number(&self, hash: Block::Hash) -> Result>, Error>; } impl BlockStatus for Arc> where B: Backend, E: CallExecutor, - NumberFor: As, + NumberFor: BlockNumberOps, { - fn block_number(&self, hash: Block::Hash) -> Result, Error> { + fn block_number(&self, hash: Block::Hash) -> Result>, Error> { self.block_number_from_id(&BlockId::Hash(hash)) .map_err(|e| Error::Blockchain(format!("{:?}", e))) - .map(|num| num.map(|n| n.as_())) } } @@ -415,13 +423,13 @@ pub struct Environment { network: N, } -impl grandpa::Chain for Environment where +impl grandpa::Chain> for Environment where Block: 'static, B: Backend + 'static, E: CallExecutor + 'static, N: Network + 'static, N::In: 'static, - NumberFor: As, + NumberFor: BlockNumberOps, DigestItemFor: CompatibleDigestItem>, { fn ancestry(&self, base: Block::Hash, block: Block::Hash) -> Result, GrandpaError> { @@ -450,13 +458,13 @@ impl grandpa::Chain for Environment Option<(Block::Hash, u32)> { + fn best_chain_containing(&self, block: Block::Hash) -> Option<(Block::Hash, NumberFor)> { match self.inner.best_containing(block, None) { Ok(Some(hash)) => { let header = self.inner.header(&BlockId::Hash(hash)).ok()? .expect("Header known to exist after `best_containing` call; qed"); - Some((hash, header.number().as_())) + Some((hash, header.number().clone())) } Ok(None) => None, Err(e) => { @@ -486,20 +494,26 @@ pub trait CompatibleDigestItem { fn scheduled_change(&self) -> Option> { None } } -impl voter::Environment for Environment where +impl voter::Environment> for Environment where Block: 'static, B: Backend + 'static, E: CallExecutor + 'static, N: Network + 'static, N::In: 'static, - NumberFor: As, + NumberFor: BlockNumberOps, DigestItemFor: CompatibleDigestItem>, { type Timer = Box>; type Id = AuthorityId; type Signature = ed25519::Signature; - type In = Box, Error = Self::Error>>; - type Out = Box, SinkError = Self::Error>>; + type In = Box, Self::Signature, Self::Id>, + Error = Self::Error, + >>; + type Out = Box>, + SinkError = Self::Error, + >>; type Error = Error; #[allow(unreachable_code)] @@ -560,7 +574,7 @@ impl voter::Environment for Environment) -> Result<(), Self::Error> { + fn completed(&self, round: u64, state: RoundState>) -> Result<(), Self::Error> { let encoded_state = (round, state).encode(); if let Err(e) = self.inner.backend() .insert_aux(&[(LAST_COMPLETED_KEY, &encoded_state[..])], &[]) @@ -572,7 +586,7 @@ impl voter::Environment for Environment Result<(), Self::Error> { + fn finalize_block(&self, hash: Block::Hash, number: NumberFor) -> Result<(), Self::Error> { // TODO: don't unconditionally notify. if let Err(e) = self.inner.finalize_block(BlockId::Hash(hash), true) { warn!(target: "afg", "Error applying finality to block {:?}: {:?}", (hash, number), e); @@ -586,7 +600,7 @@ impl voter::Environment for Environment, Self::Signature> + equivocation: ::grandpa::Equivocation, Self::Signature> ) { warn!(target: "afg", "Detected prevote equivocation in the finality worker: {:?}", equivocation); // nothing yet; this could craft misbehavior reports of some kind. @@ -595,7 +609,7 @@ impl voter::Environment for Environment, Self::Signature> + equivocation: Equivocation, Self::Signature> ) { warn!(target: "afg", "Detected precommit equivocation in the finality worker: {:?}", equivocation); // nothing yet @@ -658,19 +672,21 @@ pub fn run_grandpa( E: CallExecutor + 'static, N: Network + 'static, N::In: 'static, - NumberFor: As, + NumberFor: BlockNumberOps, DigestItemFor: CompatibleDigestItem>, { + use runtime_primitives::traits::Zero; + let chain_info = client.info()?; let genesis_hash = chain_info.chain.genesis_hash; let last_finalized = ( chain_info.chain.finalized_hash, - chain_info.chain.finalized_number.as_() + chain_info.chain.finalized_number, ); let (last_round_number, last_state) = match client.backend().get_aux(LAST_COMPLETED_KEY)? { - None => (0, RoundState::genesis((genesis_hash, 0))), - Some(raw) => <(u64, RoundState)>::decode(&mut &raw[..]) + None => (0, RoundState::genesis((genesis_hash, >::zero()))), + Some(raw) => <(u64, RoundState>)>::decode(&mut &raw[..]) .ok_or_else(|| ::client::error::ErrorKind::Backend( format!("Last GRANDPA round state kept in invalid format") ))? From 5f6dc6bc6a378d227efc154076aea202aa911077 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Fri, 26 Oct 2018 19:19:12 +0200 Subject: [PATCH 08/36] apply authority set changes --- .../core/finality-grandpa/src/authorities.rs | 87 +++++++++++++++-- substrate/core/finality-grandpa/src/lib.rs | 95 +++++++++++++++---- 2 files changed, 157 insertions(+), 25 deletions(-) diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index 0ed2bcb36c..37712a7615 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -54,12 +54,14 @@ impl SharedAuthoritySet { } } -impl + Ord + Clone> SharedAuthoritySet { +impl + Ord + Clone> SharedAuthoritySet { /// Note an upcoming pending transition. pub(crate) fn add_pending_change(&self, pending: PendingChange) { + // ordered first by effective number and then by signal-block number. let mut inner = self.inner.write(); + let key = (pending.effective_number(), pending.canon_height); let idx = inner.pending_changes - .binary_search_by_key(&pending.effective_number(), |change| change.effective_number()) + .binary_search_by_key(&key, |change| (change.effective_number(), change.canon_height)) .unwrap_or_else(|i| i); inner.pending_changes.insert(idx, pending); @@ -67,7 +69,17 @@ impl + Ord + Clone> SharedAuthoritySet { /// Get the earliest limit-block number, if any. pub(crate) fn current_limit(&self) -> Option { - self.inner.read().pending_changes.get(0).map(|change| change.effective_number().clone()) + self.inner.read().current_limit() + } + + /// Get the current set ID. This is incremented every time the set changes. + pub(crate) fn set_id(&self) -> u64 { + self.inner.read().set_id + } + + /// Execute a closure with the inner set mutably. + pub(crate) fn with_mut(&self, f: F) -> U where F: FnOnce(&mut AuthoritySet) -> U { + f(&mut *self.inner.write()) } } @@ -78,18 +90,81 @@ impl From> for SharedAuthoritySet { } /// A set of authorities. -#[derive(Encode, Decode)] +#[derive(Debug, Clone, Encode, Decode)] pub(crate) struct AuthoritySet { current_authorities: Vec<(AuthorityId, u64)>, set_id: u64, pending_changes: Vec>, } +impl AuthoritySet { + /// Get the earliest limit-block number, if any. + pub(crate) fn current_limit(&self) -> Option { + self.pending_changes.get(0).map(|change| change.effective_number().clone()) + } + + /// Get the set identifier. + pub(crate) fn set_id(&self) -> u64 { + self.set_id + } + + /// Get the current set id and a reference to the current authority set. + pub(crate) fn current(&self) -> (u64, &[(AuthorityId, u64)]) { + (self.set_id, &self.current_authorities[..]) + } +} + +impl AuthoritySet { + /// Apply or prune any pending transitions. Provide a closure that can be used to check for the + /// finalized block with given number. + /// + /// Returns true when the set's representation has changed. + pub(crate) fn apply_changes(&mut self, just_finalized: N, canonical: F) -> Result + where F: FnMut(N) -> Result + { + let mut changed = false; + loop { + let remove_up_to = match self.pending_changes.first() { + None => break, + Some(change) => { + let effective_number = change.effective_number(); + if effective_number > just_finalized { break } + + // check if the block that signalled the change is canonical in + // our chain. + if canonical(change.canon_height)? == change.canon_hash { + // apply this change: make the set canonical + info!(target: "finality", "Applying authority set change scheduled at block #{:?}", + change.canon_height); + + self.current_authorities = change.next_authorities.clone(); + self.set_id += 1; + + // discard any signalled changes + // that happened before or equal to the effective number of the change. + self.pending_changes.iter() + .take_while(|c| c.canon_height <= effective_number) + .count() + } else { + 1 // prune out this entry; it's no longer relevant. + } + } + }; + + let remove_up_to = ::std::cmp::min(remove_up_to, self.pending_changes.len()); + self.pending_changes.drain(..remove_up_to); + changed = true; // always changed because we strip at least the first change. + } + + Ok(changed) + } +} + /// A pending change to the authority set. /// /// This will be applied when the announcing block is at some depth within /// the finalized chain. -#[derive(Encode, Decode)] +#[derive(Debug, Clone, Encode, Decode)] pub(crate) struct PendingChange { /// The new authorities and weights to apply. pub(crate) next_authorities: Vec<(AuthorityId, u64)>, @@ -103,7 +178,7 @@ pub(crate) struct PendingChange { } impl + Clone> PendingChange { - /// Returns the effective number. + /// Returns the effective number this change will be applied at. fn effective_number(&self) -> N { self.canon_height.clone() + self.finalization_depth.clone() } diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index faf1611e1a..2e907bf9fa 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -47,11 +47,11 @@ extern crate parity_codec_derive; use futures::prelude::*; use futures::stream::Fuse; use futures::sync::mpsc; -use client::{Client, ImportNotifications, backend::Backend, CallExecutor}; +use client::{Client, error::Error as ClientError, ImportNotifications, backend::Backend, CallExecutor}; use codec::{Encode, Decode}; use consensus_common::BlockImport; use runtime_primitives::traits::{ - As, NumberFor, Block as BlockT, Header as HeaderT, DigestItemFor, + NumberFor, Block as BlockT, Header as HeaderT, DigestItemFor, }; use runtime_primitives::{generic::BlockId, Justification}; use substrate_primitives::{ed25519, AuthorityId, Blake2Hasher}; @@ -108,7 +108,7 @@ pub enum Error { /// A blockchain error. Blockchain(String), /// Could not complete a round on disk. - CouldNotCompleteRound(::client::error::Error), + CouldNotCompleteRound(ClientError), /// A timer failed to fire. Timer(::tokio::timer::Error), } @@ -421,6 +421,7 @@ pub struct Environment { config: Config, authority_set: SharedAuthoritySet>, network: N, + set_id: u64, } impl grandpa::Chain> for Environment where @@ -459,13 +460,20 @@ impl grandpa::Chain> for E } fn best_chain_containing(&self, block: Block::Hash) -> Option<(Block::Hash, NumberFor)> { - match self.inner.best_containing(block, None) { + // we refuse to vote beyond the current limit number where transitions are scheduled to + // occur. + // once blocks are finalized that make that transition irrelevant or activate it, + // we will proceed onwards. most of the time there will be no pending transition. + let limit = self.authority_set.current_limit(); + match self.inner.best_containing(block, limit) { Ok(Some(hash)) => { let header = self.inner.header(&BlockId::Hash(hash)).ok()? .expect("Header known to exist after `best_containing` call; qed"); Some((hash, header.number().clone())) } + // Ok(None) can be returned when `block` is after `limit`. That might cause issues. + // might be better to return the header itself in this (rare) case. Ok(None) => None, Err(e) => { debug!(target: "afg", "Encountered error finding best chain containing {:?}: {:?}", block, e); @@ -486,7 +494,8 @@ pub struct ScheduledChange { /// A GRANDPA-compatible DigestItem. This can describe when GRANDPA set changes /// are scheduled. -// TODO: with specialization, do a blanket implementation so this trait +// +// With specialization, could do a blanket implementation so this trait // doesn't have to be implemented by users. pub trait CompatibleDigestItem { /// If this digest item notes a GRANDPA set change, return information about @@ -494,6 +503,33 @@ pub trait CompatibleDigestItem { fn scheduled_change(&self) -> Option> { None } } +/// Signals either an early exit of a voter or an error. +#[derive(Debug)] +pub enum ExitOrError { + /// An error occurred. + Error(Error), + /// Early exit of the voter: the new set ID and the new authorities along with respective weights. + AuthoritiesChanged(u64, Vec<(AuthorityId, u64)>), +} + +impl From for ExitOrError { + fn from(e: Error) -> Self { + ExitOrError::Error(e) + } +} + +impl From for ExitOrError { + fn from(e: ClientError) -> Self { + ExitOrError::Error(Error::from(e)) + } +} + +impl From for ExitOrError { + fn from(e: grandpa::Error) -> Self { + ExitOrError::Error(Error::from(e)) + } +} + impl voter::Environment> for Environment where Block: 'static, B: Backend + 'static, @@ -514,7 +550,7 @@ impl voter::Environment> f SinkItem = ::grandpa::Message>, SinkError = Self::Error, >>; - type Error = Error; + type Error = ExitOrError; #[allow(unreachable_code)] fn round_data( @@ -528,24 +564,21 @@ impl voter::Environment> f let prevote_timer = Delay::new(now + self.config.gossip_duration * 2); let precommit_timer = Delay::new(now + self.config.gossip_duration * 4); - // TODO [now]: Get from shared authority set. - let set_id = unimplemented!(); - // TODO: dispatch this with `mpsc::spawn`. let incoming = checked_message_stream::( round, - set_id, + self.set_id, self.network.messages_for(round), self.config.genesis_voters.clone(), ); let (out_rx, outgoing) = outgoing_messages::( round, - set_id, + self.set_id, self.config.local_key.clone(), self.config.genesis_voters.clone(), self.network.clone(), - ); + ).sink_map_err(Into::into); // schedule incoming messages from the network to be held until // corresponding blocks are imported. @@ -556,7 +589,7 @@ impl voter::Environment> f ); // join incoming network messages with locally originating ones. - let incoming = Box::new(incoming.select(out_rx)); + let incoming = Box::new(incoming.select(out_rx).map_err(Into::into)); // schedule network message cleanup when sink drops. let outgoing = Box::new(ClearOnDrop { @@ -580,21 +613,44 @@ impl voter::Environment> f .insert_aux(&[(LAST_COMPLETED_KEY, &encoded_state[..])], &[]) { warn!(target: "afg", "Shutting down voter due to error bookkeeping last completed round in DB: {:?}", e); - Err(Error::CouldNotCompleteRound(e)) + Err(Error::CouldNotCompleteRound(e).into()) } else { Ok(()) } } fn finalize_block(&self, hash: Block::Hash, number: NumberFor) -> Result<(), Self::Error> { - // TODO: don't unconditionally notify. + // ideally some handle to a synchronization oracle would be used + // to avoid unconditionally notifying. if let Err(e) = self.inner.finalize_block(BlockId::Hash(hash), true) { warn!(target: "afg", "Error applying finality to block {:?}: {:?}", (hash, number), e); + + // we return without error because not being able to finalize (temporarily) is + // non-fatal. + return Ok(()); } - // we return without error in all cases because not being able to finalize is - // non-fatal. - Ok(()) + self.authority_set.with_mut(|authority_set| { + let client = &self.inner; + let prior_id = authority_set.set_id(); + let has_changed = authority_set.apply_changes(number, |canon_number| { + client.block_hash_from_id(&BlockId::number(canon_number)) + .map(|h| h.expect("given number always less than newly-finalized number; \ + thus there is a block with that number finalized already; qed")) + })?; + + if has_changed { + // TODO [now]: write to disk. if it fails, exit the node. + } + + let (new_id, set_ref) = authority_set.current(); + if new_id != prior_id { + // the authority set has changed. + return Err(ExitOrError::AuthoritiesChanged(new_id, set_ref.to_vec())); + } + + Ok(()) + }) } fn prevote_equivocation( @@ -692,7 +748,7 @@ pub fn run_grandpa( ))? }; - // TODO: attempt to load from disk. + // TODO [now]: attempt to load from disk. let authority_set = SharedAuthoritySet::genesis( voters.iter().map(|(&id, &weight)| (id, weight)).collect(), ); @@ -707,6 +763,7 @@ pub fn run_grandpa( config, voters, network, + set_id: authority_set.set_id(), authority_set, }); From e33a8dde4ade2950d27ebde30de95ca638c06577 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 27 Oct 2018 15:43:03 +0200 Subject: [PATCH 09/36] compiles --- .../core/finality-grandpa/src/authorities.rs | 31 ++++++++++++------- substrate/core/finality-grandpa/src/lib.rs | 14 ++++----- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index 37712a7615..cbe53ceb3d 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -20,6 +20,7 @@ use parking_lot::RwLock; use substrate_primitives::AuthorityId; use std::cmp::Ord; +use std::fmt::Debug; use std::ops::Add; use std::sync::Arc; @@ -54,14 +55,19 @@ impl SharedAuthoritySet { } } -impl + Ord + Clone> SharedAuthoritySet { +impl SharedAuthoritySet + where N: Add + Ord + Clone + Debug +{ /// Note an upcoming pending transition. pub(crate) fn add_pending_change(&self, pending: PendingChange) { // ordered first by effective number and then by signal-block number. let mut inner = self.inner.write(); - let key = (pending.effective_number(), pending.canon_height); + let key = (pending.effective_number(), pending.canon_height.clone()); let idx = inner.pending_changes - .binary_search_by_key(&key, |change| (change.effective_number(), change.canon_height)) + .binary_search_by_key(&key, |change| ( + change.effective_number(), + change.canon_height.clone(), + )) .unwrap_or_else(|i| i); inner.pending_changes.insert(idx, pending); @@ -98,11 +104,6 @@ pub(crate) struct AuthoritySet { } impl AuthoritySet { - /// Get the earliest limit-block number, if any. - pub(crate) fn current_limit(&self) -> Option { - self.pending_changes.get(0).map(|change| change.effective_number().clone()) - } - /// Get the set identifier. pub(crate) fn set_id(&self) -> u64 { self.set_id @@ -114,12 +115,20 @@ impl AuthoritySet { } } -impl AuthoritySet { +impl AuthoritySet + where N: Add + Ord + Clone + Debug, +{ + /// Get the earliest limit-block number, if any. + pub(crate) fn current_limit(&self) -> Option { + self.pending_changes.get(0).map(|change| change.effective_number().clone()) + } + /// Apply or prune any pending transitions. Provide a closure that can be used to check for the /// finalized block with given number. /// /// Returns true when the set's representation has changed. - pub(crate) fn apply_changes(&mut self, just_finalized: N, canonical: F) -> Result + pub(crate) fn apply_changes(&mut self, just_finalized: N, mut canonical: F) + -> Result where F: FnMut(N) -> Result { let mut changed = false; @@ -132,7 +141,7 @@ impl AuthoritySet { // check if the block that signalled the change is canonical in // our chain. - if canonical(change.canon_height)? == change.canon_hash { + if canonical(change.canon_height.clone())? == change.canon_hash { // apply this change: make the set canonical info!(target: "finality", "Applying authority set change scheduled at block #{:?}", change.canon_height); diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 2e907bf9fa..7c12be4a8a 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -108,7 +108,7 @@ pub enum Error { /// A blockchain error. Blockchain(String), /// Could not complete a round on disk. - CouldNotCompleteRound(ClientError), + Client(ClientError), /// A timer failed to fire. Timer(::tokio::timer::Error), } @@ -520,7 +520,7 @@ impl From for ExitOrError { impl From for ExitOrError { fn from(e: ClientError) -> Self { - ExitOrError::Error(Error::from(e)) + ExitOrError::Error(Error::Client(e)) } } @@ -578,7 +578,7 @@ impl voter::Environment> f self.config.local_key.clone(), self.config.genesis_voters.clone(), self.network.clone(), - ).sink_map_err(Into::into); + ); // schedule incoming messages from the network to be held until // corresponding blocks are imported. @@ -595,12 +595,12 @@ impl voter::Environment> f let outgoing = Box::new(ClearOnDrop { round, network: self.network.clone(), - inner: outgoing, + inner: outgoing.sink_map_err(Into::into), }); voter::RoundData { - prevote_timer: Box::new(prevote_timer.map_err(Error::Timer)), - precommit_timer: Box::new(precommit_timer.map_err(Error::Timer)), + prevote_timer: Box::new(prevote_timer.map_err(|e| Error::Timer(e).into())), + precommit_timer: Box::new(precommit_timer.map_err(|e| Error::Timer(e).into())), voters: self.voters.clone(), incoming, outgoing, @@ -613,7 +613,7 @@ impl voter::Environment> f .insert_aux(&[(LAST_COMPLETED_KEY, &encoded_state[..])], &[]) { warn!(target: "afg", "Shutting down voter due to error bookkeeping last completed round in DB: {:?}", e); - Err(Error::CouldNotCompleteRound(e).into()) + Err(Error::Client(e).into()) } else { Ok(()) } From 25984d8dc8368de2ded95a3633ec35d326b28dad Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 27 Oct 2018 16:18:01 +0200 Subject: [PATCH 10/36] add some tests for authority set logic --- substrate/core/finality-grandpa/Cargo.toml | 2 - .../core/finality-grandpa/src/authorities.rs | 167 ++++++++++++++++-- substrate/core/finality-grandpa/src/lib.rs | 12 +- 3 files changed, 154 insertions(+), 27 deletions(-) diff --git a/substrate/core/finality-grandpa/Cargo.toml b/substrate/core/finality-grandpa/Cargo.toml index 34c0e99b63..afff3ad5cb 100644 --- a/substrate/core/finality-grandpa/Cargo.toml +++ b/substrate/core/finality-grandpa/Cargo.toml @@ -11,7 +11,6 @@ sr-primitives = { path = "../sr-primitives" } substrate-consensus-common = { path = "../consensus/common" } substrate-primitives = { path = "../primitives" } substrate-client = { path = "../client" } -substrate-network = { path = "../network" } log = "0.4" parking_lot = "0.4" tokio = "0.1.7" @@ -23,5 +22,4 @@ features = ["derive-codec"] [dev-dependencies] substrate-network = { path = "../network", features = ["test-helpers"] } -parking_lot = "0.4" substrate-keyring = { path = "../keyring" } diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index cbe53ceb3d..4e435f5420 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -53,6 +53,11 @@ impl SharedAuthoritySet { { f(&*self.inner.read()) } + + /// Execute a closure with the inner set mutably. + pub(crate) fn with_mut(&self, f: F) -> U where F: FnOnce(&mut AuthoritySet) -> U { + f(&mut *self.inner.write()) + } } impl SharedAuthoritySet @@ -60,17 +65,7 @@ impl SharedAuthoritySet { /// Note an upcoming pending transition. pub(crate) fn add_pending_change(&self, pending: PendingChange) { - // ordered first by effective number and then by signal-block number. - let mut inner = self.inner.write(); - let key = (pending.effective_number(), pending.canon_height.clone()); - let idx = inner.pending_changes - .binary_search_by_key(&key, |change| ( - change.effective_number(), - change.canon_height.clone(), - )) - .unwrap_or_else(|i| i); - - inner.pending_changes.insert(idx, pending); + self.inner.write().add_pending_change(pending) } /// Get the earliest limit-block number, if any. @@ -82,11 +77,6 @@ impl SharedAuthoritySet pub(crate) fn set_id(&self) -> u64 { self.inner.read().set_id } - - /// Execute a closure with the inner set mutably. - pub(crate) fn with_mut(&self, f: F) -> U where F: FnOnce(&mut AuthoritySet) -> U { - f(&mut *self.inner.write()) - } } impl From> for SharedAuthoritySet { @@ -118,6 +108,19 @@ impl AuthoritySet { impl AuthoritySet where N: Add + Ord + Clone + Debug, { + /// Note an upcoming pending transition. + pub(crate) fn add_pending_change(&mut self, pending: PendingChange) { + // ordered first by effective number and then by signal-block number. + let key = (pending.effective_number(), pending.canon_height.clone()); + let idx = self.pending_changes + .binary_search_by_key(&key, |change| ( + change.effective_number(), + change.canon_height.clone(), + )) + .unwrap_or_else(|i| i); + + self.pending_changes.insert(idx, pending); + } /// Get the earliest limit-block number, if any. pub(crate) fn current_limit(&self) -> Option { self.pending_changes.get(0).map(|change| change.effective_number().clone()) @@ -173,7 +176,7 @@ impl AuthoritySet /// /// This will be applied when the announcing block is at some depth within /// the finalized chain. -#[derive(Debug, Clone, Encode, Decode)] +#[derive(Debug, Clone, Encode, Decode, PartialEq)] pub(crate) struct PendingChange { /// The new authorities and weights to apply. pub(crate) next_authorities: Vec<(AuthorityId, u64)>, @@ -192,3 +195,133 @@ impl + Clone> PendingChange { self.canon_height.clone() + self.finalization_depth.clone() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn changes_sorted_in_correct_order() { + let mut authorities = AuthoritySet { + current_authorities: Vec::new(), + set_id: 0, + pending_changes: Vec::new(), + }; + + let change_a = PendingChange { + next_authorities: Vec::new(), + finalization_depth: 10, + canon_height: 5, + canon_hash: "hash_a", + }; + + let change_b = PendingChange { + next_authorities: Vec::new(), + finalization_depth: 0, + canon_height: 16, + canon_hash: "hash_b", + }; + + let change_c = PendingChange { + next_authorities: Vec::new(), + finalization_depth: 5, + canon_height: 10, + canon_hash: "hash_c", + }; + + authorities.add_pending_change(change_a.clone()); + authorities.add_pending_change(change_b.clone()); + authorities.add_pending_change(change_c.clone()); + + assert_eq!(authorities.pending_changes, vec![change_a, change_c, change_b]); + } + + #[test] + fn apply_change() { + let mut authorities = AuthoritySet { + current_authorities: Vec::new(), + set_id: 0, + pending_changes: Vec::new(), + }; + + let set_a = vec![([1; 32].into(), 5)]; + let set_b = vec![([2; 32].into(), 5)]; + + let change_a = PendingChange { + next_authorities: set_a.clone(), + finalization_depth: 10, + canon_height: 5, + canon_hash: "hash_a", + }; + + let change_b = PendingChange { + next_authorities: set_b.clone(), + finalization_depth: 10, + canon_height: 5, + canon_hash: "hash_b", + }; + + authorities.add_pending_change(change_a.clone()); + authorities.add_pending_change(change_b.clone()); + + authorities.apply_changes::<_, ()>(10, |_| panic!()).unwrap(); + assert!(authorities.current_authorities.is_empty()); + + authorities.apply_changes::<_, ()>( + 15, + |n| if n == 5 { Ok("hash_a") } else { panic!() } + ).unwrap(); + + assert_eq!(authorities.current_authorities, set_a); + assert_eq!(authorities.set_id, 1); + } + + #[test] + fn apply_many_changes_at_once() { + let mut authorities = AuthoritySet { + current_authorities: Vec::new(), + set_id: 0, + pending_changes: Vec::new(), + }; + + let set_a = vec![([1; 32].into(), 5)]; + let set_b = vec![([2; 32].into(), 5)]; + let set_c = vec![([3; 32].into(), 5)]; + + let change_a = PendingChange { + next_authorities: set_a.clone(), + finalization_depth: 10, + canon_height: 5, + canon_hash: "hash_a", + }; + + // will be ignored because it was signalled when change_a still pending. + let change_b = PendingChange { + next_authorities: set_b.clone(), + finalization_depth: 10, + canon_height: 15, + canon_hash: "hash_b", + }; + + let change_c = PendingChange { + next_authorities: set_c.clone(), + finalization_depth: 10, + canon_height: 16, + canon_hash: "hash_c", + }; + + authorities.add_pending_change(change_a.clone()); + authorities.add_pending_change(change_b.clone()); + authorities.add_pending_change(change_c.clone()); + + authorities.apply_changes(26, |n| match n { + 5 => Ok("hash_a"), + 15 => Ok("hash_b"), + 16 => Ok("hash_c"), + _ => Err(()), + }).unwrap(); + + assert_eq!(authorities.current_authorities, set_c); + assert_eq!(authorities.set_id, 2); // has been bumped only twice + } +} diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 7c12be4a8a..f5379a8113 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -24,7 +24,6 @@ extern crate substrate_client as client; extern crate sr_primitives as runtime_primitives; extern crate substrate_consensus_common as consensus_common; extern crate substrate_primitives; -extern crate substrate_network as network; extern crate tokio; extern crate parking_lot; extern crate parity_codec as codec; @@ -35,9 +34,6 @@ extern crate log; #[cfg(test)] extern crate substrate_network as network; -#[cfg(test)] -extern crate parking_lot; - #[cfg(test)] extern crate substrate_keyring as keyring; @@ -871,10 +867,10 @@ mod tests { .take_while(|n| Ok(n.header.number() < &20)) .for_each(move |_| Ok(())) ); - let voter = run_grandpa( + let (voter, _) = run_grandpa( Config { gossip_duration: TEST_GOSSIP_DURATION, - voters: voters.clone(), + genesis_voters: voters.clone(), local_key: Some(Arc::new(key.clone().into())), }, client, @@ -930,10 +926,10 @@ mod tests { .take_while(|n| Ok(n.header.number() < &20)) .for_each(move |_| Ok(())) ); - let voter = run_grandpa( + let (voter, _) = run_grandpa( Config { gossip_duration: TEST_GOSSIP_DURATION, - voters: voters.keys().cloned().collect(), + genesis_voters: voters.keys().cloned().collect(), local_key, }, client, From 1a3fe5657d52f193d1cc790da79e6c07d983db3f Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 27 Oct 2018 17:32:42 +0200 Subject: [PATCH 11/36] restart voter with new authority set as necessary --- .../core/finality-grandpa/src/authorities.rs | 42 +++-- substrate/core/finality-grandpa/src/lib.rs | 155 ++++++++++++------ 2 files changed, 136 insertions(+), 61 deletions(-) diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index 4e435f5420..dd6a433995 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -85,6 +85,15 @@ impl From> for SharedAuthoritySet { } } +/// Status of the set after changes were applied. +pub(crate) struct Status { + /// Whether internal changes were made. + pub(crate) changed: bool, + /// `Some` when underlying authority set has changed, containign the + /// block where that set changed. + pub(crate) new_set_block: Option<(H, N)>, +} + /// A set of authorities. #[derive(Debug, Clone, Encode, Decode)] pub(crate) struct AuthoritySet { @@ -129,12 +138,16 @@ impl AuthoritySet /// Apply or prune any pending transitions. Provide a closure that can be used to check for the /// finalized block with given number. /// - /// Returns true when the set's representation has changed. + /// When the set has changed, the return value will be `Ok(Some((H, N)))` which is the cnaonical + /// block where the set last changed. pub(crate) fn apply_changes(&mut self, just_finalized: N, mut canonical: F) - -> Result + -> Result, E> where F: FnMut(N) -> Result { - let mut changed = false; + let mut status = Status { + changed: false, + new_set_block: None, + }; loop { let remove_up_to = match self.pending_changes.first() { None => break, @@ -152,6 +165,11 @@ impl AuthoritySet self.current_authorities = change.next_authorities.clone(); self.set_id += 1; + status.new_set_block = Some(( + canonical(effective_number.clone())?, + effective_number.clone(), + )); + // discard any signalled changes // that happened before or equal to the effective number of the change. self.pending_changes.iter() @@ -165,10 +183,10 @@ impl AuthoritySet let remove_up_to = ::std::cmp::min(remove_up_to, self.pending_changes.len()); self.pending_changes.drain(..remove_up_to); - changed = true; // always changed because we strip at least the first change. + status.changed = true; // always changed because we strip at least the first change. } - Ok(changed) + Ok(status) } } @@ -264,16 +282,18 @@ mod tests { authorities.add_pending_change(change_a.clone()); authorities.add_pending_change(change_b.clone()); - authorities.apply_changes::<_, ()>(10, |_| panic!()).unwrap(); + authorities.apply_changes(10, |_| Err(())).unwrap(); assert!(authorities.current_authorities.is_empty()); - authorities.apply_changes::<_, ()>( - 15, - |n| if n == 5 { Ok("hash_a") } else { panic!() } - ).unwrap(); + authorities.apply_changes(15, |n| match n { + 5 => Ok("hash_a"), + 15 => Ok("hash_15_canon"), + _ => Err(()), + }).unwrap(); assert_eq!(authorities.current_authorities, set_a); assert_eq!(authorities.set_id, 1); + assert!(authorities.pending_changes.is_empty()); } #[test] @@ -318,10 +338,12 @@ mod tests { 5 => Ok("hash_a"), 15 => Ok("hash_b"), 16 => Ok("hash_c"), + 26 => Ok("hash_26"), _ => Err(()), }).unwrap(); assert_eq!(authorities.current_authorities, set_c); assert_eq!(authorities.set_id, 2); // has been bumped only twice + assert!(authorities.pending_changes.is_empty()); } } diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index f5379a8113..8b45ab5641 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -67,8 +67,8 @@ mod authorities; const LAST_COMPLETED_KEY: &[u8] = b"grandpa_completed_round"; const AUTHORITY_SET_KEY: &[u8] = b"grandpa_voters"; -/// round-number, round-state, set indicator -type LastCompleted = (u64, RoundState, u64); +/// round-number, round-state +type LastCompleted = (u64, RoundState); /// A GRANDPA message for a substrate chain. pub type Message = grandpa::Message<::Hash, NumberFor>; @@ -85,11 +85,10 @@ pub type Prevote = grandpa::Prevote<::Hash, NumberFor = grandpa::Precommit<::Hash, NumberFor>; /// Configuration for the GRANDPA service. +#[derive(Clone)] pub struct Config { /// The expected duration for a message to be gossiped across the network. pub gossip_duration: Duration, - /// The voters. - pub genesis_voters: Vec, /// The local signing key. pub local_key: Option>, } @@ -331,7 +330,7 @@ fn checked_message_stream( round: u64, set_id: u64, inner: S, - voters: Vec, + voters: Arc>, ) -> impl Stream,Error=Error> where S: Stream,Error=()> @@ -346,7 +345,7 @@ fn checked_message_stream( }) .and_then(move |msg| { // check signature. - if !voters.contains(&msg.id) { + if !voters.contains_key(&msg.id) { debug!(target: "afg", "Skipping message from unknown voter {}", msg.id); return Ok(None); } @@ -368,7 +367,7 @@ fn outgoing_messages( round: u64, set_id: u64, local_key: Option>, - voters: Vec, + voters: Arc>, network: N, ) -> ( impl Stream,Error=Error>, @@ -376,7 +375,12 @@ fn outgoing_messages( ) { let locals = local_key.and_then(|pair| { let public = pair.public(); - voters.iter().find(|id| id.0 == public.0).map(move |id| (pair, id.clone())) + let id = AuthorityId(public.0); + if voters.contains_key(&id) { + Some((pair, id)) + } else { + None + } }); let (tx, rx) = mpsc::unbounded(); @@ -413,7 +417,7 @@ fn outgoing_messages( /// The environment we run GRANDPA in. pub struct Environment { inner: Arc>, - voters: HashMap, + voters: Arc>, config: Config, authority_set: SharedAuthoritySet>, network: N, @@ -499,28 +503,37 @@ pub trait CompatibleDigestItem { fn scheduled_change(&self) -> Option> { None } } +/// A new authority set along with the canonical block it changed at. +#[derive(Debug)] +pub struct NewAuthoritySet { + canon_number: N, + canon_hash: H, + set_id: u64, + authorities: Vec<(AuthorityId, u64)>, +} + /// Signals either an early exit of a voter or an error. #[derive(Debug)] -pub enum ExitOrError { +pub enum ExitOrError { /// An error occurred. Error(Error), /// Early exit of the voter: the new set ID and the new authorities along with respective weights. - AuthoritiesChanged(u64, Vec<(AuthorityId, u64)>), + AuthoritiesChanged(NewAuthoritySet), } -impl From for ExitOrError { +impl From for ExitOrError { fn from(e: Error) -> Self { ExitOrError::Error(e) } } -impl From for ExitOrError { +impl From for ExitOrError { fn from(e: ClientError) -> Self { ExitOrError::Error(Error::Client(e)) } } -impl From for ExitOrError { +impl From for ExitOrError { fn from(e: grandpa::Error) -> Self { ExitOrError::Error(Error::from(e)) } @@ -546,7 +559,7 @@ impl voter::Environment> f SinkItem = ::grandpa::Message>, SinkError = Self::Error, >>; - type Error = ExitOrError; + type Error = ExitOrError>; #[allow(unreachable_code)] fn round_data( @@ -565,14 +578,14 @@ impl voter::Environment> f round, self.set_id, self.network.messages_for(round), - self.config.genesis_voters.clone(), + self.voters.clone(), ); let (out_rx, outgoing) = outgoing_messages::( round, self.set_id, self.config.local_key.clone(), - self.config.genesis_voters.clone(), + self.voters.clone(), self.network.clone(), ); @@ -597,7 +610,7 @@ impl voter::Environment> f voter::RoundData { prevote_timer: Box::new(prevote_timer.map_err(|e| Error::Timer(e).into())), precommit_timer: Box::new(precommit_timer.map_err(|e| Error::Timer(e).into())), - voters: self.voters.clone(), + voters: (&*self.voters).clone(), incoming, outgoing, } @@ -628,21 +641,30 @@ impl voter::Environment> f self.authority_set.with_mut(|authority_set| { let client = &self.inner; - let prior_id = authority_set.set_id(); - let has_changed = authority_set.apply_changes(number, |canon_number| { + let status = authority_set.apply_changes(number, |canon_number| { client.block_hash_from_id(&BlockId::number(canon_number)) .map(|h| h.expect("given number always less than newly-finalized number; \ thus there is a block with that number finalized already; qed")) })?; - if has_changed { + if status.changed { // TODO [now]: write to disk. if it fails, exit the node. + // write `authorities.encode()` + + if let Some((ref canon_hash, ref canon_number)) = status.new_set_block { + // write `LastFinalized` entry with `RoundState::genesis(canon)`. + } } - let (new_id, set_ref) = authority_set.current(); - if new_id != prior_id { + if let Some((canon_hash, canon_number)) = status.new_set_block { // the authority set has changed. - return Err(ExitOrError::AuthoritiesChanged(new_id, set_ref.to_vec())); + let (new_id, set_ref) = authority_set.current(); + return Err(ExitOrError::AuthoritiesChanged(NewAuthoritySet { + canon_hash, + canon_number, + set_id: new_id, + authorities: set_ref.to_vec(), + })); } Ok(()) @@ -727,22 +749,12 @@ pub fn run_grandpa( NumberFor: BlockNumberOps, DigestItemFor: CompatibleDigestItem>, { + use futures::future::{self, Loop as FutureLoop}; + use runtime_primitives::traits::Zero; let chain_info = client.info()?; let genesis_hash = chain_info.chain.genesis_hash; - let last_finalized = ( - chain_info.chain.finalized_hash, - chain_info.chain.finalized_number, - ); - - let (last_round_number, last_state) = match client.backend().get_aux(LAST_COMPLETED_KEY)? { - None => (0, RoundState::genesis((genesis_hash, >::zero()))), - Some(raw) => <(u64, RoundState>)>::decode(&mut &raw[..]) - .ok_or_else(|| ::client::error::ErrorKind::Backend( - format!("Last GRANDPA round state kept in invalid format") - ))? - }; // TODO [now]: attempt to load from disk. let authority_set = SharedAuthoritySet::genesis( @@ -754,23 +766,66 @@ pub fn run_grandpa( authority_set: authority_set.clone(), }; - let environment = Arc::new(Environment { - inner: client, - config, - voters, - network, + let (last_round_number, last_state) = match client.backend().get_aux(LAST_COMPLETED_KEY)? { + None => (0, RoundState::genesis((genesis_hash, >::zero()))), + Some(raw) => LastCompleted::decode(&mut &raw[..]) + .ok_or_else(|| ::client::error::ErrorKind::Backend( + format!("Last GRANDPA round state kept in invalid format") + ))? + }; + + let initial_environment = Arc::new(Environment { + inner: client.clone(), + config: config.clone(), + voters: Arc::new(voters), + network: network.clone(), set_id: authority_set.set_id(), - authority_set, + authority_set: authority_set.clone(), }); - let voter = voter::Voter::new( - environment, - last_round_number, - last_state, - last_finalized, - ); + let voters = future::loop_fn((initial_environment, last_round_number, last_state), move |params| { + let (env, last_round_number, last_state) = params; + let chain_info = match client.info() { + Ok(i) => i, + Err(e) => return future::Either::B(future::err(Error::Client(e))), + }; - let work = voter.map_err(|e| warn!("GRANDPA Voter failed: {:?}", e)); + let last_finalized = ( + chain_info.chain.finalized_hash, + chain_info.chain.finalized_number, + ); + + let voter = voter::Voter::new(env, last_round_number, last_state, last_finalized); + let client = client.clone(); + let config = config.clone(); + let network = network.clone(); + let authority_set = authority_set.clone(); + future::Either::A(voter.then(move |res| match res { + // voters don't conclude naturally; this could reasonably be an error. + Ok(()) => Ok(FutureLoop::Break(())), + Err(ExitOrError::Error(e)) => Err(e), + Err(ExitOrError::AuthoritiesChanged(new)) => { + let env = Arc::new(Environment { + inner: client, + config, + voters: Arc::new(new.authorities.into_iter().collect()), + set_id: new.set_id, + network, + authority_set, + }); + + // start the new authority set using the block where the + // set changed (not where the signal happened!) as the base. + Ok(FutureLoop::Continue(( + env, + 0, // always start at round 0 when changing sets. + RoundState::genesis((new.canon_hash, new.canon_number)), + ))) + } + })) + }); + + let work = voters.map_err(|e| warn!("GRANDPA Voter failed: {:?}", e)); Ok((work, block_import)) } @@ -870,7 +925,6 @@ mod tests { let (voter, _) = run_grandpa( Config { gossip_duration: TEST_GOSSIP_DURATION, - genesis_voters: voters.clone(), local_key: Some(Arc::new(key.clone().into())), }, client, @@ -929,7 +983,6 @@ mod tests { let (voter, _) = run_grandpa( Config { gossip_duration: TEST_GOSSIP_DURATION, - genesis_voters: voters.keys().cloned().collect(), local_key, }, client, From 66dafbcb0b71b3a126d88fd3a1f1ae47acccee59 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 28 Oct 2018 18:45:42 +0100 Subject: [PATCH 12/36] minimize public interface --- substrate/core/finality-grandpa/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 988a13d74d..fde673eb5a 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -155,7 +155,7 @@ impl BlockStatus for Arc> where } /// Buffering imported messages until blocks with given hashes are imported. -pub struct UntilImported { +struct UntilImported { import_notifications: Fuse>, status_check: Status, inner: Fuse, @@ -417,7 +417,7 @@ fn outgoing_messages( } /// The environment we run GRANDPA in. -pub struct Environment { +struct Environment { inner: Arc>, voters: Arc>, config: Config, @@ -507,7 +507,7 @@ pub trait CompatibleDigestItem { /// A new authority set along with the canonical block it changed at. #[derive(Debug)] -pub struct NewAuthoritySet { +struct NewAuthoritySet { canon_number: N, canon_hash: H, set_id: u64, @@ -516,7 +516,7 @@ pub struct NewAuthoritySet { /// Signals either an early exit of a voter or an error. #[derive(Debug)] -pub enum ExitOrError { +enum ExitOrError { /// An error occurred. Error(Error), /// Early exit of the voter: the new set ID and the new authorities along with respective weights. From 442e300163fbc472d4642b3a29272c27bc0a8128 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 28 Oct 2018 20:36:59 +0100 Subject: [PATCH 13/36] add substrate finality grandpa primitives for WASM --- substrate/Cargo.lock | 12 +++ substrate/Cargo.toml | 1 + substrate/core/finality-grandpa/Cargo.toml | 1 + .../finality-grandpa/primitives/Cargo.toml | 21 ++++++ .../finality-grandpa/primitives/src/lib.rs | 73 +++++++++++++++++++ substrate/core/finality-grandpa/src/lib.rs | 25 +------ substrate/core/test-runtime/src/lib.rs | 1 - 7 files changed, 111 insertions(+), 23 deletions(-) create mode 100644 substrate/core/finality-grandpa/primitives/Cargo.toml create mode 100644 substrate/core/finality-grandpa/primitives/src/lib.rs diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index ab11df3d8a..c988464c3d 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -3019,6 +3019,17 @@ dependencies = [ "wasmi 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "substrate-fg-primitives" +version = "0.1.0" +dependencies = [ + "parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-codec-derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-api 0.1.0", + "sr-primitives 0.1.0", + "substrate-primitives 0.1.0", +] + [[package]] name = "substrate-finality-grandpa" version = "0.1.0" @@ -3032,6 +3043,7 @@ dependencies = [ "sr-primitives 0.1.0", "substrate-client 0.1.0", "substrate-consensus-common 0.1.0", + "substrate-fg-primitives 0.1.0", "substrate-keyring 0.1.0", "substrate-network 0.1.0", "substrate-primitives 0.1.0", diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml index 54ae7d9d82..899c9cedfb 100644 --- a/substrate/Cargo.toml +++ b/substrate/Cargo.toml @@ -27,6 +27,7 @@ members = [ "core/consensus/rhd", "core/executor", "core/finality-grandpa", + "core/finality-grandpa/primitives", "core/keyring", "core/network", "core/primitives", diff --git a/substrate/core/finality-grandpa/Cargo.toml b/substrate/core/finality-grandpa/Cargo.toml index afff3ad5cb..2181d2f16d 100644 --- a/substrate/core/finality-grandpa/Cargo.toml +++ b/substrate/core/finality-grandpa/Cargo.toml @@ -14,6 +14,7 @@ substrate-client = { path = "../client" } log = "0.4" parking_lot = "0.4" tokio = "0.1.7" +substrate-fg-primitives = { path = "primitives" } [dependencies.finality-grandpa] #version = "0.3.0" diff --git a/substrate/core/finality-grandpa/primitives/Cargo.toml b/substrate/core/finality-grandpa/primitives/Cargo.toml new file mode 100644 index 0000000000..b568da0c06 --- /dev/null +++ b/substrate/core/finality-grandpa/primitives/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "substrate-fg-primitives" +version = "0.1.0" +authors = ["Parity Technologies "] + +[dependencies] +substrate-primitives = { path = "../../primitives", default-features = false } +parity-codec = { version = "2.1", default-features = false } +parity-codec-derive = { version = "2.1", default-features = false } +sr-api = { path = "../../sr-api", default-features = false } +sr-primitives = { path = "../../sr-primitives", default-features = false } + +[features] +default = ["std"] +std = [ + "substrate-primitives/std", + "parity-codec/std", + "parity-codec-derive/std", + "sr-api/std", + "sr-primitives/std", +] diff --git a/substrate/core/finality-grandpa/primitives/src/lib.rs b/substrate/core/finality-grandpa/primitives/src/lib.rs new file mode 100644 index 0000000000..047c37632a --- /dev/null +++ b/substrate/core/finality-grandpa/primitives/src/lib.rs @@ -0,0 +1,73 @@ +// Copyright 2018 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 . + +//! Primitives for GRANDPA integration, suitable for WASM compilation. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate substrate_primitives; +extern crate sr_primitives; +extern crate parity_codec; + +#[macro_use] +extern crate parity_codec_derive; + +#[macro_use] +extern crate sr_api; + +use substrate_primitives::AuthorityId; +use sr_primitives::traits::{Block as BlockT, DigestFor, NumberFor}; + +/// A scheduled change of authority set. +#[cfg_attr(feature = "std", derive(Debug, PartialEq))] +#[derive(Encode, Decode)] +pub struct ScheduledChange { + /// The new authorities after the change, along with their respective weights. + pub next_authorities: Vec<(AuthorityId, u64)>, + /// The number of blocks to delay. + pub delay: N, +} + +/// WASM function call to check for pending changes. +pub const PENDING_CHANGE_CALL: &str = "grandpa_pending_change"; +/// WASM function call to get current GRANDPA authorities. +pub const AUTHORITIES_CALL: &str = "grandpa_pending_change"; + +decl_apis! { + /// APIs for integrating the GRANDPA finality gadget into runtimes. + /// + /// This is primarily used for negotiating authority-set changes for the + /// gadget. GRANDPA uses a signalling model of changing authority sets: + /// changes should be signalled with a delay of N blocks, and then automatically + /// applied in the runtime after those N blocks have passed. + /// + /// The consensus protocol will coordinate the handoff externally. + pub trait Api { + /// Check a digest for pending changes. + /// Return `None` if there are no pending changes. + /// + /// Precedence towards earlier or later digest items can be given + /// based on the rules of the chain. + /// + /// No change should be scheduled if one is already and the delay has not + /// passed completely. + fn grandpa_pending_change(digest: DigestFor) -> Option>>; + + /// Get the current GRANDPA authorities. This should not change except + /// for when changes are scheduled and the corresponding delay has passed. + fn grandpa_authorities() -> Vec; + } +} diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index fde673eb5a..0b8405a6fe 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -14,11 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . -// tag::description[] //! Integration of the GRANDPA finality gadget into substrate. //! //! This is a long-running future that produces finality notifications. -// end::description[] extern crate finality_grandpa as grandpa; extern crate futures; @@ -29,6 +27,7 @@ extern crate substrate_primitives; extern crate tokio; extern crate parking_lot; extern crate parity_codec as codec; +extern crate substrate_fg_primitives as fg_primitives; #[macro_use] extern crate log; @@ -64,6 +63,8 @@ use std::time::{Instant, Duration}; use authorities::SharedAuthoritySet; +pub use fg_primitives::ScheduledChange; + mod authorities; const LAST_COMPLETED_KEY: &[u8] = b"grandpa_completed_round"; @@ -485,26 +486,6 @@ impl grandpa::Chain> for E } } -/// A scheduled change of authority set. -#[derive(Debug, PartialEq)] -pub struct ScheduledChange { - /// The new authorities after the change, along with their respective weights. - pub next_authorities: Vec<(AuthorityId, u64)>, - /// The number of blocks to delay. - pub delay: N, -} - -/// A GRANDPA-compatible DigestItem. This can describe when GRANDPA set changes -/// are scheduled. -// -// With specialization, could do a blanket implementation so this trait -// doesn't have to be implemented by users. -pub trait CompatibleDigestItem { - /// If this digest item notes a GRANDPA set change, return information about - /// the scheduled change. - fn scheduled_change(&self) -> Option> { None } -} - /// A new authority set along with the canonical block it changed at. #[derive(Debug)] struct NewAuthoritySet { diff --git a/substrate/core/test-runtime/src/lib.rs b/substrate/core/test-runtime/src/lib.rs index de678e7205..a136f02cf0 100644 --- a/substrate/core/test-runtime/src/lib.rs +++ b/substrate/core/test-runtime/src/lib.rs @@ -38,7 +38,6 @@ extern crate sr_io as runtime_io; #[macro_use] extern crate sr_version as runtime_version; - #[cfg(test)] #[macro_use] extern crate hex_literal; From 55ca13ab0925fee4a65eff339f3093fd373960c6 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 28 Oct 2018 20:50:31 +0100 Subject: [PATCH 14/36] call into chain WASM to check for changes --- substrate/core/finality-grandpa/src/lib.rs | 27 ++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 0b8405a6fe..dd5f97f264 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -48,7 +48,7 @@ use client::{Client, error::Error as ClientError, ImportNotifications, backend:: use codec::{Encode, Decode}; use consensus_common::{BlockImport, ImportBlock, ImportResult}; use runtime_primitives::traits::{ - NumberFor, Block as BlockT, Header as HeaderT, DigestItemFor, + NumberFor, Block as BlockT, Header as HeaderT, DigestFor, }; use runtime_primitives::generic::BlockId; use substrate_primitives::{ed25519, AuthorityId, Blake2Hasher}; @@ -434,7 +434,6 @@ impl grandpa::Chain> for E N: Network + 'static, N::In: 'static, NumberFor: BlockNumberOps, - DigestItemFor: CompatibleDigestItem>, { fn ancestry(&self, base: Block::Hash, block: Block::Hash) -> Result, GrandpaError> { let tree_route_res = ::client::blockchain::tree_route( @@ -529,7 +528,6 @@ impl voter::Environment> f N: Network + 'static, N::In: 'static, NumberFor: BlockNumberOps, - DigestItemFor: CompatibleDigestItem>, { type Timer = Box>; type Id = AuthorityId; @@ -686,7 +684,7 @@ pub struct GrandpaBlockImport { impl BlockImport for GrandpaBlockImport where B: Backend + 'static, E: CallExecutor + 'static + Clone, - DigestItemFor: CompatibleDigestItem>, + DigestFor: Encode, { type Error = ClientError; @@ -696,13 +694,20 @@ impl BlockImport for GrandpaBlockImport use runtime_primitives::traits::Digest; use authorities::PendingChange; - let maybe_event = block.header.digest().logs().iter() - .filter_map(|log| log.scheduled_change()) - .next() - .map(|change| (block.header.hash(), *block.header.number(), change)); + let maybe_change: Option>> = self.inner.call_api_at( + &BlockId::hash(block.header.parent_hash().clone()), + ::fg_primitives::PENDING_CHANGE_CALL, + block.header.digest() + )?; + + let maybe_change = maybe_change.map(|change| ( + block.header.hash(), + block.header.number().clone(), + change + )); let result = self.inner.import_block(block, new_authorities); - if let (true, Some((hash, number, change))) = (result.is_ok(), maybe_event) { + if let (true, Some((hash, number, change))) = (result.is_ok(), maybe_change) { self.authority_set.add_pending_change(PendingChange { next_authorities: change.next_authorities, finalization_depth: number + change.delay, @@ -734,7 +739,7 @@ pub fn run_grandpa( N: Network + 'static, N::In: 'static, NumberFor: BlockNumberOps, - DigestItemFor: CompatibleDigestItem>, + DigestFor: Encode, { use futures::future::{self, Loop as FutureLoop}; @@ -826,8 +831,6 @@ mod tests { use keyring::Keyring; use client::BlockchainEvents; - impl CompatibleDigestItem> for DigestItemFor { } - #[derive(Clone)] struct TestGrandpaNetwork { inner: Arc>, From 3317fee0a176663508473ef64cf7dadcc56349f1 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 29 Oct 2018 02:05:47 +0100 Subject: [PATCH 15/36] write authority set to DB --- .../core/finality-grandpa/src/authorities.rs | 13 +-- substrate/core/finality-grandpa/src/lib.rs | 101 ++++++++++++------ 2 files changed, 69 insertions(+), 45 deletions(-) diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index dd6a433995..7896c08495 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -47,16 +47,9 @@ impl SharedAuthoritySet { } } - /// Execute some work using the inner authority set. - pub(crate) fn with(&self, f: F) -> U - where F: FnOnce(&AuthoritySet) -> U - { - f(&*self.inner.read()) - } - - /// Execute a closure with the inner set mutably. - pub(crate) fn with_mut(&self, f: F) -> U where F: FnOnce(&mut AuthoritySet) -> U { - f(&mut *self.inner.write()) + /// Acquire a reference to the inner read-write lock. + pub(crate) fn inner(&self) -> &RwLock> { + &*self.inner } } diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index dd5f97f264..58de2553b3 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -620,36 +620,57 @@ impl voter::Environment> f return Ok(()); } - self.authority_set.with_mut(|authority_set| { - let client = &self.inner; - let status = authority_set.apply_changes(number, |canon_number| { - client.block_hash_from_id(&BlockId::number(canon_number)) - .map(|h| h.expect("given number always less than newly-finalized number; \ - thus there is a block with that number finalized already; qed")) - })?; + let mut authority_set = self.authority_set.inner().write(); + let client = &self.inner; + let status = authority_set.apply_changes(number, |canon_number| { + client.block_hash_from_id(&BlockId::number(canon_number)) + .map(|h| h.expect("given number always less than newly-finalized number; \ + thus there is a block with that number finalized already; qed")) + })?; - if status.changed { - // TODO [now]: write to disk. if it fails, exit the node. - // write `authorities.encode()` + if status.changed { + // write new authority set state to disk. + let encoded_set = authority_set.encode(); - if let Some((ref canon_hash, ref canon_number)) = status.new_set_block { - // write `LastFinalized` entry with `RoundState::genesis(canon)`. - } - } - - if let Some((canon_hash, canon_number)) = status.new_set_block { - // the authority set has changed. - let (new_id, set_ref) = authority_set.current(); - return Err(ExitOrError::AuthoritiesChanged(NewAuthoritySet { - canon_hash, - canon_number, - set_id: new_id, - authorities: set_ref.to_vec(), - })); + let write_result = if let Some((ref canon_hash, ref canon_number)) = status.new_set_block { + // we also overwrite the "last completed round" entry with a blank slate + // because from the perspective of the finality gadget, the chain has + // reset. + let round_state = RoundState::genesis((*canon_hash, *canon_number)); + let last_completed: LastCompleted<_, _> = (0, round_state); + let encoded = last_completed.encode(); + + client.backend().insert_aux( + &[ + (AUTHORITY_SET_KEY, &encoded_set[..]), + (LAST_COMPLETED_KEY, &encoded[..]), + ], + &[] + ) + } else { + client.backend().insert_aux(&[(AUTHORITY_SET_KEY, &encoded_set[..])], &[]) + }; + + if let Err(e) = write_result { + warn!(target: "finality", "Failed to write updated authority set to disk. Bailing."); + warn!(target: "finality", "Node is in a potentially inconsistent state."); + + return Err(e.into()); } + } + if let Some((canon_hash, canon_number)) = status.new_set_block { + // the authority set has changed. + let (new_id, set_ref) = authority_set.current(); + Err(ExitOrError::AuthoritiesChanged(NewAuthoritySet { + canon_hash, + canon_number, + set_id: new_id, + authorities: set_ref.to_vec(), + })) + } else { Ok(()) - }) + } } fn prevote_equivocation( @@ -688,7 +709,7 @@ impl BlockImport for GrandpaBlockImport { type Error = ClientError; - fn import_block(&self, block: ImportBlock, new_authorities: Option>) + fn import_block(&self, mut block: ImportBlock, new_authorities: Option>) -> Result { use runtime_primitives::traits::Digest; @@ -700,22 +721,32 @@ impl BlockImport for GrandpaBlockImport block.header.digest() )?; - let maybe_change = maybe_change.map(|change| ( - block.header.hash(), - block.header.number().clone(), - change - )); + // when we update the authorities, we need to hold the lock + // until the block is written to prevent a race if we need to restore + // the old authority set on error. + let just_in_case = maybe_change.map(|change| { + let hash = block.header.hash(); + let number = block.header.number().clone(); - let result = self.inner.import_block(block, new_authorities); - if let (true, Some((hash, number, change))) = (result.is_ok(), maybe_change) { - self.authority_set.add_pending_change(PendingChange { + let mut authorities = self.authority_set.inner().write(); + let old_set = authorities.clone(); + authorities.add_pending_change(PendingChange { next_authorities: change.next_authorities, finalization_depth: number + change.delay, canon_height: number, canon_hash: hash, }); - // TODO [now]: write to DB, and what to do on failure? + block.auxiliary.push((AUTHORITY_SET_KEY.to_vec(), Some(authorities.encode()))); + (old_set, authorities) + }); + + let result = self.inner.import_block(block, new_authorities); + if let Err(ref e) = result { + if let Some((old_set, mut authorities)) = just_in_case { + debug!(target: "afg", "Restoring old set after block import error: {:?}", e); + *authorities = old_set; + } } result From e2ee6a2b90a80e9197bdf4283075c2ab5be85d87 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 29 Oct 2018 02:31:51 +0100 Subject: [PATCH 16/36] read authority set from DB on startup --- .../finality-grandpa/primitives/src/lib.rs | 6 ++-- .../core/finality-grandpa/src/authorities.rs | 10 ------- substrate/core/finality-grandpa/src/lib.rs | 28 +++++++++++++++---- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/substrate/core/finality-grandpa/primitives/src/lib.rs b/substrate/core/finality-grandpa/primitives/src/lib.rs index 047c37632a..ddf924cc2e 100644 --- a/substrate/core/finality-grandpa/primitives/src/lib.rs +++ b/substrate/core/finality-grandpa/primitives/src/lib.rs @@ -44,7 +44,7 @@ pub struct ScheduledChange { /// WASM function call to check for pending changes. pub const PENDING_CHANGE_CALL: &str = "grandpa_pending_change"; /// WASM function call to get current GRANDPA authorities. -pub const AUTHORITIES_CALL: &str = "grandpa_pending_change"; +pub const AUTHORITIES_CALL: &str = "grandpa_authorities"; decl_apis! { /// APIs for integrating the GRANDPA finality gadget into runtimes. @@ -66,8 +66,8 @@ decl_apis! { /// passed completely. fn grandpa_pending_change(digest: DigestFor) -> Option>>; - /// Get the current GRANDPA authorities. This should not change except + /// Get the current GRANDPA authorities and weights. This should not change except /// for when changes are scheduled and the corresponding delay has passed. - fn grandpa_authorities() -> Vec; + fn grandpa_authorities() -> Vec<(AuthorityId, u64)>; } } diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index 7896c08495..6a32e3ddb7 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -56,11 +56,6 @@ impl SharedAuthoritySet { impl SharedAuthoritySet where N: Add + Ord + Clone + Debug { - /// Note an upcoming pending transition. - pub(crate) fn add_pending_change(&self, pending: PendingChange) { - self.inner.write().add_pending_change(pending) - } - /// Get the earliest limit-block number, if any. pub(crate) fn current_limit(&self) -> Option { self.inner.read().current_limit() @@ -96,11 +91,6 @@ pub(crate) struct AuthoritySet { } impl AuthoritySet { - /// Get the set identifier. - pub(crate) fn set_id(&self) -> u64 { - self.set_id - } - /// Get the current set id and a reference to the current authority set. pub(crate) fn current(&self) -> (u64, &[(AuthorityId, u64)]) { (self.set_id, &self.current_authorities[..]) diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 58de2553b3..2291c0c823 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -712,7 +712,6 @@ impl BlockImport for GrandpaBlockImport fn import_block(&self, mut block: ImportBlock, new_authorities: Option>) -> Result { - use runtime_primitives::traits::Digest; use authorities::PendingChange; let maybe_change: Option>> = self.inner.call_api_at( @@ -779,10 +778,29 @@ pub fn run_grandpa( let chain_info = client.info()?; let genesis_hash = chain_info.chain.genesis_hash; - // TODO [now]: attempt to load from disk. - let authority_set = SharedAuthoritySet::genesis( - voters.iter().map(|(&id, &weight)| (id, weight)).collect(), - ); + let authority_set = match client.backend().get_aux(AUTHORITY_SET_KEY)? { + None => { + info!(target: "afg", "Loading GRANDPA authorities \ + from genesis on what appears to be first startup."); + + // no authority set on disk: fetch authorities from genesis state. + // if genesis state is not available, we may be a light client, but these + // are unsupported for following GRANDPA directly. + let genesis_authorities: Vec<(AuthorityId, u64)> = client + .call_api_at(&BlockId::Number(NumberFor::::zero()), fg_primitives::AUTHORITIES_CALL, &())?; + + let authority_set = SharedAuthoritySet::genesis(genesis_authorities); + let encoded = authority_set.inner().read().encode(); + client.backend().insert_aux(&[(AUTHORITY_SET_KEY, &encoded[..])], &[])?; + + authority_set + } + Some(raw) => ::authorities::AuthoritySet::decode(&mut &raw[..]) + .ok_or_else(|| ::client::error::ErrorKind::Backend( + format!("GRANDPA authority set kept in invalid format") + ))? + .into(), + }; let block_import = GrandpaBlockImport { inner: client.clone(), From 0302a1fc5e511cf4027c9f417f796076651f7c84 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 29 Oct 2018 15:56:10 +0100 Subject: [PATCH 17/36] refactor import queue to use explicit block import references --- substrate/core/network/src/import_queue.rs | 61 +++++++++++++--------- substrate/core/network/src/service.rs | 2 - substrate/core/network/src/test/mod.rs | 38 +++++++++++--- 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/substrate/core/network/src/import_queue.rs b/substrate/core/network/src/import_queue.rs index 4a6775a2a9..42fcb1b6d6 100644 --- a/substrate/core/network/src/import_queue.rs +++ b/substrate/core/network/src/import_queue.rs @@ -34,14 +34,16 @@ use primitives::AuthorityId; use runtime_primitives::traits::{Block as BlockT, Header as HeaderT, NumberFor, Zero}; pub use blocks::BlockData; -use chain::Client; +use client::error::Error as ClientError; use error::{ErrorKind, Error}; use protocol::Context; use service::ExecuteInContext; use sync::ChainSync; -pub use consensus::{ImportBlock, ImportResult, BlockOrigin}; +pub use consensus::{ImportBlock, BlockImport, ImportResult, BlockOrigin}; +/// Shared block import struct used by the queue. +pub type SharedBlockImport = Arc + Send + Sync>; #[cfg(any(test, feature = "test-helpers"))] use std::cell::RefCell; @@ -98,6 +100,7 @@ pub struct BasicQueue> { handle: Mutex>>, data: Arc>, verifier: Arc, + block_import: SharedBlockImport, } /// Locks order: queue, queue_blocks, best_importing_number @@ -111,11 +114,12 @@ struct AsyncImportQueueData { impl> BasicQueue { /// Instantiate a new basic queue, with given verifier. - pub fn new(verifier: Arc) -> Self { + pub fn new(verifier: Arc, block_import: SharedBlockImport) -> Self { Self { handle: Mutex::new(None), data: Arc::new(AsyncImportQueueData::new()), verifier, + block_import, } } } @@ -141,8 +145,9 @@ impl> ImportQueue for BasicQueue { let qdata = self.data.clone(); let verifier = self.verifier.clone(); + let block_import = self.block_import.clone(); *self.handle.lock() = Some(::std::thread::Builder::new().name("ImportQueue".into()).spawn(move || { - import_thread(link, qdata, verifier) + import_thread(block_import, link, qdata, verifier) }).map_err(|err| Error::from(ErrorKind::Io(err)))?); Ok(()) } @@ -205,6 +210,7 @@ impl> Drop for BasicQueue { /// Blocks import thread. fn import_thread, V: Verifier>( + block_import: SharedBlockImport, link: L, qdata: Arc>, verifier: Arc @@ -229,6 +235,7 @@ fn import_thread, V: Verifier>( let blocks_hashes: Vec = new_blocks.1.iter().map(|b| b.block.hash.clone()).collect(); if !import_many_blocks( + &*block_import, &link, Some(&*qdata), new_blocks, @@ -249,8 +256,6 @@ fn import_thread, V: Verifier>( /// Hooks that the verification queue can use to influence the synchronization /// algorithm. pub trait Link: Send { - /// Get chain reference. - fn chain(&self) -> &Client; /// Block imported. fn block_imported(&self, _hash: &B::Hash, _number: NumberFor) { } /// Maintain sync. @@ -265,8 +270,6 @@ pub trait Link: Send { /// A link implementation that connects to the network. pub struct NetworkLink> { - /// The client handle. - pub(crate) client: Arc>, /// The chain-sync handle pub(crate) sync: Weak>>, /// Network context. @@ -286,10 +289,6 @@ impl> NetworkLink { } impl> Link for NetworkLink { - fn chain(&self) -> &Client { - &*self.client - } - fn block_imported(&self, hash: &B::Hash, number: NumberFor) { self.with_sync(|sync, _| sync.block_imported(&hash, number)) } @@ -342,6 +341,7 @@ enum BlockImportError { /// Import a bunch of blocks. fn import_many_blocks<'a, B: BlockT, L: Link, V: Verifier>( + import_handle: &BlockImport, link: &L, qdata: Option<&AsyncImportQueueData>, blocks: (BlockOrigin, Vec>), @@ -365,7 +365,7 @@ fn import_many_blocks<'a, B: BlockT, L: Link, V: Verifier>( // Blocks in the response/drain should be in ascending order. for block in blocks { let import_result = import_single_block( - link.chain(), + import_handle, blocks_origin.clone(), block, verifier.clone(), @@ -389,7 +389,7 @@ fn import_many_blocks<'a, B: BlockT, L: Link, V: Verifier>( /// Single block import function. fn import_single_block>( - chain: &Client, + import_handle: &BlockImport, block_origin: BlockOrigin, block: BlockData, verifier: Arc @@ -431,7 +431,7 @@ fn import_single_block>( BlockImportError::VerificationFailed(peer, msg) })?; - match chain.import(import_block, new_authorities) { + match import_handle.import_block(import_block, new_authorities) { Ok(ImportResult::AlreadyInChain) => { trace!(target: "sync", "Block already in chain {}: {:?}", number, hash); Ok(BlockImportResult::ImportedKnown(hash, number)) @@ -558,15 +558,25 @@ impl Verifier for PassThroughVerifier { } } -#[cfg(any(test, feature = "test-helpers"))] /// Blocks import queue that is importing blocks in the same thread. /// The boolean value indicates whether blocks should be imported without instant finality. -pub struct SyncImportQueue>(Arc, ImportCB); +#[cfg(any(test, feature = "test-helpers"))] +pub struct SyncImportQueue> { + verifier: Arc, + link: ImportCB, + block_import: SharedBlockImport, +} + #[cfg(any(test, feature = "test-helpers"))] impl> SyncImportQueue { - /// Create a new SyncImportQueue wrapping the given Verifier - pub fn new(verifier: Arc) -> Self { - SyncImportQueue(verifier, ImportCB::new()) + /// Create a new SyncImportQueue wrapping the given Verifier and block import + /// handle. + pub fn new(verifier: Arc, block_import: SharedBlockImport) -> Self { + SyncImportQueue { + verifier, + link: ImportCB::new(), + block_import, + } } } @@ -577,10 +587,12 @@ impl> ImportQueue for SyncImpor &self, link: L, ) -> Result<(), Error> { - let v = self.0.clone(); - self.1.set(Box::new(move |origin, new_blocks| { + let v = self.verifier.clone(); + let import_handle = self.block_import.clone(); + self.link.set(Box::new(move |origin, new_blocks| { let verifier = v.clone(); import_many_blocks( + &*import_handle, &link, None, (origin, new_blocks), @@ -605,7 +617,7 @@ impl> ImportQueue for SyncImpor } fn import_blocks(&self, origin: BlockOrigin, blocks: Vec>) { - self.1.call(origin, blocks); + self.link.call(origin, blocks); } } @@ -644,7 +656,6 @@ pub mod tests { } impl Link for TestLink { - fn chain(&self) -> &Client { &*self.chain } fn block_imported(&self, _hash: &Hash, _number: NumberFor) { self.imported.set(self.imported.get() + 1); } @@ -762,7 +773,9 @@ pub mod tests { let qdata = AsyncImportQueueData::new(); let verifier = Arc::new(PassThroughVerifier(true)); qdata.is_stopping.store(true, Ordering::SeqCst); + let client = test_client::new(); assert!(!import_many_blocks( + &client, &mut TestLink::new(), Some(&qdata), (BlockOrigin::File, vec![block.clone(), block]), diff --git a/substrate/core/network/src/service.rs b/substrate/core/network/src/service.rs index 0e46f50998..976fb8bc8e 100644 --- a/substrate/core/network/src/service.rs +++ b/substrate/core/network/src/service.rs @@ -130,7 +130,6 @@ impl, H: ExHashT> Service { protocol_id: ProtocolId, import_queue: I, ) -> Result>, Error> { - let chain = params.chain.clone(); let import_queue = Arc::new(import_queue); let handler = Arc::new(Protocol::new( params.config, @@ -153,7 +152,6 @@ impl, H: ExHashT> Service { // connect the import-queue to the network service. let link = ::import_queue::NetworkLink { - client: chain, sync: Arc::downgrade(service.handler.sync()), context: Arc::downgrade(&service), }; diff --git a/substrate/core/network/src/test/mod.rs b/substrate/core/network/src/test/mod.rs index 34eb873cc6..b852ceab80 100644 --- a/substrate/core/network/src/test/mod.rs +++ b/substrate/core/network/src/test/mod.rs @@ -24,6 +24,7 @@ use std::sync::Arc; use parking_lot::RwLock; use client; +use client::error::Error as ClientError; use client::block_builder::BlockBuilder; use runtime_primitives::generic::BlockId; use io::SyncIo; @@ -38,7 +39,7 @@ use import_queue::{SyncImportQueue, PassThroughVerifier, Verifier}; use consensus::BlockOrigin; use specialization::Specialization; use consensus_gossip::ConsensusGossip; -use import_queue::ImportQueue; +use import_queue::{BlockImport, ImportQueue}; use service::ExecuteInContext; use test_client; @@ -163,7 +164,6 @@ impl> Peer { 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(); let network_link = ::import_queue::NetworkLink { - client: self.sync.context_data().chain.clone(), sync: Arc::downgrade(self.sync.sync()), context: Arc::downgrade(&self.executor), }; @@ -235,16 +235,31 @@ impl> Peer { pub fn generate_blocks(&self, count: usize, origin: BlockOrigin, mut edit_block: F) where F: FnMut(&mut BlockBuilder) { - for _ in 0 .. count { + use blocks::BlockData; + + for _ in 0..count { let mut builder = self.client.new_block().unwrap(); edit_block(&mut builder); let block = builder.bake().unwrap(); let hash = block.header.hash(); trace!("Generating {}, (#{}, parent={})", hash, block.header.number, block.header.parent_hash); let header = block.header.clone(); - self.client.justify_and_import(origin, block).unwrap(); - self.sync.on_block_imported(&mut TestIo::new(&self.queue, None), hash, &header); + + // NOTE: if we use a non-synchronous queue in the test-net in the future, + // this may not work. + self.import_queue.import_blocks(origin, vec![BlockData { + origin: None, + block: ::message::BlockData:: { + hash, + header: Some(header), + body: Some(block.extrinsics), + receipt: None, + message_queue: None, + justification: None, + }, + }]); } + } /// Push blocks to the peer (simplified: with or without a TX) @@ -308,7 +323,12 @@ pub trait TestNetFactory: Sized { fn mut_peers>>)>(&mut self, closure: F ); fn started(&self) -> bool; - fn set_started(&mut self, now: bool); + fn set_started(&mut self, now: bool); + + /// Get custom block import handle for fresh client. + fn make_block_import(&self, client: Arc) -> Arc + Send + Sync> { + client + } fn default_config() -> ProtocolConfig { ProtocolConfig::default() @@ -330,7 +350,9 @@ pub trait TestNetFactory: Sized { let client = Arc::new(test_client::new()); let tx_pool = Arc::new(EmptyTransactionPool); let verifier = self.make_verifier(client.clone(), config); - let import_queue = Arc::new(SyncImportQueue::new(verifier)); + let block_import = self.make_block_import(client.clone()); + + let import_queue = Arc::new(SyncImportQueue::new(verifier, block_import)); let specialization = DummySpecialization { gossip: ConsensusGossip::new(), }; @@ -471,7 +493,7 @@ impl TestNetFactory for TestNet { started: false } } - + fn make_verifier(&self, _client: Arc, _config: &ProtocolConfig) -> Arc { From 84925067f474ced9bf4d6ece208424a177d01423 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 29 Oct 2018 16:25:22 +0100 Subject: [PATCH 18/36] generalizable data for peers in test network --- substrate/core/consensus/aura/src/lib.rs | 7 +++-- substrate/core/network/src/test/mod.rs | 38 ++++++++++++++---------- substrate/core/service/src/lib.rs | 4 +-- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/substrate/core/consensus/aura/src/lib.rs b/substrate/core/consensus/aura/src/lib.rs index 7355119074..cb4a24de3e 100644 --- a/substrate/core/consensus/aura/src/lib.rs +++ b/substrate/core/consensus/aura/src/lib.rs @@ -464,6 +464,7 @@ mod tests { impl TestNetFactory for AuraTestNet { type Verifier = AuraVerifier; + type PeerData = (); /// Create new test network with peers and given config. fn from_config(_config: &ProtocolConfig) -> Self { @@ -480,15 +481,15 @@ mod tests { Arc::new(AuraVerifier { client, config }) } - fn peer(&self, i: usize) -> &Peer { + fn peer(&self, i: usize) -> &Peer { &self.peers[i] } - fn peers(&self) -> &Vec>> { + fn peers(&self) -> &Vec>> { &self.peers } - fn mut_peers>>)>(&mut self, closure: F ) { + fn mut_peers>>)>(&mut self, closure: F) { closure(&mut self.peers); } diff --git a/substrate/core/network/src/test/mod.rs b/substrate/core/network/src/test/mod.rs index b852ceab80..735f6c0c13 100644 --- a/substrate/core/network/src/test/mod.rs +++ b/substrate/core/network/src/test/mod.rs @@ -140,23 +140,26 @@ pub struct TestPacket { pub type PeersClient = client::Client; -pub struct Peer> { +pub struct Peer, D> { client: Arc, pub sync: Arc>, pub queue: Arc>>, import_queue: Arc>, executor: Arc, + /// Some custom data set up at initialization time. + pub data: D, } -impl> Peer { +impl, D> Peer { fn new( client: Arc, sync: Arc>, queue: Arc>>, import_queue: Arc>, + data: D, ) -> Self { let executor = Arc::new(DummyContextExecutor(sync.clone(), queue.clone())); - Peer { client, sync, queue, import_queue, executor} + Peer { client, sync, queue, import_queue, executor, data } } /// Called after blockchain has been populated to updated current state. fn start(&self) { @@ -311,6 +314,7 @@ impl TransactionPool for EmptyTransactionPool { pub trait TestNetFactory: Sized { type Verifier: 'static + Verifier; + type PeerData: Default; /// These two need to be implemented! fn from_config(config: &ProtocolConfig) -> Self; @@ -318,16 +322,18 @@ pub trait TestNetFactory: Sized { /// Get reference to peer. - fn peer(&self, i: usize) -> &Peer; - fn peers(&self) -> &Vec>>; - fn mut_peers>>)>(&mut self, closure: F ); + fn peer(&self, i: usize) -> &Peer; + fn peers(&self) -> &Vec>>; + fn mut_peers>>)>(&mut self, closure: F ); fn started(&self) -> bool; fn set_started(&mut self, now: bool); - /// Get custom block import handle for fresh client. - fn make_block_import(&self, client: Arc) -> Arc + Send + Sync> { - client + /// Get custom block import handle for fresh client, along with peer data. + fn make_block_import(&self, client: Arc) + -> (Arc + Send + Sync>, Self::PeerData) + { + (client, Default::default()) } fn default_config() -> ProtocolConfig { @@ -350,7 +356,7 @@ pub trait TestNetFactory: Sized { let client = Arc::new(test_client::new()); let tx_pool = Arc::new(EmptyTransactionPool); let verifier = self.make_verifier(client.clone(), config); - let block_import = self.make_block_import(client.clone()); + let (block_import, data) = self.make_block_import(client.clone()); let import_queue = Arc::new(SyncImportQueue::new(verifier, block_import)); let specialization = DummySpecialization { @@ -369,7 +375,8 @@ pub trait TestNetFactory: Sized { client, Arc::new(sync), Arc::new(RwLock::new(VecDeque::new())), - import_queue + import_queue, + data, )); self.mut_peers(|peers| { @@ -479,12 +486,13 @@ pub trait TestNetFactory: Sized { } pub struct TestNet { - peers: Vec>>, + peers: Vec>>, started: bool } impl TestNetFactory for TestNet { type Verifier = PassThroughVerifier; + type PeerData = (); /// Create new test network with peers and given config. fn from_config(_config: &ProtocolConfig) -> Self { @@ -500,15 +508,15 @@ impl TestNetFactory for TestNet { Arc::new(PassThroughVerifier(false)) } - fn peer(&self, i: usize) -> &Peer { + fn peer(&self, i: usize) -> &Peer { &self.peers[i] } - fn peers(&self) -> &Vec>> { + fn peers(&self) -> &Vec>> { &self.peers } - fn mut_peers>>)>(&mut self, closure: F ) { + fn mut_peers>>)>(&mut self, closure: F ) { closure(&mut self.peers); } diff --git a/substrate/core/service/src/lib.rs b/substrate/core/service/src/lib.rs index 9e7c201e8f..1265486449 100644 --- a/substrate/core/service/src/lib.rs +++ b/substrate/core/service/src/lib.rs @@ -528,8 +528,8 @@ macro_rules! construct_simple_service { /// // The first one is for the initializing the full import queue and the second for the /// // light import queue. /// ImportQueue = BasicQueue -/// { |_, _| Ok(BasicQueue::new(Arc::new(NoneVerifier {}))) } -/// { |_, _| Ok(BasicQueue::new(Arc::new(NoneVerifier {}))) }, +/// { |_, client| Ok(BasicQueue::new(Arc::new(NoneVerifier {}, client))) } +/// { |_, client| Ok(BasicQueue::new(Arc::new(NoneVerifier {}, client))) }, /// } /// } /// ``` From 6d4acb053dab19df517d7d952a9827af30fa91a9 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 29 Oct 2018 17:03:08 +0100 Subject: [PATCH 19/36] tests compile using new test framework --- substrate/Cargo.lock | 1 + substrate/core/finality-grandpa/Cargo.toml | 1 + substrate/core/finality-grandpa/src/lib.rs | 184 +++++++++++++++------ substrate/core/network/src/test/mod.rs | 2 +- 4 files changed, 140 insertions(+), 48 deletions(-) diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index c988464c3d..3b1b27388d 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -3047,6 +3047,7 @@ dependencies = [ "substrate-keyring 0.1.0", "substrate-network 0.1.0", "substrate-primitives 0.1.0", + "substrate-test-client 0.1.0", "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/substrate/core/finality-grandpa/Cargo.toml b/substrate/core/finality-grandpa/Cargo.toml index 2181d2f16d..96892ec6f7 100644 --- a/substrate/core/finality-grandpa/Cargo.toml +++ b/substrate/core/finality-grandpa/Cargo.toml @@ -24,3 +24,4 @@ features = ["derive-codec"] [dev-dependencies] substrate-network = { path = "../network", features = ["test-helpers"] } substrate-keyring = { path = "../keyring" } +substrate-test-client = { path = "../test-client"} diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 2291c0c823..d8fe874a5e 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -38,6 +38,9 @@ extern crate substrate_network as network; #[cfg(test)] extern crate substrate_keyring as keyring; +#[cfg(test)] +extern crate substrate_test_client as test_client; + #[macro_use] extern crate parity_codec_derive; @@ -620,6 +623,7 @@ impl voter::Environment> f return Ok(()); } + // lock must be held through writing to DB to avoid race let mut authority_set = self.authority_set.inner().write(); let client = &self.inner; let status = authority_set.apply_changes(number, |canon_number| { @@ -752,32 +756,23 @@ impl BlockImport for GrandpaBlockImport } } -/// Run a GRANDPA voter as a task. This returns two pieces of data: a task to run, -/// and a `BlockImport` implementation. -pub fn run_grandpa( - config: Config, +/// Half of a link between a block-import worker and a the background voter. +// This should remain non-clone. +pub struct LinkHalf { client: Arc>, - voters: HashMap, - network: N, -) -> ::client::error::Result<( - impl Future, - GrandpaBlockImport, -)> where - Block::Hash: Ord, + authority_set: SharedAuthoritySet>, +} + +/// Make block importer and link half necessary to tie the background voter +/// to it. +pub fn block_import(client: Arc>) + -> Result<(GrandpaBlockImport, LinkHalf), ClientError> + where B: Backend + 'static, E: CallExecutor + 'static, - N: Network + 'static, - N::In: 'static, - NumberFor: BlockNumberOps, - DigestFor: Encode, { - use futures::future::{self, Loop as FutureLoop}; - use runtime_primitives::traits::Zero; - let chain_info = client.info()?; - let genesis_hash = chain_info.chain.genesis_hash; - let authority_set = match client.backend().get_aux(AUTHORITY_SET_KEY)? { None => { info!(target: "afg", "Loading GRANDPA authorities \ @@ -802,10 +797,33 @@ pub fn run_grandpa( .into(), }; - let block_import = GrandpaBlockImport { - inner: client.clone(), - authority_set: authority_set.clone(), - }; + Ok(( + GrandpaBlockImport { inner: client.clone(), authority_set: authority_set.clone() }, + LinkHalf { client, authority_set }, + )) +} + +/// Run a GRANDPA voter as a task. Provide configuration and a link to a +/// block import worker that has already been instantiated with `block_import`. +pub fn run_grandpa( + config: Config, + link: LinkHalf, + network: N, +) -> ::client::error::Result> where + Block::Hash: Ord, + B: Backend + 'static, + E: CallExecutor + 'static, + N: Network + 'static, + N::In: 'static, + NumberFor: BlockNumberOps, + DigestFor: Encode, +{ + use futures::future::{self, Loop as FutureLoop}; + use runtime_primitives::traits::Zero; + + let LinkHalf { client, authority_set } = link; + let chain_info = client.info()?; + let genesis_hash = chain_info.chain.genesis_hash; let (last_round_number, last_state) = match client.backend().get_aux(LAST_COMPLETED_KEY)? { None => (0, RoundState::genesis((genesis_hash, >::zero()))), @@ -815,6 +833,10 @@ pub fn run_grandpa( ))? }; + let voters = authority_set.inner().read().current().1.iter() + .cloned() + .collect(); + let initial_environment = Arc::new(Environment { inner: client.clone(), config: config.clone(), @@ -824,7 +846,7 @@ pub fn run_grandpa( authority_set: authority_set.clone(), }); - let voters = future::loop_fn((initial_environment, last_round_number, last_state), move |params| { + let work = future::loop_fn((initial_environment, last_round_number, last_state), move |params| { let (env, last_round_number, last_state) = params; let chain_info = match client.info() { Ok(i) => i, @@ -866,29 +888,84 @@ pub fn run_grandpa( })) }); - let work = voters.map_err(|e| warn!("GRANDPA Voter failed: {:?}", e)); - - Ok((work, block_import)) + Ok(work.map_err(|e| warn!("GRANDPA Voter failed: {:?}", e))) } #[cfg(test)] mod tests { use super::*; - use network::test::*; + use network::test::{Block, Hash, TestNetFactory, Peer, PeersClient}; + use network::import_queue::{PassThroughVerifier}; + use network::ProtocolConfig; use parking_lot::Mutex; use tokio::runtime::current_thread; use keyring::Keyring; use client::BlockchainEvents; + use test_client; + + type PeerData = Mutex>>; + type GrandpaPeer = Peer; + + struct GrandpaTestNet { + peers: Vec>, + started: bool + } + + impl TestNetFactory for GrandpaTestNet { + type Verifier = PassThroughVerifier; + type PeerData = PeerData; + + /// Create new test network with peers and given config. + fn from_config(_config: &ProtocolConfig) -> Self { + GrandpaTestNet { + peers: Vec::new(), + started: false + } + } + + fn make_verifier(&self, _client: Arc, _cfg: &ProtocolConfig) + -> Arc + { + Arc::new(PassThroughVerifier(false)) // use non-instant finality. + } + + fn make_block_import(&self, client: Arc) + -> (Arc + Send + Sync>, PeerData) + { + let (import, link) = block_import(client).expect("Could not create block import for fresh peer."); + (Arc::new(import), Mutex::new(Some(link))) + } + + fn peer(&self, i: usize) -> &GrandpaPeer { + &self.peers[i] + } + + fn peers(&self) -> &Vec> { + &self.peers + } + + fn mut_peers>)>(&mut self, closure: F) { + closure(&mut self.peers); + } + + fn started(&self) -> bool { + self.started + } + + fn set_started(&mut self, new: bool) { + self.started = new; + } + } #[derive(Clone)] - struct TestGrandpaNetwork { - inner: Arc>, + struct MessageRouting { + inner: Arc>, peer_id: usize, } - impl TestGrandpaNetwork { - fn new(inner: Arc>, peer_id: usize,) -> Self { - TestGrandpaNetwork { + impl MessageRouting { + fn new(inner: Arc>, peer_id: usize,) -> Self { + MessageRouting { inner, peer_id, } @@ -904,7 +981,7 @@ mod tests { hash } - impl Network for TestGrandpaNetwork { + impl Network for MessageRouting { type In = Box,Error=()>>; fn messages_for(&self, round: u64) -> Self::In { @@ -936,7 +1013,7 @@ mod tests { #[test] fn finalize_20_unanimous_3_peers() { - let mut net = TestNet::new(3); + let mut net = GrandpaTestNet::new(3); net.peer(0).push_blocks(20, false); net.sync(); @@ -955,20 +1032,27 @@ mod tests { let mut runtime = current_thread::Runtime::new().unwrap(); for (peer_id, key) in peers { - let client = net.lock().peer(*peer_id).client().clone(); + let (client, link) = { + let mut net = net.lock(); + // temporary needed for some reason + let link = net.peers[*peer_id].data.lock().take().expect("link initialized at startup; qed"); + ( + net.peers[*peer_id].client().clone(), + link, + ) + }; finality_notifications.push( client.finality_notification_stream() .take_while(|n| Ok(n.header.number() < &20)) .for_each(move |_| Ok(())) ); - let (voter, _) = run_grandpa( + let voter = run_grandpa( Config { gossip_duration: TEST_GOSSIP_DURATION, local_key: Some(Arc::new(key.clone().into())), }, - client, - voters.iter().map(|&id| (id, 1)).collect(), - TestGrandpaNetwork::new(net.clone(), *peer_id), + link, + MessageRouting::new(net.clone(), *peer_id), ).expect("all in order with client and network"); runtime.spawn(voter); @@ -989,7 +1073,7 @@ mod tests { #[test] fn observer_can_finalize() { - let mut net = TestNet::new(4); + let mut net = GrandpaTestNet::new(4); net.peer(0).push_blocks(20, false); net.sync(); @@ -1013,20 +1097,26 @@ mod tests { .chain(::std::iter::once((3, None))); for (peer_id, local_key) in all_peers { - let client = net.lock().peer(peer_id).client().clone(); + let (client, link) = { + let mut net = net.lock(); + let link = net.peers[peer_id].data.lock().take().expect("link initialized at startup; qed"); + ( + net.peers[peer_id].client().clone(), + link, + ) + }; finality_notifications.push( client.finality_notification_stream() .take_while(|n| Ok(n.header.number() < &20)) .for_each(move |_| Ok(())) ); - let (voter, _) = run_grandpa( + let voter = run_grandpa( Config { gossip_duration: TEST_GOSSIP_DURATION, local_key, }, - client, - voters.clone(), - TestGrandpaNetwork::new(net.clone(), peer_id), + link, + MessageRouting::new(net.clone(), peer_id), ).expect("all in order with client and network"); runtime.spawn(voter); diff --git a/substrate/core/network/src/test/mod.rs b/substrate/core/network/src/test/mod.rs index 735f6c0c13..ac7968ea97 100644 --- a/substrate/core/network/src/test/mod.rs +++ b/substrate/core/network/src/test/mod.rs @@ -324,7 +324,7 @@ pub trait TestNetFactory: Sized { /// Get reference to peer. fn peer(&self, i: usize) -> &Peer; fn peers(&self) -> &Vec>>; - fn mut_peers>>)>(&mut self, closure: F ); + fn mut_peers>>)>(&mut self, closure: F); fn started(&self) -> bool; fn set_started(&mut self, now: bool); From afbe00db75a0fc19ec63c5c12bd7ea79b2eb5beb Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 29 Oct 2018 17:05:42 +0100 Subject: [PATCH 20/36] extract out tests to file --- substrate/core/finality-grandpa/src/lib.rs | 248 +------------------ substrate/core/finality-grandpa/src/tests.rs | 241 ++++++++++++++++++ 2 files changed, 244 insertions(+), 245 deletions(-) create mode 100644 substrate/core/finality-grandpa/src/tests.rs diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index d8fe874a5e..21d5c054fb 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -70,6 +70,9 @@ pub use fg_primitives::ScheduledChange; mod authorities; +#[cfg(test)] +mod tests; + const LAST_COMPLETED_KEY: &[u8] = b"grandpa_completed_round"; const AUTHORITY_SET_KEY: &[u8] = b"grandpa_voters"; @@ -890,248 +893,3 @@ pub fn run_grandpa( Ok(work.map_err(|e| warn!("GRANDPA Voter failed: {:?}", e))) } - -#[cfg(test)] -mod tests { - use super::*; - use network::test::{Block, Hash, TestNetFactory, Peer, PeersClient}; - use network::import_queue::{PassThroughVerifier}; - use network::ProtocolConfig; - use parking_lot::Mutex; - use tokio::runtime::current_thread; - use keyring::Keyring; - use client::BlockchainEvents; - use test_client; - - type PeerData = Mutex>>; - type GrandpaPeer = Peer; - - struct GrandpaTestNet { - peers: Vec>, - started: bool - } - - impl TestNetFactory for GrandpaTestNet { - type Verifier = PassThroughVerifier; - type PeerData = PeerData; - - /// Create new test network with peers and given config. - fn from_config(_config: &ProtocolConfig) -> Self { - GrandpaTestNet { - peers: Vec::new(), - started: false - } - } - - fn make_verifier(&self, _client: Arc, _cfg: &ProtocolConfig) - -> Arc - { - Arc::new(PassThroughVerifier(false)) // use non-instant finality. - } - - fn make_block_import(&self, client: Arc) - -> (Arc + Send + Sync>, PeerData) - { - let (import, link) = block_import(client).expect("Could not create block import for fresh peer."); - (Arc::new(import), Mutex::new(Some(link))) - } - - fn peer(&self, i: usize) -> &GrandpaPeer { - &self.peers[i] - } - - fn peers(&self) -> &Vec> { - &self.peers - } - - fn mut_peers>)>(&mut self, closure: F) { - closure(&mut self.peers); - } - - fn started(&self) -> bool { - self.started - } - - fn set_started(&mut self, new: bool) { - self.started = new; - } - } - - #[derive(Clone)] - struct MessageRouting { - inner: Arc>, - peer_id: usize, - } - - impl MessageRouting { - fn new(inner: Arc>, peer_id: usize,) -> Self { - MessageRouting { - inner, - peer_id, - } - } - } - - fn round_to_topic(round: u64) -> Hash { - let mut hash = Hash::default(); - round.using_encoded(|s| { - let raw = hash.as_mut(); - raw[..8].copy_from_slice(s); - }); - hash - } - - impl Network for MessageRouting { - type In = Box,Error=()>>; - - fn messages_for(&self, round: u64) -> Self::In { - let messages = self.inner.lock().peer(self.peer_id) - .with_spec(|spec, _| spec.gossip.messages_for(round_to_topic(round))); - - let messages = messages.map_err( - move |_| panic!("Messages for round {} dropped too early", round) - ); - - Box::new(messages) - } - - fn send_message(&self, round: u64, message: Vec) { - let mut inner = self.inner.lock(); - inner.peer(self.peer_id).gossip_message(round_to_topic(round), message); - inner.route(); - } - - fn drop_messages(&self, round: u64) { - let topic = round_to_topic(round); - self.inner.lock().peer(self.peer_id) - .with_spec(|spec, _| spec.gossip.collect_garbage(|t| t == &topic)); - } - } - - const TEST_GOSSIP_DURATION: Duration = Duration::from_millis(500); - const TEST_ROUTING_INTERVAL: Duration = Duration::from_millis(50); - - #[test] - fn finalize_20_unanimous_3_peers() { - let mut net = GrandpaTestNet::new(3); - net.peer(0).push_blocks(20, false); - net.sync(); - - let net = Arc::new(Mutex::new(net)); - let peers = &[ - (0, Keyring::Alice), - (1, Keyring::Bob), - (2, Keyring::Charlie), - ]; - - let voters: Vec<_> = peers.iter() - .map(|&(_, ref key)| AuthorityId(key.to_raw_public())) - .collect(); - - let mut finality_notifications = Vec::new(); - - let mut runtime = current_thread::Runtime::new().unwrap(); - for (peer_id, key) in peers { - let (client, link) = { - let mut net = net.lock(); - // temporary needed for some reason - let link = net.peers[*peer_id].data.lock().take().expect("link initialized at startup; qed"); - ( - net.peers[*peer_id].client().clone(), - link, - ) - }; - finality_notifications.push( - client.finality_notification_stream() - .take_while(|n| Ok(n.header.number() < &20)) - .for_each(move |_| Ok(())) - ); - let voter = run_grandpa( - Config { - gossip_duration: TEST_GOSSIP_DURATION, - local_key: Some(Arc::new(key.clone().into())), - }, - link, - MessageRouting::new(net.clone(), *peer_id), - ).expect("all in order with client and network"); - - runtime.spawn(voter); - } - - // wait for all finalized on each. - let wait_for = ::futures::future::join_all(finality_notifications) - .map(|_| ()) - .map_err(|_| ()); - - let drive_to_completion = ::tokio::timer::Interval::new_interval(TEST_ROUTING_INTERVAL) - .for_each(move |_| { net.lock().route_until_complete(); Ok(()) }) - .map(|_| ()) - .map_err(|_| ()); - - runtime.block_on(wait_for.select(drive_to_completion).map_err(|_| ())).unwrap(); - } - - #[test] - fn observer_can_finalize() { - let mut net = GrandpaTestNet::new(4); - net.peer(0).push_blocks(20, false); - net.sync(); - - let net = Arc::new(Mutex::new(net)); - let peers = &[ - (0, Keyring::Alice), - (1, Keyring::Bob), - (2, Keyring::Charlie), - ]; - - let voters: HashMap<_, _> = peers.iter() - .map(|&(_, ref key)| (AuthorityId(key.to_raw_public()), 1)) - .collect(); - - let mut finality_notifications = Vec::new(); - - let mut runtime = current_thread::Runtime::new().unwrap(); - let all_peers = peers.iter() - .cloned() - .map(|(id, key)| (id, Some(Arc::new(key.into())))) - .chain(::std::iter::once((3, None))); - - for (peer_id, local_key) in all_peers { - let (client, link) = { - let mut net = net.lock(); - let link = net.peers[peer_id].data.lock().take().expect("link initialized at startup; qed"); - ( - net.peers[peer_id].client().clone(), - link, - ) - }; - finality_notifications.push( - client.finality_notification_stream() - .take_while(|n| Ok(n.header.number() < &20)) - .for_each(move |_| Ok(())) - ); - let voter = run_grandpa( - Config { - gossip_duration: TEST_GOSSIP_DURATION, - local_key, - }, - link, - MessageRouting::new(net.clone(), peer_id), - ).expect("all in order with client and network"); - - runtime.spawn(voter); - } - - // wait for all finalized on each. - let wait_for = ::futures::future::join_all(finality_notifications) - .map(|_| ()) - .map_err(|_| ()); - - let drive_to_completion = ::tokio::timer::Interval::new_interval(TEST_ROUTING_INTERVAL) - .for_each(move |_| { net.lock().route_until_complete(); Ok(()) }) - .map(|_| ()) - .map_err(|_| ()); - - runtime.block_on(wait_for.select(drive_to_completion).map_err(|_| ())).unwrap(); - } -} diff --git a/substrate/core/finality-grandpa/src/tests.rs b/substrate/core/finality-grandpa/src/tests.rs new file mode 100644 index 0000000000..7b3c6a39bf --- /dev/null +++ b/substrate/core/finality-grandpa/src/tests.rs @@ -0,0 +1,241 @@ +use super::*; +use network::test::{Block, Hash, TestNetFactory, Peer, PeersClient}; +use network::import_queue::{PassThroughVerifier}; +use network::ProtocolConfig; +use parking_lot::Mutex; +use tokio::runtime::current_thread; +use keyring::Keyring; +use client::BlockchainEvents; +use test_client; + +type PeerData = Mutex>>; +type GrandpaPeer = Peer; + +struct GrandpaTestNet { + peers: Vec>, + started: bool +} + +impl TestNetFactory for GrandpaTestNet { + type Verifier = PassThroughVerifier; + type PeerData = PeerData; + + /// Create new test network with peers and given config. + fn from_config(_config: &ProtocolConfig) -> Self { + GrandpaTestNet { + peers: Vec::new(), + started: false + } + } + + fn make_verifier(&self, _client: Arc, _cfg: &ProtocolConfig) + -> Arc + { + Arc::new(PassThroughVerifier(false)) // use non-instant finality. + } + + fn make_block_import(&self, client: Arc) + -> (Arc + Send + Sync>, PeerData) + { + let (import, link) = block_import(client).expect("Could not create block import for fresh peer."); + (Arc::new(import), Mutex::new(Some(link))) + } + + fn peer(&self, i: usize) -> &GrandpaPeer { + &self.peers[i] + } + + fn peers(&self) -> &Vec> { + &self.peers + } + + fn mut_peers>)>(&mut self, closure: F) { + closure(&mut self.peers); + } + + fn started(&self) -> bool { + self.started + } + + fn set_started(&mut self, new: bool) { + self.started = new; + } +} + +#[derive(Clone)] +struct MessageRouting { + inner: Arc>, + peer_id: usize, +} + +impl MessageRouting { + fn new(inner: Arc>, peer_id: usize,) -> Self { + MessageRouting { + inner, + peer_id, + } + } +} + +fn round_to_topic(round: u64) -> Hash { + let mut hash = Hash::default(); + round.using_encoded(|s| { + let raw = hash.as_mut(); + raw[..8].copy_from_slice(s); + }); + hash +} + +impl Network for MessageRouting { + type In = Box,Error=()>>; + + fn messages_for(&self, round: u64) -> Self::In { + let messages = self.inner.lock().peer(self.peer_id) + .with_spec(|spec, _| spec.gossip.messages_for(round_to_topic(round))); + + let messages = messages.map_err( + move |_| panic!("Messages for round {} dropped too early", round) + ); + + Box::new(messages) + } + + fn send_message(&self, round: u64, message: Vec) { + let mut inner = self.inner.lock(); + inner.peer(self.peer_id).gossip_message(round_to_topic(round), message); + inner.route(); + } + + fn drop_messages(&self, round: u64) { + let topic = round_to_topic(round); + self.inner.lock().peer(self.peer_id) + .with_spec(|spec, _| spec.gossip.collect_garbage(|t| t == &topic)); + } +} + +const TEST_GOSSIP_DURATION: Duration = Duration::from_millis(500); +const TEST_ROUTING_INTERVAL: Duration = Duration::from_millis(50); + +#[test] +fn finalize_20_unanimous_3_peers() { + let mut net = GrandpaTestNet::new(3); + net.peer(0).push_blocks(20, false); + net.sync(); + + let net = Arc::new(Mutex::new(net)); + let peers = &[ + (0, Keyring::Alice), + (1, Keyring::Bob), + (2, Keyring::Charlie), + ]; + + let voters: Vec<_> = peers.iter() + .map(|&(_, ref key)| AuthorityId(key.to_raw_public())) + .collect(); + + let mut finality_notifications = Vec::new(); + + let mut runtime = current_thread::Runtime::new().unwrap(); + for (peer_id, key) in peers { + let (client, link) = { + let mut net = net.lock(); + // temporary needed for some reason + let link = net.peers[*peer_id].data.lock().take().expect("link initialized at startup; qed"); + ( + net.peers[*peer_id].client().clone(), + link, + ) + }; + finality_notifications.push( + client.finality_notification_stream() + .take_while(|n| Ok(n.header.number() < &20)) + .for_each(move |_| Ok(())) + ); + let voter = run_grandpa( + Config { + gossip_duration: TEST_GOSSIP_DURATION, + local_key: Some(Arc::new(key.clone().into())), + }, + link, + MessageRouting::new(net.clone(), *peer_id), + ).expect("all in order with client and network"); + + runtime.spawn(voter); + } + + // wait for all finalized on each. + let wait_for = ::futures::future::join_all(finality_notifications) + .map(|_| ()) + .map_err(|_| ()); + + let drive_to_completion = ::tokio::timer::Interval::new_interval(TEST_ROUTING_INTERVAL) + .for_each(move |_| { net.lock().route_until_complete(); Ok(()) }) + .map(|_| ()) + .map_err(|_| ()); + + runtime.block_on(wait_for.select(drive_to_completion).map_err(|_| ())).unwrap(); +} + +#[test] +fn observer_can_finalize() { + let mut net = GrandpaTestNet::new(4); + net.peer(0).push_blocks(20, false); + net.sync(); + + let net = Arc::new(Mutex::new(net)); + let peers = &[ + (0, Keyring::Alice), + (1, Keyring::Bob), + (2, Keyring::Charlie), + ]; + + let voters: HashMap<_, _> = peers.iter() + .map(|&(_, ref key)| (AuthorityId(key.to_raw_public()), 1)) + .collect(); + + let mut finality_notifications = Vec::new(); + + let mut runtime = current_thread::Runtime::new().unwrap(); + let all_peers = peers.iter() + .cloned() + .map(|(id, key)| (id, Some(Arc::new(key.into())))) + .chain(::std::iter::once((3, None))); + + for (peer_id, local_key) in all_peers { + let (client, link) = { + let mut net = net.lock(); + let link = net.peers[peer_id].data.lock().take().expect("link initialized at startup; qed"); + ( + net.peers[peer_id].client().clone(), + link, + ) + }; + finality_notifications.push( + client.finality_notification_stream() + .take_while(|n| Ok(n.header.number() < &20)) + .for_each(move |_| Ok(())) + ); + let voter = run_grandpa( + Config { + gossip_duration: TEST_GOSSIP_DURATION, + local_key, + }, + link, + MessageRouting::new(net.clone(), peer_id), + ).expect("all in order with client and network"); + + runtime.spawn(voter); + } + + // wait for all finalized on each. + let wait_for = ::futures::future::join_all(finality_notifications) + .map(|_| ()) + .map_err(|_| ()); + + let drive_to_completion = ::tokio::timer::Interval::new_interval(TEST_ROUTING_INTERVAL) + .for_each(move |_| { net.lock().route_until_complete(); Ok(()) }) + .map(|_| ()) + .map_err(|_| ()); + + runtime.block_on(wait_for.select(drive_to_completion).map_err(|_| ())).unwrap(); +} From fe6dd131f0f17281dce21b5fabced1bca1ab22a7 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Tue, 30 Oct 2018 23:46:27 +0100 Subject: [PATCH 21/36] tests compile after changes --- substrate/Cargo.lock | 4 +- substrate/core/finality-grandpa/Cargo.toml | 2 +- .../finality-grandpa/primitives/src/lib.rs | 2 +- substrate/core/finality-grandpa/src/lib.rs | 60 ++++++--- substrate/core/finality-grandpa/src/tests.rs | 114 ++++++++++++------ substrate/core/network/src/import_queue.rs | 32 ++++- substrate/core/network/src/test/mod.rs | 2 +- 7 files changed, 156 insertions(+), 60 deletions(-) diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 3b1b27388d..b14022ea3a 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -581,7 +581,6 @@ dependencies = [ [[package]] name = "finality-grandpa" version = "0.3.0" -source = "git+https://github.com/paritytech/finality-grandpa#f1ad8d7ca020e5db0e51e32cd62e8cd3c578d121" dependencies = [ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3034,7 +3033,7 @@ dependencies = [ name = "substrate-finality-grandpa" version = "0.1.0" dependencies = [ - "finality-grandpa 0.3.0 (git+https://github.com/paritytech/finality-grandpa)", + "finality-grandpa 0.3.0", "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4202,7 +4201,6 @@ dependencies = [ "checksum failure_derive 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "946d0e98a50d9831f5d589038d2ca7f8f455b1c21028c0db0e84116a12696426" "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" "checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa" -"checksum finality-grandpa 0.3.0 (git+https://github.com/paritytech/finality-grandpa)" = "" "checksum fixed-hash 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d5ec8112f00ea8a483e04748a85522184418fd1cf02890b626d8fc28683f7de" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" diff --git a/substrate/core/finality-grandpa/Cargo.toml b/substrate/core/finality-grandpa/Cargo.toml index 96892ec6f7..fa30e427f8 100644 --- a/substrate/core/finality-grandpa/Cargo.toml +++ b/substrate/core/finality-grandpa/Cargo.toml @@ -18,7 +18,7 @@ substrate-fg-primitives = { path = "primitives" } [dependencies.finality-grandpa] #version = "0.3.0" -git = "https://github.com/paritytech/finality-grandpa" +path = "../../../finality-afg" features = ["derive-codec"] [dev-dependencies] diff --git a/substrate/core/finality-grandpa/primitives/src/lib.rs b/substrate/core/finality-grandpa/primitives/src/lib.rs index ddf924cc2e..649d424e6b 100644 --- a/substrate/core/finality-grandpa/primitives/src/lib.rs +++ b/substrate/core/finality-grandpa/primitives/src/lib.rs @@ -33,7 +33,7 @@ use sr_primitives::traits::{Block as BlockT, DigestFor, NumberFor}; /// A scheduled change of authority set. #[cfg_attr(feature = "std", derive(Debug, PartialEq))] -#[derive(Encode, Decode)] +#[derive(Clone, Encode, Decode)] pub struct ScheduledChange { /// The new authorities after the change, along with their respective weights. pub next_authorities: Vec<(AuthorityId, u64)>, diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 21d5c054fb..d12b35ea1f 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -699,20 +699,58 @@ impl voter::Environment> f } } +/// Client side of the GRANDPA APIs declared in fg-primitives. +pub trait ApiClient { + /// Get the genesis authorities for GRANDPA. + fn genesis_authorities(&self) -> Result, ClientError>; + + /// Check a header's digest for a scheduled change. + fn scheduled_change(&self, header: &Block::Header) + -> Result>>, ClientError>; +} + +impl ApiClient for Arc> where + B: Backend + 'static, + E: CallExecutor + 'static + Clone, + DigestFor: Encode, +{ + fn genesis_authorities(&self) -> Result, ClientError> { + use runtime_primitives::traits::Zero; + + self.call_api_at( + &BlockId::Number(NumberFor::::zero()), + fg_primitives::AUTHORITIES_CALL, + &() + ) + } + + fn scheduled_change(&self, header: &Block::Header) + -> Result>>, ClientError> + { + self.call_api_at( + &BlockId::hash(header.parent_hash().clone()), + ::fg_primitives::PENDING_CHANGE_CALL, + header.digest(), + ) + } +} + /// A block-import handler for GRANDPA. /// /// This scans each imported block for signals of changing authority set. /// When using GRANDPA, the block import worker should be using this block import /// object. -pub struct GrandpaBlockImport { +pub struct GrandpaBlockImport { inner: Arc>, authority_set: SharedAuthoritySet>, + api_client: Api, } -impl BlockImport for GrandpaBlockImport where +impl BlockImport for GrandpaBlockImport where B: Backend + 'static, E: CallExecutor + 'static + Clone, DigestFor: Encode, + Api: ApiClient, { type Error = ClientError; @@ -721,11 +759,7 @@ impl BlockImport for GrandpaBlockImport { use authorities::PendingChange; - let maybe_change: Option>> = self.inner.call_api_at( - &BlockId::hash(block.header.parent_hash().clone()), - ::fg_primitives::PENDING_CHANGE_CALL, - block.header.digest() - )?; + let maybe_change = self.api_client.scheduled_change(&block.header)?; // when we update the authorities, we need to hold the lock // until the block is written to prevent a race if we need to restore @@ -768,14 +802,13 @@ pub struct LinkHalf { /// Make block importer and link half necessary to tie the background voter /// to it. -pub fn block_import(client: Arc>) - -> Result<(GrandpaBlockImport, LinkHalf), ClientError> +pub fn block_import(client: Arc>, api_client: Api) + -> Result<(GrandpaBlockImport, LinkHalf), ClientError> where B: Backend + 'static, E: CallExecutor + 'static, + Api: ApiClient, { - use runtime_primitives::traits::Zero; - let authority_set = match client.backend().get_aux(AUTHORITY_SET_KEY)? { None => { info!(target: "afg", "Loading GRANDPA authorities \ @@ -784,8 +817,7 @@ pub fn block_import(client: Arc>) // no authority set on disk: fetch authorities from genesis state. // if genesis state is not available, we may be a light client, but these // are unsupported for following GRANDPA directly. - let genesis_authorities: Vec<(AuthorityId, u64)> = client - .call_api_at(&BlockId::Number(NumberFor::::zero()), fg_primitives::AUTHORITIES_CALL, &())?; + let genesis_authorities: Vec<(AuthorityId, u64)> = api_client.genesis_authorities()?; let authority_set = SharedAuthoritySet::genesis(genesis_authorities); let encoded = authority_set.inner().read().encode(); @@ -801,7 +833,7 @@ pub fn block_import(client: Arc>) }; Ok(( - GrandpaBlockImport { inner: client.clone(), authority_set: authority_set.clone() }, + GrandpaBlockImport { inner: client.clone(), authority_set: authority_set.clone(), api_client }, LinkHalf { client, authority_set }, )) } diff --git a/substrate/core/finality-grandpa/src/tests.rs b/substrate/core/finality-grandpa/src/tests.rs index 7b3c6a39bf..adc1646413 100644 --- a/substrate/core/finality-grandpa/src/tests.rs +++ b/substrate/core/finality-grandpa/src/tests.rs @@ -6,16 +6,34 @@ use parking_lot::Mutex; use tokio::runtime::current_thread; use keyring::Keyring; use client::BlockchainEvents; -use test_client; +use test_client::{self, runtime::BlockNumber}; type PeerData = Mutex>>; type GrandpaPeer = Peer; struct GrandpaTestNet { peers: Vec>, + test_config: TestApi, started: bool } +impl GrandpaTestNet { + fn new(test_config: TestApi, n_peers: usize) -> Self { + let mut net = GrandpaTestNet { + peers: Vec::with_capacity(n_peers), + started: false, + test_config, + }; + let config = Self::default_config(); + + for _ in 0..n_peers { + net.add_peer(&config); + } + + net + } +} + impl TestNetFactory for GrandpaTestNet { type Verifier = PassThroughVerifier; type PeerData = PeerData; @@ -24,6 +42,7 @@ impl TestNetFactory for GrandpaTestNet { fn from_config(_config: &ProtocolConfig) -> Self { GrandpaTestNet { peers: Vec::new(), + test_config: Default::default(), started: false } } @@ -37,7 +56,7 @@ impl TestNetFactory for GrandpaTestNet { fn make_block_import(&self, client: Arc) -> (Arc + Send + Sync>, PeerData) { - let (import, link) = block_import(client).expect("Could not create block import for fresh peer."); + let (import, link) = block_import(client, self.test_config.clone()).expect("Could not create block import for fresh peer."); (Arc::new(import), Mutex::new(Some(link))) } @@ -113,43 +132,74 @@ impl Network for MessageRouting { } } +#[derive(Default, Clone)] +struct TestApi { + genesis_authorities: Vec<(AuthorityId, u64)>, + scheduled_changes: HashMap>, +} + +impl TestApi { + fn new(genesis_authorities: Vec<(AuthorityId, u64)>) -> Self { + TestApi { + genesis_authorities, + scheduled_changes: HashMap::new(), + } + } +} + +impl ApiClient for TestApi { + fn genesis_authorities(&self) -> Result, ClientError> { + Ok(self.genesis_authorities.clone()) + } + + fn scheduled_change(&self, header: &::Header) + -> Result>>, ClientError> + { + // we take only scheduled changes at given block number where there are no + // extrinsics. + Ok(self.scheduled_changes.get(header.number()).map(|c| c.clone())) + } +} + const TEST_GOSSIP_DURATION: Duration = Duration::from_millis(500); const TEST_ROUTING_INTERVAL: Duration = Duration::from_millis(50); #[test] -fn finalize_20_unanimous_3_peers() { - let mut net = GrandpaTestNet::new(3); +fn finalize_3_voters_no_observers() { + let peers = &[Keyring::Alice, Keyring::Bob, Keyring::Charlie]; + let voters: Vec<_> = peers.iter() + .map(|key| AuthorityId(key.to_raw_public())) + .map(|id| (id, 1)) + .collect(); + + let mut net = GrandpaTestNet::new(TestApi::new(voters), 3); net.peer(0).push_blocks(20, false); net.sync(); - let net = Arc::new(Mutex::new(net)); - let peers = &[ - (0, Keyring::Alice), - (1, Keyring::Bob), - (2, Keyring::Charlie), - ]; + for i in 0..3 { + assert_eq!(net.peer(i).client().info().unwrap().chain.best_number, 20, + "Peer #{} failed to sync", i); + } - let voters: Vec<_> = peers.iter() - .map(|&(_, ref key)| AuthorityId(key.to_raw_public())) - .collect(); + let net = Arc::new(Mutex::new(net)); let mut finality_notifications = Vec::new(); - let mut runtime = current_thread::Runtime::new().unwrap(); - for (peer_id, key) in peers { + + for (peer_id, key) in peers.iter().enumerate() { let (client, link) = { let mut net = net.lock(); // temporary needed for some reason - let link = net.peers[*peer_id].data.lock().take().expect("link initialized at startup; qed"); + let link = net.peers[peer_id].data.lock().take().expect("link initialized at startup; qed"); ( - net.peers[*peer_id].client().clone(), + net.peers[peer_id].client().clone(), link, ) }; finality_notifications.push( client.finality_notification_stream() .take_while(|n| Ok(n.header.number() < &20)) - .for_each(move |_| Ok(())) + .for_each(|_| Ok(())) ); let voter = run_grandpa( Config { @@ -157,7 +207,7 @@ fn finalize_20_unanimous_3_peers() { local_key: Some(Arc::new(key.clone().into())), }, link, - MessageRouting::new(net.clone(), *peer_id), + MessageRouting::new(net.clone(), peer_id), ).expect("all in order with client and network"); runtime.spawn(voter); @@ -177,31 +227,27 @@ fn finalize_20_unanimous_3_peers() { } #[test] -fn observer_can_finalize() { - let mut net = GrandpaTestNet::new(4); +fn finalize_3_voters_1_observer() { + let peers = &[Keyring::Alice, Keyring::Bob, Keyring::Charlie]; + let voters: Vec<_> = peers.iter() + .map(|key| AuthorityId(key.to_raw_public())) + .map(|id| (id, 1)) + .collect(); + + let mut net = GrandpaTestNet::new(TestApi::new(voters), 4); net.peer(0).push_blocks(20, false); net.sync(); let net = Arc::new(Mutex::new(net)); - let peers = &[ - (0, Keyring::Alice), - (1, Keyring::Bob), - (2, Keyring::Charlie), - ]; - - let voters: HashMap<_, _> = peers.iter() - .map(|&(_, ref key)| (AuthorityId(key.to_raw_public()), 1)) - .collect(); - let mut finality_notifications = Vec::new(); let mut runtime = current_thread::Runtime::new().unwrap(); let all_peers = peers.iter() .cloned() - .map(|(id, key)| (id, Some(Arc::new(key.into())))) - .chain(::std::iter::once((3, None))); + .map(|key| Some(Arc::new(key.into()))) + .chain(::std::iter::once(None)); - for (peer_id, local_key) in all_peers { + for (peer_id, local_key) in all_peers.enumerate() { let (client, link) = { let mut net = net.lock(); let link = net.peers[peer_id].data.lock().take().expect("link initialized at startup; qed"); diff --git a/substrate/core/network/src/import_queue.rs b/substrate/core/network/src/import_queue.rs index 42fcb1b6d6..96496e33ad 100644 --- a/substrate/core/network/src/import_queue.rs +++ b/substrate/core/network/src/import_queue.rs @@ -268,6 +268,11 @@ pub trait Link: Send { fn restart(&self) { } } +/// A link implementation that does nothing. +pub struct NoopLink; + +impl Link for NoopLink { } + /// A link implementation that connects to the network. pub struct NetworkLink> { /// The chain-sync handle @@ -340,9 +345,9 @@ enum BlockImportError { } /// Import a bunch of blocks. -fn import_many_blocks<'a, B: BlockT, L: Link, V: Verifier>( +fn import_many_blocks<'a, B: BlockT, V: Verifier>( import_handle: &BlockImport, - link: &L, + link: &Link, qdata: Option<&AsyncImportQueueData>, blocks: (BlockOrigin, Vec>), verifier: Arc @@ -460,7 +465,7 @@ fn import_single_block>( } /// Process single block import result. -fn process_import_result<'a, B: BlockT>( +fn process_import_result( link: &Link, result: Result::Header as HeaderT>::Number>, BlockImportError> ) -> usize @@ -568,15 +573,30 @@ pub struct SyncImportQueue> { } #[cfg(any(test, feature = "test-helpers"))] -impl> SyncImportQueue { +impl> SyncImportQueue { /// Create a new SyncImportQueue wrapping the given Verifier and block import /// handle. pub fn new(verifier: Arc, block_import: SharedBlockImport) -> Self { - SyncImportQueue { + let queue = SyncImportQueue { verifier, link: ImportCB::new(), block_import, - } + }; + + let v = queue.verifier.clone(); + let import_handle = queue.block_import.clone(); + queue.link.set(Box::new(move |origin, new_blocks| { + let verifier = v.clone(); + import_many_blocks( + &*import_handle, + &NoopLink, + None, + (origin, new_blocks), + verifier, + ) + })); + + queue } } diff --git a/substrate/core/network/src/test/mod.rs b/substrate/core/network/src/test/mod.rs index ac7968ea97..c5addcd4e1 100644 --- a/substrate/core/network/src/test/mod.rs +++ b/substrate/core/network/src/test/mod.rs @@ -258,7 +258,7 @@ impl, D> Peer { body: Some(block.extrinsics), receipt: None, message_queue: None, - justification: None, + justification: Some(Vec::new()), }, }]); } From c64ff787761f594abcc29b6c02bed3477fcd80c9 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Wed, 31 Oct 2018 12:39:27 +0100 Subject: [PATCH 22/36] initial test to ensure auxiliary data is generated correctly --- .../core/finality-grandpa/src/authorities.rs | 21 ++- substrate/core/finality-grandpa/src/tests.rs | 175 ++++++++++++++++-- substrate/core/network/src/test/mod.rs | 12 +- 3 files changed, 186 insertions(+), 22 deletions(-) diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index 6a32e3ddb7..8541a64123 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -39,11 +39,7 @@ impl SharedAuthoritySet { /// The genesis authority set. pub(crate) fn genesis(initial: Vec<(AuthorityId, u64)>) -> Self { SharedAuthoritySet { - inner: Arc::new(RwLock::new(AuthoritySet { - current_authorities: initial, - set_id: 0, - pending_changes: Vec::new(), - })) + inner: Arc::new(RwLock::new(AuthoritySet::genesis(initial))) } } @@ -91,6 +87,15 @@ pub(crate) struct AuthoritySet { } impl AuthoritySet { + /// Get a genesis set with given authorities. + pub(crate) fn genesis(initial: Vec<(AuthorityId, u64)>) -> Self { + AuthoritySet { + current_authorities: initial, + set_id: 0, + pending_changes: Vec::new(), + } + } + /// Get the current set id and a reference to the current authority set. pub(crate) fn current(&self) -> (u64, &[(AuthorityId, u64)]) { (self.set_id, &self.current_authorities[..]) @@ -113,6 +118,12 @@ impl AuthoritySet self.pending_changes.insert(idx, pending); } + + /// Inspect pending changes. + pub(crate) fn pending_changes(&self) -> &[PendingChange] { + &self.pending_changes + } + /// Get the earliest limit-block number, if any. pub(crate) fn current_limit(&self) -> Option { self.pending_changes.get(0).map(|change| change.effective_number().clone()) diff --git a/substrate/core/finality-grandpa/src/tests.rs b/substrate/core/finality-grandpa/src/tests.rs index adc1646413..252fbcf830 100644 --- a/substrate/core/finality-grandpa/src/tests.rs +++ b/substrate/core/finality-grandpa/src/tests.rs @@ -1,3 +1,21 @@ +// Copyright 2018 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 . + +//! Tests and test helpers for GRANDPA. + use super::*; use network::test::{Block, Hash, TestNetFactory, Peer, PeersClient}; use network::import_queue::{PassThroughVerifier}; @@ -7,6 +25,10 @@ use tokio::runtime::current_thread; use keyring::Keyring; use client::BlockchainEvents; use test_client::{self, runtime::BlockNumber}; +use codec::Decode; +use consensus_common::BlockOrigin; + +use authorities::AuthoritySet; type PeerData = Mutex>>; type GrandpaPeer = Peer; @@ -135,14 +157,14 @@ impl Network for MessageRouting { #[derive(Default, Clone)] struct TestApi { genesis_authorities: Vec<(AuthorityId, u64)>, - scheduled_changes: HashMap>, + scheduled_changes: Arc>>>, } impl TestApi { fn new(genesis_authorities: Vec<(AuthorityId, u64)>) -> Self { TestApi { genesis_authorities, - scheduled_changes: HashMap::new(), + scheduled_changes: Arc::new(Mutex::new(HashMap::new())), } } } @@ -157,20 +179,29 @@ impl ApiClient for TestApi { { // we take only scheduled changes at given block number where there are no // extrinsics. - Ok(self.scheduled_changes.get(header.number()).map(|c| c.clone())) + let change = self.scheduled_changes.lock().get(&header.hash()).map(|c| c.clone()); + if change.is_some() { + println!("Found transition for {:?}", header.hash()); + } + + Ok(change) } } const TEST_GOSSIP_DURATION: Duration = Duration::from_millis(500); const TEST_ROUTING_INTERVAL: Duration = Duration::from_millis(50); +fn make_ids(keys: &[Keyring]) -> Vec<(AuthorityId, u64)> { + keys.iter() + .map(|key| AuthorityId(key.to_raw_public())) + .map(|id| (id, 1)) + .collect() +} + #[test] fn finalize_3_voters_no_observers() { let peers = &[Keyring::Alice, Keyring::Bob, Keyring::Charlie]; - let voters: Vec<_> = peers.iter() - .map(|key| AuthorityId(key.to_raw_public())) - .map(|id| (id, 1)) - .collect(); + let voters = make_ids(peers); let mut net = GrandpaTestNet::new(TestApi::new(voters), 3); net.peer(0).push_blocks(20, false); @@ -229,10 +260,7 @@ fn finalize_3_voters_no_observers() { #[test] fn finalize_3_voters_1_observer() { let peers = &[Keyring::Alice, Keyring::Bob, Keyring::Charlie]; - let voters: Vec<_> = peers.iter() - .map(|key| AuthorityId(key.to_raw_public())) - .map(|id| (id, 1)) - .collect(); + let voters = make_ids(peers); let mut net = GrandpaTestNet::new(TestApi::new(voters), 4); net.peer(0).push_blocks(20, false); @@ -285,3 +313,128 @@ fn finalize_3_voters_1_observer() { runtime.block_on(wait_for.select(drive_to_completion).map_err(|_| ())).unwrap(); } + +#[test] +fn transition_3_voters_twice_1_observer() { + let peers_a = &[ + Keyring::Alice, + Keyring::Bob, + Keyring::Charlie, + ]; + + let peers_b = &[ + Keyring::Dave, + Keyring::Eve, + Keyring::Ferdie, + ]; + + let peers_c = &[ + Keyring::Alice, + Keyring::Eve, + Keyring::Two, + ]; + + let observer = &[Keyring::One]; + + let genesis_voters = make_ids(peers_a); + + let api = TestApi::new(genesis_voters); + let transitions = api.scheduled_changes.clone(); + let add_transition = move |hash, change| { + transitions.lock().insert(hash, change); + }; + + let mut net = GrandpaTestNet::new(api, 8); + + // first 20 blocks: transition at 15, applied at 20. + { + net.peer(0).push_blocks(14, false); + net.peer(0).generate_blocks(1, BlockOrigin::File, |builder| { + let block = builder.bake().unwrap(); + println!("Adding transition for {:?}", block.header.hash()); + add_transition(block.header.hash(), ScheduledChange { + next_authorities: make_ids(peers_b), + delay: 4, + }); + + block + }); + net.peer(0).push_blocks(5, false); + } + + // at block 21 we do another transition, but this time instant. + // add more until we have 30. + { + net.peer(0).generate_blocks(1, BlockOrigin::File, |builder| { + let block = builder.bake().unwrap(); + println!("Adding transition for {:?}", block.header.hash()); + add_transition(block.header.hash(), ScheduledChange { + next_authorities: make_ids(peers_c), + delay: 0, + }); + + block + }); + + net.peer(0).push_blocks(9, false); + } + net.sync(); + + for (i, peer) in net.peers().iter().enumerate() { + assert_eq!(peer.client().info().unwrap().chain.best_number, 30, + "Peer #{} failed to sync", i); + + let set_raw = peer.client().backend().get_aux(::AUTHORITY_SET_KEY).unwrap().unwrap(); + let set = AuthoritySet::::decode(&mut &set_raw[..]).unwrap(); + + assert_eq!(set.current(), (0, make_ids(peers_a).as_slice())); + assert_eq!(set.pending_changes().len(), 2); + } + + // let net = Arc::new(Mutex::new(net)); + // let mut finality_notifications = Vec::new(); + + // let mut runtime = current_thread::Runtime::new().unwrap(); + // let all_peers = peers.iter() + // .cloned() + // .map(|key| Some(Arc::new(key.into()))) + // .chain(::std::iter::once(None)); + + // for (peer_id, local_key) in all_peers.enumerate() { + // let (client, link) = { + // let mut net = net.lock(); + // let link = net.peers[peer_id].data.lock().take().expect("link initialized at startup; qed"); + // ( + // net.peers[peer_id].client().clone(), + // link, + // ) + // }; + // finality_notifications.push( + // client.finality_notification_stream() + // .take_while(|n| Ok(n.header.number() < &20)) + // .for_each(move |_| Ok(())) + // ); + // let voter = run_grandpa( + // Config { + // gossip_duration: TEST_GOSSIP_DURATION, + // local_key, + // }, + // link, + // MessageRouting::new(net.clone(), peer_id), + // ).expect("all in order with client and network"); + + // runtime.spawn(voter); + // } + + // // wait for all finalized on each. + // let wait_for = ::futures::future::join_all(finality_notifications) + // .map(|_| ()) + // .map_err(|_| ()); + + // let drive_to_completion = ::tokio::timer::Interval::new_interval(TEST_ROUTING_INTERVAL) + // .for_each(move |_| { net.lock().route_until_complete(); Ok(()) }) + // .map(|_| ()) + // .map_err(|_| ()); + + // runtime.block_on(wait_for.select(drive_to_completion).map_err(|_| ())).unwrap(); +} diff --git a/substrate/core/network/src/test/mod.rs b/substrate/core/network/src/test/mod.rs index c5addcd4e1..f8869fcc81 100644 --- a/substrate/core/network/src/test/mod.rs +++ b/substrate/core/network/src/test/mod.rs @@ -236,14 +236,13 @@ impl, D> Peer { /// Add blocks to the peer -- edit the block before adding pub fn generate_blocks(&self, count: usize, origin: BlockOrigin, mut edit_block: F) - where F: FnMut(&mut BlockBuilder) + where F: FnMut(BlockBuilder) -> Block { use blocks::BlockData; for _ in 0..count { - let mut builder = self.client.new_block().unwrap(); - edit_block(&mut builder); - let block = builder.bake().unwrap(); + let builder = self.client.new_block().unwrap(); + let block = edit_block(builder); let hash = block.header.hash(); trace!("Generating {}, (#{}, parent={})", hash, block.header.number, block.header.parent_hash); let header = block.header.clone(); @@ -269,7 +268,7 @@ impl, D> Peer { pub fn push_blocks(&self, count: usize, with_tx: bool) { let mut nonce = 0; if with_tx { - self.generate_blocks(count, BlockOrigin::File, |builder| { + self.generate_blocks(count, BlockOrigin::File, |mut builder| { let transfer = Transfer { from: Keyring::Alice.to_raw_public().into(), to: Keyring::Alice.to_raw_public().into(), @@ -279,9 +278,10 @@ impl, D> Peer { let signature = Keyring::from_raw_public(transfer.from.0).unwrap().sign(&transfer.encode()).into(); builder.push(Extrinsic { transfer, signature }).unwrap(); nonce = nonce + 1; + builder.bake().unwrap() }); } else { - self.generate_blocks(count, BlockOrigin::File, |_| ()); + self.generate_blocks(count, BlockOrigin::File, |builder| builder.bake().unwrap()); } } From 3f2a7f3cba1816e2f8b03fb016f4dff077c5b643 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Wed, 17 Oct 2018 13:05:30 -0700 Subject: [PATCH 23/36] set aux in block import --- substrate/core/client/src/backend.rs | 3 ++ substrate/core/client/src/in_mem.rs | 37 ++++++++++++++++--- substrate/core/client/src/light/backend.rs | 10 +++++ substrate/core/client/src/light/blockchain.rs | 4 ++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/substrate/core/client/src/backend.rs b/substrate/core/client/src/backend.rs index 615704361c..b994b1307d 100644 --- a/substrate/core/client/src/backend.rs +++ b/substrate/core/client/src/backend.rs @@ -76,6 +76,9 @@ where fn reset_storage, Vec)>>(&mut self, iter: I) -> error::Result<()>; /// Inject changes trie data into the database. fn update_changes_trie(&mut self, update: MemoryDB) -> error::Result<()>; + /// Update auxiliary keys. Values are `None` if should be deleted. + fn set_aux(&mut self, ops: I) -> error::Result<()> + where I: IntoIterator, Option>)>; } /// Client backend. Manages the data layer. diff --git a/substrate/core/client/src/in_mem.rs b/substrate/core/client/src/in_mem.rs index 5548528cc7..88f94dfbed 100644 --- a/substrate/core/client/src/in_mem.rs +++ b/substrate/core/client/src/in_mem.rs @@ -96,6 +96,7 @@ struct BlockchainStorage { genesis_hash: Block::Hash, cht_roots: HashMap, Block::Hash>, leaves: LeafSet>, + aux: HashMap, Vec>, } /// In-memory blockchain. Supports concurrent reads. @@ -144,6 +145,7 @@ impl Blockchain { genesis_hash: Default::default(), cht_roots: HashMap::new(), leaves: LeafSet::new(), + aux: HashMap::new(), })); Blockchain { storage: storage.clone(), @@ -247,6 +249,16 @@ impl Blockchain { self.storage.write().finalized_hash = hash; Ok(()) } + + fn write_aux(&self, ops: Vec<(Vec, Option>)>) { + let mut storage = self.storage.write(); + for (k, v) in ops { + match v { + Some(v) => storage.aux.insert(k, v), + None => storage.aux.remove(&k), + }; + } + } } impl HeaderBackend for Blockchain { @@ -320,6 +332,7 @@ impl light::blockchain::Storage for Blockchain header: Block::Header, authorities: Option>, state: NewBlockState, + aux_ops: Vec<(Vec, Option>)>, ) -> error::Result<()> { let hash = header.hash(); let parent_hash = *header.parent_hash(); @@ -328,6 +341,7 @@ impl light::blockchain::Storage for Blockchain self.cache.insert(parent_hash, authorities); } + self.write_aux(aux_ops); Ok(()) } @@ -356,6 +370,7 @@ pub struct BlockImportOperation { old_state: InMemory, new_state: Option>, changes_trie_update: Option>, + aux: Option, Option>)>>, } impl backend::BlockImportOperation for BlockImportOperation @@ -404,6 +419,13 @@ where self.new_state = Some(InMemory::from(iter.collect::>())); Ok(()) } + + fn set_aux(&mut self, ops: I) -> error::Result<()> + where I: IntoIterator, Option>)> + { + self.aux = Some(ops.into_iter().collect()); + Ok(()) + } } /// In-memory backend. Keeps all states and blocks in memory. Useful for testing. @@ -417,7 +439,6 @@ where states: RwLock>>, changes_trie_storage: InMemoryChangesTrieStorage, blockchain: Blockchain, - aux: RwLock, Vec>>, } impl Backend @@ -433,7 +454,6 @@ where states: RwLock::new(HashMap::new()), changes_trie_storage: InMemoryChangesTrieStorage::new(), blockchain: Blockchain::new(), - aux: RwLock::new(HashMap::new()), } } } @@ -461,6 +481,7 @@ where old_state: state, new_state: None, changes_trie_update: None, + aux: None, }) } @@ -488,6 +509,10 @@ where self.blockchain.cache.insert(parent_hash, operation.pending_authorities); } } + + if let Some(ops) = operation.aux { + self.blockchain.write_aux(ops); + } Ok(()) } @@ -515,18 +540,18 @@ where } fn insert_aux<'a, 'b: 'a, 'c: 'a, I: IntoIterator, D: IntoIterator>(&self, insert: I, delete: D) -> error::Result<()> { - let mut aux = self.aux.write(); + let mut storage = self.blockchain.storage.write(); for (k, v) in insert { - aux.insert(k.to_vec(), v.to_vec()); + storage.aux.insert(k.to_vec(), v.to_vec()); } for k in delete { - aux.remove(*k); + storage.aux.remove(*k); } Ok(()) } fn get_aux(&self, key: &[u8]) -> error::Result>> { - Ok(self.aux.read().get(key).cloned()) + Ok(self.blockchain.storage.read().aux.get(key).cloned()) } } diff --git a/substrate/core/client/src/light/backend.rs b/substrate/core/client/src/light/backend.rs index e83640aa6c..5a1cee15c6 100644 --- a/substrate/core/client/src/light/backend.rs +++ b/substrate/core/client/src/light/backend.rs @@ -45,6 +45,7 @@ pub struct ImportOperation { header: Option, authorities: Option>, leaf_state: NewBlockState, + aux_ops: Vec<(Vec, Option>)>, _phantom: ::std::marker::PhantomData<(S, F)>, } @@ -86,6 +87,7 @@ impl ClientBackend for Backend where header: None, authorities: None, leaf_state: NewBlockState::Normal, + aux_ops: Vec::new(), _phantom: Default::default(), }) } @@ -96,6 +98,7 @@ impl ClientBackend for Backend where header, operation.authorities, operation.leaf_state, + operation.aux_ops, ) } @@ -193,6 +196,13 @@ where // we're not storing anything locally => ignore changes Ok(()) } + + fn set_aux(&mut self, ops: I) -> ClientResult<()> + where I: IntoIterator, Option>)> + { + self.aux_ops = ops.into_iter().collect(); + Ok(()) + } } impl StateBackend for OnDemandState diff --git a/substrate/core/client/src/light/blockchain.rs b/substrate/core/client/src/light/blockchain.rs index 97c20ecc90..2dd7097751 100644 --- a/substrate/core/client/src/light/blockchain.rs +++ b/substrate/core/client/src/light/blockchain.rs @@ -35,11 +35,15 @@ use light::fetcher::{Fetcher, RemoteHeaderRequest}; /// Light client blockchain storage. pub trait Storage: BlockchainHeaderBackend { /// Store new header. Should refuse to revert any finalized blocks. + /// + /// Takes new authorities, the leaf state of the new block, and + /// any auxiliary storage updates to place in the same operation. fn import_header( &self, header: Block::Header, authorities: Option>, state: NewBlockState, + aux_ops: Vec<(Vec, Option>)>, ) -> ClientResult<()>; /// Mark historic header as finalized. From d346b330f93b048f4a32578b5a96f83e6a3283d3 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Wed, 17 Oct 2018 13:27:39 -0700 Subject: [PATCH 24/36] implement aux-storing in client-db --- substrate/core/client/db/src/lib.rs | 21 +++++++++++++++++++++ substrate/core/client/db/src/light.rs | 17 +++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/substrate/core/client/db/src/lib.rs b/substrate/core/client/db/src/lib.rs index 36ab5f4b2a..2d1fe3fd7c 100644 --- a/substrate/core/client/db/src/lib.rs +++ b/substrate/core/client/db/src/lib.rs @@ -277,6 +277,18 @@ pub struct BlockImportOperation { updates: MemoryDB, changes_trie_updates: MemoryDB, pending_block: Option>, + aux_ops: Vec<(Vec, Option>)>, +} + +impl BlockImportOperation { + fn apply_aux(&mut self, transaction: &mut DBTransaction) { + for (key, maybe_val) in self.aux_ops.drain(..) { + match maybe_val { + Some(val) => transaction.put_vec(columns::AUX, &key, val), + None => transaction.delete(columns::AUX, &key), + } + } + } } impl client::backend::BlockImportOperation @@ -326,6 +338,13 @@ where Block: BlockT, self.changes_trie_updates = update; Ok(()) } + + fn set_aux(&mut self, ops: I) -> Result<(), client::error::Error> + where I: IntoIterator, Option>)> + { + self.aux_ops = ops.into_iter().collect(); + Ok(()) + } } struct StorageDb { @@ -581,6 +600,7 @@ impl client::backend::Backend for Backend whe old_state: state, updates: MemoryDB::default(), changes_trie_updates: MemoryDB::default(), + aux_ops: Vec::new(), }) } @@ -588,6 +608,7 @@ impl client::backend::Backend for Backend whe -> Result<(), client::error::Error> { let mut transaction = DBTransaction::new(); + operation.apply_aux(&mut transaction); if let Some(pending_block) = operation.pending_block { let hash = pending_block.header.hash(); diff --git a/substrate/core/client/db/src/light.rs b/substrate/core/client/db/src/light.rs index f336df4d28..eed75f25f8 100644 --- a/substrate/core/client/db/src/light.rs +++ b/substrate/core/client/db/src/light.rs @@ -43,6 +43,7 @@ pub(crate) mod columns { pub const HEADER: Option = Some(2); pub const CACHE: Option = Some(3); pub const CHT: Option = Some(4); + pub const AUX: Option = Some(5); } /// Light blockchain storage. Stores most recent headers + CHTs for older headers. @@ -238,6 +239,7 @@ impl LightBlockchainStorage for LightStorage header: Block::Header, authorities: Option>, leaf_state: NewBlockState, + aux_ops: Vec<(Vec, Option>)>, ) -> ClientResult<()> { let mut transaction = DBTransaction::new(); @@ -253,6 +255,13 @@ impl LightBlockchainStorage for LightStorage ::utils::number_and_hash_to_lookup_key(number, hash) }; + for (key, maybe_val) in aux_ops { + match maybe_val { + Some(val) => transaction.put_vec(columns::AUX, &key, val), + None => transaction.delete(columns::AUX, &key), + } + } + if leaf_state.is_best() { // handle reorg. { @@ -427,7 +436,7 @@ pub(crate) mod tests { ) -> Hash { let header = prepare_header(parent, number, extrinsics_root); let hash = header.hash(); - db.import_header(header, authorities, NewBlockState::Best).unwrap(); + db.import_header(header, authorities, NewBlockState::Best, Vec::new()).unwrap(); hash } @@ -439,7 +448,7 @@ pub(crate) mod tests { ) -> Hash { let header = prepare_header(parent, number, Default::default()); let hash = header.hash(); - db.import_header(header, authorities, NewBlockState::Best).unwrap(); + db.import_header(header, authorities, NewBlockState::Best, Vec::new()).unwrap(); hash } @@ -451,7 +460,7 @@ pub(crate) mod tests { ) -> Hash { let header = prepare_header(parent, number, Default::default()); let hash = header.hash(); - db.import_header(header, authorities, NewBlockState::Final).unwrap(); + db.import_header(header, authorities, NewBlockState::Final, Vec::new()).unwrap(); hash } @@ -463,7 +472,7 @@ pub(crate) mod tests { ) -> Hash { let header = prepare_header(parent, number, Default::default()); let hash = header.hash(); - db.import_header(header, authorities, NewBlockState::Normal).unwrap(); + db.import_header(header, authorities, NewBlockState::Normal, Vec::new()).unwrap(); hash } From 9630f3477536829fdc09f3aba371da140b3799d5 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Wed, 17 Oct 2018 13:34:59 -0700 Subject: [PATCH 25/36] client sets aux in import pipeline --- substrate/core/client/src/client.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/substrate/core/client/src/client.rs b/substrate/core/client/src/client.rs index a82b2133f0..eb9bbd6645 100644 --- a/substrate/core/client/src/client.rs +++ b/substrate/core/client/src/client.rs @@ -503,6 +503,7 @@ impl Client where body: Option>, authorities: Option>, finalized: bool, + aux: Vec<(Vec, Option>)>, ) -> error::Result { let parent_hash = import_headers.post().parent_hash().clone(); match self.backend.blockchain().status(BlockId::Hash(hash))? { @@ -593,6 +594,8 @@ impl Client where if let Some(Some(changes_update)) = changes_update { transaction.update_changes_trie(changes_update)?; } + + transaction.set_aux(aux)?; self.backend.commit_operation(transaction)?; if make_notifications { @@ -916,7 +919,7 @@ impl consensus::BlockImport for Client where post_runtime_digests, body, finalized, - .. + auxiliary, } = import_block; let parent_hash = header.parent_hash().clone(); @@ -948,6 +951,7 @@ impl consensus::BlockImport for Client where body, new_authorities, finalized, + auxiliary, ); *self.importing_block.write() = None; From 6704f15e99183dc9eee00eb8973220b3a7c72ee4 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 1 Nov 2018 20:15:51 +0100 Subject: [PATCH 26/36] finish GRANDPA test: dynamic authority sets --- substrate/Cargo.lock | 1 + substrate/core/finality-grandpa/Cargo.toml | 1 + .../core/finality-grandpa/src/authorities.rs | 1 + substrate/core/finality-grandpa/src/lib.rs | 48 +++++-- substrate/core/finality-grandpa/src/tests.rs | 134 ++++++++++-------- 5 files changed, 120 insertions(+), 65 deletions(-) diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index b14022ea3a..1c3bd04108 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -3033,6 +3033,7 @@ dependencies = [ name = "substrate-finality-grandpa" version = "0.1.0" dependencies = [ + "env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)", "finality-grandpa 0.3.0", "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/substrate/core/finality-grandpa/Cargo.toml b/substrate/core/finality-grandpa/Cargo.toml index fa30e427f8..f570f4c4d8 100644 --- a/substrate/core/finality-grandpa/Cargo.toml +++ b/substrate/core/finality-grandpa/Cargo.toml @@ -25,3 +25,4 @@ features = ["derive-codec"] substrate-network = { path = "../network", features = ["test-helpers"] } substrate-keyring = { path = "../keyring" } substrate-test-client = { path = "../test-client"} +env_logger = "0.5" diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index 8541a64123..79674acefa 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -120,6 +120,7 @@ impl AuthoritySet } /// Inspect pending changes. + #[cfg(test)] pub(crate) fn pending_changes(&self) -> &[PendingChange] { &self.pending_changes } diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index d12b35ea1f..ee493d67e5 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -41,6 +41,9 @@ extern crate substrate_keyring as keyring; #[cfg(test)] extern crate substrate_test_client as test_client; +#[cfg(test)] +extern crate env_logger; + #[macro_use] extern crate parity_codec_derive; @@ -100,6 +103,14 @@ pub struct Config { pub gossip_duration: Duration, /// The local signing key. pub local_key: Option>, + /// Some local identifier of the voter. + pub name: Option, +} + +impl Config { + fn name(&self) -> &str { + self.name.as_ref().map(|s| s.as_str()).unwrap_or("") + } } /// Errors that can occur while voting in GRANDPA. @@ -133,13 +144,13 @@ pub trait Network: Clone { /// Get a stream of messages for a specific round. This stream should /// never logically conclude. - fn messages_for(&self, round: u64) -> Self::In; + fn messages_for(&self, round: u64, set_id: u64) -> Self::In; /// Send a message at a specific round out. - fn send_message(&self, round: u64, message: Vec); + fn send_message(&self, round: u64, set_id: u64, message: Vec); /// Clean up messages for a round. - fn drop_messages(&self, round: u64); + fn drop_messages(&self, round: u64, set_id: u64); } /// Something which can determine if a block is known. @@ -297,6 +308,7 @@ impl, I> Stream for UntilImported { round: u64, + set_id: u64, inner: I, network: N, } @@ -320,7 +332,7 @@ impl Sink for ClearOnDrop { impl Drop for ClearOnDrop { fn drop(&mut self) { - self.network.drop_messages(self.round); + self.network.drop_messages(self.round, self.set_id); } } @@ -406,7 +418,7 @@ fn outgoing_messages( }; // forward to network. - network.send_message(round, signed.encode()); + network.send_message(round, set_id, signed.encode()); Some(signed) } else { None @@ -473,6 +485,8 @@ impl grandpa::Chain> for E // once blocks are finalized that make that transition irrelevant or activate it, // we will proceed onwards. most of the time there will be no pending transition. let limit = self.authority_set.current_limit(); + trace!(target: "afg", "Finding best chain containing block {:?} with number limit {:?}", block, limit); + match self.inner.best_containing(block, limit) { Ok(Some(hash)) => { let header = self.inner.header(&BlockId::Hash(hash)).ok()? @@ -564,7 +578,7 @@ impl voter::Environment> f let incoming = checked_message_stream::( round, self.set_id, - self.network.messages_for(round), + self.network.messages_for(round, self.set_id), self.voters.clone(), ); @@ -585,11 +599,12 @@ impl voter::Environment> f ); // join incoming network messages with locally originating ones. - let incoming = Box::new(incoming.select(out_rx).map_err(Into::into)); + let incoming = Box::new(out_rx.select(incoming).map_err(Into::into)); // schedule network message cleanup when sink drops. let outgoing = Box::new(ClearOnDrop { round, + set_id: self.set_id, network: self.network.clone(), inner: outgoing.sink_map_err(Into::into), }); @@ -604,6 +619,15 @@ impl voter::Environment> f } fn completed(&self, round: u64, state: RoundState>) -> Result<(), Self::Error> { + debug!( + target: "afg", "Voter {} completed round {} in set {}. Estimate = {:?}, Finalized in round = {:?}", + self.config.name(), + round, + self.set_id, + state.estimate.as_ref().map(|e| e.1), + state.finalized.as_ref().map(|e| e.1), + ); + let encoded_state = (round, state).encode(); if let Err(e) = self.inner.backend() .insert_aux(&[(LAST_COMPLETED_KEY, &encoded_state[..])], &[]) @@ -669,6 +693,12 @@ impl voter::Environment> f if let Some((canon_hash, canon_number)) = status.new_set_block { // the authority set has changed. let (new_id, set_ref) = authority_set.current(); + + if set_ref.len() > 16 { + info!("Applying GRANDPA set change to new set with {} authorities", set_ref.len()); + } else { + info!("Applying GRANDPA set change to new set {:?}", set_ref); + } Err(ExitOrError::AuthoritiesChanged(NewAuthoritySet { canon_hash, canon_number, @@ -772,7 +802,7 @@ impl BlockImport for GrandpaBlockImport( let work = future::loop_fn((initial_environment, last_round_number, last_state), move |params| { let (env, last_round_number, last_state) = params; + debug!(target: "afg", "{}: Starting new voter with set ID {}", config.name(), env.set_id); + let chain_info = match client.info() { Ok(i) => i, Err(e) => return future::Either::B(future::err(Error::Client(e))), diff --git a/substrate/core/finality-grandpa/src/tests.rs b/substrate/core/finality-grandpa/src/tests.rs index 252fbcf830..1a47a8fa8b 100644 --- a/substrate/core/finality-grandpa/src/tests.rs +++ b/substrate/core/finality-grandpa/src/tests.rs @@ -27,6 +27,7 @@ use client::BlockchainEvents; use test_client::{self, runtime::BlockNumber}; use codec::Decode; use consensus_common::BlockOrigin; +use std::collections::HashSet; use authorities::AuthoritySet; @@ -69,6 +70,13 @@ impl TestNetFactory for GrandpaTestNet { } } + fn default_config() -> ProtocolConfig { + // the authority role ensures gossip hits all nodes here. + ProtocolConfig { + roles: ::network::Roles::AUTHORITY, + } + } + fn make_verifier(&self, _client: Arc, _cfg: &ProtocolConfig) -> Arc { @@ -118,21 +126,25 @@ impl MessageRouting { } } -fn round_to_topic(round: u64) -> Hash { +fn make_topic(round: u64, set_id: u64) -> Hash { let mut hash = Hash::default(); round.using_encoded(|s| { let raw = hash.as_mut(); raw[..8].copy_from_slice(s); }); + set_id.using_encoded(|s| { + let raw = hash.as_mut(); + raw[8..16].copy_from_slice(s); + }); hash } impl Network for MessageRouting { type In = Box,Error=()>>; - fn messages_for(&self, round: u64) -> Self::In { + fn messages_for(&self, round: u64, set_id: u64) -> Self::In { let messages = self.inner.lock().peer(self.peer_id) - .with_spec(|spec, _| spec.gossip.messages_for(round_to_topic(round))); + .with_spec(|spec, _| spec.gossip.messages_for(make_topic(round, set_id))); let messages = messages.map_err( move |_| panic!("Messages for round {} dropped too early", round) @@ -141,14 +153,14 @@ impl Network for MessageRouting { Box::new(messages) } - fn send_message(&self, round: u64, message: Vec) { + fn send_message(&self, round: u64, set_id: u64, message: Vec) { let mut inner = self.inner.lock(); - inner.peer(self.peer_id).gossip_message(round_to_topic(round), message); - inner.route(); + inner.peer(self.peer_id).gossip_message(make_topic(round, set_id), message); + inner.route_until_complete(); } - fn drop_messages(&self, round: u64) { - let topic = round_to_topic(round); + fn drop_messages(&self, round: u64, set_id: u64) { + let topic = make_topic(round, set_id); self.inner.lock().peer(self.peer_id) .with_spec(|spec, _| spec.gossip.collect_garbage(|t| t == &topic)); } @@ -179,12 +191,7 @@ impl ApiClient for TestApi { { // we take only scheduled changes at given block number where there are no // extrinsics. - let change = self.scheduled_changes.lock().get(&header.hash()).map(|c| c.clone()); - if change.is_some() { - println!("Found transition for {:?}", header.hash()); - } - - Ok(change) + Ok(self.scheduled_changes.lock().get(&header.hash()).map(|c| c.clone())) } } @@ -236,6 +243,7 @@ fn finalize_3_voters_no_observers() { Config { gossip_duration: TEST_GOSSIP_DURATION, local_key: Some(Arc::new(key.clone().into())), + name: Some(format!("peer#{}", peer_id)), }, link, MessageRouting::new(net.clone(), peer_id), @@ -293,6 +301,7 @@ fn finalize_3_voters_1_observer() { Config { gossip_duration: TEST_GOSSIP_DURATION, local_key, + name: Some(format!("peer#{}", peer_id)), }, link, MessageRouting::new(net.clone(), peer_id), @@ -344,14 +353,13 @@ fn transition_3_voters_twice_1_observer() { transitions.lock().insert(hash, change); }; - let mut net = GrandpaTestNet::new(api, 8); + let mut net = GrandpaTestNet::new(api, 9); // first 20 blocks: transition at 15, applied at 20. { net.peer(0).push_blocks(14, false); net.peer(0).generate_blocks(1, BlockOrigin::File, |builder| { let block = builder.bake().unwrap(); - println!("Adding transition for {:?}", block.header.hash()); add_transition(block.header.hash(), ScheduledChange { next_authorities: make_ids(peers_b), delay: 4, @@ -367,7 +375,6 @@ fn transition_3_voters_twice_1_observer() { { net.peer(0).generate_blocks(1, BlockOrigin::File, |builder| { let block = builder.bake().unwrap(); - println!("Adding transition for {:?}", block.header.hash()); add_transition(block.header.hash(), ScheduledChange { next_authorities: make_ids(peers_c), delay: 0, @@ -391,50 +398,63 @@ fn transition_3_voters_twice_1_observer() { assert_eq!(set.pending_changes().len(), 2); } - // let net = Arc::new(Mutex::new(net)); - // let mut finality_notifications = Vec::new(); + let net = Arc::new(Mutex::new(net)); + let mut finality_notifications = Vec::new(); - // let mut runtime = current_thread::Runtime::new().unwrap(); - // let all_peers = peers.iter() - // .cloned() - // .map(|key| Some(Arc::new(key.into()))) - // .chain(::std::iter::once(None)); + let mut runtime = current_thread::Runtime::new().unwrap(); + let all_peers = peers_a.iter() + .chain(peers_b) + .chain(peers_c) + .chain(observer) + .cloned() + .collect::>() // deduplicate + .into_iter() + .map(|key| Some(Arc::new(key.into()))) + .enumerate(); - // for (peer_id, local_key) in all_peers.enumerate() { - // let (client, link) = { - // let mut net = net.lock(); - // let link = net.peers[peer_id].data.lock().take().expect("link initialized at startup; qed"); - // ( - // net.peers[peer_id].client().clone(), - // link, - // ) - // }; - // finality_notifications.push( - // client.finality_notification_stream() - // .take_while(|n| Ok(n.header.number() < &20)) - // .for_each(move |_| Ok(())) - // ); - // let voter = run_grandpa( - // Config { - // gossip_duration: TEST_GOSSIP_DURATION, - // local_key, - // }, - // link, - // MessageRouting::new(net.clone(), peer_id), - // ).expect("all in order with client and network"); + for (peer_id, local_key) in all_peers { + let (client, link) = { + let mut net = net.lock(); + let link = net.peers[peer_id].data.lock().take().expect("link initialized at startup; qed"); + ( + net.peers[peer_id].client().clone(), + link, + ) + }; + finality_notifications.push( + client.finality_notification_stream() + .take_while(|n| Ok(n.header.number() < &30)) + .for_each(move |_| Ok(())) + .map(move |()| { + let set_raw = client.backend().get_aux(::AUTHORITY_SET_KEY).unwrap().unwrap(); + let set = AuthoritySet::::decode(&mut &set_raw[..]).unwrap(); - // runtime.spawn(voter); - // } + assert_eq!(set.current(), (2, make_ids(peers_c).as_slice())); + assert!(set.pending_changes().is_empty()); + }) + ); + let voter = run_grandpa( + Config { + gossip_duration: TEST_GOSSIP_DURATION, + local_key, + name: Some(format!("peer#{}", peer_id)), + }, + link, + MessageRouting::new(net.clone(), peer_id), + ).expect("all in order with client and network"); - // // wait for all finalized on each. - // let wait_for = ::futures::future::join_all(finality_notifications) - // .map(|_| ()) - // .map_err(|_| ()); + runtime.spawn(voter); + } - // let drive_to_completion = ::tokio::timer::Interval::new_interval(TEST_ROUTING_INTERVAL) - // .for_each(move |_| { net.lock().route_until_complete(); Ok(()) }) - // .map(|_| ()) - // .map_err(|_| ()); + // wait for all finalized on each. + let wait_for = ::futures::future::join_all(finality_notifications) + .map(|_| ()) + .map_err(|_| ()); - // runtime.block_on(wait_for.select(drive_to_completion).map_err(|_| ())).unwrap(); + let drive_to_completion = ::tokio::timer::Interval::new_interval(TEST_ROUTING_INTERVAL) + .for_each(move |_| { net.lock().route_until_complete(); Ok(()) }) + .map(|_| ()) + .map_err(|_| ()); + + runtime.block_on(wait_for.select(drive_to_completion).map_err(|_| ())).unwrap(); } From 5b2a93821c44f54dd9427bd1bbe59cd4c6a5cdf3 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Thu, 1 Nov 2018 20:25:59 +0100 Subject: [PATCH 27/36] use published finality-grandpa --- substrate/Cargo.lock | 4 +++- substrate/core/finality-grandpa/Cargo.toml | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 1c3bd04108..d2db5f91f6 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -581,6 +581,7 @@ dependencies = [ [[package]] name = "finality-grandpa" version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3034,7 +3035,7 @@ name = "substrate-finality-grandpa" version = "0.1.0" dependencies = [ "env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)", - "finality-grandpa 0.3.0", + "finality-grandpa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4202,6 +4203,7 @@ dependencies = [ "checksum failure_derive 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "946d0e98a50d9831f5d589038d2ca7f8f455b1c21028c0db0e84116a12696426" "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" "checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa" +"checksum finality-grandpa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "be6d2735e8f570474c7925a60ebe04ec0bdd9eea7cc4fddab78a0ecfdefec20e" "checksum fixed-hash 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d5ec8112f00ea8a483e04748a85522184418fd1cf02890b626d8fc28683f7de" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" diff --git a/substrate/core/finality-grandpa/Cargo.toml b/substrate/core/finality-grandpa/Cargo.toml index f570f4c4d8..a4b20e5177 100644 --- a/substrate/core/finality-grandpa/Cargo.toml +++ b/substrate/core/finality-grandpa/Cargo.toml @@ -17,8 +17,7 @@ tokio = "0.1.7" substrate-fg-primitives = { path = "primitives" } [dependencies.finality-grandpa] -#version = "0.3.0" -path = "../../../finality-afg" +version = "0.3.0" features = ["derive-codec"] [dev-dependencies] From ebeacee7d4e1d3b8d14202926b41cc4185618576 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Fri, 2 Nov 2018 18:17:57 +0100 Subject: [PATCH 28/36] all tests compile --- substrate/core/consensus/aura/src/lib.rs | 8 ++++---- substrate/core/network/src/import_queue.rs | 4 +--- substrate/core/network/src/test/sync.rs | 2 +- substrate/core/service/src/chain_ops.rs | 10 +++------- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/substrate/core/consensus/aura/src/lib.rs b/substrate/core/consensus/aura/src/lib.rs index cb4a24de3e..c57161668d 100644 --- a/substrate/core/consensus/aura/src/lib.rs +++ b/substrate/core/consensus/aura/src/lib.rs @@ -404,11 +404,11 @@ pub type AuraImportQueue = BasicQueue>; /// Start an import queue for the Aura consensus algorithm. pub fn import_queue(config: Config, client: Arc) -> AuraImportQueue where B: Block, - C: Authorities + BlockImport + Send + Sync, + C: Authorities + BlockImport + Send + Sync, DigestItemFor: CompatibleDigestItem, { - let verifier = Arc::new(AuraVerifier { config, client }); - BasicQueue::new(verifier) + let verifier = Arc::new(AuraVerifier { config, client: client.clone() }); + BasicQueue::new(verifier, client) } @@ -458,7 +458,7 @@ mod tests { const TEST_ROUTING_INTERVAL: Duration = Duration::from_millis(50); pub struct AuraTestNet { - peers: Vec>>>, + peers: Vec, ()>>>, started: bool } diff --git a/substrate/core/network/src/import_queue.rs b/substrate/core/network/src/import_queue.rs index 96496e33ad..0d760e0d15 100644 --- a/substrate/core/network/src/import_queue.rs +++ b/substrate/core/network/src/import_queue.rs @@ -652,7 +652,6 @@ pub mod tests { use super::*; struct TestLink { - chain: Arc>, imported: Cell, maintains: Cell, disconnects: Cell, @@ -662,7 +661,6 @@ pub mod tests { impl TestLink { fn new() -> TestLink { TestLink { - chain: Arc::new(test_client::new()), imported: Cell::new(0), maintains: Cell::new(0), disconnects: Cell::new(0), @@ -806,7 +804,7 @@ pub mod tests { #[test] fn async_import_queue_drops() { let verifier = Arc::new(PassThroughVerifier(true)); - let queue = BasicQueue::new(verifier); + let queue = BasicQueue::new(verifier, Arc::new(test_client::new())); queue.start(TestLink::new()).unwrap(); drop(queue); } diff --git a/substrate/core/network/src/test/sync.rs b/substrate/core/network/src/test/sync.rs index 0f9e407828..c31b1c3d5e 100644 --- a/substrate/core/network/src/test/sync.rs +++ b/substrate/core/network/src/test/sync.rs @@ -94,7 +94,7 @@ fn own_blocks_are_announced() { ::env_logger::init().ok(); let mut net = TestNet::new(3); net.sync(); // connect'em - net.peer(0).generate_blocks(1, BlockOrigin::Own, |_| ()); + net.peer(0).generate_blocks(1, BlockOrigin::Own, |builder| builder.bake().unwrap()); net.sync(); assert_eq!(net.peer(0).client.backend().blockchain().info().unwrap().best_number, 1); assert_eq!(net.peer(1).client.backend().blockchain().info().unwrap().best_number, 1); diff --git a/substrate/core/service/src/chain_ops.rs b/substrate/core/service/src/chain_ops.rs index d23e82b1cf..f9b7790863 100644 --- a/substrate/core/service/src/chain_ops.rs +++ b/substrate/core/service/src/chain_ops.rs @@ -88,16 +88,12 @@ pub fn export_blocks(config: FactoryFullConfiguration, exit: E, mut pub fn import_blocks(config: FactoryFullConfiguration, exit: E, mut input: R) -> error::Result<()> where F: ServiceFactory, E: Future + Send + 'static, R: Read, { - use network::ClientHandle; - - struct DummyLink(::std::sync::Arc); - impl> Link for DummyLink { - fn chain(&self) -> &ClientHandle { &*self.0 } - } + struct DummyLink; + impl Link for DummyLink { } let client = new_client::(&config)?; let queue = components::FullComponents::::build_import_queue(&config, client.clone())?; - queue.start(DummyLink(client.clone()))?; + queue.start(DummyLink)?; let (exit_send, exit_recv) = std::sync::mpsc::channel(); ::std::thread::spawn(move || { From dc59431474da59a44cfabedf298646c4add5514e Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 3 Nov 2018 16:45:18 +0100 Subject: [PATCH 29/36] API id module --- substrate/core/finality-grandpa/primitives/src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/substrate/core/finality-grandpa/primitives/src/lib.rs b/substrate/core/finality-grandpa/primitives/src/lib.rs index 649d424e6b..21fd9c6e3b 100644 --- a/substrate/core/finality-grandpa/primitives/src/lib.rs +++ b/substrate/core/finality-grandpa/primitives/src/lib.rs @@ -46,6 +46,14 @@ pub const PENDING_CHANGE_CALL: &str = "grandpa_pending_change"; /// WASM function call to get current GRANDPA authorities. pub const AUTHORITIES_CALL: &str = "grandpa_authorities"; +/// The ApiIds for GRANDPA API. +pub mod id { + use sr_api::ApiId; + + /// ApiId for the GrandpaApi trait. + pub const GRANDPA_API: ApiId = *b"fgrandpa"; +} + decl_apis! { /// APIs for integrating the GRANDPA finality gadget into runtimes. /// @@ -55,7 +63,7 @@ decl_apis! { /// applied in the runtime after those N blocks have passed. /// /// The consensus protocol will coordinate the handoff externally. - pub trait Api { + pub trait GrandpaApi { /// Check a digest for pending changes. /// Return `None` if there are no pending changes. /// From abfc4a24a91c25ade4ee9f93e4a14ae673d25f7c Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 3 Nov 2018 16:47:38 +0100 Subject: [PATCH 30/36] ignore test temporarily --- substrate/core/network/src/test/sync.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/substrate/core/network/src/test/sync.rs b/substrate/core/network/src/test/sync.rs index c31b1c3d5e..6b1af9a6af 100644 --- a/substrate/core/network/src/test/sync.rs +++ b/substrate/core/network/src/test/sync.rs @@ -90,6 +90,7 @@ fn sync_after_fork_works() { } #[test] +#[ignore] // reinstate when import queue handlers dispatch announce messages. fn own_blocks_are_announced() { ::env_logger::init().ok(); let mut net = TestNet::new(3); From ddd46db4d506a342a5733593e6952257436eb1a3 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 3 Nov 2018 17:08:47 +0100 Subject: [PATCH 31/36] fix test --- substrate/core/network/src/test/mod.rs | 5 +++++ substrate/core/network/src/test/sync.rs | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/substrate/core/network/src/test/mod.rs b/substrate/core/network/src/test/mod.rs index f8869fcc81..d3975e032b 100644 --- a/substrate/core/network/src/test/mod.rs +++ b/substrate/core/network/src/test/mod.rs @@ -194,6 +194,11 @@ impl, D> Peer { io.to_disconnect.clone() } + fn with_io<'a, F, U>(&'a self, f: F) -> U where F: FnOnce(&mut TestIo<'a>) -> U { + let mut io = TestIo::new(&self.queue, None); + f(&mut io) + } + /// Produce the next pending message to send to another peer. fn pending_message(&self) -> Option { self.flush(); diff --git a/substrate/core/network/src/test/sync.rs b/substrate/core/network/src/test/sync.rs index 6b1af9a6af..4d714eb828 100644 --- a/substrate/core/network/src/test/sync.rs +++ b/substrate/core/network/src/test/sync.rs @@ -90,12 +90,14 @@ fn sync_after_fork_works() { } #[test] -#[ignore] // reinstate when import queue handlers dispatch announce messages. fn own_blocks_are_announced() { ::env_logger::init().ok(); let mut net = TestNet::new(3); net.sync(); // connect'em net.peer(0).generate_blocks(1, BlockOrigin::Own, |builder| builder.bake().unwrap()); + + let header = net.peer(0).client().header(&BlockId::Number(1)).unwrap().unwrap(); + net.peer(0).with_io(|io| net.peer(0).sync.on_block_imported(io, header.hash(), &header)); net.sync(); assert_eq!(net.peer(0).client.backend().blockchain().info().unwrap().best_number, 1); assert_eq!(net.peer(1).client.backend().blockchain().info().unwrap().best_number, 1); From 3968553f8c7f8f11658df1e23c3d6b61312184c3 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 5 Nov 2018 10:47:34 +0100 Subject: [PATCH 32/36] re-export ApiId from sr-api --- substrate/core/sr-api/src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/substrate/core/sr-api/src/lib.rs b/substrate/core/sr-api/src/lib.rs index 7fffc43590..ed148c0030 100644 --- a/substrate/core/sr-api/src/lib.rs +++ b/substrate/core/sr-api/src/lib.rs @@ -25,16 +25,18 @@ extern crate sr_primitives as primitives; #[doc(hidden)] pub extern crate parity_codec as codec; extern crate sr_version as runtime_version; +use rstd::vec::Vec; #[doc(hidden)] pub use primitives::{traits::Block as BlockT, generic::BlockId, transaction_validity::TransactionValidity, ApplyResult}; -use runtime_version::{ApiId, RuntimeVersion}; -use rstd::vec::Vec; #[doc(hidden)] pub use rstd::slice; #[doc(hidden)] pub use codec::{Encode, Decode}; +// these are part of the public API, so need to be re-exported +pub use runtime_version::{ApiId, RuntimeVersion}; + /// Declare the given API traits. /// /// # Example: @@ -432,7 +434,7 @@ macro_rules! decl_apis { /// The ApiIds for the various standard runtime APIs. pub mod id { use super::ApiId; - + /// ApiId for the BlockBuilder trait. pub const BLOCK_BUILDER: ApiId = *b"blkbuild"; From 876969d6f032be92d195a956ddc0dfd36a40535b Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 5 Nov 2018 11:27:01 +0100 Subject: [PATCH 33/36] add some docs --- substrate/core/finality-grandpa/src/lib.rs | 34 +++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index cc3d6b0349..0ea5d782b0 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -16,7 +16,39 @@ //! Integration of the GRANDPA finality gadget into substrate. //! -//! This is a long-running future that produces finality notifications. +//! This crate provides a long-running future that produces finality notifications. +//! +//! # Usage +//! +//! First, create a block-import wrapper with the `block_import` function. +//! The GRANDPA worker needs to be linked together with this block import object, +//! so a `LinkHalf` is returned as well. All blocks imported (from network or consensus or otherwise) +//! must pass through this wrapper, otherwise consensus is likely to break in +//! unexpected ways. +//! +//! Next, use the `LinkHalf` and a local configuration to `run_grandpa`. This requires a +//! `Network` implementation. The returned future should be driven to completion and +//! will finalize blocks in the background. +//! +//! # Changing authority sets +//! +//! The rough idea behind changing authority sets in GRANDPA is that at some point, +//! we obtain agreement for some maximum block height that the current set can +//! finalize, and once a block with that height is finalized the next set will +//! pick up finalization from there. +//! +//! Technically speaking, this would be implemented as a voting rule which says, +//! "if there is a signal for a change in N blocks in block B, only vote on +//! chains with length NUM(B) + N if they contain B". This conditional-inclusion +//! logic is complex to compute because it requires looking arbitrarily far +//! back in the chain. +//! +//! Instead, we keep track of a list of all signals we've seen so far, +//! sorted ascending by the block number they would be applied at. We never vote +//! on chains with number higher than the earliest handoff block number +//! (this is num(signal) + N). When finalizing a block, we either apply or prune +//! any signaled changes based on whether the signaling block is included in the +//! newly-finalized chain. extern crate finality_grandpa as grandpa; extern crate futures; From 7e088f6342abd561fa535f9c55fd0d1c00e5bb93 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Wed, 14 Nov 2018 14:34:32 +0100 Subject: [PATCH 34/36] address grumbles --- substrate/core/finality-grandpa/src/authorities.rs | 2 +- substrate/core/finality-grandpa/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/substrate/core/finality-grandpa/src/authorities.rs b/substrate/core/finality-grandpa/src/authorities.rs index 79674acefa..c97769cb43 100644 --- a/substrate/core/finality-grandpa/src/authorities.rs +++ b/substrate/core/finality-grandpa/src/authorities.rs @@ -73,7 +73,7 @@ impl From> for SharedAuthoritySet { pub(crate) struct Status { /// Whether internal changes were made. pub(crate) changed: bool, - /// `Some` when underlying authority set has changed, containign the + /// `Some` when underlying authority set has changed, containing the /// block where that set changed. pub(crate) new_set_block: Option<(H, N)>, } diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 63851b1e4b..ba23383db2 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -798,7 +798,7 @@ impl, RA> ApiClient for Arc Date: Thu, 15 Nov 2018 16:36:28 +0100 Subject: [PATCH 35/36] start addressing basti comments --- substrate/core/client/src/client.rs | 3 +- .../finality-grandpa/primitives/src/lib.rs | 95 +++++++++++++++---- substrate/core/finality-grandpa/src/lib.rs | 55 +++-------- 3 files changed, 88 insertions(+), 65 deletions(-) diff --git a/substrate/core/client/src/client.rs b/substrate/core/client/src/client.rs index 4d4a438a08..f889a7045f 100644 --- a/substrate/core/client/src/client.rs +++ b/substrate/core/client/src/client.rs @@ -916,7 +916,8 @@ impl CallApiAt for Client where B: backend::Backend, E: CallExecutor + Clone + Send + Sync, Block: BlockT, - RA: Send + Sync, + RA: CoreAPI, // not strictly necessary at the moment + // but we want to bound to make sure the API is actually available. { fn call_api_at( &self, diff --git a/substrate/core/finality-grandpa/primitives/src/lib.rs b/substrate/core/finality-grandpa/primitives/src/lib.rs index 2f53523555..ed8ae408b7 100644 --- a/substrate/core/finality-grandpa/primitives/src/lib.rs +++ b/substrate/core/finality-grandpa/primitives/src/lib.rs @@ -54,27 +54,82 @@ pub mod id { pub const GRANDPA_API: ApiId = *b"fgrandpa"; } -/// APIs for integrating the GRANDPA finality gadget into runtimes. -/// This should be implemented on the runtime side. -/// -/// This is primarily used for negotiating authority-set changes for the -/// gadget. GRANDPA uses a signalling model of changing authority sets: -/// changes should be signalled with a delay of N blocks, and then automatically -/// applied in the runtime after those N blocks have passed. -/// -/// The consensus protocol will coordinate the handoff externally. -pub trait GrandpaApi { - /// Check a digest for pending changes. - /// Return `None` if there are no pending changes. +decl_runtime_apis! { + /// APIs for integrating the GRANDPA finality gadget into runtimes. + /// This should be implemented on the runtime side. /// - /// Precedence towards earlier or later digest items can be given - /// based on the rules of the chain. + /// This is primarily used for negotiating authority-set changes for the + /// gadget. GRANDPA uses a signalling model of changing authority sets: + /// changes should be signalled with a delay of N blocks, and then automatically + /// applied in the runtime after those N blocks have passed. /// - /// No change should be scheduled if one is already and the delay has not - /// passed completely. - fn grandpa_pending_change(digest: DigestFor) -> Option>>; + /// The consensus protocol will coordinate the handoff externally. + pub trait GrandpaApi { + /// Check a digest for pending changes. + /// Return `None` if there are no pending changes. + /// + /// Precedence towards earlier or later digest items can be given + /// based on the rules of the chain. + /// + /// No change should be scheduled if one is already and the delay has not + /// passed completely. + fn grandpa_pending_change(digest: DigestFor) + -> Option>>; - /// Get the current GRANDPA authorities and weights. This should not change except - /// for when changes are scheduled and the corresponding delay has passed. - fn grandpa_authorities() -> Vec<(AuthorityId, u64)>; + /// Get the current GRANDPA authorities and weights. This should not change except + /// for when changes are scheduled and the corresponding delay has passed. + fn grandpa_authorities() -> Vec<(AuthorityId, u64)>; + } +} + +#[cfg(feature = "std")] +mod implementation { + use super::{GrandpaApi, ScheduledChange}; + use sr_primitives::traits::{Block as BlockT, DigestFor, NumberFor}; + use sr_primitives::generic::BlockId; + use parity_codec::{Encode, Decode}; + use client::{Client, error::Error as ClientError, backend::Backend, CallExecutor}; + use client::runtime_api::{CallApiAt, Core as CoreAPI}; + use substrate_primitives::{AuthorityId, H256, Blake2Hasher}; + + // TODO [basti]: do this implementation in runtime. + impl, RA> GrandpaApi for Client where + B: Backend + 'static, + E: CallExecutor + 'static + Clone + Send + Sync, + DigestFor: Encode, + RA: CoreAPI, + { + fn grandpa_authorities(&self, at: &BlockId) -> Result, ClientError> { + let raw = self.call_api_at( + &at, + ::AUTHORITIES_CALL, + Encode::encode(&()), + &mut Default::default(), + &mut None, + ); + + // TODO [basti]: implement this in runtime with macro. + match Decode::decode(&mut &raw[..]) { + Some(x) => Ok(x), + None => Err(::client::error::ErrorKind::CallResultDecode(::AUTHORITIES_CALL).into()), + } + } + + fn grandpa_pending_change(&self, at: &BlockId, digest: DigestFor) + -> Result>>, ClientError> + { + let raw = self.call_api_at( + at, + ::PENDING_CHANGE_CALL, + digest.encode(), + &mut Default::default(), + &mut None, + ); + + match Decode::decode(&mut &raw[..]) { + Some(x) => Ok(x), + None => Err(::client::error::ErrorKind::CallResultDecode(::PENDING_CHANGE_CALL).into()), + } + } + } } diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index ba23383db2..1a0f10334a 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -84,12 +84,13 @@ use futures::stream::Fuse; use futures::sync::mpsc; use client::{Client, error::Error as ClientError, ImportNotifications, backend::Backend, CallExecutor}; use client::blockchain::HeaderBackend; -use client::runtime_api::{CallApiAt, TaggedTransactionQueue}; +use client::runtime_api::{CallApiAt, TaggedTransactionQueue, Core as CoreAPI}; use codec::{Encode, Decode}; use consensus_common::{BlockImport, ImportBlock, ImportResult}; use runtime_primitives::traits::{ NumberFor, Block as BlockT, Header as HeaderT, DigestFor, }; +use fg_primitives::GrandpaApi; use runtime_primitives::generic::BlockId; use substrate_primitives::{ed25519, H256, AuthorityId, Blake2Hasher}; use tokio::timer::Interval; @@ -765,46 +766,7 @@ impl, N, RA> voter::Environment> { - /// Get the genesis authorities for GRANDPA. - fn genesis_authorities(&self) -> Result, ClientError>; - /// Check a header's digest for a scheduled change. - fn scheduled_change(&self, header: &Block::Header) - -> Result>>, ClientError>; -} - -impl, RA> ApiClient for Arc> where - B: Backend + 'static, - E: CallExecutor + 'static + Clone + Send + Sync, - DigestFor: Encode, - RA: Send + Sync, -{ - fn genesis_authorities(&self) -> Result, ClientError> { - use runtime_primitives::traits::Zero; - - self.call_api_at_strong( - &BlockId::Number(NumberFor::::zero()), - fg_primitives::AUTHORITIES_CALL, - &(), - &mut Default::default(), - &mut None, - ) - } - - fn scheduled_change(&self, header: &Block::Header) - -> Result>>, ClientError> - { - self.call_api_at_strong( - &BlockId::hash(header.parent_hash().clone()), - fg_primitives::PENDING_CHANGE_CALL, - header.digest(), - &mut Default::default(), - &mut None, - ) - } -} /// A block-import handler for GRANDPA. /// @@ -821,7 +783,7 @@ impl, Api, RA> BlockImport for GrandpaBloc B: Backend + 'static, E: CallExecutor + 'static + Clone + Send + Sync, DigestFor: Encode, - Api: ApiClient, + Api: GrandpaApi, RA: TaggedTransactionQueue + Send + Sync, // necessary for client to import `BlockImport`. { type Error = ClientError; @@ -831,7 +793,10 @@ impl, Api, RA> BlockImport for GrandpaBloc { use authorities::PendingChange; - let maybe_change = self.api_client.scheduled_change(&block.header)?; + let maybe_change = self.api_client.grandpa_pending_change( + &BlockId::hash(*block.header.parent_hash()), + &block.header.digest().clone(), + )?; // when we update the authorities, we need to hold the lock // until the block is written to prevent a race if we need to restore @@ -879,9 +844,10 @@ pub fn block_import, Api, RA>(client: Arc + 'static, E: CallExecutor + 'static, - Api: ApiClient, + Api: GrandpaApi, RA: Send + Sync, { + use runtime_primitives::traits::Zero; let authority_set = match client.backend().get_aux(AUTHORITY_SET_KEY)? { None => { info!(target: "afg", "Loading GRANDPA authorities \ @@ -890,7 +856,8 @@ pub fn block_import, Api, RA>(client: Arc = api_client.genesis_authorities()?; + let genesis_authorities = api_client + .grandpa_authorities(&BlockId::number(Zero::zero()))?; let authority_set = SharedAuthoritySet::genesis(genesis_authorities); let encoded = authority_set.inner().read().encode(); From 3cd2739f6674a36962dd22eda7e92d3dbc2c5d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20K=C3=B6cher?= Date: Thu, 15 Nov 2018 17:34:55 +0100 Subject: [PATCH 36/36] Fixes `GrandpaApi` implementation and usage --- substrate/Cargo.lock | 2 + .../finality-grandpa/primitives/Cargo.toml | 2 + .../finality-grandpa/primitives/src/lib.rs | 55 +----------------- substrate/core/finality-grandpa/src/lib.rs | 29 +++++---- .../substrate_test_runtime.compact.wasm | Bin 45154 -> 45152 bytes substrate/node/runtime/Cargo.toml | 6 +- substrate/node/runtime/src/lib.rs | 29 ++++++++- substrate/node/runtime/wasm/Cargo.lock | 13 +++++ substrate/node/runtime/wasm/Cargo.toml | 2 + .../release/node_runtime.compact.wasm | Bin 618357 -> 622383 bytes 10 files changed, 67 insertions(+), 71 deletions(-) diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 32eb742442..993b4bdd6b 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -1804,6 +1804,7 @@ dependencies = [ "srml-treasury 0.1.0", "srml-upgrade-key 0.1.0", "substrate-client 0.1.0", + "substrate-fg-primitives 0.1.0", "substrate-keyring 0.1.0", "substrate-primitives 0.1.0", ] @@ -3186,6 +3187,7 @@ dependencies = [ "parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-codec-derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 0.1.0", + "sr-std 0.1.0", "substrate-client 0.1.0", "substrate-primitives 0.1.0", ] diff --git a/substrate/core/finality-grandpa/primitives/Cargo.toml b/substrate/core/finality-grandpa/primitives/Cargo.toml index 2b2711fab8..8f9c6628b8 100644 --- a/substrate/core/finality-grandpa/primitives/Cargo.toml +++ b/substrate/core/finality-grandpa/primitives/Cargo.toml @@ -9,6 +9,7 @@ substrate-primitives = { path = "../../primitives", default-features = false } parity-codec = { version = "2.1", default-features = false } parity-codec-derive = { version = "2.1", default-features = false } sr-primitives = { path = "../../sr-primitives", default-features = false } +sr-std = { path = "../../sr-std", default-features = false } [features] default = ["std"] @@ -18,4 +19,5 @@ std = [ "parity-codec/std", "parity-codec-derive/std", "sr-primitives/std", + "sr-std/std", ] diff --git a/substrate/core/finality-grandpa/primitives/src/lib.rs b/substrate/core/finality-grandpa/primitives/src/lib.rs index ed8ae408b7..aaca423ecd 100644 --- a/substrate/core/finality-grandpa/primitives/src/lib.rs +++ b/substrate/core/finality-grandpa/primitives/src/lib.rs @@ -28,8 +28,11 @@ extern crate parity_codec_derive; #[macro_use] extern crate substrate_client as client; +extern crate sr_std as rstd; + use substrate_primitives::AuthorityId; use sr_primitives::traits::{Block as BlockT, DigestFor, NumberFor}; +use rstd::vec::Vec; /// A scheduled change of authority set. #[cfg_attr(feature = "std", derive(Debug, PartialEq))] @@ -81,55 +84,3 @@ decl_runtime_apis! { fn grandpa_authorities() -> Vec<(AuthorityId, u64)>; } } - -#[cfg(feature = "std")] -mod implementation { - use super::{GrandpaApi, ScheduledChange}; - use sr_primitives::traits::{Block as BlockT, DigestFor, NumberFor}; - use sr_primitives::generic::BlockId; - use parity_codec::{Encode, Decode}; - use client::{Client, error::Error as ClientError, backend::Backend, CallExecutor}; - use client::runtime_api::{CallApiAt, Core as CoreAPI}; - use substrate_primitives::{AuthorityId, H256, Blake2Hasher}; - - // TODO [basti]: do this implementation in runtime. - impl, RA> GrandpaApi for Client where - B: Backend + 'static, - E: CallExecutor + 'static + Clone + Send + Sync, - DigestFor: Encode, - RA: CoreAPI, - { - fn grandpa_authorities(&self, at: &BlockId) -> Result, ClientError> { - let raw = self.call_api_at( - &at, - ::AUTHORITIES_CALL, - Encode::encode(&()), - &mut Default::default(), - &mut None, - ); - - // TODO [basti]: implement this in runtime with macro. - match Decode::decode(&mut &raw[..]) { - Some(x) => Ok(x), - None => Err(::client::error::ErrorKind::CallResultDecode(::AUTHORITIES_CALL).into()), - } - } - - fn grandpa_pending_change(&self, at: &BlockId, digest: DigestFor) - -> Result>>, ClientError> - { - let raw = self.call_api_at( - at, - ::PENDING_CHANGE_CALL, - digest.encode(), - &mut Default::default(), - &mut None, - ); - - match Decode::decode(&mut &raw[..]) { - Some(x) => Ok(x), - None => Err(::client::error::ErrorKind::CallResultDecode(::PENDING_CHANGE_CALL).into()), - } - } - } -} diff --git a/substrate/core/finality-grandpa/src/lib.rs b/substrate/core/finality-grandpa/src/lib.rs index 1a0f10334a..b107c3089e 100644 --- a/substrate/core/finality-grandpa/src/lib.rs +++ b/substrate/core/finality-grandpa/src/lib.rs @@ -84,11 +84,11 @@ use futures::stream::Fuse; use futures::sync::mpsc; use client::{Client, error::Error as ClientError, ImportNotifications, backend::Backend, CallExecutor}; use client::blockchain::HeaderBackend; -use client::runtime_api::{CallApiAt, TaggedTransactionQueue, Core as CoreAPI}; +use client::runtime_api::TaggedTransactionQueue; use codec::{Encode, Decode}; use consensus_common::{BlockImport, ImportBlock, ImportResult}; use runtime_primitives::traits::{ - NumberFor, Block as BlockT, Header as HeaderT, DigestFor, + NumberFor, Block as BlockT, Header as HeaderT, DigestFor, ProvideRuntimeApi }; use fg_primitives::GrandpaApi; use runtime_primitives::generic::BlockId; @@ -773,18 +773,16 @@ impl, N, RA> voter::Environment, Api, RA> { +pub struct GrandpaBlockImport, RA> { inner: Arc>, authority_set: SharedAuthoritySet>, - api_client: Api, } -impl, Api, RA> BlockImport for GrandpaBlockImport where +impl, RA> BlockImport for GrandpaBlockImport where B: Backend + 'static, E: CallExecutor + 'static + Clone + Send + Sync, DigestFor: Encode, - Api: GrandpaApi, - RA: TaggedTransactionQueue + Send + Sync, // necessary for client to import `BlockImport`. + RA: GrandpaApi + TaggedTransactionQueue, { type Error = ClientError; @@ -793,7 +791,7 @@ impl, Api, RA> BlockImport for GrandpaBloc { use authorities::PendingChange; - let maybe_change = self.api_client.grandpa_pending_change( + let maybe_change = self.inner.runtime_api().grandpa_pending_change( &BlockId::hash(*block.header.parent_hash()), &block.header.digest().clone(), )?; @@ -839,13 +837,12 @@ pub struct LinkHalf, RA> { /// Make block importer and link half necessary to tie the background voter /// to it. -pub fn block_import, Api, RA>(client: Arc>, api_client: Api) - -> Result<(GrandpaBlockImport, LinkHalf), ClientError> +pub fn block_import, RA>(client: Arc>) + -> Result<(GrandpaBlockImport, LinkHalf), ClientError> where - B: Backend + 'static, - E: CallExecutor + 'static, - Api: GrandpaApi, - RA: Send + Sync, + B: Backend + 'static, + E: CallExecutor + 'static + Clone + Send + Sync, + RA: GrandpaApi, { use runtime_primitives::traits::Zero; let authority_set = match client.backend().get_aux(AUTHORITY_SET_KEY)? { @@ -856,7 +853,7 @@ pub fn block_import, Api, RA>(client: Arc, Api, RA>(client: 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 E55f=$9{>OV diff --git a/substrate/node/runtime/Cargo.toml b/substrate/node/runtime/Cargo.toml index 1f6ccad0c3..5865927893 100644 --- a/substrate/node/runtime/Cargo.toml +++ b/substrate/node/runtime/Cargo.toml @@ -14,7 +14,8 @@ parity-codec-derive = "2.1" sr-std = { path = "../../core/sr-std" } srml-support = { path = "../../srml/support" } substrate-primitives = { path = "../../core/primitives" } -substrate-client = { path = "../../core/client", optional = true } +substrate-fg-primitives = { path = "../../core/finality-grandpa/primitives" } +substrate-client = { path = "../../core/client" } substrate-keyring = { path = "../../core/keyring" } srml-balances = { path = "../../srml/balances" } srml-consensus = { path = "../../srml/consensus" } @@ -57,5 +58,6 @@ std = [ "serde_derive", "serde/std", "safe-mix/std", - "substrate-client", + "substrate-client/std", + "substrate-fg-primitives/std", ] diff --git a/substrate/node/runtime/src/lib.rs b/substrate/node/runtime/src/lib.rs index 84ffbb4ab0..dca769f4d4 100644 --- a/substrate/node/runtime/src/lib.rs +++ b/substrate/node/runtime/src/lib.rs @@ -56,6 +56,7 @@ extern crate srml_upgrade_key as upgrade_key; #[macro_use] extern crate sr_version as version; extern crate node_primitives; +extern crate substrate_fg_primitives; #[cfg(feature = "std")] use codec::{Encode, Decode}; @@ -72,7 +73,7 @@ use client::runtime_api::ApiExt; use runtime_primitives::ApplyResult; use runtime_primitives::transaction_validity::TransactionValidity; use runtime_primitives::generic; -use runtime_primitives::traits::{Convert, BlakeTwo256, Block as BlockT}; +use runtime_primitives::traits::{Convert, BlakeTwo256, Block as BlockT, DigestFor, NumberFor}; #[cfg(feature = "std")] use runtime_primitives::traits::ApiRef; #[cfg(feature = "std")] @@ -84,6 +85,7 @@ use council::seats as council_seats; #[cfg(any(feature = "std", test))] use version::NativeVersion; use substrate_primitives::OpaqueMetadata; +use substrate_fg_primitives::{runtime::GrandpaApi, ScheduledChange}; #[cfg(any(feature = "std", test))] pub use runtime_primitives::BuildStorage; @@ -395,6 +397,19 @@ impl client::runtime_api::Metadata for ClientWithApi { } } +#[cfg(feature = "std")] +impl substrate_fg_primitives::GrandpaApi for ClientWithApi { + fn grandpa_pending_change(&self, at: &GBlockId, digest: &DigestFor) + -> Result>>, client::error::Error> { + self.call_api_at(at, "grandpa_pending_change", digest) + } + + fn grandpa_authorities(&self, at: &GBlockId) + -> Result, client::error::Error> { + self.call_api_at(at, "grandpa_authorities", &()) + } +} + impl_runtime_apis! { impl Core for Runtime { fn version() -> RuntimeVersion { @@ -447,4 +462,16 @@ impl_runtime_apis! { Executive::validate_transaction(tx) } } + + + impl GrandpaApi for ClientWithApi { + fn grandpa_pending_change(digest: DigestFor) + -> Option>> { + unimplemented!("Robert, where is the impl?") + } + + fn grandpa_authorities() -> Vec<(SessionKey, u64)> { + unimplemented!("Robert, where is the impl?") + } + } } diff --git a/substrate/node/runtime/wasm/Cargo.lock b/substrate/node/runtime/wasm/Cargo.lock index bb067bc400..4eeae1fd9d 100644 --- a/substrate/node/runtime/wasm/Cargo.lock +++ b/substrate/node/runtime/wasm/Cargo.lock @@ -526,6 +526,7 @@ dependencies = [ "srml-treasury 0.1.0", "srml-upgrade-key 0.1.0", "substrate-client 0.1.0", + "substrate-fg-primitives 0.1.0", "substrate-primitives 0.1.0", ] @@ -1272,6 +1273,18 @@ dependencies = [ "wasmi 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "substrate-fg-primitives" +version = "0.1.0" +dependencies = [ + "parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-codec-derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-primitives 0.1.0", + "sr-std 0.1.0", + "substrate-client 0.1.0", + "substrate-primitives 0.1.0", +] + [[package]] name = "substrate-keyring" version = "0.1.0" diff --git a/substrate/node/runtime/wasm/Cargo.toml b/substrate/node/runtime/wasm/Cargo.toml index 9e74ff9d5d..6e9cfd0910 100644 --- a/substrate/node/runtime/wasm/Cargo.toml +++ b/substrate/node/runtime/wasm/Cargo.toml @@ -12,6 +12,7 @@ safe-mix = { version = "1.0", default-features = false } parity-codec-derive = { version = "2.1" } parity-codec = { version = "2.1", default-features = false } substrate-primitives = { path = "../../../core/primitives", default-features = false } +substrate-fg-primitives = { path = "../../../core/finality-grandpa/primitives", default-features = false } substrate-client = { path = "../../../core/client", default-features = false } sr-std = { path = "../../../core/sr-std", default-features = false } srml-support = { path = "../../../srml/support", default-features = false } @@ -38,6 +39,7 @@ std = [ "parity-codec/std", "substrate-primitives/std", "substrate-client/std", + "substrate-fg-primitives/std", "sr-std/std", "srml-support/std", "srml-balances/std", diff --git a/substrate/node/runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm b/substrate/node/runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm index 766de4bda344ef0120ad9430fe43fcc41ecdabd8..8a89da66f1660fb72501339dae6ce988eadd865a 100644 GIT binary patch delta 182072 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�JF%UzA%d13jZmLl{du+#;zl=o_P4A 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

pokgbCC5*o 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ⅅA+Y*#`A7? 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