Aura: Broadcast empty step messages instead of creating empty blocks (#7605)

* aura: broadcast empty step message instead of sealing empty block

* aura: add empty_step messages to seal

* aura: include parent_hash in empty step message

* aura: verify received empty step messages

* aura: verify empty step messages in block

* aura: fix dead lock on empty_steps

* aura: fix EmptyStep Encodable

* aura: take number of empty steps into account in chain score

* aura: use empty step signers for finality

* aura: add empty "empty step" messages to seal when reading from spec

* aura: fix EmptyStep rlp encoding

* aura: use Vec<u8> instead of Bytes

* aura: fix block empty step verification

* Update .gitlab-ci.yml

fix lint

* aura: fix accumulation of empty step signatures for finality

* aura: include empty steps in seal signature

* aura: configurable max number of empty steps

* engine: pass block header to seal_fields method

This is necessary to make the number of seal fields dynamic,
e.g. activating a transition on a certain block number that changes
the seal.

* aura: add transition to enable empty step messages

* aura: clear old empty step messages on verify_block_external

* aura: ignore empty step messages from the future

* aura: report skipped primaries when empty steps are not enabled

* aura: fix tests

* aura: report misbehavior

* aura: add tests for rolling finality with multiple signatures

* engine: fix validator set test

In this test the block validation wasn't failing because the block was in the
future (expected failure) but was instead failing because the author of the
block isn't the expected authority. Since we added reporting of blocks produced
by the wrong authority this test started failing.

* aura: reward all the authors of empty step messages

* aura: fix reward attribution for new blocks

* aura: add tests for empty steps broadcasting and inclusion in blocks

* aura: reduce size of empty step messages in seal

* aura: add test for empty step inclusion in blocks

* aura: add test for rewarding of empty steps

* aura: add test for empty steps validation

* aura: fix rlp encoding of sealed empty step

* aura: fix grumbles
This commit is contained in:
André Silva 2018-02-15 00:39:29 +00:00 committed by Robert Habermeier
parent 5d6e7e1439
commit aab63c339d
17 changed files with 953 additions and 131 deletions

View File

@ -630,4 +630,8 @@ impl<T: ChainDataFetcher> ::ethcore::client::EngineClient for Client<T> {
fn block_number(&self, id: BlockId) -> Option<BlockNumber> {
self.block_header(id).map(|hdr| hdr.number())
}
fn block_header(&self, id: BlockId) -> Option<encoded::Header> {
Client::block_header(self, id)
}
}

View File

@ -0,0 +1,51 @@
{
"name": "TestAuthorityRoundEmptySteps",
"engine": {
"authorityRound": {
"params": {
"stepDuration": 1,
"startStep": 2,
"validators": {
"list": [
"0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e",
"0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1"
]
},
"blockReward": "10",
"immediateTransitions": true,
"emptyStepsTransition": "1",
"maximumEmptySteps": "2"
}
}
},
"params": {
"gasLimitBoundDivisor": "0x0400",
"accountStartNonce": "0x0",
"maximumExtraDataSize": "0x20",
"minGasLimit": "0x1388",
"networkID" : "0x69"
},
"genesis": {
"seal": {
"authorityRound": {
"step": "0x0",
"signature": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
},
"difficulty": "0x20000",
"author": "0x0000000000000000000000000000000000000000",
"timestamp": "0x00",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x",
"gasLimit": "0x222222"
},
"accounts": {
"0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
"0000000000000000000000000000000000000002": { "balance": "1", "nonce": "1048576", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
"0000000000000000000000000000000000000003": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
"0000000000000000000000000000000000000004": { "balance": "1", "nonce": "1048576", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
"9cce34f7ab185c7aba1b7c8140d620b4bda941d6": { "balance": "1606938044258990275541962092341162602522202993782792835301376", "nonce": "1048576" },
"7d577a597b2742b498cb5cf0c26cdcd726d39e6e": { "balance": "1000000000" },
"82a978b3f5962a5b0957d9ee9eef472ee55b42f1": { "balance": "1000000000" }
}
}

View File

@ -543,9 +543,11 @@ impl LockedBlock {
///
/// NOTE: This does not check the validity of `seal` with the engine.
pub fn seal(self, engine: &EthEngine, seal: Vec<Bytes>) -> Result<SealedBlock, BlockError> {
let expected_seal_fields = engine.seal_fields(self.header());
let mut s = self;
if seal.len() != engine.seal_fields() {
return Err(BlockError::InvalidSealArity(Mismatch{expected: engine.seal_fields(), found: seal.len()}));
if seal.len() != expected_seal_fields {
return Err(BlockError::InvalidSealArity(
Mismatch { expected: expected_seal_fields, found: seal.len() }));
}
s.block.header.set_seal(seal);
Ok(SealedBlock { block: s.block, uncle_bytes: s.uncle_bytes })

View File

@ -2002,6 +2002,10 @@ impl super::traits::EngineClient for Client {
fn block_number(&self, id: BlockId) -> Option<BlockNumber> {
BlockChainClient::block_number(self, id)
}
fn block_header(&self, id: BlockId) -> Option<::encoded::Header> {
BlockChainClient::block_header(self, id)
}
}
impl ProvingBlockChainClient for Client {

View File

@ -834,4 +834,8 @@ impl super::traits::EngineClient for TestBlockChainClient {
fn block_number(&self, id: BlockId) -> Option<BlockNumber> {
BlockChainClient::block_number(self, id)
}
fn block_header(&self, id: BlockId) -> Option<::encoded::Header> {
BlockChainClient::block_header(self, id)
}
}

View File

@ -339,6 +339,9 @@ pub trait EngineClient: Sync + Send {
/// Get a block number by ID.
fn block_number(&self, id: BlockId) -> Option<BlockNumber>;
/// Get raw block header data by block id.
fn block_header(&self, id: BlockId) -> Option<encoded::Header>;
}
/// Extended client interface for providing proofs of the state.

View File

@ -30,7 +30,7 @@ pub struct UnknownValidator;
/// Rolling finality checker for authority round consensus.
/// Stores a chain of unfinalized hashes that can be pushed onto.
pub struct RollingFinality {
headers: VecDeque<(H256, Address)>,
headers: VecDeque<(H256, Vec<Address>)>,
signers: SimpleList,
sign_count: HashMap<Address, usize>,
last_pushed: Option<H256>,
@ -52,28 +52,31 @@ impl RollingFinality {
///
/// Fails if any provided signature isn't part of the signers set.
pub fn build_ancestry_subchain<I>(&mut self, iterable: I) -> Result<(), UnknownValidator>
where I: IntoIterator<Item=(H256, Address)>
where I: IntoIterator<Item=(H256, Vec<Address>)>
{
self.clear();
for (hash, signer) in iterable {
if !self.signers.contains(&signer) { return Err(UnknownValidator) }
for (hash, signers) in iterable {
if signers.iter().any(|s| !self.signers.contains(s)) { return Err(UnknownValidator) }
if self.last_pushed.is_none() { self.last_pushed = Some(hash) }
// break when we've got our first finalized block.
{
let current_signed = self.sign_count.len();
let would_be_finalized = (current_signed + 1) * 2 > self.signers.len();
let entry = self.sign_count.entry(signer);
if let (true, &Entry::Vacant(_)) = (would_be_finalized, &entry) {
let new_signers = signers.iter().filter(|s| !self.sign_count.contains_key(s)).count();
let would_be_finalized = (current_signed + new_signers) * 2 > self.signers.len();
if would_be_finalized {
trace!(target: "finality", "Encountered already finalized block {}", hash);
break
}
*entry.or_insert(0) += 1;
for signer in signers.iter() {
*self.sign_count.entry(*signer).or_insert(0) += 1;
}
}
self.headers.push_front((hash, signer));
self.headers.push_front((hash, signers));
}
trace!(target: "finality", "Rolling finality state: {:?}", self.headers);
@ -103,30 +106,35 @@ impl RollingFinality {
/// Fails if `signer` isn't a member of the active validator set.
/// Returns a list of all newly finalized headers.
// TODO: optimize with smallvec.
pub fn push_hash(&mut self, head: H256, signer: Address) -> Result<Vec<H256>, UnknownValidator> {
if !self.signers.contains(&signer) { return Err(UnknownValidator) }
pub fn push_hash(&mut self, head: H256, signers: Vec<Address>) -> Result<Vec<H256>, UnknownValidator> {
if signers.iter().any(|s| !self.signers.contains(s)) { return Err(UnknownValidator) }
self.headers.push_back((head, signer));
*self.sign_count.entry(signer).or_insert(0) += 1;
for signer in signers.iter() {
*self.sign_count.entry(*signer).or_insert(0) += 1;
}
self.headers.push_back((head, signers));
let mut newly_finalized = Vec::new();
while self.sign_count.len() * 2 > self.signers.len() {
let (hash, signer) = self.headers.pop_front()
let (hash, signers) = self.headers.pop_front()
.expect("headers length always greater than sign count length; qed");
newly_finalized.push(hash);
match self.sign_count.entry(signer) {
Entry::Occupied(mut entry) => {
// decrement count for this signer and purge on zero.
*entry.get_mut() -= 1;
for signer in signers {
match self.sign_count.entry(signer) {
Entry::Occupied(mut entry) => {
// decrement count for this signer and purge on zero.
*entry.get_mut() -= 1;
if *entry.get() == 0 {
entry.remove();
if *entry.get() == 0 {
entry.remove();
}
}
Entry::Vacant(_) => panic!("all hashes in `header` should have entries in `sign_count` for their signers; qed"),
}
Entry::Vacant(_) => panic!("all hashes in `header` should have an entry in `sign_count` for their signer; qed"),
}
}
@ -137,7 +145,7 @@ impl RollingFinality {
}
}
pub struct Iter<'a>(::std::collections::vec_deque::Iter<'a, (H256, Address)>);
pub struct Iter<'a>(::std::collections::vec_deque::Iter<'a, (H256, Vec<Address>)>);
impl<'a> Iterator for Iter<'a> {
type Item = H256;
@ -153,10 +161,10 @@ mod tests {
use super::RollingFinality;
#[test]
fn rejects_unknown_signer() {
let signers = (0..3).map(|_| Address::random()).collect();
let mut finality = RollingFinality::blank(signers);
assert!(finality.push_hash(H256::random(), Address::random()).is_err());
fn rejects_unknown_signers() {
let signers = (0..3).map(|_| Address::random()).collect::<Vec<_>>();
let mut finality = RollingFinality::blank(signers.clone());
assert!(finality.push_hash(H256::random(), vec![signers[0], Address::random()]).is_err());
}
#[test]
@ -169,19 +177,29 @@ mod tests {
// 3 / 6 signers is < 51% so no finality.
for (i, hash) in hashes.iter().take(6).cloned().enumerate() {
let i = i % 3;
assert!(finality.push_hash(hash, signers[i]).unwrap().len() == 0);
assert!(finality.push_hash(hash, vec![signers[i]]).unwrap().len() == 0);
}
// after pushing a block signed by a fourth validator, the first four
// blocks of the unverified chain become verified.
assert_eq!(finality.push_hash(hashes[6], signers[4]).unwrap(),
assert_eq!(finality.push_hash(hashes[6], vec![signers[4]]).unwrap(),
vec![hashes[0], hashes[1], hashes[2], hashes[3]]);
}
#[test]
fn finalize_multiple_signers() {
let signers: Vec<_> = (0..6).map(|_| Address::random()).collect();
let mut finality = RollingFinality::blank(signers.clone());
let hash = H256::random();
// after pushing a block signed by four validators, it becomes verified right away.
assert_eq!(finality.push_hash(hash, signers[0..4].to_vec()).unwrap(), vec![hash]);
}
#[test]
fn from_ancestry() {
let signers: Vec<_> = (0..6).map(|_| Address::random()).collect();
let hashes: Vec<_> = (0..12).map(|i| (H256::random(), signers[i % 6])).collect();
let hashes: Vec<_> = (0..12).map(|i| (H256::random(), vec![signers[i % 6]])).collect();
let mut finality = RollingFinality::blank(signers.clone());
finality.build_ancestry_subchain(hashes.iter().rev().cloned()).unwrap();
@ -189,4 +207,20 @@ mod tests {
assert_eq!(finality.unfinalized_hashes().count(), 3);
assert_eq!(finality.subchain_head(), Some(hashes[11].0));
}
#[test]
fn from_ancestry_multiple_signers() {
let signers: Vec<_> = (0..6).map(|_| Address::random()).collect();
let hashes: Vec<_> = (0..12).map(|i| {
(H256::random(), vec![signers[i % 6], signers[(i + 1) % 6], signers[(i + 2) % 6]])
}).collect();
let mut finality = RollingFinality::blank(signers.clone());
finality.build_ancestry_subchain(hashes.iter().rev().cloned()).unwrap();
// only the last hash has < 51% of authorities' signatures
assert_eq!(finality.unfinalized_hashes().count(), 1);
assert_eq!(finality.unfinalized_hashes().next(), Some(hashes[11].0));
assert_eq!(finality.subchain_head(), Some(hashes[11].0));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -97,7 +97,7 @@ impl Engine<EthereumMachine> for BasicAuthority {
fn machine(&self) -> &EthereumMachine { &self.machine }
// One field - the signature
fn seal_fields(&self) -> usize { 1 }
fn seal_fields(&self, _header: &Header) -> usize { 1 }
fn seals_internally(&self) -> Option<bool> {
Some(self.signer.read().is_some())

View File

@ -181,7 +181,7 @@ pub trait Engine<M: Machine>: Sync + Send {
fn machine(&self) -> &M;
/// The number of additional header fields required for this engine.
fn seal_fields(&self) -> usize { 0 }
fn seal_fields(&self, _header: &M::Header) -> usize { 0 }
/// Additional engine-specific information for the user/developer concerning `header`.
fn extra_info(&self, _header: &M::Header) -> BTreeMap<String, String> { BTreeMap::new() }
@ -406,27 +406,27 @@ pub mod common {
use trace::{Tracer, ExecutiveTracer, RewardType};
use state::CleanupMode;
use ethereum_types::U256;
use ethereum_types::{Address, U256};
/// Give reward and trace.
pub fn bestow_block_reward(block: &mut ExecutedBlock, reward: U256) -> Result<(), Error> {
pub fn bestow_block_reward(block: &mut ExecutedBlock, reward: U256, receiver: Address) -> Result<(), Error> {
let fields = block.fields_mut();
// Bestow block reward
let res = fields.state.add_balance(fields.header.author(), &reward, CleanupMode::NoEmpty)
let res = fields.state.add_balance(&receiver, &reward, CleanupMode::NoEmpty)
.map_err(::error::Error::from)
.and_then(|_| fields.state.commit());
let block_author = fields.header.author().clone();
fields.traces.as_mut().map(move |traces| {
let mut tracer = ExecutiveTracer::default();
tracer.trace_reward(block_author, reward, RewardType::Block);
tracer.trace_reward(receiver, reward, RewardType::Block);
traces.push(tracer.drain())
});
// Commit state so that we can actually figure out the state root.
if let Err(ref e) = res {
warn!("Encountered error on bestowing reward: {}", e);
}
res
}
}

View File

@ -441,7 +441,7 @@ impl Engine<EthereumMachine> for Tendermint {
fn name(&self) -> &str { "Tendermint" }
/// (consensus view, proposal signature, authority signatures)
fn seal_fields(&self) -> usize { 3 }
fn seal_fields(&self, _header: &Header) -> usize { 3 }
fn machine(&self) -> &EthereumMachine { &self.machine }
@ -561,7 +561,8 @@ impl Engine<EthereumMachine> for Tendermint {
/// Apply the block reward on finalisation of the block.
fn on_close_block(&self, block: &mut ExecutedBlock) -> Result<(), Error>{
::engines::common::bestow_block_reward(block, self.block_reward)
let author = *block.header().author();
::engines::common::bestow_block_reward(block, self.block_reward, author)
}
fn verify_local_seal(&self, _header: &Header) -> Result<(), Error> {
@ -570,7 +571,8 @@ impl Engine<EthereumMachine> for Tendermint {
fn verify_block_basic(&self, header: &Header) -> Result<(), Error> {
let seal_length = header.seal().len();
if seal_length == self.seal_fields() {
let expected_seal_fields = self.seal_fields(header);
if seal_length == expected_seal_fields {
// Either proposal or commit.
if (header.seal()[1] == ::rlp::NULL_RLP)
!= (header.seal()[2] == ::rlp::EMPTY_LIST_RLP) {
@ -581,7 +583,7 @@ impl Engine<EthereumMachine> for Tendermint {
}
} else {
Err(BlockError::InvalidSealArity(
Mismatch { expected: self.seal_fields(), found: seal_length }
Mismatch { expected: expected_seal_fields, found: seal_length }
).into())
}
}
@ -777,7 +779,6 @@ mod tests {
use block::*;
use error::{Error, BlockError};
use header::Header;
use client::ChainNotify;
use miner::MinerService;
use tests::helpers::*;
use account_provider::AccountProvider;
@ -837,17 +838,6 @@ mod tests {
addr
}
#[derive(Default)]
struct TestNotify {
messages: RwLock<Vec<Bytes>>,
}
impl ChainNotify for TestNotify {
fn broadcast(&self, data: Vec<u8>) {
self.messages.write().push(data);
}
}
#[test]
fn has_valid_metadata() {
let engine = Spec::new_test_tendermint().engine;

View File

@ -174,7 +174,7 @@ mod tests {
// Check a block that is a bit in future, reject it but don't report the validator.
let mut header = Header::default();
let seal = vec![encode(&5u8).into_vec(), encode(&(&H520::default() as &[u8])).into_vec()];
let seal = vec![encode(&4u8).into_vec(), encode(&(&H520::default() as &[u8])).into_vec()];
header.set_seal(seal);
header.set_author(v1);
header.set_number(2);

View File

@ -169,11 +169,11 @@ impl Engine<EthereumMachine> for Arc<Ethash> {
fn machine(&self) -> &EthereumMachine { &self.machine }
// Two fields - nonce and mix.
fn seal_fields(&self) -> usize { 2 }
fn seal_fields(&self, _header: &Header) -> usize { 2 }
/// Additional engine-specific information for the user/developer concerning `header`.
fn extra_info(&self, header: &Header) -> BTreeMap<String, String> {
if header.seal().len() == self.seal_fields() {
if header.seal().len() == self.seal_fields(header) {
map![
"nonce".to_owned() => format!("0x{:x}", header.nonce()),
"mixHash".to_owned() => format!("0x{:x}", header.mix_hash())
@ -265,9 +265,10 @@ impl Engine<EthereumMachine> for Arc<Ethash> {
fn verify_block_basic(&self, header: &Header) -> Result<(), Error> {
// check the seal fields.
if header.seal().len() != self.seal_fields() {
let expected_seal_fields = self.seal_fields(header);
if header.seal().len() != expected_seal_fields {
return Err(From::from(BlockError::InvalidSealArity(
Mismatch { expected: self.seal_fields(), found: header.seal().len() }
Mismatch { expected: expected_seal_fields, found: header.seal().len() }
)));
}
UntrustedRlp::new(&header.seal()[0]).as_val::<H256>()?;
@ -296,9 +297,10 @@ impl Engine<EthereumMachine> for Arc<Ethash> {
}
fn verify_block_unordered(&self, header: &Header) -> Result<(), Error> {
if header.seal().len() != self.seal_fields() {
let expected_seal_fields = self.seal_fields(header);
if header.seal().len() != expected_seal_fields {
return Err(From::from(BlockError::InvalidSealArity(
Mismatch { expected: self.seal_fields(), found: header.seal().len() }
Mismatch { expected: expected_seal_fields, found: header.seal().len() }
)));
}
let result = self.pow.compute_light(header.number() as u64, &header.bare_hash().0, header.nonce().low_u64());

View File

@ -762,6 +762,13 @@ impl Spec {
load_bundled!("authority_round")
}
/// Create a new Spec with AuthorityRound consensus which does internal sealing (not
/// requiring work) with empty step messages enabled.
/// Accounts with secrets keccak("0") and keccak("1") are the validators.
pub fn new_test_round_empty_steps() -> Self {
load_bundled!("authority_round_empty_steps")
}
/// Create a new Spec with Tendermint consensus which does internal sealing (not requiring
/// work).
/// Account keccak("0") and keccak("1") are a authorities.

View File

@ -19,7 +19,7 @@ use ethereum_types::{H256, U256};
use block::{OpenBlock, Drain};
use blockchain::{BlockChain, Config as BlockChainConfig};
use bytes::Bytes;
use client::{BlockChainClient, Client, ClientConfig};
use client::{BlockChainClient, ChainNotify, Client, ClientConfig};
use ethereum::ethash::EthashParams;
use ethkey::KeyPair;
use evm::Factory as EvmFactory;
@ -29,6 +29,7 @@ use header::Header;
use io::*;
use machine::EthashExtensions;
use miner::Miner;
use parking_lot::RwLock;
use rlp::{self, RlpStream};
use spec::*;
use state_db::StateDB;
@ -388,3 +389,14 @@ pub fn get_default_ethash_params() -> EthashParams {
expip2_duration_limit: 30,
}
}
#[derive(Default)]
pub struct TestNotify {
pub messages: RwLock<Vec<Bytes>>,
}
impl ChainNotify for TestNotify {
fn broadcast(&self, data: Vec<u8>) {
self.messages.write().push(data);
}
}

View File

@ -242,9 +242,10 @@ pub fn verify_block_final(expected: &Header, got: &Header) -> Result<(), Error>
/// Check basic header parameters.
pub fn verify_header_params(header: &Header, engine: &EthEngine, is_full: bool) -> Result<(), Error> {
if header.seal().len() != engine.seal_fields() {
let expected_seal_fields = engine.seal_fields(header);
if header.seal().len() != expected_seal_fields {
return Err(From::from(BlockError::InvalidSealArity(
Mismatch { expected: engine.seal_fields(), found: header.seal().len() }
Mismatch { expected: expected_seal_fields, found: header.seal().len() }
)));
}

View File

@ -49,6 +49,12 @@ pub struct AuthorityRoundParams {
/// Maximum number of accepted uncles.
#[serde(rename="maximumUncleCount")]
pub maximum_uncle_count: Option<Uint>,
/// Block at which empty step messages should start.
#[serde(rename="emptyStepsTransition")]
pub empty_steps_transition: Option<Uint>,
/// Maximum number of accepted empty steps.
#[serde(rename="maximumEmptySteps")]
pub maximum_empty_steps: Option<Uint>,
}
/// Authority engine deserialization.