From 6d2861f35857e3cf9f824d469db1bd4e231ba44e Mon Sep 17 00:00:00 2001 From: Parth Date: Tue, 10 Dec 2019 22:06:16 +0530 Subject: [PATCH] add unit test for minority fork overcommit attack (#7292) * add unit test for minority fork overcommit attack * add generic function to simulate fork selection --- core/src/replay_stage.rs | 285 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 284 insertions(+), 1 deletion(-) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 0119e71e1c..7cff277672 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1147,6 +1147,7 @@ pub(crate) mod tests { use super::*; use crate::{ commitment::BlockCommitment, + consensus::Tower, genesis_utils::{create_genesis_config, create_genesis_config_with_leader}, replay_stage::ReplayStage, transaction_status_service::TransactionStatusService, @@ -1164,6 +1165,8 @@ pub(crate) mod tests { }, }; use solana_runtime::genesis_utils::GenesisConfigInfo; + use solana_sdk::account::Account; + use solana_sdk::rent::Rent; use solana_sdk::{ hash::{hash, Hash}, instruction::InstructionError, @@ -1172,12 +1175,292 @@ pub(crate) mod tests { system_transaction, transaction::TransactionError, }; - use solana_vote_program::vote_state::VoteState; + use solana_stake_program::stake_state; + use solana_vote_program::vote_state; + use solana_vote_program::vote_state::{Vote, VoteState}; + use std::iter; use std::{ fs::remove_dir_all, sync::{Arc, RwLock}, }; + struct ForkInfo { + leader: usize, + fork: Vec, + voters: Vec, + } + + struct ValidatorInfo { + stake: u64, + keypair: Keypair, + voting_keypair: Keypair, + staking_keypair: Keypair, + } + + struct ForkSelectionResponse { + slot: u64, + is_locked_out: bool, + } + + fn simulate_fork_selection( + neutral_fork: &ForkInfo, + forks: &Vec, + validators: &Vec, + ) -> Vec> { + fn vote(bank: &Arc, pubkey: &Pubkey, slot: Slot) { + let mut vote_account = bank.get_account(&pubkey).unwrap(); + let mut vote_state = VoteState::from(&vote_account).unwrap(); + vote_state.process_slot_vote_unchecked(slot); + vote_state.to(&mut vote_account).unwrap(); + bank.store_account(&pubkey, &vote_account); + } + + let mut towers: Vec = iter::repeat_with(|| Tower::new_for_tests(8, 0.67)) + .take(validators.len()) + .collect(); + + for slot in &neutral_fork.fork { + for tower in towers.iter_mut() { + tower.record_bank_vote(Vote { + hash: Hash::default(), + slots: vec![*slot], + timestamp: None, + }); + } + } + + for fork_info in forks.iter() { + for slot in fork_info.fork.iter() { + for voter_index in fork_info.voters.iter() { + towers[*voter_index].record_bank_vote(Vote { + hash: Hash::default(), + slots: vec![*slot], + timestamp: None, + }); + } + } + } + + let genesis_vote_accounts: Vec = validators + .iter() + .map(|validator| { + vote_state::create_account( + &validator.voting_keypair.pubkey(), + &validator.keypair.pubkey(), + 0, + validator.stake, + ) + }) + .collect(); + + let genesis_stake_accounts: Vec = validators + .iter() + .enumerate() + .map(|(i, validator)| { + stake_state::create_account( + &validator.staking_keypair.pubkey(), + &validator.voting_keypair.pubkey(), + &genesis_vote_accounts[i], + &Rent::default(), + validator.stake, + ) + }) + .collect(); + + let mut genesis_config = create_genesis_config(10_000).genesis_config; + genesis_config.accounts.clear(); + + for i in 0..validators.len() { + genesis_config.accounts.insert( + validators[i].voting_keypair.pubkey(), + genesis_vote_accounts[i].clone(), + ); + genesis_config.accounts.insert( + validators[i].staking_keypair.pubkey(), + genesis_stake_accounts[i].clone(), + ); + } + + let mut bank_forks = BankForks::new(neutral_fork.fork[0], Bank::new(&genesis_config)); + + let mut fork_progresses: Vec> = iter::repeat_with(HashMap::new) + .take(validators.len()) + .collect(); + + for fork_progress in fork_progresses.iter_mut() { + fork_progress + .entry(neutral_fork.fork[0]) + .or_insert_with(|| { + ForkProgress::new( + bank_forks.banks[&0].slot(), + bank_forks.banks[&0].last_blockhash(), + ) + }); + } + + for index in 1..neutral_fork.fork.len() { + let bank = Bank::new_from_parent( + &bank_forks.banks[&neutral_fork.fork[index - 1]].clone(), + &validators[neutral_fork.leader].keypair.pubkey(), + neutral_fork.fork[index], + ); + + bank_forks.insert(bank); + + for validator in validators.iter() { + vote( + &bank_forks.banks[&neutral_fork.fork[index]].clone(), + &validator.voting_keypair.pubkey(), + neutral_fork.fork[index - 1], + ); + } + + bank_forks.banks[&neutral_fork.fork[index]].freeze(); + + for fork_progress in fork_progresses.iter_mut() { + fork_progress + .entry(bank_forks.banks[&neutral_fork.fork[index]].slot()) + .or_insert_with(|| { + ForkProgress::new( + bank_forks.banks[&neutral_fork.fork[index]].slot(), + bank_forks.banks[&neutral_fork.fork[index]].last_blockhash(), + ) + }); + } + } + + let last_neutral_bank = &bank_forks.banks[neutral_fork.fork.last().unwrap()].clone(); + + for fork_info in forks.iter() { + for index in 0..fork_info.fork.len() { + let last_bank: &Arc; + let last_bank_in_fork: Arc; + + if index == 0 { + last_bank = &last_neutral_bank; + } else { + last_bank_in_fork = bank_forks.banks[&fork_info.fork[index - 1]].clone(); + last_bank = &last_bank_in_fork; + } + + let bank = Bank::new_from_parent( + last_bank, + &validators[fork_info.leader].keypair.pubkey(), + fork_info.fork[index], + ); + + bank_forks.insert(bank); + + for voter_index in fork_info.voters.iter() { + vote( + &bank_forks.banks[&fork_info.fork[index]].clone(), + &validators[*voter_index].voting_keypair.pubkey(), + last_bank.slot(), + ); + } + + bank_forks.banks[&fork_info.fork[index]].freeze(); + + for fork_progress in fork_progresses.iter_mut() { + fork_progress + .entry(bank_forks.banks[&fork_info.fork[index]].slot()) + .or_insert_with(|| { + ForkProgress::new( + bank_forks.banks[&fork_info.fork[index]].slot(), + bank_forks.banks[&fork_info.fork[index]].last_blockhash(), + ) + }); + } + } + } + + let bank_fork_ancestors = bank_forks.ancestors(); + let wrapped_bank_fork = Arc::new(RwLock::new(bank_forks)); + + (0..validators.len()) + .map(|i| { + let response = ReplayStage::select_fork( + &validators[i].keypair.pubkey(), + &bank_fork_ancestors, + &wrapped_bank_fork, + &towers[i], + &mut fork_progresses[i], + ); + + if response.is_none() { + None + } else { + let (_bank, stats) = response.unwrap(); + + Some(ForkSelectionResponse { + slot: stats.slot, + is_locked_out: stats.is_locked_out, + }) + } + }) + .collect() + } + + #[test] + fn test_minority_fork_overcommit_attack() { + let neutral_fork = ForkInfo { + leader: 0, + fork: vec![0, 1, 2], + voters: vec![], + }; + + let forks: Vec = vec![ + // Minority fork + ForkInfo { + leader: 2, + fork: (3..=3 + 8).collect(), + voters: vec![2], + }, + ForkInfo { + leader: 1, + fork: (12..12 + 8).collect(), + voters: vec![0, 1], + }, + ]; + + let validators: Vec = vec![ + ValidatorInfo { + stake: 34_000_000, + keypair: Keypair::new(), + voting_keypair: Keypair::new(), + staking_keypair: Keypair::new(), + }, + ValidatorInfo { + stake: 33_000_000, + keypair: Keypair::new(), + voting_keypair: Keypair::new(), + staking_keypair: Keypair::new(), + }, + // Malicious Node + ValidatorInfo { + stake: 33_000_000, + keypair: Keypair::new(), + voting_keypair: Keypair::new(), + staking_keypair: Keypair::new(), + }, + ]; + + let resp = simulate_fork_selection(&neutral_fork, &forks, &validators); + // Both honest nodes are now want to switch to minority fork and are locked out + assert!(resp[0].is_some()); + assert_eq!(resp[0].as_ref().unwrap().is_locked_out, true); + assert_eq!( + resp[0].as_ref().unwrap().slot, + forks[0].fork.last().unwrap().clone() + ); + assert!(resp[1].is_some()); + assert_eq!(resp[1].as_ref().unwrap().is_locked_out, true); + assert_eq!( + resp[1].as_ref().unwrap().slot, + forks[0].fork.last().unwrap().clone() + ); + } + #[test] fn test_child_slots_of_same_parent() { let ledger_path = get_tmp_ledger_path!();