From 147400dc77fab9a4ec3b1f2920d342cba1592b56 Mon Sep 17 00:00:00 2001 From: pls148 <184445976+pls148@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:18:02 -0800 Subject: [PATCH] Add randomized committees for testing --- crates/example-types/src/node_types.rs | 42 ++- crates/hotshot/src/traits/election/helpers.rs | 282 ++++++++++++++++++ .../traits/{election.rs => election/mod.rs} | 8 + .../election/randomized_committee_members.rs | 250 ++++++++++++++++ .../src/traits/networking/memory_network.rs | 55 +++- crates/macros/src/lib.rs | 75 ++++- crates/task-impls/src/consensus/handlers.rs | 5 +- crates/testing/src/test_runner.rs | 2 +- crates/testing/tests/tests_1/block_builder.rs | 8 +- crates/testing/tests/tests_1/test_success.rs | 21 +- 10 files changed, 715 insertions(+), 33 deletions(-) create mode 100644 crates/hotshot/src/traits/election/helpers.rs rename crates/hotshot/src/traits/{election.rs => election/mod.rs} (80%) create mode 100644 crates/hotshot/src/traits/election/randomized_committee_members.rs diff --git a/crates/example-types/src/node_types.rs b/crates/example-types/src/node_types.rs index 80b634515c..b45e2e7377 100644 --- a/crates/example-types/src/node_types.rs +++ b/crates/example-types/src/node_types.rs @@ -6,7 +6,9 @@ use hotshot::traits::{ election::{ - randomized_committee::RandomizedCommittee, static_committee::StaticCommittee, + randomized_committee::RandomizedCommittee, + randomized_committee_members::RandomizedCommitteeMembers, + static_committee::StaticCommittee, static_committee_leader_two_views::StaticCommitteeLeaderForTwoViews, }, implementations::{CombinedNetworks, Libp2pNetwork, MemoryNetwork, PushCdnNetwork}, @@ -87,6 +89,42 @@ impl NodeType for TestTypesRandomizedLeader { type BuilderSignatureKey = BuilderKey; } +#[derive( + Copy, + Clone, + Debug, + Default, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + serde::Serialize, + serde::Deserialize, +)] +/// filler struct to implement node type and allow us +/// to select our traits +pub struct TestTypesRandomizedCommitteeMembers; +impl NodeType + for TestTypesRandomizedCommitteeMembers +{ + type AuctionResult = TestAuctionResult; + type View = ViewNumber; + type Epoch = EpochNumber; + type BlockHeader = TestBlockHeader; + type BlockPayload = TestBlockPayload; + type SignatureKey = BLSPubKey; + type Transaction = TestTransaction; + type ValidatedState = TestValidatedState; + type InstanceState = TestInstanceState; + type Membership = RandomizedCommitteeMembers< + TestTypesRandomizedCommitteeMembers, + SEED, + OVERLAP, + >; + type BuilderSignatureKey = BuilderKey; +} + #[derive( Copy, Clone, @@ -133,7 +171,7 @@ pub struct Libp2pImpl; #[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)] pub struct WebImpl; -/// Combined Network implementation (libp2p + web sever) +/// Combined Network implementation (libp2p + web server) #[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)] pub struct CombinedImpl; diff --git a/crates/hotshot/src/traits/election/helpers.rs b/crates/hotshot/src/traits/election/helpers.rs new file mode 100644 index 0000000000..334458f57d --- /dev/null +++ b/crates/hotshot/src/traits/election/helpers.rs @@ -0,0 +1,282 @@ +// Copyright (c) 2021-2024 Espresso Systems (espressosys.com) +// This file is part of the HotShot repository. + +// You should have received a copy of the MIT License +// along with the HotShot repository. If not, see . + +use std::collections::BTreeSet; + +use rand::{rngs::StdRng, Rng, SeedableRng}; + +/// Helper which allows producing random numbers within a range and preventing duplicates +/// If consumed as a regular iterator, will return a randomly ordered permutation of all +/// values from 0..max +struct NonRepeatValueIterator { + /// Random number generator to use + rng: StdRng, + + /// Values which have already been emitted, to avoid duplicates + values: BTreeSet, + + /// Maximum value, open-ended. Numbers returned will be 0..max + max: u64, +} + +impl NonRepeatValueIterator { + /// Create a new NonRepeatValueIterator + pub fn new(rng: StdRng, max: u64) -> Self { + Self { + rng, + values: BTreeSet::new(), + max, + } + } +} + +impl Iterator for NonRepeatValueIterator { + type Item = u64; + + fn next(&mut self) -> Option { + if self.values.len() as u64 >= self.max { + return None; + } + + loop { + let v = self.rng.gen_range(0..self.max); + if !self.values.contains(&v) { + self.values.insert(v); + return Some(v); + } + } + } +} + +/// Create a single u64 seed by merging two u64s. Done this way to allow easy seeding of the number generator +/// from both a stable SOUND as well as a moving value ROUND (typically, epoch). +fn make_seed(seed: u64, round: u64) -> u64 { + seed.wrapping_add(round.wrapping_shl(8)) +} + +/// Create a pair of PRNGs for the given SEED and ROUND. Prev_rng is the PRNG for the previous ROUND, used to +/// deterministically replay random numbers generated for the previous ROUND. +fn make_rngs(seed: u64, round: u64) -> (StdRng, StdRng) { + let prev_rng = SeedableRng::seed_from_u64(make_seed(seed, round.wrapping_sub(1))); + let this_rng = SeedableRng::seed_from_u64(make_seed(seed, round)); + + (prev_rng, this_rng) +} + +/// Iterator which returns odd/even values for a given COUNT of nodes. For OVERLAP=0, this will return +/// [0, 2, 4, 6, ...] for an even round, and [1, 3, 5, 7, ...] for an odd round. Setting OVERLAP>0 will +/// randomly introduce OVERLAP elements from the previous round, so an even round with OVERLAP=2 will contain +/// something like [1, 7, 2, 4, 0, ...]. Note that the total number of nodes will always be COUNT/2, so +/// for OVERLAP>0 a random number of nodes which would have been in the round for OVERLAP=0 will be dropped. +/// Ordering of nodes is random. Outputs is deterministic when prev_rng and this_rng are provided by make_rngs +/// using the same values for SEED and ROUND. +pub struct StableQuorumIterator { + /// PRNG from the previous round + prev_rng: NonRepeatValueIterator, + + /// PRNG for the current round + this_rng: NonRepeatValueIterator, + + /// Current ROUND + round: u64, + + /// Count of nodes in the source quorum being filtered against + count: u64, + + /// OVERLAP of nodes to be carried over from the previous round + overlap: u64, + + /// The next call to next() will emit the value with this index. Starts at 0 and is incremented for each + /// call to next() + index: u64, +} + +/// Determines how many possible values can be made for the given odd/even +/// E.g. if count is 5, then possible values would be [0, 1, 2, 3, 4] +/// if odd = true, slots = 2 (1 or 3), else slots = 3 (0, 2, 4) +fn calc_num_slots(count: u64, odd: bool) -> u64 { + (count / 2) + if odd { count % 2 } else { 0 } +} + +impl StableQuorumIterator { + /// Create a new StableQuorumIterator + pub fn new(seed: u64, round: u64, count: u64, overlap: u64) -> Self { + assert!( + count / 2 > overlap, + "Overlap cannot be greater than the entire set size" + ); + + let (prev_rng, this_rng) = make_rngs(seed, round); + + Self { + prev_rng: NonRepeatValueIterator::new(prev_rng, calc_num_slots(count, round % 2 == 0)), + this_rng: NonRepeatValueIterator::new(this_rng, calc_num_slots(count, round % 2 == 1)), + round, + count, + overlap, + index: 0, + } + } +} + +impl Iterator for StableQuorumIterator { + type Item = u64; + + fn next(&mut self) -> Option { + if self.index >= (self.count / 2) { + None + } else if self.index < self.overlap { + // Generate enough values for the previous round + let v = self.prev_rng.next().unwrap(); + self.index += 1; + Some(v * 2 + self.round % 2) + } else { + // Generate new values + let v = self.this_rng.next().unwrap(); + self.index += 1; + Some(v * 2 + (1 - self.round % 2)) + } + } +} + +/// Helper function to convert the arguments to a StableQuorumIterator into an ordered set of values. +pub fn stable_quorum_filter(seed: u64, round: u64, count: usize, overlap: u64) -> BTreeSet { + StableQuorumIterator::new(seed, round, count as u64, overlap) + // We should never have more than u32_max members in a test + .map(|x| usize::try_from(x).unwrap()) + .collect() +} + +/// Constructs a quorum with a random number of members and overlaps. Functions similar to StableQuorumIterator, +/// except that the number of MEMBERS and OVERLAP are also (deterministically) random, to allow additional variance +/// in testing. +pub struct RandomOverlapQuorumIterator { + /// PRNG from the previous round + prev_rng: NonRepeatValueIterator, + + /// PRNG for the current round + this_rng: NonRepeatValueIterator, + + /// Current ROUND + round: u64, + + /// Number of members to emit for the current round + members: u64, + + /// OVERLAP of nodes to be carried over from the previous round + overlap: u64, + + /// The next call to next() will emit the value with this index. Starts at 0 and is incremented for each + /// call to next() + index: u64, +} + +impl RandomOverlapQuorumIterator { + /// Create a new RandomOverlapQuorumIterator + pub fn new( + seed: u64, + round: u64, + count: u64, + members_min: u64, + members_max: u64, + overlap_min: u64, + overlap_max: u64, + ) -> Self { + assert!( + members_min <= members_max, + "Members_min cannot be greater than members_max" + ); + assert!( + overlap_min <= overlap_max, + "Overlap_min cannot be greater than overlap_max" + ); + assert!( + overlap_max < members_min, + "Overlap_max must be less than members_min" + ); + assert!( + count / 2 > overlap_max, + "Overlap cannot be greater than the entire set size" + ); + + let (mut prev_rng, mut this_rng) = make_rngs(seed, round); + + // Consume two values from prev_rng to advance it to the same state it was at the beginning of the previous round + let _prev_members = prev_rng.gen_range(members_min..=members_max); + let _prev_overlap = prev_rng.gen_range(overlap_min..=overlap_max); + let this_members = this_rng.gen_range(members_min..=members_max); + let this_overlap = this_rng.gen_range(overlap_min..=overlap_max); + + Self { + prev_rng: NonRepeatValueIterator::new(prev_rng, calc_num_slots(count, round % 2 == 0)), + this_rng: NonRepeatValueIterator::new(this_rng, calc_num_slots(count, round % 2 == 1)), + round, + members: this_members, + overlap: this_overlap, + index: 0, + } + } +} + +impl Iterator for RandomOverlapQuorumIterator { + type Item = u64; + + fn next(&mut self) -> Option { + if self.index >= self.members { + None + } else if self.index < self.overlap { + // Generate enough values for the previous round + let v = self.prev_rng.next().unwrap(); + self.index += 1; + Some(v * 2 + self.round % 2) + } else { + // Generate new values + let v = self.this_rng.next().unwrap(); + self.index += 1; + Some(v * 2 + (1 - self.round % 2)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stable() { + for _ in 0..100 { + let seed = rand::random::(); + let prev_set: Vec = StableQuorumIterator::new(seed, 1, 10, 2).collect(); + let this_set: Vec = StableQuorumIterator::new(seed, 2, 10, 2).collect(); + + // The first two elements from prev_set are from its previous round. But its 2nd and 3rd elements + // are new, and should be carried over to become the first two elements from this_set. + assert_eq!( + prev_set[2..4], + this_set[0..2], + "prev_set={prev_set:?}, this_set={this_set:?}" + ); + } + } + + #[test] + fn test_random_overlap() { + for _ in 0..100 { + let seed = rand::random::(); + let prev_set: Vec = + RandomOverlapQuorumIterator::new(seed, 1, 20, 5, 10, 2, 3).collect(); + let this_set: Vec = + RandomOverlapQuorumIterator::new(seed, 2, 20, 5, 10, 2, 3).collect(); + + // Similar to the overlap before, but there are 4 possible cases: the previous set might have had + // either 2 or 3 overlaps, meaning we should start with index 2 or 3, and the overlap size might + // be either 2 or 3. We'll just check for 2 overlaps, meaning we have two possible overlap cases + // to verify. + let matched = (prev_set[2..4] == this_set[0..2]) || (prev_set[3..5] == this_set[0..2]); + assert!(matched, "prev_set={prev_set:?}, this_set={this_set:?}"); + } + } +} diff --git a/crates/hotshot/src/traits/election.rs b/crates/hotshot/src/traits/election/mod.rs similarity index 80% rename from crates/hotshot/src/traits/election.rs rename to crates/hotshot/src/traits/election/mod.rs index 4f9212705f..8020aa9d08 100644 --- a/crates/hotshot/src/traits/election.rs +++ b/crates/hotshot/src/traits/election/mod.rs @@ -8,7 +8,15 @@ /// leader completely randomized every view pub mod randomized_committee; + +/// quorum randomized every view, with configurable overlap +pub mod randomized_committee_members; + /// static (round robin) committee election pub mod static_committee; + /// static (round robin leader for 2 consecutive views) committee election pub mod static_committee_leader_two_views; + +/// general helpers +mod helpers; diff --git a/crates/hotshot/src/traits/election/randomized_committee_members.rs b/crates/hotshot/src/traits/election/randomized_committee_members.rs new file mode 100644 index 0000000000..ab8f8a534e --- /dev/null +++ b/crates/hotshot/src/traits/election/randomized_committee_members.rs @@ -0,0 +1,250 @@ +// Copyright (c) 2021-2024 Espresso Systems (espressosys.com) +// This file is part of the HotShot repository. + +// You should have received a copy of the MIT License +// along with the HotShot repository. If not, see . + +use std::{ + cmp::max, + collections::{BTreeMap, BTreeSet}, + num::NonZeroU64, +}; + +use ethereum_types::U256; +use hotshot_types::{ + traits::{ + election::Membership, + network::Topic, + node_implementation::{ConsensusTime, NodeType}, + signature_key::{SignatureKey, StakeTableEntryType}, + }, + PeerConfig, +}; +use rand::{rngs::StdRng, Rng}; +use utils::anytrace::Result; + +use super::helpers::stable_quorum_filter; + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] + +/// The static committee election + +pub struct RandomizedCommitteeMembers { + /// The nodes eligible for leadership. + /// NOTE: This is currently a hack because the DA leader needs to be the quorum + /// leader but without voting rights. + eligible_leaders: Vec<::StakeTableEntry>, + + /// The nodes on the committee and their stake + stake_table: Vec<::StakeTableEntry>, + + /// The nodes on the committee and their stake, indexed by public key + indexed_stake_table: + BTreeMap::StakeTableEntry>, + + /// The network topic of the committee + committee_topic: Topic, +} + +impl + RandomizedCommitteeMembers +{ + /// Creates a set of indices into the stake_table which reference the nodes selected for this epoch's committee + fn make_quorum_filter(&self, epoch: ::Epoch) -> BTreeSet { + stable_quorum_filter(SEED, epoch.u64(), self.stake_table.len(), OVERLAP) + } +} + +impl Membership + for RandomizedCommitteeMembers +{ + type Error = utils::anytrace::Error; + + /// Create a new election + fn new( + eligible_leaders: Vec::SignatureKey>>, + committee_members: Vec::SignatureKey>>, + committee_topic: Topic, + ) -> Self { + // The only time these two are unequal is in a benchmarking scenario that isn't covered by this test case + assert_eq!( + eligible_leaders, committee_members, + "eligible_leaders should be the same as committee_members!" + ); + + // For each eligible leader, get the stake table entry + let eligible_leaders: Vec<::StakeTableEntry> = + eligible_leaders + .iter() + .map(|member| member.stake_table_entry.clone()) + .filter(|entry| entry.stake() > U256::zero()) + .collect(); + + // For each member, get the stake table entry + let members: Vec<::StakeTableEntry> = + committee_members + .iter() + .map(|member| member.stake_table_entry.clone()) + .filter(|entry| entry.stake() > U256::zero()) + .collect(); + + // Index the stake table by public key + let indexed_stake_table: BTreeMap< + TYPES::SignatureKey, + ::StakeTableEntry, + > = members + .iter() + .map(|entry| (TYPES::SignatureKey::public_key(entry), entry.clone())) + .collect(); + + Self { + eligible_leaders, + stake_table: members, + indexed_stake_table, + committee_topic, + } + } + + /// Get the stake table for the current view + fn stake_table( + &self, + epoch: ::Epoch, + ) -> Vec<<::SignatureKey as SignatureKey>::StakeTableEntry> { + let filter = self.make_quorum_filter(epoch); + //self.stake_table.clone()s + self.stake_table + .iter() + .enumerate() + .filter(|(idx, _)| filter.contains(idx)) + .map(|(_, v)| v.clone()) + .collect() + } + + /// Get all members of the committee for the current view + fn committee_members( + &self, + _view_number: ::View, + epoch: ::Epoch, + ) -> BTreeSet<::SignatureKey> { + let filter = self.make_quorum_filter(epoch); + self.stake_table + .iter() + .enumerate() + .filter(|(idx, _)| filter.contains(idx)) + .map(|(_, v)| TYPES::SignatureKey::public_key(v)) + .collect() + } + + /// Get all eligible leaders of the committee for the current view + fn committee_leaders( + &self, + view_number: ::View, + epoch: ::Epoch, + ) -> BTreeSet<::SignatureKey> { + self.committee_members(view_number, epoch) + } + + /// Get the stake table entry for a public key + fn stake( + &self, + pub_key: &::SignatureKey, + epoch: ::Epoch, + ) -> Option<::StakeTableEntry> { + let filter = self.make_quorum_filter(epoch); + let actual_members: BTreeSet<_> = self + .stake_table + .iter() + .enumerate() + .filter(|(idx, _)| filter.contains(idx)) + .map(|(_, v)| TYPES::SignatureKey::public_key(v)) + .collect(); + + if actual_members.contains(pub_key) { + // Only return the stake if it is above zero + self.indexed_stake_table.get(pub_key).cloned() + } else { + // Skip members which aren't included based on the quorum filter + None + } + } + + /// Check if a node has stake in the committee + fn has_stake( + &self, + pub_key: &::SignatureKey, + epoch: ::Epoch, + ) -> bool { + let filter = self.make_quorum_filter(epoch); + let actual_members: BTreeSet<_> = self + .stake_table + .iter() + .enumerate() + .filter(|(idx, _)| filter.contains(idx)) + .map(|(_, v)| TYPES::SignatureKey::public_key(v)) + .collect(); + + if actual_members.contains(pub_key) { + self.indexed_stake_table + .get(pub_key) + .is_some_and(|x| x.stake() > U256::zero()) + } else { + // Skip members which aren't included based on the quorum filter + false + } + } + + /// Get the network topic for the committee + fn committee_topic(&self) -> Topic { + self.committee_topic.clone() + } + + /// Index the vector of public keys with the current view number + fn lookup_leader( + &self, + view_number: TYPES::View, + epoch: ::Epoch, + ) -> Result { + let filter = self.make_quorum_filter(epoch); + let leader_vec: Vec<_> = self + .stake_table + .iter() + .enumerate() + .filter(|(idx, _)| filter.contains(idx)) + .map(|(_, v)| v.clone()) + .collect(); + + let mut rng: StdRng = rand::SeedableRng::seed_from_u64(*view_number); + + let randomized_view_number: u64 = rng.gen_range(0..=u64::MAX); + #[allow(clippy::cast_possible_truncation)] + let index = randomized_view_number as usize % leader_vec.len(); + + let res = leader_vec[index].clone(); + + Ok(TYPES::SignatureKey::public_key(&res)) + } + + /// Get the total number of nodes in the committee + fn total_nodes(&self, epoch: ::Epoch) -> usize { + self.make_quorum_filter(epoch).len() + } + + /// Get the voting success threshold for the committee + fn success_threshold(&self) -> NonZeroU64 { + let len = self.stake_table.len() / 2; // Divide by two as we are flipping between odds and evens + NonZeroU64::new(((len as u64 * 2) / 3) + 1).unwrap() + } + + /// Get the voting failure threshold for the committee + fn failure_threshold(&self) -> NonZeroU64 { + let len = self.stake_table.len() / 2; // Divide by two as we are flipping between odds and evens + NonZeroU64::new(((len as u64) / 3) + 1).unwrap() + } + + /// Get the voting upgrade threshold for the committee + fn upgrade_threshold(&self) -> NonZeroU64 { + let len = self.stake_table.len() / 2; // Divide by two as we are flipping between odds and evens + + NonZeroU64::new(max((len as u64 * 9) / 10, ((len as u64 * 2) / 3) + 1)).unwrap() + } +} diff --git a/crates/hotshot/src/traits/networking/memory_network.rs b/crates/hotshot/src/traits/networking/memory_network.rs index 16eaa8ccbe..47fb9bc93c 100644 --- a/crates/hotshot/src/traits/networking/memory_network.rs +++ b/crates/hotshot/src/traits/networking/memory_network.rs @@ -93,7 +93,7 @@ struct MemoryNetworkInner { /// This provides an in memory simulation of a networking implementation, allowing nodes running on /// the same machine to mock networking while testing other functionality. /// -/// Under the hood, this simply maintains mpmc channels to every other `MemoryNetwork` insane of the +/// Under the hood, this simply maintains mpmc channels to every other `MemoryNetwork` instance of the /// same group. #[derive(Clone)] pub struct MemoryNetwork { @@ -302,22 +302,53 @@ impl ConnectedNetwork for MemoryNetwork { &self, message: Vec, recipients: Vec, - broadcast_delay: BroadcastDelay, + _broadcast_delay: BroadcastDelay, ) -> Result<(), NetworkError> { - // Iterate over all topics, compare to recipients, and get the `Topic` - let topic = self + trace!(?message, "Broadcasting message to DA"); + for node in self .inner .master_map .subscribed_map + .entry(Topic::Da) + .or_default() .iter() - .find(|v| v.value().iter().all(|(k, _)| recipients.contains(k))) - .map(|v| v.key().clone()) - .ok_or(NetworkError::MessageSendError( - "no topic found for recipients".to_string(), - ))?; - - self.broadcast_message(message, topic, broadcast_delay) - .await + { + if !recipients.contains(&node.0) { + tracing::error!("Skipping node because not in recipient list: {:?}", &node.0); + continue; + } + // TODO delay/drop etc here + let (key, node) = node; + trace!(?key, "Sending message to node"); + if let Some(ref config) = &self.inner.reliability_config { + { + let node2 = node.clone(); + let fut = config.chaos_send_msg( + message.clone(), + Arc::new(move |msg: Vec| { + let node3 = (node2).clone(); + boxed_sync(async move { + let _res = node3.input(msg).await; + // NOTE we're dropping metrics here but this is only for testing + // purposes. I think that should be okay + }) + }), + ); + spawn(fut); + } + } else { + let res = node.input(message.clone()).await; + match res { + Ok(()) => { + trace!(?key, "Delivered message to remote"); + } + Err(e) => { + warn!(?e, ?key, "Error sending broadcast message to node"); + } + } + } + } + Ok(()) } #[instrument(name = "MemoryNetwork::direct_message")] diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index e1920bf4fa..2666b8fbca 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -11,25 +11,41 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{ parse::{Parse, ParseStream, Result}, - parse_macro_input, Expr, ExprArray, ExprPath, ExprTuple, Ident, LitBool, Token, + parse_macro_input, + punctuated::Punctuated, + Expr, ExprArray, ExprPath, ExprTuple, Ident, LitBool, Token, TypePath, }; +/// Bracketed types, e.g. [A, B, C] +/// These types can have generic parameters, whereas ExprArray items must be Expr. +#[derive(derive_builder::Builder, Debug, Clone)] +struct TypePathBracketedArray { + /// elems + pub elems: Punctuated, +} + /// description of a crosstest #[derive(derive_builder::Builder, Debug, Clone)] struct CrossTestData { /// imlementations impls: ExprArray, + /// builder impl #[builder(default = "syn::parse_str(\"[SimpleBuilderImplementation]\").unwrap()")] builder_impls: ExprArray, + /// versions versions: ExprArray, + /// types - types: ExprArray, + types: TypePathBracketedArray, + /// name of the test test_name: Ident, + /// test description/spec metadata: Expr, + /// whether or not to ignore ignore: LitBool, } @@ -51,17 +67,23 @@ impl CrossTestDataBuilder { #[derive(derive_builder::Builder, Debug, Clone)] struct TestData { /// type - ty: ExprPath, + ty: TypePath, + /// impl imply: ExprPath, + /// builder implementation builder_impl: ExprPath, + /// impl version: ExprPath, + /// name of test test_name: Ident, + /// test description metadata: Expr, + /// whether or not to ignore the test ignore: LitBool, } @@ -86,6 +108,20 @@ impl ToLowerSnakeStr for ExprPath { } } +impl ToLowerSnakeStr for TypePath { + fn to_lower_snake_str(&self) -> String { + self.path + .segments + .iter() + .fold(String::new(), |mut acc, s| { + acc.push_str(&s.ident.to_string().to_lowercase()); + acc.push('_'); + acc + }) + .to_lowercase() + } +} + impl ToLowerSnakeStr for ExprTuple { /// allow panic because this is a compiler error #[allow(clippy::panic)] @@ -149,6 +185,28 @@ mod keywords { syn::custom_keyword!(Versions); } +impl Parse for TypePathBracketedArray { + /// allow panic because this is a compiler error + #[allow(clippy::panic)] + fn parse(input: ParseStream<'_>) -> Result { + let content; + syn::bracketed!(content in input); + let mut elems = Punctuated::new(); + + while !content.is_empty() { + let first: TypePath = content.parse()?; + elems.push_value(first); + if content.is_empty() { + break; + } + let punct = content.parse()?; + elems.push_punct(punct); + } + + Ok(Self { elems }) + } +} + impl Parse for CrossTestData { /// allow panic because this is a compiler error #[allow(clippy::panic)] @@ -159,7 +217,7 @@ impl Parse for CrossTestData { if input.peek(keywords::Types) { let _ = input.parse::()?; input.parse::()?; - let types = input.parse::()?; + let types = input.parse::()?; //ExprArray>()?; description.types(types); } else if input.peek(keywords::Impls) { let _ = input.parse::()?; @@ -216,13 +274,8 @@ fn cross_tests_internal(test_spec: CrossTestData) -> TokenStream { }; p }); - // - let types = test_spec.types.elems.iter().map(|t| { - let Expr::Path(p) = t else { - panic!("Expected Path for Type! Got {t:?}"); - }; - p - }); + + let types = test_spec.types.elems.iter(); let versions = test_spec.versions.elems.iter().map(|t| { let Expr::Path(p) = t else { diff --git a/crates/task-impls/src/consensus/handlers.rs b/crates/task-impls/src/consensus/handlers.rs index 287e14e03e..7b7daf1ffc 100644 --- a/crates/task-impls/src/consensus/handlers.rs +++ b/crates/task-impls/src/consensus/handlers.rs @@ -278,7 +278,10 @@ pub(crate) async fn handle_timeout task_state .timeout_membership .has_stake(&task_state.public_key, task_state.cur_epoch), - debug!("We were not chosen for the consensus committee for view {view_number:?}") + debug!( + "We were not chosen for the consensus committee for view {:?}", + view_number + ) ); let vote = TimeoutVote::create_signed_vote( diff --git a/crates/testing/src/test_runner.rs b/crates/testing/src/test_runner.rs index 2a08fda5e4..871fc3120d 100644 --- a/crates/testing/src/test_runner.rs +++ b/crates/testing/src/test_runner.rs @@ -306,7 +306,7 @@ where for node in &mut *nodes { node.handle.shut_down().await; } - tracing::info!("Nodes shtudown"); + tracing::info!("Nodes shutdown"); completion_handle.abort(); diff --git a/crates/testing/tests/tests_1/block_builder.rs b/crates/testing/tests/tests_1/block_builder.rs index 5b0a6cf5c2..572741607a 100644 --- a/crates/testing/tests/tests_1/block_builder.rs +++ b/crates/testing/tests/tests_1/block_builder.rs @@ -21,14 +21,13 @@ use hotshot_testing::block_builder::{ use hotshot_types::{ network::RandomBuilderConfig, traits::{ - block_contents::vid_commitment, - node_implementation::{NodeType, Versions}, - signature_key::SignatureKey, + block_contents::vid_commitment, node_implementation::NodeType, signature_key::SignatureKey, BlockPayload, }, }; use tide_disco::Url; use tokio::time::sleep; +use vbs::version::StaticVersion; #[cfg(test)] #[tokio::test(flavor = "multi_thread")] @@ -50,8 +49,7 @@ async fn test_random_block_builder() { let builder_started = Instant::now(); - let client: BuilderClient::Base> = - BuilderClient::new(api_url); + let client: BuilderClient> = BuilderClient::new(api_url); assert!(client.connect(Duration::from_millis(100)).await); let (pub_key, private_key) = diff --git a/crates/testing/tests/tests_1/test_success.rs b/crates/testing/tests/tests_1/test_success.rs index e81060aedb..a40d865ed6 100644 --- a/crates/testing/tests/tests_1/test_success.rs +++ b/crates/testing/tests/tests_1/test_success.rs @@ -9,7 +9,7 @@ use std::time::Duration; use hotshot_example_types::{ node_types::{ EpochsTestVersions, Libp2pImpl, MemoryImpl, PushCdnImpl, TestConsecutiveLeaderTypes, - TestTypes, TestTypesRandomizedLeader, TestVersions, + TestTypes, TestTypesRandomizedCommitteeMembers, TestTypesRandomizedLeader, TestVersions, }, testable_delay::{DelayConfig, DelayOptions, DelaySettings, SupportedTraitTypesForAsyncDelay}, }; @@ -41,6 +41,25 @@ cross_tests!( }, ); +cross_tests!( + TestName: test_epoch_success, + Impls: [MemoryImpl, Libp2pImpl, PushCdnImpl], + Types: [TestTypes, TestTypesRandomizedLeader, TestTypesRandomizedCommitteeMembers<123, 2>], + Versions: [EpochsTestVersions], + Ignore: false, + Metadata: { + TestDescription { + // allow more time to pass in CI + completion_task_description: CompletionTaskDescription::TimeBasedCompletionTaskBuilder( + TimeBasedCompletionTaskDescription { + duration: Duration::from_secs(60), + }, + ), + ..TestDescription::default() + } + }, +); + cross_tests!( TestName: test_success_with_async_delay, Impls: [MemoryImpl, Libp2pImpl, PushCdnImpl],