diff --git a/builder/src/lib.rs b/builder/src/lib.rs index 534773df39..2a16ff89e5 100755 --- a/builder/src/lib.rs +++ b/builder/src/lib.rs @@ -139,6 +139,7 @@ pub mod testing { stop_proposing_time: 0, stop_voting_time: 0, epoch_height: 0, + epoch_start_block: 0, }; Self { diff --git a/hotshot-examples/infra/mod.rs b/hotshot-examples/infra/mod.rs index f8fcdf4c9f..c125bd371f 100755 --- a/hotshot-examples/infra/mod.rs +++ b/hotshot-examples/infra/mod.rs @@ -368,6 +368,8 @@ pub trait RunDa< let initializer = hotshot::HotShotInitializer::::from_genesis::( TestInstanceState::default(), self.config().config.epoch_height, + self.config().config.epoch_start_block, + vec![], ) .await .expect("Couldn't generate genesis block"); diff --git a/hotshot-orchestrator/run-config.toml b/hotshot-orchestrator/run-config.toml index 1e5a8700d7..110fa5a85d 100644 --- a/hotshot-orchestrator/run-config.toml +++ b/hotshot-orchestrator/run-config.toml @@ -35,6 +35,7 @@ fixed_leader_for_gpuvid = 1 next_view_timeout = 30000 num_bootstrap = 5 epoch_height = 0 +epoch_start_block = 0 [random_builder] txn_in_block = 100 diff --git a/hotshot-query-service/examples/simple-server.rs b/hotshot-query-service/examples/simple-server.rs index d22c341858..a4e2ebd36c 100644 --- a/hotshot-query-service/examples/simple-server.rs +++ b/hotshot-query-service/examples/simple-server.rs @@ -217,6 +217,7 @@ async fn init_consensus( start_voting_time: 0, stop_voting_time: 0, epoch_height: 0, + epoch_start_block: 0, }; let nodes = join_all(priv_keys.into_iter().zip(data_sources).enumerate().map( @@ -246,6 +247,8 @@ async fn init_consensus( HotShotInitializer::from_genesis::( TestInstanceState::default(), 0, + 0, + vec![], ) .await .unwrap(), diff --git a/hotshot-query-service/src/testing/consensus.rs b/hotshot-query-service/src/testing/consensus.rs index 4e04ac0a86..88a146aa79 100644 --- a/hotshot-query-service/src/testing/consensus.rs +++ b/hotshot-query-service/src/testing/consensus.rs @@ -149,6 +149,7 @@ impl MockNetwork { start_voting_time: 0, stop_voting_time: 0, epoch_height: 0, + epoch_start_block: 0, }; update_config(&mut config); @@ -191,6 +192,8 @@ impl MockNetwork { HotShotInitializer::from_genesis::( TestInstanceState::default(), 0, + 0, + vec![], ) .await .unwrap(), diff --git a/hotshot-task-impls/src/helpers.rs b/hotshot-task-impls/src/helpers.rs index cd6aade29b..b56a6e736d 100644 --- a/hotshot-task-impls/src/helpers.rs +++ b/hotshot-task-impls/src/helpers.rs @@ -186,6 +186,7 @@ async fn decide_epoch_root( TYPES::Epoch::new(epoch_from_block_number(decided_block_number, epoch_height) + 1); let write_callback = { + tracing::debug!("Calling add_epoch_root for epoch {:?}", next_epoch_number); let membership_reader = membership.read().await; membership_reader .add_epoch_root(next_epoch_number, decided_leaf.block_header().clone()) @@ -364,7 +365,7 @@ pub async fn decide_from_proposal_2( /// # Panics /// If the leaf chain contains no decided leaf while reaching a decided view, which should be /// impossible. -pub async fn decide_from_proposal( +pub async fn decide_from_proposal( proposal: &QuorumProposalWrapper, consensus: OuterConsensus, existing_upgrade_cert: Arc>>>, @@ -477,10 +478,10 @@ pub async fn decide_from_proposal( tracing::debug!("Leaf ascension failed; error={e}"); } - if with_epochs && res.new_decided_view_number.is_some() { - let epoch_height = consensus_reader.epoch_height; - drop(consensus_reader); + let epoch_height = consensus_reader.epoch_height; + drop(consensus_reader); + if with_epochs && res.new_decided_view_number.is_some() { if let Some(decided_leaf_info) = res.leaf_views.last() { decide_epoch_root(&decided_leaf_info.leaf, epoch_height, membership).await; } else { diff --git a/hotshot-task-impls/src/quorum_vote/handlers.rs b/hotshot-task-impls/src/quorum_vote/handlers.rs index 7f1937c253..768d6a162c 100644 --- a/hotshot-task-impls/src/quorum_vote/handlers.rs +++ b/hotshot-task-impls/src/quorum_vote/handlers.rs @@ -13,7 +13,7 @@ use committable::Committable; use hotshot_types::{ consensus::OuterConsensus, data::{Leaf2, QuorumProposalWrapper, VidDisperseShare}, - drb::{compute_drb_result, DrbResult}, + drb::{compute_drb_result, DrbResult, INITIAL_DRB_RESULT}, event::{Event, EventType}, message::{Proposal, UpgradeLock}, simple_vote::{HasEpoch, QuorumData2, QuorumVote2}, @@ -51,6 +51,7 @@ async fn notify_membership_of_drb_result( epoch: ::Epoch, drb_result: DrbResult, ) { + tracing::debug!("Calling add_drb_result for epoch {:?}", epoch); membership.write().await.add_drb_result(epoch, drb_result); } @@ -379,7 +380,7 @@ pub(crate) async fn handle_quorum_proposal_validated< ) .await } else { - decide_from_proposal( + decide_from_proposal::( proposal, OuterConsensus::new(Arc::clone(&task_state.consensus.inner_consensus)), Arc::clone(&task_state.upgrade_lock.decided_upgrade_certificate), @@ -390,21 +391,58 @@ pub(crate) async fn handle_quorum_proposal_validated< .await }; + if let Some(cert) = &task_state.staged_epoch_upgrade_certificate { + if leaf_views.last().unwrap().leaf.height() >= task_state.epoch_upgrade_block_height { + let mut decided_certificate_lock = task_state + .upgrade_lock + .decided_upgrade_certificate + .write() + .await; + *decided_certificate_lock = Some(cert.clone()); + drop(decided_certificate_lock); + + let _ = task_state + .storage + .write() + .await + .update_decided_upgrade_certificate(Some(cert.clone())) + .await; + + task_state.staged_epoch_upgrade_certificate = None; + } + }; + if let Some(cert) = decided_upgrade_cert.clone() { - let mut decided_certificate_lock = task_state - .upgrade_lock - .decided_upgrade_certificate - .write() - .await; - *decided_certificate_lock = Some(cert.clone()); - drop(decided_certificate_lock); + if cert.data.new_version == V::Epochs::VERSION { + task_state.staged_epoch_upgrade_certificate = Some(cert); - let _ = task_state - .storage - .write() - .await - .update_decided_upgrade_certificate(Some(cert.clone())) - .await; + let epoch_height = task_state.consensus.read().await.epoch_height; + let first_epoch_number = TYPES::Epoch::new(epoch_from_block_number( + task_state.epoch_upgrade_block_height, + epoch_height, + )); + tracing::debug!("Calling set_first_epoch for epoch {:?}", first_epoch_number); + task_state + .membership + .write() + .await + .set_first_epoch(first_epoch_number, INITIAL_DRB_RESULT); + } else { + let mut decided_certificate_lock = task_state + .upgrade_lock + .decided_upgrade_certificate + .write() + .await; + *decided_certificate_lock = Some(cert.clone()); + drop(decided_certificate_lock); + + let _ = task_state + .storage + .write() + .await + .update_decided_upgrade_certificate(Some(cert.clone())) + .await; + } } let mut consensus_writer = task_state.consensus.write().await; diff --git a/hotshot-task-impls/src/quorum_vote/mod.rs b/hotshot-task-impls/src/quorum_vote/mod.rs index 32354d7c6b..8b26cbb39a 100644 --- a/hotshot-task-impls/src/quorum_vote/mod.rs +++ b/hotshot-task-impls/src/quorum_vote/mod.rs @@ -22,6 +22,7 @@ use hotshot_types::{ drb::DrbComputation, event::Event, message::{Proposal, UpgradeLock}, + simple_certificate::UpgradeCertificate, simple_vote::HasEpoch, traits::{ block_contents::BlockHeader, @@ -338,6 +339,12 @@ pub struct QuorumVoteTaskState, V: /// Number of blocks in an epoch, zero means there are no epochs pub epoch_height: u64, + + /// Upgrade certificate to enable epochs, staged until we reach the specified block height + pub staged_epoch_upgrade_certificate: Option>, + + /// Block height at which to enable the epoch upgrade + pub epoch_upgrade_block_height: u64, } impl, V: Versions> QuorumVoteTaskState { diff --git a/hotshot-testing/src/helpers.rs b/hotshot-testing/src/helpers.rs index 253a1e4265..082e965b83 100644 --- a/hotshot-testing/src/helpers.rs +++ b/hotshot-testing/src/helpers.rs @@ -5,8 +5,6 @@ // along with the HotShot repository. If not, see . #![allow(clippy::panic)] -use std::{collections::BTreeMap, fmt::Debug, hash::Hash, marker::PhantomData, sync::Arc}; - use async_broadcast::{Receiver, Sender}; use async_lock::RwLock; use bitvec::bitvec; @@ -14,7 +12,7 @@ use committable::Committable; use hotshot::{ traits::{BlockPayload, NodeImplementation, TestableNodeImplementation}, types::{SignatureKey, SystemContextHandle}, - HotShotInitializer, SystemContext, + HotShotInitializer, InitializerEpochInfo, SystemContext, }; use hotshot_example_types::{ auction_results_provider_types::TestAuctionResultsProvider, @@ -24,9 +22,11 @@ use hotshot_example_types::{ storage_types::TestStorage, }; use hotshot_task_impls::events::HotShotEvent; +use hotshot_types::traits::node_implementation::ConsensusTime; use hotshot_types::{ consensus::ConsensusMetricsValue, data::{vid_commitment, Leaf2, VidCommitment, VidDisperse, VidDisperseShare}, + drb::INITIAL_DRB_RESULT, message::{Proposal, UpgradeLock}, simple_certificate::DaCertificate2, simple_vote::{DaData2, DaVote2, SimpleVote, VersionedVoteData}, @@ -41,6 +41,7 @@ use hotshot_types::{ }; use primitive_types::U256; use serde::Serialize; +use std::{collections::BTreeMap, fmt::Debug, hash::Hash, marker::PhantomData, sync::Arc}; use vbs::version::Version; use crate::{test_builder::TestDescription, test_launcher::TestLauncher}; @@ -105,6 +106,19 @@ pub async fn build_system_handle_from_launcher< let initializer = HotShotInitializer::::from_genesis::( TestInstanceState::new(launcher.metadata.async_delay_config.clone()), launcher.metadata.test_config.epoch_height, + launcher.metadata.test_config.epoch_start_block, + vec![ + InitializerEpochInfo:: { + epoch: TYPES::Epoch::new(1), + drb_result: INITIAL_DRB_RESULT, + block_header: None, + }, + InitializerEpochInfo:: { + epoch: TYPES::Epoch::new(2), + drb_result: INITIAL_DRB_RESULT, + block_header: None, + }, + ], ) .await .unwrap(); diff --git a/hotshot-testing/src/spinning_task.rs b/hotshot-testing/src/spinning_task.rs index 8f54eaee0a..55b51949f9 100644 --- a/hotshot-testing/src/spinning_task.rs +++ b/hotshot-testing/src/spinning_task.rs @@ -14,7 +14,8 @@ use async_lock::RwLock; use async_trait::async_trait; use futures::future::join_all; use hotshot::{ - traits::TestableNodeImplementation, types::EventType, HotShotInitializer, SystemContext, + traits::TestableNodeImplementation, types::EventType, HotShotInitializer, InitializerEpochInfo, + SystemContext, }; use hotshot_example_types::{ auction_results_provider_types::TestAuctionResultsProvider, @@ -57,6 +58,10 @@ pub struct SpinningTask< > { /// epoch height pub epoch_height: u64, + /// Epoch start block + pub epoch_start_block: u64, + /// Saved epoch information. This must be sorted ascending by epoch. + pub start_epoch_info: Vec>, /// handle to the nodes pub(crate) handles: Arc>>>, /// late start nodes @@ -161,6 +166,8 @@ where let initializer = HotShotInitializer::::load( TestInstanceState::new(self.async_delay_config.clone()), self.epoch_height, + self.epoch_start_block, + self.start_epoch_info.clone(), self.last_decided_leaf.clone(), ( TYPES::View::genesis(), @@ -268,6 +275,8 @@ where let initializer = HotShotInitializer::::load( TestInstanceState::new(self.async_delay_config.clone()), self.epoch_height, + self.epoch_start_block, + self.start_epoch_info.clone(), self.last_decided_leaf.clone(), (start_view, start_epoch), (high_qc, next_epoch_high_qc), diff --git a/hotshot-testing/src/test_builder.rs b/hotshot-testing/src/test_builder.rs index 452d90473a..0d9d5d861f 100644 --- a/hotshot-testing/src/test_builder.rs +++ b/hotshot-testing/src/test_builder.rs @@ -4,25 +4,26 @@ // You should have received a copy of the MIT License // along with the HotShot repository. If not, see . -use std::{collections::HashMap, num::NonZeroUsize, rc::Rc, sync::Arc, time::Duration}; - use async_lock::RwLock; use hotshot::{ tasks::EventTransformerState, traits::{NetworkReliability, NodeImplementation, TestableNodeImplementation}, types::SystemContextHandle, - HotShotInitializer, MarketplaceConfig, SystemContext, TwinsHandlerState, + HotShotInitializer, InitializerEpochInfo, MarketplaceConfig, SystemContext, TwinsHandlerState, }; use hotshot_example_types::{ auction_results_provider_types::TestAuctionResultsProvider, node_types::TestTypes, state_types::TestInstanceState, storage_types::TestStorage, testable_delay::DelayConfig, }; +use hotshot_types::traits::node_implementation::ConsensusTime; use hotshot_types::{ consensus::ConsensusMetricsValue, + drb::INITIAL_DRB_RESULT, traits::node_implementation::{NodeType, Versions}, HotShotConfig, PeerConfig, ValidatorConfig, }; use hotshot_utils::anytrace::*; +use std::{collections::HashMap, num::NonZeroUsize, rc::Rc, sync::Arc, time::Duration}; use tide_disco::Url; use vec1::Vec1; @@ -61,6 +62,7 @@ pub fn default_hotshot_config( known_da_nodes: Vec>, num_bootstrap_nodes: usize, epoch_height: u64, + epoch_start_block: u64, ) -> HotShotConfig { HotShotConfig { start_threshold: (1, 1), @@ -85,6 +87,7 @@ pub fn default_hotshot_config( start_voting_time: u64::MAX, stop_voting_time: 0, epoch_height, + epoch_start_block, } } @@ -238,6 +241,12 @@ pub async fn create_test_handle< let initializer = HotShotInitializer::::from_genesis::( TestInstanceState::new(metadata.async_delay_config), metadata.test_config.epoch_height, + metadata.test_config.epoch_start_block, + vec![InitializerEpochInfo:: { + epoch: TYPES::Epoch::new(1), + drb_result: INITIAL_DRB_RESULT, + block_header: None, + }], ) .await .unwrap(); @@ -393,6 +402,7 @@ impl, V: Versions> TestDescription let num_nodes_with_stake = 20; let num_da_nodes = 14; let epoch_height = 10; + let epoch_start_block = 0; let (staked_nodes, da_nodes) = gen_node_lists::(num_nodes_with_stake, num_da_nodes); @@ -402,6 +412,7 @@ impl, V: Versions> TestDescription da_nodes, num_nodes_with_stake.try_into().unwrap(), epoch_height, + epoch_start_block, ), // The first 14 (i.e., 20 - f) nodes are in the DA committee and we may shutdown the // remaining 6 (i.e., f) nodes. We could remove this restriction after fixing the @@ -438,6 +449,7 @@ impl, V: Versions> TestDescription da_nodes, self.test_config.num_bootstrap, self.test_config.epoch_height, + self.test_config.epoch_start_block, ), ..self } @@ -463,6 +475,7 @@ impl, V: Versions> Default let num_nodes_with_stake = 7; let num_da_nodes = num_nodes_with_stake; let epoch_height = 10; + let epoch_start_block = 0; let (staked_nodes, da_nodes) = gen_node_lists::(num_nodes_with_stake, num_da_nodes); @@ -472,6 +485,7 @@ impl, V: Versions> Default da_nodes, num_nodes_with_stake.try_into().unwrap(), epoch_height, + epoch_start_block, ), timing_data: TimingData::default(), skip_late: false, diff --git a/hotshot-testing/src/test_runner.rs b/hotshot-testing/src/test_runner.rs index 545004f4d2..dd6cf62661 100644 --- a/hotshot-testing/src/test_runner.rs +++ b/hotshot-testing/src/test_runner.rs @@ -5,15 +5,10 @@ // along with the HotShot repository. If not, see . #![allow(clippy::panic)] -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - marker::PhantomData, - sync::Arc, -}; - use async_broadcast::{broadcast, Receiver, Sender}; use async_lock::RwLock; use futures::future::join_all; +use hotshot::InitializerEpochInfo; use hotshot::{ traits::TestableNodeImplementation, types::{Event, SystemContextHandle}, @@ -27,6 +22,7 @@ use hotshot_example_types::{ }; use hotshot_fakeapi::fake_solver::FakeSolverState; use hotshot_task_impls::events::HotShotEvent; +use hotshot_types::drb::INITIAL_DRB_RESULT; use hotshot_types::{ consensus::ConsensusMetricsValue, constants::EVENT_CHANNEL_SIZE, @@ -37,9 +33,13 @@ use hotshot_types::{ network::ConnectedNetwork, node_implementation::{ConsensusTime, NodeImplementation, NodeType, Versions}, }, - utils::genesis_epoch_from_version, HotShotConfig, ValidatorConfig, }; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + marker::PhantomData, + sync::Arc, +}; use tide_disco::Url; use tokio::{spawn, task::JoinHandle}; #[allow(deprecated)] @@ -178,6 +178,8 @@ where let spinning_task_state = SpinningTask { epoch_height: launcher.metadata.test_config.epoch_height, + epoch_start_block: launcher.metadata.test_config.epoch_start_block, + start_epoch_info: Vec::new(), // #2652 REVIEW NOTE: Same as other instances of start_epoch_info handles: Arc::clone(&handles), late_start, latest_view: None, @@ -318,7 +320,6 @@ where pub async fn init_builders>( &self, - num_nodes: usize, ) -> (Vec>>, Vec, Url) { let config = self.launcher.metadata.test_config.clone(); let mut builder_tasks = Vec::new(); @@ -328,7 +329,7 @@ where let builder_url = Url::parse(&format!("http://localhost:{builder_port}")).expect("Invalid URL"); let builder_task = B::start( - num_nodes, + 0, // This field gets updated while the test is running, 0 is just to seed it builder_url.clone(), B::Config::default(), metadata.changes.clone(), @@ -393,15 +394,10 @@ where let mut results = vec![]; let config = self.launcher.metadata.test_config.clone(); - // TODO This is only a workaround. Number of nodes changes from epoch to epoch. Builder should be made epoch-aware. - let temp_memberships = ::Membership::new( - config.known_nodes_with_stake.clone(), - config.known_da_nodes.clone(), - ); - // #3967 is it enough to check versions now? Or should we also be checking epoch_height? - let num_nodes = temp_memberships.total_nodes(genesis_epoch_from_version::()); + // Num_nodes is updated on the fly now via claim_block_with_num_nodes. This stays around to seed num_nodes + // in the builders for tests which don't update that field. let (mut builder_tasks, builder_urls, fallback_builder_url) = - self.init_builders::(num_nodes).await; + self.init_builders::().await; if self.launcher.metadata.start_solver { self.add_solver(builder_urls.clone()).await; @@ -420,11 +416,6 @@ where self.next_node_id += 1; tracing::debug!("launch node {}", i); - //let memberships =Arc::new(RwLock::new(::Membership::new( - //config.known_nodes_with_stake.clone(), - //config.known_da_nodes.clone(), - //))); - config.builder_urls = builder_urls .clone() .try_into() @@ -475,6 +466,12 @@ where let initializer = HotShotInitializer::::from_genesis::( TestInstanceState::new(self.launcher.metadata.async_delay_config.clone()), config.epoch_height, + config.epoch_start_block, + vec![InitializerEpochInfo:: { + epoch: TYPES::Epoch::new(1), + drb_result: INITIAL_DRB_RESULT, + block_header: None, + }], ) .await .unwrap(); diff --git a/hotshot-testing/tests/tests_1/quorum_proposal_task.rs b/hotshot-testing/tests/tests_1/quorum_proposal_task.rs index 6a5db34c52..58a8c6f568 100644 --- a/hotshot-testing/tests/tests_1/quorum_proposal_task.rs +++ b/hotshot-testing/tests/tests_1/quorum_proposal_task.rs @@ -25,11 +25,9 @@ use hotshot_testing::{ view_generator::TestViewGenerator, }; use hotshot_types::{ - data::{null_block, EpochNumber, Leaf2, ViewChangeEvidence2, ViewNumber}, + data::{null_block, Leaf2, ViewChangeEvidence2, ViewNumber}, simple_vote::{TimeoutData2, ViewSyncFinalizeData2}, - traits::{ - node_implementation::{ConsensusTime, Versions}, - }, + traits::node_implementation::{ConsensusTime, Versions}, utils::BuilderCommitment, }; use sha2::Digest; @@ -59,7 +57,7 @@ async fn test_quorum_proposal_task_quorum_proposal_view_1() { let payload_commitment = build_payload_commitment::( &membership, ViewNumber::new(node_id), - Some(EpochNumber::new(1)), + None, version, ) .await; @@ -206,7 +204,7 @@ async fn test_quorum_proposal_task_quorum_proposal_view_gt_1() { build_payload_commitment::( &membership, ViewNumber::new(1), - Some(EpochNumber::new(1)), + None, version_1, ) .await, @@ -227,7 +225,7 @@ async fn test_quorum_proposal_task_quorum_proposal_view_gt_1() { build_payload_commitment::( &membership, ViewNumber::new(2), - Some(EpochNumber::new(1)), + None, version_2, ) .await, @@ -246,7 +244,7 @@ async fn test_quorum_proposal_task_quorum_proposal_view_gt_1() { build_payload_commitment::( &membership, ViewNumber::new(3), - Some(EpochNumber::new(1)), + None, version_3, ) .await, @@ -265,7 +263,7 @@ async fn test_quorum_proposal_task_quorum_proposal_view_gt_1() { build_payload_commitment::( &membership, ViewNumber::new(4), - Some(EpochNumber::new(1)), + None, version_4, ) .await, @@ -284,7 +282,7 @@ async fn test_quorum_proposal_task_quorum_proposal_view_gt_1() { build_payload_commitment::( &membership, ViewNumber::new(5), - Some(EpochNumber::new(1)), + None, version_5, ) .await, @@ -338,7 +336,7 @@ async fn test_quorum_proposal_task_qc_timeout() { let payload_commitment = build_payload_commitment::( &membership, ViewNumber::new(node_id), - Some(EpochNumber::new(1)), + None, version, ) .await; @@ -433,7 +431,7 @@ async fn test_quorum_proposal_task_view_sync() { let payload_commitment = build_payload_commitment::( &membership, ViewNumber::new(node_id), - Some(EpochNumber::new(1)), + None, version, ) .await; @@ -574,7 +572,7 @@ async fn test_quorum_proposal_task_liveness_check() { build_payload_commitment::( &membership, ViewNumber::new(1), - Some(EpochNumber::new(1)), + None, version_1, ) .await, @@ -595,7 +593,7 @@ async fn test_quorum_proposal_task_liveness_check() { build_payload_commitment::( &membership, ViewNumber::new(2), - Some(EpochNumber::new(1)), + None, version_2, ) .await, @@ -614,7 +612,7 @@ async fn test_quorum_proposal_task_liveness_check() { build_payload_commitment::( &membership, ViewNumber::new(3), - Some(EpochNumber::new(1)), + None, version_3, ) .await, @@ -633,7 +631,7 @@ async fn test_quorum_proposal_task_liveness_check() { build_payload_commitment::( &membership, ViewNumber::new(4), - Some(EpochNumber::new(1)), + None, version_4, ) .await, @@ -652,7 +650,7 @@ async fn test_quorum_proposal_task_liveness_check() { build_payload_commitment::( &membership, ViewNumber::new(5), - Some(EpochNumber::new(1)), + None, version_5, ) .await, diff --git a/hotshot-testing/tests/tests_1/transaction_task.rs b/hotshot-testing/tests/tests_1/transaction_task.rs index ee5a80bed0..bab4968388 100644 --- a/hotshot-testing/tests/tests_1/transaction_task.rs +++ b/hotshot-testing/tests/tests_1/transaction_task.rs @@ -9,9 +9,7 @@ use hotshot_task_impls::{ use hotshot_testing::helpers::build_system_handle; use hotshot_types::{ data::{null_block, EpochNumber, PackedBundle, ViewNumber}, - traits::{ - node_implementation::{ConsensusTime, Versions}, - }, + traits::node_implementation::{ConsensusTime, Versions}, }; use vbs::version::StaticVersionType; diff --git a/hotshot-testing/tests/tests_1/upgrade_task_with_proposal.rs b/hotshot-testing/tests/tests_1/upgrade_task_with_proposal.rs index 3707be29d8..00f970a40b 100644 --- a/hotshot-testing/tests/tests_1/upgrade_task_with_proposal.rs +++ b/hotshot-testing/tests/tests_1/upgrade_task_with_proposal.rs @@ -159,7 +159,7 @@ async fn test_upgrade_task_with_proposal() { build_payload_commitment::( &membership, ViewNumber::new(1), - Some(EpochNumber::new(1)), + None, version_1, ) .await, @@ -180,7 +180,7 @@ async fn test_upgrade_task_with_proposal() { build_payload_commitment::( &membership, ViewNumber::new(2), - Some(EpochNumber::new(1)), + None, version_2, ) .await, @@ -200,7 +200,7 @@ async fn test_upgrade_task_with_proposal() { build_payload_commitment::( &membership, ViewNumber::new(3), - Some(EpochNumber::new(1)), + None, version_3, ) .await, diff --git a/hotshot-testing/tests/tests_6/test_epochs.rs b/hotshot-testing/tests/tests_6/test_epochs.rs index 43c71d188d..686ea8f6ab 100644 --- a/hotshot-testing/tests/tests_6/test_epochs.rs +++ b/hotshot-testing/tests/tests_6/test_epochs.rs @@ -4,7 +4,6 @@ // You should have received a copy of the MIT License // along with the HotShot repository. If not, see . -use std::time::Duration; use hotshot_example_types::{ node_types::{ CombinedImpl, EpochUpgradeTestVersions, EpochsTestVersions, Libp2pImpl, MemoryImpl, @@ -23,6 +22,7 @@ use hotshot_testing::{ test_builder::{TestDescription, TimingData}, view_sync_task::ViewSyncTaskDescription, }; +use std::time::Duration; cross_tests!( TestName: test_success_with_epochs, diff --git a/hotshot-types/src/hotshot_config_file.rs b/hotshot-types/src/hotshot_config_file.rs index 53633d5d7a..7eca53be10 100644 --- a/hotshot-types/src/hotshot_config_file.rs +++ b/hotshot-types/src/hotshot_config_file.rs @@ -55,6 +55,8 @@ pub struct HotShotConfigFile { pub upgrade: UpgradeConfig, /// Number of blocks in an epoch, zero means there are no epochs pub epoch_height: u64, + /// Epoch start block + pub epoch_start_block: u64, } impl From> for HotShotConfig { @@ -83,6 +85,7 @@ impl From> for HotShotConfig { start_voting_time: val.upgrade.start_voting_time, stop_voting_time: val.upgrade.stop_voting_time, epoch_height: val.epoch_height, + epoch_start_block: val.epoch_start_block, } } } @@ -128,6 +131,7 @@ impl HotShotConfigFile { builder_urls: default_builder_urls(), upgrade: UpgradeConfig::default(), epoch_height: 0, + epoch_start_block: 0, } } } diff --git a/hotshot-types/src/lib.rs b/hotshot-types/src/lib.rs index 822fc62f6e..9066f14e08 100644 --- a/hotshot-types/src/lib.rs +++ b/hotshot-types/src/lib.rs @@ -221,6 +221,8 @@ pub struct HotShotConfig { pub stop_voting_time: u64, /// Number of blocks in an epoch, zero means there are no epochs pub epoch_height: u64, + /// Epoch start block + pub epoch_start_block: u64, } impl HotShotConfig { diff --git a/hotshot-types/src/traits/election.rs b/hotshot-types/src/traits/election.rs index 853bcfae5f..b57065f435 100644 --- a/hotshot-types/src/traits/election.rs +++ b/hotshot-types/src/traits/election.rs @@ -142,4 +142,10 @@ pub trait Membership: Debug + Send + Sync { /// Called to notify the Membership when a new DRB result has been calculated. /// Observes the same semantics as add_epoch_root fn add_drb_result(&mut self, _epoch: TYPES::Epoch, _drb_result: DrbResult); + + /// Called to notify the Membership that Epochs are enabled. + /// Implementations should copy the pre-epoch stake table into epoch and epoch+1 + /// when this is called. The value of initial_drb_result should be used for DRB + /// calculations for epochs (epoch+1) and earlier. + fn set_first_epoch(&mut self, _epoch: TYPES::Epoch, _initial_drb_result: DrbResult); } diff --git a/hotshot/src/lib.rs b/hotshot/src/lib.rs index 84400a2364..6b0e77b1cb 100644 --- a/hotshot/src/lib.rs +++ b/hotshot/src/lib.rs @@ -14,6 +14,7 @@ pub mod documentation; use committable::Committable; use futures::future::{select, Either}; use hotshot_types::{ + drb::{DrbResult, INITIAL_DRB_RESULT}, message::UpgradeLock, traits::{block_contents::BlockHeader, network::BroadcastDelay, node_implementation::Versions}, }; @@ -256,7 +257,7 @@ impl, V: Versions> SystemContext::PrivateKey, nonce: u64, config: HotShotConfig, - memberships: Arc>, + membership: Arc>, network: Arc, initializer: HotShotInitializer, metrics: ConsensusMetricsValue, @@ -300,6 +301,8 @@ impl, V: Versions> SystemContext, V: Versions> SystemContext, V: Versions> ConsensusApi { + pub epoch: TYPES::Epoch, + pub drb_result: DrbResult, + // pub stake_table: Option, // TODO: Figure out how to connect this up + pub block_header: Option, +} + #[derive(Clone)] /// initializer struct for creating starting block pub struct HotShotInitializer { @@ -995,6 +1006,9 @@ pub struct HotShotInitializer { /// Epoch height pub epoch_height: u64, + /// Epoch start block + pub epoch_start_block: u64, + /// the anchor leaf for the hotshot initializer pub anchor_leaf: Leaf2, @@ -1037,6 +1051,9 @@ pub struct HotShotInitializer { /// Saved VID shares pub saved_vid_shares: VidShares, + + /// Saved epoch information. This must be sorted ascending by epoch. + pub start_epoch_info: Vec>, } impl HotShotInitializer { @@ -1046,6 +1063,8 @@ impl HotShotInitializer { pub async fn from_genesis( instance_state: TYPES::InstanceState, epoch_height: u64, + epoch_start_block: u64, + start_epoch_info: Vec>, ) -> Result> { let (validated_state, state_delta) = TYPES::ValidatedState::genesis(&instance_state); let high_qc = QuorumCertificate2::genesis::(&validated_state, &instance_state).await; @@ -1066,6 +1085,8 @@ impl HotShotInitializer { instance_state, saved_vid_shares: BTreeMap::new(), epoch_height, + epoch_start_block, + start_epoch_info, }) } @@ -1117,6 +1138,8 @@ impl HotShotInitializer { pub fn load( instance_state: TYPES::InstanceState, epoch_height: u64, + epoch_start_block: u64, + start_epoch_info: Vec>, anchor_leaf: Leaf2, (start_view, start_epoch): (TYPES::View, Option), (high_qc, next_epoch_high_qc): ( @@ -1135,6 +1158,7 @@ impl HotShotInitializer { let initializer = Self { instance_state, epoch_height, + epoch_start_block, anchor_leaf, anchor_state, anchor_state_delta, @@ -1148,8 +1172,43 @@ impl HotShotInitializer { decided_upgrade_certificate, undecided_leaves: BTreeMap::new(), undecided_state: BTreeMap::new(), + start_epoch_info, }; initializer.update_undecided() } } + +async fn load_start_epoch_info( + membership: &Arc>, + start_epoch_info: &Vec>, +) { + for epoch_info in start_epoch_info { + tracing::debug!("Calling add_drb_result for epoch {:?}", epoch_info.epoch); + membership + .write() + .await + .add_drb_result(epoch_info.epoch, epoch_info.drb_result); + + if let Some(block_header) = &epoch_info.block_header { + tracing::debug!("Calling add_epoch_root for epoch {:?}", epoch_info.epoch); + let write_callback = { + let membership_reader = membership.read().await; + membership_reader + .add_epoch_root(epoch_info.epoch, block_header.clone()) + .await + }; + + if let Some(write_callback) = write_callback { + let mut membership_writer = membership.write().await; + write_callback(&mut *membership_writer); + } + } else { + tracing::debug!("Calling set_first_epoch for epoch {:?}", epoch_info.epoch); + membership + .write() + .await + .set_first_epoch(epoch_info.epoch, INITIAL_DRB_RESULT); + } + } +} diff --git a/hotshot/src/tasks/task_state.rs b/hotshot/src/tasks/task_state.rs index b0c1b9a11d..d574ac91c6 100644 --- a/hotshot/src/tasks/task_state.rs +++ b/hotshot/src/tasks/task_state.rs @@ -4,6 +4,12 @@ // You should have received a copy of the MIT License // along with the HotShot repository. If not, see . +use std::{ + collections::{BTreeMap, HashMap}, + sync::{atomic::AtomicBool, Arc}, + time::Instant, +}; + use async_trait::async_trait; use chrono::Utc; use hotshot_task_impls::{ @@ -20,11 +26,6 @@ use hotshot_types::{ node_implementation::{ConsensusTime, NodeImplementation, NodeType}, }, }; -use std::{ - collections::{BTreeMap, HashMap}, - sync::{atomic::AtomicBool, Arc}, - time::Instant, -}; use tokio::spawn; use crate::{types::SystemContextHandle, Versions}; @@ -244,6 +245,8 @@ impl, V: Versions> CreateTaskState storage: Arc::clone(&handle.storage), upgrade_lock: handle.hotshot.upgrade_lock.clone(), epoch_height: handle.hotshot.config.epoch_height, + epoch_upgrade_block_height: handle.hotshot.config.epoch_start_block, + staged_epoch_upgrade_certificate: None, consensus_metrics, } } diff --git a/hotshot/src/traits/election/randomized_committee.rs b/hotshot/src/traits/election/randomized_committee.rs index dca2593d33..cd3838fa05 100644 --- a/hotshot/src/traits/election/randomized_committee.rs +++ b/hotshot/src/traits/election/randomized_committee.rs @@ -265,5 +265,7 @@ impl Membership for Committee { .unwrap() } - fn add_drb_result(&mut self, _epoch: ::Epoch, _drb: DrbResult) {} + fn add_drb_result(&mut self, _epoch: ::Epoch, _drb_result: DrbResult) {} + + fn set_first_epoch(&mut self, _epoch: TYPES::Epoch, _initial_drb_result: DrbResult) {} } diff --git a/hotshot/src/traits/election/randomized_committee_members.rs b/hotshot/src/traits/election/randomized_committee_members.rs index 78d5881005..7a66573096 100644 --- a/hotshot/src/traits/election/randomized_committee_members.rs +++ b/hotshot/src/traits/election/randomized_committee_members.rs @@ -448,4 +448,6 @@ impl Membership } fn add_drb_result(&mut self, _epoch: ::Epoch, _drb_result: DrbResult) {} + + fn set_first_epoch(&mut self, _epoch: TYPES::Epoch, _initial_drb_result: DrbResult) {} } diff --git a/hotshot/src/traits/election/static_committee.rs b/hotshot/src/traits/election/static_committee.rs index 6b6232a5a6..603fc45d44 100644 --- a/hotshot/src/traits/election/static_committee.rs +++ b/hotshot/src/traits/election/static_committee.rs @@ -41,6 +41,22 @@ pub struct StaticCommittee { /// The nodes on the committee and their stake, indexed by public key indexed_da_stake_table: BTreeMap>, + + /// The first epoch which will be encountered. For testing, will panic if an epoch-carrying function is called + /// when first_epoch is None or is Some greater than that epoch. + first_epoch: Option, +} + +impl StaticCommittee { + fn check_first_epoch(&self, epoch: Option<::Epoch>) { + if let Some(epoch) = epoch { + if let Some(first_epoch) = self.first_epoch { + assert!(first_epoch <= epoch, "Called a method in StaticCommittee where first_epoch={first_epoch:} but epoch={epoch}"); + } else { + panic!("Called a method in StaticCommittee with non-None epoch={epoch}, but set_first_epoch was not yet called"); + } + } + } } impl Membership for StaticCommittee { @@ -98,22 +114,25 @@ impl Membership for StaticCommittee { da_stake_table: da_members, indexed_stake_table, indexed_da_stake_table, + first_epoch: None, } } /// Get the stake table for the current view fn stake_table( &self, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> Vec::SignatureKey>> { + self.check_first_epoch(epoch); self.stake_table.clone() } /// Get the stake table for the current view fn da_stake_table( &self, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> Vec::SignatureKey>> { + self.check_first_epoch(epoch); self.da_stake_table.clone() } @@ -121,8 +140,9 @@ impl Membership for StaticCommittee { fn committee_members( &self, _view_number: ::View, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> BTreeSet<::SignatureKey> { + self.check_first_epoch(epoch); self.stake_table .iter() .map(|sc| TYPES::SignatureKey::public_key(&sc.stake_table_entry)) @@ -133,8 +153,9 @@ impl Membership for StaticCommittee { fn da_committee_members( &self, _view_number: ::View, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> BTreeSet<::SignatureKey> { + self.check_first_epoch(epoch); self.da_stake_table .iter() .map(|da| TYPES::SignatureKey::public_key(&da.stake_table_entry)) @@ -145,8 +166,9 @@ impl Membership for StaticCommittee { fn committee_leaders( &self, _view_number: ::View, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> BTreeSet<::SignatureKey> { + self.check_first_epoch(epoch); self.eligible_leaders .iter() .map(|leader| TYPES::SignatureKey::public_key(&leader.stake_table_entry)) @@ -157,8 +179,9 @@ impl Membership for StaticCommittee { fn stake( &self, pub_key: &::SignatureKey, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> Option::SignatureKey>> { + self.check_first_epoch(epoch); // Only return the stake if it is above zero self.indexed_stake_table.get(pub_key).cloned() } @@ -167,8 +190,9 @@ impl Membership for StaticCommittee { fn da_stake( &self, pub_key: &::SignatureKey, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> Option::SignatureKey>> { + self.check_first_epoch(epoch); // Only return the stake if it is above zero self.indexed_da_stake_table.get(pub_key).cloned() } @@ -177,8 +201,9 @@ impl Membership for StaticCommittee { fn has_stake( &self, pub_key: &::SignatureKey, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> bool { + self.check_first_epoch(epoch); self.indexed_stake_table .get(pub_key) .is_some_and(|x| x.stake_table_entry.stake() > U256::zero()) @@ -188,8 +213,9 @@ impl Membership for StaticCommittee { fn has_da_stake( &self, pub_key: &::SignatureKey, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> bool { + self.check_first_epoch(epoch); self.indexed_da_stake_table .get(pub_key) .is_some_and(|x| x.stake_table_entry.stake() > U256::zero()) @@ -199,8 +225,9 @@ impl Membership for StaticCommittee { fn lookup_leader( &self, view_number: ::View, - _epoch: Option<::Epoch>, + epoch: Option<::Epoch>, ) -> Result { + self.check_first_epoch(epoch); #[allow(clippy::cast_possible_truncation)] let index = *view_number as usize % self.eligible_leaders.len(); let res = self.eligible_leaders[index].clone(); @@ -208,35 +235,45 @@ impl Membership for StaticCommittee { } /// Get the total number of nodes in the committee - fn total_nodes(&self, _epoch: Option<::Epoch>) -> usize { + fn total_nodes(&self, epoch: Option<::Epoch>) -> usize { + self.check_first_epoch(epoch); self.stake_table.len() } /// Get the total number of DA nodes in the committee - fn da_total_nodes(&self, _epoch: Option<::Epoch>) -> usize { + fn da_total_nodes(&self, epoch: Option<::Epoch>) -> usize { + self.check_first_epoch(epoch); self.da_stake_table.len() } /// Get the voting success threshold for the committee - fn success_threshold(&self, _epoch: Option<::Epoch>) -> NonZeroU64 { + fn success_threshold(&self, epoch: Option<::Epoch>) -> NonZeroU64 { + self.check_first_epoch(epoch); NonZeroU64::new(((self.stake_table.len() as u64 * 2) / 3) + 1).unwrap() } /// Get the voting success threshold for the committee - fn da_success_threshold(&self, _epoch: Option<::Epoch>) -> NonZeroU64 { + fn da_success_threshold(&self, epoch: Option<::Epoch>) -> NonZeroU64 { + self.check_first_epoch(epoch); NonZeroU64::new(((self.da_stake_table.len() as u64 * 2) / 3) + 1).unwrap() } /// Get the voting failure threshold for the committee - fn failure_threshold(&self, _epoch: Option<::Epoch>) -> NonZeroU64 { + fn failure_threshold(&self, epoch: Option<::Epoch>) -> NonZeroU64 { + self.check_first_epoch(epoch); NonZeroU64::new(((self.stake_table.len() as u64) / 3) + 1).unwrap() } /// Get the voting upgrade threshold for the committee - fn upgrade_threshold(&self, _epoch: Option<::Epoch>) -> NonZeroU64 { + fn upgrade_threshold(&self, epoch: Option<::Epoch>) -> NonZeroU64 { + self.check_first_epoch(epoch); let len = self.stake_table.len(); NonZeroU64::new(max((len as u64 * 9) / 10, ((len as u64 * 2) / 3) + 1)).unwrap() } fn add_drb_result(&mut self, _epoch: ::Epoch, _drb_result: DrbResult) {} + + fn set_first_epoch(&mut self, epoch: TYPES::Epoch, _initial_drb_result: DrbResult) { + self.first_epoch = Some(epoch); + } } diff --git a/hotshot/src/traits/election/static_committee_leader_two_views.rs b/hotshot/src/traits/election/static_committee_leader_two_views.rs index 37ce00dcd5..64b79112e8 100644 --- a/hotshot/src/traits/election/static_committee_leader_two_views.rs +++ b/hotshot/src/traits/election/static_committee_leader_two_views.rs @@ -240,4 +240,6 @@ impl Membership for StaticCommitteeLeaderForTwoViews::Epoch, _drb_result: DrbResult) {} + + fn set_first_epoch(&mut self, _epoch: TYPES::Epoch, _initial_drb_result: DrbResult) {} } diff --git a/hotshot/src/traits/election/two_static_committees.rs b/hotshot/src/traits/election/two_static_committees.rs index 205a127029..c13200c75e 100644 --- a/hotshot/src/traits/election/two_static_committees.rs +++ b/hotshot/src/traits/election/two_static_committees.rs @@ -429,4 +429,6 @@ impl Membership for TwoStaticCommittees { } fn add_drb_result(&mut self, _epoch: ::Epoch, _drb_result: DrbResult) {} + + fn set_first_epoch(&mut self, _epoch: TYPES::Epoch, _initial_drb_result: DrbResult) {} } diff --git a/sequencer/src/api/data_source.rs b/sequencer/src/api/data_source.rs index 1232865f7b..a74fcce4d9 100644 --- a/sequencer/src/api/data_source.rs +++ b/sequencer/src/api/data_source.rs @@ -248,6 +248,7 @@ pub struct PublicHotShotConfig { start_voting_time: u64, stop_voting_time: u64, epoch_height: u64, + epoch_start_block: u64, } impl From> for PublicHotShotConfig { @@ -277,6 +278,7 @@ impl From> for PublicHotShotConfig { start_voting_time, stop_voting_time, epoch_height, + epoch_start_block, } = v; Self { @@ -301,6 +303,7 @@ impl From> for PublicHotShotConfig { start_voting_time, stop_voting_time, epoch_height, + epoch_start_block, } } } @@ -329,6 +332,7 @@ impl PublicHotShotConfig { start_voting_time: self.start_voting_time, stop_voting_time: self.stop_voting_time, epoch_height: self.epoch_height, + epoch_start_block: self.epoch_start_block, } } } diff --git a/sequencer/src/lib.rs b/sequencer/src/lib.rs index c50598d6da..72f6566bc3 100644 --- a/sequencer/src/lib.rs +++ b/sequencer/src/lib.rs @@ -812,6 +812,7 @@ pub mod testing { stop_proposing_time: 0, stop_voting_time: 0, epoch_height: 0, + epoch_start_block: 0, }; Self { diff --git a/types/src/v0/impls/stake_table.rs b/types/src/v0/impls/stake_table.rs index 808e5bd54d..52b015a1b1 100644 --- a/types/src/v0/impls/stake_table.rs +++ b/types/src/v0/impls/stake_table.rs @@ -110,6 +110,10 @@ pub struct EpochCommittees { /// Randomized committees, filled when we receive the DrbResult randomized_committees: BTreeMap>>, + + /// Contains the epoch after which initial_drb_result will not be used (set_first_epoch.epoch + 2) + /// And the DrbResult to use before that epoch + initial_drb_result: Option<(Epoch, DrbResult)>, } #[derive(Debug, Clone, PartialEq)] @@ -277,6 +281,7 @@ impl EpochCommittees { l1_client: instance_state.l1_client.clone(), contract_address: instance_state.chain_config.stake_table_contract, randomized_committees: BTreeMap::new(), + initial_drb_result: None, } } @@ -497,6 +502,13 @@ impl Membership for EpochCommittees { self.randomized_committees .insert(epoch, randomized_committee); } + + fn set_first_epoch(&mut self, epoch: Epoch, initial_drb_result: DrbResult) { + self.state.insert(epoch, self.non_epoch_committee.clone()); + self.state + .insert(epoch + 1, self.non_epoch_committee.clone()); + self.initial_drb_result = Some((epoch + 2, initial_drb_result)); + } } #[cfg(test)] diff --git a/types/src/v0/traits.rs b/types/src/v0/traits.rs index 9a33355288..b5f5bd5d83 100644 --- a/types/src/v0/traits.rs +++ b/types/src/v0/traits.rs @@ -581,6 +581,7 @@ pub trait SequencerPersistence: Sized + Send + Sync + Clone + 'static { HotShotInitializer { instance_state: state, epoch_height: 0, + epoch_start_block: 0, anchor_leaf: leaf, anchor_state: validated_state.unwrap_or_default(), anchor_state_delta: None, @@ -597,6 +598,7 @@ pub trait SequencerPersistence: Sized + Send + Sync + Clone + 'static { .collect(), undecided_state, saved_vid_shares: Default::default(), // TODO: implement saved_vid_shares + start_epoch_info: Default::default(), // TODO: implement start_epoch_info }, anchor_view, ))