diff --git a/consensus/src/state.rs b/consensus/src/state.rs index d2b8816b..4b7ae4a8 100644 --- a/consensus/src/state.rs +++ b/consensus/src/state.rs @@ -358,7 +358,7 @@ impl State { } ConsensusResponse::ViolationReport { violator, - description, + misbehavior, } => { let pubkey = self .block_header @@ -368,7 +368,12 @@ impl State { .0 .clone(); ( - ProgressResult::ViolationReported(pubkey, description, timestamp), + // TODO: add misbehavior handling + ProgressResult::ViolationReported( + pubkey, + format!("{misbehavior:?}"), + timestamp, + ), None, ) } diff --git a/vetomint/src/lib.rs b/vetomint/src/lib.rs index 4b406779..f1d89a41 100644 --- a/vetomint/src/lib.rs +++ b/vetomint/src/lib.rs @@ -61,6 +61,59 @@ pub enum ConsensusEvent { Timer, } +/// The report and trace of a misbehavior committed by a malicious node. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub enum Misbehavior { + DoubleProposal { + /// The malicious node. + byzantine_node: ValidatorIndex, + /// The round in which the misbehavior is committed. + round: Round, + /// The two conflicting proposals. + proposals: (BlockIdentifier, BlockIdentifier), + }, + DoublePrevote { + /// The malicious node. + byzantine_node: ValidatorIndex, + /// The round in which the misbehavior is committed. + round: Round, + /// The two conflicting proposals that the node has prevoted. + proposals: (Option, Option), + }, + DoublePrecommit { + /// The malicious node. + byzantine_node: ValidatorIndex, + /// The round in which the misbehavior is committed. + round: Round, + /// The two conflicting proposals that the node has precommitted. + proposals: (Option, Option), + }, + InvalidProposal { + /// The malicious node. + byzantine_node: ValidatorIndex, + /// The round in which the misbehavior is committed. + round: Round, + /// The proposal that the node has proposed. + proposal: BlockIdentifier, + }, + InvalidPrevote { + /// The malicious node. + byzantine_node: ValidatorIndex, + /// The round in which the misbehavior is committed. + round: Round, + /// The proposal that the node has prevoted. + proposal: BlockIdentifier, + }, + InvalidPrecommit { + /// The malicious node. + byzantine_node: ValidatorIndex, + /// The round in which the misbehavior is committed. + round: Round, + /// The proposal that the node has precommitted. + proposal: BlockIdentifier, + }, +} + /// A response that the consensus might emit for a given event, which must be properly handled by the lower layer. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub enum ConsensusResponse { @@ -84,7 +137,7 @@ pub enum ConsensusResponse { }, ViolationReport { violator: ValidatorIndex, - description: String, + misbehavior: Misbehavior, }, } diff --git a/vetomint/src/misbehavior.rs b/vetomint/src/misbehavior.rs new file mode 100644 index 00000000..a7a59f41 --- /dev/null +++ b/vetomint/src/misbehavior.rs @@ -0,0 +1,185 @@ +use super::*; +use state::*; + +use std::collections::HashMap; + +pub(crate) fn check_misbehavior( + state: &ConsensusState, + check_round: Round, + check_proposal: BlockIdentifier, +) -> Vec { + let mut misbehavior: Vec = vec![]; + + if let Some(m) = check_double_proposals(state, check_round) { + misbehavior.push(m); + } else { + println!("not found double proposals in this round"); + } + + if let Some(m) = check_double_prevote(state, check_round, check_proposal) { + misbehavior.push(m); + } else { + println!("not found double prevote in this round"); + } + + if let Some(m) = check_double_precommit(state, check_round, check_proposal) { + misbehavior.push(m); + } else { + println!("not found double precommit in this round"); + } + + if let Some(m) = check_invalid_proposal(state, check_proposal) { + misbehavior.push(m); + } else { + println!("not found invalid proposal in this round"); + } + + if let Some(m) = check_invalid_prevote(state, check_round, check_proposal) { + misbehavior.push(m); + } else { + println!("not found double precommit in this round"); + } + + if let Some(m) = check_invalid_precommit(state, check_round, check_proposal) { + misbehavior.push(m); + } else { + println!("not found double precommit in this round"); + } + + misbehavior +} + +fn check_double_proposals(state: &ConsensusState, check_round: Round) -> Option { + let proposals: Vec<_> = state + .proposals + .iter() + .filter(|(_, proposal)| proposal.round == check_round) + .map(|(_, proposal)| proposal) + .collect(); + + if proposals.len() > 1 { + // returnSome(result[0].1.proposer) + return Some(Misbehavior::DoubleProposal { + byzantine_node: proposals[0].proposer, + round: check_round, + proposals: (proposals[0].proposal, proposals[1].proposal), + }); + } + + None +} + +fn check_double_prevote( + state: &ConsensusState, + check_round: Round, + check_proposal: BlockIdentifier, +) -> Option { + let mut validators_map = HashMap::new(); + + for vote in state.prevotes.iter() { + let count = validators_map.entry(vote.signer).or_insert(0); + *count += 1; + + if *count == 2 { + return Some(Misbehavior::DoublePrevote { + byzantine_node: vote.signer, + round: check_round, + proposals: (Some(check_proposal), Some(check_proposal)), + }); + } + } + + None +} + +fn check_double_precommit( + state: &ConsensusState, + check_round: Round, + check_proposal: BlockIdentifier, +) -> Option { + let mut validators_map = HashMap::new(); + + for vote in state.precommits.iter() { + let count = validators_map.entry(vote.signer).or_insert(0); + *count += 1; + + if *count == 2 { + return Some(Misbehavior::DoublePrecommit { + byzantine_node: vote.signer, + round: check_round, + proposals: (Some(check_proposal), Some(check_proposal)), + }); + } + } + + None +} + +fn check_invalid_proposal( + state: &ConsensusState, + check_proposal: BlockIdentifier, +) -> Option { + if let Some(proposal) = state.proposals.get(&check_proposal) { + if proposal.valid == false { + return Some(Misbehavior::InvalidProposal { + byzantine_node: proposal.proposer, + round: proposal.round, + proposal: proposal.proposal, + }); + } + } + + None +} + +fn check_invalid_prevote( + state: &ConsensusState, + check_round: Round, + check_proposal: BlockIdentifier, +) -> Option { + let valid_prevotes: Vec<_> = state + .prevotes + .iter() + .filter(|prevote| prevote.round == check_round) + .collect(); + + for prevote in valid_prevotes.iter() { + if let Some(proposal) = prevote.proposal { + if proposal == check_proposal { + return Some(Misbehavior::InvalidPrevote { + byzantine_node: prevote.signer, + round: prevote.round, + proposal: proposal, + }); + } + } + } + + None +} + +fn check_invalid_precommit( + state: &ConsensusState, + check_round: Round, + check_proposal: BlockIdentifier, +) -> Option { + let valid_precommits: Vec<_> = state + .precommits + .iter() + .filter(|prevote| prevote.round == check_round) + .collect(); + + for precommit in valid_precommits.iter() { + if let Some(proposal) = precommit.proposal { + if proposal == check_proposal { + return Some(Misbehavior::InvalidPrecommit { + byzantine_node: precommit.signer, + round: precommit.round, + proposal: proposal, + }); + } + } + } + + None +} diff --git a/vetomint/tests/test_suite1.rs b/vetomint/tests/test_suite1.rs index b9b32c86..882181f7 100644 --- a/vetomint/tests/test_suite1.rs +++ b/vetomint/tests/test_suite1.rs @@ -133,9 +133,150 @@ fn normal_1() { fn lock_1() {} /// A byzantine node broadcasts both nil and non-nil prevotes but fails to break the safety. -#[ignore] #[test] -fn double_votes_1() {} +fn double_votes_1() { + let mut height_info = HeightInfo { + validators: vec![1, 1, 1, 1], + this_node_index: Some(0), + timestamp: 0, + consensus_params: ConsensusParams { + timeout_ms: 100, + repeat_round_for_first_leader: 1, + }, + initial_block_candidate: 0, + }; + + let mut proposer = Vetomint::new(height_info.clone()); + + let mut nodes = Vec::new(); + for i in 1..=2 { + height_info.this_node_index = Some(i); + nodes.push(Vetomint::new(height_info.clone())); + } + + let response = proposer.progress(ConsensusEvent::Start, 0); + assert_eq!( + response, + vec![ + ConsensusResponse::BroadcastProposal { + proposal: 0, + valid_round: None, + round: 0, + }, + ConsensusResponse::BroadcastPrevote { + proposal: Some(0), + round: 0 + } + ] + ); + + for node in nodes.iter_mut() { + let response = node.progress(ConsensusEvent::Start, 0); + assert_eq!(response, vec![]); + } + + for node in nodes.iter_mut() { + let response = node.progress( + ConsensusEvent::BlockProposalReceived { + proposal: 0, + valid: true, + valid_round: None, + proposer: 0, + round: 0, + favor: true, + }, + 1, + ); + assert_eq!( + response, + vec![ConsensusResponse::BroadcastPrevote { + proposal: Some(0), + round: 0, + }] + ); + } + + let mut nodes = vec![vec![proposer], nodes].concat(); + + for (i, node) in nodes.iter_mut().enumerate() { + // byzantine node's will send nil-prevote to node + let response = node.progress( + ConsensusEvent::Prevote { + proposal: None, + signer: 3, // byzantine node index + round: 0, + }, + 2, + ); + assert_eq!(response, Vec::new()); + + // byzantine node's will send prevote to node agiain to break the consensus + let response = node.progress( + ConsensusEvent::Prevote { + proposal: Some(0), + signer: 3, // byzantine node index + round: 0, + }, + 2, + ); + assert_eq!(response, Vec::new()); + + let response = node.progress( + ConsensusEvent::Prevote { + proposal: Some(0), + signer: (i + 1) % 3, + round: 0, + }, + 2, + ); + assert_eq!( + response, + vec![ConsensusResponse::BroadcastPrecommit { + proposal: Some(0), + round: 0, + }] + ); + + let response = node.progress( + ConsensusEvent::Prevote { + proposal: Some(0), + signer: (i + 2) % 3, + round: 0, + }, + 2, + ); + assert_eq!(response, Vec::new()); + } + + for (i, node) in nodes.iter_mut().enumerate() { + let response = node.progress( + ConsensusEvent::Precommit { + proposal: Some(0), + signer: (i + 1) % 3, + round: 0, + }, + 3, + ); + assert_eq!(response, Vec::new()); + let response = node.progress( + ConsensusEvent::Precommit { + proposal: Some(0), + signer: (i + 2) % 3, + round: 0, + }, + 3, + ); + + assert_eq!( + response, + vec![ConsensusResponse::FinalizeBlock { + proposal: 0, + proof: (0..3).collect(), + round: 0 + }] + ); + } +} /// Timeout occurs in the prevote stage, skipping the first round but eventually reaching consensus. #[ignore]