From 7143e85f39de2eb4dfec6834e70c13de9027bdb2 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 6 Aug 2018 11:55:55 +0200 Subject: [PATCH] Availability/Extrinsic store (#465) --- polkadot/availability-store/Cargo.toml | 15 + polkadot/availability-store/src/lib.rs | 258 ++++++++++++++++++ polkadot/consensus/Cargo.toml | 1 + polkadot/consensus/src/collation.rs | 5 + polkadot/consensus/src/lib.rs | 36 ++- polkadot/consensus/src/service.rs | 67 ++++- polkadot/consensus/src/shared_table/mod.rs | 156 ++++++++++- polkadot/network/Cargo.toml | 1 + polkadot/network/src/lib.rs | 33 ++- polkadot/network/src/router.rs | 53 ++-- polkadot/network/src/tests.rs | 56 +++- .../release/polkadot_runtime.compact.wasm | Bin 322938 -> 322956 bytes .../release/polkadot_runtime.wasm | Bin 323027 -> 323041 bytes polkadot/service/Cargo.toml | 1 + polkadot/service/src/lib.rs | 22 +- 15 files changed, 650 insertions(+), 54 deletions(-) create mode 100644 polkadot/availability-store/Cargo.toml create mode 100644 polkadot/availability-store/src/lib.rs diff --git a/polkadot/availability-store/Cargo.toml b/polkadot/availability-store/Cargo.toml new file mode 100644 index 0000000000..9a926310b0 --- /dev/null +++ b/polkadot/availability-store/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "polkadot-availability-store" +description = "Persistent database for parachain data" +version = "0.1.0" +authors = ["Parity Technologies "] + +[dependencies] +polkadot-primitives = { path = "../primitives" } +parking_lot = "0.4" +log = "0.3" +substrate-codec = { path = "../../substrate/codec" } +substrate-primitives = { path = "../../substrate/primitives" } +kvdb = { git = "https://github.com/paritytech/parity.git" } +kvdb-rocksdb = { git = "https://github.com/paritytech/parity.git" } +kvdb-memorydb = { git = "https://github.com/paritytech/parity.git" } diff --git a/polkadot/availability-store/src/lib.rs b/polkadot/availability-store/src/lib.rs new file mode 100644 index 0000000000..33ace5794c --- /dev/null +++ b/polkadot/availability-store/src/lib.rs @@ -0,0 +1,258 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Persistent database for parachain data. + +extern crate polkadot_primitives; +extern crate parking_lot; +extern crate substrate_codec as codec; +extern crate substrate_primitives; +extern crate kvdb; +extern crate kvdb_rocksdb; +extern crate kvdb_memorydb; + +#[macro_use] +extern crate log; + +use codec::{Encode, Decode}; +use kvdb::{KeyValueDB, DBTransaction}; +use kvdb_rocksdb::{Database, DatabaseConfig}; +use polkadot_primitives::Hash; +use polkadot_primitives::parachain::{Id as ParaId, BlockData, Extrinsic}; + +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::Arc; +use std::io; + +mod columns { + pub const DATA: Option = Some(0); + pub const META: Option = Some(1); + pub const NUM_COLUMNS: u32 = 2; +} + +/// Configuration for the availability store. +pub struct Config { + /// Cache size in bytes. If `None` default is used. + pub cache_size: Option, + /// Path to the database. + pub path: PathBuf, +} + +/// Some data to keep available. +pub struct Data { + /// The relay chain parent hash this should be localized to. + pub relay_parent: Hash, + /// The parachain index for this candidate. + pub parachain_id: ParaId, + /// Unique candidate receipt hash. + pub candidate_hash: Hash, + /// Block data. + pub block_data: BlockData, + /// Extrinsic data. + pub extrinsic: Option, +} + +fn extract_io_err(err: ::kvdb::Error) -> io::Error { + match err { + ::kvdb::Error(::kvdb::ErrorKind::Io(io_err), _) => io_err, + ::kvdb::Error(::kvdb::ErrorKind::Msg(msg), _) => io::Error::new( + io::ErrorKind::Other, + msg, + ), + x => io::Error::new( + io::ErrorKind::Other, + format!("Unexpected error variant: {:?}", x), // only necessary because of nonexaustive match. + ) + } +} + +fn block_data_key(relay_parent: &Hash, candidate_hash: &Hash) -> Vec { + (relay_parent, candidate_hash, 0i8).encode() +} + +fn extrinsic_key(relay_parent: &Hash, candidate_hash: &Hash) -> Vec { + (relay_parent, candidate_hash, 1i8).encode() +} + +/// Handle to the availability store. +#[derive(Clone)] +pub struct Store { + inner: Arc, +} + +impl Store { + /// Create a new `Store` with given config on disk. + pub fn new(config: Config) -> io::Result { + let mut db_config = DatabaseConfig::with_columns(Some(columns::NUM_COLUMNS)); + db_config.memory_budget = config.cache_size; + db_config.wal = true; + + let path = config.path.to_str().ok_or_else(|| io::Error::new( + io::ErrorKind::Other, + format!("Bad database path: {:?}", config.path), + ))?; + + let db = Database::open(&db_config, &path).map_err(extract_io_err)?; + + Ok(Store { + inner: Arc::new(db), + }) + } + + /// Create a new `Store` in-memory. Useful for tests. + pub fn new_in_memory() -> Self { + Store { + inner: Arc::new(::kvdb_memorydb::create(::columns::NUM_COLUMNS)), + } + } + + /// Make some data available provisionally. + pub fn make_available(&self, data: Data) -> io::Result<()> { + let mut tx = DBTransaction::new(); + + // note the meta key. + let mut v = match self.inner.get(columns::META, &*data.relay_parent) { + Ok(Some(raw)) => Vec::decode(&mut &raw[..]).expect("all stored data serialized correctly; qed"), + Ok(None) => Vec::new(), + Err(e) => { + warn!(target: "availability", "Error reading from availability store: {:?}", e); + Vec::new() + } + }; + + v.push(data.candidate_hash); + tx.put_vec(columns::META, &data.relay_parent[..], v.encode()); + + tx.put_vec( + columns::DATA, + block_data_key(&data.relay_parent, &data.candidate_hash).as_slice(), + data.block_data.encode() + ); + + if let Some(_extrinsic) = data.extrinsic { + tx.put_vec( + columns::DATA, + extrinsic_key(&data.relay_parent, &data.candidate_hash).as_slice(), + vec![], + ); + } + + self.inner.write(tx).map_err(extract_io_err) + } + + /// Note that a set of candidates have been included in a finalized block with given hash and parent hash. + pub fn candidates_finalized(&self, parent: Hash, finalized_candidates: HashSet) -> io::Result<()> { + let mut tx = DBTransaction::new(); + + let v = match self.inner.get(columns::META, &parent[..]) { + Ok(Some(raw)) => Vec::decode(&mut &raw[..]).expect("all stored data serialized correctly; qed"), + Ok(None) => Vec::new(), + Err(e) => { + warn!(target: "availability", "Error reading from availability store: {:?}", e); + Vec::new() + } + }; + tx.delete(columns::META, &parent[..]); + + for candidate_hash in v { + if !finalized_candidates.contains(&candidate_hash) { + tx.delete(columns::DATA, block_data_key(&parent, &candidate_hash).as_slice()); + tx.delete(columns::DATA, extrinsic_key(&parent, &candidate_hash).as_slice()); + } + } + + self.inner.write(tx).map_err(extract_io_err) + } + + /// Query block data. + pub fn block_data(&self, relay_parent: Hash, candidate_hash: Hash) -> Option { + let encoded_key = block_data_key(&relay_parent, &candidate_hash); + match self.inner.get(columns::DATA, &encoded_key[..]) { + Ok(Some(raw)) => Some( + BlockData::decode(&mut &raw[..]).expect("all stored data serialized correctly; qed") + ), + Ok(None) => None, + Err(e) => { + warn!(target: "availability", "Error reading from availability store: {:?}", e); + None + } + } + } + + /// Query extrinsic data. + pub fn extrinsic(&self, relay_parent: Hash, candidate_hash: Hash) -> Option { + let encoded_key = extrinsic_key(&relay_parent, &candidate_hash); + match self.inner.get(columns::DATA, &encoded_key[..]) { + Ok(Some(_raw)) => Some(Extrinsic), + Ok(None) => None, + Err(e) => { + warn!(target: "availability", "Error reading from availability store: {:?}", e); + None + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finalization_removes_unneeded() { + let relay_parent = [1; 32].into(); + + let para_id_1 = 5.into(); + let para_id_2 = 6.into(); + + let candidate_1 = [2; 32].into(); + let candidate_2 = [3; 32].into(); + + let block_data_1 = BlockData(vec![1, 2, 3]); + let block_data_2 = BlockData(vec![4, 5, 6]); + + let store = Store::new_in_memory(); + store.make_available(Data { + relay_parent, + parachain_id: para_id_1, + candidate_hash: candidate_1, + block_data: block_data_1.clone(), + extrinsic: Some(Extrinsic), + }).unwrap(); + + store.make_available(Data { + relay_parent, + parachain_id: para_id_2, + candidate_hash: candidate_2, + block_data: block_data_2.clone(), + extrinsic: Some(Extrinsic), + }).unwrap(); + + assert_eq!(store.block_data(relay_parent, candidate_1).unwrap(), block_data_1); + assert_eq!(store.block_data(relay_parent, candidate_2).unwrap(), block_data_2); + + assert!(store.extrinsic(relay_parent, candidate_1).is_some()); + assert!(store.extrinsic(relay_parent, candidate_2).is_some()); + + store.candidates_finalized(relay_parent, [candidate_1].iter().cloned().collect()).unwrap(); + + assert_eq!(store.block_data(relay_parent, candidate_1).unwrap(), block_data_1); + assert!(store.block_data(relay_parent, candidate_2).is_none()); + + assert!(store.extrinsic(relay_parent, candidate_1).is_some()); + assert!(store.extrinsic(relay_parent, candidate_2).is_none()); + } +} diff --git a/polkadot/consensus/Cargo.toml b/polkadot/consensus/Cargo.toml index 68ad826551..11f85499bb 100644 --- a/polkadot/consensus/Cargo.toml +++ b/polkadot/consensus/Cargo.toml @@ -13,6 +13,7 @@ log = "0.3" exit-future = "0.1" rhododendron = "0.2" polkadot-api = { path = "../api" } +polkadot-availability-store = { path = "../availability-store" } polkadot-parachain = { path = "../parachain" } polkadot-primitives = { path = "../primitives" } polkadot-runtime = { path = "../runtime" } diff --git a/polkadot/consensus/src/collation.rs b/polkadot/consensus/src/collation.rs index f7db48db61..b02b49aaa6 100644 --- a/polkadot/consensus/src/collation.rs +++ b/polkadot/consensus/src/collation.rs @@ -73,6 +73,11 @@ impl CollationFetch { live_fetch: None, } } + + /// Access the underlying relay parent hash. + pub fn relay_parent(&self) -> Hash { + self.relay_parent_hash + } } impl Future for CollationFetch { diff --git a/polkadot/consensus/src/lib.rs b/polkadot/consensus/src/lib.rs index 149e69f324..195d50a00f 100644 --- a/polkadot/consensus/src/lib.rs +++ b/polkadot/consensus/src/lib.rs @@ -32,6 +32,7 @@ extern crate ed25519; extern crate parking_lot; extern crate polkadot_api; +extern crate polkadot_availability_store as extrinsic_store; extern crate polkadot_statement_table as table; extern crate polkadot_parachain as parachain; extern crate polkadot_transaction_pool as transaction_pool; @@ -66,6 +67,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use codec::{Decode, Encode}; +use extrinsic_store::Store as ExtrinsicStore; use polkadot_api::PolkadotApi; use polkadot_primitives::{Hash, Block, BlockId, BlockNumber, Header, Timestamp, SessionKey}; use polkadot_primitives::parachain::{Id as ParaId, Chain, DutyRoster, BlockData, Extrinsic as ParachainExtrinsic, CandidateReceipt, CandidateSignature}; @@ -236,6 +238,8 @@ pub struct ProposerFactory { pub handle: TaskExecutor, /// The duration after which parachain-empty blocks will be allowed. pub parachain_empty_duration: Duration, + /// Store for extrinsic data. + pub extrinsic_store: ExtrinsicStore, } impl bft::Environment for ProposerFactory @@ -279,7 +283,7 @@ impl bft::Environment for ProposerFactory debug!(target: "consensus", "Active parachains: {:?}", active_parachains); let n_parachains = active_parachains.len(); - let table = Arc::new(SharedTable::new(group_info, sign_with.clone(), parent_hash)); + let table = Arc::new(SharedTable::new(group_info, sign_with.clone(), parent_hash, self.extrinsic_store.clone())); let (router, input, output) = self.network.communication_for( authorities, table.clone(), @@ -309,6 +313,7 @@ impl bft::Environment for ProposerFactory router.clone(), &self.handle, collation_work, + self.extrinsic_store.clone(), ); let proposer = Proposer { @@ -334,19 +339,42 @@ fn dispatch_collation_work( router: R, handle: &TaskExecutor, work: Option>, + extrinsic_store: ExtrinsicStore, ) -> exit_future::Signal where C: Collators + Send + 'static, P: PolkadotApi + Send + Sync + 'static, ::Future: Send + 'static, R: TableRouter + Send + 'static, { + use extrinsic_store::Data; + let (signal, exit) = exit_future::signal(); + + let work = match work { + Some(w) => w, + None => return signal, + }; + + let relay_parent = work.relay_parent(); let handled_work = work.then(move |result| match result { - Ok(Some((collation, extrinsic))) => { - router.local_candidate(collation.receipt, collation.block_data, extrinsic); + Ok((collation, extrinsic)) => { + let res = extrinsic_store.make_available(Data { + relay_parent, + parachain_id: collation.receipt.parachain_index, + candidate_hash: collation.receipt.hash(), + block_data: collation.block_data.clone(), + extrinsic: Some(extrinsic.clone()), + }); + + match res { + Ok(()) => + router.local_candidate(collation.receipt, collation.block_data, extrinsic), + Err(e) => + warn!(target: "consensus", "Failed to make collation data available: {:?}", e), + } + Ok(()) } - Ok(None) => Ok(()), Err(_e) => { warn!(target: "consensus", "Failed to collate candidate"); Ok(()) diff --git a/polkadot/consensus/src/service.rs b/polkadot/consensus/src/service.rs index 2e22470afc..4b5bfb259b 100644 --- a/polkadot/consensus/src/service.rs +++ b/polkadot/consensus/src/service.rs @@ -28,12 +28,13 @@ use std::time::{Duration, Instant}; use std::sync::Arc; use bft::{self, BftService}; -use client::{BlockchainEvents, ChainHead}; +use client::{BlockchainEvents, ChainHead, BlockBody}; use ed25519; use futures::prelude::*; use polkadot_api::LocalPolkadotApi; use polkadot_primitives::{Block, Header}; use transaction_pool::TransactionPool; +use extrinsic_store::Store as ExtrinsicStore; use tokio::executor::current_thread::TaskExecutor as LocalThreadHandle; use tokio::runtime::TaskExecutor as ThreadPoolHandle; @@ -89,6 +90,56 @@ fn start_bft( } } +// creates a task to prune redundant entries in availability store upon block finalization +// +// NOTE: this will need to be changed to finality notification rather than +// block import notifications when the consensus switches to non-instant finality. +fn prune_unneeded_availability(client: Arc, extrinsic_store: ExtrinsicStore) + -> impl Future + Send + where C: Send + Sync + BlockchainEvents + BlockBody + 'static +{ + use codec::{Encode, Decode}; + use polkadot_primitives::BlockId; + use polkadot_runtime::CheckedBlock; + + enum NotifyError { + NoBody(::client::error::Error), + UnexpectedFormat, + ExtrinsicsWrong, + } + + impl NotifyError { + fn log(&self, hash: &::polkadot_primitives::Hash) { + match *self { + NotifyError::NoBody(ref err) => warn!("Failed to fetch block body for imported block {:?}: {:?}", hash, err), + NotifyError::UnexpectedFormat => warn!("Consensus outdated: Block {:?} has unexpected body format", hash), + NotifyError::ExtrinsicsWrong => warn!("Consensus outdated: Failed to fetch block body for imported block {:?}", hash), + } + } + } + + client.import_notification_stream() + .for_each(move |notification| { + let checked_block = client.block_body(&BlockId::hash(notification.hash)) + .map_err(NotifyError::NoBody) + .map(|b| ::polkadot_runtime::Block::decode(&mut b.encode().as_slice())) + .and_then(|maybe_block| maybe_block.ok_or(NotifyError::UnexpectedFormat)) + .and_then(|block| CheckedBlock::new(block).map_err(|_| NotifyError::ExtrinsicsWrong)); + + match checked_block { + Ok(block) => { + let candidate_hashes = block.parachain_heads().iter().map(|c| c.hash()).collect(); + if let Err(e) = extrinsic_store.candidates_finalized(notification.header.parent_hash, candidate_hashes) { + warn!(target: "consensus", "Failed to prune unneeded available data: {:?}", e); + } + } + Err(e) => e.log(¬ification.hash) + } + + Ok(()) + }) +} + /// Consensus service. Starts working when created. pub struct Service { thread: Option>, @@ -105,10 +156,11 @@ impl Service { thread_pool: ThreadPoolHandle, parachain_empty_duration: Duration, key: ed25519::Pair, + extrinsic_store: ExtrinsicStore, ) -> Service where A: LocalPolkadotApi + Send + Sync + 'static, - C: BlockchainEvents + ChainHead + bft::BlockImport + bft::Authorities + Send + Sync + 'static, + C: BlockchainEvents + ChainHead + BlockBody + bft::BlockImport + bft::Authorities + Send + Sync + 'static, N: Network + Collators + Send + 'static, N::TableRouter: Send + 'static, ::Future: Send + 'static, @@ -124,7 +176,8 @@ impl Service { collators: network.clone(), network, parachain_empty_duration, - handle: thread_pool, + handle: thread_pool.clone(), + extrinsic_store: extrinsic_store.clone(), }; let bft_service = Arc::new(BftService::new(client.clone(), key, factory)); @@ -172,6 +225,14 @@ impl Service { runtime.spawn(notifications); runtime.spawn(timed); + + let prune_available = prune_unneeded_availability(client, extrinsic_store) + .select(exit.clone()) + .then(|_| Ok(())); + + // spawn this on the tokio executor since it's fine on a thread pool. + thread_pool.spawn(prune_available); + if let Err(e) = runtime.block_on(exit) { debug!("BFT event loop error {:?}", e); } diff --git a/polkadot/consensus/src/shared_table/mod.rs b/polkadot/consensus/src/shared_table/mod.rs index 825c624282..e953a05729 100644 --- a/polkadot/consensus/src/shared_table/mod.rs +++ b/polkadot/consensus/src/shared_table/mod.rs @@ -20,6 +20,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use extrinsic_store::{Data, Store as ExtrinsicStore}; use table::{self, Table, Context as TableContextTrait}; use polkadot_primitives::{Hash, SessionKey}; use polkadot_primitives::parachain::{Id as ParaId, BlockData, Collation, Extrinsic, CandidateReceipt}; @@ -82,6 +83,7 @@ struct SharedTableInner { checked_validity: HashSet, checked_availability: HashSet, trackers: Vec, + extrinsic_store: ExtrinsicStore, } impl SharedTableInner { @@ -153,6 +155,8 @@ impl SharedTableInner { work.map(|work| StatementProducer { produced_statements: Default::default(), + extrinsic_store: self.extrinsic_store.clone(), + relay_parent: context.parent_hash.clone(), work }) } @@ -186,6 +190,8 @@ pub struct ProducedStatements { pub struct StatementProducer { produced_statements: ProducedStatements, work: Work, + relay_parent: Hash, + extrinsic_store: ExtrinsicStore, } impl StatementProducer { @@ -221,25 +227,32 @@ impl Future for PrimedStatementProducer D: Future, E: Future, C: FnMut(Collation) -> Option, + Err: From<::std::io::Error>, { type Item = ProducedStatements; type Error = Err; fn poll(&mut self) -> Poll { let work = &mut self.inner.work; + let candidate = &work.candidate_receipt; + let statements = &mut self.inner.produced_statements; + + let mut candidate_hash = None; + let mut candidate_hash = move || + candidate_hash.get_or_insert_with(|| candidate.hash()).clone(); if let Async::Ready(block_data) = work.fetch_block_data.poll()? { - self.inner.produced_statements.block_data = Some(block_data.clone()); + statements.block_data = Some(block_data.clone()); if work.evaluate { let is_good = (self.check_candidate)(Collation { block_data, receipt: work.candidate_receipt.clone(), }); - let hash = work.candidate_receipt.hash(); + let hash = candidate_hash(); debug!(target: "consensus", "Making validity statement about candidate {}: is_good? {:?}", hash, is_good); - self.inner.produced_statements.validity = match is_good { + statements.validity = match is_good { Some(true) => Some(GenericStatement::Valid(hash)), Some(false) => Some(GenericStatement::Invalid(hash)), None => None, @@ -251,12 +264,11 @@ impl Future for PrimedStatementProducer if let Async::Ready(Some(extrinsic)) = work.fetch_extrinsic.poll()? { if work.ensure_available { - let hash = work.candidate_receipt.hash(); + let hash = candidate_hash(); debug!(target: "consensus", "Claiming candidate {} available.", hash); - // TODO: actually wait for block data and then ensure availability. - self.inner.produced_statements.extrinsic = Some(extrinsic); - self.inner.produced_statements.availability = + statements.extrinsic = Some(extrinsic); + statements.availability = Some(GenericStatement::Available(hash)); work.ensure_available = false; @@ -269,7 +281,18 @@ impl Future for PrimedStatementProducer }; if done { - Ok(Async::Ready(::std::mem::replace(&mut self.inner.produced_statements, Default::default()))) + // commit claimed-available data to disk before returning statements from the future. + if let (&Some(ref block), extrinsic) = (&statements.block_data, &statements.extrinsic) { + self.inner.extrinsic_store.make_available(Data { + relay_parent: self.inner.relay_parent, + parachain_id: work.candidate_receipt.parachain_index, + candidate_hash: candidate_hash(), + block_data: block.clone(), + extrinsic: extrinsic.clone(), + })?; + } + + Ok(Async::Ready(::std::mem::replace(statements, Default::default()))) } else { Ok(Async::NotReady) } @@ -296,7 +319,12 @@ impl SharedTable { /// /// Provide the key to sign with, and the parent hash of the relay chain /// block being built. - pub fn new(groups: HashMap, key: Arc<::ed25519::Pair>, parent_hash: Hash) -> Self { + pub fn new( + groups: HashMap, + key: Arc<::ed25519::Pair>, + parent_hash: Hash, + extrinsic_store: ExtrinsicStore, + ) -> Self { SharedTable { context: Arc::new(TableContext { groups, key, parent_hash }), inner: Arc::new(Mutex::new(SharedTableInner { @@ -305,6 +333,7 @@ impl SharedTable { checked_validity: HashSet::new(), checked_availability: HashSet::new(), trackers: Vec::new(), + extrinsic_store, })) } } @@ -457,9 +486,9 @@ mod tests { #[derive(Clone)] struct DummyRouter; impl TableRouter for DummyRouter { - type Error = (); - type FetchCandidate = ::futures::future::Empty; - type FetchExtrinsic = ::futures::future::Empty; + type Error = ::std::io::Error; + type FetchCandidate = ::futures::future::Empty; + type FetchExtrinsic = ::futures::future::Empty; fn local_candidate(&self, _candidate: CandidateReceipt, _block_data: BlockData, _extrinsic: Extrinsic) { @@ -491,7 +520,12 @@ mod tests { needed_availability: 0, }); - let shared_table = SharedTable::new(groups, local_key.clone(), parent_hash); + let shared_table = SharedTable::new( + groups, + local_key.clone(), + parent_hash, + ExtrinsicStore::new_in_memory(), + ); let candidate = CandidateReceipt { parachain_index: para_id, @@ -541,7 +575,12 @@ mod tests { needed_availability: 1, }); - let shared_table = SharedTable::new(groups, local_key.clone(), parent_hash); + let shared_table = SharedTable::new( + groups, + local_key.clone(), + parent_hash, + ExtrinsicStore::new_in_memory(), + ); let candidate = CandidateReceipt { parachain_index: para_id, @@ -572,4 +611,93 @@ mod tests { assert!(!producer.work.evaluate, "should not evaluate validity"); assert!(producer.work.ensure_available); } + + #[test] + fn evaluate_makes_block_data_available() { + let store = ExtrinsicStore::new_in_memory(); + let relay_parent = [0; 32].into(); + let para_id = 5.into(); + let block_data = BlockData(vec![1, 2, 3]); + + let candidate = CandidateReceipt { + parachain_index: para_id, + collator: [1; 32].into(), + signature: Default::default(), + head_data: ::polkadot_primitives::parachain::HeadData(vec![1, 2, 3, 4]), + balance_uploads: Vec::new(), + egress_queue_roots: Vec::new(), + fees: 1_000_000, + block_data_hash: [2; 32].into(), + }; + + let hash = candidate.hash(); + + let block_data_res: ::std::io::Result<_> = Ok(block_data.clone()); + let producer: StatementProducer<_, future::Empty<_, _>> = StatementProducer { + produced_statements: Default::default(), + work: Work { + candidate_receipt: candidate, + fetch_block_data: block_data_res.into_future().fuse(), + fetch_extrinsic: None, + evaluate: true, + ensure_available: false, + }, + relay_parent, + extrinsic_store: store.clone(), + }; + + let produced = producer.prime(|_| Some(true)).wait().unwrap(); + + assert_eq!(produced.block_data.as_ref(), Some(&block_data)); + assert!(produced.validity.is_some()); + assert!(produced.availability.is_none()); + + assert_eq!(store.block_data(relay_parent, hash).unwrap(), block_data); + assert!(store.extrinsic(relay_parent, hash).is_none()); + } + + #[test] + fn full_availability() { + let store = ExtrinsicStore::new_in_memory(); + let relay_parent = [0; 32].into(); + let para_id = 5.into(); + let block_data = BlockData(vec![1, 2, 3]); + + let candidate = CandidateReceipt { + parachain_index: para_id, + collator: [1; 32].into(), + signature: Default::default(), + head_data: ::polkadot_primitives::parachain::HeadData(vec![1, 2, 3, 4]), + balance_uploads: Vec::new(), + egress_queue_roots: Vec::new(), + fees: 1_000_000, + block_data_hash: [2; 32].into(), + }; + + let hash = candidate.hash(); + + let block_data_res: ::std::io::Result<_> = Ok(block_data.clone()); + let extrinsic_res: ::std::io::Result<_> = Ok(Extrinsic); + let producer = StatementProducer { + produced_statements: Default::default(), + work: Work { + candidate_receipt: candidate, + fetch_block_data: block_data_res.into_future().fuse(), + fetch_extrinsic: Some(extrinsic_res.into_future().fuse()), + evaluate: false, + ensure_available: true, + }, + relay_parent, + extrinsic_store: store.clone(), + }; + + let produced = producer.prime(|_| Some(true)).wait().unwrap(); + + assert_eq!(produced.block_data.as_ref(), Some(&block_data)); + assert!(produced.validity.is_none()); + assert!(produced.availability.is_some()); + + assert_eq!(store.block_data(relay_parent, hash).unwrap(), block_data); + assert!(store.extrinsic(relay_parent, hash).is_some()); + } } diff --git a/polkadot/network/Cargo.toml b/polkadot/network/Cargo.toml index 37d36ea205..eb861e6265 100644 --- a/polkadot/network/Cargo.toml +++ b/polkadot/network/Cargo.toml @@ -7,6 +7,7 @@ description = "Polkadot-specific networking protocol" [dependencies] parking_lot = "0.4" polkadot-api = { path = "../api" } +polkadot-availability-store = { path = "../availability-store" } polkadot-consensus = { path = "../consensus" } polkadot-primitives = { path = "../primitives" } substrate-bft = { path = "../../substrate/bft" } diff --git a/polkadot/network/src/lib.rs b/polkadot/network/src/lib.rs index dfbaa2ea17..366a2f659b 100644 --- a/polkadot/network/src/lib.rs +++ b/polkadot/network/src/lib.rs @@ -26,6 +26,7 @@ extern crate substrate_network; extern crate substrate_primitives; extern crate polkadot_api; +extern crate polkadot_availability_store as av_store; extern crate polkadot_consensus; extern crate polkadot_primitives; @@ -197,7 +198,9 @@ struct CurrentConsensus { impl CurrentConsensus { // get locally stored block data for a candidate. - fn block_data(&self, hash: &Hash) -> Option { + fn block_data(&self, relay_parent: &Hash, hash: &Hash) -> Option { + if relay_parent != &self.parent_hash { return None } + self.knowledge.lock().candidates.get(hash) .and_then(|entry| entry.block_data.clone()) } @@ -211,8 +214,8 @@ pub enum Message { /// As a validator, tell the peer your current session key. // TODO: do this with a cryptographic proof of some kind SessionKey(SessionKey), - /// Requesting parachain block data by candidate hash. - RequestBlockData(RequestId, Hash), + /// Requesting parachain block data by (relay_parent, candidate_hash). + RequestBlockData(RequestId, Hash, Hash), /// Provide block data by candidate hash or nothing if unknown. BlockData(RequestId, Option), /// Tell a collator their role. @@ -233,9 +236,10 @@ impl Encode for Message { dest.push_byte(1); dest.push(k); } - Message::RequestBlockData(ref id, ref d) => { + Message::RequestBlockData(ref id, ref r, ref d) => { dest.push_byte(2); dest.push(id); + dest.push(r); dest.push(d); } Message::BlockData(ref id, ref d) => { @@ -261,7 +265,10 @@ impl Decode for Message { match input.read_byte()? { 0 => Some(Message::Statement(Decode::decode(input)?, Decode::decode(input)?)), 1 => Some(Message::SessionKey(Decode::decode(input)?)), - 2 => Some(Message::RequestBlockData(Decode::decode(input)?, Decode::decode(input)?)), + 2 => { + let x: (_, _, _) = Decode::decode(input)?; + Some(Message::RequestBlockData(x.0, x.1, x.2)) + } 3 => Some(Message::BlockData(Decode::decode(input)?, Decode::decode(input)?)), 4 => Some(Message::CollatorRole(Decode::decode(input)?)), 5 => Some(Message::Collation(Decode::decode(input)?, Decode::decode(input)?)), @@ -287,6 +294,7 @@ pub struct PolkadotProtocol { live_consensus: Option, in_flight: HashMap<(RequestId, NodeIndex), BlockDataRequest>, pending: Vec, + extrinsic_store: Option<::av_store::Store>, next_req_id: u64, } @@ -303,6 +311,7 @@ impl PolkadotProtocol { live_consensus: None, in_flight: HashMap::new(), pending: Vec::new(), + extrinsic_store: None, next_req_id: 1, } } @@ -385,7 +394,7 @@ impl PolkadotProtocol { send_polkadot_message( ctx, who, - Message::RequestBlockData(req_id, pending.candidate_hash) + Message::RequestBlockData(req_id, pending.consensus_parent, pending.candidate_hash) ); self.in_flight.insert((req_id, who), pending); @@ -406,9 +415,12 @@ impl PolkadotProtocol { Message::Statement(parent_hash, _statement) => self.consensus_gossip.on_chain_specific(ctx, who, raw, parent_hash), Message::SessionKey(key) => self.on_session_key(ctx, who, key), - Message::RequestBlockData(req_id, hash) => { + Message::RequestBlockData(req_id, relay_parent, candidate_hash) => { let block_data = self.live_consensus.as_ref() - .and_then(|c| c.block_data(&hash)); + .and_then(|c| c.block_data(&relay_parent, &candidate_hash)) + .or_else(|| self.extrinsic_store.as_ref() + .and_then(|s| s.block_data(relay_parent, candidate_hash)) + ); send_polkadot_message(ctx, who, Message::BlockData(req_id, block_data)); } @@ -720,4 +732,9 @@ impl PolkadotProtocol { } } } + + /// register availability store. + pub fn register_availability_store(&mut self, extrinsic_store: ::av_store::Store) { + self.extrinsic_store = Some(extrinsic_store); + } } diff --git a/polkadot/network/src/router.rs b/polkadot/network/src/router.rs index 8d6bc18540..79a50f4283 100644 --- a/polkadot/network/src/router.rs +++ b/polkadot/network/src/router.rs @@ -32,6 +32,7 @@ use tokio::runtime::TaskExecutor; use parking_lot::Mutex; use std::collections::{HashMap, HashSet}; +use std::io; use std::sync::Arc; use super::{NetworkService, Knowledge}; @@ -135,8 +136,8 @@ impl Router

{ } fn dispatch_work(&self, candidate_hash: Hash, producer: StatementProducer) where - D: Future + Send + 'static, - E: Future + Send + 'static, + D: Future + Send + 'static, + E: Future + Send + 'static, { let parent_hash = self.parent_hash.clone(); @@ -156,28 +157,34 @@ impl Router

{ let network = self.network.clone(); let knowledge = self.knowledge.clone(); - let work = producer.prime(validate).map(move |produced| { - // store the data before broadcasting statements, so other peers can fetch. - knowledge.lock().note_candidate(candidate_hash, produced.block_data, produced.extrinsic); + let work = producer.prime(validate) + .map(move |produced| { + // store the data before broadcasting statements, so other peers can fetch. + knowledge.lock().note_candidate( + candidate_hash, + produced.block_data, + produced.extrinsic + ); - // propagate the statements - if let Some(validity) = produced.validity { - let signed = table.sign_and_import(validity.clone()).0; - network.with_spec(|spec, ctx| spec.gossip_statement(ctx, parent_hash, signed)); - } + // propagate the statements + if let Some(validity) = produced.validity { + let signed = table.sign_and_import(validity.clone()).0; + network.with_spec(|spec, ctx| spec.gossip_statement(ctx, parent_hash, signed)); + } - if let Some(availability) = produced.availability { - let signed = table.sign_and_import(availability).0; - network.with_spec(|spec, ctx| spec.gossip_statement(ctx, parent_hash, signed)); - } - }); + if let Some(availability) = produced.availability { + let signed = table.sign_and_import(availability).0; + network.with_spec(|spec, ctx| spec.gossip_statement(ctx, parent_hash, signed)); + } + }) + .map_err(|e| debug!(target: "p_net", "Failed to produce statements: {:?}", e)); self.task_executor.spawn(work); } } impl TableRouter for Router

{ - type Error = (); + type Error = io::Error; type FetchCandidate = BlockDataReceiver; type FetchExtrinsic = Result; @@ -213,12 +220,18 @@ pub struct BlockDataReceiver { impl Future for BlockDataReceiver { type Item = BlockData; - type Error = (); + type Error = io::Error; - fn poll(&mut self) -> Poll { + fn poll(&mut self) -> Poll { match self.inner { - Some(ref mut inner) => inner.poll().map_err(|_| ()), - None => return Err(()), + Some(ref mut inner) => inner.poll().map_err(|_| io::Error::new( + io::ErrorKind::Other, + "Sending end of channel hung up", + )), + None => return Err(io::Error::new( + io::ErrorKind::Other, + "Network service is unavailable", + )), } } } diff --git a/polkadot/network/src/tests.rs b/polkadot/network/src/tests.rs index 356d272e3f..f5ad78dae9 100644 --- a/polkadot/network/src/tests.rs +++ b/polkadot/network/src/tests.rs @@ -174,7 +174,7 @@ fn fetches_from_those_with_knowledge() { let mut ctx = TestContext::default(); on_message(&mut protocol, &mut ctx, peer_a, Message::SessionKey(a_key)); assert!(protocol.validators.contains_key(&a_key)); - assert!(ctx.has_message(peer_a, Message::RequestBlockData(1, candidate_hash))); + assert!(ctx.has_message(peer_a, Message::RequestBlockData(1, parent_hash, candidate_hash))); } knowledge.lock().note_statement(b_key, &GenericStatement::Valid(candidate_hash)); @@ -184,7 +184,7 @@ fn fetches_from_those_with_knowledge() { let mut ctx = TestContext::default(); protocol.on_connect(&mut ctx, peer_b, make_status(&status, Roles::AUTHORITY)); on_message(&mut protocol, &mut ctx, peer_b, Message::SessionKey(b_key)); - assert!(!ctx.has_message(peer_b, Message::RequestBlockData(2, candidate_hash))); + assert!(!ctx.has_message(peer_b, Message::RequestBlockData(2, parent_hash, candidate_hash))); } @@ -193,7 +193,7 @@ fn fetches_from_those_with_knowledge() { let mut ctx = TestContext::default(); protocol.on_disconnect(&mut ctx, peer_a); assert!(!protocol.validators.contains_key(&a_key)); - assert!(ctx.has_message(peer_b, Message::RequestBlockData(2, candidate_hash))); + assert!(ctx.has_message(peer_b, Message::RequestBlockData(2, parent_hash, candidate_hash))); } // peer B comes back with block data. @@ -205,6 +205,56 @@ fn fetches_from_those_with_knowledge() { } } +#[test] +fn fetches_available_block_data() { + let mut protocol = PolkadotProtocol::new(None); + + let peer_a = 1; + let parent_hash = [0; 32].into(); + + let block_data = BlockData(vec![1, 2, 3, 4]); + let block_data_hash = block_data.hash(); + let para_id = 5.into(); + let candidate_receipt = CandidateReceipt { + parachain_index: para_id, + collator: [255; 32].into(), + head_data: HeadData(vec![9, 9, 9]), + signature: H512::from([1; 64]).into(), + balance_uploads: Vec::new(), + egress_queue_roots: Vec::new(), + fees: 1_000_000, + block_data_hash, + }; + + let candidate_hash = candidate_receipt.hash(); + let av_store = ::av_store::Store::new_in_memory(); + + let status = Status { collating_for: None }; + + protocol.register_availability_store(av_store.clone()); + + av_store.make_available(::av_store::Data { + relay_parent: parent_hash, + parachain_id: para_id, + candidate_hash, + block_data: block_data.clone(), + extrinsic: None, + }).unwrap(); + + // connect peer A + { + let mut ctx = TestContext::default(); + protocol.on_connect(&mut ctx, peer_a, make_status(&status, Roles::FULL)); + } + + // peer A asks for historic block data and gets response + { + let mut ctx = TestContext::default(); + on_message(&mut protocol, &mut ctx, peer_a, Message::RequestBlockData(1, parent_hash, candidate_hash)); + assert!(ctx.has_message(peer_a, Message::BlockData(1, Some(block_data)))); + } +} + #[test] fn remove_bad_collator() { let mut protocol = PolkadotProtocol::new(None); diff --git a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm index 74088d063a466b7525c24f58eeb55360060d15f7..831b77b45e1c1f8dcbcc7780e361380736c2f18f 100644 GIT binary patch delta 16725 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 delta 16601 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 delta 16538 zcmb7r2V7Lg^Z4!T;|@8>Ap!!52O0$nq9P)G70*PCNsJ|G)YvGeVi1sGi3PCtpidnW zHI~>*MA1AA7GjdG8f(~B1FXYjyls7IfPd5Pa^4Pd7c~_am;O1j68Nguh0XH8oc^Ck| z%?%9T?QL)~`g()p2JRl<29@1Jg`0=Fo59b|&&^LQ{QL~^a;W38b!Vy0n5VO-rD3{! z21e%1V8+DUoWU7csX3|EY~4&|N=>KN#I)3GYyYIQjO34XvzSj}W@g%`{??H>S*hvS zsmZ!enU{5>H90p&qnXRR2d1XGwvw4&YWiSnmNh-c*)Ur-kNFHIvXsP}jI93288vg$ zb9D2WhXa?bTfizAna;wDZV4^hyxG=cV)1Qm@v5P`L3HF!5N%ohsleMbx z_LqDOvqNjOV@mN5-zQ!T9c&ym&ej@ZXX`2QtQrI1_}1d5HM&GoiF;l^Ejg|8K=yI* z$dx3|2eepRZoG%>X!Ij4SAys%r0iCW1=Q#uGHLDtL61_sHM z2mJzNUlsp8Xt_b4KF?SpD4J(lK$K&$mQ-RTa`2d7{}RD5T;+H`8(xXrVN5d7Vd4W8 zAusu>718>Sy)GF2F}8#NWn8LY6P{NSRLKf@2ykhE#8^r3%JI9aGDUe=3mAjH=HRdv z;E$dIA=KUE-PTKQnKc*VYr|)(C!VSeH(>HyoYerr@lqY|5q&<)0zZ7&5I(?e5m4RE zpfS}bYa$?$!C4d>1p@VTVUwL85--<<$}nRgKB^0E!~A*pX*7iUem1Xl3!Tnf$)X=( z(KXO5nui(nK!$G@qEsKgsMOQUE#ujca5ubI8`|N+`tZ5nNAg6&y}7uz0knl4Kh(p(@N)s{_!~ixk$WF?fj{`Zb0WxI7=fZwwv&|D8l* zRug#7RjT$F978&sBV$wyylWqLxHe{^U5BRHP~xi*tonpIhre(m=Rjr{-e?gs==BC_;V|Wfb#h^%ZNyTH|+Z90ng^+ zngmE-eeqTTJQLuR)?|Kjae)Q0Tq8%}1kAHQAK@gIEKVnpoK9kD18oBOL2I+r){DG{ zg-mncbd!xZ$gg4 z)-JvY+v{Anv5?Fr*Os?!4yLudI~RAn1+lC@mcIo{;o<_Nur0J-t*)%x_LFmin^_UK8iu1Fp+#c!toRc`OHz}?k9_kIF|G_f`B||Kh#Y02<<0A-RNf_7%eiL09 z`ocIkwF;;7h3@~*u6{olRXN$yWVBQ;TGdHvR6sclUUP>MEbj-msSnxivAwYE0BEDX zvW!Rru}d7h;Ulb_%&LgY$|Usu?Eu)tQj~Ft;K}q>Pj~7T3zH}~E}DiaoXgAO$PR|?&(}j;z9rWLP zBnVRm!AVwvaf8X4mQBDugJF)C>GHu)TbpUkt?tfMYNtXG`=6q6i;v+CIdRc&-z{IX zZbm-lM*d~dMd)^ZQG9z*M>XIzTara_sd&Q@iNWqK+aXEA%e5iA*Zy}{bS zvsEb7W=HKvKfG0&b%zG?}y zZ~je#%Hmu&&qQP||CUmbJtH7c9)9{1@hpDt%)9Eezjr2CetEVdeiOf^UrEM5nDtIb4!&whX+T>+7gK=R5 z^TL!`ER;>f(<7i2T>lKOPNU?aa4JR2gpm*>dZnDu4`+>mZg_emnBka>X2Ey}QUhh* zvCzMEIJEWhGwa0aVCqX{v~7{FnV3!#(H6?HROWj}5f_r=>L}>I!dwiiNXwU9aHQ#X zM^kQc>9t<>kA^W7#Z4ar9@%+2?AdiSRsMe0GzJKP-&VEU#r=xkdX%Wz6k#I z_sW<%9z^1eQ^v!;S(5Vp1fUtB#MDj@g%>B%pzgiKv%w_j4J;9-O@XFyE`vJo4+AX} zMh|8W(_55BxX_m1vndennzfr9$p$uGc&D_f=ce5BwgYe7@3WTI&kD0WuJHC~$^xp)Qymnpz=sSzj;on9>49=cO zYb8Zs`?zfu@XJiNR^uaz8qq_-By;638igfqNVtcW&S)fCI16MG%Jo^W*Uh-*bK>k{ zk}QutQ@)bnrn|>r3jZM@>F~qEsSqp{M2FVP7CW?_pfp+njrH21sI%mQYGOfVf3YYA zS6mc>or|LUCB(CS%F!<&P=?oxv6-Ir}esR)J(relgAhJCDT zDT3OZ9O7&-Jb)v`_~SO(Y5iP`ceX(_mWGDy&{G&k;&$*>qua2rp)DJVJHCcNP*99v z3XxAMR$40%#aKFK?tpDn)ZR%Jx8`$fw-d&T&VJkpD=0Q9>EFO1CL*gE*$Q@18edW@ z!fYLbnW<}wLa{cKd%4&-H8YpO5hf_9Szjg29p)QJ^rL>zz(;v+A9QpReJt7!lSKRt zKLAfz0aiT-B+F_%aZp{z)qq#^M>y@D&nWyK*Hw)F_@8jt#4>PAO;F&+#q+%>WD-x8 zUGK%eqT&2v98e1?)VOPs79adV&sfjm<4YH+A$rL2eXwGw9JL}G)-IL1uE-^R#RpgP zW%{C}Wa{p6`Jq~J=arQREq}CfB;X49=aqS^r+j>AwtQgKEYY~`TD9@}Yx4!w@wMmO z&8)2#w`^Z7Us==PpD3<*eYc`i7baP1hB41KjPg&10u2_I4v(%Edqdf@K0|a^u)e0N zH1^6%>vsm*nvkNVioFndqPy@!1Ev2qmh+d>7!0msc(vgZ{o>cL#Tz#oIjmeE@7$aK z7naK}HsANI$h!DRQ5ObZE|=SH4fox!T+MUTM1=MZ#mnVSwoVZ8nhz()t%^HA=_L8n z;=pz;{-$M=-j3Kug4vEmw3(?GHnj$HjB>v?hzv$HYjt$W=zeeKid}rTbIel_O-Mz>TV-Ukq>@Xhouxh{LaK)>!HK3BzfTvb)^(f zKcl>8SCAb2tyRwW-nY2>ztb5!TPBwu36RS+ga}5@_n*}AS)2AmJ}X~5P$nwHJij_< zsW?_jNaQ|0x`(96W26NLR%cD3pb#};}!`!#bj$n5(nGC%%Ao_eCKu-juNLN&YPj_F6= z>MTI6ax#SVk)uyGmOGq#Pm{`-#LKJCO{V$==WEj6F6aBQ;qvD5F+4L&k~dulg0DBr z>_UtXyV-?L;mk_;fDQWd!fG}`UUhLv!poJayMU#LtCndp`v{}7^$@9>xXQk35JnlP zo2h|SRU4hRN-nsRC)mT~Q9(|RGP{XAKJjEj%$}wsdEVtFq}BtM2hiWhD>YbBafd5m zYJ^KG`%Kuu=IdgSl4GtS8OPzPo{R|3T?-|`=hrBqpsFg@T?sE<7lb)C>e-XcMo#@& zH0N|dt<|R5i4*G4-&sj>3{bl_uy6)|Lfgq0y5s~3QJbW{qIExfUn|4cE9C*@ z)#L}ist33K`Bm-8PX5_%WAzJGkqQj5ja;en_lkF@-Z*OX!e7)M#;RAY$!w34jh$ZinUmW+IUL>?B zET7YoS3K;GYmAMOyS;kH_Y*T6wB_?~y(p)0(`b3~t7wyk^)Z|1NRyD|uj)~QkUtx- zPvnk&j`+aE5}dE5Iu(~Ar&YU}i<90J`(TnB_*Wx%x%qEd2rHStj77opqWQi9T9h z!bmn2OI}jaJPv<<3F%Y}dIbZaXtHwb6}%GL&Fx2dBy)|^3n$yJNi@f%{LCMC3p$AH z0z84`;auR!B2A-yBGJsb@MNVP=am`D$5s*_1tqI-r^LhPY~_N)>%#4dwAjLpkB0J! zNaa2u)ub@hNF1os>qh=IooqGm5b%@us-UpH~-hP_r?jMaux*n{wjM0{D8drzCU?jtbh(6k8f4wr+ja1QiHOmYM%{s<(tqWn77BwAU@6aa>crvYTY(ALD?J3 zdoq3gHZo8nS^{_@e7_nG5Ne~o>Z+SWB#dZtP8soScJXGxN)QXcecn7+Rscz<9P^81%X0C2O?wWa+ee9iJ@p2oj z9L5*?eK1OVI0wciAW7_xU2AePn~2#p`54w$c~FxFb2b<~>+m$o1I;iTFH^Pqv*X%J zsYYWvgeSi#)z*S#Q*dV;vb7TYr4G-dygfOBudq)^Q{`WgycJZ)A(YAWNCC!8t5p|g zbQ*!J5_k~mqqx5?xav`SI-@Pc+J^jNiR?x3kKyl&wF|9H`3aH1ZCDbld=Sg`F%Jh5 z6CU41ry*YZpc1x<pCIAJ3a|TZ*OJXvsSR0osq!5RCiZZEPRKkIBMauHFMy0Q@OcG8QO+>=-FgtSzn zDQDqTwr(IzxSOWZc0y+wXc}Yc6vi;~A*;$ZW7z{1uHH77b++R^@`$mNj);mnLMN3T zvH@L$`OG5SEK4iJD*~SQaKVs^26fyY#cRgZZ(GRBBgThqp z#U)ZXDxEm&@`MHIcW)+%ebM(R3&pn|J9tK5-ecB;k|v`{CS*DK*db;F)_B4?z)^DC zr>v&lJ|6iA8>t%fQ`Sky+wUoB1=G~=2kLK#egbjULsr9^=uMuMgNq1bFipkVPg&D8 z)0okLA?-=xAj4d7Gx=X5{B!kq2iJGxT|~dHI`XY-7#6+HgJAb&JoLV54JdWuxgy9n z>dK>3-`?1X=h+ce(9)SKjHt(T<_W^n_jcxDS;t>H^AQ4`)|EFvxeIS8R6#77VWqCT zwwMkATC!Zs=*p|oN+AfvDW0;kE8iweUCHRik@=|0T6DP1?5VD5=--2!cofF=;DaNb zbK<%zxnQ*w1P8x>ZbB-$#lGaOVU@QVvKe6OVL&+L(h)G&G#anbf?0Kx?Q%=tqQi}5 zgSaL1q?AI*Y0Pemt7YuhlvQKZ?1w zu~_h=qKwXIV!=d5Xkf`_Lt>#Sz0l5}Y>Kigmi_3?=~q2&Z)2ZRW=@AUbofsL=(L^f7bCG9R1?E+iG7Ig?_#sr zK|H>j1>lSCm>K4K;(%>T$J3}aH2gVuGL;jwU-hcc9PXKzof-K7Ix6>tK}?Jsg^}c zeJpI_gevk;h)(3l?W_L*QZ=PWZOmACggq<|Pyi%sey=U2p5cN{dM3-`O zvZN?S*5Sr;WQJ4LDZid$@r+GUA}+8&jF$Mqi!_rN_|-)+9@4VYIifDH z14gH?w7tB{qJzazHEkkjD{Ch2l!O~r8e#_1Y^J<(nK+Z5E0eCU5N@`4^mAwM$frbK zxb#F(l4?t@QCKLOf)}ol{nC_w@C!R*5CNgyJ+gr*44>bl3pL?r_gON3%PEQ_f10+` ziNj*su~eCcdv1SO=L6n(Fs`P~Qp!FKhgv!lu`GYVf^gO|7B9w1*}0!`@fka%#_6rk znZH{-XD{lO!exfIeTK9nu@ZdrO7&!=_MfbmA;vb|2pXF$R*G0gqphM2Rw#`EB)vya z_%c&Trw26lVmC+@t438lU5$}Q?e#Fc;wdfusE3+tgz}pn-eK$<>JQP0YF}mBA=u5_ zj})tEFGtH=s2curaqD>6p_1iB@=_))8@XAEg#*Ih;)a;HDGj$BfqtT&Dn}t(uU84_<*_F55_r0J%Xyf_5*S4 zDwYn!hn)b~G`+Y~oSN1M^YqjkEmx`Elh9rqaQ8h)C)v}AaqLO>q{=!QRb`cmWQ`bN zN>w~h!MjF1No`rku>V!c2y?E$EjYgw`?_81u94V37ON-p zo)!-}mT90nT#Tcx(H^T0Zo3AT#fiKs0iDQ`1i^Z{cIKR*bMVI3p@+(6`VH#G&PQ_c z*?j}fiXq@lx{{_Ll-z_V@bxz3ty^?$0H=$wfNtQmT?Og37LL9{w_>!P7MJpC?!au( zy4hW|werzjxXS3#q1Aoxqe}43zyrFFry-U- z_(#z`d<2N=AHqnXrdp4jYI2zuqFG266AvE23!#Slk8Kwd#wY5U~~KZVJ{ifTQh!BB|W_6!nLl?6U`s@_Fq?VhWuFL(|Q+Gr+AxdQF{JuQ`n zgnK%V*Ba>l$%u)df)W1*zE%$Q;7r7?Lm%?yEJJbY$-nd`aNST|gNlPgc|+(NgfE8j z9?&BgyQcG~ywg=pbj`2RT{D<;ov3U>52L3?ut`^w%JH5iT}LX@?f3x%htvBT?mC?Z zl^R@+U~hs8y-d310#Bu&3kDsTnoif6;2;5Cv$U|%1T&P?wbzB9cG`m5e1U9=0niygm_@DbF%?i9Uvxe#=<6C@J;T^Ia^ zcDyFA){!Q_01`lqFrP{>a{CA!0^jkqX>u6V2@Mw!+?@J%G|*I)PZh2z);fHh!2>^= zs-vq^RCOugMSE>ZG>rxN+cash3n2LBiMfBhB9jQkND8TR9mzKYHzNE!Dt)Oep~rPf zG<;pcixFyqzm8uQXwvDaes-WKPZvTj6A2-V=`lS(PiK-$7?GJuA%LbCZ6wnOFO2dn zdR#mDZ}>lnUf4r5JH5u|&#HM9D=JkpsYGkZ`dy9gMuOly{TXl{0nImv2HG!M_nu@co9wN?P;)bteVNOO8d zjyX9uOMHc`v+R~JJCE0b3o?F|#~UY3ndj(nP^QizKESdGkeryVv&?sZS=K>1%VGhV zk`hzQIf+?=tU0z0boBik;VTP#mNhdYi^R}s=p)kl`E-y)-`T#G&uf^=9SoDxY532a zpcwwEc{n4Vgro1qw&e3_uw_0T%jZq%UZzhTjK2Rc8u2ZJP23TwBrbhUS-*g`>huYw zwSdR?{=7h|PE4k;UtECe3;4h2YfHRcz$1Y^l=K_VBh6<~Bg@VvtzU6~nF73t*k?Si z7W7h}eNxktGf0)mIp&;iN-HIVEaVt?8JW~Stl1vtQ*tf6E94v^-WEWiOK3*&n98SbCJTLg(u zG>ks&bF*^IL#-*PIf*%`8KjQV&oN{ouK{JB3sHh`+z9Rsg^Mum6Q1BN&Q6SmoYbN5 zDXA$s%W(nLLpF|A!?82CFZ{6xhvxDGd@+UxM2bD7(Ikk~p9AzKVNx@w)p>zenb61k zI?L6?XdO!@C>xjHj&+Ed546v#GK##IVe4sdH34S1hFl-hxSb#!&hZ zLQZNr*{itFHo6P!h}5)rokiTs8chODChrkS>OatG6<4(yJ|`A%TRwDV31Ee2T=qz6fu(I0JCU{0xcK?AHG}qggqwgp>LAtOU6Gl-Q_i8z& zj^gG(Tp>E^VAqZ66gtbw6}W5^N%-9g+&_v(hTarJRWzcC-kSfVP+TPYQZ)qqoiB-0 zHP9Bg;}c%R{Pjvgr~;>FlX7-Dz~Lfd(ed;BPk2zZGXkv&o^8#}7OKp$j!4WRqqwsY z!$#A=*~^vquhHCW7MEc*e%YDCZ;1m;vl73Rt8ffaH&X=KC=`*Dm~B<3WuFr@D#wO8 z;%H3nr?b!}u!l$Uh#JoX+Ft}ZYkEfRpuy&WSyrogU~YN}c^G|9={JUlHC(+~$dO_l z8lRR(p0>#WX67d8EZdx*@HD#H_H#tc1={zL-khh3jqZ>pzXIBOa``1%kp2 zPO&PB{lC#YGchaC76HE5fFI7F;CXKY7S7;-bZ~;}X7KjnSm@CVx?(%Gk?3RbBYMZ` zhM#+N{>Bm7j%VmM8q@o5}6UioSIT6VjDP#q7^$|Ub!X+Ia zdd{Hqy9Db`qmWE{`sQPK-6|95tk>v~ot4}$Ej1}htc9~TQEOlS`jdTSk=@X-^XzGKRYdnjo2YE~($S9miCmX1 z9Bo|F(}<0@f3$&Wlv`Hh)FK=&vU$<8EktAYqA#cODsXxWme1r-`aeZQXIuQgJ(2s@ z6I*7Zjv}roJ(4qq$2+2_z|hJ-m@G0b+UuGp@!Ea`w6`@@66oR9oD2$M(~Gg!By#q3 z#h5>dS8ZA%2<+`hQ~LyHROv-VkWDR)I9U*i5VXfUGKoj~i<>JOZE{9NTE@VE8^U)Y*v(e&rBJzuV4572zR(Vfdfw4Xr!rXU!a#Je z(FW(KEd_Y_HR^sa28II>4}$ zm}wvk)fr?Doo)+NR}#!ks-)j&%qy#sYM@|Vom%g8R*S~pYrLzdUHzbedA7bZSoF1# z$Pa0){ZwY>G;;beCe`L@wO*K8Bqh5Bk@WBG?tBRXxef;m>4cK$LXKKnH(KW2JaEs>p01ZDFjL z65dkUGNE##q$VkG)@IEc$23n#jEie({uqC~E7em>-%8(_*laAgAXUZat5PNOyCL<% z{+H>PzbF3mlQaW*b;5$P5`9kbt>l5j&rwBh%)BghhmX40Dt4cfs?kAc*nX)1GfqkY z(6<*3`$-DIvddCy`aM(F6$zog6Bk`Mu}P!mO_Hs#jpCA08pXyY4(KR2#beAF$&Ws_ z*Es2MaT#@&g!yNsSe$xF3Zvg*(EEpw@}aHkhNq-(u=Wu^Ec%?IpU(`$ugauvV9)~F zkczHJ2JHNcWW?5GQV+a$RSKkU8=M`?xF(q(wW~wk8hHAe6wHRWkc|3;NIo8{>I+F< zRdMq%$=@%!xiz^-j5T&(Ok9jLIVL3y85ZuL+S#|I46JgU8V&7@%a2Nd*y_6UCZs3Z zfT-J2IPShK)nyqA?n~7$?-EgF_P{xpq-L0ZRMO*&(^3%KHy2)#-iIvv)I2y#Q zN@v;F?y%3EVEw`k2y0ml7 zk91oy@qGFYNmV52?k%Z+?hPH9&g0{mE6ZMY$Y3Vm!)wyi%8i;PTjNriBqugXv^Gjf zjGf3F>Z*hT?^55BoEQ(ha98qSlkwhNsjs*!YI9M_;Zw=hHB+vE6Yfdjd>ZTF&{``j zxFId7Zx~LA6>WNEGS{EZ&-(i>=V!AuHiei^NS>H^jHV{~yfoVX)4%RV zG*8@Szv7SWX&=47^{4j8>~5n*0^<#7xbo*c=@<6@ E05zt#VgLXD diff --git a/polkadot/service/Cargo.toml b/polkadot/service/Cargo.toml index 77c7589c91..b0561122e1 100644 --- a/polkadot/service/Cargo.toml +++ b/polkadot/service/Cargo.toml @@ -12,6 +12,7 @@ slog = "^2" tokio = "0.1.7" hex-literal = "0.1" ed25519 = { path = "../../substrate/ed25519" } +polkadot-availability-store = { path = "../availability-store" } polkadot-primitives = { path = "../primitives" } polkadot-runtime = { path = "../runtime" } polkadot-consensus = { path = "../consensus" } diff --git a/polkadot/service/src/lib.rs b/polkadot/service/src/lib.rs index 10349d94dd..3814a1c98d 100644 --- a/polkadot/service/src/lib.rs +++ b/polkadot/service/src/lib.rs @@ -19,6 +19,7 @@ //! Polkadot service. Specialized wrapper over substrate service. extern crate ed25519; +extern crate polkadot_availability_store as av_store; extern crate polkadot_primitives; extern crate polkadot_runtime; extern crate polkadot_executor; @@ -43,7 +44,7 @@ pub mod chain_spec; use std::sync::Arc; use std::collections::HashMap; -use codec::Encode; +use codec::{Encode, Decode}; use transaction_pool::TransactionPool; use polkadot_api::{PolkadotApi, light::RemotePolkadotApiWrapper}; use polkadot_primitives::{parachain, AccountId, Block, BlockId, Hash}; @@ -195,8 +196,22 @@ pub fn new_light(config: Configuration, executor: TaskExecutor) pub fn new_full(config: Configuration, executor: TaskExecutor) -> Result>, Error> { + // open availability store. + let av_store = { + use std::path::PathBuf; + + let mut path = PathBuf::from(config.database_path.clone()); + path.push("availability"); + + ::av_store::Store::new(::av_store::Config { + cache_size: None, + path, + })? + }; + let is_validator = (config.roles & Roles::AUTHORITY) == Roles::AUTHORITY; let service = service::Service::>::new(config, executor.clone())?; + // Spin consensus service if configured let consensus = if is_validator { // Load the first available key @@ -214,11 +229,14 @@ pub fn new_full(config: Configuration, executor: TaskExecutor) executor, ::std::time::Duration::from_secs(4), // TODO: dynamic key, + av_store.clone(), )) } else { None }; + service.network().with_spec(|spec, _| spec.register_availability_store(av_store)); + Ok(Service { client: service.client(), network: service.network(), @@ -295,7 +313,7 @@ impl network::TransactionPool for TransactionPoolAdapter Some(*xt.hash()),