light: finish basic header chain and add tests

This commit is contained in:
Robert Habermeier 2016-12-13 14:48:03 +01:00
parent c2264bed27
commit 45ef986c04
8 changed files with 270 additions and 80 deletions

7
Cargo.lock generated
View File

@ -469,6 +469,7 @@ dependencies = [
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rlp 0.1.0", "rlp 0.1.0",
"smallvec 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1681,6 +1682,11 @@ name = "smallvec"
version = "0.1.8" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "smallvec"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "solicit" name = "solicit"
version = "0.4.4" version = "0.4.4"
@ -2152,6 +2158,7 @@ dependencies = [
"checksum slab 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6dbdd334bd28d328dad1c41b0ea662517883d8880d8533895ef96c8003dec9c4" "checksum slab 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6dbdd334bd28d328dad1c41b0ea662517883d8880d8533895ef96c8003dec9c4"
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" "checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
"checksum smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "fcc8d19212aacecf95e4a7a2179b26f7aeb9732a915cf01f05b0d3e044865410" "checksum smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "fcc8d19212aacecf95e4a7a2179b26f7aeb9732a915cf01f05b0d3e044865410"
"checksum smallvec 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a3c84984c278afe61a46e19868e8b23e2ee3be5b3cc6dea6edad4893bc6c841"
"checksum solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "172382bac9424588d7840732b250faeeef88942e37b6e35317dce98cafdd75b2" "checksum solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "172382bac9424588d7840732b250faeeef88942e37b6e35317dce98cafdd75b2"
"checksum spmc 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "93bdab61c1a413e591c4d17388ffa859eaff2df27f1e13a5ec8b716700605adf" "checksum spmc 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "93bdab61c1a413e591c4d17388ffa859eaff2df27f1e13a5ec8b716700605adf"
"checksum stable-heap 0.1.0 (git+https://github.com/carllerche/stable-heap?rev=3c5cd1ca47)" = "<none>" "checksum stable-heap 0.1.0 (git+https://github.com/carllerche/stable-heap?rev=3c5cd1ca47)" = "<none>"

View File

@ -19,6 +19,7 @@ ethcore-io = { path = "../../util/io" }
ethcore-ipc = { path = "../../ipc/rpc", optional = true } ethcore-ipc = { path = "../../ipc/rpc", optional = true }
rlp = { path = "../../util/rlp" } rlp = { path = "../../util/rlp" }
time = "0.1" time = "0.1"
smallvec = "0.3.1"
[features] [features]
default = [] default = []

View File

@ -23,28 +23,35 @@
//! This is separate from the `BlockChain` for two reasons: //! This is separate from the `BlockChain` for two reasons:
//! - It stores only headers (and a pruned subset of them) //! - It stores only headers (and a pruned subset of them)
//! - To allow for flexibility in the database layout once that's incorporated. //! - To allow for flexibility in the database layout once that's incorporated.
// TODO: use DB instead of memory. // TODO: use DB instead of memory. DB Layout: just the contents of `candidates`/`headers`
//
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use ethcore::header::Header;
use ethcore::error::BlockError; use ethcore::error::BlockError;
use ethcore::ids::BlockId; use ethcore::ids::BlockId;
use ethcore::views::HeaderView; use ethcore::views::HeaderView;
use util::{Bytes, H256, U256, Mutex, RwLock}; use util::{Bytes, H256, U256, Mutex, RwLock};
/// Delay this many blocks before producing a CHT. use smallvec::SmallVec;
/// Delay this many blocks before producing a CHT. required to be at
/// least 1 but should be more in order to be resilient against reorgs.
const CHT_DELAY: u64 = 2048; const CHT_DELAY: u64 = 2048;
/// Generate CHT roots of this size. /// Generate CHT roots of this size.
// TODO: move into more generic module. // TODO: move CHT definition/creation into more generic module.
const CHT_SIZE: u64 = 2048; const CHT_SIZE: u64 = 2048;
/// Information about a block.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct BestBlock { pub struct BlockDescriptor {
hash: H256, /// The block's hash
number: u64, pub hash: H256,
total_difficulty: U256, /// The block's number
pub number: u64,
/// The block's total difficulty.
pub total_difficulty: U256,
} }
// candidate block description. // candidate block description.
@ -55,7 +62,7 @@ struct Candidate {
} }
struct Entry { struct Entry {
candidates: Vec<Candidate>, candidates: SmallVec<[Candidate; 3]>, // 3 arbitrarily chosen
canonical_hash: H256, canonical_hash: H256,
} }
@ -64,7 +71,7 @@ pub struct HeaderChain {
genesis_header: Bytes, // special-case the genesis. genesis_header: Bytes, // special-case the genesis.
candidates: RwLock<BTreeMap<u64, Entry>>, candidates: RwLock<BTreeMap<u64, Entry>>,
headers: RwLock<HashMap<H256, Bytes>>, headers: RwLock<HashMap<H256, Bytes>>,
best_block: RwLock<BestBlock>, best_block: RwLock<BlockDescriptor>,
cht_roots: Mutex<Vec<H256>>, cht_roots: Mutex<Vec<H256>>,
} }
@ -75,7 +82,7 @@ impl HeaderChain {
HeaderChain { HeaderChain {
genesis_header: genesis.to_owned(), genesis_header: genesis.to_owned(),
best_block: RwLock::new(BestBlock { best_block: RwLock::new(BlockDescriptor {
hash: g_view.hash(), hash: g_view.hash(),
number: 0, number: 0,
total_difficulty: g_view.difficulty(), total_difficulty: g_view.difficulty(),
@ -114,7 +121,7 @@ impl HeaderChain {
// insert headers and candidates entries. // insert headers and candidates entries.
let mut candidates = self.candidates.write(); let mut candidates = self.candidates.write();
candidates.entry(number).or_insert_with(|| Entry { candidates: Vec::new(), canonical_hash: hash}) candidates.entry(number).or_insert_with(|| Entry { candidates: SmallVec::new(), canonical_hash: hash})
.candidates.push(Candidate { .candidates.push(Candidate {
hash: hash, hash: hash,
parent_hash: parent_hash, parent_hash: parent_hash,
@ -137,7 +144,7 @@ impl HeaderChain {
canon_hash = canon.parent_hash; canon_hash = canon.parent_hash;
} }
*self.best_block.write() = BestBlock { *self.best_block.write() = BlockDescriptor {
hash: hash, hash: hash,
number: number, number: number,
total_difficulty: total_difficulty, total_difficulty: total_difficulty,
@ -145,13 +152,24 @@ impl HeaderChain {
// produce next CHT root if it's time. // produce next CHT root if it's time.
let earliest_era = *candidates.keys().next().expect("at least one era just created; qed"); let earliest_era = *candidates.keys().next().expect("at least one era just created; qed");
if earliest_era + CHT_DELAY + CHT_SIZE < number { if earliest_era + CHT_DELAY + CHT_SIZE <= number {
let values: Vec<_> = (0..CHT_SIZE).map(|x| x + earliest_era) let mut values = Vec::with_capacity(CHT_SIZE as usize);
.map(|x| candidates.remove(&x).map(|entry| (x, entry))) {
.map(|x| x.expect("all eras stored are sequential with no gaps; qed")) let mut headers = self.headers.write();
.map(|(x, entry)| (::rlp::encode(&x), ::rlp::encode(&entry.canonical_hash))) for i in (0..CHT_SIZE).map(|x| x + earliest_era) {
.map(|(k, v)| (k.to_vec(), v.to_vec())) let era_entry = candidates.remove(&i)
.collect(); .expect("all eras are sequential with no gaps; qed");
for ancient in &era_entry.candidates {
headers.remove(&ancient.hash);
}
values.push((
::rlp::encode(&i).to_vec(),
::rlp::encode(&era_entry.canonical_hash).to_vec(),
));
}
}
let cht_root = ::util::triehash::trie_root(values); let cht_root = ::util::triehash::trie_root(values);
debug!(target: "chain", "Produced CHT {} root: {:?}", (earliest_era - 1) % CHT_SIZE, cht_root); debug!(target: "chain", "Produced CHT {} root: {:?}", (earliest_era - 1) % CHT_SIZE, cht_root);
@ -165,7 +183,7 @@ impl HeaderChain {
/// Get a block header. In the case of query by number, only canonical blocks /// Get a block header. In the case of query by number, only canonical blocks
/// will be returned. /// will be returned.
pub fn block_header(&self, id: BlockId) -> Option<Bytes> { pub fn get_header(&self, id: BlockId) -> Option<Bytes> {
match id { match id {
BlockId::Earliest | BlockId::Number(0) => Some(self.genesis_header.clone()), BlockId::Earliest | BlockId::Number(0) => Some(self.genesis_header.clone()),
BlockId::Hash(hash) => self.headers.read().get(&hash).map(|x| x.to_vec()), BlockId::Hash(hash) => self.headers.read().get(&hash).map(|x| x.to_vec()),
@ -182,4 +200,141 @@ impl HeaderChain {
} }
} }
/// Get the nth CHT root, if it's been computed.
///
/// CHT root 0 is from block `1..2048`.
/// CHT root 1 is from block `2049..4096`
/// and so on.
///
/// This is because it's assumed that the genesis hash is known,
/// so including it within a CHT would be redundant.
pub fn cht_root(&self, n: usize) -> Option<H256> {
self.cht_roots.lock().get(n).map(|h| h.clone())
}
/// Get the genesis hash.
pub fn genesis_hash(&self) -> H256 {
use util::Hashable;
self.genesis_header.sha3()
}
/// Get the best block's data.
pub fn best_block(&self) -> BlockDescriptor {
self.best_block.read().clone()
}
/// If there is a gap between the genesis and the rest
/// of the stored blocks, return the first post-gap block.
pub fn first_block(&self) -> Option<BlockDescriptor> {
let candidates = self.candidates.read();
match candidates.iter().next() {
None | Some((&1, _)) => None,
Some((&height, entry)) => Some(BlockDescriptor {
number: height,
hash: entry.canonical_hash,
total_difficulty: entry.candidates.iter().find(|x| x.hash == entry.canonical_hash)
.expect("entry always stores canonical candidate; qed").total_difficulty,
})
}
}
}
#[cfg(test)]
mod tests {
use super::HeaderChain;
use ethcore::ids::BlockId;
use ethcore::header::Header;
use ethcore::spec::Spec;
#[test]
fn it_works() {
let spec = Spec::new_test();
let genesis_header = spec.genesis_header();
let chain = HeaderChain::new(&::rlp::encode(&genesis_header));
let mut parent_hash = genesis_header.hash();
let mut rolling_timestamp = genesis_header.timestamp();
for i in 1..10000 {
let mut header = Header::new();
header.set_parent_hash(parent_hash);
header.set_number(i);
header.set_timestamp(rolling_timestamp);
header.set_difficulty(*genesis_header.difficulty() * i.into());
chain.insert(::rlp::encode(&header).to_vec());
parent_hash = header.hash();
rolling_timestamp += 10;
}
assert!(chain.get_header(BlockId::Number(10)).is_none());
assert!(chain.get_header(BlockId::Number(9000)).is_some());
assert!(chain.cht_root(2).is_some());
assert!(chain.cht_root(3).is_none());
}
#[test]
fn reorganize() {
let spec = Spec::new_test();
let genesis_header = spec.genesis_header();
let chain = HeaderChain::new(&::rlp::encode(&genesis_header));
let mut parent_hash = genesis_header.hash();
let mut rolling_timestamp = genesis_header.timestamp();
for i in 1..6 {
let mut header = Header::new();
header.set_parent_hash(parent_hash);
header.set_number(i);
header.set_timestamp(rolling_timestamp);
header.set_difficulty(*genesis_header.difficulty() * i.into());
chain.insert(::rlp::encode(&header).to_vec()).unwrap();
parent_hash = header.hash();
rolling_timestamp += 10;
}
{
let mut rolling_timestamp = rolling_timestamp;
let mut parent_hash = parent_hash;
for i in 6..16 {
let mut header = Header::new();
header.set_parent_hash(parent_hash);
header.set_number(i);
header.set_timestamp(rolling_timestamp);
header.set_difficulty(*genesis_header.difficulty() * i.into());
chain.insert(::rlp::encode(&header).to_vec()).unwrap();
parent_hash = header.hash();
rolling_timestamp += 10;
}
}
assert_eq!(chain.best_block().number, 15);
{
let mut rolling_timestamp = rolling_timestamp;
let mut parent_hash = parent_hash;
// import a shorter chain which has better TD.
for i in 6..13 {
let mut header = Header::new();
header.set_parent_hash(parent_hash);
header.set_number(i);
header.set_timestamp(rolling_timestamp);
header.set_difficulty(*genesis_header.difficulty() * (i * i).into());
chain.insert(::rlp::encode(&header).to_vec()).unwrap();
parent_hash = header.hash();
rolling_timestamp += 11;
}
}
assert_eq!(chain.best_block().number, 12);
}
} }

View File

@ -20,41 +20,53 @@ use std::sync::Arc;
use ethcore::engines::Engine; use ethcore::engines::Engine;
use ethcore::ids::BlockId; use ethcore::ids::BlockId;
use ethcore::service::ClientIoMessage;
use ethcore::block_import_error::BlockImportError; use ethcore::block_import_error::BlockImportError;
use ethcore::block_status::BlockStatus; use ethcore::block_status::BlockStatus;
use ethcore::verification::queue::{HeaderQueue, QueueInfo}; use ethcore::verification::queue::{HeaderQueue, QueueInfo, Config as QueueConfig};
use ethcore::transaction::SignedTransaction; use ethcore::transaction::SignedTransaction;
use ethcore::blockchain_info::BlockChainInfo; use ethcore::blockchain_info::BlockChainInfo;
use ethcore::spec::Spec;
use ethcore::service::ClientIoMessage;
use io::IoChannel; use io::IoChannel;
use util::hash::{H256, H256FastMap}; use util::hash::{H256, H256FastMap};
use util::{Bytes, Mutex}; use util::{Bytes, Mutex};
use provider::Provider; use provider::Provider;
use request; use request;
use self::header_chain::HeaderChain;
mod header_chain; mod header_chain;
/// Configuration for the light client.
#[derive(Debug, Default, Clone)]
pub struct Config {
queue: QueueConfig,
}
/// Light client implementation. /// Light client implementation.
pub struct Client { pub struct Client {
_engine: Arc<Engine>, queue: HeaderQueue,
header_queue: HeaderQueue, chain: HeaderChain,
_message_channel: Mutex<IoChannel<ClientIoMessage>>,
tx_pool: Mutex<H256FastMap<SignedTransaction>>, tx_pool: Mutex<H256FastMap<SignedTransaction>>,
} }
impl Client { impl Client {
/// Create a new `Client`.
pub fn new(config: Config, spec: &Spec, io_channel: IoChannel<ClientIoMessage>) -> Self {
Client {
queue: HeaderQueue::new(config.queue, spec.engine.clone(), io_channel, true),
chain: HeaderChain::new(&::rlp::encode(&spec.genesis_header())),
tx_pool: Mutex::new(Default::default()),
}
}
/// Import a header as rlp-encoded bytes. /// Import a header as rlp-encoded bytes.
pub fn import_header(&self, bytes: Bytes) -> Result<H256, BlockImportError> { pub fn import_header(&self, bytes: Bytes) -> Result<H256, BlockImportError> {
let header = ::rlp::decode(&bytes); let header = ::rlp::decode(&bytes);
self.header_queue.import(header).map_err(Into::into) self.queue.import(header).map_err(Into::into)
}
/// Whether the block is already known (but not necessarily part of the canonical chain)
pub fn is_known(&self, _id: BlockId) -> bool {
false
} }
/// Import a local transaction. /// Import a local transaction.
@ -68,30 +80,34 @@ impl Client {
} }
/// Inquire about the status of a given block (or header). /// Inquire about the status of a given block (or header).
pub fn status(&self, _id: BlockId) -> BlockStatus { pub fn status(&self, id: BlockId) -> BlockStatus {
BlockStatus::Unknown BlockStatus::Unknown
} }
/// Get the header queue info. /// Get the header queue info.
pub fn queue_info(&self) -> QueueInfo { pub fn queue_info(&self) -> QueueInfo {
self.header_queue.queue_info() self.queue.queue_info()
}
/// Best block number.
pub fn best_block_number(&self) -> u64 {
0
}
/// Best block hash.
pub fn best_block_hash(&self) -> u64 {
unimplemented!()
} }
} }
// dummy implementation -- may draw from canonical cache further on. // dummy implementation -- may draw from canonical cache further on.
impl Provider for Client { impl Provider for Client {
fn chain_info(&self) -> BlockChainInfo { fn chain_info(&self) -> BlockChainInfo {
unimplemented!() let best_block = self.chain.best_block();
let first_block = self.chain.first_block();
let genesis_hash = self.chain.genesis_hash();
BlockChainInfo {
total_difficulty: best_block.total_difficulty,
pending_total_difficulty: best_block.total_difficulty,
genesis_hash: genesis_hash,
best_block_hash: best_block.hash,
best_block_number: best_block.number,
ancient_block_hash: if first_block.is_some() { Some(genesis_hash) } else { None },
ancient_block_number: if first_block.is_some() { Some(0) } else { None },
first_block_hash: first_block.as_ref().map(|first| first.hash),
first_block_number: first_block.as_ref().map(|first| first.number),
}
} }
fn reorg_depth(&self, _a: &H256, _b: &H256) -> Option<u64> { fn reorg_depth(&self, _a: &H256, _b: &H256) -> Option<u64> {

View File

@ -60,6 +60,7 @@ extern crate ethcore_util as util;
extern crate ethcore_network as network; extern crate ethcore_network as network;
extern crate ethcore_io as io; extern crate ethcore_io as io;
extern crate rlp; extern crate rlp;
extern crate smallvec;
extern crate time; extern crate time;
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]

View File

@ -20,7 +20,7 @@ use network::{NetworkContext, PeerId, NodeId};
use super::{Announcement, LightProtocol, ReqId}; use super::{Announcement, LightProtocol, ReqId};
use super::error::Error; use super::error::Error;
use request::Request; use request::{self, Request};
/// An I/O context which allows sending and receiving packets as well as /// An I/O context which allows sending and receiving packets as well as
/// disconnecting peers. This is used as a generalization of the portions /// disconnecting peers. This is used as a generalization of the portions
@ -93,6 +93,10 @@ pub trait EventContext {
// TODO: maybe just put this on a timer in LightProtocol? // TODO: maybe just put this on a timer in LightProtocol?
fn make_announcement(&self, announcement: Announcement); fn make_announcement(&self, announcement: Announcement);
/// Find the maximum number of requests of a specific type which can be made from
/// supplied peer.
fn max_requests(&self, peer: PeerId, kind: request::Kind) -> Option<usize>;
/// Disconnect a peer. /// Disconnect a peer.
fn disconnect_peer(&self, peer: PeerId); fn disconnect_peer(&self, peer: PeerId);
@ -128,6 +132,10 @@ impl<'a> EventContext for Ctx<'a> {
self.proto.make_announcement(self.io, announcement); self.proto.make_announcement(self.io, announcement);
} }
fn max_requests(&self, peer: PeerId, kind: request::Kind) -> Option<usize> {
self.proto.max_requests(peer, kind)
}
fn disconnect_peer(&self, peer: PeerId) { fn disconnect_peer(&self, peer: PeerId) {
self.io.disconnect_peer(peer); self.io.disconnect_peer(peer);
} }

View File

@ -188,7 +188,9 @@ pub trait Handler: Send + Sync {
/// Called when a peer responds with header proofs. Each proof is a block header coupled /// Called when a peer responds with header proofs. Each proof is a block header coupled
/// with a series of trie nodes is ascending order by distance from the root. /// with a series of trie nodes is ascending order by distance from the root.
fn on_header_proofs(&self, _ctx: &EventContext, _req_id: ReqId, _proofs: &[(Bytes, Vec<Bytes>)]) { } fn on_header_proofs(&self, _ctx: &EventContext, _req_id: ReqId, _proofs: &[(Bytes, Vec<Bytes>)]) { }
/// Called on abort. /// Called on abort. This signals to handlers that they should clean up
/// and ignore peers.
// TODO: coreresponding `on_activate`?
fn on_abort(&self) { } fn on_abort(&self) { }
} }