diff --git a/crates/hotshot/src/lib.rs b/crates/hotshot/src/lib.rs index 090a9687de..85df5edac5 100644 --- a/crates/hotshot/src/lib.rs +++ b/crates/hotshot/src/lib.rs @@ -637,6 +637,29 @@ impl, V: Versions> SystemContext` with minimal setup for task state testing. + #[cfg(feature = "hotshot-testing")] + pub fn build_inactive_handle(&self) -> SystemContextHandle { + let (internal_sender, internal_receiver) = broadcast(EVENT_CHANNEL_SIZE); + + SystemContextHandle { + consensus_registry: ConsensusTaskRegistry::new(), + network_registry: NetworkTaskRegistry::new(), + output_event_stream: self.external_event_stream.clone(), + internal_event_stream: (internal_sender, internal_receiver.deactivate()), + hotshot: self.clone().into(), + storage: Arc::clone(&self.storage), + network: Arc::clone(&self.network), + memberships: Arc::clone(&self.memberships), + } + } } /// An async broadcast channel diff --git a/crates/testing/src/helpers.rs b/crates/testing/src/helpers.rs index 09c3b6e87e..f3b4411185 100644 --- a/crates/testing/src/helpers.rs +++ b/crates/testing/src/helpers.rs @@ -40,11 +40,12 @@ use hotshot_types::{ utils::{View, ViewInner}, vid::{vid_scheme, VidCommitment, VidSchemeType}, vote::{Certificate, HasViewNumber, Vote}, + PeerConfig, }; use jf_vid::VidScheme; use serde::Serialize; -use crate::test_builder::TestDescription; +use crate::{test_builder::TestDescription, test_launcher::TestLauncher}; /// create the [`SystemContextHandle`] from a node id /// # Panics @@ -128,6 +129,105 @@ pub async fn build_system_handle< .expect("Could not init hotshot") } +/// create the [`SystemContextHandle`] from a launcher +/// # Panics +/// if cannot create a [`HotShotInitializer`] +pub async fn build_system_handle_from_launcher< + TYPES: NodeType, + I: NodeImplementation< + TYPES, + Storage = TestStorage, + AuctionResultsProvider = TestAuctionResultsProvider, + > + TestableNodeImplementation, + V: Versions, +>( + node_id: u64, + launcher: TestLauncher, + membership_config: Option>, +) -> Result, anyhow::Error> { + tracing::info!("Creating system handle for node {}", node_id); + + let network = (launcher.resource_generator.channel_generator)(node_id).await; + let storage = (launcher.resource_generator.storage)(node_id); + let marketplace_config = (launcher.resource_generator.marketplace_config)(node_id); + let config = launcher.resource_generator.config.clone(); + + let known_nodes_with_stake = config.known_nodes_with_stake.clone(); + let private_key = config.my_own_validator_config.private_key.clone(); + let public_key = config.my_own_validator_config.public_key.clone(); + + let memberships = membership_config.unwrap_or_else(|| { + let create_membership = |nodes: &[PeerConfig], topic: Topic| { + TYPES::Membership::create_election( + known_nodes_with_stake.clone(), + nodes.to_vec(), + topic, + config.fixed_leader_for_gpuvid, + ) + }; + + Memberships { + quorum_membership: create_membership(&known_nodes_with_stake, Topic::Global), + da_membership: create_membership(&config.known_da_nodes, Topic::Da), + vid_membership: create_membership(&known_nodes_with_stake, Topic::Global), + view_sync_membership: create_membership(&known_nodes_with_stake, Topic::Global), + } + }); + + let initializer = HotShotInitializer::::from_genesis(TestInstanceState::new( + launcher.metadata.async_delay_config, + )) + .await + .unwrap(); + + let system_context = SystemContext::new( + public_key, + private_key, + node_id, + config, + memberships, + network, + initializer, + ConsensusMetricsValue::default(), + storage, + marketplace_config, + ); + + let system_context_handle = system_context.build_inactive_handle(); + + tracing::info!("Successfully created system handle for node {}", node_id); + Ok(system_context_handle) +} + +/// create certificate +/// # Panics +/// if we fail to sign the data +pub fn build_da_cert< + TYPES: NodeType, + DATAType: Committable + Clone + Eq + Hash + Serialize + Debug + 'static, + VOTE: Vote, + CERT: Certificate, +>( + data: DATAType, + membership: &TYPES::Membership, + view: TYPES::Time, + public_key: &TYPES::SignatureKey, + private_key: &::PrivateKey, +) -> CERT { + let real_qc_sig = + build_da_assembled_sig::(&data, membership, view); + + let vote = + SimpleVote::::create_signed_vote(data, view, public_key, private_key) + .expect("Failed to sign data!"); + CERT::create_signed_certificate( + vote.date_commitment(), + vote.date().clone(), + real_qc_sig, + vote.view_number(), + ) +} + /// create certificate /// # Panics /// if we fail to sign the data @@ -171,6 +271,46 @@ pub fn vid_share( .clone() } +/// create DA signature +/// # Panics +/// if fails to convert node id into keypair +pub fn build_da_assembled_sig( + data: &DATAType, + membership: &TYPES::Membership, + view: TYPES::Time, +) -> ::QcType +where + TYPES: NodeType, + VOTE: Vote, + CERT: Certificate, + DATAType: Committable + Clone + Eq + Hash + Serialize + Debug + 'static, +{ + let stake_table = membership.committee_qc_stake_table(); + let total_nodes = stake_table.len(); + let threshold = CERT::threshold(membership) as usize; + let real_qc_pp = + TYPES::SignatureKey::public_parameter(stake_table.clone(), U256::from(threshold as u64)); + + let mut signers = bitvec![0; total_nodes]; + let sig_lists: Vec<_> = (0..threshold) + .map(|node_id| { + let (private_key, public_key) = key_pair_for_id(node_id as u64); + let vote = SimpleVote::::create_signed_vote( + data.clone(), + view, + &public_key, + &private_key, + ) + .expect("Failed to sign data"); + + signers.set(node_id, true); + vote.signature() + }) + .collect(); + + TYPES::SignatureKey::assemble(&real_qc_pp, signers.as_bitslice(), &sig_lists) +} + /// create signature /// # Panics /// if fails to convert node id into keypair @@ -330,7 +470,7 @@ pub fn build_da_certificate( payload_commit: da_payload_commitment, }; - build_cert::, DaCertificate>( + build_da_cert::, DaCertificate>( da_data, da_membership, view_number, diff --git a/crates/testing/tests/tests_1/da_task.rs b/crates/testing/tests/tests_1/da_task.rs index 34dfc406b6..0c7ed2ed84 100644 --- a/crates/testing/tests/tests_1/da_task.rs +++ b/crates/testing/tests/tests_1/da_task.rs @@ -13,13 +13,17 @@ use hotshot_example_types::{ node_types::{MemoryImpl, TestTypes, TestVersions}, }; use hotshot_macros::{run_test, test_scripts}; -use hotshot_task_impls::{da::DaTaskState, events::HotShotEvent::*}; +use hotshot_task_impls::{ + da::DaTaskState, events::HotShotEvent::*, + events::HotShotEvent, +}; use hotshot_testing::{ - helpers::build_system_handle, + helpers::{ build_system_handle, build_system_handle_from_launcher }, predicates::event::exact, script::{Expectations, InputOrder, TaskScript}, serial, view_generator::TestViewGenerator, + test_builder::TestDescription, }; use hotshot_types::{ data::{null_block, PackedBundle, ViewNumber}, @@ -209,3 +213,470 @@ async fn test_da_task_storage_failure() { run_test![inputs, da_script].await; } + + +/// Test the DA Task for handling an outdated proposal. +/// +/// This test checks that when an outdated DA proposal is received, it doesn't produce +/// any output, while a current proposal triggers appropriate actions (validation and voting). +#[cfg_attr(async_executor_impl = "tokio", tokio::test(flavor = "multi_thread"))] +#[cfg_attr(async_executor_impl = "async-std", async_std::test)] +async fn test_da_task_outdated_proposal() { + // Setup logging and backtrace for debugging + async_compatibility_layer::logging::setup_logging(); + async_compatibility_layer::logging::setup_backtrace(); + + // Parameters for the test + let node_id: u64 = 2; + let num_nodes: usize = 10; + let da_committee_size: usize = 7; + + // Initialize test description with node and committee details + let test_description = TestDescription { + num_nodes_with_stake: num_nodes, + da_staked_committee_size: da_committee_size, + start_nodes: num_nodes, + ..TestDescription::default() + }; + + // Generate a launcher for the test system with a custom configuration + let launcher = test_description + .gen_launcher(node_id) + .modify_default_config(|config| { + config.next_view_timeout = 1000; + config.timeout_ratio = (12, 10); + config.da_staked_committee_size = da_committee_size; + }); + + // Build the system handle using the launcher configuration + let handle = + build_system_handle_from_launcher::(node_id, launcher, None) + .await + .expect("Failed to initialize HotShot"); + + // Prepare empty transactions and compute a commitment for later use + let transactions = vec![TestTransaction::new(vec![0])]; + let encoded_transactions = Arc::from(TestTransaction::encode(&transactions)); + let (payload_commit, _precompute) = precompute_vid_commitment( + &encoded_transactions, + handle.hotshot.memberships.quorum_membership.total_nodes(), + ); + + // Initialize a view generator using the current memberships + let mut view_generator = TestViewGenerator::generate( + handle.hotshot.memberships.quorum_membership.clone(), + handle.hotshot.memberships.da_membership.clone(), + ); + + // Generate views for the test + let view1 = view_generator.next().await.unwrap(); + let _view2 = view_generator.next().await.unwrap(); + view_generator.add_transactions(transactions); + let view3 = view_generator.next().await.unwrap(); + + // Define input events for the test: + // 1. Three view changes and an outdated proposal for view 1 when in view 3 + // 2. A current proposal for view 3 + let inputs = vec![ + serial![ + HotShotEvent::ViewChange(ViewNumber::new(1)), + HotShotEvent::ViewChange(ViewNumber::new(2)), + HotShotEvent::ViewChange(ViewNumber::new(3)), + // Send an outdated proposal (view 1) when we're in view 3 + HotShotEvent::DaProposalRecv(view1.da_proposal.clone(), view1.leader_public_key), + ], + serial![ + // Send a current proposal (view 3) + HotShotEvent::DaProposalRecv(view3.da_proposal.clone(), view3.leader_public_key), + ], + ]; + + // Define expectations: + // 1. No output for the outdated proposal + // 2. Validation and voting actions for the current proposal + let expectations = vec![ + Expectations::from_outputs(vec![]), + Expectations::from_outputs(vec![ + exact(HotShotEvent::DaProposalValidated( + view3.da_proposal.clone(), + view3.leader_public_key, + )), + exact(HotShotEvent::DaVoteSend(view3.create_da_vote( + DaData { + payload_commit, + }, + &handle, + ))), + ]), + ]; + + // Create DA task state and script for the test + let da_state = DaTaskState::::create_from(&handle).await; + let mut da_script = TaskScript { + timeout: Duration::from_millis(100), + state: da_state, + expectations, + }; + + // Run the test with the inputs and check the resulting events + run_test![inputs, da_script].await; +} + +/// Test the DA Task for handling duplicate votes. +/// +/// This test ensures that when duplicate votes are received in the DA Task, +/// they are correctly ignored without producing any output. The original +/// proposal and vote are processed as expected, validating the proposal +/// and sending the first vote, while the duplicate vote is disregarded. +#[cfg_attr(async_executor_impl = "tokio", tokio::test(flavor = "multi_thread"))] +#[cfg_attr(async_executor_impl = "async-std", async_std::test)] +async fn test_da_task_duplicate_votes() { + // Setup logging and backtrace for debugging + async_compatibility_layer::logging::setup_logging(); + async_compatibility_layer::logging::setup_backtrace(); + + // Parameters for the test + let node_id: u64 = 2; + let num_nodes: usize = 10; + let da_committee_size: usize = 7; + + // Initialize test description with node and committee details + let test_description = TestDescription { + num_nodes_with_stake: num_nodes, + da_staked_committee_size: da_committee_size, + start_nodes: num_nodes, + ..TestDescription::default() + }; + + // Generate a launcher for the test system with a custom configuration + let launcher = test_description + .gen_launcher(node_id) + .modify_default_config(|config| { + config.next_view_timeout = 1000; + config.timeout_ratio = (12, 10); + config.da_staked_committee_size = da_committee_size; + }); + + // Build the system handle using the launcher configuration + let handle = + build_system_handle_from_launcher::(node_id, launcher, None) + .await + .expect("Failed to initialize HotShot"); + + // Prepare empty transactions and compute a commitment for later use + let transactions = vec![TestTransaction::new(vec![0])]; + let encoded_transactions = Arc::from(TestTransaction::encode(&transactions)); + let (payload_commit, _precompute) = precompute_vid_commitment( + &encoded_transactions, + handle.hotshot.memberships.quorum_membership.total_nodes(), + ); + + // Initialize a view generator using the current memberships + let mut view_generator = TestViewGenerator::generate( + handle.hotshot.memberships.quorum_membership.clone(), + handle.hotshot.memberships.da_membership.clone(), + ); + + // Generate views for the test + let _view1 = view_generator.next().await.unwrap(); + view_generator.add_transactions(transactions); + let view2 = view_generator.next().await.unwrap(); + + // Create a duplicate vote + let duplicate_vote = view2.create_da_vote( + DaData { + payload_commit, + }, + &handle, + ); + + let inputs = vec![ + serial![ + HotShotEvent::ViewChange(ViewNumber::new(1)), + HotShotEvent::ViewChange(ViewNumber::new(2)), + HotShotEvent::DaProposalRecv(view2.da_proposal.clone(), view2.leader_public_key), + ], + serial![ + // Send the original vote + HotShotEvent::DaVoteRecv(duplicate_vote.clone()), + // Send the duplicate vote + HotShotEvent::DaVoteRecv(duplicate_vote.clone()), + ], + ]; + + // We expect the task to process the proposal and the first vote, but ignore the duplicate + let expectations = vec![ + Expectations::from_outputs(vec![ + exact(HotShotEvent::DaProposalValidated( + view2.da_proposal.clone(), + view2.leader_public_key, + )), + exact(HotShotEvent::DaVoteSend(duplicate_vote.clone())), + ]), + Expectations::from_outputs(vec![ + // No output expected for the duplicate vote + ]), + ]; + + // Create DA task state and script for the test + let da_state = DaTaskState::::create_from(&handle).await; + let mut da_script = TaskScript { + timeout: Duration::from_millis(100), + state: da_state, + expectations, + }; + + // Run the test with the inputs and check the resulting events + run_test![inputs, da_script].await; +} + +/// Tests the DA Task for collecting and processing valid votes. +/// +/// This test verifies that the DA Task correctly handles the proposal and votes +/// from DA committee members. It does the following: +/// +/// 1. Initializes a test environment with multiple nodes, configuring one as the leader. +/// 2. Creates and sends a DA proposal followed by valid votes from the committee. +/// 3. Asserts that the proposal is validated and a vote is sent in response by the leader. +/// +/// The test ensures that only the intended vote is processed. +#[cfg_attr(async_executor_impl = "tokio", tokio::test(flavor = "multi_thread"))] +#[cfg_attr(async_executor_impl = "async-std", async_std::test)] +async fn test_da_task_vote_collection() { + // Initialize logging and backtrace for debugging + async_compatibility_layer::logging::setup_logging(); + async_compatibility_layer::logging::setup_backtrace(); + + // Setup: Create system handles for multiple nodes (at least 5 for DA committee) + let leader_node_id: usize = 4; + let num_nodes: usize = 6; + let da_committee_size: usize = 5; + + let test_description = TestDescription { + num_nodes_with_stake: num_nodes, + da_staked_committee_size: da_committee_size, + start_nodes: num_nodes, + ..TestDescription::default() + }; + + // Create handles and view generators for all nodes + let mut handles = Vec::new(); + let mut view_generators = Vec::new(); + + for node_id in 0..num_nodes as u64 { + let launcher = test_description + .clone() + .gen_launcher(node_id) + .modify_default_config(|config| { + config.next_view_timeout = 1000; + config.timeout_ratio = (12, 10); + config.da_staked_committee_size = da_committee_size; + }); + + let handle = + build_system_handle_from_launcher::(node_id, launcher, None) + .await + .expect("Failed to initialize HotShot"); + + // Create the scenario with necessary views + let view_generator = TestViewGenerator::generate( + handle.hotshot.memberships.quorum_membership.clone(), + handle.hotshot.memberships.da_membership.clone(), + ); + + handles.push(handle); + view_generators.push(view_generator); + } + + // Prepare empty transactions and compute a commitment for later use + let transactions = vec![TestTransaction::new(vec![0])]; + let encoded_transactions = Arc::from(TestTransaction::encode(&transactions)); + let (payload_commit, _precompute) = precompute_vid_commitment( + &encoded_transactions, + handles[leader_node_id] + .hotshot + .memberships + .quorum_membership + .total_nodes(), + ); + + let _view1 = view_generators[leader_node_id].next().await.unwrap(); + let _view2 = view_generators[leader_node_id].next().await.unwrap(); + let _view3 = view_generators[leader_node_id].next().await.unwrap(); + view_generators[leader_node_id].add_transactions(transactions); + let view4 = view_generators[leader_node_id].next().await.unwrap(); + + // Create votes for all nodes + let votes: Vec<_> = handles + .iter() + .map(|handle| view4.create_da_vote(DaData { payload_commit }, handle)) + .collect(); + + // Simulate sending valid votes + let inputs = vec![ + serial![ + HotShotEvent::ViewChange(ViewNumber::new(1)), + HotShotEvent::ViewChange(ViewNumber::new(2)), + HotShotEvent::ViewChange(ViewNumber::new(3)), + HotShotEvent::ViewChange(ViewNumber::new(4)), + HotShotEvent::DaProposalRecv(view4.da_proposal.clone(), view4.leader_public_key), + ], + serial![ + // Send votes from the DA committee members + HotShotEvent::DaVoteRecv(votes[0].clone()), + HotShotEvent::DaVoteRecv(votes[1].clone()), + HotShotEvent::DaVoteRecv(votes[2].clone()), + HotShotEvent::DaVoteRecv(votes[3].clone()), + ], + ]; + + // Assert the outcome + let expectations = vec![ + Expectations::from_outputs(vec![ + exact(HotShotEvent::DaProposalValidated( + view4.da_proposal.clone(), + view4.leader_public_key, + )), + exact(HotShotEvent::DaVoteSend(votes[leader_node_id].clone())), + ]), + Expectations::from_outputs(vec![exact(HotShotEvent::DacSend( + view4.da_certificate, + view4.leader_public_key, + ))]), + ]; + + // Create DA task state and script + let da_state = + DaTaskState::::create_from(&handles[leader_node_id]).await; + let mut da_script = TaskScript { + timeout: Duration::from_millis(100), + state: da_state, + expectations, + }; + + // Run the test + run_test![inputs, da_script].await; +} + +/// Test that non-leader nodes correctly handle DA (Data Availability) votes and +/// ignore the certificate event when the node is not the leader during the voting process. +/// +/// This test sets up a scenario where a specific node (not the leader) processes +/// multiple views and receives votes from DA committee members. The test checks +/// that the expected outcome occurs, verifying that a `DacSend` event is not +/// generated since the node is not the leader. +#[cfg_attr(async_executor_impl = "tokio", tokio::test(flavor = "multi_thread"))] +#[cfg_attr(async_executor_impl = "async-std", async_std::test)] +async fn test_da_task_non_leader_vote_collection_ignore() { + // Initialize logging and backtrace for debugging + async_compatibility_layer::logging::setup_logging(); + async_compatibility_layer::logging::setup_backtrace(); + + // Setup: Create system handles for multiple nodes (at least 5 for DA committee) + let leader_node_id: usize = 3; + let num_nodes: usize = 6; + let da_committee_size: usize = 5; + + let test_description = TestDescription { + num_nodes_with_stake: num_nodes, + da_staked_committee_size: da_committee_size, + start_nodes: num_nodes, + ..TestDescription::default() + }; + + // Create handles and view generators for all nodes + let mut handles = Vec::new(); + let mut view_generators = Vec::new(); + + for node_id in 0..num_nodes as u64 { + let launcher = test_description + .clone() + .gen_launcher(node_id) + .modify_default_config(|config| { + config.next_view_timeout = 1000; + config.timeout_ratio = (12, 10); + config.da_staked_committee_size = da_committee_size; + }); + + let handle = + build_system_handle_from_launcher::(node_id, launcher, None) + .await + .expect("Failed to initialize HotShot"); + + // Create the scenario with necessary views + let view_generator = TestViewGenerator::generate( + handle.hotshot.memberships.quorum_membership.clone(), + handle.hotshot.memberships.da_membership.clone(), + ); + + handles.push(handle); + view_generators.push(view_generator); + } + + // Prepare empty transactions and compute a commitment for later use + let transactions = vec![TestTransaction::new(vec![0])]; + let encoded_transactions = Arc::from(TestTransaction::encode(&transactions)); + let (payload_commit, _precompute) = precompute_vid_commitment( + &encoded_transactions, + handles[leader_node_id] + .hotshot + .memberships + .quorum_membership + .total_nodes(), + ); + + // Generate multiple views and add transactions for the leader node + let _view1 = view_generators[leader_node_id].next().await.unwrap(); + let _view2 = view_generators[leader_node_id].next().await.unwrap(); + let _view3 = view_generators[leader_node_id].next().await.unwrap(); + view_generators[leader_node_id].add_transactions(transactions); + let view4 = view_generators[leader_node_id].next().await.unwrap(); + + // Create votes for all nodes + let votes: Vec<_> = handles + .iter() + .map(|handle| view4.create_da_vote(DaData { payload_commit }, handle)) + .collect(); + + // Simulate sending valid votes and a proposal to the DA committee + let inputs = vec![ + serial![ + HotShotEvent::ViewChange(ViewNumber::new(1)), + HotShotEvent::ViewChange(ViewNumber::new(2)), + HotShotEvent::ViewChange(ViewNumber::new(3)), + HotShotEvent::ViewChange(ViewNumber::new(4)), + HotShotEvent::DaProposalRecv(view4.da_proposal.clone(), view4.leader_public_key), + ], + serial![ + // Send votes from the DA committee members + HotShotEvent::DaVoteRecv(votes[0].clone()), + HotShotEvent::DaVoteRecv(votes[1].clone()), + HotShotEvent::DaVoteRecv(votes[2].clone()), + HotShotEvent::DaVoteRecv(votes[3].clone()), + ], + ]; + + // Define the expected outcome for the non-leader node + let expectations = vec![ + Expectations::from_outputs(vec![ + exact(HotShotEvent::DaProposalValidated( + view4.da_proposal.clone(), + view4.leader_public_key, + )), + exact(HotShotEvent::DaVoteSend(votes[leader_node_id].clone())), + ]), + Expectations::from_outputs(vec![]), + ]; + + // Create DA task state and script for the test + let da_state = + DaTaskState::::create_from(&handles[leader_node_id]).await; + let mut da_script = TaskScript { + timeout: Duration::from_millis(100), + state: da_state, + expectations, + }; + + // Run the test with the prepared inputs and script + run_test![inputs, da_script].await; +}