diff --git a/crates/sequencing/papyrus_consensus/src/single_height_consensus_test.rs b/crates/sequencing/papyrus_consensus/src/single_height_consensus_test.rs index 681835fa17..3e005b86d0 100644 --- a/crates/sequencing/papyrus_consensus/src/single_height_consensus_test.rs +++ b/crates/sequencing/papyrus_consensus/src/single_height_consensus_test.rs @@ -1,14 +1,29 @@ use std::sync::{Arc, OnceLock}; use futures::channel::{mpsc, oneshot}; +use lazy_static::lazy_static; use papyrus_protobuf::consensus::{ConsensusMessage, Vote, VoteType}; use starknet_api::block::{BlockHash, BlockNumber}; use starknet_types_core::felt::Felt; +use test_case::test_case; use tokio; use super::SingleHeightConsensus; use crate::test_utils::{MockTestContext, TestBlock}; -use crate::types::{ConsensusBlock, ProposalInit, ValidatorId}; +use crate::types::{ConsensusBlock, ConsensusError, ProposalInit, ValidatorId}; + +lazy_static! { + static ref PROPOSER_ID: ValidatorId = 0_u32.into(); + static ref VALIDATOR_ID_1: ValidatorId = 1_u32.into(); + static ref VALIDATOR_ID_2: ValidatorId = 2_u32.into(); + static ref VALIDATOR_ID_3: ValidatorId = 3_u32.into(); + static ref VALIDATORS: Vec = + vec![*PROPOSER_ID, *VALIDATOR_ID_1, *VALIDATOR_ID_2, *VALIDATOR_ID_3]; + static ref BLOCK: TestBlock = TestBlock { content: vec![1, 2, 3], id: BlockHash(Felt::ONE) }; + static ref BLOCK_ID: BlockHash = BLOCK.id(); + static ref PROPOSAL_INIT: ProposalInit = + ProposalInit { height: BlockNumber(0), round: 0, proposer: *PROPOSER_ID }; +} fn prevote( block_hash: Option, @@ -38,22 +53,13 @@ fn precommit( async fn proposer() { let mut context = MockTestContext::new(); - let node_id: ValidatorId = 1_u32.into(); - let block = TestBlock { content: vec![1, 2, 3], id: BlockHash(Felt::ONE) }; - let block_id = block.id(); + let mut shc = SingleHeightConsensus::new(BlockNumber(0), *VALIDATOR_ID_1, VALIDATORS.to_vec()); - let mut shc = SingleHeightConsensus::new( - BlockNumber(0), - node_id, - vec![node_id, 2_u32.into(), 3_u32.into(), 4_u32.into()], - ); - - context.expect_proposer().times(1).returning(move |_, _| node_id); - let block_clone = block.clone(); + context.expect_proposer().times(1).returning(move |_, _| *VALIDATOR_ID_1); context.expect_build_proposal().times(1).returning(move |_| { let (_, content_receiver) = mpsc::channel(1); let (block_sender, block_receiver) = oneshot::channel(); - block_sender.send(block_clone.clone()).unwrap(); + block_sender.send(BLOCK.clone()).unwrap(); (content_receiver, block_receiver) }); let fin_receiver = Arc::new(OnceLock::new()); @@ -61,43 +67,47 @@ async fn proposer() { context.expect_propose().times(1).return_once(move |init, _, fin_receiver| { // Ignore content receiver, since this is the context's responsibility. assert_eq!(init.height, BlockNumber(0)); - assert_eq!(init.proposer, node_id); + assert_eq!(init.proposer, *VALIDATOR_ID_1); fin_receiver_clone.set(fin_receiver).unwrap(); Ok(()) }); context .expect_broadcast() .times(1) - .withf(move |msg: &ConsensusMessage| msg == &prevote(Some(block_id), 0, 0, node_id)) + .withf(move |msg: &ConsensusMessage| { + msg == &prevote(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_1) + }) .returning(move |_| Ok(())); // Sends proposal and prevote. assert!(matches!(shc.start(&mut context).await, Ok(None))); assert_eq!( - shc.handle_message(&mut context, prevote(Some(block.id()), 0, 0, 2_u32.into())).await, + shc.handle_message(&mut context, prevote(Some(*BLOCK_ID), 0, 0, *PROPOSER_ID)).await, Ok(None) ); // 3 of 4 Prevotes is enough to send a Precommit. context .expect_broadcast() .times(1) - .withf(move |msg: &ConsensusMessage| msg == &precommit(Some(block_id), 0, 0, node_id)) + .withf(move |msg: &ConsensusMessage| { + msg == &precommit(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_1) + }) .returning(move |_| Ok(())); assert_eq!( - shc.handle_message(&mut context, prevote(Some(block.id()), 0, 0, 3_u32.into())).await, + shc.handle_message(&mut context, prevote(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_2)).await, Ok(None) ); let precommits = vec![ - precommit(Some(block.id()), 0, 0, 1_u32.into()), - precommit(Some(BlockHash(Felt::TWO)), 0, 0, 4_u32.into()), // Ignores since disagrees. - precommit(Some(block.id()), 0, 0, 2_u32.into()), - precommit(Some(block.id()), 0, 0, 3_u32.into()), + precommit(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_1), + precommit(Some(BlockHash(Felt::TWO)), 0, 0, *VALIDATOR_ID_3), // Ignores since disagrees. + precommit(Some(*BLOCK_ID), 0, 0, *PROPOSER_ID), + precommit(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_2), ]; assert_eq!(shc.handle_message(&mut context, precommits[1].clone()).await, Ok(None)); assert_eq!(shc.handle_message(&mut context, precommits[2].clone()).await, Ok(None)); let decision = shc.handle_message(&mut context, precommits[3].clone()).await.unwrap().unwrap(); - assert_eq!(decision.block, block); + assert_eq!(decision.block, *BLOCK); assert!( decision .precommits @@ -107,74 +117,84 @@ async fn proposer() { // Check the fin sent to the network. let fin = Arc::into_inner(fin_receiver).unwrap().take().unwrap().await.unwrap(); - assert_eq!(fin, block.id()); + assert_eq!(fin, *BLOCK_ID); } +#[test_case(false; "single_proposal")] +#[test_case(true; "repeat_proposal")] #[tokio::test] -async fn validator() { +async fn validator(repeat_proposal: bool) { let mut context = MockTestContext::new(); - let node_id: ValidatorId = 1_u32.into(); - let proposer: ValidatorId = 2_u32.into(); - let block = TestBlock { content: vec![1, 2, 3], id: BlockHash(Felt::ONE) }; - let block_id = block.id(); - // Creation calls to `context.validators`. - let mut shc = SingleHeightConsensus::new( - BlockNumber(0), - node_id, - vec![node_id, proposer, 3_u32.into(), 4_u32.into()], - ); + let mut shc = SingleHeightConsensus::new(BlockNumber(0), *VALIDATOR_ID_1, VALIDATORS.to_vec()); // Send the proposal from the peer. let (fin_sender, fin_receiver) = oneshot::channel(); - fin_sender.send(block.id()).unwrap(); + fin_sender.send(*BLOCK_ID).unwrap(); - context.expect_proposer().times(1).returning(move |_, _| proposer); - let block_clone = block.clone(); + context.expect_proposer().returning(move |_, _| *PROPOSER_ID); context.expect_validate_proposal().times(1).returning(move |_, _| { let (block_sender, block_receiver) = oneshot::channel(); - block_sender.send(block_clone.clone()).unwrap(); + block_sender.send(BLOCK.clone()).unwrap(); block_receiver }); context .expect_broadcast() .times(1) - .withf(move |msg: &ConsensusMessage| msg == &prevote(Some(block_id), 0, 0, node_id)) + .withf(move |msg: &ConsensusMessage| { + msg == &prevote(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_1) + }) .returning(move |_| Ok(())); let res = shc .handle_proposal( &mut context, - ProposalInit { height: BlockNumber(0), round: 0, proposer }, + PROPOSAL_INIT.clone(), mpsc::channel(1).1, // content - ignored by SHC. fin_receiver, ) .await; assert_eq!(res, Ok(None)); + if repeat_proposal { + // Send the same proposal again, which should be ignored (no expectations). + let (fin_sender, fin_receiver) = oneshot::channel(); + fin_sender.send(*BLOCK_ID).unwrap(); + let res = shc + .handle_proposal( + &mut context, + PROPOSAL_INIT.clone(), + mpsc::channel(1).1, // content - ignored by SHC. + fin_receiver, + ) + .await; + assert_eq!(res, Ok(None)); + } assert_eq!( - shc.handle_message(&mut context, prevote(Some(block.id()), 0, 0, 2_u32.into())).await, + shc.handle_message(&mut context, prevote(Some(*BLOCK_ID), 0, 0, *PROPOSER_ID)).await, Ok(None) ); // 3 of 4 Prevotes is enough to send a Precommit. context .expect_broadcast() .times(1) - .withf(move |msg: &ConsensusMessage| msg == &precommit(Some(block_id), 0, 0, node_id)) + .withf(move |msg: &ConsensusMessage| { + msg == &precommit(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_1) + }) .returning(move |_| Ok(())); assert_eq!( - shc.handle_message(&mut context, prevote(Some(block.id()), 0, 0, 3_u32.into())).await, + shc.handle_message(&mut context, prevote(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_2)).await, Ok(None) ); let precommits = vec![ - precommit(Some(block.id()), 0, 0, 2_u32.into()), - precommit(Some(block.id()), 0, 0, 3_u32.into()), - precommit(Some(block.id()), 0, 0, node_id), + precommit(Some(*BLOCK_ID), 0, 0, *PROPOSER_ID), + precommit(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_2), + precommit(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_1), ]; assert_eq!(shc.handle_message(&mut context, precommits[0].clone()).await, Ok(None)); let decision = shc.handle_message(&mut context, precommits[1].clone()).await.unwrap().unwrap(); - assert_eq!(decision.block, block); + assert_eq!(decision.block, *BLOCK); assert!( decision .precommits @@ -182,3 +202,71 @@ async fn validator() { .all(|item| precommits.contains(&ConsensusMessage::Vote(item))) ); } + +#[test_case(true; "repeat")] +#[test_case(false; "equivocation")] +#[tokio::test] +async fn vote_twice(same_vote: bool) { + let mut context = MockTestContext::new(); + + let mut shc = SingleHeightConsensus::new(BlockNumber(0), *VALIDATOR_ID_1, VALIDATORS.to_vec()); + + let (fin_sender, fin_receiver) = oneshot::channel(); + fin_sender.send(*BLOCK_ID).unwrap(); + + context.expect_proposer().times(1).returning(move |_, _| *PROPOSER_ID); + context.expect_validate_proposal().times(1).returning(move |_, _| { + let (block_sender, block_receiver) = oneshot::channel(); + block_sender.send(BLOCK.clone()).unwrap(); + block_receiver + }); + context + .expect_broadcast() + .times(1) // Shows the repeat vote is ignored. + .withf(move |msg: &ConsensusMessage| msg == &prevote(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_1)) + .returning(move |_| Ok(())); + let res = shc + .handle_proposal( + &mut context, + PROPOSAL_INIT.clone(), + mpsc::channel(1).1, // content - ignored by SHC. + fin_receiver, + ) + .await; + assert_eq!(res, Ok(None)); + + let res = shc.handle_message(&mut context, prevote(Some(*BLOCK_ID), 0, 0, *PROPOSER_ID)).await; + assert_eq!(res, Ok(None)); + + context + .expect_broadcast() + .times(1) // Shows the repeat vote is ignored. + .withf(move |msg: &ConsensusMessage| msg == &precommit(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_1)) + .returning(move |_| Ok(())); + let res = + shc.handle_message(&mut context, prevote(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_2)).await; + assert_eq!(res, Ok(None)); + + let first_vote = precommit(Some(*BLOCK_ID), 0, 0, *PROPOSER_ID); + let res = shc.handle_message(&mut context, first_vote.clone()).await; + assert_eq!(res, Ok(None)); + + let second_vote = if same_vote { + first_vote.clone() + } else { + precommit(Some(BlockHash(Felt::TWO)), 0, 0, *PROPOSER_ID) + }; + let res = shc.handle_message(&mut context, second_vote.clone()).await; + if same_vote { + assert_eq!(res, Ok(None)); + } else { + assert!(matches!(res, Err(ConsensusError::Equivocation(_, _, _)))); + } + + let decision = shc + .handle_message(&mut context, precommit(Some(*BLOCK_ID), 0, 0, *VALIDATOR_ID_2)) + .await + .unwrap() + .unwrap(); + assert_eq!(decision.block, *BLOCK); +}