diff --git a/crates/example-types/src/storage_types.rs b/crates/example-types/src/storage_types.rs index 4f72a930fb..b5f3610f21 100644 --- a/crates/example-types/src/storage_types.rs +++ b/crates/example-types/src/storage_types.rs @@ -20,7 +20,7 @@ use hotshot_types::{ }, event::HotShotAction, message::Proposal, - simple_certificate::{QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2, UpgradeCertificate}, traits::{ node_implementation::{ConsensusTime, NodeType}, storage::Storage, @@ -52,6 +52,8 @@ pub struct TestStorageState { proposals2: BTreeMap>>, high_qc: Option>, high_qc2: Option>, + next_epoch_high_qc2: + Option>, action: TYPES::View, epoch: TYPES::Epoch, } @@ -66,6 +68,7 @@ impl Default for TestStorageState { proposals: BTreeMap::new(), proposals2: BTreeMap::new(), high_qc: None, + next_epoch_high_qc2: None, high_qc2: None, action: TYPES::View::genesis(), epoch: TYPES::Epoch::genesis(), @@ -112,6 +115,9 @@ impl TestStorage { pub async fn high_qc_cloned(&self) -> Option> { self.inner.read().await.high_qc2.clone() } + pub async fn next_epoch_high_qc_cloned(&self) -> Option> { + self.inner.read().await.next_epoch_high_qc2.clone() + } pub async fn decided_upgrade_certificate(&self) -> Option> { self.decided_upgrade_certificate.read().await.clone() } @@ -268,6 +274,26 @@ impl Storage for TestStorage { } Ok(()) } + async fn update_next_epoch_high_qc2( + &self, + new_next_epoch_high_qc: hotshot_types::simple_certificate::NextEpochQuorumCertificate2< + TYPES, + >, + ) -> Result<()> { + if self.should_return_err { + bail!("Failed to update next epoch high qc to storage"); + } + Self::run_delay_settings_from_config(&self.delay_config).await; + let mut inner = self.inner.write().await; + if let Some(ref current_next_epoch_high_qc) = inner.next_epoch_high_qc2 { + if new_next_epoch_high_qc.view_number() > current_next_epoch_high_qc.view_number() { + inner.next_epoch_high_qc2 = Some(new_next_epoch_high_qc); + } + } else { + inner.next_epoch_high_qc2 = Some(new_next_epoch_high_qc); + } + Ok(()) + } async fn update_undecided_state( &self, _leaves: CommitmentMap>, diff --git a/crates/hotshot/src/lib.rs b/crates/hotshot/src/lib.rs index 16fa47f31d..b76b3cc7c9 100644 --- a/crates/hotshot/src/lib.rs +++ b/crates/hotshot/src/lib.rs @@ -52,7 +52,7 @@ use hotshot_types::{ data::{Leaf2, QuorumProposal, QuorumProposal2}, event::{EventType, LeafInfo}, message::{convert_proposal, DataMessage, Message, MessageKind, Proposal}, - simple_certificate::{QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2, UpgradeCertificate}, traits::{ consensus_api::ConsensusApi, election::Membership, @@ -344,6 +344,7 @@ impl, V: Versions> SystemContext, V: Versions> SystemContext = @@ -518,7 +523,7 @@ impl, V: Versions> SystemContext { /// than `inner`s view number for the non genesis case because we must have seen higher QCs /// to decide on the leaf. high_qc: QuorumCertificate2, + /// Next epoch highest QC that was seen. This is needed to propose during epoch transition after restart. + next_epoch_high_qc: Option>, /// Previously decided upgrade certificate; this is necessary if an upgrade has happened and we are not restarting with the new version decided_upgrade_certificate: Option>, /// Undecided leaves that were seen, but not yet decided on. These allow a restarting node @@ -1030,6 +1037,7 @@ impl HotShotInitializer { actioned_view: TYPES::View::new(0), saved_proposals: BTreeMap::new(), high_qc, + next_epoch_high_qc: None, decided_upgrade_certificate: None, undecided_leaves: Vec::new(), undecided_state: BTreeMap::new(), @@ -1054,6 +1062,7 @@ impl HotShotInitializer { actioned_view: TYPES::View, saved_proposals: BTreeMap>>, high_qc: QuorumCertificate2, + next_epoch_high_qc: Option>, decided_upgrade_certificate: Option>, undecided_leaves: Vec>, undecided_state: BTreeMap>, @@ -1068,6 +1077,7 @@ impl HotShotInitializer { actioned_view, saved_proposals, high_qc, + next_epoch_high_qc, decided_upgrade_certificate, undecided_leaves, undecided_state, diff --git a/crates/hotshot/src/tasks/task_state.rs b/crates/hotshot/src/tasks/task_state.rs index 2c8d8607a4..3213ce2ce2 100644 --- a/crates/hotshot/src/tasks/task_state.rs +++ b/crates/hotshot/src/tasks/task_state.rs @@ -132,6 +132,7 @@ impl, V: Versions> CreateTaskState public_key: handle.public_key().clone(), private_key: handle.private_key().clone(), id: handle.hotshot.id, + epoch_height: handle.epoch_height, } } } @@ -318,6 +319,7 @@ impl, V: Versions> CreateTaskState network: Arc::clone(&handle.hotshot.network), membership: (*handle.hotshot.memberships).clone().into(), vote_collectors: BTreeMap::default(), + next_epoch_vote_collectors: BTreeMap::default(), timeout_vote_collectors: BTreeMap::default(), cur_view: handle.cur_view().await, cur_view_time: Utc::now().timestamp(), diff --git a/crates/hotshot/src/traits/election/static_committee.rs b/crates/hotshot/src/traits/election/static_committee.rs index d2b62f80b7..b6010c174d 100644 --- a/crates/hotshot/src/traits/election/static_committee.rs +++ b/crates/hotshot/src/traits/election/static_committee.rs @@ -215,22 +215,22 @@ impl Membership for StaticCommittee { } /// Get the voting success threshold for the committee - fn success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 { + fn success_threshold(&self, _epoch: TYPES::Epoch) -> NonZeroU64 { 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: ::Epoch) -> NonZeroU64 { + fn da_success_threshold(&self, _epoch: TYPES::Epoch) -> NonZeroU64 { 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: ::Epoch) -> NonZeroU64 { + fn failure_threshold(&self, _epoch: TYPES::Epoch) -> NonZeroU64 { NonZeroU64::new(((self.stake_table.len() as u64) / 3) + 1).unwrap() } /// Get the voting upgrade threshold for the committee - fn upgrade_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 { + fn upgrade_threshold(&self, _epoch: TYPES::Epoch) -> NonZeroU64 { let len = self.stake_table.len(); NonZeroU64::new(max((len as u64 * 9) / 10, ((len as u64 * 2) / 3) + 1)).unwrap() } diff --git a/crates/hotshot/src/traits/election/static_committee_leader_two_views.rs b/crates/hotshot/src/traits/election/static_committee_leader_two_views.rs index 8833d06872..d2635cc273 100644 --- a/crates/hotshot/src/traits/election/static_committee_leader_two_views.rs +++ b/crates/hotshot/src/traits/election/static_committee_leader_two_views.rs @@ -217,22 +217,22 @@ impl Membership for StaticCommitteeLeaderForTwoViews::Epoch) -> NonZeroU64 { + fn success_threshold(&self, _epoch: TYPES::Epoch) -> NonZeroU64 { 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: ::Epoch) -> NonZeroU64 { + fn da_success_threshold(&self, _epoch: TYPES::Epoch) -> NonZeroU64 { 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: ::Epoch) -> NonZeroU64 { + fn failure_threshold(&self, _epoch: TYPES::Epoch) -> NonZeroU64 { NonZeroU64::new(((self.stake_table.len() as u64) / 3) + 1).unwrap() } /// Get the voting upgrade threshold for the committee - fn upgrade_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 { + fn upgrade_threshold(&self, _epoch: TYPES::Epoch) -> NonZeroU64 { NonZeroU64::new(((self.stake_table.len() as u64 * 9) / 10) + 1).unwrap() } } diff --git a/crates/task-impls/src/consensus/handlers.rs b/crates/task-impls/src/consensus/handlers.rs index fbd1960ce4..3a9f44c34f 100644 --- a/crates/task-impls/src/consensus/handlers.rs +++ b/crates/task-impls/src/consensus/handlers.rs @@ -10,12 +10,13 @@ use async_broadcast::Sender; use chrono::Utc; use hotshot_types::{ event::{Event, EventType}, - simple_vote::{QuorumVote2, TimeoutData2, TimeoutVote2}, + simple_vote::{HasEpoch, QuorumVote2, TimeoutData2, TimeoutVote2}, traits::{ election::Membership, node_implementation::{ConsensusTime, NodeImplementation, NodeType}, }, - vote::HasViewNumber, + utils::EpochTransitionIndicator, + vote::{HasViewNumber, Vote}, }; use tokio::{spawn, time::sleep}; use tracing::instrument; @@ -46,7 +47,7 @@ pub(crate) async fn handle_quorum_vote_recv< .is_high_qc_for_last_block(); let we_are_leader = task_state .membership - .leader(vote.view_number() + 1, task_state.cur_epoch)? + .leader(vote.view_number() + 1, vote.data.epoch)? == task_state.public_key; ensure!( in_transition || we_are_leader, @@ -56,20 +57,45 @@ pub(crate) async fn handle_quorum_vote_recv< ) ); + let transition_indicator = if in_transition { + EpochTransitionIndicator::InTransition + } else { + EpochTransitionIndicator::NotInTransition + }; handle_vote( &mut task_state.vote_collectors, vote, task_state.public_key.clone(), &task_state.membership, - task_state.cur_epoch, + vote.data.epoch, task_state.id, &event, sender, &task_state.upgrade_lock, - !in_transition, + transition_indicator.clone(), ) .await?; + // If the vote sender belongs to the next epoch, collect it separately to form the second QC + if task_state + .membership + .has_stake(&vote.signing_key(), vote.epoch() + 1) + { + handle_vote( + &mut task_state.next_epoch_vote_collectors, + &vote.clone().into(), + task_state.public_key.clone(), + &task_state.membership, + vote.data.epoch, + task_state.id, + &event, + sender, + &task_state.upgrade_lock, + transition_indicator, + ) + .await?; + } + Ok(()) } @@ -101,12 +127,12 @@ pub(crate) async fn handle_timeout_vote_recv< vote, task_state.public_key.clone(), &task_state.membership, - task_state.cur_epoch, + vote.data.epoch, task_state.id, &event, sender, &task_state.upgrade_lock, - true, + EpochTransitionIndicator::NotInTransition, ) .await?; diff --git a/crates/task-impls/src/consensus/mod.rs b/crates/task-impls/src/consensus/mod.rs index 7c50cd8df6..e6897a2fe5 100644 --- a/crates/task-impls/src/consensus/mod.rs +++ b/crates/task-impls/src/consensus/mod.rs @@ -14,8 +14,8 @@ use hotshot_types::{ consensus::OuterConsensus, event::Event, message::UpgradeLock, - simple_certificate::{QuorumCertificate2, TimeoutCertificate2}, - simple_vote::{QuorumVote2, TimeoutVote2}, + simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2, TimeoutCertificate2}, + simple_vote::{NextEpochQuorumVote2, QuorumVote2, TimeoutVote2}, traits::{ node_implementation::{ConsensusTime, NodeImplementation, NodeType, Versions}, signature_key::SignatureKey, @@ -55,6 +55,14 @@ pub struct ConsensusTaskState, V: /// A map of `QuorumVote` collector tasks. pub vote_collectors: VoteCollectorsMap, QuorumCertificate2, V>, + /// A map of `QuorumVote` collector tasks. They collect votes from the nodes in the next epoch. + pub next_epoch_vote_collectors: VoteCollectorsMap< + TYPES, + NextEpochQuorumVote2, + NextEpochQuorumCertificate2, + V, + >, + /// A map of `TimeoutVote` collector tasks. pub timeout_vote_collectors: VoteCollectorsMap, TimeoutCertificate2, V>, diff --git a/crates/task-impls/src/da.rs b/crates/task-impls/src/da.rs index fb2775d378..b292814e24 100644 --- a/crates/task-impls/src/da.rs +++ b/crates/task-impls/src/da.rs @@ -25,6 +25,7 @@ use hotshot_types::{ signature_key::SignatureKey, storage::Storage, }, + utils::EpochTransitionIndicator, vote::HasViewNumber, }; use sha2::{Digest, Sha256}; @@ -106,21 +107,15 @@ impl, V: Versions> DaTaskState, V: Versions> DaTaskState, V: Versions> DaTaskState, V: Versions> DaTaskState, V: Versions> DaTaskState Is it always the case that this is cur_view + 1? view_number, - epoch: *epoch_number, + epoch, }; let message = Proposal { @@ -348,6 +343,15 @@ impl, V: Versions> DaTaskState {} } diff --git a/crates/task-impls/src/events.rs b/crates/task-impls/src/events.rs index 06d3ff5db6..195b2137cd 100644 --- a/crates/task-impls/src/events.rs +++ b/crates/task-impls/src/events.rs @@ -17,8 +17,8 @@ use hotshot_types::{ message::Proposal, request_response::ProposalRequestPayload, simple_certificate::{ - DaCertificate2, QuorumCertificate, QuorumCertificate2, TimeoutCertificate, - TimeoutCertificate2, UpgradeCertificate, ViewSyncCommitCertificate2, + DaCertificate2, NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, + TimeoutCertificate, TimeoutCertificate2, UpgradeCertificate, ViewSyncCommitCertificate2, ViewSyncFinalizeCertificate2, ViewSyncPreCommitCertificate2, }, simple_vote::{ @@ -124,6 +124,8 @@ pub enum HotShotEvent { QcFormed(Either, TimeoutCertificate>), /// The next leader has collected enough votes to form a QC; emitted by the next leader in the consensus task; an internal event only Qc2Formed(Either, TimeoutCertificate2>), + /// The next leader has collected enough votes from the next epoch nodes to form a QC; emitted by the next leader in the consensus task; an internal event only + NextEpochQc2Formed(Either, TimeoutCertificate>), /// The DA leader has collected enough votes to form a DAC; emitted by the DA leader in the DA task; sent to the entire network via the networking task DacSend(DaCertificate2, TYPES::SignatureKey), /// The current view has changed; emitted by the replica in the consensus task or replica in the view sync task; received by almost all other tasks @@ -283,6 +285,10 @@ impl HotShotEvent { either::Left(qc) => Some(qc.view_number()), either::Right(tc) => Some(tc.view_number()), }, + HotShotEvent::NextEpochQc2Formed(cert) => match cert { + either::Left(qc) => Some(qc.view_number()), + either::Right(tc) => Some(tc.view_number()), + }, HotShotEvent::ViewSyncCommitVoteSend(vote) | HotShotEvent::ViewSyncCommitVoteRecv(vote) => Some(vote.view_number()), HotShotEvent::ViewSyncPreCommitVoteRecv(vote) @@ -406,8 +412,16 @@ impl Display for HotShotEvent { either::Right(tc) => write!(f, "QcFormed(view_number={:?})", tc.view_number()), }, HotShotEvent::Qc2Formed(cert) => match cert { - either::Left(qc) => write!(f, "QcFormed(view_number={:?})", qc.view_number()), - either::Right(tc) => write!(f, "QcFormed(view_number={:?})", tc.view_number()), + either::Left(qc) => write!(f, "QcFormed2(view_number={:?})", qc.view_number()), + either::Right(tc) => write!(f, "QcFormed2(view_number={:?})", tc.view_number()), + }, + HotShotEvent::NextEpochQc2Formed(cert) => match cert { + either::Left(qc) => { + write!(f, "NextEpochQc2Formed(view_number={:?})", qc.view_number()) + } + either::Right(tc) => { + write!(f, "NextEpochQc2Formed(view_number={:?})", tc.view_number()) + } }, HotShotEvent::DacSend(cert, _) => { write!(f, "DacSend(view_number={:?})", cert.view_number()) diff --git a/crates/task-impls/src/helpers.rs b/crates/task-impls/src/helpers.rs index 6765c76022..898ee8e1ee 100644 --- a/crates/task-impls/src/helpers.rs +++ b/crates/task-impls/src/helpers.rs @@ -28,7 +28,7 @@ use hotshot_types::{ signature_key::SignatureKey, BlockPayload, ValidatedState, }, - utils::{epoch_from_block_number, Terminator, View, ViewInner}, + utils::{epoch_from_block_number, is_last_block_in_epoch, Terminator, View, ViewInner}, vote::{Certificate, HasViewNumber}, }; use tokio::time::timeout; @@ -573,6 +573,12 @@ pub async fn validate_proposal_safety_and_liveness< } ); + // Make sure that the epoch transition proposal includes the next epoch QC + if is_last_block_in_epoch(parent_leaf.height(), validation_info.epoch_height) { + ensure!(proposal.data.next_epoch_justify_qc.is_some(), + "Epoch transition proposal does not include the next epoch justify QC. Do not vote!"); + } + // Liveness check. let liveness_check = justify_qc.view_number() > consensus_reader.locked_view(); diff --git a/crates/task-impls/src/network.rs b/crates/task-impls/src/network.rs index 0b9b536a15..a72344dcc6 100644 --- a/crates/task-impls/src/network.rs +++ b/crates/task-impls/src/network.rs @@ -10,18 +10,24 @@ use std::{ sync::Arc, }; +use crate::{ + events::{HotShotEvent, HotShotTaskCompleted}, + helpers::broadcast_event, +}; use async_broadcast::{Receiver, Sender}; use async_lock::RwLock; use async_trait::async_trait; use hotshot_task::task::TaskState; +use hotshot_types::data::{VidDisperseShare, VidDisperseShare2}; use hotshot_types::{ consensus::OuterConsensus, - data::{VidDisperse, VidDisperseShare}, + data::VidDisperse, event::{Event, EventType, HotShotAction}, message::{ convert_proposal, DaConsensusMessage, DataMessage, GeneralConsensusMessage, Message, MessageKind, Proposal, SequencingMessage, UpgradeLock, }, + simple_vote::HasEpoch, traits::{ election::Membership, network::{ @@ -38,11 +44,6 @@ use tracing::instrument; use utils::anytrace::*; use vbs::version::StaticVersionType; -use crate::{ - events::{HotShotEvent, HotShotTaskCompleted}, - helpers::broadcast_event, -}; - /// the network message task state #[derive(Clone)] pub struct NetworkMessageTaskState { @@ -321,16 +322,35 @@ impl< sender: &::SignatureKey, ) -> Option { let view = vid_proposal.data.view_number; - let vid_share_proposals = VidDisperseShare::to_vid_share_proposals(vid_proposal); + let vid_share_proposals = VidDisperseShare2::to_vid_share_proposals(vid_proposal); let mut messages = HashMap::new(); for proposal in vid_share_proposals { let recipient = proposal.data.recipient_key.clone(); - let message = Message { - sender: sender.clone(), - kind: MessageKind::::from_consensus_message(SequencingMessage::Da( - DaConsensusMessage::VidDisperseMsg(proposal), - )), + let message = if self + .upgrade_lock + .version_infallible(proposal.data.view_number()) + .await + >= V::Epochs::VERSION + { + Message { + sender: sender.clone(), + kind: MessageKind::::from_consensus_message(SequencingMessage::Da( + DaConsensusMessage::VidDisperseMsg2(proposal), + )), + } + } else { + let vid_share_proposal = Proposal { + data: VidDisperseShare::from(proposal.data), + signature: proposal.signature, + _pd: proposal._pd, + }; + Message { + sender: sender.clone(), + kind: MessageKind::::from_consensus_message(SequencingMessage::Da( + DaConsensusMessage::VidDisperseMsg(vid_share_proposal), + )), + } }; let serialized_message = match self.upgrade_lock.serialize(&message).await { Ok(serialized) => serialized, @@ -449,7 +469,7 @@ impl< HotShotEvent::QuorumVoteSend(vote) => { *maybe_action = Some(HotShotAction::Vote); let view_number = vote.view_number() + 1; - let leader = match self.membership.leader(view_number, self.epoch) { + let leader = match self.membership.leader(view_number, vote.epoch()) { Ok(l) => l, Err(e) => { tracing::warn!( diff --git a/crates/task-impls/src/quorum_proposal/handlers.rs b/crates/task-impls/src/quorum_proposal/handlers.rs index 7c29ffe426..7dbedb9107 100644 --- a/crates/task-impls/src/quorum_proposal/handlers.rs +++ b/crates/task-impls/src/quorum_proposal/handlers.rs @@ -17,13 +17,17 @@ use anyhow::{ensure, Context, Result}; use async_broadcast::{Receiver, Sender}; use async_lock::RwLock; use committable::Committable; -use hotshot_task::dependency_task::HandleDepOutput; +use either::Either; +use hotshot_task::{ + dependency::{Dependency, EventDependency}, + dependency_task::HandleDepOutput, +}; use hotshot_types::{ consensus::{CommitmentAndMetadata, OuterConsensus}, data::{Leaf2, QuorumProposal2, VidDisperse, ViewChangeEvidence}, drb::{INITIAL_DRB_RESULT, INITIAL_DRB_SEED_INPUT}, message::Proposal, - simple_certificate::{QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2, UpgradeCertificate}, traits::{ block_contents::BlockHeader, election::Membership, @@ -130,8 +134,6 @@ impl ProposalDependencyHandle { if let HotShotEvent::HighQcRecv(qc, _sender) = event.as_ref() { if qc .is_valid_cert( - // TODO take epoch from `qc` - // https://github.com/EspressoSystems/HotShot/issues/3917 self.quorum_membership.stake_table(qc.data.epoch), self.quorum_membership.success_threshold(qc.data.epoch), &self.upgrade_lock, @@ -144,7 +146,7 @@ impl ProposalDependencyHandle { } None } - /// Waits for the ocnfigured timeout for nodes to send HighQc messages to us. We'll + /// Waits for the configured timeout for nodes to send HighQc messages to us. We'll /// then propose with the highest QC from among these proposals. async fn wait_for_highest_qc(&mut self) { tracing::error!("waiting for QC"); @@ -187,6 +189,68 @@ impl ProposalDependencyHandle { } } } + /// Gets the next epoch QC corresponding to this epoch QC, times out if it takes too long. + /// We need the QC for the epoch transition proposals. + async fn get_next_epoch_qc( + &self, + high_qc: &QuorumCertificate2, + ) -> Option> { + tracing::debug!("getting the next epoch QC"); + // If we haven't upgraded to Epochs just return None right away + if self.upgrade_lock.version_infallible(self.view_number).await < V::Epochs::VERSION { + return None; + } + if let Some(next_epoch_qc) = self.consensus.read().await.next_epoch_high_qc() { + if next_epoch_qc.data.leaf_commit == high_qc.data.leaf_commit { + // We have it already, no reason to wait + return Some(next_epoch_qc.clone()); + } + }; + + let wait_duration = Duration::from_millis(self.timeout / 2); + + // TODO configure timeout + let Some(time_spent) = Instant::now().checked_duration_since(self.view_start_time) else { + // Shouldn't be possible, now must be after the start + return None; + }; + let Some(time_left) = wait_duration.checked_sub(time_spent) else { + // No time left + return None; + }; + let receiver = self.receiver.clone(); + let Ok(Some(event)) = tokio::time::timeout(time_left, async move { + let this_epoch_high_qc = high_qc.clone(); + EventDependency::new( + receiver, + Box::new(move |event| { + let event = event.as_ref(); + if let HotShotEvent::NextEpochQc2Formed(Either::Left(qc)) = event { + qc.data.leaf_commit == this_epoch_high_qc.data.leaf_commit + } else { + false + } + }), + ) + .completed() + .await + }) + .await + else { + // Check again, there is a chance we missed it + if let Some(next_epoch_qc) = self.consensus.read().await.next_epoch_high_qc() { + if next_epoch_qc.data.leaf_commit == high_qc.data.leaf_commit { + return Some(next_epoch_qc.clone()); + } + }; + return None; + }; + let HotShotEvent::NextEpochQc2Formed(Either::Left(qc)) = event.as_ref() else { + // this shouldn't happen + return None; + }; + Some(qc.clone()) + } /// Publishes a proposal given the [`CommitmentAndMetadata`], [`VidDisperse`] /// and high qc [`hotshot_types::simple_certificate::QuorumCertificate`], /// with optional [`ViewChangeEvidence`]. @@ -312,10 +376,21 @@ impl ProposalDependencyHandle { ); return Ok(()); } + let next_epoch_qc = if self + .consensus + .read() + .await + .is_leaf_for_last_block(parent_qc.data.leaf_commit) + { + self.get_next_epoch_qc(&parent_qc).await + } else { + None + }; let proposal = QuorumProposal2 { block_header, view_number: self.view_number, justify_qc: parent_qc, + next_epoch_justify_qc: next_epoch_qc, upgrade_certificate, view_change_evidence: proposal_certificate, drb_seed: INITIAL_DRB_SEED_INPUT, diff --git a/crates/task-impls/src/quorum_proposal/mod.rs b/crates/task-impls/src/quorum_proposal/mod.rs index 9eda9b13b4..77bfef856c 100644 --- a/crates/task-impls/src/quorum_proposal/mod.rs +++ b/crates/task-impls/src/quorum_proposal/mod.rs @@ -462,12 +462,7 @@ impl, V: Versions> )?; } HotShotEvent::ViewSyncFinalizeCertificateRecv(certificate) => { - // MERGE TODO - // - // HotShotEvent::ViewSyncFinalizeCertificate2Recv(certificate) => { - // let cert_epoch_number = certificate.data.epoch; - // - let epoch_number = self.consensus.read().await.cur_epoch(); + let epoch_number = certificate.data.epoch; ensure!( certificate @@ -554,6 +549,32 @@ impl, V: Versions> ); self.highest_qc = qc.clone(); } + HotShotEvent::NextEpochQc2Formed(Either::Left(next_epoch_qc)) => { + // Only update if the qc is from a newer view + let current_next_epoch_qc = + self.consensus.read().await.next_epoch_high_qc().cloned(); + ensure!(current_next_epoch_qc.is_none() || + next_epoch_qc.view_number > current_next_epoch_qc.unwrap().view_number, + debug!("Received a next epoch QC for a view that was not > than our current next epoch high QC") + ); + self.consensus + .write() + .await + .update_next_epoch_high_qc(next_epoch_qc.clone()) + .wrap() + .context(error!( + "Failed to update next epoch high QC in internal consensus state!" + ))?; + + // Then update the next epoch high QC in storage + self.storage + .write() + .await + .update_next_epoch_high_qc2(next_epoch_qc.clone()) + .await + .wrap() + .context(error!("Failed to update next epoch high QC in storage!"))?; + } _ => {} } Ok(()) diff --git a/crates/task-impls/src/quorum_proposal_recv/handlers.rs b/crates/task-impls/src/quorum_proposal_recv/handlers.rs index 2f5df35a65..d6a768c2cd 100644 --- a/crates/task-impls/src/quorum_proposal_recv/handlers.rs +++ b/crates/task-impls/src/quorum_proposal_recv/handlers.rs @@ -152,7 +152,10 @@ pub(crate) async fn handle_quorum_proposal_recv< .context(warn!("Failed to validate proposal view or attached certs"))?; let view_number = proposal.data.view_number(); + let justify_qc = proposal.data.justify_qc.clone(); + let maybe_next_epoch_justify_qc = proposal.data.next_epoch_justify_qc.clone(); + let proposal_block_number = proposal.data.block_header.block_number(); let proposal_epoch = TYPES::Epoch::new(epoch_from_block_number( proposal_block_number, @@ -176,6 +179,34 @@ pub(crate) async fn handle_quorum_proposal_recv< bail!("Invalid justify_qc in proposal for view {}", *view_number); } + if let Some(ref next_epoch_justify_qc) = maybe_next_epoch_justify_qc { + // If the next epoch justify qc exists, make sure it's equal to the justify qc + if justify_qc.view_number() != next_epoch_justify_qc.view_number() + || justify_qc.data.epoch != next_epoch_justify_qc.data.epoch + || justify_qc.data.leaf_commit != next_epoch_justify_qc.data.leaf_commit + { + bail!("Next epoch justify qc exists but it's not equal with justify qc."); + } + // Validate the next epoch justify qc as well + if !next_epoch_justify_qc + .is_valid_cert( + validation_info + .quorum_membership + .stake_table(justify_qc.data.epoch + 1), + validation_info + .quorum_membership + .success_threshold(justify_qc.data.epoch + 1), + &validation_info.upgrade_lock, + ) + .await + { + bail!( + "Invalid next_epoch_justify_qc in proposal for view {}", + *view_number + ); + } + } + broadcast_event( Arc::new(HotShotEvent::QuorumProposalPreliminarilyValidated( proposal.clone(), @@ -232,6 +263,20 @@ pub(crate) async fn handle_quorum_proposal_recv< { bail!("Failed to store High QC, not voting; error = {:?}", e); } + if let Some(ref next_epoch_justify_qc) = maybe_next_epoch_justify_qc { + if let Err(e) = validation_info + .storage + .write() + .await + .update_next_epoch_high_qc2(next_epoch_justify_qc.clone()) + .await + { + bail!( + "Failed to store next epoch High QC, not voting; error = {:?}", + e + ); + } + } } drop(consensus_reader); @@ -239,6 +284,11 @@ pub(crate) async fn handle_quorum_proposal_recv< if let Err(e) = consensus_writer.update_high_qc(justify_qc.clone()) { tracing::trace!("{e:?}"); } + if let Some(ref next_epoch_justify_qc) = maybe_next_epoch_justify_qc { + if let Err(e) = consensus_writer.update_next_epoch_high_qc(next_epoch_justify_qc.clone()) { + tracing::trace!("{e:?}"); + } + } drop(consensus_writer); let Some((parent_leaf, _parent_state)) = parent else { diff --git a/crates/task-impls/src/quorum_vote/handlers.rs b/crates/task-impls/src/quorum_vote/handlers.rs index 98f8c5561d..577c5325e6 100644 --- a/crates/task-impls/src/quorum_vote/handlers.rs +++ b/crates/task-impls/src/quorum_vote/handlers.rs @@ -24,7 +24,7 @@ use hotshot_types::{ storage::Storage, ValidatedState, }, - utils::epoch_from_block_number, + utils::{epoch_from_block_number, is_last_block_in_epoch}, vote::HasViewNumber, }; use tracing::instrument; @@ -53,7 +53,7 @@ async fn handle_quorum_proposal_validated_drb_calculation_start< ) { let current_epoch_number = TYPES::Epoch::new(epoch_from_block_number( proposal.block_header.block_number(), - task_state.epoch_height, + TYPES::EPOCH_HEIGHT, )); // Start the new task if we're in the committee for this epoch @@ -401,7 +401,6 @@ pub(crate) async fn submit_vote, V private_key: ::PrivateKey, upgrade_lock: UpgradeLock, view_number: TYPES::View, - epoch_height: u64, storage: Arc>, leaf: Leaf2, vid_share: Proposal>, @@ -409,11 +408,17 @@ pub(crate) async fn submit_vote, V ) -> Result<()> { let epoch_number = TYPES::Epoch::new(epoch_from_block_number( leaf.block_header().block_number(), - epoch_height, + TYPES::EPOCH_HEIGHT, )); + let committee_member_in_current_epoch = quorum_membership.has_stake(&public_key, epoch_number); + // If the proposed leaf is for the last block in the epoch and the node is part of the quorum committee + // in the next epoch, the node should vote to achieve the double quorum. + let committee_member_in_next_epoch = is_last_block_in_epoch(leaf.height(), TYPES::EPOCH_HEIGHT) + && quorum_membership.has_stake(&public_key, epoch_number + 1); + ensure!( - quorum_membership.has_stake(&public_key, epoch_number), + committee_member_in_current_epoch || committee_member_in_next_epoch, info!( "We were not chosen for quorum committee on {:?}", view_number diff --git a/crates/task-impls/src/quorum_vote/mod.rs b/crates/task-impls/src/quorum_vote/mod.rs index 04b8a677dc..a009e6aee6 100644 --- a/crates/task-impls/src/quorum_vote/mod.rs +++ b/crates/task-impls/src/quorum_vote/mod.rs @@ -238,7 +238,6 @@ impl + 'static, V: Versions> Handl self.private_key.clone(), self.upgrade_lock.clone(), self.view_number, - self.epoch_height, Arc::clone(&self.storage), leaf, vid_share, @@ -504,6 +503,7 @@ impl, V: Versions> QuorumVoteTaskS ); let cert_epoch = cert.data.epoch; + // Validate the DAC. ensure!( cert.is_valid_cert( @@ -721,7 +721,6 @@ impl, V: Versions> QuorumVoteTaskS self.private_key.clone(), self.upgrade_lock.clone(), proposal.data.view_number(), - self.epoch_height, Arc::clone(&self.storage), proposed_leaf, updated_vid, diff --git a/crates/task-impls/src/request.rs b/crates/task-impls/src/request.rs index 56e16c1a37..3951102c24 100644 --- a/crates/task-impls/src/request.rs +++ b/crates/task-impls/src/request.rs @@ -332,7 +332,7 @@ impl> NetworkRequestState, sender: &Sender>>, diff --git a/crates/task-impls/src/upgrade.rs b/crates/task-impls/src/upgrade.rs index a8dec2e3f3..16f8c7e555 100644 --- a/crates/task-impls/src/upgrade.rs +++ b/crates/task-impls/src/upgrade.rs @@ -25,6 +25,7 @@ use hotshot_types::{ node_implementation::{ConsensusTime, NodeType, Versions}, signature_key::SignatureKey, }, + utils::EpochTransitionIndicator, vote::HasViewNumber, }; use tracing::instrument; @@ -243,7 +244,7 @@ impl UpgradeTaskState { &event, &tx, &self.upgrade_lock, - true, + EpochTransitionIndicator::NotInTransition, ) .await?; } @@ -287,6 +288,7 @@ impl UpgradeTaskState { let upgrade_proposal = UpgradeProposal { upgrade_proposal: upgrade_proposal_data.clone(), view_number: TYPES::View::new(view + UPGRADE_PROPOSE_OFFSET), + epoch: self.cur_epoch, }; let signature = TYPES::SignatureKey::sign( diff --git a/crates/task-impls/src/vid.rs b/crates/task-impls/src/vid.rs index 07dd5d1b72..b27038fabf 100644 --- a/crates/task-impls/src/vid.rs +++ b/crates/task-impls/src/vid.rs @@ -14,11 +14,13 @@ use hotshot_types::{ data::{PackedBundle, VidDisperse, VidDisperseShare2}, message::Proposal, traits::{ + block_contents::BlockHeader, election::Membership, - node_implementation::{NodeImplementation, NodeType}, + node_implementation::{ConsensusTime, NodeImplementation, NodeType}, signature_key::SignatureKey, BlockPayload, }, + utils::epoch_from_block_number, }; use tracing::{debug, error, info, instrument}; use utils::anytrace::Result; @@ -53,6 +55,9 @@ pub struct VidTaskState> { /// This state's ID pub id: u64, + + /// Number of blocks in an epoch, zero means there are no epochs + pub epoch_height: u64, } impl> VidTaskState { @@ -89,6 +94,7 @@ impl> VidTaskState { &Arc::clone(&self.membership), *view_number, epoch, + None, vid_precompute.clone(), ) .await; @@ -124,7 +130,10 @@ impl> VidTaskState { error!("VID: failed to sign dispersal payload"); return None; }; - debug!("publishing VID disperse for view {}", *view_number); + debug!( + "publishing VID disperse for view {} and epoch {}", + *view_number, *epoch + ); broadcast_event( Arc::new(HotShotEvent::VidDisperseSend( Proposal { @@ -157,6 +166,68 @@ impl> VidTaskState { return None; } + HotShotEvent::QuorumProposalSend(proposal, _) => { + let proposed_block_number = proposal.data.block_header.block_number(); + if self.epoch_height == 0 || proposed_block_number % self.epoch_height != 0 { + // This is not the last block in the epoch, do nothing. + return None; + } + // We just sent a proposal for the last block in the epoch. We need to calculate + // and send VID for the nodes in the next epoch so that they can vote. + let proposal_view_number = proposal.data.view_number; + let sender_epoch = TYPES::Epoch::new(epoch_from_block_number( + proposed_block_number, + self.epoch_height, + )); + let target_epoch = TYPES::Epoch::new( + epoch_from_block_number(proposed_block_number, self.epoch_height) + 1, + ); + + let consensus_reader = self.consensus.read().await; + let Some(txns) = consensus_reader.saved_payloads().get(&proposal_view_number) + else { + tracing::warn!( + "We need to calculate VID for the nodes in the next epoch \ + but we don't have the transactions" + ); + return None; + }; + let txns = Arc::clone(txns); + drop(consensus_reader); + + let next_epoch_vid_disperse = VidDisperse::calculate_vid_disperse( + txns, + &Arc::clone(&self.membership), + proposal_view_number, + target_epoch, + Some(sender_epoch), + None, + ) + .await; + let Ok(next_epoch_signature) = TYPES::SignatureKey::sign( + &self.private_key, + next_epoch_vid_disperse.payload_commitment.as_ref(), + ) else { + error!("VID: failed to sign dispersal payload for the next epoch"); + return None; + }; + debug!( + "publishing VID disperse for view {} and epoch {}", + *proposal_view_number, *target_epoch + ); + broadcast_event( + Arc::new(HotShotEvent::VidDisperseSend( + Proposal { + signature: next_epoch_signature, + data: next_epoch_vid_disperse.clone(), + _pd: PhantomData, + }, + self.public_key.clone(), + )), + &event_stream, + ) + .await; + } HotShotEvent::Shutdown => { return Some(HotShotTaskCompleted); } diff --git a/crates/task-impls/src/view_sync.rs b/crates/task-impls/src/view_sync.rs index 98b9fd1876..65e1ad7d6b 100644 --- a/crates/task-impls/src/view_sync.rs +++ b/crates/task-impls/src/view_sync.rs @@ -29,6 +29,7 @@ use hotshot_types::{ node_implementation::{ConsensusTime, NodeType, Versions}, signature_key::SignatureKey, }, + utils::EpochTransitionIndicator, vote::{Certificate, HasViewNumber, Vote}, }; use tokio::{spawn, task::JoinHandle, time::sleep}; @@ -318,15 +319,15 @@ impl ViewSyncTaskState { public_key: self.public_key.clone(), membership: Arc::clone(&self.membership), view: vote_view, - epoch: self.cur_epoch, id: self.id, + epoch: vote.data.epoch, }; let vote_collector = create_vote_accumulator( &info, event, &event_stream, self.upgrade_lock.clone(), - true, + EpochTransitionIndicator::NotInTransition, ) .await?; @@ -363,8 +364,8 @@ impl ViewSyncTaskState { public_key: self.public_key.clone(), membership: Arc::clone(&self.membership), view: vote_view, - epoch: self.cur_epoch, id: self.id, + epoch: vote.data.epoch, }; let vote_collector = create_vote_accumulator( @@ -372,7 +373,7 @@ impl ViewSyncTaskState { event, &event_stream, self.upgrade_lock.clone(), - true, + EpochTransitionIndicator::NotInTransition, ) .await?; relay_map.insert(relay, vote_collector); @@ -408,15 +409,15 @@ impl ViewSyncTaskState { public_key: self.public_key.clone(), membership: Arc::clone(&self.membership), view: vote_view, - epoch: self.cur_epoch, id: self.id, + epoch: vote.data.epoch, }; let vote_collector = create_vote_accumulator( &info, event, &event_stream, self.upgrade_lock.clone(), - true, + EpochTransitionIndicator::NotInTransition, ) .await; if let Ok(vote_task) = vote_collector { diff --git a/crates/task-impls/src/vote_collection.rs b/crates/task-impls/src/vote_collection.rs index 182a40a8c3..cc2ec6c7c9 100644 --- a/crates/task-impls/src/vote_collection.rs +++ b/crates/task-impls/src/vote_collection.rs @@ -17,18 +17,19 @@ use either::Either::{self, Left, Right}; use hotshot_types::{ message::UpgradeLock, simple_certificate::{ - DaCertificate2, QuorumCertificate, QuorumCertificate2, TimeoutCertificate2, - UpgradeCertificate, ViewSyncCommitCertificate2, ViewSyncFinalizeCertificate2, - ViewSyncPreCommitCertificate2, + DaCertificate2, NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, + TimeoutCertificate2, UpgradeCertificate, ViewSyncCommitCertificate2, + ViewSyncFinalizeCertificate2, ViewSyncPreCommitCertificate2, }, simple_vote::{ - DaVote2, QuorumVote, QuorumVote2, TimeoutVote2, UpgradeVote, ViewSyncCommitVote2, - ViewSyncFinalizeVote2, ViewSyncPreCommitVote2, + DaVote2, NextEpochQuorumVote2, QuorumVote, QuorumVote2, TimeoutVote2, UpgradeVote, + ViewSyncCommitVote2, ViewSyncFinalizeVote2, ViewSyncPreCommitVote2, }, traits::{ election::Membership, node_implementation::{NodeType, Versions}, }, + utils::EpochTransitionIndicator, vote::{Certificate, HasViewNumber, Vote, VoteAccumulator}, }; use utils::anytrace::*; @@ -65,7 +66,7 @@ pub struct VoteCollectionTaskState< pub id: u64, /// Whether we should check if we are the leader when handling a vote - pub check_if_leader: bool, + pub transition_indicator: EpochTransitionIndicator, } /// Describes the functions a vote must implement for it to be aggregatable by the generic vote collection task @@ -105,14 +106,17 @@ impl< pub async fn accumulate_vote( &mut self, vote: &VOTE, + sender_epoch: TYPES::Epoch, event_stream: &Sender>>, ) -> Result> { - if self.check_if_leader { - ensure!( - vote.leader(&self.membership, self.epoch)? == self.public_key, - info!("Received vote for a view in which we were not the leader.") - ); - } + ensure!( + matches!( + self.transition_indicator, + EpochTransitionIndicator::InTransition + ) || vote.leader(&self.membership, self.epoch)? == self.public_key, + info!("Received vote for a view in which we were not the leader.") + ); + ensure!( vote.view_number() == self.view, error!( @@ -127,7 +131,7 @@ impl< ))?; match accumulator - .accumulate(vote, &self.membership, self.epoch) + .accumulate(vote, &self.membership, sender_epoch) .await { Either::Left(()) => Ok(None), @@ -195,7 +199,7 @@ pub async fn create_vote_accumulator( event: Arc>, sender: &Sender>>, upgrade_lock: UpgradeLock, - check_if_leader: bool, + transition_indicator: EpochTransitionIndicator, ) -> Result> where TYPES: NodeType, @@ -226,7 +230,7 @@ where view: info.view, epoch: info.epoch, id: info.id, - check_if_leader, + transition_indicator, }; state.handle_vote_event(Arc::clone(&event), sender).await?; @@ -258,7 +262,7 @@ pub async fn handle_vote< event: &Arc>, event_stream: &Sender>>, upgrade_lock: &UpgradeLock, - check_if_leader: bool, + transition_indicator: EpochTransitionIndicator, ) -> Result<()> where VoteCollectionTaskState: HandleVoteEvent, @@ -278,7 +282,7 @@ where Arc::clone(event), event_stream, upgrade_lock.clone(), - check_if_leader, + transition_indicator, ) .await?; @@ -306,6 +310,13 @@ where /// Alias for Quorum vote accumulator type QuorumVoteState = VoteCollectionTaskState, QuorumCertificate2, V>; +/// Alias for Quorum vote accumulator +type NextEpochQuorumVoteState = VoteCollectionTaskState< + TYPES, + NextEpochQuorumVote2, + NextEpochQuorumCertificate2, + V, +>; /// Alias for DA vote accumulator type DaVoteState = VoteCollectionTaskState, DaCertificate2, V>; @@ -373,6 +384,25 @@ impl AggregatableVote, QuorumCertific } } +impl + AggregatableVote, NextEpochQuorumCertificate2> + for NextEpochQuorumVote2 +{ + fn leader( + &self, + membership: &TYPES::Membership, + epoch: TYPES::Epoch, + ) -> Result { + membership.leader(self.view_number() + 1, epoch) + } + fn make_cert_event( + certificate: NextEpochQuorumCertificate2, + _key: &TYPES::SignatureKey, + ) -> HotShotEvent { + HotShotEvent::NextEpochQc2Formed(Left(certificate)) + } +} + impl AggregatableVote, UpgradeCertificate> for UpgradeVote { @@ -496,7 +526,33 @@ impl sender: &Sender>>, ) -> Result>> { match event.as_ref() { - HotShotEvent::QuorumVoteRecv(vote) => self.accumulate_vote(vote, sender).await, + HotShotEvent::QuorumVoteRecv(vote) => { + self.accumulate_vote(vote, self.epoch, sender).await + } + _ => Ok(None), + } + } + fn filter(event: Arc>) -> bool { + matches!(event.as_ref(), HotShotEvent::QuorumVoteRecv(_)) + } +} + +// Handlers for all vote accumulators +#[async_trait] +impl + HandleVoteEvent, NextEpochQuorumCertificate2> + for NextEpochQuorumVoteState +{ + async fn handle_vote_event( + &mut self, + event: Arc>, + sender: &Sender>>, + ) -> Result>> { + match event.as_ref() { + HotShotEvent::QuorumVoteRecv(vote) => { + self.accumulate_vote(&vote.clone().into(), self.epoch + 1, sender) + .await + } _ => Ok(None), } } @@ -517,7 +573,9 @@ impl sender: &Sender>>, ) -> Result>> { match event.as_ref() { - HotShotEvent::UpgradeVoteRecv(vote) => self.accumulate_vote(vote, sender).await, + HotShotEvent::UpgradeVoteRecv(vote) => { + self.accumulate_vote(vote, self.epoch, sender).await + } _ => Ok(None), } } @@ -536,7 +594,7 @@ impl HandleVoteEvent, DaCert sender: &Sender>>, ) -> Result>> { match event.as_ref() { - HotShotEvent::DaVoteRecv(vote) => self.accumulate_vote(vote, sender).await, + HotShotEvent::DaVoteRecv(vote) => self.accumulate_vote(vote, self.epoch, sender).await, _ => Ok(None), } } @@ -556,7 +614,9 @@ impl sender: &Sender>>, ) -> Result>> { match event.as_ref() { - HotShotEvent::TimeoutVoteRecv(vote) => self.accumulate_vote(vote, sender).await, + HotShotEvent::TimeoutVoteRecv(vote) => { + self.accumulate_vote(vote, self.epoch, sender).await + } _ => Ok(None), } } @@ -577,7 +637,7 @@ impl ) -> Result>> { match event.as_ref() { HotShotEvent::ViewSyncPreCommitVoteRecv(vote) => { - self.accumulate_vote(vote, sender).await + self.accumulate_vote(vote, self.epoch, sender).await } _ => Ok(None), } @@ -598,7 +658,9 @@ impl sender: &Sender>>, ) -> Result>> { match event.as_ref() { - HotShotEvent::ViewSyncCommitVoteRecv(vote) => self.accumulate_vote(vote, sender).await, + HotShotEvent::ViewSyncCommitVoteRecv(vote) => { + self.accumulate_vote(vote, self.epoch, sender).await + } _ => Ok(None), } } @@ -619,7 +681,7 @@ impl ) -> Result>> { match event.as_ref() { HotShotEvent::ViewSyncFinalizeVoteRecv(vote) => { - self.accumulate_vote(vote, sender).await + self.accumulate_vote(vote, self.epoch, sender).await } _ => Ok(None), } diff --git a/crates/testing/src/helpers.rs b/crates/testing/src/helpers.rs index 2fa1fd6eec..49aca83652 100644 --- a/crates/testing/src/helpers.rs +++ b/crates/testing/src/helpers.rs @@ -332,6 +332,7 @@ pub fn build_vid_proposal( vid.disperse(&encoded_transactions).unwrap(), quorum_membership, epoch_number, + None, ); let signature = diff --git a/crates/testing/src/overall_safety_task.rs b/crates/testing/src/overall_safety_task.rs index c82315d18d..042bc7f9b7 100644 --- a/crates/testing/src/overall_safety_task.rs +++ b/crates/testing/src/overall_safety_task.rs @@ -19,7 +19,10 @@ use hotshot_types::{ error::RoundTimedoutState, event::{Event, EventType, LeafChain}, simple_certificate::QuorumCertificate2, - traits::node_implementation::{ConsensusTime, NodeType, Versions}, + traits::{ + election::Membership, + node_implementation::{ConsensusTime, NodeType, Versions}, + }, vid::VidCommitment, }; use thiserror::Error; @@ -138,7 +141,6 @@ impl, V: Versions> TestTas check_block, num_failed_views, num_successful_views, - threshold_calculator, transaction_threshold, .. }: OverallSafetyPropertiesDescription = self.properties.clone(); @@ -183,10 +185,34 @@ impl, V: Versions> TestTas _ => return Ok(()), }; - let len = self.handles.read().await.len(); + if let Some(ref key) = key { + if *key.epoch() > self.ctx.latest_epoch { + self.ctx.latest_epoch = *key.epoch(); + } + } + + let epoch = TYPES::Epoch::new(self.ctx.latest_epoch); + let len = self + .handles + .read() + .await + .first() + .unwrap() + .handle + .memberships + .total_nodes(epoch); // update view count - let threshold = (threshold_calculator)(len, len); + let threshold = self + .handles + .read() + .await + .first() + .unwrap() + .handle + .memberships + .success_threshold(epoch) + .get() as usize; let view = self.ctx.round_results.get_mut(&view_number).unwrap(); if let Some(key) = key { @@ -352,6 +378,7 @@ impl Default for RoundCtx { round_results: HashMap::default(), failed_views: HashSet::default(), successful_views: HashSet::default(), + latest_epoch: 0u64, } } } @@ -369,6 +396,8 @@ pub struct RoundCtx { pub failed_views: HashSet, /// successful views pub successful_views: HashSet, + /// latest epoch, updated when a leaf with a higher epoch is seen + pub latest_epoch: u64, } impl RoundCtx { diff --git a/crates/testing/src/spinning_task.rs b/crates/testing/src/spinning_task.rs index 3847a20a62..e9dd819802 100644 --- a/crates/testing/src/spinning_task.rs +++ b/crates/testing/src/spinning_task.rs @@ -28,7 +28,7 @@ use hotshot_types::{ constants::EVENT_CHANNEL_SIZE, data::Leaf2, event::Event, - simple_certificate::QuorumCertificate2, + simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2}, traits::{ network::{AsyncGenerator, ConnectedNetwork}, node_implementation::{ConsensusTime, NodeImplementation, NodeType, Versions}, @@ -65,6 +65,8 @@ pub struct SpinningTask< pub(crate) last_decided_leaf: Leaf2, /// Highest qc seen in the test for restarting nodes pub(crate) high_qc: QuorumCertificate2, + /// Next epoch highest qc seen in the test for restarting nodes + pub(crate) next_epoch_high_qc: Option>, /// Add specified delay to async calls pub(crate) async_delay_config: DelayConfig, /// Context stored for nodes to be restarted with @@ -160,6 +162,7 @@ where TYPES::View::genesis(), BTreeMap::new(), self.high_qc.clone(), + self.next_epoch_high_qc.clone(), None, Vec::new(), BTreeMap::new(), @@ -249,6 +252,7 @@ where ) .await, ), + read_storage.next_epoch_high_qc_cloned().await, read_storage.decided_upgrade_certificate().await, Vec::new(), BTreeMap::new(), diff --git a/crates/testing/src/test_runner.rs b/crates/testing/src/test_runner.rs index f13cba62ca..ec03e3770a 100644 --- a/crates/testing/src/test_runner.rs +++ b/crates/testing/src/test_runner.rs @@ -189,6 +189,7 @@ where &TestInstanceState::default(), ) .await, + next_epoch_high_qc: None, async_delay_config: launcher.metadata.async_delay_config, restart_contexts: HashMap::new(), channel_generator: launcher.resource_generator.channel_generator, @@ -323,6 +324,7 @@ where pub async fn init_builders>( &self, + num_nodes: usize, ) -> (Vec>>, Vec, Url) { let config = self.launcher.resource_generator.config.clone(); let mut builder_tasks = Vec::new(); @@ -332,7 +334,7 @@ where let builder_url = Url::parse(&format!("http://localhost:{builder_port}")).expect("Invalid URL"); let builder_task = B::start( - config.num_nodes_with_stake.into(), + num_nodes, builder_url.clone(), B::Config::default(), metadata.changes.clone(), @@ -397,8 +399,14 @@ where let mut results = vec![]; let config = self.launcher.resource_generator.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(), + ); + let num_nodes = temp_memberships.total_nodes(TYPES::Epoch::new(0)); let (mut builder_tasks, builder_urls, fallback_builder_url) = - self.init_builders::().await; + self.init_builders::(num_nodes).await; if self.launcher.metadata.start_solver { self.add_solver(builder_urls.clone()).await; diff --git a/crates/testing/src/view_generator.rs b/crates/testing/src/view_generator.rs index d5c1cb7ca5..a7390f98ed 100644 --- a/crates/testing/src/view_generator.rs +++ b/crates/testing/src/view_generator.rs @@ -138,6 +138,7 @@ impl TestView { &TestInstanceState::default(), ) .await, + next_epoch_justify_qc: None, upgrade_certificate: None, view_change_evidence: None, drb_result: INITIAL_DRB_RESULT, @@ -368,6 +369,7 @@ impl TestView { block_header: block_header.clone(), view_number: next_view, justify_qc: quorum_certificate.clone(), + next_epoch_justify_qc: None, upgrade_certificate: upgrade_certificate.clone(), view_change_evidence, drb_result: INITIAL_DRB_RESULT, diff --git a/crates/testing/tests/tests_1/test_success.rs b/crates/testing/tests/tests_1/test_success.rs index 8e92935567..bdf55c1f6a 100644 --- a/crates/testing/tests/tests_1/test_success.rs +++ b/crates/testing/tests/tests_1/test_success.rs @@ -8,8 +8,9 @@ use std::{sync::Arc, time::Duration}; use hotshot_example_types::{ node_types::{ - EpochsTestVersions, Libp2pImpl, MemoryImpl, PushCdnImpl, TestConsecutiveLeaderTypes, - TestTwoStakeTablesTypes, TestTypes, TestTypesRandomizedLeader, TestVersions, + CombinedImpl, EpochsTestVersions, Libp2pImpl, MemoryImpl, PushCdnImpl, + TestConsecutiveLeaderTypes, TestTwoStakeTablesTypes, TestTypes, TestTypesRandomizedLeader, + TestVersions, }, testable_delay::{DelayConfig, DelayOptions, DelaySettings, SupportedTraitTypesForAsyncDelay}, }; @@ -156,8 +157,8 @@ cross_tests!( cross_tests!( TestName: test_epoch_end, - Impls: [PushCdnImpl], - Types: [TestTwoStakeTablesTypes], + Impls: [CombinedImpl, Libp2pImpl, PushCdnImpl], + Types: [TestTypes, TestTwoStakeTablesTypes], Versions: [EpochsTestVersions], Ignore: false, Metadata: { diff --git a/crates/testing/tests/tests_1/vid_task.rs b/crates/testing/tests/tests_1/vid_task.rs index 3cd850183c..fc33e7f9a4 100644 --- a/crates/testing/tests/tests_1/vid_task.rs +++ b/crates/testing/tests/tests_1/vid_task.rs @@ -79,6 +79,7 @@ async fn test_vid_task() { num_transactions: encoded_transactions.len() as u64, }, view_number: ViewNumber::new(2), + epoch: EpochNumber::new(0), }; let message = Proposal { data: proposal.clone(), @@ -91,6 +92,7 @@ async fn test_vid_task() { vid_disperse, &membership, EpochNumber::new(0), + None, ); let vid_proposal = Proposal { diff --git a/crates/types/src/consensus.rs b/crates/types/src/consensus.rs index 5a94692be4..edbf78acab 100644 --- a/crates/types/src/consensus.rs +++ b/crates/types/src/consensus.rs @@ -25,7 +25,7 @@ use crate::{ error::HotShotError, event::{HotShotAction, LeafInfo}, message::Proposal, - simple_certificate::{DaCertificate2, QuorumCertificate2}, + simple_certificate::{DaCertificate2, NextEpochQuorumCertificate2, QuorumCertificate2}, traits::{ block_contents::BuilderFee, metrics::{Counter, Gauge, Histogram, Metrics, NoMetrics}, @@ -34,7 +34,8 @@ use crate::{ BlockPayload, ValidatedState, }, utils::{ - epoch_from_block_number, BuilderCommitment, LeafCommitment, StateAndDelta, Terminator, + epoch_from_block_number, is_last_block_in_epoch, BuilderCommitment, LeafCommitment, + StateAndDelta, Terminator, }, vid::VidCommitment, vote::{Certificate, HasViewNumber}, @@ -317,6 +318,9 @@ pub struct Consensus { /// the highqc per spec high_qc: QuorumCertificate2, + /// The high QC for the next epoch + next_epoch_high_qc: Option>, + /// A reference to the metrics trait pub metrics: Arc, @@ -412,6 +416,7 @@ impl Consensus { saved_leaves: CommitmentMap>, saved_payloads: BTreeMap>, high_qc: QuorumCertificate2, + next_epoch_high_qc: Option>, metrics: Arc, epoch_height: u64, ) -> Self { @@ -428,6 +433,7 @@ impl Consensus { saved_leaves, saved_payloads, high_qc, + next_epoch_high_qc, metrics, epoch_height, } @@ -458,6 +464,11 @@ impl Consensus { &self.high_qc } + /// Get the next epoch high QC. + pub fn next_epoch_high_qc(&self) -> Option<&NextEpochQuorumCertificate2> { + self.next_epoch_high_qc.as_ref() + } + /// Get the validated state map. pub fn validated_state_map(&self) -> &BTreeMap> { &self.validated_state_map @@ -740,6 +751,28 @@ impl Consensus { Ok(()) } + /// Update the next epoch high QC if given a newer one. + /// # Errors + /// Can return an error when the provided high_qc is not newer than the existing entry. + /// # Panics + /// It can't actually panic. If the option is None, we will not call unwrap on it. + pub fn update_next_epoch_high_qc( + &mut self, + high_qc: NextEpochQuorumCertificate2, + ) -> Result<()> { + if let Some(next_epoch_high_qc) = self.next_epoch_high_qc() { + ensure!( + high_qc.view_number > next_epoch_high_qc.view_number + || high_qc == *next_epoch_high_qc, + debug!("Next epoch high QC with an equal or higher view exists.") + ); + } + tracing::debug!("Updating next epoch high QC"); + self.next_epoch_high_qc = Some(high_qc); + + Ok(()) + } + /// Add a new entry to the vid_shares map. pub fn update_vid_shares( &mut self, @@ -916,7 +949,8 @@ impl Consensus { .get(&view)? .view_inner .epoch()?; - let vid = VidDisperse::calculate_vid_disperse(txns, &membership, view, epoch, None).await; + let vid = + VidDisperse::calculate_vid_disperse(txns, &membership, view, epoch, None, None).await; let shares = VidDisperseShare2::from_vid_disperse(vid); let mut consensus_writer = consensus.write().await; for share in shares { @@ -1010,11 +1044,7 @@ impl Consensus { return false; }; let block_height = leaf.height(); - if block_height == 0 || self.epoch_height == 0 { - false - } else { - block_height % self.epoch_height == 0 - } + is_last_block_in_epoch(block_height, self.epoch_height) } /// Returns true if our high QC is for the last block in the epoch @@ -1024,11 +1054,7 @@ impl Consensus { return false; }; let block_height = leaf.height(); - if block_height == 0 || self.epoch_height == 0 { - false - } else { - block_height % self.epoch_height == 0 - } + is_last_block_in_epoch(block_height, self.epoch_height) } /// Returns true if the `parent_leaf` formed an eQC for the previous epoch to the `proposed_leaf` diff --git a/crates/types/src/data.rs b/crates/types/src/data.rs index 73acf9d6f0..f3853e412e 100644 --- a/crates/types/src/data.rs +++ b/crates/types/src/data.rs @@ -34,8 +34,8 @@ use crate::{ impl_has_epoch, message::{Proposal, UpgradeLock}, simple_certificate::{ - QuorumCertificate, QuorumCertificate2, TimeoutCertificate2, UpgradeCertificate, - ViewSyncFinalizeCertificate2, + NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, TimeoutCertificate2, + UpgradeCertificate, ViewSyncFinalizeCertificate2, }, simple_vote::{HasEpoch, QuorumData, QuorumData2, UpgradeProposalData, VersionedVoteData}, traits::{ @@ -146,6 +146,8 @@ pub struct DaProposal { pub metadata: >::Metadata, /// View this proposal applies to pub view_number: TYPES::View, + /// Epoch this proposal applies to + pub epoch: TYPES::Epoch, } /// A proposal to start providing data availability for a block. @@ -179,6 +181,7 @@ impl From> for DaProposal { encoded_transactions: da_proposal2.encoded_transactions, metadata: da_proposal2.metadata, view_number: da_proposal2.view_number, + epoch: TYPES::Epoch::new(0), } } } @@ -194,6 +197,8 @@ where pub upgrade_proposal: UpgradeProposalData, /// View this proposal applies to pub view_number: TYPES::View, + /// Epoch this proposal applies to + pub epoch: TYPES::Epoch, } /// VID dispersal data @@ -205,7 +210,7 @@ where pub struct VidDisperse { /// The view number for which this VID data is intended pub view_number: TYPES::View, - /// The epoch number for which this VID data is intended + /// Epoch this proposal applies to pub epoch: TYPES::Epoch, /// Block payload commitment pub payload_commitment: VidCommitment, @@ -216,32 +221,34 @@ pub struct VidDisperse { } impl VidDisperse { - /// Create VID dispersal from a specified membership for a given epoch. + /// Create VID dispersal from a specified membership for the target epoch. /// Uses the specified function to calculate share dispersal /// Allows for more complex stake table functionality pub fn from_membership( view_number: TYPES::View, mut vid_disperse: JfVidDisperse, membership: &TYPES::Membership, - epoch: TYPES::Epoch, + target_epoch: TYPES::Epoch, + sender_epoch: Option, ) -> Self { let shares = membership - .committee_members(view_number, epoch) + .committee_members(view_number, target_epoch) .iter() .map(|node| (node.clone(), vid_disperse.shares.remove(0))) .collect(); Self { view_number, - epoch, shares, common: vid_disperse.common, payload_commitment: vid_disperse.commit, + epoch: sender_epoch.unwrap_or(target_epoch), } } /// Calculate the vid disperse information from the payload given a view, epoch and membership, - /// optionally using precompute data from builder + /// optionally using precompute data from builder. + /// If the sender epoch is missing, it means it's the same as the target epoch. /// /// # Panics /// Panics if the VID calculation fails, this should not happen. @@ -250,10 +257,11 @@ impl VidDisperse { txns: Arc<[u8]>, membership: &Arc, view: TYPES::View, - epoch: TYPES::Epoch, + target_epoch: TYPES::Epoch, + sender_epoch: Option, precompute_data: Option, ) -> Self { - let num_nodes = membership.total_nodes(epoch); + let num_nodes = membership.total_nodes(target_epoch); let vid_disperse = spawn_blocking(move || { precompute_data @@ -266,7 +274,13 @@ impl VidDisperse { // Unwrap here will just propagate any panic from the spawned task, it's not a new place we can panic. let vid_disperse = vid_disperse.unwrap(); - Self::from_membership(view, vid_disperse, membership.as_ref(), epoch) + Self::from_membership( + view, + vid_disperse, + membership.as_ref(), + target_epoch, + sender_epoch, + ) } } @@ -574,6 +588,9 @@ pub struct QuorumProposal2 { /// certificate that the proposal is chaining from pub justify_qc: QuorumCertificate2, + /// certificate that the proposal is chaining from formed by the next epoch nodes + pub next_epoch_justify_qc: Option>, + /// Possible upgrade certificate, which the leader may optionally attach. pub upgrade_certificate: Option>, @@ -599,6 +616,7 @@ impl From> for QuorumProposal2 { block_header: quorum_proposal.block_header, view_number: quorum_proposal.view_number, justify_qc: quorum_proposal.justify_qc.to_qc2(), + next_epoch_justify_qc: None, upgrade_certificate: quorum_proposal.upgrade_certificate, view_change_evidence: quorum_proposal.proposal_certificate, drb_seed: INITIAL_DRB_SEED_INPUT, @@ -627,6 +645,7 @@ impl From> for Leaf2 { view_number: leaf.view_number, epoch: TYPES::Epoch::genesis(), justify_qc: leaf.justify_qc.to_qc2(), + next_epoch_justify_qc: None, parent_commitment: Commitment::from_raw(bytes), block_header: leaf.block_header, upgrade_certificate: leaf.upgrade_certificate, @@ -759,6 +778,9 @@ pub struct Leaf2 { /// Per spec, justification justify_qc: QuorumCertificate2, + /// certificate that the proposal is chaining from formed by the next epoch nodes + next_epoch_justify_qc: Option>, + /// The hash of the parent `Leaf` /// So we can ask if it extends parent_commitment: Commitment, @@ -834,6 +856,7 @@ impl Leaf2 { Self { view_number: TYPES::View::genesis(), justify_qc, + next_epoch_justify_qc: None, parent_commitment: null_quorum_data.leaf_commit, upgrade_certificate: None, block_header: block_header.clone(), @@ -1001,6 +1024,7 @@ impl PartialEq for Leaf2 { view_number, epoch, justify_qc, + next_epoch_justify_qc, parent_commitment, block_header, upgrade_certificate, @@ -1013,6 +1037,7 @@ impl PartialEq for Leaf2 { *view_number == other.view_number && *epoch == other.epoch && *justify_qc == other.justify_qc + && *next_epoch_justify_qc == other.next_epoch_justify_qc && *parent_commitment == other.parent_commitment && *block_header == other.block_header && *upgrade_certificate == other.upgrade_certificate @@ -1378,6 +1403,7 @@ impl Leaf2 { let QuorumProposal2 { view_number, justify_qc, + next_epoch_justify_qc, block_header, upgrade_certificate, view_change_evidence, @@ -1392,6 +1418,7 @@ impl Leaf2 { TYPES::EPOCH_HEIGHT, )), justify_qc: justify_qc.clone(), + next_epoch_justify_qc: next_epoch_justify_qc.clone(), parent_commitment: justify_qc.data().leaf_commit, block_header: block_header.clone(), upgrade_certificate: upgrade_certificate.clone(), diff --git a/crates/types/src/simple_certificate.rs b/crates/types/src/simple_certificate.rs index 91d5f00d7a..5ea857ed21 100644 --- a/crates/types/src/simple_certificate.rs +++ b/crates/types/src/simple_certificate.rs @@ -24,10 +24,10 @@ use crate::{ data::serialize_signature2, message::UpgradeLock, simple_vote::{ - DaData, DaData2, QuorumData, QuorumData2, QuorumMarker, TimeoutData, TimeoutData2, - UpgradeProposalData, VersionedVoteData, ViewSyncCommitData, ViewSyncCommitData2, - ViewSyncFinalizeData, ViewSyncFinalizeData2, ViewSyncPreCommitData, ViewSyncPreCommitData2, - Voteable, + DaData, DaData2, NextEpochQuorumData2, QuorumData, QuorumData2, QuorumMarker, TimeoutData, + TimeoutData2, UpgradeProposalData, VersionedVoteData, ViewSyncCommitData, + ViewSyncCommitData2, ViewSyncFinalizeData, ViewSyncFinalizeData2, ViewSyncPreCommitData, + ViewSyncPreCommitData2, Voteable, }, traits::{ election::Membership, @@ -42,7 +42,7 @@ pub trait Threshold { /// Calculate a threshold based on the membership fn threshold>( membership: &MEMBERSHIP, - epoch: ::Epoch, + epoch: TYPES::Epoch, ) -> u64; } @@ -53,7 +53,7 @@ pub struct SuccessThreshold {} impl Threshold for SuccessThreshold { fn threshold>( membership: &MEMBERSHIP, - epoch: ::Epoch, + epoch: TYPES::Epoch, ) -> u64 { membership.success_threshold(epoch).into() } @@ -66,7 +66,7 @@ pub struct OneHonestThreshold {} impl Threshold for OneHonestThreshold { fn threshold>( membership: &MEMBERSHIP, - epoch: ::Epoch, + epoch: TYPES::Epoch, ) -> u64 { membership.failure_threshold(epoch).into() } @@ -79,7 +79,7 @@ pub struct UpgradeThreshold {} impl Threshold for UpgradeThreshold { fn threshold>( membership: &MEMBERSHIP, - epoch: ::Epoch, + epoch: TYPES::Epoch, ) -> u64 { membership.upgrade_threshold(epoch).into() } @@ -211,7 +211,7 @@ impl> Certificate } fn threshold>( membership: &MEMBERSHIP, - epoch: ::Epoch, + epoch: TYPES::Epoch, ) -> u64 { membership.da_success_threshold(epoch).into() } @@ -367,7 +367,7 @@ impl< } fn threshold>( membership: &MEMBERSHIP, - epoch: ::Epoch, + epoch: TYPES::Epoch, ) -> u64 { THRESHOLD::threshold(membership, epoch) } @@ -736,6 +736,9 @@ impl TimeoutCertificate2 { pub type QuorumCertificate = SimpleCertificate, SuccessThreshold>; /// Type alias for a `QuorumCertificate2`, which is a `SimpleCertificate` over `QuorumData2` pub type QuorumCertificate2 = SimpleCertificate, SuccessThreshold>; +/// Type alias for a `QuorumCertificate2`, which is a `SimpleCertificate` over `QuorumData2` +pub type NextEpochQuorumCertificate2 = + SimpleCertificate, SuccessThreshold>; /// Type alias for a `DaCertificate`, which is a `SimpleCertificate` over `DaData` pub type DaCertificate = SimpleCertificate; /// Type alias for a `DaCertificate2`, which is a `SimpleCertificate` over `DaData2` diff --git a/crates/types/src/simple_vote.rs b/crates/types/src/simple_vote.rs index 1211756644..f75d451914 100644 --- a/crates/types/src/simple_vote.rs +++ b/crates/types/src/simple_vote.rs @@ -6,7 +6,12 @@ //! Implementations of the simple vote types. -use std::{fmt::Debug, hash::Hash, marker::PhantomData}; +use std::{ + fmt::Debug, + hash::Hash, + marker::PhantomData, + ops::{Deref, DerefMut}, +}; use committable::{Commitment, Committable}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -40,9 +45,13 @@ pub struct QuorumData { pub struct QuorumData2 { /// Commitment to the leaf pub leaf_commit: Commitment>, - /// Epoch number + /// An epoch to which the data belongs to. Relevant for validating against the correct stake table pub epoch: TYPES::Epoch, } +/// Data used for a yes vote. Used to distinguish votes sent by the next epoch nodes. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +#[serde(bound(deserialize = ""))] +pub struct NextEpochQuorumData2(QuorumData2); #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] /// Data used for a DA vote. pub struct DaData { @@ -186,6 +195,7 @@ mod sealed { impl QuorumMarker for QuorumData {} impl QuorumMarker for QuorumData2 {} +impl QuorumMarker for NextEpochQuorumData2 {} impl QuorumMarker for TimeoutData {} impl QuorumMarker for TimeoutData2 {} impl QuorumMarker for ViewSyncPreCommitData {} @@ -356,6 +366,14 @@ impl Committable for QuorumData2 { } } +impl Committable for NextEpochQuorumData2 { + fn commit(&self) -> Commitment { + committable::RawCommitmentBuilder::new("Quorum data") + .var_size_bytes(self.leaf_commit.as_ref()) + .finalize() + } +} + impl Committable for TimeoutData { fn commit(&self) -> Commitment { committable::RawCommitmentBuilder::new("Timeout data") @@ -517,6 +535,7 @@ macro_rules! impl_has_epoch { impl_has_epoch!( QuorumData2, + NextEpochQuorumData2, DaData2, TimeoutData2, ViewSyncPreCommitData2, @@ -524,6 +543,14 @@ impl_has_epoch!( ViewSyncFinalizeData2 ); +impl + HasEpoch> HasEpoch + for SimpleVote +{ + fn epoch(&self) -> TYPES::Epoch { + self.data.epoch() + } +} + // impl votable for all the data types in this file sealed marker should ensure nothing is accidentally // implemented for structs that aren't "voteable" impl< @@ -575,7 +602,7 @@ impl QuorumVote2 { pub fn to_vote(self) -> QuorumVote { let bytes: [u8; 32] = self.data.leaf_commit.into(); - let signature = self.signature; + let signature = self.signature.clone(); let data = QuorumData { leaf_commit: Commitment::from_raw(bytes), }; @@ -777,7 +804,8 @@ pub type QuorumVote = SimpleVote>; // Type aliases for simple use of all the main votes. We should never see `SimpleVote` outside this file /// Quorum vote Alias pub type QuorumVote2 = SimpleVote>; - +/// Quorum vote Alias. This type is useful to distinguish the next epoch nodes' votes. +pub type NextEpochQuorumVote2 = SimpleVote>; /// DA vote type alias pub type DaVote = SimpleVote; /// DA vote 2 type alias @@ -800,8 +828,37 @@ pub type ViewSyncFinalizeVote2 = SimpleVote = SimpleVote>; /// View Sync Commit Vote 2 type alias pub type ViewSyncCommitVote2 = SimpleVote>; - /// Upgrade proposal vote pub type UpgradeVote = SimpleVote>; /// Upgrade proposal 2 vote pub type UpgradeVote2 = SimpleVote>; + +impl Deref for NextEpochQuorumData2 { + type Target = QuorumData2; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for NextEpochQuorumData2 { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl From> for NextEpochQuorumData2 { + fn from(data: QuorumData2) -> Self { + Self(QuorumData2 { + epoch: data.epoch, + leaf_commit: data.leaf_commit, + }) + } +} + +impl From> for NextEpochQuorumVote2 { + fn from(qvote: QuorumVote2) -> Self { + Self { + data: qvote.data.into(), + view_number: qvote.view_number, + signature: qvote.signature.clone(), + } + } +} diff --git a/crates/types/src/traits/storage.rs b/crates/types/src/traits/storage.rs index 7d3aa42241..5a4fdb4586 100644 --- a/crates/types/src/traits/storage.rs +++ b/crates/types/src/traits/storage.rs @@ -24,7 +24,9 @@ use crate::{ }, event::HotShotAction, message::Proposal, - simple_certificate::{QuorumCertificate, QuorumCertificate2, UpgradeCertificate}, + simple_certificate::{ + NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, UpgradeCertificate, + }, vid::VidSchemeType, }; @@ -64,6 +66,11 @@ pub trait Storage: Send + Sync + Clone { async fn update_high_qc(&self, high_qc: QuorumCertificate) -> Result<()>; /// Update the current high QC in storage. async fn update_high_qc2(&self, high_qc: QuorumCertificate2) -> Result<()>; + /// Update the current high QC in storage. + async fn update_next_epoch_high_qc2( + &self, + next_epoch_high_qc: NextEpochQuorumCertificate2, + ) -> Result<()>; /// Update the currently undecided state of consensus. This includes the undecided leaf chain, /// and the undecided state. async fn update_undecided_state( diff --git a/crates/types/src/utils.rs b/crates/types/src/utils.rs index 62503f08b5..50877f047a 100644 --- a/crates/types/src/utils.rs +++ b/crates/types/src/utils.rs @@ -262,9 +262,20 @@ pub fn mnemonic(bytes: H) -> String { /// A helper enum to indicate whether a node is in the epoch transition /// A node is in epoch transition when its high QC is for the last block in an epoch +#[derive(Debug, Clone)] pub enum EpochTransitionIndicator { /// A node is currently in the epoch transition InTransition, /// A node is not in the epoch transition NotInTransition, } + +/// Returns true if the given block number is the last in the epoch based on the given epoch height. +#[must_use] +pub fn is_last_block_in_epoch(block_number: u64, epoch_height: u64) -> bool { + if block_number == 0 || epoch_height == 0 { + false + } else { + block_number % epoch_height == 0 + } +} diff --git a/crates/types/src/vote.rs b/crates/types/src/vote.rs index e70dbe41a3..103c470e6d 100644 --- a/crates/types/src/vote.rs +++ b/crates/types/src/vote.rs @@ -83,7 +83,7 @@ pub trait Certificate: HasViewNumber { // TODO: Make this a static ratio of the total stake of `Membership` fn threshold>( membership: &MEMBERSHIP, - epoch: ::Epoch, + epoch: TYPES::Epoch, ) -> u64; /// Get Stake Table from Membership implementation.