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:
committed by
Arkadiy Paronyan
parent
3637c14da5
commit
d069b98b45
@@ -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(())
|
||||
|
||||
@@ -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, ¤t_epoch_data)?;
|
||||
let current_verifier = self.engine.epoch_verifier(&header, ¤t_epoch_data)
|
||||
.known_confirmed()?;
|
||||
let current_verifier = AncientVerifier::new(self.engine.clone(), current_verifier);
|
||||
|
||||
verify_with(¤t_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())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user