From 175051bac706a7a5779c014ff0ec4098e6f7b522 Mon Sep 17 00:00:00 2001 From: Andreas Fackler Date: Wed, 21 Aug 2019 14:55:51 +0200 Subject: [PATCH] Add a 2/3 quorum option to Authority Round. (#10909) * Add a 2/3 quorum option to Authority Round. This prevents the "Attack of the Clones": https://arxiv.org/pdf/1902.10244.pdf * Make RollingFinality::remove_signers more tolerant. * Rename parameter to two_thirds_majority_transition. --- .../src/engines/authority_round/finality.rs | 204 +++++++++++++----- ethcore/src/engines/authority_round/mod.rs | 40 +++- json/src/spec/authority_round.rs | 2 + 3 files changed, 180 insertions(+), 66 deletions(-) diff --git a/ethcore/src/engines/authority_round/finality.rs b/ethcore/src/engines/authority_round/finality.rs index e1f050393ea..76b086efccc 100644 --- a/ethcore/src/engines/authority_round/finality.rs +++ b/ethcore/src/engines/authority_round/finality.rs @@ -20,6 +20,7 @@ use std::collections::{VecDeque}; use std::collections::hash_map::{HashMap, Entry}; use ethereum_types::{H256, Address}; +use types::BlockNumber; use engines::validator_set::SimpleList; @@ -30,21 +31,24 @@ 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, Vec
)>, + headers: VecDeque<(H256, BlockNumber, Vec
)>, signers: SimpleList, sign_count: HashMap, last_pushed: Option, + /// First block for which a 2/3 quorum (instead of 1/2) is required. + two_thirds_majority_transition: BlockNumber, } impl RollingFinality { /// Create a blank finality checker under the given validator set. - pub fn blank(signers: Vec
) -> Self { + pub fn blank(signers: Vec
, two_thirds_majority_transition: BlockNumber) -> Self { trace!(target: "finality", "Instantiating blank RollingFinality with {} signers: {:?}", signers.len(), signers); RollingFinality { headers: VecDeque::new(), signers: SimpleList::new(signers), sign_count: HashMap::new(), last_pushed: None, + two_thirds_majority_transition, } } @@ -53,38 +57,28 @@ impl RollingFinality { /// /// Fails if any provided signature isn't part of the signers set. pub fn build_ancestry_subchain(&mut self, iterable: I) -> Result<(), UnknownValidator> - where I: IntoIterator)> + where I: IntoIterator)>, { self.clear(); - for (hash, signers) in iterable { + for (hash, number, 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) } - + self.add_signers(&signers); + self.headers.push_front((hash, number, signers)); // break when we've got our first finalized block. - { - let current_signed = self.sign_count.len(); - - 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 - } - - for signer in signers.iter() { - *self.sign_count.entry(*signer).or_insert(0) += 1; - } + if self.is_finalized() { + let (hash, _, signers) = self.headers.pop_front().expect("we just pushed a block; qed"); + self.remove_signers(&signers); + trace!(target: "finality", "Encountered already finalized block {}", hash); + break } - - self.headers.push_front((hash, signers)); } trace!(target: "finality", "Rolling finality state: {:?}", self.headers); Ok(()) } - /// Clear the finality status, but keeps the validator set. + /// Clears the finality status, but keeps the validator set. pub fn clear(&mut self) { self.headers.clear(); self.sign_count.clear(); @@ -99,7 +93,7 @@ impl RollingFinality { /// Get an iterator over stored hashes in order. #[cfg(test)] pub fn unfinalized_hashes(&self) -> impl Iterator { - self.headers.iter().map(|(h, _)| h) + self.headers.iter().map(|(h, _, _)| h) } /// Get the validator set. @@ -110,7 +104,9 @@ 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, signers: Vec
) -> Result, UnknownValidator> { + pub fn push_hash(&mut self, head: H256, number: BlockNumber, signers: Vec
) + -> Result, UnknownValidator> + { for their_signer in signers.iter() { if !self.signers.contains(their_signer) { warn!(target: "finality", "Unknown validator: {}", their_signer); @@ -118,33 +114,16 @@ impl RollingFinality { } } - for signer in signers.iter() { - *self.sign_count.entry(*signer).or_insert(0) += 1; - } - - self.headers.push_back((head, signers)); + self.add_signers(&signers); + self.headers.push_back((head, number, signers)); let mut newly_finalized = Vec::new(); - while self.sign_count.len() * 2 > self.signers.len() { - let (hash, signers) = self.headers.pop_front() + while self.is_finalized() { + let (hash, _, signers) = self.headers.pop_front() .expect("headers length always greater than sign count length; qed"); - + self.remove_signers(&signers); newly_finalized.push(hash); - - 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(); - } - } - Entry::Vacant(_) => panic!("all hashes in `header` should have entries in `sign_count` for their signers; qed"), - } - } } trace!(target: "finality", "{} Blocks finalized by {:?}: {:?}", newly_finalized.len(), head, newly_finalized); @@ -152,55 +131,100 @@ impl RollingFinality { self.last_pushed = Some(head); Ok(newly_finalized) } + + /// Returns the first block for which a 2/3 quorum (instead of 1/2) is required. + pub fn two_thirds_majority_transition(&self) -> BlockNumber { + self.two_thirds_majority_transition + } + + /// Returns whether the first entry in `self.headers` is finalized. + fn is_finalized(&self) -> bool { + match self.headers.front() { + None => false, + Some((_, number, _)) if *number < self.two_thirds_majority_transition => { + self.sign_count.len() * 2 > self.signers.len() + } + Some((_, _, _)) => { + self.sign_count.len() * 3 > self.signers.len() * 2 + } + } + } + + /// Adds the signers to the sign count. + fn add_signers(&mut self, signers: &[Address]) { + for signer in signers { + *self.sign_count.entry(*signer).or_insert(0) += 1; + } + } + + /// Removes the signers from the sign count. + fn remove_signers(&mut self, signers: &[Address]) { + for signer in signers { + match self.sign_count.entry(*signer) { + Entry::Occupied(mut entry) => { + // decrement count for this signer and purge on zero. + if *entry.get() <= 1 { + entry.remove(); + } else { + *entry.get_mut() -= 1; + } + } + Entry::Vacant(_) => { + panic!("all hashes in `header` should have entries in `sign_count` for their signers; qed"); + } + } + } + } } #[cfg(test)] mod tests { use ethereum_types::{H256, Address}; + use types::BlockNumber; use super::RollingFinality; #[test] fn rejects_unknown_signers() { let signers = (0..3).map(|_| Address::random()).collect::>(); - let mut finality = RollingFinality::blank(signers.clone()); - assert!(finality.push_hash(H256::random(), vec![signers[0], Address::random()]).is_err()); + let mut finality = RollingFinality::blank(signers.clone(), BlockNumber::max_value()); + assert!(finality.push_hash(H256::random(), 0, vec![signers[0], Address::random()]).is_err()); } #[test] fn finalize_multiple() { let signers: Vec<_> = (0..6).map(|_| Address::random()).collect(); - let mut finality = RollingFinality::blank(signers.clone()); + let mut finality = RollingFinality::blank(signers.clone(), BlockNumber::max_value()); let hashes: Vec<_> = (0..7).map(|_| H256::random()).collect(); // 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, vec![signers[i]]).unwrap().len() == 0); + assert!(finality.push_hash(hash, i as u64, 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], vec![signers[4]]).unwrap(), + assert_eq!(finality.push_hash(hashes[6], 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 mut finality = RollingFinality::blank(signers.clone(), BlockNumber::max_value()); 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]); + assert_eq!(finality.push_hash(hash, 0, 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(), vec![signers[i % 6]])).collect(); + let hashes: Vec<_> = (0..12).map(|i| (H256::random(), i as u64, vec![signers[i % 6]])).collect(); - let mut finality = RollingFinality::blank(signers.clone()); + let mut finality = RollingFinality::blank(signers.clone(), BlockNumber::max_value()); finality.build_ancestry_subchain(hashes.iter().rev().cloned()).unwrap(); assert_eq!(finality.unfinalized_hashes().count(), 3); @@ -211,10 +235,10 @@ mod tests { 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]]) + (H256::random(), i as u64, vec![signers[i % 6], signers[(i + 1) % 6], signers[(i + 2) % 6]]) }).collect(); - let mut finality = RollingFinality::blank(signers.clone()); + let mut finality = RollingFinality::blank(signers.clone(), BlockNumber::max_value()); finality.build_ancestry_subchain(hashes.iter().rev().cloned()).unwrap(); // only the last hash has < 51% of authorities' signatures @@ -222,4 +246,70 @@ mod tests { assert_eq!(finality.unfinalized_hashes().next(), Some(&hashes[11].0)); assert_eq!(finality.subchain_head(), Some(hashes[11].0)); } + + #[test] + fn rejects_unknown_signers_2_3() { + let signers = (0..3).map(|_| Address::random()).collect::>(); + let mut finality = RollingFinality::blank(signers.clone(), 0); + assert!(finality.push_hash(H256::random(), 0, vec![signers[0], Address::random()]).is_err()); + } + + #[test] + fn finalize_multiple_2_3() { + let signers: Vec<_> = (0..7).map(|_| Address::random()).collect(); + + let mut finality = RollingFinality::blank(signers.clone(), 0); + let hashes: Vec<_> = (0..9).map(|_| H256::random()).collect(); + + // 4 / 7 signers is < 67% so no finality. + for (i, hash) in hashes.iter().take(8).cloned().enumerate() { + let i = i % 4; + assert!(finality.push_hash(hash, i as u64, vec![signers[i]]).unwrap().len() == 0); + } + + // after pushing a block signed by a fifth validator, the first five + // blocks of the unverified chain become verified. + assert_eq!(finality.push_hash(hashes[8], 8, vec![signers[4]]).unwrap(), + vec![hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]); + } + + #[test] + fn finalize_multiple_signers_2_3() { + let signers: Vec<_> = (0..5).map(|_| Address::random()).collect(); + let mut finality = RollingFinality::blank(signers.clone(), 0); + let hash = H256::random(); + + // after pushing a block signed by four validators, it becomes verified right away. + assert_eq!(finality.push_hash(hash, 0, signers[0..4].to_vec()).unwrap(), vec![hash]); + } + + #[test] + fn from_ancestry_2_3() { + let signers: Vec<_> = (0..6).map(|_| Address::random()).collect(); + let hashes: Vec<_> = (0..12).map(|i| (H256::random(), i as u64, vec![signers[i % 6]])).collect(); + + let mut finality = RollingFinality::blank(signers, 0); + finality.build_ancestry_subchain(hashes.iter().rev().cloned()).unwrap(); + + // The last four hashes, with index 11, 10, 9, and 8, have been pushed. 7 would have finalized a block. + assert_eq!(finality.unfinalized_hashes().count(), 4); + assert_eq!(finality.subchain_head(), Some(hashes[11].0)); + } + + #[test] + fn from_ancestry_multiple_signers_2_3() { + let signers: Vec<_> = (0..6).map(|_| Address::random()).collect(); + let hashes: Vec<_> = (0..12).map(|i| { + let hash_signers = signers.iter().cycle().skip(i).take(4).cloned().collect(); + (H256::random(), i as u64, hash_signers) + }).collect(); + + let mut finality = RollingFinality::blank(signers.clone(), 0); + finality.build_ancestry_subchain(hashes.iter().rev().cloned()).unwrap(); + + // only the last hash has < 67% 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)); + } } diff --git a/ethcore/src/engines/authority_round/mod.rs b/ethcore/src/engines/authority_round/mod.rs index dae24e9ef55..a670149e6fc 100644 --- a/ethcore/src/engines/authority_round/mod.rs +++ b/ethcore/src/engines/authority_round/mod.rs @@ -15,6 +15,10 @@ // 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. use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::{cmp, fmt}; @@ -93,6 +97,8 @@ pub struct AuthorityRoundParams { pub maximum_uncle_count: usize, /// Empty step messages transition block. pub empty_steps_transition: u64, + /// First block for which a 2/3 quorum (instead of 1/2) is required. + pub two_thirds_majority_transition: BlockNumber, /// Number of accepted empty steps. pub maximum_empty_steps: usize, /// Transition block to strict empty steps validation. @@ -126,6 +132,7 @@ impl From for AuthorityRoundParams { maximum_uncle_count: p.maximum_uncle_count.map_or(0, Into::into), empty_steps_transition: p.empty_steps_transition.map_or(u64::max_value(), |n| ::std::cmp::max(n.into(), 1)), maximum_empty_steps: p.maximum_empty_steps.map_or(0, Into::into), + two_thirds_majority_transition: p.two_thirds_majority_transition.map_or_else(BlockNumber::max_value, Into::into), strict_empty_steps_transition: p.strict_empty_steps_transition.map_or(0, Into::into), } } @@ -221,11 +228,11 @@ struct EpochManager { } impl EpochManager { - fn blank() -> Self { + fn blank(two_thirds_majority_transition: BlockNumber) -> Self { EpochManager { epoch_transition_hash: H256::zero(), epoch_transition_number: 0, - finality_checker: RollingFinality::blank(Vec::new()), + finality_checker: RollingFinality::blank(Vec::new(), two_thirds_majority_transition), force: true, } } @@ -289,7 +296,8 @@ impl EpochManager { }) .expect("proof produced by this engine; therefore it is valid; qed"); - self.finality_checker = RollingFinality::blank(epoch_set); + let two_thirds_majority_transition = self.finality_checker.two_thirds_majority_transition(); + self.finality_checker = RollingFinality::blank(epoch_set, two_thirds_majority_transition); } self.epoch_transition_hash = last_transition.block_hash; @@ -452,6 +460,7 @@ pub struct AuthorityRound { maximum_uncle_count: usize, empty_steps_transition: u64, strict_empty_steps_transition: u64, + two_thirds_majority_transition: BlockNumber, maximum_empty_steps: usize, machine: Machine, } @@ -461,6 +470,8 @@ struct EpochVerifier { step: Arc, subchain_validators: SimpleList, empty_steps_transition: u64, + /// First block for which a 2/3 quorum (instead of 1/2) is required. + two_thirds_majority_transition: BlockNumber, } impl engine::EpochVerifier for EpochVerifier { @@ -473,7 +484,8 @@ impl engine::EpochVerifier for EpochVerifier { } fn check_finality_proof(&self, proof: &[u8]) -> Option> { - let mut finality_checker = RollingFinality::blank(self.subchain_validators.clone().into_inner()); + let signers = self.subchain_validators.clone().into_inner(); + let mut finality_checker = RollingFinality::blank(signers, self.two_thirds_majority_transition); let mut finalized = Vec::new(); let headers: Vec
= Rlp::new(proof).as_list().ok()?; @@ -498,7 +510,8 @@ impl engine::EpochVerifier for EpochVerifier { }; signers.push(*parent_header.author()); - let newly_finalized = finality_checker.push_hash(parent_header.hash(), signers).ok()?; + let newly_finalized = + finality_checker.push_hash(parent_header.hash(), parent_header.number(), signers).ok()?; finalized.extend(newly_finalized); Some(()) @@ -708,7 +721,7 @@ impl AuthorityRound { validate_score_transition: our_params.validate_score_transition, validate_step_transition: our_params.validate_step_transition, empty_steps: Default::default(), - epoch_manager: Mutex::new(EpochManager::blank()), + epoch_manager: Mutex::new(EpochManager::blank(our_params.two_thirds_majority_transition)), immediate_transitions: our_params.immediate_transitions, block_reward: our_params.block_reward, block_reward_contract_transition: our_params.block_reward_contract_transition, @@ -717,6 +730,7 @@ impl AuthorityRound { maximum_uncle_count: our_params.maximum_uncle_count, empty_steps_transition: our_params.empty_steps_transition, maximum_empty_steps: our_params.maximum_empty_steps, + two_thirds_majority_transition: our_params.two_thirds_majority_transition, strict_empty_steps_transition: our_params.strict_empty_steps_transition, machine, }); @@ -884,7 +898,7 @@ impl AuthorityRound { signers.extend(parent_empty_steps_signers.drain(..)); if let Ok(empty_step_signers) = header_empty_steps_signers(&header, self.empty_steps_transition) { - let res = (header.hash(), signers); + let res = (header.hash(), header.number(), signers); trace!(target: "finality", "Ancestry iteration: yielding {:?}", res); parent_empty_steps_signers = empty_step_signers; @@ -897,7 +911,7 @@ impl AuthorityRound { } }) .while_some() - .take_while(|&(h, _)| h != epoch_transition_hash); + .take_while(|&(h, _, _)| h != epoch_transition_hash); if let Err(e) = epoch_manager.finality_checker.build_ancestry_subchain(ancestry_iter) { debug!(target: "engine", "inconsistent validator set within epoch: {:?}", e); @@ -905,7 +919,8 @@ impl AuthorityRound { } } - let finalized = epoch_manager.finality_checker.push_hash(chain_head.hash(), vec![*chain_head.author()]); + let finalized = epoch_manager.finality_checker.push_hash( + chain_head.hash(), chain_head.number(), vec![*chain_head.author()]); finalized.unwrap_or_default() } @@ -1255,6 +1270,11 @@ impl Engine for AuthorityRound { parent: &Header, ) -> Result<(), Error> { let mut beneficiaries = Vec::new(); + + if block.header.number() == self.two_thirds_majority_transition { + info!(target: "engine", "Block {}: Transitioning to 2/3 quorum.", self.two_thirds_majority_transition); + } + if block.header.number() >= self.empty_steps_transition { let empty_steps = if block.header.seal().is_empty() { // this is a new block, calculate rewards based on the empty steps messages we have accumulated @@ -1567,6 +1587,7 @@ impl Engine for AuthorityRound { step: self.step.clone(), subchain_validators: list, empty_steps_transition: self.empty_steps_transition, + two_thirds_majority_transition: self.two_thirds_majority_transition, }); match finalize { @@ -1666,6 +1687,7 @@ mod tests { block_reward_contract_transition: 0, block_reward_contract: Default::default(), strict_empty_steps_transition: 0, + two_thirds_majority_transition: 0, }; // mutate aura params diff --git a/json/src/spec/authority_round.rs b/json/src/spec/authority_round.rs index cc437f6b947..7432ab88b94 100644 --- a/json/src/spec/authority_round.rs +++ b/json/src/spec/authority_round.rs @@ -58,6 +58,8 @@ pub struct AuthorityRoundParams { pub maximum_empty_steps: Option, /// Strict validation of empty steps transition block. pub strict_empty_steps_transition: Option, + /// First block for which a 2/3 quorum (instead of 1/2) is required. + pub two_thirds_majority_transition: Option, } /// Authority engine deserialization.