diff --git a/crates/hotshot/new-protocol/bench/src/metrics.rs b/crates/hotshot/new-protocol/bench/src/metrics.rs index 0166ba53caa..195c687f3c2 100644 --- a/crates/hotshot/new-protocol/bench/src/metrics.rs +++ b/crates/hotshot/new-protocol/bench/src/metrics.rs @@ -70,7 +70,7 @@ impl MetricsCollector { self.view_mut(v).vid_disperse_ns = Some(ts); }, // Replica: proposal processing - ConsensusInput::ProposalWithVidShare(_sender, p, _) => { + ConsensusInput::Proposal(_sender, p) => { let v = *p.view_number(); self.view_mut(v).proposal_recv_ns = Some(ts); }, diff --git a/crates/hotshot/new-protocol/src/consensus.rs b/crates/hotshot/new-protocol/src/consensus.rs index e8655735ca0..9503bd1423a 100644 --- a/crates/hotshot/new-protocol/src/consensus.rs +++ b/crates/hotshot/new-protocol/src/consensus.rs @@ -102,11 +102,12 @@ pub enum ConsensusInput { }, EpochChange(EpochChangeMessage), HeaderCreated(ViewNumber, Commitment>, T::BlockHeader), - ProposalWithVidShare( - T::SignatureKey, - ProposalMessage, - VidDisperseShare2, - ), + /// A validated proposal. Consensus parks it until this node's VID share + /// for the same payload arrives ([`ConsensusInput::VidShare`]) and only + /// processes the two together. + Proposal(T::SignatureKey, ProposalMessage), + /// This node's validated VID share. + VidShare(VidDisperseShare2), FetchedProposal(ProposalMessage), StateValidated(StateResponse), StateValidationFailed(StateResponse), @@ -156,6 +157,11 @@ pub enum ConsensusOutput { ViewChanged(ViewNumber, EpochNumber), /// A view timed out with a timeout certificate. ViewTimedOut(ViewNumber), + /// A validated proposal met this node's VID share. + ProposalPaired { + proposal: SignedProposal>, + vid_share: VidDisperseShare2, + }, ProposalValidated { proposal: SignedProposal>, sender: T::SignatureKey, @@ -178,6 +184,13 @@ pub enum ConsensusOutput { BroadcastVidShare(VidDisperseShare2), } +type UnpairedProposals = BTreeMap< + (ViewNumber, VidCommitment2), + (::SignatureKey, ProposalMessage), +>; + +type UnpairedVidShares = BTreeMap<(ViewNumber, VidCommitment2), VidDisperseShare2>; + /// Views to retain decide inputs (`proposals`, `certs`, `certs2`) behind the /// decided view, letting a late-broadcast Cert2 decide an older gap view. pub(crate) const DECIDE_BUFFER: u64 = 20; @@ -190,6 +203,8 @@ pub struct Consensus { signed_proposals: BTreeMap>>, proposed_views: BTreeSet, vid_shares: BTreeMap>, + unpaired_proposals: UnpairedProposals, + unpaired_vid_shares: UnpairedVidShares, states_verified: BTreeMap>>, blocks_reconstructed: BTreeSet<(ViewNumber, VidCommitment2)>, blocks: BTreeMap<(ViewNumber, VidCommitment2), T::BlockPayload>, @@ -369,6 +384,8 @@ impl Consensus { state_certs: BTreeMap::new(), upgrade_lock, vid_shares: BTreeMap::new(), + unpaired_proposals: BTreeMap::new(), + unpaired_vid_shares: BTreeMap::new(), epoch_height: epoch_height.into(), } } @@ -659,14 +676,18 @@ impl Consensus { input.view_number() }; let proto = match input { - ConsensusInput::ProposalWithVidShare(sender, proposal, vid_share) => { + ConsensusInput::Proposal(sender, proposal) => { debug!( sender = %KeyPrefix::from(&sender), block = %proposal.proposal.data.block_header.block_number(), epoch = %proposal.proposal.data.epoch, - "apply: proposal+vid share" + "apply: proposal" ); - self.handle_proposal_with_vid_share(sender, proposal, vid_share, outbox) + self.pair_proposal(sender, proposal, outbox) + }, + ConsensusInput::VidShare(vid_share) => { + debug!("apply: vid share"); + self.pair_vid_share(vid_share, outbox) }, ConsensusInput::FetchedProposal(message) => { debug!( @@ -946,7 +967,10 @@ impl Consensus { match scope { GcScope::Local(view) => { let c = Commitment::default_commitment_no_preimage(); + let vc = VidCommitment2::default(); self.headers = self.headers.split_off(&(view, c)); + self.unpaired_proposals = self.unpaired_proposals.split_off(&(view, vc)); + self.unpaired_vid_shares = self.unpaired_vid_shares.split_off(&(view, vc)); self.proposed_views = self.proposed_views.split_off(&view); self.states_verified = self.states_verified.split_off(&view); self.timeout_certs = self.timeout_certs.split_off(&view); @@ -1009,6 +1033,64 @@ impl Consensus { self.proposals.insert(view, proposal); } + /// Pair a validated proposal with this node's VID share for the same payload. + /// + /// The half arriving first is parked, keyed by (view, payload commitment). + fn pair_proposal( + &mut self, + sender: T::SignatureKey, + proposal: ProposalMessage, + outbox: &mut Outbox>, + ) -> Protocol { + let view = proposal.view_number(); + let VidCommitment::V2(commit) = proposal.proposal.data.block_header.payload_commitment() + else { + warn!(%view, "proposal payload commitment is not V2, discarding"); + return Protocol::Abort; + }; + let Some(vid_share) = self.unpaired_vid_shares.remove(&(view, commit)) else { + self.unpaired_proposals + .insert((view, commit), (sender, proposal)); + return Protocol::Abort; + }; + self.on_proposal_paired(sender, proposal, vid_share, outbox) + } + + /// Pair this node's VID share with a validated proposal for the same payload. + /// + /// The half arriving first is parked, keyed by (view, payload commitment). + fn pair_vid_share( + &mut self, + vid_share: VidDisperseShare2, + outbox: &mut Outbox>, + ) -> Protocol { + let key = (vid_share.view_number(), vid_share.payload_commitment); + let Some((sender, proposal)) = self.unpaired_proposals.remove(&key) else { + self.unpaired_vid_shares.insert(key, vid_share); + return Protocol::Abort; + }; + self.on_proposal_paired(sender, proposal, vid_share, outbox) + } + + fn on_proposal_paired( + &mut self, + sender: T::SignatureKey, + proposal: ProposalMessage, + vid_share: VidDisperseShare2, + outbox: &mut Outbox>, + ) -> Protocol { + // Parked halves for this and older views can no longer pair. + let view = proposal.view_number(); + let vc = VidCommitment2::default(); + self.unpaired_proposals = self.unpaired_proposals.split_off(&(view + 1, vc)); + self.unpaired_vid_shares = self.unpaired_vid_shares.split_off(&(view + 1, vc)); + outbox.push_back(ConsensusOutput::ProposalPaired { + proposal: proposal.proposal.clone(), + vid_share: vid_share.clone(), + }); + self.handle_proposal_with_vid_share(sender, proposal, vid_share, outbox) + } + #[instrument(level = "debug", skip_all)] fn handle_proposal_with_vid_share( &mut self, @@ -2625,7 +2707,8 @@ impl ConsensusInput { ConsensusInput::AdvanceView(cert) => cert.view_number() + 1, ConsensusInput::EpochRootCertificates { cert1, .. } => cert1.view_number(), ConsensusInput::HeaderCreated(view, ..) => *view, - ConsensusInput::ProposalWithVidShare(_, prop, _) => prop.view_number(), + ConsensusInput::Proposal(_, prop) => prop.view_number(), + ConsensusInput::VidShare(share) => share.view_number(), ConsensusInput::FetchedProposal(prop) => prop.view_number(), ConsensusInput::StateValidated(response) => response.view, ConsensusInput::StateValidationFailed(request) => request.view, diff --git a/crates/hotshot/new-protocol/src/coordinator.rs b/crates/hotshot/new-protocol/src/coordinator.rs index 2b74679e6c1..e0f4bc368fa 100644 --- a/crates/hotshot/new-protocol/src/coordinator.rs +++ b/crates/hotshot/new-protocol/src/coordinator.rs @@ -14,7 +14,7 @@ use hotshot::{HotShotInitializer, traits::BlockPayload, types::SignatureKey}; use hotshot_types::{ consensus::{ConsensusMetricsValue, ParticipationTracker}, data::{ - EpochNumber, Leaf2, VidCommitment, VidCommitment2, VidDisperseShare2, ViewNumber, + EpochNumber, Leaf2, VidCommitment, VidCommitment2, ViewNumber, vid_disperse::vid_total_weight, }, epoch_membership::EpochMembershipCoordinator, @@ -51,7 +51,7 @@ use crate::{ }, network::Cliquenet, outbox::Outbox, - proposal::{ProposalValidator, ValidatedProposal, VidShareValidator}, + proposal::{ProposalValidator, VidShareValidator}, state::{HeaderRequest, StateEntry, StateManager, StateManagerOutput}, storage::{NewProtocolStorage, Storage}, vid::{VidDisperseRequest, VidDisperser, VidFragmentAccumulator, VidReconstructor}, @@ -112,10 +112,6 @@ pub struct Coordinator { pending_proposal_fetches: PendingProposalFetches, #[builder(skip)] requested_missing_proposals: HashSet>, - #[builder(default)] - cached_validated_proposals: BTreeMap<(ViewNumber, VidCommitment2), ValidatedProposal>, - #[builder(default)] - cached_vid_shares: BTreeMap<(ViewNumber, VidCommitment2), VidDisperseShare2>, #[builder(skip)] da_payloads: BTreeMap<(ViewNumber, VidCommitment2), PendingDa>, metrics: Option, @@ -475,14 +471,7 @@ where } Some(item) = self.share_validator.next() => match item { Ok(vid_share) => { - let view = vid_share.view_number(); - let key = (view, vid_share.payload_commitment); - let Some(validated) = self.cached_validated_proposals.remove(&key) else { - // Wait for the proposal - self.cached_vid_shares.insert(key, vid_share); - continue; - }; - return self.on_proposal_and_vid_share(validated, vid_share) + return Ok(ConsensusInput::VidShare(vid_share)) }, Err(e) => { return Err(CoordinatorError::regular(e).context("vid share validation")) @@ -496,21 +485,7 @@ where // Refresh the network's peer set when a proposal is validated. let epoch = validated.message.proposal.data.epoch; self.bump_network_epoch(epoch); - - let view = validated.message.proposal.data.view_number(); - let VidCommitment::V2(commit) = - validated.message.proposal.data.block_header.payload_commitment() - else { - warn!(%view, "proposal payload commitment is not V2, discarding"); - continue; - }; - let key = (view, commit); - let Some(vid_share) = self.cached_vid_shares.remove(&key) else { - // Wait for the vid share describing this payload. - self.cached_validated_proposals.insert(key, validated); - continue; - }; - return self.on_proposal_and_vid_share(validated, vid_share) + return Ok(ConsensusInput::Proposal(validated.sender, validated.message)) } Err(e) => { return Err(CoordinatorError::regular(e).context("proposal validation")) @@ -746,6 +721,31 @@ where } } }, + ConsensusOutput::ProposalPaired { + proposal, + vid_share, + } => { + let view = proposal.data.view_number; + debug!(%node, %view, "proposal paired with vid share"); + self.storage.append_vid(vid_share.clone()); + self.storage.append_proposal(proposal.data.clone()); + if let Some(state_cert) = &proposal.data.state_cert { + self.storage.append_state_cert( + ViewNumber::new(state_cert.light_client_state.view_number), + state_cert.clone(), + ); + } + let expected_param = self.expected_vid_param(vid_share.target_epoch); + self.vid_reconstructor.handle_proposal( + view, + vid_share.payload_commitment, + proposal.data.block_header.metadata().clone(), + proposal.data.epoch, + expected_param, + ); + self.vid_reconstructor + .handle_vid_share(self.public_key.clone(), vid_share); + }, ConsensusOutput::SendProposal(proposal) => { let view = proposal.data.view_number; let epoch = proposal.data.epoch; @@ -1318,52 +1318,6 @@ where init_avidm_gf2_param(total_weight).ok() } - fn on_proposal_and_vid_share( - &mut self, - validated: ValidatedProposal, - vid_share: VidDisperseShare2, - ) -> Result, CoordinatorError> { - self.storage.append_vid(vid_share.clone()); - self.storage - .append_proposal(validated.message.proposal.data.clone()); - - if let Some(state_cert) = &validated.message.proposal.data.state_cert { - self.storage.append_state_cert( - ViewNumber::new(state_cert.light_client_state.view_number), - state_cert.clone(), - ); - } - - let expected_param = self.expected_vid_param(vid_share.target_epoch); - let proposal = &validated.message.proposal.data; - self.vid_reconstructor.handle_proposal( - proposal.view_number(), - vid_share.payload_commitment, - proposal.block_header.metadata().clone(), - proposal.epoch, - expected_param, - ); - // This is our own share, addressed to us by the leader and already - // verified by the share validator. - self.vid_reconstructor - .handle_vid_share(self.public_key.clone(), vid_share.clone()); - - // GC for the cache - let view = validated.message.proposal.data.view_number(); - self.cached_vid_shares = self - .cached_vid_shares - .split_off(&(view + 1, VidCommitment2::default())); - self.cached_validated_proposals = self - .cached_validated_proposals - .split_off(&(view + 1, VidCommitment2::default())); - - Ok(ConsensusInput::ProposalWithVidShare( - validated.sender, - validated.message, - vid_share, - )) - } - fn broadcast( &self, message_type: ConsensusMessage, @@ -1715,11 +1669,7 @@ where self.consensus.gc(scope); match scope { GcScope::Local(view) => { - let vc = VidCommitment2::default(); self.block_builder.gc(view); - self.cached_validated_proposals = - self.cached_validated_proposals.split_off(&(view, vc)); - self.cached_vid_shares = self.cached_vid_shares.split_off(&(view, vc)); self.vid_disperser.gc(view); self.vid_fragment_accumulator.gc(view); // When we enter a new view, we do not want to GC certain data diff --git a/crates/hotshot/new-protocol/src/tests/common/utils.rs b/crates/hotshot/new-protocol/src/tests/common/utils.rs index 0860975c128..621904bad11 100644 --- a/crates/hotshot/new-protocol/src/tests/common/utils.rs +++ b/crates/hotshot/new-protocol/src/tests/common/utils.rs @@ -147,11 +147,17 @@ impl TestView { )), } } - pub fn proposal_input_consensus(&self, recipient_key: &BLSPubKey) -> ConsensusInput { - ConsensusInput::ProposalWithVidShare( - self.leader_public_key, - self.proposal_message(), - self.vid_share_for(recipient_key), + /// Build the proposal and VID share inputs a node receives for this view. + /// + /// Consensus pairs the two internally, so both must be applied + /// (cf. [`ConsensusHarness::apply_pair`]). + pub fn proposal_input_consensus( + &self, + recipient_key: &BLSPubKey, + ) -> (ConsensusInput, ConsensusInput) { + ( + ConsensusInput::Proposal(self.leader_public_key, self.proposal_message()), + ConsensusInput::VidShare(self.vid_share_for(recipient_key)), ) } @@ -1067,24 +1073,20 @@ impl ConsensusHarness { /// actions that consensus expects feedback for. pub async fn apply(&mut self, input: ConsensusInput) { let mut outbox = Outbox::new(); - // The coordinator persists incoming proposals and VID shares before - // consensus sees them; simulate those storage confirmations. - if let ConsensusInput::ProposalWithVidShare(_, msg, vid_share) = &input { - let view = msg.proposal.data.view_number; - let commitment = proposal_commitment(&msg.proposal.data); - self.consensus.apply( - ConsensusInput::Stored(StorageOutput::Proposal(view, commitment)), - &mut outbox, - ); - self.consensus.apply( - ConsensusInput::Stored(StorageOutput::Vid(vid_share.view_number)), - &mut outbox, - ); - } self.consensus.apply(input, &mut outbox); self.drain_outbox(&mut outbox).await; } + /// Apply a proposal/VID-share input pair + /// (cf. `TestView::proposal_input_consensus`). + pub async fn apply_pair( + &mut self, + (proposal, vid_share): (ConsensusInput, ConsensusInput), + ) { + self.apply(proposal).await; + self.apply(vid_share).await; + } + async fn drain_outbox(&mut self, outbox: &mut Outbox>) { while let Some(output) = outbox.pop_front() { self.handle_output(&output, outbox).await; @@ -1122,6 +1124,23 @@ impl ConsensusHarness { outbox, ); }, + // The coordinator persists a paired proposal and VID share; + // simulate the storage confirmations. + ConsensusOutput::ProposalPaired { + proposal, + vid_share, + } => { + let view = proposal.data.view_number; + let commitment = proposal_commitment(&proposal.data); + self.consensus.apply( + ConsensusInput::Stored(StorageOutput::Proposal(view, commitment)), + outbox, + ); + self.consensus.apply( + ConsensusInput::Stored(StorageOutput::Vid(vid_share.view_number)), + outbox, + ); + }, ConsensusOutput::RequestBlockAndHeader(req) => { let mock_block = MockBlock::new(); let parent_leaf = req.parent_proposal.clone().into(); diff --git a/crates/hotshot/new-protocol/src/tests/consensus.rs b/crates/hotshot/new-protocol/src/tests/consensus.rs index 16483ad7347..69e75a8e4f0 100644 --- a/crates/hotshot/new-protocol/src/tests/consensus.rs +++ b/crates/hotshot/new-protocol/src/tests/consensus.rs @@ -39,7 +39,7 @@ async fn test_safety_genesis_no_lock() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; assert!( @@ -67,12 +67,12 @@ async fn test_timeout_filters_vote1_not_processing() { // Send stale proposal (view 2, which is <= timeout_view 3). // It is still processed (state validation requested) but vote1 is suppressed. harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; // Send fresh proposal (view 4, which is > timeout_view 3) harness - .apply(test_data.views[3].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[3].proposal_input_consensus(&node_key)) .await; // Both proposals are processed. @@ -94,14 +94,14 @@ async fn test_vote1_for_sequential_views() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; assert_eq!( @@ -129,7 +129,7 @@ async fn test_vote1_with_seeded_parent_proposal() { // The view-2 proposal arrives and builds on the re-seeded parent. harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; assert!( @@ -156,7 +156,7 @@ async fn test_vote1_parent_reconstruction_from_lock() { .seed_locked_cert(test_data.views[0].cert1.clone()); harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; assert!( @@ -178,7 +178,7 @@ async fn test_vote1_blocked_without_parent_proposal() { .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; assert!( @@ -195,7 +195,7 @@ async fn test_vote1_genesis_parent() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; assert!( @@ -213,14 +213,14 @@ async fn test_vote2_missing_cert1() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -240,14 +240,14 @@ async fn test_vote2_with_cert1() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -268,14 +268,14 @@ async fn test_single_view_decide() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -300,13 +300,13 @@ async fn test_cert2_broadcast_once() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -348,7 +348,7 @@ async fn test_late_cert2_decides_after_view_change_gc() { // Drive views 1..=4 forward with Cert1 only, so nothing decides. for view in &test_data.views[0..4] { harness - .apply(view.proposal_input_consensus(&node_key)) + .apply_pair(view.proposal_input_consensus(&node_key)) .await; harness.apply(view.block_reconstructed_input()).await; harness.apply(view.cert1_input()).await; @@ -382,7 +382,7 @@ async fn test_gap_fill_decide_of_older_view() { // Decide view 4 without ever delivering views 1-3, leaving a gap. harness - .apply(test_data.views[3].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[3].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[3].block_reconstructed_input()) @@ -437,7 +437,7 @@ async fn test_no_duplicate_vote1() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) @@ -459,14 +459,14 @@ async fn test_no_duplicate_vote2() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -489,7 +489,7 @@ async fn test_state_validation_failed_removes_proposal() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) @@ -499,9 +499,10 @@ async fn test_state_validation_failed_removes_proposal() { // by directly applying the proposal input, then manually sending // StateValidationFailed instead of letting the harness auto-respond. // We need to call consensus.apply directly to avoid auto StateVerified. - let proposal_input = test_data.views[1].proposal_input_consensus(&node_key); + let (proposal_input, vid_share_input) = test_data.views[1].proposal_input_consensus(&node_key); let mut outbox = Outbox::new(); harness.consensus.apply(proposal_input, &mut outbox); + harness.consensus.apply(vid_share_input, &mut outbox); harness.collected.extend(outbox.take()); // Send StateVerificationFailed — removes proposal @@ -539,14 +540,14 @@ async fn test_decide_requires_cert2() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -569,7 +570,7 @@ async fn test_vote2_missing_block_reconstructed() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) @@ -577,7 +578,7 @@ async fn test_vote2_missing_block_reconstructed() { // View 2: proposal + cert1, but NO block_reconstructed for view 2 harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness.apply(test_data.views[1].cert1_input()).await; @@ -595,7 +596,7 @@ async fn test_vote2_block_reconstructed_arrives_late() { let node_key = BLSPubKey::generated_from_seed_indexed([0; 32], 0).0; harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) @@ -603,7 +604,7 @@ async fn test_vote2_block_reconstructed_arrives_late() { // View 2: proposal + cert1 first (no block_reconstructed yet) harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness.apply(test_data.views[1].cert1_input()).await; @@ -627,7 +628,7 @@ async fn test_multi_view_chain_decide() { for view in &test_data.views { harness - .apply(view.proposal_input_consensus(&node_key)) + .apply_pair(view.proposal_input_consensus(&node_key)) .await; harness.apply(view.block_reconstructed_input()).await; harness.apply(view.cert1_input()).await; @@ -647,7 +648,7 @@ async fn test_timeout_prevents_vote1_but_allows_vote2() { // Process view 1 to establish state. harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) @@ -671,7 +672,7 @@ async fn test_timeout_prevents_vote1_but_allows_vote2() { // The proposal is still stored (inputs are processed), but vote1 is // suppressed because view 2 <= timeout_view. harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -702,7 +703,7 @@ async fn test_leader_sends_proposal() { let mut harness = ConsensusHarness::new(leader_index).await; harness - .apply(test_data.views[0].proposal_input_consensus(&leader_for_view_2)) + .apply_pair(test_data.views[0].proposal_input_consensus(&leader_for_view_2)) .await; harness.apply(test_data.views[0].cert1_input()).await; @@ -740,7 +741,7 @@ async fn test_propose_refuses_when_stored_proposal_differs_from_cert() { // triggers RequestBlockAndHeader for view 2 (auto-built by the harness // against the canonical view-1 proposal as parent). harness - .apply(test_data.views[0].proposal_input_consensus(&leader_for_view_2)) + .apply_pair(test_data.views[0].proposal_input_consensus(&leader_for_view_2)) .await; // Sanity: header for view 2 has been built off the canonical view-1 @@ -814,7 +815,7 @@ async fn test_leader_proposes_after_timeout() { // Build up locked_cert: process view 1 so cert1 sets locked_cert harness - .apply(test_data.views[0].proposal_input_consensus(&leader_for_view_3)) + .apply_pair(test_data.views[0].proposal_input_consensus(&leader_for_view_3)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) @@ -849,7 +850,7 @@ async fn test_non_leader_does_not_propose() { let mut harness = ConsensusHarness::new(non_leader_index).await; harness - .apply(test_data.views[0].proposal_input_consensus(&non_leader_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&non_leader_key)) .await; harness.apply(test_data.views[0].cert1_input()).await; @@ -870,7 +871,7 @@ async fn test_safety_rejects_proposal_below_lock() { // Process view 1 fully: proposal + block_reconstructed + cert1 → locked_qc = view 1 harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) @@ -879,7 +880,7 @@ async fn test_safety_rejects_proposal_below_lock() { // Process view 2 fully → locked_qc = view 2 harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -894,7 +895,7 @@ async fn test_safety_rejects_proposal_below_lock() { // Safety: genesis commitment != cert1(view 2) commitment = false // → Proposal rejected by is_safe, no RequestState emitted. harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; let state_requests_after = count_matching(harness.outputs(), is_request_state); @@ -915,7 +916,7 @@ async fn test_vote_after_timeout_cert() { // Process view 1 fully → locked_qc = view 1 harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) @@ -924,7 +925,7 @@ async fn test_vote_after_timeout_cert() { // Process view 2 proposal + block_reconstructed (need parent data for view 3) harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -953,7 +954,7 @@ async fn test_vote_after_timeout_cert() { // Process proposal for view 3. Its justify_qc is at view 2, which is // >= locked_qc at view 1 (liveness passes). Parent data for view 2 exists. harness - .apply(test_data.views[2].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[2].proposal_input_consensus(&node_key)) .await; assert!( @@ -979,7 +980,7 @@ async fn test_no_vote_after_timeout_for_proposal_below_lock() { // Process views 1-3 fully → locked_qc = cert1 for view 3 for i in 0..3 { harness - .apply(test_data.views[i].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[i].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[i].block_reconstructed_input()) @@ -1000,7 +1001,7 @@ async fn test_no_vote_after_timeout_for_proposal_below_lock() { // justify_qc commitment ≠ locked_qc commitment → false (safety fails) // → Proposal rejected by is_safe. harness - .apply(test_data.views[2].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[2].proposal_input_consensus(&node_key)) .await; assert_eq!( @@ -1019,13 +1020,13 @@ async fn test_decide_not_repeated_for_same_view() { // Full round for view 2 harness - .apply(test_data.views[0].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[0].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[0].block_reconstructed_input()) .await; harness - .apply(test_data.views[1].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[1].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[1].block_reconstructed_input()) @@ -1072,7 +1073,7 @@ async fn test_restart_does_not_redecide_anchor() { // Drive the first decide after the restart at the next view. harness - .apply(decider.proposal_input_consensus(&node_key)) + .apply_pair(decider.proposal_input_consensus(&node_key)) .await; harness.apply(decider.block_reconstructed_input()).await; harness.apply(decider.cert1_input()).await; @@ -1296,10 +1297,9 @@ async fn test_vote1_gated_on_storage() { let consensus = &mut harness.consensus; let mut outbox = Outbox::new(); - consensus.apply( - test_data.views[0].proposal_input_consensus(&node_key), - &mut outbox, - ); + let (proposal_input, vid_share_input) = test_data.views[0].proposal_input_consensus(&node_key); + consensus.apply(proposal_input, &mut outbox); + consensus.apply(vid_share_input, &mut outbox); consensus.apply( state_verified_input(&test_data.views[0].proposal.data, view), &mut outbox, @@ -1344,10 +1344,9 @@ async fn test_vote2_gated_on_vid_storage() { let consensus = &mut harness.consensus; let mut outbox = Outbox::new(); - consensus.apply( - test_data.views[0].proposal_input_consensus(&node_key), - &mut outbox, - ); + let (proposal_input, vid_share_input) = test_data.views[0].proposal_input_consensus(&node_key); + consensus.apply(proposal_input, &mut outbox); + consensus.apply(vid_share_input, &mut outbox); consensus.apply( state_verified_input(&test_data.views[0].proposal.data, view), &mut outbox, @@ -1401,10 +1400,9 @@ async fn test_pending_vote1_dropped_on_timeout() { let consensus = &mut harness.consensus; let mut outbox = Outbox::new(); - consensus.apply( - test_data.views[0].proposal_input_consensus(&node_key), - &mut outbox, - ); + let (proposal_input, vid_share_input) = test_data.views[0].proposal_input_consensus(&node_key); + consensus.apply(proposal_input, &mut outbox); + consensus.apply(vid_share_input, &mut outbox); consensus.apply( state_verified_input(&test_data.views[0].proposal.data, view), &mut outbox, @@ -1439,7 +1437,7 @@ async fn test_proposal_release_follows_storage() { let mut harness = ConsensusHarness::new(leader_index).await; harness - .apply(test_data.views[0].proposal_input_consensus(&leader_for_view_2)) + .apply_pair(test_data.views[0].proposal_input_consensus(&leader_for_view_2)) .await; harness.apply(test_data.views[0].cert1_input()).await; diff --git a/crates/hotshot/new-protocol/src/tests/epoch_change.rs b/crates/hotshot/new-protocol/src/tests/epoch_change.rs index 5f5ad5712f3..24811260a32 100644 --- a/crates/hotshot/new-protocol/src/tests/epoch_change.rs +++ b/crates/hotshot/new-protocol/src/tests/epoch_change.rs @@ -49,7 +49,7 @@ async fn run_views_full( for i in range { harness - .apply(test_data.views[i].proposal_input_consensus(node_key)) + .apply_pair(test_data.views[i].proposal_input_consensus(node_key)) .await; harness .apply(test_data.views[i].block_reconstructed_input()) @@ -430,10 +430,12 @@ async fn test_epoch_change_votes() { .clone(); harness - .apply(ConsensusInput::ProposalWithVidShare( - first_view.leader_public_key, - ProposalMessage::validated(signed_proposal), - vid_share, + .apply_pair(( + ConsensusInput::Proposal( + first_view.leader_public_key, + ProposalMessage::validated(signed_proposal), + ), + ConsensusInput::VidShare(vid_share), )) .await; @@ -460,7 +462,7 @@ async fn test_first_epoch_leader_proposes_without_drb() { // should propose — but maybe_propose also needs to skip the DRB check. for i in 0..7 { harness - .apply(test_data.views[i].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[i].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[i].block_reconstructed_input()) @@ -529,10 +531,12 @@ async fn test_second_epoch_leader_proposes_without_drb() { .expect("VID share not found") .clone(); harness - .apply(ConsensusInput::ProposalWithVidShare( - first_e2_view.leader_public_key, - ProposalMessage::validated(signed), - vid_share, + .apply_pair(( + ConsensusInput::Proposal( + first_e2_view.leader_public_key, + ProposalMessage::validated(signed), + ), + ConsensusInput::VidShare(vid_share), )) .await; harness @@ -544,7 +548,7 @@ async fn test_second_epoch_leader_proposes_without_drb() { // ---- Epoch 2 views 12-16 (blocks 12-16, before transition window) ---- for i in 11..16 { harness - .apply(test_data.views[i].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[i].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[i].block_reconstructed_input()) @@ -558,7 +562,7 @@ async fn test_second_epoch_leader_proposes_without_drb() { // Process view 17 (block 17, first block in transition window). // After cert1 for view 17, the leader for view 18 should propose. harness - .apply(test_data.views[16].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[16].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[16].block_reconstructed_input()) @@ -617,10 +621,12 @@ async fn test_epoch3_transition_requests_drb_for_future_epoch() { .expect("VID share not found") .clone(); harness - .apply(ConsensusInput::ProposalWithVidShare( - first_e2_view.leader_public_key, - ProposalMessage::validated(signed), - vid_share, + .apply_pair(( + ConsensusInput::Proposal( + first_e2_view.leader_public_key, + ProposalMessage::validated(signed), + ), + ConsensusInput::VidShare(vid_share), )) .await; harness @@ -632,7 +638,7 @@ async fn test_epoch3_transition_requests_drb_for_future_epoch() { // ---- Epoch 2 views 12-16 (before transition window) ---- for i in 11..16 { harness - .apply(test_data.views[i].proposal_input_consensus(&node_key)) + .apply_pair(test_data.views[i].proposal_input_consensus(&node_key)) .await; harness .apply(test_data.views[i].block_reconstructed_input()) @@ -662,10 +668,9 @@ async fn test_epoch3_transition_requests_drb_for_future_epoch() { .expect("VID share not found") .clone(); harness - .apply(ConsensusInput::ProposalWithVidShare( - v17.leader_public_key, - ProposalMessage::validated(signed), - vid_share, + .apply_pair(( + ConsensusInput::Proposal(v17.leader_public_key, ProposalMessage::validated(signed)), + ConsensusInput::VidShare(vid_share), )) .await;