1use 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#[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 let signed_proposal_request = ProposalRequestPayload {
70 view_number,
71 key: sender_public_key,
72 };
73
74 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 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 let Ok(Some(proposal)) =
94 timeout(REQUEST_TIMEOUT, async move {
96 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
162pub(crate) async fn verify_drb_result<TYPES: NodeType, I: NodeImplementation<TYPES>>(
169 proposal: &QuorumProposalWrapper<TYPES>,
170 validation_info: &ValidationInfo<TYPES, I>,
171) -> Result<()> {
172 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 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
232async 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 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 {
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#[derive(Debug)]
307pub struct LeafChainTraversalOutcome<TYPES: NodeType> {
308 pub new_locked_view_number: Option<ViewNumber>,
310
311 pub new_decided_view_number: Option<ViewNumber>,
313
314 pub committing_qc: Option<CertificatePair<TYPES>>,
318
319 pub deciding_qc: Option<CertificatePair<TYPES>>,
324
325 pub leaf_views: Vec<LeafInfo<TYPES>>,
327
328 pub included_txns: Option<HashSet<Commitment<<TYPES as NodeType>::Transaction>>>,
330
331 pub decided_upgrade_cert: Option<UpgradeCertificate<TYPES>>,
333}
334
335impl<TYPES: NodeType + Default> Default for LeafChainTraversalOutcome<TYPES> {
339 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#[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 let Some(parent_info) = consensus_reader
402 .parent_leaf_info(&proposed_leaf, public_key)
403 .await
404 else {
405 return res;
406 };
407 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 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 let info = &mut current_leaf_info.unwrap();
433 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 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#[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 if res.new_decided_view_number.is_none() {
557 if last_view_number_visited == leaf.view_number() + 1 {
559 last_view_number_visited = leaf.view_number();
560
561 current_chain_length += 1;
563
564 if current_chain_length == 2 {
566 res.new_locked_view_number = Some(leaf.view_number());
567 res.committing_qc = Some(CertificatePair::for_parent(leaf));
570 } else if current_chain_length == 3 {
571 res.new_decided_view_number = Some(leaf.view_number());
573 }
574 } else {
575 return false;
578 }
579 }
580
581 if let Some(new_decided_view) = res.new_decided_view_number {
583 let mut leaf = leaf.clone();
585
586 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 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 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 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 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 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#[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 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 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 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 update_high_qc(proposal, validation_info).await?;
893 } else {
894 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#[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 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 proposed_leaf.extends_upgrade(&parent_leaf, &validation_info.upgrade_lock)?;
992
993 let justify_qc = proposal.data.justify_qc().clone();
994 {
998 let consensus_reader = validation_info.consensus.read().await;
999 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 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 let liveness_check =
1032 justify_qc.view_number() > consensus_reader.locked_view() || valid_epoch_transition;
1033
1034 let outcome = consensus_reader.visit_leaf_ancestors(
1037 justify_qc.view_number(),
1038 Terminator::Inclusive(consensus_reader.locked_view()),
1039 false,
1040 |leaf, _, _| {
1041 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 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 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
1096pub(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 let mut membership = validation_info.membership.clone();
1118 proposal.validate_signature(&membership)?;
1119
1120 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_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 {
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
1203pub 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
1222pub 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 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
1268pub 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 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 #[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; broadcast_event(Ev::B, &tx).await; assert!(matches!(
1391 rx.recv_direct().await,
1392 Err(async_broadcast::RecvError::Overflowed(1))
1393 ));
1394 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}