PoA: Wait for transition finality before applying (#5774)

* final engine changes

* migration to v13

* adding and removing pending transitions

* epoch_transition_for

* port snapshot to new engine methods

* final validator set interface

* fix compiler errors

* revert v13/epoch_depth transition

* make call on new epoch

* rolling finality checker

* tests for finality checker

* constructing finality proof upon pending transition

* fix warnings and finality proof checking

* fix compiler warnings in tests

* test fixes

* don't include genesis in finality checking

* change snapshot test chain building logic

* minor refactorings

* fetch epoch transition based on parent, fix divide-by-zero in SimpleList

* fix formatting

* fix ABIs and finality checking in snapshot restoration

* encode signal number in proof

* create more blocks at the end of tests

* update gist to accurate contract code

* test for epoch_transition_for

* fix tests with immediateTransitions parameter

* disable force flag after forcing

* rename ValidatorsChanged to InitiateChange and finalizeSignal to finalizeChange

* a few more validator set tests
This commit is contained in:
Robert Habermeier
2017-06-28 13:17:36 +02:00
committed by Arkadiy Paronyan
parent 3637c14da5
commit d069b98b45
41 changed files with 1905 additions and 804 deletions

View File

@@ -18,8 +18,8 @@
use std::sync::Arc;
use engines::{Engine, EpochVerifier, EpochChange};
use error::Error;
use blockchain::BlockChain;
use engines::{Engine, EpochVerifier};
use header::Header;
use rand::Rng;
@@ -46,21 +46,21 @@ impl AncientVerifier {
/// Verify the next block header, randomly choosing whether to do heavy or light
/// verification. If the block is the end of an epoch, updates the epoch verifier.
pub fn verify<R: Rng, F: Fn(u64) -> Result<Box<EpochVerifier>, Error>>(
pub fn verify<R: Rng>(
&self,
rng: &mut R,
header: &Header,
block: &[u8],
receipts: &[::receipt::Receipt],
load_verifier: F,
chain: &BlockChain,
) -> Result<(), ::error::Error> {
match rng.gen::<f32>() <= HEAVY_VERIFY_RATE {
true => self.cur_verifier.read().verify_heavy(header)?,
false => self.cur_verifier.read().verify_light(header)?,
}
if let EpochChange::Yes(num) = self.engine.is_epoch_end(header, Some(block), Some(receipts)) {
*self.cur_verifier.write() = load_verifier(num)?;
// ancient import will only use transitions obtained from the snapshot.
if let Some(transition) = chain.epoch_transition(header.number(), header.hash()) {
let v = self.engine.epoch_verifier(&header, &transition.proof).known_confirmed()?;
*self.cur_verifier.write() = v;
}
Ok(())

View File

@@ -32,7 +32,7 @@ use util::kvdb::*;
// other
use basic_types::Seal;
use block::*;
use blockchain::{BlockChain, BlockProvider, EpochTransition, TreeRoute, ImportRoute};
use blockchain::{BlockChain, BlockProvider, TreeRoute, ImportRoute};
use blockchain::extras::TransactionAddress;
use client::ancient_import::AncientVerifier;
use client::Error as ClientError;
@@ -42,7 +42,7 @@ use client::{
ChainNotify, PruningInfo, ProvingBlockChainClient,
};
use encoded;
use engines::Engine;
use engines::{Engine, EpochTransition};
use env_info::EnvInfo;
use env_info::LastHashes;
use error::{ImportError, ExecutionError, CallError, BlockError, ImportResult, Error as EthcoreError};
@@ -256,9 +256,31 @@ impl Client {
{
let chain = client.chain.read();
let gh = spec.genesis_header();
if chain.epoch_transition(0, spec.genesis_header().hash()).is_none() {
if chain.epoch_transition(0, gh.hash()).is_none() {
trace!(target: "client", "No genesis transition found.");
client.generate_epoch_proof(&gh, 0, &*chain);
let proof = client.with_proving_caller(
BlockId::Number(0),
|call| client.engine.genesis_epoch_data(&gh, call)
);
let proof = match proof {
Ok(proof) => proof,
Err(e) => {
warn!(target: "client", "Error generating genesis epoch data: {}. Snapshots generated may not be complete.", e);
Vec::new()
}
};
debug!(target: "client", "Obtained genesis transition proof: {:?}", proof);
let mut batch = DBTransaction::new();
chain.insert_epoch_transition(&mut batch, 0, EpochTransition {
block_hash: gh.hash(),
block_number: 0,
proof: proof,
});
client.db.read().write_buffered(batch);
}
}
@@ -405,7 +427,16 @@ impl Client {
let last_hashes = self.build_last_hashes(header.parent_hash().clone());
let db = self.state_db.lock().boxed_clone_canon(header.parent_hash());
let enact_result = enact_verified(block, engine, self.tracedb.read().tracing_enabled(), db, &parent, last_hashes, self.factories.clone());
let is_epoch_begin = chain.epoch_transition(parent.number(), *header.parent_hash()).is_some();
let enact_result = enact_verified(block,
engine,
self.tracedb.read().tracing_enabled(),
db,
&parent,
last_hashes,
self.factories.clone(),
is_epoch_begin,
);
let mut locked_block = enact_result.map_err(|e| {
warn!(target: "client", "Block import failed for #{} ({})\nError: {:?}", header.number(), header.hash(), e);
})?;
@@ -555,18 +586,9 @@ impl Client {
{
// closure for verifying a block.
let verify_with = |verifier: &AncientVerifier| -> Result<(), ::error::Error> {
// verify the block, passing a closure used to load an epoch verifier
// by number.
verifier.verify(
&mut *self.rng.lock(),
&header,
&block_bytes,
&receipts,
|epoch_num| chain.epoch_transition(epoch_num, hash)
.ok_or(BlockError::UnknownEpochTransition(epoch_num))
.map_err(Into::into)
.and_then(|t| self.engine.epoch_verifier(&header, &t.proof))
)
// verify the block, passing the chain for updating the epoch
// verifier.
verifier.verify(&mut *self.rng.lock(), &header, &chain)
};
// initialize the ancient block verifier if we don't have one already.
@@ -583,7 +605,8 @@ impl Client {
.map(|(_, t)| t.proof)
.expect("At least one epoch entry (genesis) always stored; qed");
let current_verifier = self.engine.epoch_verifier(&header, &current_epoch_data)?;
let current_verifier = self.engine.epoch_verifier(&header, &current_epoch_data)
.known_confirmed()?;
let current_verifier = AncientVerifier::new(self.engine.clone(), current_verifier);
verify_with(&current_verifier)?;
@@ -606,6 +629,7 @@ impl Client {
fn commit_block<B>(&self, block: B, hash: &H256, block_data: &[u8]) -> ImportRoute where B: IsBlock + Drain {
let number = block.header().number();
let parent = block.header().parent_hash().clone();
let header = block.header().clone(); // TODO: optimize and avoid copy.
let chain = self.chain.read();
// Commit results
@@ -619,28 +643,13 @@ impl Client {
let mut batch = DBTransaction::new();
// generate validation proof if the engine requires them.
// TODO: make conditional?
let entering_new_epoch = {
use engines::EpochChange;
match self.engine.is_epoch_end(block.header(), Some(block_data), Some(&receipts)) {
EpochChange::Yes(e) => Some((block.header().clone(), e)),
EpochChange::No => None,
EpochChange::Unsure(_) => {
warn!(target: "client", "Detected invalid engine implementation.");
warn!(target: "client", "Engine claims to require more block data, but everything provided.");
None
}
}
};
// CHECK! I *think* this is fine, even if the state_root is equal to another
// already-imported block of the same number.
// TODO: Prove it with a test.
let mut state = block.drain();
state.journal_under(&mut batch, number, hash).expect("DB commit failed");
let route = chain.insert_block(&mut batch, block_data, receipts);
let route = chain.insert_block(&mut batch, block_data, receipts.clone());
self.tracedb.read().import(&mut batch, TraceImportRequest {
traces: traces.into(),
@@ -655,65 +664,102 @@ impl Client {
// Final commit to the DB
self.db.read().write_buffered(batch);
chain.commit();
// check for epoch end. do this after writing first batch so we can prove
// transactions on the block's state.
// TODO: work these changes into the existing DBTransaction.
self.check_epoch_end_signal(&header, block_data, &receipts, &chain);
self.check_epoch_end(&header, &chain);
self.update_last_hashes(&parent, hash);
if let Err(e) = self.prune_ancient(state, &chain) {
warn!("Failed to prune ancient state data: {}", e);
}
if let Some((header, epoch)) = entering_new_epoch {
self.generate_epoch_proof(&header, epoch, &chain);
}
route
}
// generate an epoch transition proof at the given block, and write it into the given blockchain.
fn generate_epoch_proof(&self, header: &Header, epoch_number: u64, chain: &BlockChain) {
use std::cell::RefCell;
use std::collections::BTreeSet;
// check for epoch end signal and write pending transition if it occurs.
// state for the given block must be available.
fn check_epoch_end_signal(&self, header: &Header, block: &[u8], receipts: &[Receipt], chain: &BlockChain) {
use engines::EpochChange;
let mut batch = DBTransaction::new();
let hash = header.hash();
debug!(target: "client", "Generating validation proof for epoch {} at block {}",
epoch_number, hash);
match self.engine.signals_epoch_end(header, Some(block), Some(&receipts)) {
EpochChange::Yes(proof) => {
use engines::epoch::PendingTransition;
use engines::Proof;
// proof is two-part. state items read in lexicographical order,
// and the secondary "proof" part.
let read_values = RefCell::new(BTreeSet::new());
let block_id = BlockId::Hash(hash.clone());
let proof = {
let call = |a, d| {
let tx = self.contract_call_tx(block_id, a, d);
let (result, items) = self.prove_transaction(tx, block_id)
.ok_or_else(|| format!("Unable to make call to generate epoch proof."))?;
let proof = match proof {
Proof::Known(proof) => proof,
Proof::WithState(with_state) =>
match self.with_proving_caller(BlockId::Hash(hash), move |c| with_state(c)) {
Ok(proof) => proof,
Err(e) => {
warn!(target: "client", "Failed to generate transition proof for block {}: {}", hash, e);
warn!(target: "client", "Snapshots produced by this client may be incomplete");
read_values.borrow_mut().extend(items);
Ok(result)
};
Vec::new()
}
},
};
self.engine.epoch_proof(&header, &call)
};
debug!(target: "client", "Block {} signals epoch end.", hash);
// insert into database, using the generated proof.
match proof {
Ok(proof) => {
chain.insert_epoch_transition(&mut batch, epoch_number, EpochTransition {
block_hash: hash.clone(),
block_number: header.number(),
proof: proof,
state_proof: read_values.into_inner().into_iter().collect(),
});
// write pending transition to DB.
let mut batch = DBTransaction::new();
let pending = PendingTransition { proof: proof };
chain.insert_pending_transition(&mut batch, hash, pending);
self.db.read().write_buffered(batch);
}
Err(e) => {
warn!(target: "client", "Error generating epoch change proof for block {}: {}", hash, e);
warn!(target: "client", "Snapshots generated by this node will be incomplete.");
},
EpochChange::No => {},
EpochChange::Unsure(_) => {
warn!(target: "client", "Detected invalid engine implementation.");
warn!(target: "client", "Engine claims to require more block data, but everything provided.");
}
}
}
// check for ending of epoch and write transition if it occurs.
fn check_epoch_end<'a>(&self, header: &'a Header, chain: &BlockChain) {
let is_epoch_end = self.engine.is_epoch_end(
header,
&(|hash| chain.block_header(&hash)),
&(|hash| chain.get_pending_transition(hash)), // TODO: limit to current epoch.
);
if let Some(proof) = is_epoch_end {
debug!(target: "client", "Epoch transition at block {}", header.hash());
let mut batch = DBTransaction::new();
chain.insert_epoch_transition(&mut batch, header.number(), EpochTransition {
block_hash: header.hash(),
block_number: header.number(),
proof: proof,
});
self.db.read().write_buffered(batch);
}
}
// use a state-proving closure for the given block.
fn with_proving_caller<F, T>(&self, id: BlockId, with_call: F) -> T
where F: FnOnce(&::engines::Call) -> T
{
let call = |a, d| {
let tx = self.contract_call_tx(id, a, d);
let (result, items) = self.prove_transaction(tx, id)
.ok_or_else(|| format!("Unable to make call. State unavailable?"))?;
let items = items.into_iter().map(|x| x.to_vec()).collect();
Ok((result, items))
};
with_call(&call)
}
// prune ancient states until below the memory limit or only the minimum amount remain.
fn prune_ancient(&self, mut state_db: StateDB, chain: &BlockChain) -> Result<(), ClientError> {
let number = match state_db.journal_db().latest_era() {
@@ -1622,17 +1668,21 @@ impl MiningBlockChainClient for Client {
let engine = &*self.engine;
let chain = self.chain.read();
let h = chain.best_block_hash();
let best_header = &chain.block_header(&h)
.expect("h is best block hash: so its header must exist: qed");
let is_epoch_begin = chain.epoch_transition(best_header.number(), h).is_some();
let mut open_block = OpenBlock::new(
engine,
self.factories.clone(),
false, // TODO: this will need to be parameterised once we want to do immediate mining insertion.
self.state_db.lock().boxed_clone_canon(&h),
&chain.block_header(&h).expect("h is best block hash: so its header must exist: qed"),
best_header,
self.build_last_hashes(h.clone()),
author,
gas_range_target,
extra_data,
is_epoch_begin,
).expect("OpenBlock::new only fails if parent state root invalid; state root of best block's header is never invalid; qed");
// Add uncles
@@ -1717,6 +1767,10 @@ impl EngineClient for Client {
fn broadcast_consensus_message(&self, message: Bytes) {
self.notify(|notify| notify.broadcast(message.clone()));
}
fn epoch_transition_for(&self, parent_hash: H256) -> Option<::engines::EpochTransition> {
self.chain.read().epoch_transition_for(parent_hash)
}
}
impl ProvingBlockChainClient for Client {
@@ -1746,7 +1800,10 @@ impl ProvingBlockChainClient for Client {
match res {
Err(ExecutionError::Internal(_)) => None,
Err(_) => Some((Vec::new(), state.drop().1.extract_proof())),
Err(e) => {
trace!(target: "client", "Proved call failed: {}", e);
Some((Vec::new(), state.drop().1.extract_proof()))
}
Ok(res) => Some((res.output, state.drop().1.extract_proof())),
}
}

View File

@@ -372,7 +372,8 @@ impl MiningBlockChainClient for TestBlockChainClient {
Arc::new(last_hashes),
author,
gas_range_target,
extra_data
extra_data,
false,
).expect("Opening block for tests will not fail.");
// TODO [todr] Override timestamp for predictability (set_timestamp_now kind of sucks)
open_block.set_timestamp(*self.latest_block_timestamp.read());
@@ -786,4 +787,8 @@ impl EngineClient for TestBlockChainClient {
}
fn broadcast_consensus_message(&self, _message: Bytes) {}
fn epoch_transition_for(&self, _block_hash: H256) -> Option<::engines::EpochTransition> {
None
}
}

View File

@@ -311,6 +311,13 @@ pub trait EngineClient: MiningBlockChainClient {
/// Broadcast a consensus message to the network.
fn broadcast_consensus_message(&self, message: Bytes);
/// Get the transition to the epoch the given parent hash is part of
/// or transitions to.
/// This will give the epoch that any children of this parent belong to.
///
/// The block corresponding the the parent hash must be stored already.
fn epoch_transition_for(&self, parent_hash: H256) -> Option<::engines::EpochTransition>;
}
/// Extended client interface for providing proofs of the state.