Skip to main content

hotshot_task_impls/
helpers.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7use std::{collections::HashSet, sync::Arc, time::Instant};
8
9use async_broadcast::{Receiver, SendError, Sender};
10use committable::{Commitment, Committable};
11use hotshot_task::dependency::{Dependency, EventDependency};
12use hotshot_types::{
13    consensus::OuterConsensus,
14    data::{
15        EpochNumber, Leaf2, QuorumProposalWrapper, VidDisperseShare, ViewChangeEvidence2,
16        ViewNumber,
17    },
18    drb::DrbResult,
19    epoch_membership::EpochMembershipCoordinator,
20    event::{Event, EventType, LeafInfo},
21    message::{Proposal, UpgradeLock},
22    request_response::ProposalRequestPayload,
23    simple_certificate::{
24        CertificatePair, DaCertificate2, NextEpochQuorumCertificate2, QuorumCertificate2,
25        UpgradeCertificate,
26    },
27    simple_vote::HasEpoch,
28    stake_table::StakeTableEntries,
29    traits::{
30        BlockPayload, ValidatedState,
31        block_contents::BlockHeader,
32        election::Membership,
33        node_implementation::{NodeImplementation, NodeType},
34        signature_key::SignatureKey,
35        storage::Storage,
36    },
37    utils::{
38        Terminator, View, ViewInner, epoch_from_block_number, is_epoch_root, is_epoch_transition,
39        is_transition_block, option_epoch_from_block_number,
40    },
41    vote::{Certificate, HasViewNumber},
42};
43use hotshot_utils::anytrace::*;
44use time::OffsetDateTime;
45use tokio::time::timeout;
46use tracing::instrument;
47use versions::EPOCH_VERSION;
48
49use crate::{events::HotShotEvent, quorum_proposal_recv::ValidationInfo, request::REQUEST_TIMEOUT};
50
51/// Trigger a request to the network for a proposal for a view and wait for the response or timeout.
52#[instrument(skip_all)]
53#[allow(clippy::too_many_arguments)]
54pub(crate) async fn fetch_proposal<TYPES: NodeType>(
55    qc: &QuorumCertificate2<TYPES>,
56    event_sender: Sender<Arc<HotShotEvent<TYPES>>>,
57    event_receiver: Receiver<Arc<HotShotEvent<TYPES>>>,
58    membership_coordinator: EpochMembershipCoordinator<TYPES>,
59    consensus: OuterConsensus<TYPES>,
60    sender_public_key: TYPES::SignatureKey,
61    sender_private_key: <TYPES::SignatureKey as SignatureKey>::PrivateKey,
62    upgrade_lock: &UpgradeLock<TYPES>,
63    epoch_height: u64,
64) -> Result<(Leaf2<TYPES>, View<TYPES>)> {
65    let view_number = qc.view_number();
66    let leaf_commit = qc.data.leaf_commit;
67    // We need to be able to sign this request before submitting it to the network. Compute the
68    // payload first.
69    let signed_proposal_request = ProposalRequestPayload {
70        view_number,
71        key: sender_public_key,
72    };
73
74    // Finally, compute the signature for the payload.
75    let signature = TYPES::SignatureKey::sign(
76        &sender_private_key,
77        signed_proposal_request.commit().as_ref(),
78    )
79    .wrap()
80    .context(error!("Failed to sign proposal. This should never happen."))?;
81
82    tracing::info!("Sending proposal request for view {view_number}");
83
84    // First, broadcast that we need a proposal to the current leader
85    broadcast_event(
86        HotShotEvent::QuorumProposalRequestSend(signed_proposal_request, signature).into(),
87        &event_sender,
88    )
89    .await;
90
91    let mut rx = event_receiver.clone();
92    // Make a background task to await the arrival of the event data.
93    let Ok(Some(proposal)) =
94        // We want to explicitly timeout here so we aren't waiting around for the data.
95        timeout(REQUEST_TIMEOUT, async move {
96            // We want to iterate until the proposal is not None, or until we reach the timeout.
97            while let Ok(event) = rx.recv_direct().await {
98                if let HotShotEvent::QuorumProposalResponseRecv(quorum_proposal) = event.as_ref() {
99                    let leaf = Leaf2::from_quorum_proposal(&quorum_proposal.data);
100                    if leaf.view_number() == view_number && leaf.commit() == leaf_commit {
101                        return Some(quorum_proposal.clone());
102                    }
103                }
104            }
105            None
106        })
107        .await
108    else {
109        bail!("Request for proposal failed");
110    };
111
112    let view_number = proposal.data.view_number();
113    let justify_qc = proposal.data.justify_qc().clone();
114
115    let justify_qc_epoch = justify_qc.data.epoch();
116
117    let epoch_membership = membership_coordinator.stake_table_for_epoch(justify_qc_epoch)?;
118    let membership_stake_table = StakeTableEntries::from_iter(epoch_membership.stake_table()).0;
119    let membership_success_threshold = epoch_membership.success_threshold();
120
121    justify_qc
122        .is_valid_cert(
123            &membership_stake_table,
124            membership_success_threshold,
125            upgrade_lock,
126        )
127        .context(|e| warn!("Invalid justify_qc in proposal for view {view_number}: {e}"))?;
128
129    let mut consensus_writer = consensus.write().await;
130    let leaf = Leaf2::from_quorum_proposal(&proposal.data);
131    let state = Arc::new(
132        <TYPES::ValidatedState as ValidatedState<TYPES>>::from_header(proposal.data.block_header()),
133    );
134
135    if let Err(e) = consensus_writer.update_leaf(leaf.clone(), Arc::clone(&state), None) {
136        tracing::trace!("{e:?}");
137    }
138    let view = View {
139        view_inner: ViewInner::Leaf {
140            leaf: leaf.commit(),
141            state,
142            delta: None,
143            epoch: leaf.epoch(epoch_height),
144        },
145    };
146    Ok((leaf, view))
147}
148pub async fn handle_drb_result<TYPES: NodeType, I: NodeImplementation<TYPES>>(
149    membership: &TYPES::Membership,
150    epoch: EpochNumber,
151    storage: &I::Storage,
152    drb_result: DrbResult,
153) {
154    tracing::debug!("Calling store_drb_result for epoch {epoch}");
155    if let Err(e) = storage.store_drb_result(epoch, drb_result).await {
156        tracing::error!("Failed to store drb result for epoch {epoch}: {e}");
157    }
158
159    membership.add_drb_result(epoch, drb_result)
160}
161
162/// Verify the DRB result from the proposal for the next epoch if this is the last block of the
163/// current epoch.
164///
165/// Uses the result from `start_drb_task`.
166///
167/// Returns an error if we should not vote.
168pub(crate) async fn verify_drb_result<TYPES: NodeType, I: NodeImplementation<TYPES>>(
169    proposal: &QuorumProposalWrapper<TYPES>,
170    validation_info: &ValidationInfo<TYPES, I>,
171) -> Result<()> {
172    // Skip if this is not the expected block.
173    if validation_info.epoch_height == 0
174        || !is_epoch_transition(
175            proposal.block_header().block_number(),
176            validation_info.epoch_height,
177        )
178    {
179        tracing::debug!("Skipping DRB result verification");
180        return Ok(());
181    }
182
183    // #3967 REVIEW NOTE: Check if this is the right way to decide if we're doing epochs
184    // Alternatively, should we just return Err() if epochs aren't happening here? Or can we assume
185    // that epochs are definitely happening by virtue of getting here?
186    let epoch = option_epoch_from_block_number(
187        validation_info
188            .upgrade_lock
189            .epochs_enabled(proposal.view_number()),
190        proposal.block_header().block_number(),
191        validation_info.epoch_height,
192    );
193
194    let proposal_result = proposal
195        .next_drb_result()
196        .context(info!("Proposal is missing the next epoch's DRB result."))?;
197
198    if let Some(epoch_val) = epoch {
199        let current_epoch_membership = validation_info
200            .membership
201            .coordinator
202            .stake_table_for_epoch(epoch)
203            .context(warn!("No stake table for epoch {}", epoch_val))?;
204
205        let has_stake_current_epoch =
206            current_epoch_membership.has_stake(&validation_info.public_key);
207
208        if has_stake_current_epoch {
209            let computed_result = current_epoch_membership
210                .next_epoch()
211                .context(warn!("No stake table for epoch {}", epoch_val + 1))?
212                .get_epoch_drb()
213                .await
214                .clone()
215                .context(warn!("DRB result not found"))?;
216
217            ensure!(
218                proposal_result == computed_result,
219                warn!(
220                    "Our calculated DRB result is {computed_result:?}, which does not match the \
221                     proposed DRB result of {proposal_result:?}"
222                )
223            );
224        }
225
226        Ok(())
227    } else {
228        Err(error!("Epochs are not available"))
229    }
230}
231
232/// Handles calling add_epoch_root and sync_l1 on Membership if necessary.
233async fn decide_epoch_root<TYPES: NodeType, I: NodeImplementation<TYPES>>(
234    decided_leaf: &Leaf2<TYPES>,
235    epoch_height: u64,
236    membership: &EpochMembershipCoordinator<TYPES>,
237    storage: &I::Storage,
238    consensus: &OuterConsensus<TYPES>,
239) {
240    let decided_leaf = decided_leaf.clone();
241    let decided_block_number = decided_leaf.block_header().block_number();
242
243    // Skip if this is not the expected block.
244    if epoch_height != 0 && is_epoch_root(decided_block_number, epoch_height) {
245        let next_epoch_number =
246            EpochNumber::new(epoch_from_block_number(decided_block_number, epoch_height) + 2);
247
248        let start = Instant::now();
249        if let Err(e) = storage
250            .store_epoch_root(next_epoch_number, decided_leaf.block_header().clone())
251            .await
252        {
253            tracing::error!("Failed to store epoch root for epoch {next_epoch_number}: {e}");
254        }
255        tracing::info!("Time taken to store epoch root: {:?}", start.elapsed());
256
257        let membership = membership.clone();
258        let decided_block_header = decided_leaf.block_header().clone();
259        let storage = storage.clone();
260        let consensus = consensus.clone();
261
262        let consensus_reader = consensus.read().await;
263
264        drop(consensus_reader);
265
266        tokio::spawn(async move {
267            let membership_clone = membership.clone();
268
269            // First add the epoch root to `membership`
270            {
271                let start = Instant::now();
272                if let Err(e) = membership_clone.add_epoch_root(decided_block_header).await {
273                    tracing::error!("Failed to add epoch root for epoch {next_epoch_number}: {e}");
274                }
275                tracing::info!("Time taken to add epoch root: {:?}", start.elapsed());
276            }
277
278            let membership_clone = membership.clone();
279
280            let drb_result = membership_clone
281                .compute_drb_result(next_epoch_number, decided_leaf.clone())
282                .await;
283
284            let drb_result = match drb_result {
285                Ok(result) => result,
286                Err(e) => {
287                    tracing::error!("Failed to compute DRB result from decide: {e}");
288                    return;
289                },
290            };
291
292            let start = Instant::now();
293            handle_drb_result::<TYPES, I>(
294                membership.membership(),
295                next_epoch_number,
296                &storage,
297                drb_result,
298            )
299            .await;
300            tracing::info!("Time taken to handle drb result: {:?}", start.elapsed());
301        });
302    }
303}
304
305/// Helper type to give names and to the output values of the leaf chain traversal operation.
306#[derive(Debug)]
307pub struct LeafChainTraversalOutcome<TYPES: NodeType> {
308    /// The new locked view obtained from a 2 chain starting from the proposal's parent.
309    pub new_locked_view_number: Option<ViewNumber>,
310
311    /// The new decided view obtained from a 3 chain starting from the proposal's parent.
312    pub new_decided_view_number: Option<ViewNumber>,
313
314    /// The QC signing the latest new leaf, causing it to become committed.
315    ///
316    /// Only present if a new leaf chain has been decided.
317    pub committing_qc: Option<CertificatePair<TYPES>>,
318
319    /// A second QC extending the committing QC, causing the new leaf chain to become decided.
320    ///
321    /// This is only applicable in HotStuff2, and will be [`None`] prior to HotShot version 0.3.
322    /// HotStuff1 (HotShot < 0.3) uses a different commit rule, which is not captured in this type.
323    pub deciding_qc: Option<CertificatePair<TYPES>>,
324
325    /// The decided leaves with corresponding validated state and VID info.
326    pub leaf_views: Vec<LeafInfo<TYPES>>,
327
328    /// The transactions in the block payload for each leaf.
329    pub included_txns: Option<HashSet<Commitment<<TYPES as NodeType>::Transaction>>>,
330
331    /// The most recent upgrade certificate from one of the leaves.
332    pub decided_upgrade_cert: Option<UpgradeCertificate<TYPES>>,
333}
334
335/// We need Default to be implemented because the leaf ascension has very few failure branches,
336/// and when they *do* happen, we still return intermediate states. Default makes the burden
337/// of filling values easier.
338impl<TYPES: NodeType + Default> Default for LeafChainTraversalOutcome<TYPES> {
339    /// The default method for this type is to set all of the returned values to `None`.
340    fn default() -> Self {
341        Self {
342            new_locked_view_number: None,
343            new_decided_view_number: None,
344            committing_qc: None,
345            deciding_qc: None,
346            leaf_views: Vec::new(),
347            included_txns: None,
348            decided_upgrade_cert: None,
349        }
350    }
351}
352
353async fn update_metrics<TYPES: NodeType>(
354    consensus: &OuterConsensus<TYPES>,
355    leaf_views: &[LeafInfo<TYPES>],
356) {
357    let consensus_reader = consensus.read().await;
358    let now = OffsetDateTime::now_utc().unix_timestamp() as u64;
359
360    for leaf_view in leaf_views {
361        let proposal_timestamp = leaf_view.leaf.block_header().timestamp();
362
363        let Some(proposal_to_decide_time) = now.checked_sub(proposal_timestamp) else {
364            tracing::error!("Failed to calculate proposal to decide time: {proposal_timestamp}");
365            continue;
366        };
367        consensus_reader
368            .metrics
369            .proposal_to_decide_time
370            .add_point(proposal_to_decide_time as f64);
371        if let Some(txn_bytes) = leaf_view.leaf.block_payload().map(|p| p.txn_bytes()) {
372            consensus_reader
373                .metrics
374                .finalized_bytes
375                .add_point(txn_bytes as f64);
376        }
377    }
378}
379
380/// calculate the new decided leaf chain based on the rules of HotStuff 2
381///
382/// # Panics
383/// If the leaf chain contains no decided leaf while reaching a decided view, which should be
384/// impossible.
385#[allow(clippy::too_many_arguments)]
386pub async fn decide_from_proposal_2<TYPES: NodeType, I: NodeImplementation<TYPES>>(
387    proposal: &QuorumProposalWrapper<TYPES>,
388    consensus: OuterConsensus<TYPES>,
389    upgrade_lock: &UpgradeLock<TYPES>,
390    public_key: &TYPES::SignatureKey,
391    with_epochs: bool,
392    membership: &EpochMembershipCoordinator<TYPES>,
393    storage: &I::Storage,
394) -> LeafChainTraversalOutcome<TYPES> {
395    let mut res = LeafChainTraversalOutcome::default();
396    let consensus_reader = consensus.read().await;
397    let proposed_leaf = Leaf2::from_quorum_proposal(proposal);
398    res.new_locked_view_number = Some(proposed_leaf.justify_qc().view_number());
399
400    // If we don't have the proposals parent return early
401    let Some(parent_info) = consensus_reader
402        .parent_leaf_info(&proposed_leaf, public_key)
403        .await
404    else {
405        return res;
406    };
407    // Get the parents parent and check if it's consecutive in view to the parent, if so we can decided
408    // the grandparents view.  If not we're done.
409    let Some(grand_parent_info) = consensus_reader
410        .parent_leaf_info(&parent_info.leaf, public_key)
411        .await
412    else {
413        return res;
414    };
415    if grand_parent_info.leaf.view_number() + 1 != parent_info.leaf.view_number() {
416        return res;
417    }
418    res.committing_qc = Some(CertificatePair::for_parent(&parent_info.leaf));
419    res.deciding_qc = Some(CertificatePair::for_parent(&proposed_leaf));
420    let decided_view_number = grand_parent_info.leaf.view_number();
421    res.new_decided_view_number = Some(decided_view_number);
422    // We've reached decide, now get the leaf chain all the way back to the last decided view, not including it.
423    let old_anchor_view = consensus_reader.last_decided_view();
424    let mut current_leaf_info = Some(grand_parent_info);
425    let existing_upgrade_cert_reader = upgrade_lock.decided_upgrade_cert();
426    let mut txns = HashSet::new();
427    while current_leaf_info
428        .as_ref()
429        .is_some_and(|info| info.leaf.view_number() > old_anchor_view)
430    {
431        // unwrap is safe, we just checked that he option is some
432        let info = &mut current_leaf_info.unwrap();
433        // Check if there's a new upgrade certificate available.
434        if let Some(cert) = info.leaf.upgrade_certificate()
435            && info.leaf.upgrade_certificate() != existing_upgrade_cert_reader
436        {
437            if cert.data.decide_by < decided_view_number {
438                tracing::warn!("Failed to decide an upgrade certificate in time. Ignoring.");
439            } else {
440                tracing::info!("Reached decide on upgrade certificate: {cert:?}");
441                res.decided_upgrade_cert = Some(cert.clone());
442            }
443        }
444
445        // If the block payload is available for this leaf, include it in
446        // the leaf chain that we send to the client.
447        if let Some(payload) = consensus_reader
448            .saved_payloads()
449            .get(&info.leaf.view_number())
450        {
451            info.leaf
452                .fill_block_payload_unchecked(payload.as_ref().payload.clone());
453        }
454
455        if let Some(ref payload) = info.leaf.block_payload() {
456            for txn in payload.transaction_commitments(info.leaf.block_header().metadata()) {
457                txns.insert(txn);
458            }
459        }
460
461        current_leaf_info = consensus_reader
462            .parent_leaf_info(&info.leaf, public_key)
463            .await;
464        res.leaf_views.push(info.clone());
465    }
466
467    if !txns.is_empty() {
468        res.included_txns = Some(txns);
469    }
470
471    if with_epochs && res.new_decided_view_number.is_some() {
472        let Some(first_leaf) = res.leaf_views.first() else {
473            return res;
474        };
475        let epoch_height = consensus_reader.epoch_height;
476        consensus_reader
477            .metrics
478            .last_synced_block_height
479            .set(usize::try_from(first_leaf.leaf.height()).unwrap_or(0));
480        drop(consensus_reader);
481
482        for decided_leaf_info in &res.leaf_views {
483            decide_epoch_root::<TYPES, I>(
484                &decided_leaf_info.leaf,
485                epoch_height,
486                membership,
487                storage,
488                &consensus,
489            )
490            .await;
491        }
492        update_metrics(&consensus, &res.leaf_views).await;
493    }
494
495    res
496}
497
498/// Ascends the leaf chain by traversing through the parent commitments of the proposal. We begin
499/// by obtaining the parent view, and if we are in a chain (i.e. the next view from the parent is
500/// one view newer), then we begin attempting to form the chain. This is a direct impl from
501/// [HotStuff](https://arxiv.org/pdf/1803.05069) section 5:
502///
503/// > When a node b* carries a QC that refers to a direct parent, i.e., b*.justify.node = b*.parent,
504/// > we say that it forms a One-Chain. Denote by b'' = b*.justify.node. Node b* forms a Two-Chain,
505/// > if in addition to forming a One-Chain, b''.justify.node = b''.parent.
506/// > It forms a Three-Chain, if b'' forms a Two-Chain.
507///
508/// We follow this exact logic to determine if we are able to reach a commit and a decide. A commit
509/// is reached when we have a two chain, and a decide is reached when we have a three chain.
510///
511/// # Example
512/// Suppose we have a decide for view 1, and we then move on to get undecided views 2, 3, and 4. Further,
513/// suppose that our *next* proposal is for view 5, but this leader did not see info for view 4, so the
514/// justify qc of the proposal points to view 3. This is fine, and the undecided chain now becomes
515/// 2-3-5.
516///
517/// Assuming we continue with honest leaders, we then eventually could get a chain like: 2-3-5-6-7-8. This
518/// will prompt a decide event to occur (this code), where the `proposal` is for view 8. Now, since the
519/// lowest value in the 3-chain here would be 5 (excluding 8 since we only walk the parents), we begin at
520/// the first link in the chain, and walk back through all undecided views, making our new anchor view 5,
521/// and out new locked view will be 6.
522///
523/// Upon receipt then of a proposal for view 9, assuming it is valid, this entire process will repeat, and
524/// the anchor view will be set to view 6, with the locked view as view 7.
525///
526/// # Panics
527/// If the leaf chain contains no decided leaf while reaching a decided view, which should be
528/// impossible.
529#[allow(clippy::too_many_arguments)]
530pub async fn decide_from_proposal<TYPES: NodeType, I: NodeImplementation<TYPES>>(
531    proposal: &QuorumProposalWrapper<TYPES>,
532    consensus: OuterConsensus<TYPES>,
533    upgrade_lock: &UpgradeLock<TYPES>,
534    public_key: &TYPES::SignatureKey,
535    with_epochs: bool,
536    membership: &EpochMembershipCoordinator<TYPES>,
537    storage: &I::Storage,
538    epoch_height: u64,
539) -> LeafChainTraversalOutcome<TYPES> {
540    let consensus_reader = consensus.read().await;
541    let existing_upgrade_cert_reader = upgrade_lock.decided_upgrade_cert();
542    let view_number = proposal.view_number();
543    let parent_view_number = proposal.justify_qc().view_number();
544    let old_anchor_view = consensus_reader.last_decided_view();
545
546    let mut last_view_number_visited = view_number;
547    let mut current_chain_length = 0usize;
548    let mut res = LeafChainTraversalOutcome::default();
549
550    if let Err(e) = consensus_reader.visit_leaf_ancestors(
551        parent_view_number,
552        Terminator::Exclusive(old_anchor_view),
553        true,
554        |leaf, state, delta| {
555            // This is the core paper logic. We're implementing the chain in chained hotstuff.
556            if res.new_decided_view_number.is_none() {
557                // If the last view number is the child of the leaf we've moved to...
558                if last_view_number_visited == leaf.view_number() + 1 {
559                    last_view_number_visited = leaf.view_number();
560
561                    // The chain grows by one
562                    current_chain_length += 1;
563
564                    // We emit a locked view when the chain length is 2
565                    if current_chain_length == 2 {
566                        res.new_locked_view_number = Some(leaf.view_number());
567                        // The next leaf in the chain, if there is one, is decided, so this
568                        // leaf's justify_qc would become the QC for the decided chain.
569                        res.committing_qc = Some(CertificatePair::for_parent(leaf));
570                    } else if current_chain_length == 3 {
571                        // And we decide when the chain length is 3.
572                        res.new_decided_view_number = Some(leaf.view_number());
573                    }
574                } else {
575                    // There isn't a new chain extension available, so we signal to the callback
576                    // owner that we can exit for now.
577                    return false;
578                }
579            }
580
581            // Now, if we *have* reached a decide, we need to do some state updates.
582            if let Some(new_decided_view) = res.new_decided_view_number {
583                // First, get a mutable reference to the provided leaf.
584                let mut leaf = leaf.clone();
585
586                // Update the metrics
587                if leaf.view_number() == new_decided_view {
588                    consensus_reader
589                        .metrics
590                        .last_synced_block_height
591                        .set(usize::try_from(leaf.height()).unwrap_or(0));
592                }
593
594                // Check if there's a new upgrade certificate available.
595                if let Some(cert) = leaf.upgrade_certificate()
596                    && leaf.upgrade_certificate() != existing_upgrade_cert_reader
597                {
598                    if cert.data.decide_by < view_number {
599                        tracing::warn!(
600                            "Failed to decide an upgrade certificate in time. Ignoring."
601                        );
602                    } else {
603                        tracing::info!("Reached decide on upgrade certificate: {cert:?}");
604                        res.decided_upgrade_cert = Some(cert.clone());
605                    }
606                }
607                // If the block payload is available for this leaf, include it in
608                // the leaf chain that we send to the client.
609                if let Some(payload) = consensus_reader.saved_payloads().get(&leaf.view_number()) {
610                    leaf.fill_block_payload_unchecked(payload.as_ref().payload.clone());
611                }
612
613                // Get the VID share at the leaf's view number, corresponding to our key
614                // (if one exists)
615                let vid_share = consensus_reader
616                    .vid_shares()
617                    .get(&leaf.view_number())
618                    .and_then(|key_map| key_map.get(public_key))
619                    .and_then(|epoch_map| epoch_map.get(&leaf.epoch(epoch_height)))
620                    .map(|prop| prop.data.clone());
621
622                let state_cert = if leaf.with_epoch
623                    && is_epoch_root(
624                        leaf.block_header().block_number(),
625                        consensus_reader.epoch_height,
626                    ) {
627                    match consensus_reader.state_cert() {
628                        // Sanity check that the state cert is for the same view as the decided leaf
629                        Some(state_cert)
630                            if state_cert.light_client_state.view_number
631                                == leaf.view_number().u64() =>
632                        {
633                            Some(state_cert.clone())
634                        },
635                        _ => None,
636                    }
637                } else {
638                    None
639                };
640
641                // Add our data into a new `LeafInfo`
642                res.leaf_views.push(LeafInfo::new(
643                    leaf.clone(),
644                    Arc::clone(&state),
645                    delta.clone(),
646                    vid_share,
647                    state_cert,
648                ));
649                if let Some(ref payload) = leaf.block_payload() {
650                    res.included_txns = Some(
651                        payload
652                            .transaction_commitments(leaf.block_header().metadata())
653                            .into_iter()
654                            .collect::<HashSet<_>>(),
655                    );
656                }
657            }
658            true
659        },
660    ) {
661        tracing::debug!("Leaf ascension failed; error={e}");
662    }
663
664    let epoch_height = consensus_reader.epoch_height;
665    drop(consensus_reader);
666
667    if with_epochs && res.new_decided_view_number.is_some() {
668        for decided_leaf_info in &res.leaf_views {
669            decide_epoch_root::<TYPES, I>(
670                &decided_leaf_info.leaf,
671                epoch_height,
672                membership,
673                storage,
674                &consensus,
675            )
676            .await;
677        }
678    }
679
680    res
681}
682
683/// Gets the parent leaf and state from the parent of a proposal, returning an [`utils::anytrace::Error`] if not.
684#[instrument(skip_all)]
685#[allow(clippy::too_many_arguments)]
686pub(crate) async fn parent_leaf_and_state<TYPES: NodeType>(
687    event_sender: &Sender<Arc<HotShotEvent<TYPES>>>,
688    event_receiver: &Receiver<Arc<HotShotEvent<TYPES>>>,
689    membership: EpochMembershipCoordinator<TYPES>,
690    public_key: TYPES::SignatureKey,
691    private_key: <TYPES::SignatureKey as SignatureKey>::PrivateKey,
692    consensus: OuterConsensus<TYPES>,
693    upgrade_lock: &UpgradeLock<TYPES>,
694    parent_qc: &QuorumCertificate2<TYPES>,
695    epoch_height: u64,
696) -> Result<(Leaf2<TYPES>, Arc<<TYPES as NodeType>::ValidatedState>)> {
697    let consensus_reader = consensus.read().await;
698    // A `Da` or `Failed` entry in the state map doesn't carry the parent leaf
699    // and state, so it can't be proposed on; treat it like a missing view and
700    // fetch the proposal. This matters when we form the QC from unicast votes
701    // before validating the parent proposal ourselves: the DA proposal's
702    // entry would otherwise mask the fetch and abort our proposal.
703    let vsm_contains_parent_leaf = consensus_reader
704        .validated_state_map()
705        .get(&parent_qc.view_number())
706        .is_some_and(|view| view.leaf_and_state().is_some());
707    drop(consensus_reader);
708
709    if !vsm_contains_parent_leaf {
710        let _ = fetch_proposal(
711            parent_qc,
712            event_sender.clone(),
713            event_receiver.clone(),
714            membership,
715            consensus.clone(),
716            public_key.clone(),
717            private_key.clone(),
718            upgrade_lock,
719            epoch_height,
720        )
721        .await
722        .context(info!("Failed to fetch proposal"))?;
723    }
724
725    let consensus_reader = consensus.read().await;
726    let parent_view = consensus_reader
727        .validated_state_map()
728        .get(&parent_qc.view_number())
729        .context(debug!(
730            "Couldn't find parent view in state map, waiting for replica to see proposal; \
731             parent_view_number: {}",
732            *parent_qc.view_number()
733        ))?;
734
735    let (leaf_commitment, state) = parent_view.leaf_and_state().context(info!(
736        "Parent of high QC points to a view without a proposal; parent_view_number: {}, \
737         parent_view {:?}",
738        *parent_qc.view_number(),
739        parent_view
740    ))?;
741
742    if leaf_commitment != consensus_reader.high_qc().data().leaf_commit {
743        // NOTE: This happens on the genesis block
744        tracing::debug!(
745            "They don't equal: {:?}   {:?}",
746            leaf_commitment,
747            consensus_reader.high_qc().data().leaf_commit
748        );
749    }
750
751    let leaf = consensus_reader
752        .saved_leaves()
753        .get(&leaf_commitment)
754        .context(info!("Failed to find high QC of parent"))?;
755
756    Ok((leaf.clone(), Arc::clone(state)))
757}
758
759pub(crate) async fn update_high_qc<TYPES: NodeType, I: NodeImplementation<TYPES>>(
760    proposal: &Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
761    validation_info: &ValidationInfo<TYPES, I>,
762) -> Result<()> {
763    let in_transition_epoch = proposal
764        .data
765        .justify_qc()
766        .data
767        .block_number
768        .is_some_and(|bn| {
769            !is_transition_block(bn, validation_info.epoch_height)
770                && is_epoch_transition(bn, validation_info.epoch_height)
771                && bn % validation_info.epoch_height != 0
772        });
773    let justify_qc = proposal.data.justify_qc();
774    let maybe_next_epoch_justify_qc = proposal.data.next_epoch_justify_qc();
775    if !in_transition_epoch {
776        tracing::debug!(
777            "Storing high QC for view {:?} and height {:?}",
778            justify_qc.view_number(),
779            justify_qc.data.block_number
780        );
781        if let Err(e) = validation_info
782            .storage
783            .update_high_qc2(justify_qc.clone())
784            .await
785        {
786            bail!("Failed to store High QC, not voting; error = {e:?}");
787        }
788        if justify_qc
789            .data
790            .block_number
791            .is_some_and(|bn| is_epoch_root(bn, validation_info.epoch_height))
792        {
793            let Some(state_cert) = proposal.data.state_cert() else {
794                bail!("Epoch root QC has no state cert, not voting!");
795            };
796            if let Err(e) = validation_info
797                .storage
798                .update_state_cert(state_cert.clone())
799                .await
800            {
801                bail!(
802                    "Failed to store the light client state update certificate, not voting; error \
803                     = {:?}",
804                    e
805                );
806            }
807            validation_info
808                .consensus
809                .write()
810                .await
811                .update_state_cert(state_cert.clone())?;
812        }
813        if let Some(next_epoch_justify_qc) = maybe_next_epoch_justify_qc
814            && let Err(e) = validation_info
815                .storage
816                .update_next_epoch_high_qc2(next_epoch_justify_qc.clone())
817                .await
818        {
819            bail!("Failed to store next epoch High QC, not voting; error = {e:?}");
820        }
821    }
822    let mut consensus_writer = validation_info.consensus.write().await;
823    if let Some(next_epoch_justify_qc) = maybe_next_epoch_justify_qc {
824        if justify_qc
825            .data
826            .block_number
827            .is_some_and(|bn| is_transition_block(bn, validation_info.epoch_height))
828        {
829            consensus_writer.reset_high_qc(justify_qc.clone(), next_epoch_justify_qc.clone())?;
830            consensus_writer
831                .update_transition_qc(justify_qc.clone(), next_epoch_justify_qc.clone());
832            return Ok(());
833        }
834        consensus_writer.update_next_epoch_high_qc(next_epoch_justify_qc.clone())?;
835    }
836    consensus_writer.update_high_qc(justify_qc.clone())?;
837
838    Ok(())
839}
840
841async fn transition_qc<TYPES: NodeType, I: NodeImplementation<TYPES>>(
842    validation_info: &ValidationInfo<TYPES, I>,
843) -> Option<(
844    QuorumCertificate2<TYPES>,
845    NextEpochQuorumCertificate2<TYPES>,
846)> {
847    validation_info
848        .consensus
849        .read()
850        .await
851        .transition_qc()
852        .cloned()
853}
854
855pub(crate) async fn validate_epoch_transition_qc<TYPES: NodeType, I: NodeImplementation<TYPES>>(
856    proposal: &Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
857    validation_info: &ValidationInfo<TYPES, I>,
858) -> Result<()> {
859    let proposed_qc = proposal.data.justify_qc();
860    let Some(qc_block_number) = proposed_qc.data().block_number else {
861        bail!("Justify QC has no block number");
862    };
863    if !is_epoch_transition(qc_block_number, validation_info.epoch_height)
864        || qc_block_number % validation_info.epoch_height == 0
865    {
866        return Ok(());
867    }
868    let Some(next_epoch_qc) = proposal.data.next_epoch_justify_qc() else {
869        bail!("Next epoch justify QC is not present");
870    };
871    ensure!(
872        next_epoch_qc.data.leaf_commit == proposed_qc.data().leaf_commit,
873        "Next epoch QC has different leaf commit to justify QC"
874    );
875
876    if is_transition_block(qc_block_number, validation_info.epoch_height) {
877        // Height is epoch height - 2
878        ensure!(
879            transition_qc(validation_info)
880                .await
881                .is_none_or(|(qc, _)| qc.view_number() <= proposed_qc.view_number()),
882            "Proposed transition qc must have view number greater than or equal to previous \
883             transition QC"
884        );
885
886        validation_info
887            .consensus
888            .write()
889            .await
890            .update_transition_qc(proposed_qc.clone(), next_epoch_qc.clone());
891        // reset the high qc to the transition qc
892        update_high_qc(proposal, validation_info).await?;
893    } else {
894        // Height is either epoch height - 1 or epoch height
895        ensure!(
896            transition_qc(validation_info)
897                .await
898                .is_none_or(|(qc, _)| qc.view_number() < proposed_qc.view_number()),
899            "Transition block must have view number greater than previous transition QC"
900        );
901        ensure!(
902            proposal.data.view_change_evidence().is_none(),
903            "Second to last block and last block of epoch must directly extend previous block, Qc \
904             Block number: {qc_block_number}, Proposal Block number: {}",
905            proposal.data.block_header().block_number()
906        );
907        ensure!(
908            proposed_qc.view_number() + 1 == proposal.data.view_number()
909                || transition_qc(validation_info)
910                    .await
911                    .is_some_and(|(qc, _)| &qc == proposed_qc),
912            "Transition proposals must extend the previous view directly, or extend the previous \
913             transition block"
914        );
915    }
916    Ok(())
917}
918
919/// Validate the state and safety and liveness of a proposal then emit
920/// a `QuorumProposalValidated` event.
921///
922///
923/// # Errors
924/// If any validation or state update fails.
925#[allow(clippy::too_many_lines)]
926#[instrument(skip_all, fields(id = validation_info.id, view = *proposal.data.view_number()))]
927pub(crate) async fn validate_proposal_safety_and_liveness<
928    TYPES: NodeType,
929    I: NodeImplementation<TYPES>,
930>(
931    proposal: Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
932    parent_leaf: Leaf2<TYPES>,
933    validation_info: &ValidationInfo<TYPES, I>,
934    event_stream: Sender<Arc<HotShotEvent<TYPES>>>,
935    sender: TYPES::SignatureKey,
936) -> Result<()> {
937    let view_number = proposal.data.view_number();
938
939    let mut valid_epoch_transition = false;
940    if validation_info
941        .upgrade_lock
942        .version(proposal.data.justify_qc().view_number())
943        .is_ok_and(|v| v >= EPOCH_VERSION)
944    {
945        let Some(block_number) = proposal.data.justify_qc().data.block_number else {
946            bail!("Quorum Proposal has no block number but it's after the epoch upgrade");
947        };
948        if is_epoch_transition(block_number, validation_info.epoch_height) {
949            validate_epoch_transition_qc(&proposal, validation_info).await?;
950            valid_epoch_transition = true;
951        }
952    }
953
954    let proposed_leaf = Leaf2::from_quorum_proposal(&proposal.data);
955    ensure!(
956        proposed_leaf.parent_commitment() == parent_leaf.commit(),
957        "Proposed leaf does not extend the parent leaf."
958    );
959    let proposal_epoch = option_epoch_from_block_number(
960        validation_info.upgrade_lock.epochs_enabled(view_number),
961        proposed_leaf.height(),
962        validation_info.epoch_height,
963    );
964
965    let state = Arc::new(
966        <TYPES::ValidatedState as ValidatedState<TYPES>>::from_header(proposal.data.block_header()),
967    );
968
969    {
970        let mut consensus_writer = validation_info.consensus.write().await;
971        if let Err(e) = consensus_writer.update_leaf(proposed_leaf.clone(), state, None) {
972            tracing::trace!("{e:?}");
973        }
974
975        // Update our internal storage of the proposal. The proposal is valid, so
976        // we swallow this error and just log if it occurs.
977        if let Err(e) = consensus_writer.update_proposed_view(proposal.clone()) {
978            tracing::debug!("Internal proposal update failed; error = {e:#}");
979        };
980    }
981
982    UpgradeCertificate::validate(
983        proposal.data.upgrade_certificate(),
984        &validation_info.membership,
985        proposal_epoch,
986        &validation_info.upgrade_lock,
987    )
988    .await?;
989
990    // Validate that the upgrade certificate is re-attached, if we saw one on the parent
991    proposed_leaf.extends_upgrade(&parent_leaf, &validation_info.upgrade_lock)?;
992
993    let justify_qc = proposal.data.justify_qc().clone();
994    // Create a positive vote if either liveness or safety check
995    // passes.
996
997    {
998        let consensus_reader = validation_info.consensus.read().await;
999        // Epoch safety check:
1000        // The proposal is safe if
1001        // 1. the proposed block and the justify QC block belong to the same epoch or
1002        // 2. the justify QC is the eQC for the previous block
1003        let justify_qc_epoch = option_epoch_from_block_number(
1004            validation_info.upgrade_lock.epochs_enabled(view_number),
1005            parent_leaf.height(),
1006            validation_info.epoch_height,
1007        );
1008        ensure!(
1009            proposal_epoch == justify_qc_epoch
1010                || consensus_reader.check_eqc(&proposed_leaf, &parent_leaf),
1011            {
1012                error!(
1013                    "Failed epoch safety check \n Proposed leaf is {proposed_leaf:?} \n justify \
1014                     QC leaf is {parent_leaf:?}"
1015                )
1016            }
1017        );
1018
1019        // Make sure that the epoch transition proposal includes the next epoch QC
1020        if is_epoch_transition(parent_leaf.height(), validation_info.epoch_height)
1021            && validation_info.upgrade_lock.epochs_enabled(view_number)
1022        {
1023            ensure!(
1024                proposal.data.next_epoch_justify_qc().is_some(),
1025                "Epoch transition proposal does not include the next epoch justify QC. Do not \
1026                 vote!"
1027            );
1028        }
1029
1030        // Liveness check.
1031        let liveness_check =
1032            justify_qc.view_number() > consensus_reader.locked_view() || valid_epoch_transition;
1033
1034        // Safety check.
1035        // Check if proposal extends from the locked leaf.
1036        let outcome = consensus_reader.visit_leaf_ancestors(
1037            justify_qc.view_number(),
1038            Terminator::Inclusive(consensus_reader.locked_view()),
1039            false,
1040            |leaf, _, _| {
1041                // if leaf view no == locked view no then we're done, report success by
1042                // returning true
1043                leaf.view_number() != consensus_reader.locked_view()
1044            },
1045        );
1046        let safety_check = outcome.is_ok();
1047
1048        ensure!(safety_check || liveness_check, {
1049            if let Err(e) = outcome {
1050                broadcast_event(
1051                    Event {
1052                        view_number,
1053                        event: EventType::Error { error: Arc::new(e) },
1054                    },
1055                    &validation_info.output_event_stream,
1056                )
1057                .await;
1058            }
1059
1060            error!(
1061                "Failed safety and liveness check \n High QC is {:?}  Proposal QC is {:?}  Locked \
1062                 view is {:?}",
1063                consensus_reader.high_qc(),
1064                proposal.data,
1065                consensus_reader.locked_view()
1066            )
1067        });
1068    }
1069
1070    // We accept the proposal, notify the application layer
1071    broadcast_event(
1072        Event {
1073            view_number,
1074            event: EventType::QuorumProposal {
1075                proposal: proposal.clone(),
1076                sender,
1077            },
1078        },
1079        &validation_info.output_event_stream,
1080    )
1081    .await;
1082
1083    // Notify other tasks
1084    broadcast_event(
1085        Arc::new(HotShotEvent::QuorumProposalValidated(
1086            proposal.clone(),
1087            parent_leaf,
1088        )),
1089        &event_stream,
1090    )
1091    .await;
1092
1093    Ok(())
1094}
1095
1096/// Validates, from a given `proposal` that the view that it is being submitted for is valid when
1097/// compared to `cur_view` which is the highest proposed view (so far) for the caller. If the proposal
1098/// is for a view that's later than expected, that the proposal includes a timeout or view sync certificate.
1099///
1100/// # Errors
1101/// If any validation or view number check fails.
1102pub(crate) async fn validate_proposal_view_and_certs<
1103    TYPES: NodeType,
1104    I: NodeImplementation<TYPES>,
1105>(
1106    proposal: &Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
1107    validation_info: &ValidationInfo<TYPES, I>,
1108) -> Result<()> {
1109    let view_number = proposal.data.view_number();
1110    ensure!(
1111        view_number + 1 >= validation_info.consensus.read().await.cur_view(),
1112        "Proposal is from an older view {:?}",
1113        proposal.data
1114    );
1115
1116    // Validate the proposal's signature. This should also catch if the leaf_commitment does not equal our calculated parent commitment
1117    let mut membership = validation_info.membership.clone();
1118    proposal.validate_signature(&membership)?;
1119
1120    // Verify a timeout certificate OR a view sync certificate exists and is valid.
1121    if proposal.data.justify_qc().view_number() != view_number - 1 {
1122        let received_proposal_cert =
1123            proposal
1124                .data
1125                .view_change_evidence()
1126                .clone()
1127                .context(debug!(
1128                    "Quorum proposal for view {view_number} needed a timeout or view sync \
1129                     certificate, but did not have one",
1130                ))?;
1131
1132        match received_proposal_cert {
1133            ViewChangeEvidence2::Timeout(timeout_cert) => {
1134                ensure!(
1135                    timeout_cert.data().view == view_number - 1,
1136                    "Timeout certificate for view {view_number} was not for the immediately \
1137                     preceding view"
1138                );
1139                let timeout_cert_epoch = timeout_cert.data().epoch();
1140                membership = membership.get_new_epoch(timeout_cert_epoch)?;
1141
1142                let membership_stake_table =
1143                    StakeTableEntries::from_iter(membership.stake_table()).0;
1144                let membership_success_threshold = membership.success_threshold();
1145
1146                timeout_cert
1147                    .is_valid_cert(
1148                        &membership_stake_table,
1149                        membership_success_threshold,
1150                        &validation_info.upgrade_lock,
1151                    )
1152                    .context(|e| {
1153                        warn!("Timeout certificate for view {view_number} was invalid: {e}")
1154                    })?;
1155            },
1156            ViewChangeEvidence2::ViewSync(view_sync_cert) => {
1157                ensure!(
1158                    view_sync_cert.view_number == view_number,
1159                    "View sync cert view number {:?} does not match proposal view number {:?}",
1160                    view_sync_cert.view_number,
1161                    view_number
1162                );
1163
1164                let view_sync_cert_epoch = view_sync_cert.data().epoch();
1165                membership = membership.get_new_epoch(view_sync_cert_epoch)?;
1166
1167                let membership_stake_table =
1168                    StakeTableEntries::from_iter(membership.stake_table()).0;
1169                let membership_success_threshold = membership.success_threshold();
1170
1171                // View sync certs must also be valid.
1172                view_sync_cert
1173                    .is_valid_cert(
1174                        &membership_stake_table,
1175                        membership_success_threshold,
1176                        &validation_info.upgrade_lock,
1177                    )
1178                    .context(|e| warn!("Invalid view sync finalize cert provided: {e}"))?;
1179            },
1180        }
1181    }
1182
1183    // Validate the upgrade certificate -- this is just a signature validation.
1184    // Note that we don't do anything with the certificate directly if this passes; it eventually gets stored as part of the leaf if nothing goes wrong.
1185    {
1186        let epoch = option_epoch_from_block_number(
1187            proposal.data.epoch().is_some(),
1188            proposal.data.block_header().block_number(),
1189            validation_info.epoch_height,
1190        );
1191        UpgradeCertificate::validate(
1192            proposal.data.upgrade_certificate(),
1193            &validation_info.membership,
1194            epoch,
1195            &validation_info.upgrade_lock,
1196        )
1197        .await?;
1198    }
1199
1200    Ok(())
1201}
1202
1203/// Helper function to send events and log errors.
1204pub async fn broadcast_event<E: Clone + std::fmt::Display>(event: E, sender: &Sender<E>) {
1205    let broadcasting = sender.is_full().then(|| event.to_string());
1206    match sender.broadcast_direct(event).await {
1207        Ok(None) => (),
1208        Ok(Some(overflowed)) => {
1209            tracing::error!(
1210                broadcasting = broadcasting.as_deref().unwrap_or("<queue not full when checked>"),
1211                dropped = %overflowed,
1212                capacity = sender.capacity(),
1213                "Event sender queue overflow, oldest event dropped",
1214            );
1215        },
1216        Err(SendError(e)) => {
1217            tracing::warn!(event = %e, "Sending failed, event stream probably shutdown");
1218        },
1219    }
1220}
1221
1222/// Validates qc's signatures and, if provided, validates next_epoch_qc's signatures and whether it
1223/// corresponds to the provided high_qc.
1224pub async fn validate_qc_and_next_epoch_qc<TYPES: NodeType>(
1225    qc: &QuorumCertificate2<TYPES>,
1226    maybe_next_epoch_qc: Option<&NextEpochQuorumCertificate2<TYPES>>,
1227    consensus: &OuterConsensus<TYPES>,
1228    membership_coordinator: &EpochMembershipCoordinator<TYPES>,
1229    upgrade_lock: &UpgradeLock<TYPES>,
1230    epoch_height: u64,
1231) -> Result<()> {
1232    let cert = CertificatePair::new(qc.clone(), maybe_next_epoch_qc.cloned());
1233
1234    let mut epoch_membership = membership_coordinator.stake_table_for_epoch(cert.epoch())?;
1235
1236    let membership_stake_table = StakeTableEntries::from_iter(epoch_membership.stake_table()).0;
1237    let membership_success_threshold = epoch_membership.success_threshold();
1238
1239    if let Err(e) = cert.qc().is_valid_cert(
1240        &membership_stake_table,
1241        membership_success_threshold,
1242        upgrade_lock,
1243    ) {
1244        consensus.read().await.metrics.invalid_qc.update(1);
1245        return Err(warn!("Invalid certificate: {e}"));
1246    }
1247
1248    // Check the next epoch QC if required.
1249    if upgrade_lock.epochs_enabled(cert.view_number())
1250        && let Some(next_epoch_qc) = cert.verify_next_epoch_qc(epoch_height)?
1251    {
1252        epoch_membership = epoch_membership.next_epoch_stake_table()?;
1253        let membership_next_stake_table =
1254            StakeTableEntries::from_iter(epoch_membership.stake_table()).0;
1255        let membership_next_success_threshold = epoch_membership.success_threshold();
1256        next_epoch_qc
1257            .is_valid_cert(
1258                &membership_next_stake_table,
1259                membership_next_success_threshold,
1260                upgrade_lock,
1261            )
1262            .context(|e| warn!("Invalid next epoch certificate: {e}"))?;
1263    }
1264
1265    Ok(())
1266}
1267
1268/// Gets the second VID share, the current or the next epoch accordingly, from the shared consensus state;
1269/// makes sure it corresponds to the given DA certificate;
1270/// if it's not yet available, waits for it with the given timeout.
1271pub async fn wait_for_second_vid_share<TYPES: NodeType>(
1272    target_epoch: Option<EpochNumber>,
1273    vid_share: &Proposal<TYPES, VidDisperseShare<TYPES>>,
1274    da_cert: &DaCertificate2<TYPES>,
1275    consensus: &OuterConsensus<TYPES>,
1276    receiver: &Receiver<Arc<HotShotEvent<TYPES>>>,
1277    cancel_receiver: Receiver<()>,
1278    id: u64,
1279) -> Result<Proposal<TYPES, VidDisperseShare<TYPES>>> {
1280    tracing::debug!("getting the second VID share for epoch {:?}", target_epoch);
1281    let maybe_second_vid_share = consensus
1282        .read()
1283        .await
1284        .vid_shares()
1285        .get(&vid_share.data.view_number())
1286        .and_then(|key_map| key_map.get(vid_share.data.recipient_key()))
1287        .and_then(|epoch_map| epoch_map.get(&target_epoch))
1288        .cloned();
1289    if let Some(second_vid_share) = maybe_second_vid_share
1290        && ((target_epoch == da_cert.epoch()
1291            && second_vid_share.data.payload_commitment() == da_cert.data().payload_commit)
1292            || (target_epoch != da_cert.epoch()
1293                && Some(second_vid_share.data.payload_commitment())
1294                    == da_cert.data().next_epoch_payload_commit))
1295    {
1296        return Ok(second_vid_share);
1297    }
1298
1299    let receiver = receiver.clone();
1300    let da_cert_clone = da_cert.clone();
1301    let Some(event) = EventDependency::new(
1302        receiver,
1303        cancel_receiver,
1304        format!(
1305            "VoteDependency Second VID share for view {:?}, my id {:?}",
1306            vid_share.data.view_number(),
1307            id
1308        ),
1309        Box::new(move |event| {
1310            let event = event.as_ref();
1311            if let HotShotEvent::VidShareValidated(second_vid_share) = event {
1312                if target_epoch == da_cert_clone.epoch() {
1313                    second_vid_share.data.payload_commitment()
1314                        == da_cert_clone.data().payload_commit
1315                } else {
1316                    Some(second_vid_share.data.payload_commitment())
1317                        == da_cert_clone.data().next_epoch_payload_commit
1318                }
1319            } else {
1320                false
1321            }
1322        }),
1323    )
1324    .completed()
1325    .await
1326    else {
1327        return Err(warn!("Error while waiting for the second VID share."));
1328    };
1329    let HotShotEvent::VidShareValidated(second_vid_share) = event.as_ref() else {
1330        // this shouldn't happen
1331        return Err(warn!(
1332            "Received event is not VidShareValidated but we checked it earlier. Shouldn't be \
1333             possible."
1334        ));
1335    };
1336    Ok(second_vid_share.clone())
1337}
1338
1339pub async fn broadcast_view_change<TYPES: NodeType>(
1340    sender: &Sender<Arc<HotShotEvent<TYPES>>>,
1341    new_view_number: ViewNumber,
1342    epoch: Option<EpochNumber>,
1343    first_epoch: Option<(ViewNumber, EpochNumber)>,
1344) {
1345    let mut broadcast_epoch = epoch;
1346    if let Some((first_epoch_view, first_epoch)) = first_epoch
1347        && new_view_number == first_epoch_view
1348        && broadcast_epoch != Some(first_epoch)
1349    {
1350        broadcast_epoch = Some(first_epoch);
1351    }
1352    tracing::trace!("Sending ViewChange for view {new_view_number} and epoch {broadcast_epoch:?}");
1353    broadcast_event(
1354        Arc::new(HotShotEvent::ViewChange(new_view_number, broadcast_epoch)),
1355        sender,
1356    )
1357    .await
1358}
1359
1360#[cfg(test)]
1361mod tests {
1362    use super::broadcast_event;
1363
1364    #[derive(Clone, PartialEq, Eq, Debug)]
1365    enum Ev {
1366        A,
1367        B,
1368    }
1369
1370    impl std::fmt::Display for Ev {
1371        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1372            match self {
1373                Ev::A => write!(f, "A"),
1374                Ev::B => write!(f, "B"),
1375            }
1376        }
1377    }
1378
1379    /// On overflow, `broadcast_event` drops the oldest event and retains the newest
1380    /// without panicking. Regression guard for the `is_full()`-gated diagnostics path.
1381    #[tokio::test]
1382    async fn broadcast_event_overflow_retains_newest() {
1383        let (tx, mut rx) = async_broadcast::broadcast::<Ev>(1);
1384        rx.set_overflow(true);
1385
1386        broadcast_event(Ev::A, &tx).await; // queue: [A], now full
1387        broadcast_event(Ev::B, &tx).await; // full -> drops oldest (A), queue: [B]
1388
1389        // The receiver is first notified that exactly one message was dropped, ...
1390        assert!(matches!(
1391            rx.recv_direct().await,
1392            Err(async_broadcast::RecvError::Overflowed(1))
1393        ));
1394        // ... then it observes only the newest event.
1395        assert_eq!(rx.recv_direct().await.unwrap(), Ev::B);
1396        assert!(
1397            rx.try_recv().is_err(),
1398            "only the newest event should remain"
1399        );
1400    }
1401}