diff --git a/ethcore/src/engines/authority_round/mod.rs b/ethcore/src/engines/authority_round/mod.rs index e34c4ea30b8..0b6e287b5c0 100644 --- a/ethcore/src/engines/authority_round/mod.rs +++ b/ethcore/src/engines/authority_round/mod.rs @@ -15,6 +15,21 @@ // along with Parity Ethereum. If not, see . //! A blockchain engine that supports a non-instant BFT proof-of-authority. +//! +//! It is recommended to use the `two_thirds_majority_transition` option, to defend against the +//! ["Attack of the Clones"](https://arxiv.org/pdf/1902.10244.pdf). Newly started networks can +//! set this option to `0`, to use a 2/3 quorum from the beginning. +//! +//! To support on-chain governance, the [ValidatorSet] is pluggable: Aura supports simple +//! constant lists of validators as well as smart contract-based dynamic validator sets. +//! Misbehavior is reported to the [ValidatorSet] as well, so that e.g. governance contracts +//! can penalize or ban attacker's nodes. +//! +//! * "Benign" misbehavior are faults that can happen in normal operation, like failing +//! to propose a block in your slot, which could be due to a temporary network outage, or +//! wrong timestamps (due to out-of-sync clocks). +//! * "Malicious" reports are made only if the sender misbehaved deliberately (or due to a +//! software bug), e.g. if they proposed multiple blocks with the same step number. use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::{cmp::{self, Ordering}, fmt}; @@ -545,6 +560,8 @@ pub struct AuthorityRound { posdao_transition: Option, /// The addresses of a contracts that determine the block gas limit. block_gas_limit_contract_transitions: BTreeMap, + /// History of step hashes recently received from peers. + received_step_hashes: RwLock>, } // header-chain validator. @@ -635,7 +652,7 @@ fn header_expected_seal_fields(header: &Header, empty_steps_transition: u64) -> fn header_step(header: &Header, empty_steps_transition: u64) -> Result { Rlp::new(&header.seal().get(0).unwrap_or_else(|| - panic!("was either checked with verify_block_basic or is genesis; has {} fields; qed (Make sure the spec + panic!("was either checked with verify_block_basic or is genesis; has {} fields; qed (Make sure the spec \ file has a correct genesis seal)", header_expected_seal_fields(header, empty_steps_transition)) )) .as_val() @@ -819,6 +836,7 @@ impl AuthorityRound { randomness_contract_address: our_params.randomness_contract_address, posdao_transition: our_params.posdao_transition, block_gas_limit_contract_transitions: our_params.block_gas_limit_contract_transitions, + received_step_hashes: RwLock::new(Default::default()), }); // Do not initialize timeouts for tests. @@ -1528,6 +1546,26 @@ impl Engine for AuthorityRound { Err(EngineError::DoubleVote(*header.author()))?; } + // Report malice if the validator produced other sibling blocks in the same step. + let received_step_key = (step, *header.author()); + let new_hash = header.hash(); + if self.received_step_hashes.read().get(&received_step_key).map_or(false, |h| *h != new_hash) { + trace!(target: "engine", "Validator {} produced sibling blocks in the same step", header.author()); + self.validators.report_malicious(header.author(), set_number, header.number(), Default::default()); + } else { + self.received_step_hashes.write().insert(received_step_key, new_hash); + } + + // Remove hash records older than two full rounds of steps (picked as a reasonable trade-off between + // memory consumption and fault-tolerance). + let sibling_malice_detection_period = 2 * validators.count(&parent.hash()) as u64; + let oldest_step = parent_step.saturating_sub(sibling_malice_detection_period); + if oldest_step > 0 { + let mut rsh = self.received_step_hashes.write(); + let new_rsh = rsh.split_off(&(oldest_step, Address::zero())); + *rsh = new_rsh; + } + // If empty step messages are enabled we will validate the messages in the seal, missing messages are not // reported as there's no way to tell whether the empty step message was never sent or simply not included. let empty_steps_len = if header.number() >= self.empty_steps_transition { @@ -2156,6 +2194,38 @@ mod tests { assert_eq!(last_benign.load(AtomicOrdering::SeqCst), 2); } + #[test] + fn reports_multiple_blocks_per_step() { + let tap = AccountProvider::transient_provider(); + let addr0 = tap.insert_account(keccak("0").into(), &"0".into()).unwrap(); + let addr1 = tap.insert_account(keccak("1").into(), &"1".into()).unwrap(); + + let validator_set = TestSet::from_validators(vec![addr0, addr1]); + let aura = aura(|p| p.validators = Box::new(validator_set.clone())); + + aura.set_signer(Box::new((Arc::new(tap), addr0, "0".into()))); + + let mut parent_header: Header = Header::default(); + parent_header.set_number(2); + parent_header.set_seal(vec![encode(&1usize)]); + parent_header.set_gas_limit("222222".parse::().unwrap()); + let mut header: Header = Header::default(); + header.set_number(3); + header.set_difficulty(calculate_score(1, 2, 0)); + header.set_gas_limit("222222".parse::().unwrap()); + header.set_seal(vec![encode(&2usize)]); + header.set_author(addr1); + + // First sibling block. + assert!(aura.verify_block_family(&header, &parent_header).is_ok()); + assert_eq!(validator_set.last_malicious(), 0); + + // Second sibling block: should be reported. + header.set_gas_limit("222223".parse::().unwrap()); + assert!(aura.verify_block_family(&header, &parent_header).is_ok()); + assert_eq!(validator_set.last_malicious(), 3); + } + #[test] fn test_uncles_transition() { let aura = aura(|params| { diff --git a/ethcore/src/engines/validator_set/test.rs b/ethcore/src/engines/validator_set/test.rs index c66ff14ad4e..d42ce97eb71 100644 --- a/ethcore/src/engines/validator_set/test.rs +++ b/ethcore/src/engines/validator_set/test.rs @@ -30,6 +30,7 @@ use machine::{AuxiliaryData, Call, EthereumMachine}; use super::{ValidatorSet, SimpleList}; /// Set used for testing with a single validator. +#[derive(Clone, Debug)] pub struct TestSet { validator: SimpleList, last_malicious: Arc, @@ -50,6 +51,22 @@ impl TestSet { last_benign, } } + + pub fn from_validators(validators: Vec
) -> Self { + TestSet { + validator: SimpleList::new(validators), + last_malicious: Default::default(), + last_benign: Default::default(), + } + } + + pub fn last_malicious(&self) -> usize { + self.last_malicious.load(AtomicOrdering::SeqCst) + } + + pub fn last_benign(&self) -> usize { + self.last_benign.load(AtomicOrdering::SeqCst) + } } impl HeapSizeOf for TestSet {