1mod proposals;
2
3use std::{
4 cmp::max,
5 collections::{BTreeMap, BTreeSet, btree_map::Entry},
6 marker::PhantomData,
7 num::NonZeroU64,
8 sync::Arc,
9};
10
11use committable::{Commitment, CommitmentBoundsArkless, Committable};
12use hotshot_contract_adapter::light_client::derive_signed_state_digest;
13use hotshot_types::{
14 data::{
15 BlockNumber, EpochNumber, Leaf2, VidCommitment, VidCommitment2, VidDisperseShare2,
16 ViewNumber,
17 },
18 drb::DrbResult,
19 epoch_membership::EpochMembershipCoordinator,
20 message::{Proposal as SignedProposal, UpgradeLock},
21 simple_certificate::{
22 LightClientStateUpdateCertificateV2, TimeoutEvidence, UpgradeCertificate,
23 UpgradeCertificate2, check_qc_state_cert_correspondence,
24 },
25 simple_vote::{
26 HasEpoch, LightClientStateUpdateVote2, LockView, QuorumData2, SimpleVote, TimeoutData2,
27 Vote2Data,
28 },
29 stake_table::HSStakeTable,
30 traits::{
31 BlockPayload,
32 block_contents::{BlockHeader, EncodeBytes},
33 node_implementation::NodeType,
34 signature_key::{
35 LCV2StateSignatureKey, LCV3StateSignatureKey, SignatureKey, StateSignatureKey,
36 },
37 },
38 utils::{is_epoch_root, is_epoch_transition, is_last_block},
39 vote::{Certificate, HasViewNumber},
40};
41use hotshot_utils::anytrace;
42use proposals::Proposals;
43use tracing::{debug, info, instrument, warn};
44
45use crate::{
46 block::BlockAndHeaderRequest,
47 cert_verifier::ValidCert,
48 coordinator::{GcScope, VID_RECONSTRUCT_GC_MARGIN},
49 helpers::{ViewEpoch, proposal_commitment, validated_state_cert},
50 logging::KeyPrefix,
51 message::{
52 CatchupEvidence, Certificate1, Certificate2, EpochChangeMessage, Proposal,
53 ProposalFetchRequest, ProposalMessage, ReVote, ReVoteMessage, TimeoutBallot, TimeoutVote,
54 Validated, Vote1, Vote2,
55 },
56 outbox::Outbox,
57 state::{StateRequest, StateResponse},
58 storage::{ActionKind, StorageOutput},
59 vid::ns_lens_match_metadata,
60};
61
62#[derive(Eq, PartialEq, Debug, Clone)]
63#[allow(clippy::large_enum_variant)]
64pub enum ConsensusInput<T: NodeType> {
65 BlockBuilt {
66 view: ViewNumber,
67 epoch: EpochNumber,
68 payload: T::BlockPayload,
69 metadata: <T::BlockPayload as BlockPayload<T>>::Metadata,
70 payload_commitment: VidCommitment,
71 },
72 BlockReconstructed {
76 view: ViewNumber,
77 payload_commitment: VidCommitment2,
78 payload: T::BlockPayload,
79 },
80 Certificate1(ValidCert<Certificate1<T>>),
81 Certificate2(ValidCert<Certificate2<T>>),
82 AdvanceView(ValidCert<Certificate1<T>>),
86 EpochRootCertificates {
90 cert1: ValidCert<Certificate1<T>>,
91 state_cert: LightClientStateUpdateCertificateV2<T>,
92 },
93 EpochChange(EpochChangeMessage<T, Validated>),
94 HeaderCreated(ViewNumber, Commitment<Leaf2<T>>, T::BlockHeader),
95 Proposal(T::SignatureKey, ProposalMessage<T, Validated>),
99 VidShare(VidDisperseShare2<T>),
101 FetchedProposal(ProposalMessage<T, Validated>),
102 StateValidated(StateResponse<T>),
103 StateValidationFailed(StateResponse<T>),
104 Stored(StorageOutput<T>),
105 Timeout(ViewNumber),
106 TimeoutCertificate(ValidCert<TimeoutEvidence<T>>),
107 TimeoutOneHonest(ViewNumber),
108 VidDisperseCreated(ViewNumber, VidCommitment2),
109 DrbResult(EpochNumber, DrbResult),
110 UpgradeCertificateFormed(ValidCert<UpgradeCertificate2<T>>),
112 ReVote(ReVoteMessage<T, Validated>),
113}
114
115#[derive(Eq, PartialEq, Debug, Clone)]
116pub enum ConsensusOutput<T: NodeType> {
117 RequestBlockAndHeader(BlockAndHeaderRequest<T>),
118 RequestState(StateRequest<T>),
119 RequestDrbResult(EpochNumber),
120 RecordAction(ViewNumber, ActionKind),
121 PersistProposal(SignedProposal<T, Proposal<T>>),
122 SendProposal(SignedProposal<T, Proposal<T>>),
123 SendTimeoutVote(TimeoutVote<T>, Option<CatchupEvidence<T>>),
124 SendReVote(ReVoteMessage<T, Validated>),
125 SendVote1(Vote1<T>),
126 SendVote2(Vote2<T>),
127 PersistHighQc(Certificate1<T>),
129 PersistBoundaryQc(Certificate1<T>),
130 SendTimeoutCertificate(TimeoutEvidence<T>, ViewNumber, EpochNumber),
131 SendCertificate1(Certificate1<T>),
132 SendCertificate2(Certificate2<T>),
135 SendEpochChange(EpochChangeMessage<T, Validated>),
136 RequestVidDisperse {
137 view: ViewNumber,
138 epoch: EpochNumber,
139 payload: T::BlockPayload,
140 metadata: <T::BlockPayload as BlockPayload<T>>::Metadata,
141 payload_commitment: VidCommitment2,
142 },
143 LeafDecided {
144 leaves: Vec<Leaf2<T>>,
145 cert1: Certificate1<T>,
148 cert2: Option<Certificate2<T>>,
149 vid_shares: Vec<Option<SignedProposal<T, VidDisperseShare2<T>>>>,
150 },
151 LockUpdated(ViewNumber),
152 ViewChanged(ViewNumber, EpochNumber),
153 ViewTimedOut(ViewNumber),
155 ProposalPaired {
157 proposal: SignedProposal<T, Proposal<T>>,
158 vid_share: VidDisperseShare2<T>,
159 },
160 ProposalValidated {
161 proposal: SignedProposal<T, Proposal<T>>,
162 sender: T::SignatureKey,
163 },
164 ProposalCertified(Proposal<T>),
170 RequestMissingProposal {
171 view: ViewNumber,
172 leaf_commit: Commitment<Leaf2<T>>,
173 },
174 RequestMissingPayload {
175 view: ViewNumber,
176 payload_commitment: VidCommitment2,
177 },
178 BlockPayloadReconstructed {
184 view: ViewNumber,
185 header: T::BlockHeader,
186 payload: Arc<T::BlockPayload>,
187 },
188 BroadcastVidShare(VidDisperseShare2<T>),
191 UpgradeDecided(UpgradeCertificate<T>),
193}
194
195type PairingKey = (ViewNumber, Option<EpochNumber>, VidCommitment2);
205
206type UnpairedProposals<T> =
207 BTreeMap<PairingKey, (<T as NodeType>::SignatureKey, ProposalMessage<T, Validated>)>;
208
209type UnpairedVidShares<T> = BTreeMap<PairingKey, VidDisperseShare2<T>>;
210
211type ProposalKey<T> = (ViewNumber, Commitment<Leaf2<T>>);
212
213pub(crate) const DECIDE_BUFFER: u64 = 20;
216
217pub(crate) const GC_MARGIN_VIEWS: NonZeroU64 = NonZeroU64::new(2).expect("2 > 0");
221
222const _: () = assert!(DECIDE_BUFFER >= VID_RECONSTRUCT_GC_MARGIN);
224
225fn cert1_key<T: NodeType>(cert: &Certificate1<T>) -> ViewEpoch {
226 ViewEpoch(cert.view_number(), LockView::of(cert).epoch)
227}
228
229fn cert2_key<T: NodeType>(cert: &Certificate2<T>) -> ViewEpoch {
230 ViewEpoch(cert.view_number(), cert.data.epoch)
231}
232
233pub struct Consensus<T: NodeType> {
234 proposals: Proposals<T>,
235 signed_proposals: BTreeMap<ProposalKey<T>, SignedProposal<T, Proposal<T>>>,
236 proposed_views: BTreeSet<ViewEpoch>,
237 vid_shares: BTreeMap<ViewNumber, VidDisperseShare2<T>>,
238 unpaired_proposals: UnpairedProposals<T>,
239 unpaired_vid_shares: UnpairedVidShares<T>,
240 states_verified: BTreeSet<ProposalKey<T>>,
241 blocks_reconstructed: BTreeSet<(ViewNumber, VidCommitment2)>,
242 blocks: BTreeMap<(ViewNumber, VidCommitment2), T::BlockPayload>,
245 certs1: BTreeMap<ViewEpoch, Certificate1<T>>,
246 certs2: BTreeMap<ViewEpoch, Certificate2<T>>,
247 timeout_certs: BTreeMap<ViewNumber, TimeoutEvidence<T>>,
248 locked_cert: Option<Certificate1<T>>,
249 headers: BTreeMap<(ViewNumber, Commitment<Leaf2<T>>), T::BlockHeader>,
250 leaves: BTreeMap<ProposalKey<T>, Leaf2<T>>,
251 decided_views: BTreeSet<ViewNumber>,
254 decide_floor_view: ViewNumber,
258 last_decided_view: ViewNumber,
259 last_decided_leaf: Leaf2<T>,
260 drb_results: BTreeMap<EpochNumber, DrbResult>,
261
262 voted_1_views: BTreeSet<ViewEpoch>,
263 voted_2_views: BTreeSet<ViewEpoch>,
264 sent_timeout_votes: BTreeSet<ViewEpoch>,
265
266 vote1_parent: BTreeMap<ViewNumber, ViewNumber>,
268
269 stored_proposals: BTreeMap<ViewNumber, Vec<Commitment<Leaf2<T>>>>,
271 stored_vids: BTreeSet<ViewNumber>,
272 stored_actions: BTreeSet<(ViewNumber, ActionKind)>,
273 requested_actions: BTreeSet<(ViewNumber, ActionKind)>,
274 stored_high_qc: Option<LockView>,
276
277 pending_vote1: BTreeMap<ViewEpoch, Vote1<T>>,
281 pending_vote2: BTreeMap<ViewEpoch, PendingVote2<T>>,
282 pending_proposal: BTreeMap<ViewEpoch, SignedProposal<T, Proposal<T>>>,
283 pending_revote: BTreeMap<ViewEpoch, ReVoteMessage<T, Validated>>,
284 pending_timeout_vote: BTreeMap<ViewEpoch, PendingTimeoutVote<T>>,
285
286 timeout_vote_bar: Option<ViewNumber>,
293
294 revote_commits: BTreeMap<ViewNumber, EpochNumber>,
299
300 revotes: BTreeMap<ViewNumber, ReVote<T>>,
302
303 revote_fetches: BTreeSet<(ViewNumber, RevoteFetch)>,
305
306 lock_payload_fetches: BTreeMap<(ViewNumber, VidCommitment2), ViewNumber>,
310
311 formed_upgrade_certificate: Option<ValidCert<UpgradeCertificate2<T>>>,
314
315 decided_upgrade_carrier: Option<ViewNumber>,
318
319 timeout_view: ViewNumber,
320 restart_barred_view: ViewNumber,
324 current_view: ViewNumber,
325 current_epoch: EpochNumber,
326
327 stake_table_coordinator: EpochMembershipCoordinator<T>,
330
331 public_key: T::SignatureKey,
332 private_key: <T::SignatureKey as SignatureKey>::PrivateKey,
333 state_private_key: <T::StateSignatureKey as StateSignatureKey>::StatePrivateKey,
334 stake_table_capacity: usize,
335 state_certs: BTreeMap<EpochNumber, LightClientStateUpdateCertificateV2<T>>,
336 node_id: KeyPrefix,
337 upgrade_lock: UpgradeLock<T>,
338
339 pub(crate) epoch_height: BlockNumber,
340
341 pub(crate) tracer: Option<crate::leader_trace::LeaderTracerHandle>,
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
346enum RevoteFetch {
347 Proposal,
348 Payload,
349}
350
351struct PendingVote2<T: NodeType> {
353 vote: Vote2<T>,
354 lock: LockView,
356 block_view: ViewNumber,
358}
359
360type PendingTimeoutVote<T> = (TimeoutVote<T>, Option<CatchupEvidence<T>>);
362
363enum Protocol {
365 Abort,
367 Continue,
369}
370
371#[derive(Debug, thiserror::Error)]
373enum SafetyError {
374 #[error(
375 "leaf commitment at locked view does not match locked certificate \
376 locked_commit={locked_commit} proposal_commit={proposal_commit}"
377 )]
378 LockedViewCommitmentMismatch {
379 locked_commit: String,
380 proposal_commit: String,
381 },
382 #[error(
383 "justify qc neither extends nor is newer than the locked certificate \
384 locked_view={locked_view} parent_commit={parent_commit} locked_commit={locked_commit}"
385 )]
386 UnsafeProposal {
387 locked_view: ViewNumber,
388 parent_commit: String,
389 locked_commit: String,
390 },
391 #[error("failed to compute justify qc data commitment: {0}")]
392 JustifyQcCommitment(#[source] anytrace::Error),
393 #[error("failed to compute locked certificate data commitment: {0}")]
394 LockedCertCommitment(#[source] anytrace::Error),
395}
396
397impl<T: NodeType> Consensus<T> {
398 #[allow(clippy::too_many_arguments)]
399 pub fn new<B>(
400 membership_coordinator: EpochMembershipCoordinator<T>,
401 public_key: T::SignatureKey,
402 private_key: <T::SignatureKey as SignatureKey>::PrivateKey,
403 state_private_key: <T::StateSignatureKey as StateSignatureKey>::StatePrivateKey,
404 stake_table_capacity: usize,
405 upgrade_lock: UpgradeLock<T>,
406 genesis_leaf: Leaf2<T>,
407 epoch_height: B,
408 ) -> Self
409 where
410 B: Into<BlockNumber>,
411 {
412 let last_decided_view = genesis_leaf.view_number();
413 Self {
414 proposals: Proposals::new(),
415 signed_proposals: BTreeMap::new(),
416 proposed_views: BTreeSet::new(),
417 blocks: BTreeMap::new(),
418 states_verified: BTreeSet::new(),
419 blocks_reconstructed: BTreeSet::new(),
420 certs1: BTreeMap::new(),
421 certs2: BTreeMap::new(),
422 timeout_certs: BTreeMap::new(),
423 locked_cert: None,
424 leaves: BTreeMap::new(),
425 decided_views: BTreeSet::from([last_decided_view]),
426 decide_floor_view: ViewNumber::genesis(),
427 last_decided_view,
428 last_decided_leaf: genesis_leaf,
429 headers: BTreeMap::new(),
430 drb_results: BTreeMap::new(),
431 node_id: KeyPrefix::from(&public_key),
432 public_key,
433 timeout_view: ViewNumber::genesis(),
434 restart_barred_view: ViewNumber::genesis(),
435 current_view: ViewNumber::genesis(),
436 current_epoch: EpochNumber::genesis(),
437 stake_table_coordinator: membership_coordinator,
438 voted_1_views: BTreeSet::new(),
439 sent_timeout_votes: BTreeSet::new(),
440 voted_2_views: BTreeSet::new(),
441 vote1_parent: BTreeMap::new(),
442 stored_proposals: BTreeMap::new(),
443 stored_vids: BTreeSet::new(),
444 stored_actions: BTreeSet::new(),
445 requested_actions: BTreeSet::new(),
446 stored_high_qc: None,
447 pending_vote1: BTreeMap::new(),
448 pending_vote2: BTreeMap::new(),
449 pending_proposal: BTreeMap::new(),
450 pending_revote: BTreeMap::new(),
451 pending_timeout_vote: BTreeMap::new(),
452 timeout_vote_bar: None,
453 revote_commits: BTreeMap::new(),
454 revotes: BTreeMap::new(),
455 revote_fetches: BTreeSet::new(),
456 lock_payload_fetches: BTreeMap::new(),
457 formed_upgrade_certificate: None,
458 decided_upgrade_carrier: None,
459 private_key,
460 state_private_key,
461 stake_table_capacity,
462 state_certs: BTreeMap::new(),
463 upgrade_lock,
464 vid_shares: BTreeMap::new(),
465 unpaired_proposals: BTreeMap::new(),
466 unpaired_vid_shares: BTreeMap::new(),
467 epoch_height: epoch_height.into(),
468 tracer: None,
469 }
470 }
471
472 pub fn public_key(&self) -> &T::SignatureKey {
473 &self.public_key
474 }
475 pub fn set_tracer(&mut self, tracer: Option<crate::leader_trace::LeaderTracerHandle>) {
476 self.tracer = tracer;
477 }
478
479 pub fn seed_parent(
493 &mut self,
494 cert1: Certificate1<T>,
495 proposal: Proposal<T>,
496 reconstructed: impl IntoIterator<Item = (ViewNumber, VidCommitment2)>,
497 ) {
498 if is_last_block(proposal.block_header.block_number(), *self.epoch_height) {
500 self.set_current_epoch_max(proposal.epoch + 1);
501 } else {
502 self.set_current_epoch_max(proposal.epoch);
503 }
504 self.bump_stored_high_qc(LockView::of(&cert1));
506 assert_eq! {
507 self.last_decided_leaf.view_number(),
508 proposal.view_number,
509 "the anchor proposal must be rebuilt from the leaf consensus was created with"
510 }
511 let anchor = self.last_decided_leaf.commit();
512 self.hold_proposal_under(proposal, anchor);
513 self.insert_cert1(cert1.clone());
514 self.locked_cert = Some(cert1);
515 for (view, commitment) in reconstructed {
516 self.blocks_reconstructed.insert((view, commitment));
517 }
518 }
519
520 pub fn seed_signed_proposals(
525 &mut self,
526 proposals: impl IntoIterator<Item = SignedProposal<T, Proposal<T>>>,
527 ) {
528 for signed in proposals {
529 let key = (signed.data.view_number, proposal_commitment(&signed.data));
530 self.seed_proposals([signed.data.clone()]);
531 self.signed_proposals.insert(key, signed);
532 }
533 }
534
535 pub(crate) fn seed_proposals(&mut self, proposals: impl IntoIterator<Item = Proposal<T>>) {
540 for proposal in proposals {
541 self.hold_proposal(proposal);
542 }
543 }
544
545 pub fn seed_locked_cert(&mut self, cert1: Certificate1<T>) {
551 let lock = LockView::of(&cert1);
552 self.bump_stored_high_qc(lock);
553 self.insert_cert1(cert1.clone());
554 if self
555 .locked_cert
556 .as_ref()
557 .is_none_or(|locked| LockView::of(locked) < lock)
558 {
559 self.set_current_epoch_max(lock.epoch);
561 self.locked_cert = Some(cert1);
562 }
563 }
564
565 pub fn seed_boundary_cert(&mut self, cert1: Certificate1<T>) {
570 if cert1.view_number() > self.decide_floor() {
571 self.insert_cert1(cert1);
572 }
573 }
574
575 pub fn seed_anchor_cert2(&mut self, cert2: Certificate2<T>) {
579 if cert2.data.leaf_commit == self.last_decided_leaf.commit() {
580 self.insert_cert2(cert2);
581 }
582 }
583
584 fn insert_cert1(&mut self, cert: Certificate1<T>) {
585 let view = cert.view_number();
586 if let Entry::Vacant(entry) = self.certs1.entry(cert1_key(&cert)) {
587 entry.insert(cert);
588 crate::trace_leader_event!(
589 self.tracer,
590 view,
591 crate::leader_trace::LeaderEvent::Cert1VMinus1Obtained
592 );
593 }
594 }
595
596 fn insert_cert2(&mut self, cert: Certificate2<T>) -> bool {
598 let view = cert.view_number();
599 match self.certs2.entry(cert2_key(&cert)) {
600 Entry::Vacant(entry) => {
601 entry.insert(cert);
602 crate::trace_leader_event!(
603 self.tracer,
604 view,
605 crate::leader_trace::LeaderEvent::Cert2VMinus1Obtained
606 );
607 true
608 },
609 Entry::Occupied(_) => false,
610 }
611 }
612
613 fn proposed_at(&self, view: ViewNumber, epoch: EpochNumber) -> bool {
614 if self.upgrade_lock.certificate_rule(view) {
615 self.proposed_views.contains(&ViewEpoch(view, epoch))
616 } else {
617 self.proposed_views
618 .range(ViewEpoch::at_view(view))
619 .next()
620 .is_some()
621 }
622 }
623
624 fn voted_1_at(&self, view: ViewNumber, epoch: EpochNumber) -> bool {
625 if self.upgrade_lock.certificate_rule(view) {
626 self.voted_1_views.contains(&ViewEpoch(view, epoch))
627 } else {
628 self.voted_1_views
629 .range(ViewEpoch::at_view(view))
630 .next()
631 .is_some()
632 }
633 }
634
635 fn voted_2_at(&self, view: ViewNumber, epoch: EpochNumber) -> bool {
636 if self.upgrade_lock.certificate_rule(view) {
637 self.voted_2_views.contains(&ViewEpoch(view, epoch))
638 } else {
639 self.voted_2_views
640 .range(ViewEpoch::at_view(view))
641 .next()
642 .is_some()
643 }
644 }
645
646 fn bump_stored_high_qc(&mut self, lock: LockView) {
648 if self.stored_high_qc.is_none_or(|cur| cur < lock) {
649 self.stored_high_qc = Some(lock);
650 }
651 }
652
653 fn high_qc_persisted(&self, required: LockView) -> bool {
655 self.stored_high_qc.is_some_and(|stored| stored >= required)
656 }
657
658 fn behind_current_epoch(&self, view: ViewNumber, epoch: EpochNumber) -> bool {
662 self.upgrade_lock.certificate_rule(view) && epoch < self.current_epoch
663 }
664
665 pub(crate) fn lock_view(&self) -> Option<LockView> {
666 self.locked_cert.as_ref().map(LockView::of)
667 }
668
669 fn certified_proposal(
675 &self,
676 view: ViewNumber,
677 leaf_commit: Commitment<Leaf2<T>>,
678 block_number: Option<u64>,
679 ) -> Option<(ViewNumber, &Proposal<T>)> {
680 let numbered = |proposal: &&Proposal<T>| {
681 block_number.is_none_or(|bn| bn == proposal.block_header.block_number())
682 };
683 if let Some(proposal) = self.proposals.get(view, leaf_commit).filter(numbered) {
684 return Some((view, proposal));
685 }
686 if !block_number.is_some_and(|bn| is_last_block(bn, *self.epoch_height)) {
687 return None;
688 }
689 self.proposals
690 .latest_before(view, leaf_commit)
691 .filter(numbered)
692 .map(|proposal| (proposal.view_number, proposal))
693 }
694
695 fn cert2_over(
697 &self,
698 from: ViewNumber,
699 leaf_commit: Commitment<Leaf2<T>>,
700 ) -> Option<&Certificate2<T>> {
701 self.certs2
702 .range(ViewEpoch::start(from)..)
703 .map(|(_, cert2)| cert2)
704 .find(|cert2| cert2.data.leaf_commit == leaf_commit)
705 }
706
707 fn parent_reconstructed(
717 &self,
718 parent_view: ViewNumber,
719 parent_block_commitment: VidCommitment2,
720 parent_leaf: Commitment<Leaf2<T>>,
721 ) -> bool {
722 self.blocks_reconstructed
723 .contains(&(parent_view, parent_block_commitment))
724 || self
725 .locked_cert
726 .as_ref()
727 .is_some_and(|lock| lock.data().leaf_commit == parent_leaf)
728 }
729
730 pub fn seed_state_cert(&mut self, state_cert: LightClientStateUpdateCertificateV2<T>) {
734 self.state_certs.insert(state_cert.epoch, state_cert);
735 }
736
737 #[cfg(test)]
738 pub(crate) fn state_cert_for_epoch(
739 &self,
740 epoch: EpochNumber,
741 ) -> Option<&LightClientStateUpdateCertificateV2<T>> {
742 self.state_certs.get(&epoch)
743 }
744
745 pub fn last_proposal_before(&self, view: ViewNumber) -> Option<&Proposal<T>> {
747 let v = self.proposals.range(..view).last()?.view_number;
748 self.proposal_at(v)
749 }
750
751 pub fn cert1_at(&self, view: ViewNumber) -> Option<&Certificate1<T>> {
753 self.certs1
754 .range(ViewEpoch::at_view(view))
755 .next_back()
756 .map(|(_, c)| c)
757 }
758
759 pub fn cert2_at(&self, view: ViewNumber) -> Option<&Certificate2<T>> {
761 self.certs2
762 .range(ViewEpoch::at_view(view))
763 .next_back()
764 .map(|(_, c)| c)
765 }
766
767 pub fn catchup_evidence(&self) -> Option<CatchupEvidence<T>> {
771 let tc = self.timeout_certs.last_key_value().map(|(_, tc)| tc);
772 match (tc, self.locked_cert.as_ref()) {
773 (Some(tc), Some(qc)) if qc.view_number() > tc.view_number() => {
774 Some(CatchupEvidence::Qc(qc.clone()))
775 },
776 (Some(tc), _) => Some(CatchupEvidence::from(tc)),
777 (None, Some(qc)) => Some(CatchupEvidence::Qc(qc.clone())),
778 (None, None) => None,
779 }
780 }
781
782 fn signed_vid_share(&self, p: &Proposal<T>) -> Option<SignedProposal<T, VidDisperseShare2<T>>> {
787 self.vid_shares
788 .get(&p.view_number)
789 .filter(|share| {
790 VidCommitment::V2(share.payload_commitment) == p.block_header.payload_commitment()
791 })?
792 .clone()
793 .to_proposal(&self.private_key)
794 }
795
796 pub fn signed_proposal_fetch_request(
797 &self,
798 view: ViewNumber,
799 ) -> Result<ProposalFetchRequest<T>, <T::SignatureKey as SignatureKey>::SignError> {
800 ProposalFetchRequest::new(view, self.public_key.clone(), &self.private_key)
801 }
802
803 pub fn timeout_cert_at(&self, view: ViewNumber) -> Option<&TimeoutEvidence<T>> {
807 self.timeout_certs.get(&view)
808 }
809
810 pub fn is_reconstructed(&self, view: ViewNumber, payload_commitment: VidCommitment2) -> bool {
811 self.blocks_reconstructed
812 .contains(&(view, payload_commitment))
813 }
814
815 pub(crate) fn decide_floor(&self) -> ViewNumber {
819 max(
820 self.last_decided_view.saturating_sub(DECIDE_BUFFER).into(),
821 self.decide_floor_view,
822 )
823 }
824
825 fn decided_or_past(&self, view: ViewNumber) -> bool {
832 view <= self.decide_floor() || self.decided_views.contains(&view)
833 }
834
835 #[instrument(level = "debug", skip_all, fields(node = %self.node_id, view = %input.view_number()))]
837 pub fn apply(&mut self, input: ConsensusInput<T>, outbox: &mut Outbox<ConsensusOutput<T>>) {
838 let view = if matches!(
843 &input,
844 ConsensusInput::DrbResult(..) | ConsensusInput::UpgradeCertificateFormed(..)
845 ) {
846 self.current_view
847 } else {
848 input.view_number()
849 };
850 let proto = match input {
851 ConsensusInput::Proposal(sender, proposal) => {
852 debug!(
853 sender = %KeyPrefix::from(&sender),
854 block = %proposal.proposal.data.block_header.block_number(),
855 epoch = %proposal.proposal.data.epoch,
856 "apply: proposal"
857 );
858 self.pair_proposal(sender, proposal, outbox)
859 },
860 ConsensusInput::VidShare(vid_share) => {
861 debug!("apply: vid share");
862 self.pair_vid_share(vid_share, outbox)
863 },
864 ConsensusInput::FetchedProposal(message) => {
865 debug!(
866 view = %message.proposal.data.view_number,
867 "apply: fetched proposal"
868 );
869 self.adopt_certified_proposal(message, outbox);
870 self.adopt_timeout_lock(self.current_view, outbox);
871 self.retry_revotes(outbox);
872 return;
873 },
874 ConsensusInput::Certificate1(certificate) => {
875 debug!(epoch = %certificate.epoch(), "apply: certificate1");
876 let protocol = self.handle_certificate1(certificate, outbox);
877 self.retry_later_certificates(view, outbox);
878 protocol
879 },
880 ConsensusInput::Certificate2(certificate) => {
881 debug!(epoch = %certificate.epoch(), "apply: certificate2");
882 self.handle_certificate2(certificate, outbox)
883 },
884 ConsensusInput::AdvanceView(certificate) => {
885 debug!(
886 view = %certificate.view_number(),
887 epoch = %certificate.epoch(),
888 "apply: advance view"
889 );
890 self.handle_advance_view(certificate, outbox)
891 },
892 ConsensusInput::EpochRootCertificates { cert1, state_cert } => {
893 info!(
894 epoch = %state_cert.epoch,
895 "apply: epoch root certificates"
896 );
897 self.state_certs.insert(state_cert.epoch, state_cert);
901 self.handle_certificate1(cert1, outbox)
902 },
903 ConsensusInput::TimeoutCertificate(certificate) => {
904 let timed_out_view = certificate.view_number();
905 let leader = self.leader_label(timed_out_view, certificate.epoch());
906 warn!(
907 view = %timed_out_view,
908 epoch = %certificate.epoch(),
909 %leader,
910 "apply: timeout certificate"
911 );
912 self.handle_timeout_certificate(certificate, outbox)
913 },
914 ConsensusInput::BlockReconstructed {
915 view,
916 payload_commitment,
917 payload,
918 } => {
919 debug!(%view, "apply: block reconstructed");
920 self.blocks_reconstructed.insert((view, payload_commitment));
921 self.blocks.insert((view, payload_commitment), payload);
922 let children: BTreeSet<ViewNumber> = self
930 .proposals
931 .range(view.max(self.timeout_view) + 1..)
932 .filter(|p| p.justify_qc.view_number() == view)
933 .map(|p| p.view_number)
934 .collect();
935 for child in children.into_iter().rev() {
936 self.maybe_vote_1(child, outbox);
937 if self
938 .voted_1_views
939 .range(ViewEpoch::at_view(child))
940 .next()
941 .is_some()
942 {
943 break;
944 }
945 }
946 self.retry_later_certificates(view, outbox);
947 Protocol::Continue
948 },
949 ConsensusInput::StateValidated(state_response) => {
950 debug!(view = %state_response.view, "apply: state validated");
951 self.states_verified
952 .insert((state_response.view, state_response.commitment));
953 Protocol::Continue
954 },
955 ConsensusInput::HeaderCreated(view, commitment, header) => {
956 debug!(%view, block = %header.block_number(), "apply: header created");
957 crate::trace_leader_event!(
958 self.tracer,
959 view,
960 crate::leader_trace::LeaderEvent::HeaderCreatedApplied
961 );
962 self.headers.insert((view, commitment), header);
963 Protocol::Continue
964 },
965 ConsensusInput::Stored(stored) => {
966 debug!(?stored, "apply: stored");
967 self.handle_stored(stored, outbox);
968 Protocol::Continue
969 },
970 ConsensusInput::StateValidationFailed(state_response) => {
971 let view = state_response.view;
972 let stored_proposal = self.proposals.get(view, state_response.commitment);
973 if let Some(proposal) = stored_proposal {
974 warn!(
975 %view,
976 block = %proposal.block_header.block_number(),
977 epoch = %proposal.epoch,
978 qc_view = %proposal.justify_qc.view_number(),
979 qc_epoch = ?proposal.justify_qc.epoch(),
980 "apply: state validation failed"
981 );
982 } else {
983 warn!(%view, "apply: state validation failed (no stored proposal)");
984 }
985 return;
986 },
987 ConsensusInput::Timeout(view)
988 if view > self.current_view.max(self.last_decided_view + 1) =>
989 {
990 debug!(
991 %view,
992 current_view = %self.current_view,
993 "ignoring timeout for a view not reached"
994 );
995 Protocol::Abort
996 },
997 ConsensusInput::Timeout(view) => {
998 let epoch = self.current_epoch;
999 let leader = self.leader_label(view, epoch);
1000 warn!(%view, %epoch, %leader, "apply: timeout");
1001 self.handle_timeout(view, outbox)
1002 },
1003 ConsensusInput::TimeoutOneHonest(view) => {
1004 let epoch = self.current_epoch;
1005 let leader = self.leader_label(view, epoch);
1006 warn!(%view, %epoch, %leader, "apply: timeout (one honest)");
1007 self.handle_timeout(view, outbox)
1008 },
1009 ConsensusInput::BlockBuilt {
1010 view,
1011 epoch,
1012 payload,
1013 metadata,
1014 payload_commitment,
1015 } => {
1016 debug!(%view, %epoch, "apply: block built");
1017 crate::trace_leader_event!(
1018 self.tracer,
1019 view,
1020 crate::leader_trace::LeaderEvent::BlockBuiltApplied
1021 );
1022 if let VidCommitment::V2(payload_commitment) = payload_commitment {
1023 outbox.push_back(ConsensusOutput::RequestVidDisperse {
1024 view,
1025 epoch,
1026 payload: payload.clone(),
1027 metadata,
1028 payload_commitment,
1029 });
1030 self.blocks.insert((view, payload_commitment), payload);
1031 } else {
1032 warn!(%view, %epoch, "block built with non-V2 payload commitment; ignoring");
1033 }
1034 Protocol::Continue
1035 },
1036 ConsensusInput::VidDisperseCreated(view, payload_commitment) => {
1037 debug!(%view, "apply: vid disperse created");
1038 self.blocks_reconstructed.insert((view, payload_commitment));
1039 Protocol::Continue
1040 },
1041 ConsensusInput::DrbResult(epoch, drb_result) => {
1042 info!(%epoch, "apply: drb result");
1043 self.drb_results.insert(epoch, drb_result);
1044 Protocol::Continue
1045 },
1046 ConsensusInput::EpochChange(epoch_change) => {
1047 info!(
1048 view = %epoch_change.cert1.view_number(),
1049 epoch = ?epoch_change.cert1.epoch().map(|e| *e),
1050 "apply: epoch change"
1051 );
1052 self.handle_epoch_change(epoch_change, outbox)
1053 },
1054 ConsensusInput::UpgradeCertificateFormed(cert) => {
1055 info!(
1056 view = %cert.view_number(),
1057 new_version = %cert.data.new_version,
1058 "apply: upgrade certificate formed"
1059 );
1060 self.handle_upgrade_certificate_formed(cert)
1061 },
1062 ConsensusInput::ReVote(message) => {
1063 info!(
1064 epoch = %message.revote.epoch,
1065 block_view = %message.revote.cert1.view_number(),
1066 "apply: re-vote request"
1067 );
1068 self.handle_revote(message, outbox)
1069 },
1070 };
1071
1072 if matches!(proto, Protocol::Abort) {
1073 debug!("aborting protocol");
1074 return;
1075 }
1076
1077 self.maybe_vote_1(view, outbox);
1078 self.maybe_vote_2_and_update_lock(view, outbox);
1079 self.maybe_decide(view, outbox);
1080 self.maybe_propose(view, outbox);
1081 self.maybe_propose(view + 1, outbox);
1083 self.maybe_propose(self.current_view, outbox);
1086 self.retry_revotes(outbox);
1087 }
1088
1089 pub fn last_decided_view(&self) -> ViewNumber {
1090 self.last_decided_view
1091 }
1092
1093 pub fn last_decided_leaf(&self) -> &Leaf2<T> {
1094 &self.last_decided_leaf
1095 }
1096
1097 pub fn undecided_leaves(&self) -> impl Iterator<Item = &Leaf2<T>> {
1098 let first = (
1099 self.last_decided_view + 1,
1100 Commitment::default_commitment_no_preimage(),
1101 );
1102 self.leaves
1103 .range(first..)
1104 .filter(|((v, c), _)| {
1105 self.proposal_at(*v)
1106 .is_some_and(|p| proposal_commitment(p) == *c)
1107 })
1108 .map(|(_, leaf)| leaf)
1109 }
1110
1111 pub fn current_view(&self) -> ViewNumber {
1112 self.current_view
1113 }
1114
1115 pub fn upgrade_lock(&self) -> &UpgradeLock<T> {
1116 &self.upgrade_lock
1117 }
1118
1119 pub fn current_epoch(&self) -> EpochNumber {
1120 self.current_epoch
1121 }
1122
1123 #[cfg(test)]
1124 pub fn set_view(&mut self, view: ViewNumber, epoch: EpochNumber) {
1125 self.current_view = view;
1126 self.current_epoch = epoch;
1127 }
1128
1129 pub fn resume_from_restart(
1135 &mut self,
1136 anchor_view: ViewNumber,
1137 restart_view: ViewNumber,
1138 last_actioned_view: ViewNumber,
1139 ) {
1140 let first_allowed = max(anchor_view + 1, max(restart_view, last_actioned_view + 1));
1141 let last_barred = first_allowed - 1;
1142 if last_barred > self.timeout_view {
1143 self.timeout_view = last_barred;
1144 }
1145 if last_barred > self.restart_barred_view {
1146 self.restart_barred_view = last_barred;
1147 }
1148 if anchor_view > self.decide_floor_view {
1149 self.decide_floor_view = anchor_view;
1150 }
1151 if self.upgrade_lock.timeout_epoch_bound(last_barred) {
1152 self.timeout_vote_bar = max(self.timeout_vote_bar, Some(last_barred));
1153 }
1154 for proposal in self.proposals.range(..=last_barred) {
1162 let parent_view = proposal.justify_qc.view_number();
1163 self.vote1_parent
1164 .entry(proposal.view_number)
1165 .and_modify(|recorded| *recorded = (*recorded).min(parent_view))
1166 .or_insert(parent_view);
1167 }
1168 let resume_view = self
1171 .stored_high_qc
1172 .map_or(anchor_view + 1, |lock| lock.view);
1173 self.set_current_view_max(resume_view);
1174 }
1175
1176 pub fn wants_proposal_for_view(&self, view: &ViewNumber, epoch: EpochNumber) -> bool {
1186 if *view <= self.decide_floor() {
1187 return false;
1188 }
1189 if !self.upgrade_lock.certificate_rule(*view)
1190 && self
1191 .lock_view()
1192 .is_some_and(|lock| lock > LockView { epoch, view: *view })
1193 {
1194 return false;
1195 }
1196 self.proposals.live(*view).is_none() || !self.vid_shares.contains_key(view)
1202 }
1203
1204 pub fn proposals(&self) -> &Proposals<T> {
1205 &self.proposals
1206 }
1207
1208 pub fn proposal_at(&self, view: ViewNumber) -> Option<&Proposal<T>> {
1214 self.cert2_at(view)
1215 .and_then(|c2| self.proposals.get(view, c2.data.leaf_commit))
1216 .or_else(|| {
1217 self.cert1_at(view)
1218 .and_then(|c1| self.proposals.get(view, c1.data.leaf_commit))
1219 })
1220 .or_else(|| self.proposals.live(view))
1221 .or_else(|| self.proposals.at(view).next())
1222 }
1223
1224 pub fn proposal_with_payload(&self, v: ViewNumber, c: VidCommitment2) -> Option<&Proposal<T>> {
1225 self.proposal_at(v)
1226 .filter(|p| p.block_header.payload_commitment() == VidCommitment::V2(c))
1227 .or_else(|| {
1228 self.proposals
1229 .at(v)
1230 .find(|p| p.block_header.payload_commitment() == VidCommitment::V2(c))
1231 })
1232 }
1233
1234 pub fn signed_proposal(
1235 &self,
1236 v: ViewNumber,
1237 c: Commitment<Leaf2<T>>,
1238 ) -> Option<&SignedProposal<T, Proposal<T>>> {
1239 self.signed_proposals.get(&(v, c))
1240 }
1241
1242 pub fn signed_proposal_at(&self, v: ViewNumber) -> Option<&SignedProposal<T, Proposal<T>>> {
1243 let c = proposal_commitment(self.proposal_at(v)?);
1244 self.signed_proposal(v, c)
1245 }
1246
1247 pub fn epoch_change_ending(
1249 &self,
1250 epoch: EpochNumber,
1251 ) -> Option<EpochChangeMessage<T, Validated>> {
1252 self.certs2
1253 .iter()
1254 .find_map(|(ViewEpoch(view, cert2_epoch), cert2)| {
1255 let block_number = cert2.data.block_number;
1256 if *cert2_epoch != epoch || !is_last_block(block_number, *self.epoch_height) {
1257 return None;
1258 }
1259 let (block_view, proposal) =
1260 self.certified_proposal(*view, cert2.data.leaf_commit, Some(block_number))?;
1261 let cert1 = self
1262 .certs1
1263 .get(&ViewEpoch(block_view, epoch))
1264 .filter(|cert1| cert1.data.leaf_commit == cert2.data.leaf_commit)?;
1265 Some(EpochChangeMessage::validated(
1266 cert1.clone(),
1267 cert2.clone(),
1268 proposal.clone(),
1269 ))
1270 })
1271 }
1272
1273 pub fn signed_proposals_for_fetch(
1280 &self,
1281 view: &ViewNumber,
1282 ) -> Vec<&SignedProposal<T, Proposal<T>>> {
1283 let certified = self
1284 .certs1
1285 .range(ViewEpoch::at_view(*view))
1286 .map(|(_, cert)| (cert.data.leaf_commit, cert.data.block_number))
1287 .chain(
1288 self.certs2
1289 .range(ViewEpoch::at_view(*view))
1290 .map(|(_, cert)| (cert.data.leaf_commit, Some(cert.data.block_number))),
1291 )
1292 .filter_map(|(leaf_commit, block_number)| {
1293 let (block_view, _) = self.certified_proposal(*view, leaf_commit, block_number)?;
1294 self.signed_proposals.get(&(block_view, leaf_commit))
1295 });
1296 let mut proposals: Vec<_> = self.signed_proposal_at(*view).into_iter().collect();
1297 for proposal in certified {
1298 if !proposals.iter().any(|p| p.data == proposal.data) {
1299 proposals.push(proposal);
1300 }
1301 }
1302 proposals
1303 }
1304
1305 pub fn gc(&mut self, scope: GcScope) {
1312 match scope {
1313 GcScope::Local(view) => {
1314 let c = Commitment::default_commitment_no_preimage();
1315 let floor = self.decide_floor();
1316 self.headers = self.headers.split_off(&(view, c));
1317 self.proposed_views = self.proposed_views.split_off(&ViewEpoch::start(floor));
1318 self.states_verified = self.states_verified.split_off(&(view, c));
1319 self.timeout_certs = self.timeout_certs.split_off(&view);
1320 self.voted_1_views = self.voted_1_views.split_off(&ViewEpoch::start(floor));
1321 self.sent_timeout_votes =
1322 self.sent_timeout_votes.split_off(&ViewEpoch::start(floor));
1323 self.voted_2_views = self.voted_2_views.split_off(&ViewEpoch::start(floor));
1324 if self
1325 .formed_upgrade_certificate
1326 .as_ref()
1327 .is_some_and(|cert| view > cert.data.decide_by)
1328 {
1329 self.formed_upgrade_certificate = None;
1330 }
1331 },
1332 GcScope::Decided(view) => {
1333 let vc = VidCommitment2::default();
1334 self.blocks = self.blocks.split_off(&(view, vc));
1335 self.blocks_reconstructed = self.blocks_reconstructed.split_off(&(view, vc));
1336 let keep_from = self.decide_floor();
1337 self.certs1 = self.certs1.split_off(&ViewEpoch::start(keep_from));
1338 self.certs2 = self.certs2.split_off(&ViewEpoch::start(keep_from));
1339 self.decided_views = self.decided_views.split_off(&keep_from);
1340 self.proposals.retain_from(keep_from);
1341 self.unpaired_proposals = self.unpaired_proposals.split_off(&(keep_from, None, vc));
1342 self.unpaired_vid_shares =
1343 self.unpaired_vid_shares.split_off(&(keep_from, None, vc));
1344 self.vote1_parent = self.vote1_parent.split_off(&keep_from);
1345 let c = Commitment::default_commitment_no_preimage();
1346 self.leaves = self.leaves.split_off(&(view, c));
1347 self.signed_proposals = self.signed_proposals.split_off(&(view, c));
1348 self.vid_shares = self.vid_shares.split_off(&view);
1349 self.stored_proposals = self.stored_proposals.split_off(&view);
1350 self.stored_vids = self.stored_vids.split_off(&view);
1351 self.stored_actions = self.stored_actions.split_off(&(view, ActionKind::Vote));
1352 self.requested_actions =
1353 self.requested_actions.split_off(&(view, ActionKind::Vote));
1354 self.pending_vote1 = self.pending_vote1.split_off(&ViewEpoch::start(view));
1355 self.pending_vote2 = self.pending_vote2.split_off(&ViewEpoch::start(view));
1356 self.pending_proposal = self.pending_proposal.split_off(&ViewEpoch::start(view));
1357 self.pending_revote = self.pending_revote.split_off(&ViewEpoch::start(view));
1358 self.pending_timeout_vote =
1359 self.pending_timeout_vote.split_off(&ViewEpoch::start(view));
1360 self.revotes = self.revotes.split_off(&keep_from);
1361 self.revote_commits = self.revote_commits.split_off(&keep_from);
1362 self.revote_fetches = self
1363 .revote_fetches
1364 .split_off(&(keep_from, RevoteFetch::Proposal));
1365 self.lock_payload_fetches = self
1366 .lock_payload_fetches
1367 .split_off(&(keep_from, VidCommitment2::default()));
1368 let epoch = EpochNumber::new(self.current_epoch.saturating_sub(1));
1369 self.drb_results = self.drb_results.split_off(&epoch);
1370 self.state_certs = self.state_certs.split_off(&epoch);
1371 },
1372 GcScope::Timeout(view) => {
1373 if self.keeps_payload_on_timeout(view) {
1374 return;
1375 }
1376 self.vid_shares.remove(&view);
1377 let vc = VidCommitment2::default();
1378 self.blocks
1379 .extract_if((view, vc)..(view + 1, vc), |_, _| true)
1380 .for_each(drop);
1381 },
1382 }
1383 }
1384
1385 pub(crate) fn keeps_payload_on_timeout(&self, view: ViewNumber) -> bool {
1390 self.cert1_at(view).is_some()
1391 || self.cert2_at(view).is_some()
1392 || self.proposals.at(view).any(|proposal| {
1393 is_last_block(proposal.block_header.block_number(), *self.epoch_height)
1394 })
1395 }
1396
1397 pub fn leader_of(&self, view: ViewNumber, epoch: EpochNumber) -> Option<T::SignatureKey> {
1398 match self
1399 .stake_table_coordinator
1400 .membership_for_epoch(Some(epoch))
1401 {
1402 Ok(stake_table) => match stake_table.leader(view) {
1403 Ok(leader) => Some(leader),
1404 Err(err) => {
1405 warn!(%view, %epoch, %err, "failed to get leader from stake table");
1406 None
1407 },
1408 },
1409 Err(err) => {
1410 warn!(%view, %epoch, %err, "failed to get stake table");
1411 None
1412 },
1413 }
1414 }
1415
1416 #[cfg(test)]
1422 pub(crate) fn force_set_proposal(&mut self, view: ViewNumber, proposal: Proposal<T>) {
1423 debug_assert_eq!(view, proposal.view_number);
1424 self.proposals.replace_view(proposal);
1425 }
1426
1427 fn pair_proposal(
1431 &mut self,
1432 sender: T::SignatureKey,
1433 proposal: ProposalMessage<T, Validated>,
1434 outbox: &mut Outbox<ConsensusOutput<T>>,
1435 ) -> Protocol {
1436 let view = proposal.view_number();
1437 let VidCommitment::V2(commit) = proposal.proposal.data.block_header.payload_commitment()
1438 else {
1439 warn!(%view, "proposal payload commitment is not V2, discarding");
1440 return Protocol::Abort;
1441 };
1442 let key = (view, Some(proposal.proposal.data.epoch), commit);
1443 let Some(vid_share) = self.unpaired_vid_shares.remove(&key) else {
1444 let leaf_commit = proposal_commitment(&proposal.proposal.data);
1445 self.unpaired_proposals.insert(key, (sender, proposal));
1446 if self.upgrade_lock.certificate_rule(view)
1449 && !self.decided_or_past(view)
1450 && self.cert2_over(view, leaf_commit).is_some()
1451 && self.adopt_unpaired_proposal(view, leaf_commit, None, outbox)
1452 {
1453 return Protocol::Continue;
1454 }
1455 return Protocol::Abort;
1456 };
1457 self.on_proposal_paired(sender, proposal, vid_share, outbox)
1458 }
1459
1460 fn pair_vid_share(
1464 &mut self,
1465 vid_share: VidDisperseShare2<T>,
1466 outbox: &mut Outbox<ConsensusOutput<T>>,
1467 ) -> Protocol {
1468 let key = (
1469 vid_share.view_number(),
1470 vid_share.epoch,
1471 vid_share.payload_commitment,
1472 );
1473 let Some((sender, proposal)) = self.unpaired_proposals.remove(&key) else {
1474 self.unpaired_vid_shares.insert(key, vid_share);
1475 return Protocol::Abort;
1476 };
1477 self.on_proposal_paired(sender, proposal, vid_share, outbox)
1478 }
1479
1480 fn on_proposal_paired(
1481 &mut self,
1482 sender: T::SignatureKey,
1483 proposal: ProposalMessage<T, Validated>,
1484 vid_share: VidDisperseShare2<T>,
1485 outbox: &mut Outbox<ConsensusOutput<T>>,
1486 ) -> Protocol {
1487 if !ns_lens_match_metadata(
1490 &vid_share.common,
1491 &proposal.proposal.data.block_header.metadata().encode(),
1492 ) {
1493 warn!(
1494 view = %proposal.view_number(), proposer = %KeyPrefix::from(&sender),
1495 "VID share namespace lengths disagree with the proposal's namespace table"
1496 );
1497 return Protocol::Abort;
1498 }
1499 outbox.push_back(ConsensusOutput::ProposalPaired {
1500 proposal: proposal.proposal.clone(),
1501 vid_share: vid_share.clone(),
1502 });
1503 self.handle_proposal_with_vid_share(sender, proposal, vid_share, outbox)
1504 }
1505
1506 #[instrument(level = "debug", skip_all)]
1507 fn handle_proposal_with_vid_share(
1508 &mut self,
1509 sender: T::SignatureKey,
1510 proposal: ProposalMessage<T, Validated>,
1511 vid_share: VidDisperseShare2<T>,
1512 outbox: &mut Outbox<ConsensusOutput<T>>,
1513 ) -> Protocol {
1514 let view = proposal.view_number();
1515 let proposer = KeyPrefix::from(&sender);
1516 let block_number = proposal.proposal.data.block_header.block_number();
1517 let qc_view = proposal.proposal.data.justify_qc.view_number();
1518
1519 if !self.wants_proposal_for_view(&view, proposal.proposal.data.epoch) {
1520 warn!(
1521 %view, %proposer, block = %block_number,
1522 epoch = %proposal.proposal.data.epoch, %qc_view,
1523 "proposal too old"
1524 );
1525 return Protocol::Abort;
1526 }
1527
1528 let signed_proposal = proposal.proposal.clone();
1529 let proposal = proposal.proposal.data;
1530 let epoch = proposal.epoch;
1531 let Some(qc_epoch) = proposal.justify_qc.epoch() else {
1533 warn!(
1534 %view, %proposer, block = %block_number, %epoch, %qc_view,
1535 "proposal has no epoch number"
1536 );
1537 return Protocol::Abort;
1538 };
1539
1540 if !self.upgrade_lock.certificate_rule(view)
1543 && let Err(err) = self.is_safe(&proposal)
1544 {
1545 warn!(
1546 %view, %proposer, block = %block_number, %epoch, %qc_view, %qc_epoch, %err,
1547 "proposal not safe"
1548 );
1549 return Protocol::Abort;
1550 }
1551
1552 let payload_size = Some(vid_share.payload_byte_len());
1553
1554 let commit = self.hold_proposal(proposal.clone());
1559 self.proposals.set_live(view, commit);
1560 self.signed_proposals
1561 .insert((view, commit), signed_proposal.clone());
1562 self.vid_shares.insert(view, vid_share);
1563 self.adopt_certified_drb(view);
1564 self.adopt_proposal_upgrade_certificate(&proposal);
1565 let commit_view = proposal.next_epoch_justify_qc.clone().and_then(|cert2| {
1574 let commit_view = cert2.view_number();
1575 (commit_view > self.decide_floor()).then(|| {
1576 self.insert_cert2(cert2);
1577 commit_view
1578 })
1579 });
1580 let parent_epoch = LockView::of(&proposal.justify_qc).epoch;
1583 self.advance_on(proposal.justify_qc.clone(), parent_epoch, outbox);
1584 if let Some(commit_view) = commit_view {
1588 self.maybe_decide(commit_view, outbox);
1589 }
1590 if let Some(tc) = proposal.view_change_evidence.clone()
1591 && !self.timeout_certs.contains_key(&view)
1592 {
1593 let tc_epoch = HasEpoch::epoch(&tc).unwrap_or(proposal.epoch);
1594 self.handle_timeout_certificate(ValidCert::new(tc, tc_epoch), outbox);
1595 }
1596
1597 self.request_parent_proposal_if_missing(&proposal, outbox);
1598
1599 if let Some(state_cert) = validated_state_cert(&proposal, *self.epoch_height) {
1600 self.state_certs
1601 .insert(state_cert.epoch, state_cert.clone());
1602 }
1603
1604 if proposal.epoch > EpochNumber::genesis()
1612 && is_epoch_transition(block_number, *self.epoch_height)
1613 {
1614 if let Some(drb) = self.drb_results.get(&(epoch + 1)) {
1615 if proposal
1616 .next_drb_result
1617 .is_none_or(|proposed_drb| drb != &proposed_drb)
1618 {
1619 warn!(
1620 %view, %proposer, block = %block_number, %epoch, %qc_view, %qc_epoch,
1621 "DRB result does not match proposal"
1622 );
1623 return Protocol::Abort;
1624 }
1625 } else {
1626 outbox.push_back(ConsensusOutput::RequestDrbResult(epoch + 1));
1627 }
1628 }
1629
1630 self.request_state(&proposal, payload_size, outbox);
1631
1632 crate::trace_leader_event!(
1633 self.tracer,
1634 view,
1635 crate::leader_trace::LeaderEvent::ProposalValidatedVMinus1
1636 );
1637 outbox.push_back(ConsensusOutput::ProposalValidated {
1638 proposal: signed_proposal,
1639 sender,
1640 });
1641
1642 self.request_block_and_header_if_next_leader(&proposal, outbox);
1643
1644 Protocol::Continue
1645 }
1646
1647 #[instrument(level = "debug", skip_all)]
1653 fn adopt_certified_proposal(
1654 &mut self,
1655 message: ProposalMessage<T, Validated>,
1656 outbox: &mut Outbox<ConsensusOutput<T>>,
1657 ) {
1658 let chain = self.parked_ancestors(message);
1659 if chain.len() > 1 {
1660 debug!(ancestors = chain.len() - 1, "adopting parked ancestors");
1661 }
1662 for message in chain.into_iter().rev() {
1663 let view = message.proposal.data.view_number;
1664 self.store_certified_proposal(message, outbox);
1665 self.maybe_decide(view, outbox);
1668 let extending_views: BTreeSet<ViewNumber> = self
1670 .proposals
1671 .range(view + 1..)
1672 .filter(|proposal| proposal.justify_qc.view_number() == view)
1673 .map(|proposal| proposal.view_number)
1674 .collect();
1675 for extending_view in extending_views {
1676 self.maybe_vote_1(extending_view, outbox);
1677 self.maybe_vote_2_and_update_lock(extending_view, outbox);
1678 self.maybe_decide(extending_view, outbox);
1679 }
1680 self.retry_later_certificates(view, outbox);
1681 self.maybe_propose(view + 1, outbox);
1682 }
1683 }
1684
1685 fn parked_ancestors(
1691 &self,
1692 message: ProposalMessage<T, Validated>,
1693 ) -> Vec<ProposalMessage<T, Validated>> {
1694 let mut chain = vec![message];
1695 loop {
1696 let proposal = &chain.last().expect("starts non-empty").proposal.data;
1697 let view = proposal.view_number;
1698 let parent_view = proposal.justify_qc.view_number();
1699 let leaf_commit = proposal.justify_qc.data().leaf_commit;
1700 if parent_view >= view
1701 || self.decided_or_past(parent_view)
1702 || self.proposals.contains(parent_view, leaf_commit)
1703 {
1704 break;
1705 }
1706 let Some(parent) = self.parked_proposal(parent_view, leaf_commit, None) else {
1707 break;
1708 };
1709 chain.push(parent);
1710 }
1711 chain
1712 }
1713
1714 fn store_certified_proposal(
1715 &mut self,
1716 message: ProposalMessage<T, Validated>,
1717 outbox: &mut Outbox<ConsensusOutput<T>>,
1718 ) {
1719 let signed_proposal = message.proposal;
1720 let proposal = signed_proposal.data.clone();
1721 let view = proposal.view_number;
1722 if self.decided_or_past(view) {
1723 debug!(%view, "certified proposal of a decided view or one past the floor; discarding");
1724 return;
1725 }
1726 let commit = proposal_commitment(&proposal);
1727 if self.proposals.contains(view, commit) {
1728 debug!(%view, "certified proposal already present; discarding");
1729 return;
1730 }
1731 self.signed_proposals
1732 .insert((view, commit), signed_proposal);
1733 self.hold_proposal(proposal.clone());
1734 self.request_parent_proposal_if_missing(&proposal, outbox);
1738 self.adopt_certified_drb(view);
1739 self.adopt_proposal_upgrade_certificate(&proposal);
1740
1741 let payload_size = self.payload_size_for(&proposal);
1742 self.request_state(&proposal, payload_size, outbox);
1743 self.request_block_and_header_if_next_leader(&proposal, outbox);
1744 outbox.push_back(ConsensusOutput::ProposalCertified(proposal));
1745 }
1746
1747 fn adopt_unpaired_proposal(
1753 &mut self,
1754 view: ViewNumber,
1755 leaf_commit: Commitment<Leaf2<T>>,
1756 block_number: Option<u64>,
1757 outbox: &mut Outbox<ConsensusOutput<T>>,
1758 ) -> bool {
1759 let Some(message) = self.parked_proposal(view, leaf_commit, block_number) else {
1760 return false;
1761 };
1762 debug!(%view, "adopting live proposal parked without a vid share");
1763 self.adopt_certified_proposal(message, outbox);
1764 true
1765 }
1766
1767 fn parked_proposal(
1774 &self,
1775 view: ViewNumber,
1776 leaf_commit: Commitment<Leaf2<T>>,
1777 block_number: Option<u64>,
1778 ) -> Option<ProposalMessage<T, Validated>> {
1779 let vc = VidCommitment2::default();
1780 let certified = |message: &&ProposalMessage<T, Validated>| {
1781 let proposal = &message.proposal.data;
1782 proposal_commitment(proposal) == leaf_commit
1783 && block_number.is_none_or(|bn| bn == proposal.block_header.block_number())
1784 };
1785 let parked = |range| {
1786 self.unpaired_proposals
1787 .range(range)
1788 .rev()
1789 .map(|(_, (_, message))| message)
1790 .find(certified)
1791 .cloned()
1792 };
1793 parked((view, None, vc)..(view + 1, None, vc)).or_else(|| {
1794 block_number
1795 .is_some_and(|bn| is_last_block(bn, *self.epoch_height))
1796 .then(|| parked((ViewNumber::genesis(), None, vc)..(view, None, vc)))
1797 .flatten()
1798 })
1799 }
1800
1801 fn request_state(
1802 &self,
1803 proposal: &Proposal<T>,
1804 payload_size: Option<u32>,
1805 outbox: &mut Outbox<ConsensusOutput<T>>,
1806 ) {
1807 outbox.push_back(ConsensusOutput::RequestState(StateRequest {
1808 view: proposal.view_number(),
1809 parent_view: proposal.justify_qc.view_number(),
1810 epoch: proposal.epoch,
1811 block: proposal.block_header.block_number().into(),
1812 proposal: proposal.clone(),
1813 parent_commitment: proposal.justify_qc.data().leaf_commit,
1814 payload_size,
1815 }));
1816 }
1817
1818 fn payload_size_for(&self, proposal: &Proposal<T>) -> Option<u32> {
1822 let view = proposal.view_number();
1823 let VidCommitment::V2(commitment) = proposal.block_header.payload_commitment() else {
1824 return None;
1825 };
1826 if let Some(share) = self.vid_shares.get(&view)
1827 && share.payload_commitment == commitment
1828 {
1829 return Some(share.payload_byte_len());
1830 }
1831 self.unpaired_vid_shares
1832 .get(&(view, Some(proposal.epoch), commitment))
1833 .map(|share| share.payload_byte_len())
1834 }
1835
1836 fn request_block_and_header_if_next_leader(
1837 &self,
1838 proposal: &Proposal<T>,
1839 outbox: &mut Outbox<ConsensusOutput<T>>,
1840 ) {
1841 let view = proposal.view_number();
1842 let epoch = if is_last_block(proposal.block_header.block_number(), *self.epoch_height) {
1843 proposal.epoch + 1
1844 } else {
1845 proposal.epoch
1846 };
1847 if self.is_leader(view + 1, epoch) {
1848 self.push_block_request(
1849 BlockAndHeaderRequest {
1850 view: view + 1,
1851 epoch,
1852 parent_proposal: proposal.clone(),
1853 },
1854 outbox,
1855 );
1856 }
1857 }
1858
1859 pub(crate) fn push_block_request(
1861 &self,
1862 request: BlockAndHeaderRequest<T>,
1863 outbox: &mut Outbox<ConsensusOutput<T>>,
1864 ) {
1865 crate::trace_leader_event!(
1866 self.tracer,
1867 request.view,
1868 crate::leader_trace::LeaderEvent::RequestBlockHeaderQueued
1869 );
1870 outbox.push_back(ConsensusOutput::RequestBlockAndHeader(request));
1871 }
1872
1873 fn request_missing_payloads(&self, outbox: &mut Outbox<ConsensusOutput<T>>) {
1891 let earliest = self
1892 .lock_view()
1893 .map_or_else(ViewNumber::genesis, |lock| lock.view)
1894 + 1;
1895 let latest = ViewNumber::from(self.current_view.saturating_sub(GC_MARGIN_VIEWS.get()));
1896
1897 let Some(child) = self.proposals.last() else {
1898 return;
1899 };
1900
1901 let view = child.justify_qc.view_number();
1902
1903 if view < earliest || view > latest {
1904 return;
1905 }
1906
1907 if self.decided_views.contains(&view) {
1912 return;
1913 }
1914
1915 let Some(proposal) = self.proposals.get(view, child.justify_qc.data.leaf_commit) else {
1916 return;
1917 };
1918
1919 let VidCommitment::V2(payload_commitment) = proposal.block_header.payload_commitment()
1920 else {
1921 return;
1922 };
1923
1924 if self
1925 .blocks_reconstructed
1926 .contains(&(view, payload_commitment))
1927 {
1928 return;
1929 }
1930
1931 outbox.push_back(ConsensusOutput::RequestMissingPayload {
1932 view,
1933 payload_commitment,
1934 });
1935 }
1936
1937 fn request_parent_proposal_if_missing(
1938 &mut self,
1939 proposal: &Proposal<T>,
1940 outbox: &mut Outbox<ConsensusOutput<T>>,
1941 ) {
1942 let parent_view = proposal.justify_qc.view_number();
1943 let leaf_commit = proposal.justify_qc.data().leaf_commit;
1944 if self.decided_or_past(parent_view) || self.proposals.contains(parent_view, leaf_commit) {
1945 return;
1946 }
1947 if parent_view < proposal.view_number
1950 && self.adopt_unpaired_proposal(parent_view, leaf_commit, None, outbox)
1951 {
1952 return;
1953 }
1954 warn!(
1955 view = %proposal.view_number,
1956 %parent_view,
1957 "parent proposal missing; requesting fetch"
1958 );
1959 outbox.push_back(ConsensusOutput::RequestMissingProposal {
1960 view: parent_view,
1961 leaf_commit,
1962 });
1963 }
1964
1965 #[instrument(level = "debug", skip_all)]
1966 fn handle_certificate1(
1967 &mut self,
1968 certificate: ValidCert<Certificate1<T>>,
1969 outbox: &mut Outbox<ConsensusOutput<T>>,
1970 ) -> Protocol {
1971 let view = certificate.view_number();
1972 if view <= self.decide_floor() {
1973 return Protocol::Continue;
1974 }
1975 if self.upgrade_lock.certificate_rule(view) {
1976 let epoch = certificate.epoch();
1977 self.advance_on(certificate.into_cert(), epoch, outbox);
1978 return Protocol::Continue;
1979 }
1980 self.insert_cert1(certificate.into_cert());
1981 self.adopt_certified_drb(view);
1982 Protocol::Continue
1983 }
1984
1985 #[instrument(level = "debug", skip_all)]
1986 fn handle_certificate2(
1987 &mut self,
1988 certificate: ValidCert<Certificate2<T>>,
1989 outbox: &mut Outbox<ConsensusOutput<T>>,
1990 ) -> Protocol {
1991 let view = certificate.view_number();
1992 if view <= self.decide_floor() {
1993 return Protocol::Continue;
1994 }
1995 if self.certs2.contains_key(&cert2_key(certificate.cert())) {
1996 return Protocol::Continue;
1997 }
1998 if !self.decided_views.contains(&view) {
2002 outbox.push_back(ConsensusOutput::SendCertificate2(
2003 certificate.cert().clone(),
2004 ));
2005 }
2006 let leaf_commit = certificate.data.leaf_commit;
2007 let block_number = Some(certificate.data.block_number);
2008 self.insert_cert2(certificate.into_cert());
2009 if !self.decided_or_past(view)
2010 && self
2011 .certified_proposal(view, leaf_commit, block_number)
2012 .is_none()
2013 && !self.adopt_unpaired_proposal(view, leaf_commit, block_number, outbox)
2014 {
2015 warn!(%view, "have certificate2 but no proposal; requesting fetch");
2016 outbox.push_back(ConsensusOutput::RequestMissingProposal { view, leaf_commit });
2017 }
2018 Protocol::Continue
2019 }
2020
2021 fn adopt_certified_drb(&mut self, view: ViewNumber) {
2026 let Some(proposal) = self
2027 .certs1
2028 .range(ViewEpoch::at_view(view))
2029 .rev()
2030 .find_map(|(_, cert)| self.proposals.get(view, cert.data.leaf_commit))
2031 else {
2032 return;
2033 };
2034 if proposal.epoch <= EpochNumber::genesis()
2036 || !is_epoch_transition(proposal.block_header.block_number(), *self.epoch_height)
2037 {
2038 return;
2039 }
2040 let Some(drb) = proposal.next_drb_result else {
2041 return;
2042 };
2043 let next_epoch = proposal.epoch + 1;
2044 if self.drb_results.contains_key(&next_epoch) {
2045 return;
2046 }
2047 self.drb_results.insert(next_epoch, drb);
2048 debug!(%view, %next_epoch, "adopted quorum-certified next_drb_result");
2049 }
2050
2051 fn handle_upgrade_certificate_formed(
2052 &mut self,
2053 cert: ValidCert<UpgradeCertificate2<T>>,
2054 ) -> Protocol {
2055 if self.upgrade_lock.decided_upgrade_cert().is_some() {
2056 return Protocol::Continue;
2057 }
2058 if self.current_view > cert.data.decide_by {
2059 debug!(
2060 view = %cert.view_number(),
2061 decide_by = %cert.data.decide_by,
2062 "upgrade certificate formed past its decide deadline; dropping"
2063 );
2064 return Protocol::Continue;
2065 }
2066 if self
2067 .formed_upgrade_certificate
2068 .as_ref()
2069 .is_none_or(|held| held.view_number() < cert.view_number())
2070 {
2071 self.formed_upgrade_certificate = Some(cert);
2072 }
2073 Protocol::Continue
2074 }
2075
2076 fn adopt_proposal_upgrade_certificate(&mut self, proposal: &Proposal<T>) {
2079 let Some(cert) = &proposal.upgrade_certificate else {
2080 return;
2081 };
2082 if self.upgrade_lock.decided_upgrade_cert().is_some() {
2083 return;
2084 }
2085 if self
2086 .formed_upgrade_certificate
2087 .as_ref()
2088 .is_some_and(|held| held.view_number() >= cert.view_number())
2089 {
2090 return;
2091 }
2092 debug!(
2093 view = %proposal.view_number,
2094 cert_view = %cert.view_number(),
2095 "adopted upgrade certificate from proposal"
2096 );
2097 self.formed_upgrade_certificate = Some(ValidCert::new(cert.clone(), cert.data.epoch));
2098 }
2099
2100 fn upgrade_certificate_to_attach(
2104 &self,
2105 view: ViewNumber,
2106 epoch: EpochNumber,
2107 ) -> Option<UpgradeCertificate2<T>> {
2108 let cert = self.formed_upgrade_certificate.as_ref()?;
2109 let attachable = self.upgrade_lock.decided_upgrade_cert().is_none()
2110 && view <= cert.data.decide_by
2111 && cert.epoch() == epoch;
2112 attachable.then(|| cert.cert().clone())
2113 }
2114
2115 fn maybe_decide_upgrade(
2123 &mut self,
2124 decided: &[Leaf2<T>],
2125 outbox: &mut Outbox<ConsensusOutput<T>>,
2126 ) {
2127 if self.decided_upgrade_carrier.is_none()
2130 && self.upgrade_lock.decided_upgrade_cert().is_some()
2131 {
2132 return;
2133 }
2134 for leaf in decided.iter().rev() {
2136 let Some(cert) = leaf.upgrade_certificate() else {
2137 continue;
2138 };
2139 if leaf.view_number() > cert.data.decide_by {
2140 warn!(
2141 view = %leaf.view_number(),
2142 decide_by = %cert.data.decide_by,
2143 "decided leaf carries an expired upgrade certificate; ignoring"
2144 );
2145 continue;
2146 }
2147 if self
2148 .decided_upgrade_carrier
2149 .is_some_and(|carrier| carrier <= leaf.view_number())
2150 {
2151 continue;
2152 }
2153 info!(
2154 target: "announce",
2155 view = %leaf.view_number(),
2156 new_version = %cert.data.new_version,
2157 first_view = %cert.data.new_version_first_view,
2158 "upgrade decided"
2159 );
2160 self.decided_upgrade_carrier = Some(leaf.view_number());
2161 self.upgrade_lock.set_decided_upgrade_cert(cert.clone());
2162 self.formed_upgrade_certificate = None;
2163 outbox.push_back(ConsensusOutput::UpgradeDecided(cert.clone()));
2164 }
2165 }
2166
2167 #[instrument(level = "debug", skip_all)]
2169 fn handle_advance_view(
2170 &mut self,
2171 cert1: ValidCert<Certificate1<T>>,
2172 outbox: &mut Outbox<ConsensusOutput<T>>,
2173 ) -> Protocol {
2174 let epoch = cert1.epoch();
2175 self.advance_on(cert1.into_cert(), epoch, outbox);
2176 Protocol::Continue
2177 }
2178
2179 fn advance_on(
2182 &mut self,
2183 cert1: Certificate1<T>,
2184 epoch: EpochNumber,
2185 outbox: &mut Outbox<ConsensusOutput<T>>,
2186 ) {
2187 let view = cert1.view_number();
2188 if view < self.current_view && view <= self.decide_floor() {
2189 return;
2190 }
2191
2192 self.insert_cert1(cert1);
2193 self.adopt_certified_drb(view);
2194
2195 self.maybe_vote_2_and_update_lock(view, outbox);
2197 self.maybe_decide(view, outbox);
2198
2199 let curr_view = self.current_view;
2204 self.set_current_view_max(view + 1);
2205 self.set_current_epoch_max(epoch);
2206
2207 if self.current_view != curr_view {
2208 outbox.push_back(ConsensusOutput::ViewChanged(
2209 self.current_view,
2210 self.current_epoch,
2211 ));
2212 }
2213 }
2214
2215 #[instrument(level = "debug", skip_all)]
2216 fn handle_timeout(
2217 &mut self,
2218 view: ViewNumber,
2219 outbox: &mut Outbox<ConsensusOutput<T>>,
2220 ) -> Protocol {
2221 let epoch = self.current_epoch;
2222 if view < self.current_view {
2223 debug!(
2224 %view,
2225 current_view = %self.current_view,
2226 current_epoch = %self.current_epoch,
2227 "ignoring timeout for stale view"
2228 );
2229 return Protocol::Abort;
2230 }
2231 let we_were_leader = self.is_leader(view, epoch);
2232 let proposed = self
2233 .proposed_views
2234 .range(ViewEpoch::at_view(view))
2235 .next()
2236 .is_some();
2237 if we_were_leader {
2238 if proposed {
2239 warn!(%view, %epoch, "timeout: we were the leader and did propose for this view");
2240 } else {
2241 let missing = self.missing_for_propose(view);
2242 let missing_str = if missing.is_empty() {
2243 "none".to_string()
2244 } else {
2245 missing.join(",")
2246 };
2247 warn!(
2248 %view, %epoch, missing = %missing_str,
2249 "timeout: we were the leader but did not propose"
2250 );
2251 }
2252 }
2253
2254 if !we_were_leader || proposed {
2258 if self
2259 .voted_1_views
2260 .range(ViewEpoch::at_view(view))
2261 .next()
2262 .is_some()
2263 {
2264 warn!(%view, %epoch, "timeout: we did vote1 for this view");
2265 } else {
2266 let missing = self.missing_for_vote1(view);
2267 let missing_str = if missing.is_empty() {
2268 "none".to_string()
2269 } else {
2270 missing.join(",")
2271 };
2272 warn!(
2273 %view, %epoch, missing = %missing_str,
2274 "timeout: we did not vote1 for this view"
2275 );
2276 }
2277 }
2278
2279 if let Some(cert1) = self.cert1_at(view) {
2283 let proposal_commit = self
2284 .proposals
2285 .get(view, cert1.data.leaf_commit)
2286 .map(|p| p.block_header.payload_commitment());
2287 match proposal_commit {
2288 Some(VidCommitment::V2(prop))
2289 if self.blocks_reconstructed.contains(&(view, prop)) =>
2290 {
2291 warn!(%view, %epoch, "timeout: have cert1 and matching reconstructed block");
2292 },
2293 Some(VidCommitment::V2(_)) => {
2294 warn!(
2295 %view, %epoch,
2296 "timeout: have cert1, but no reconstructed block matching the proposal"
2297 );
2298 },
2299 Some(_) => {
2300 warn!(
2303 %view, %epoch,
2304 "timeout: have cert1 but proposal payload commitment is not V2"
2305 );
2306 },
2307 None => {
2308 warn!(%view, %epoch, "timeout: have cert1 but no proposal stored");
2309 },
2310 }
2311 }
2312 self.timeout_view = max(self.timeout_view, view);
2313 self.request_missing_payloads(outbox);
2314
2315 if !self.staked_in_epoch(epoch) {
2316 return Protocol::Abort;
2317 }
2318
2319 if self.upgrade_lock.timeout_epoch_bound(view) {
2320 self.send_timeout_vote3(view, epoch, outbox);
2321 return Protocol::Abort;
2322 }
2323
2324 let vote = SimpleVote::create_signed_vote(
2325 TimeoutData2 {
2326 view,
2327 epoch: Some(epoch),
2328 },
2329 view,
2330 &self.public_key,
2331 &self.private_key,
2332 &self.upgrade_lock,
2333 )
2334 .map(TimeoutVote::V2);
2335 let vote = match vote {
2336 Ok(vote) => vote,
2337 Err(err) => {
2338 warn!(%view, %err, "failed to create timeout vote");
2339 return Protocol::Abort;
2340 },
2341 };
2342 let evidence = self.catchup_evidence();
2343 self.send_timeout_vote(vote, evidence, outbox);
2344 Protocol::Abort
2345 }
2346
2347 fn send_timeout_vote3(
2354 &mut self,
2355 view: ViewNumber,
2356 epoch: EpochNumber,
2357 outbox: &mut Outbox<ConsensusOutput<T>>,
2358 ) {
2359 self.timeout_vote_bar = max(self.timeout_vote_bar, Some(view));
2360 let lock = self
2362 .locked_cert
2363 .clone()
2364 .filter(|cert| cert.view_number() != ViewNumber::genesis());
2365 let vote = match TimeoutBallot::sign(
2366 view,
2367 epoch,
2368 lock,
2369 &self.public_key,
2370 &self.private_key,
2371 &self.upgrade_lock,
2372 ) {
2373 Ok(ballot) => TimeoutVote::V3(ballot),
2374 Err(err) => {
2375 warn!(%view, %err, "failed to create timeout vote");
2376 return;
2377 },
2378 };
2379 let evidence = self.catchup_evidence();
2380 self.sent_timeout_votes.insert(ViewEpoch(view, epoch));
2382 if self.stored_actions.contains(&(view, ActionKind::Timeout)) {
2383 self.send_timeout_vote(vote, evidence, outbox);
2384 } else {
2385 self.request_action(view, ActionKind::Timeout, outbox);
2386 self.pending_timeout_vote
2387 .insert(ViewEpoch(view, epoch), (vote, evidence));
2388 }
2389 }
2390
2391 fn release_timeout_vote(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
2392 if !self.stored_actions.contains(&(view, ActionKind::Timeout)) {
2393 return;
2394 }
2395 let pending: Vec<ViewEpoch> = self
2396 .pending_timeout_vote
2397 .range(ViewEpoch::at_view(view))
2398 .map(|(key, _)| *key)
2399 .collect();
2400 for key in pending {
2401 if let Some((vote, evidence)) = self.pending_timeout_vote.remove(&key) {
2402 self.send_timeout_vote(vote, evidence, outbox);
2403 }
2404 }
2405 }
2406
2407 fn send_timeout_vote(
2408 &mut self,
2409 vote: TimeoutVote<T>,
2410 evidence: Option<CatchupEvidence<T>>,
2411 outbox: &mut Outbox<ConsensusOutput<T>>,
2412 ) {
2413 let epoch = vote.epoch().unwrap_or(self.current_epoch);
2414 self.sent_timeout_votes
2415 .insert(ViewEpoch(vote.view_number(), epoch));
2416 outbox.push_back(ConsensusOutput::SendTimeoutVote(vote, evidence));
2417 }
2418
2419 fn sent_timeout_vote(&self, view: ViewNumber, epoch: Option<EpochNumber>) -> bool {
2422 match epoch {
2423 Some(epoch) => self.sent_timeout_votes.contains(&ViewEpoch(view, epoch)),
2424 None => self
2425 .sent_timeout_votes
2426 .range(ViewEpoch::at_view(view))
2427 .next()
2428 .is_some(),
2429 }
2430 }
2431
2432 #[instrument(level = "debug", skip_all)]
2433 fn handle_timeout_certificate(
2434 &mut self,
2435 certificate: ValidCert<TimeoutEvidence<T>>,
2436 outbox: &mut Outbox<ConsensusOutput<T>>,
2437 ) -> Protocol {
2438 let timed_out_view = certificate.view_number();
2439 let view = timed_out_view + 1;
2440 if view < self.current_view {
2441 debug!(
2442 %view,
2443 current_view = %self.current_view,
2444 "ignoring stale timeout certificate"
2445 );
2446 return Protocol::Abort;
2447 }
2448 if certificate.binds_epoch() != self.upgrade_lock.timeout_epoch_bound(timed_out_view) {
2449 warn!(
2450 %timed_out_view,
2451 "timeout certificate has the wrong form for its version"
2452 );
2453 return Protocol::Abort;
2454 }
2455 if let Some(stored) = self.timeout_certs.get(&view).map(HasEpoch::epoch) {
2456 if certificate.binds_epoch() && stored < Some(certificate.epoch()) {
2457 self.set_current_epoch_max(certificate.epoch());
2458 debug!(
2459 %view,
2460 epoch = %self.current_epoch,
2461 "adopting the epoch of a later certificate"
2462 );
2463 self.timeout_certs.insert(view, certificate.into_cert());
2464 self.adopt_timeout_lock(view, outbox);
2465 } else {
2466 debug!(%view, "duplicate timeout certificate; already applied");
2467 }
2468 return Protocol::Continue;
2469 }
2470 let forward = self.current_view <= timed_out_view
2473 && !self.sent_timeout_vote(
2474 timed_out_view,
2475 certificate.binds_epoch().then(|| certificate.epoch()),
2476 );
2477 self.timeout_certs.insert(view, certificate.cert().clone());
2478 self.set_current_view_max(view);
2479 if certificate.binds_epoch() {
2480 self.set_current_epoch_max(certificate.epoch());
2481 }
2482 self.request_missing_payloads(outbox);
2483 outbox.push_back(ConsensusOutput::ViewChanged(view, self.current_epoch));
2484 outbox.push_back(ConsensusOutput::ViewTimedOut(timed_out_view));
2485 if forward {
2486 outbox.push_back(ConsensusOutput::SendTimeoutCertificate(
2487 certificate.into_cert(),
2488 view,
2489 self.current_epoch,
2490 ));
2491 }
2492 self.adopt_timeout_lock(view, outbox);
2493 Protocol::Continue
2494 }
2495
2496 fn adopt_timeout_lock(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
2504 let Some(cert) = self
2505 .timeout_certs
2506 .get(&view)
2507 .and_then(|tc| tc.timeout_lock().recent().cloned())
2508 else {
2509 return;
2510 };
2511 let lock = LockView::of(&cert);
2512 if lock.view <= self.decide_floor() || self.lock_view().is_some_and(|own| own >= lock) {
2513 return;
2514 }
2515 let leaf_commit = cert.data.leaf_commit;
2516 let block_number = cert.data.block_number;
2517 self.insert_cert1(cert);
2518 self.adopt_certified_drb(lock.view);
2519 match self.certified_proposal(lock.view, leaf_commit, block_number) {
2520 None => {
2521 if !self.adopt_unpaired_proposal(lock.view, leaf_commit, block_number, outbox) {
2522 debug!(%view, %lock, "timeout certificate lock without its block; requesting fetch");
2523 outbox.push_back(ConsensusOutput::RequestMissingProposal {
2524 view: lock.view,
2525 leaf_commit,
2526 });
2527 }
2528 },
2529 Some((block_view, proposal)) => {
2530 if let VidCommitment::V2(payload_commitment) =
2531 proposal.block_header.payload_commitment()
2532 && !self
2533 .blocks_reconstructed
2534 .contains(&(block_view, payload_commitment))
2535 && self
2536 .lock_payload_fetches
2537 .insert((block_view, payload_commitment), self.current_view)
2538 .is_none_or(|asked_in| asked_in < self.current_view)
2539 {
2540 debug!(%view, %lock, "timeout certificate lock without its payload; requesting fetch");
2541 outbox.push_back(ConsensusOutput::RequestMissingPayload {
2542 view: block_view,
2543 payload_commitment,
2544 });
2545 }
2546 },
2547 }
2548 self.maybe_vote_2_and_update_lock(lock.view, outbox);
2549 }
2550
2551 #[instrument(level = "debug", skip_all)]
2552 fn handle_epoch_change(
2553 &mut self,
2554 epoch_change: EpochChangeMessage<T, Validated>,
2555 outbox: &mut Outbox<ConsensusOutput<T>>,
2556 ) -> Protocol {
2557 let EpochChangeMessage {
2558 cert1,
2559 cert2,
2560 proposal,
2561 ..
2562 } = epoch_change;
2563
2564 let block_view = cert1.view_number();
2567 let commit_view = cert2.view_number();
2568 let boundary_epoch = cert2.data.epoch;
2569 let leaf_commit = cert2.data.leaf_commit;
2570
2571 self.hold_proposal(proposal.clone());
2572 outbox.push_back(ConsensusOutput::PersistBoundaryQc(cert1.clone()));
2573 self.insert_cert1(cert1.clone());
2574 self.insert_cert2(cert2);
2575 self.adopt_certified_drb(block_view);
2576
2577 if boundary_epoch < self.current_epoch {
2580 debug!(
2581 view = %commit_view,
2582 epoch = %boundary_epoch,
2583 current_epoch = %self.current_epoch,
2584 "ignoring stale epoch change for an epoch we have already entered"
2585 );
2586 return Protocol::Continue;
2587 }
2588 let boundary_lock = LockView {
2592 epoch: boundary_epoch,
2593 view: commit_view,
2594 };
2595 if let Some(locked) = self.locked_cert.as_ref()
2596 && LockView::of(locked) > boundary_lock
2597 && (LockView::of(locked).epoch > boundary_epoch
2598 || locked.data.leaf_commit != leaf_commit)
2599 {
2600 warn!("locked certificate is newer than the epoch change");
2601 return Protocol::Continue;
2602 }
2603 outbox.push_back(ConsensusOutput::ProposalCertified(proposal.clone()));
2606
2607 if self
2610 .lock_view()
2611 .is_none_or(|lock| lock < LockView::of(&cert1))
2612 {
2613 self.locked_cert = Some(cert1.clone());
2614 outbox.push_back(ConsensusOutput::LockUpdated(block_view));
2615 outbox.push_back(ConsensusOutput::PersistHighQc(cert1));
2616 }
2617
2618 let (curr_view, curr_epoch) = (self.current_view, self.current_epoch);
2619 let next_view = commit_view + 1;
2620 let next_epoch = boundary_epoch + 1;
2621
2622 self.set_current_view_max(next_view);
2623 self.set_current_epoch_max(next_epoch);
2624
2625 if self.current_view != curr_view || self.current_epoch != curr_epoch {
2628 outbox.push_back(ConsensusOutput::ViewChanged(
2629 self.current_view,
2630 self.current_epoch,
2631 ));
2632 }
2633
2634 if self.is_leader(next_view, next_epoch) {
2636 self.push_block_request(
2637 BlockAndHeaderRequest {
2638 view: next_view,
2639 epoch: next_epoch,
2640 parent_proposal: proposal,
2641 },
2642 outbox,
2643 );
2644 }
2645
2646 Protocol::Continue
2647 }
2648
2649 #[instrument(level = "debug", skip_all)]
2650 fn maybe_propose(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
2651 if view <= self.timeout_view {
2652 return;
2653 }
2654
2655 let certificate_rule = self.upgrade_lock.certificate_rule(view);
2656
2657 let view_change_evidence = self
2658 .timeout_certs
2659 .get(&view)
2660 .filter(|tc| !(certificate_rule && tc.epoch() < Some(self.current_epoch)))
2661 .cloned();
2662 let Some(parent_cert) = self.parent_cert(view, view_change_evidence.as_ref()) else {
2663 debug!("no parent certificate");
2664 return;
2665 };
2666 if parent_cert.view_number() >= view {
2667 debug!(parent = %parent_cert.view_number(), "parent certificate not before this view");
2668 return;
2669 }
2670 let Some((parent_view, proposal)) = self
2671 .certified_proposal(
2672 parent_cert.view_number(),
2673 parent_cert.data.leaf_commit,
2674 parent_cert.data.block_number,
2675 )
2676 .map(|(view, proposal)| (view, proposal.clone()))
2677 else {
2678 if self
2682 .proposals
2683 .at(parent_cert.view_number())
2684 .next()
2685 .is_some()
2686 {
2687 warn!(
2688 parent_view = %parent_cert.view_number(),
2689 "held proposal at the parent's view is not the one its certificate names"
2690 );
2691 }
2692 debug!(parent = %parent_cert.view_number(), "no proposal for parent certificate");
2693 return;
2694 };
2695 let parent_commitment = if parent_view == ViewNumber::genesis() {
2696 proposal_commitment(&proposal)
2697 } else {
2698 parent_cert.data.leaf_commit
2699 };
2700 let (justify_qc, next_epoch_justify_qc) = if parent_view != ViewNumber::genesis()
2703 && is_last_block(proposal.block_header.block_number(), *self.epoch_height)
2704 {
2705 let Some(cert2) =
2706 self.opening_cert2(view, parent_view, parent_commitment, &view_change_evidence)
2707 else {
2708 if certificate_rule {
2709 self.maybe_request_revote(
2710 view,
2711 &parent_cert,
2712 parent_view,
2713 &proposal,
2714 view_change_evidence.as_ref(),
2715 outbox,
2716 );
2717 }
2718 debug!("no next epoch justify QC");
2719 return;
2720 };
2721 let own = if parent_cert.view_number() == parent_view {
2722 parent_cert
2723 } else {
2724 let Some(own) = self
2725 .certs1
2726 .get(&ViewEpoch(parent_view, proposal.epoch))
2727 .cloned()
2728 else {
2729 debug!(%parent_view, "no certificate1 of the boundary block at its own view");
2730 return;
2731 };
2732 own
2733 };
2734 (own, Some(cert2))
2735 } else {
2736 (parent_cert, None)
2737 };
2738 let proposal_epoch = if next_epoch_justify_qc.is_some() {
2739 proposal.epoch + 1
2740 } else {
2741 proposal.epoch
2742 };
2743 if self.proposed_at(view, proposal_epoch) {
2744 return;
2745 }
2746 if certificate_rule {
2747 if proposal_epoch < self.current_epoch {
2750 debug!(%proposal_epoch, current_epoch = %self.current_epoch, "proposal epoch is behind");
2751 return;
2752 }
2753 if let Some(tc) = &view_change_evidence
2754 && !certificate_rule_admits(&justify_qc, tc, proposal_epoch)
2755 {
2756 debug!(
2757 parent = %LockView::of(&justify_qc),
2758 "parent is earlier than the timeout certificate's lock"
2759 );
2760 return;
2761 }
2762 }
2763
2764 let Some(header) = self.headers.get(&(view, parent_commitment)) else {
2765 if (view_change_evidence.is_some() || next_epoch_justify_qc.is_some())
2770 && self.is_leader(view, proposal_epoch)
2771 {
2772 self.push_block_request(
2773 BlockAndHeaderRequest {
2774 view,
2775 epoch: proposal_epoch,
2776 parent_proposal: proposal.clone(),
2777 },
2778 outbox,
2779 );
2780 }
2781 debug!("no block header");
2782 return;
2783 };
2784 let parent_cert = justify_qc;
2785 let VidCommitment::V2(block_commitment) = header.payload_commitment() else {
2786 debug!("header payload commitment is not V2");
2787 return;
2788 };
2789 if !self.blocks.contains_key(&(view, block_commitment)) {
2790 debug!("no block");
2791 return;
2792 };
2793
2794 if !self.is_leader(view, proposal_epoch) {
2795 warn!(epoch = %proposal_epoch, "not the leader for this view, we should not have a header");
2796 return;
2797 }
2798
2799 let next_drb_result = if proposal.epoch > EpochNumber::genesis()
2807 && is_epoch_transition(header.block_number(), *self.epoch_height)
2808 {
2809 let Some(drb) = self.drb_results.get(&EpochNumber::new(*proposal.epoch + 1)) else {
2810 debug!(%proposal.epoch, "no DRB result for epoch");
2811 outbox.push_back(ConsensusOutput::RequestDrbResult(proposal.epoch + 1));
2815 return;
2816 };
2817 Some(*drb)
2818 } else {
2819 None
2820 };
2821 let parent_block_number = parent_cert.data.block_number.unwrap_or(0);
2826 let state_cert = if is_epoch_root(parent_block_number, *self.epoch_height) {
2827 let Some(parent_epoch) = parent_cert.data.epoch() else {
2828 warn!("epoch-root parent QC has no epoch; cannot propose");
2829 return;
2830 };
2831 let Some(sc) = self.state_certs.get(&parent_epoch).cloned() else {
2832 warn!(
2833 %view,
2834 "epoch-root parent QC without state_cert — atomicity invariant broken; skipping propose"
2835 );
2836 return;
2837 };
2838 if !check_qc_state_cert_correspondence(&parent_cert, &sc, *self.epoch_height) {
2839 warn!(%view, "state_cert does not correspond to parent QC; skipping propose");
2840 return;
2841 }
2842 Some(sc)
2843 } else {
2844 None
2845 };
2846
2847 let proposal = Proposal::<T> {
2848 block_header: header.clone(),
2849 view_number: view,
2850 epoch: proposal_epoch,
2851 justify_qc: parent_cert,
2852 next_epoch_justify_qc,
2853 upgrade_certificate: self.upgrade_certificate_to_attach(view, proposal_epoch),
2854 view_change_evidence,
2855 next_drb_result,
2856 state_cert,
2857 };
2858
2859 let proposed_leaf: Leaf2<T> = proposal.clone().into();
2861 crate::trace_leader_event!(
2862 self.tracer,
2863 view,
2864 crate::leader_trace::LeaderEvent::ProposalAssembled
2865 );
2866 let signature =
2867 match T::SignatureKey::sign(&self.private_key, proposed_leaf.commit().as_ref()) {
2868 Ok(sig) => sig,
2869 Err(err) => {
2870 warn!(%view, %err, "failed to sign proposal");
2871 return;
2872 },
2873 };
2874
2875 let message = SignedProposal {
2876 data: proposal,
2877 signature,
2878 _pd: PhantomData,
2879 };
2880
2881 self.proposed_views.insert(ViewEpoch(view, proposal_epoch));
2882 outbox.push_back(ConsensusOutput::PersistProposal(message.clone()));
2883 self.request_action(view, ActionKind::Propose, outbox);
2884 crate::trace_leader_event!(
2885 self.tracer,
2886 view,
2887 crate::leader_trace::LeaderEvent::ProposalQueued
2888 );
2889 self.pending_proposal
2890 .insert(ViewEpoch(view, proposal_epoch), message);
2891 }
2892
2893 fn parent_cert(
2900 &self,
2901 view: ViewNumber,
2902 evidence: Option<&TimeoutEvidence<T>>,
2903 ) -> Option<Certificate1<T>> {
2904 let previous = ViewNumber::from(view.saturating_sub(1));
2905 if evidence.is_none() {
2906 return self.cert1_at(previous).cloned();
2907 }
2908 let lock = self.locked_cert.as_ref();
2909 if self.upgrade_lock.certificate_rule(view) {
2910 let epoch = lock.map_or_else(EpochNumber::genesis, |lock| LockView::of(lock).epoch);
2911 if let Some(boundary) = self.committed_last_block_cert1(epoch) {
2912 return Some(boundary);
2913 }
2914 }
2915 lock.cloned()
2916 }
2917
2918 fn committed_last_block_cert1(&self, epoch: EpochNumber) -> Option<Certificate1<T>> {
2921 self.certs2
2922 .iter()
2923 .rev()
2924 .filter(|(_, cert2)| {
2925 cert2.data.epoch >= epoch
2926 && is_last_block(cert2.data.block_number, *self.epoch_height)
2927 })
2928 .find_map(|(view, cert2)| {
2929 let (block_view, _) = self.certified_proposal(
2930 view.0,
2931 cert2.data.leaf_commit,
2932 Some(cert2.data.block_number),
2933 )?;
2934 self.certs1
2935 .get(&ViewEpoch(block_view, cert2.data.epoch))
2936 .filter(|cert1| cert1.data.leaf_commit == cert2.data.leaf_commit)
2937 .cloned()
2938 })
2939 }
2940
2941 fn opening_cert2(
2948 &self,
2949 view: ViewNumber,
2950 block_view: ViewNumber,
2951 leaf_commit: Commitment<Leaf2<T>>,
2952 evidence: &Option<TimeoutEvidence<T>>,
2953 ) -> Option<Certificate2<T>> {
2954 if evidence.is_none() && block_view + 1 != view {
2955 return None;
2956 }
2957 self.certs2
2958 .range(ViewEpoch::start(block_view)..ViewEpoch::start(view))
2959 .map(|(_, cert2)| cert2)
2960 .find(|cert2| cert2.data.leaf_commit == leaf_commit)
2961 .cloned()
2962 }
2963
2964 fn maybe_request_revote(
2980 &mut self,
2981 view: ViewNumber,
2982 cert1: &Certificate1<T>,
2983 block_view: ViewNumber,
2984 block: &Proposal<T>,
2985 tc: Option<&TimeoutEvidence<T>>,
2986 outbox: &mut Outbox<ConsensusOutput<T>>,
2987 ) {
2988 let epoch = block.epoch;
2989 if self.proposed_at(view, epoch)
2990 || epoch < self.current_epoch
2991 || !self.is_leader(view, epoch)
2992 {
2993 return;
2994 }
2995 let timeout = match tc {
2996 None if cert1.view_number() + 1 == view => None,
2997 Some(TimeoutEvidence::V3(timeout)) if timeout.epoch == epoch => Some(timeout.clone()),
2998 _ => return,
2999 };
3000 let leaf_commit = proposal_commitment(block);
3001 if cert1.data.leaf_commit != leaf_commit || cert1.data.epoch != Some(epoch) {
3002 debug!(%block_view, "the certificate built on is not over the last block of its epoch");
3003 return;
3004 }
3005 if self.cert2_over(block_view, leaf_commit).is_some() {
3008 return;
3009 }
3010 let cert1 = cert1.clone();
3011 if let Some(tc) = tc
3012 && !certificate_rule_admits(&cert1, tc, epoch)
3013 {
3014 debug!(%block_view, "last block is earlier than the timeout certificate's lock");
3015 return;
3016 }
3017 let VidCommitment::V2(payload_commitment) = block.block_header.payload_commitment() else {
3018 return;
3019 };
3020 if !self.parent_reconstructed(block_view, payload_commitment, leaf_commit) {
3021 debug!(%block_view, "no payload of the last block to re-vote on");
3022 return;
3023 }
3024 let revote = ReVote {
3025 view,
3026 epoch,
3027 cert1,
3028 timeout,
3029 };
3030 let message = match ReVoteMessage::new(revote, &self.private_key) {
3031 Ok(message) => message,
3032 Err(err) => {
3033 warn!(%view, %err, "failed to sign re-vote request");
3034 return;
3035 },
3036 };
3037 info!(%view, %epoch, %block_view, "re-vote on the last block of the epoch");
3038 self.proposed_views.insert(ViewEpoch(view, epoch));
3039 self.request_action(view, ActionKind::Propose, outbox);
3040 self.pending_revote.insert(ViewEpoch(view, epoch), message);
3041 self.release_proposal(view, outbox);
3042 }
3043
3044 #[instrument(level = "debug", skip_all)]
3046 fn maybe_decide(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
3047 let epochs: Vec<EpochNumber> = self
3048 .certs2
3049 .range(ViewEpoch::at_view(view))
3050 .map(|(ViewEpoch(_, epoch), _)| *epoch)
3051 .collect();
3052 if epochs.is_empty() {
3053 debug!(%view, "cert2 not available");
3054 }
3055 for epoch in epochs {
3056 self.maybe_decide_at(ViewEpoch(view, epoch), outbox);
3057 }
3058 }
3059
3060 fn maybe_decide_at(&mut self, key: ViewEpoch, outbox: &mut Outbox<ConsensusOutput<T>>) {
3061 let ViewEpoch(view, epoch) = key;
3062 let floor = self.decide_floor();
3065 if view <= floor {
3066 return;
3067 }
3068 let Some(cert2) = self.certs2.get(&key).cloned() else {
3069 return;
3070 };
3071 let Some((block_view, proposal)) =
3072 self.certified_proposal(view, cert2.data.leaf_commit, Some(cert2.data.block_number))
3073 else {
3074 debug!(%view, "proposal not available");
3075 return;
3076 };
3077 if block_view != view {
3078 if self.revote_commits.get(&view) == Some(&cert2.data.epoch) {
3081 return;
3082 }
3083 if block_view <= floor || self.decided_views.contains(&block_view) {
3084 self.revote_commits.insert(view, cert2.data.epoch);
3085 return;
3086 }
3087 } else if self.decided_views.contains(&view) {
3088 return;
3089 }
3090 let proposal_commit = cert2.data.leaf_commit;
3091 let Some(cert1) = self
3094 .certs1
3095 .get(&ViewEpoch(block_view, epoch))
3096 .or_else(|| self.certs1.get(&key))
3097 .filter(|cert1| cert1.data.leaf_commit == proposal_commit)
3098 .cloned()
3099 else {
3100 debug!(%view, "cert1 missing");
3101 return;
3102 };
3103 if is_last_block(proposal.block_header.block_number(), *self.epoch_height)
3106 && cert1.view_number() == block_view
3107 {
3108 let epoch_change =
3109 EpochChangeMessage::validated(cert1.clone(), cert2.clone(), proposal.clone());
3110 outbox.push_back(ConsensusOutput::SendEpochChange(epoch_change));
3111 }
3112 let mut leaf: Leaf2<T> = proposal.clone().into();
3114 if let VidCommitment::V2(pc) = proposal.block_header.payload_commitment()
3115 && let Some(payload) = self.blocks.get(&(block_view, pc))
3116 {
3117 leaf.fill_block_payload_unchecked(payload.clone());
3118 }
3119 let mut decided = vec![leaf];
3120 let mut vid_shares = vec![self.signed_vid_share(proposal)];
3121
3122 let mut parent_view = proposal.justify_qc.view_number();
3123 let mut parent_commit = proposal.justify_qc.data.leaf_commit;
3124
3125 while parent_view > floor
3127 && !self.decided_views.contains(&parent_view)
3128 && let Some(proposal) = self.proposals.get(parent_view, parent_commit)
3129 {
3130 let mut leaf: Leaf2<T> = proposal.clone().into();
3131 if let VidCommitment::V2(pc) = proposal.block_header.payload_commitment()
3132 && let Some(payload) = self.blocks.get(&(parent_view, pc))
3133 {
3134 leaf.fill_block_payload_unchecked(payload.clone());
3135 }
3136 vid_shares.push(self.signed_vid_share(proposal));
3137 decided.push(leaf);
3138 parent_view = proposal.justify_qc.view_number();
3139 parent_commit = proposal.justify_qc.data.leaf_commit;
3140 }
3141 self.decided_views
3142 .extend(decided.iter().map(|l| l.view_number()));
3143 if block_view != view {
3144 self.revote_commits.insert(view, cert2.data.epoch);
3145 }
3146 if block_view > self.last_decided_view {
3148 self.last_decided_view = block_view;
3149 self.last_decided_leaf = decided[0].clone();
3150 }
3151 self.maybe_decide_upgrade(&decided, outbox);
3152 crate::trace_leader_event!(
3153 self.tracer,
3154 view,
3155 crate::leader_trace::LeaderEvent::LeafDecided
3156 );
3157 outbox.push_back(ConsensusOutput::LeafDecided {
3158 leaves: decided,
3159 cert1,
3160 cert2: Some(cert2),
3161 vid_shares,
3162 });
3163 }
3164
3165 fn build_state_vote(
3172 &self,
3173 proposal: &Proposal<T>,
3174 ) -> anyhow::Result<LightClientStateUpdateVote2<T>> {
3175 let view_number = proposal.view_number;
3176 let light_client_state = proposal
3177 .block_header
3178 .get_light_client_state(view_number)
3179 .map_err(|e| anyhow::anyhow!("failed to generate light client state: {e}"))?;
3180 let auth_root = proposal
3181 .block_header
3182 .auth_root()
3183 .map_err(|e| anyhow::anyhow!("failed to fetch auth root: {e}"))?;
3184 let membership = self
3185 .stake_table_coordinator
3186 .membership_for_epoch(Some(proposal.epoch))
3187 .map_err(|e| anyhow::anyhow!("membership lookup failed: {e}"))?;
3188 let next_stake_table = membership
3189 .next_epoch_stake_table()
3190 .map_err(|e| anyhow::anyhow!("next-epoch stake table lookup failed: {e}"))?;
3191 let next_stake_table_state = HSStakeTable::from_iter(next_stake_table.stake_table())
3192 .commitment(self.stake_table_capacity)
3193 .map_err(|e| anyhow::anyhow!("failed to compute stake table commitment: {e}"))?;
3194 let v2_signature = <T::StateSignatureKey as LCV2StateSignatureKey>::sign_state(
3195 &self.state_private_key,
3196 &light_client_state,
3197 &next_stake_table_state,
3198 )
3199 .map_err(|e| anyhow::anyhow!("failed to sign LCV2 state: {e}"))?;
3200 let signed_state_digest =
3201 derive_signed_state_digest(&light_client_state, &next_stake_table_state, &auth_root);
3202 let signature = <T::StateSignatureKey as LCV3StateSignatureKey>::sign_state(
3203 &self.state_private_key,
3204 signed_state_digest,
3205 )
3206 .map_err(|e| anyhow::anyhow!("failed to sign LCV3 state: {e}"))?;
3207 Ok(LightClientStateUpdateVote2 {
3208 epoch: proposal.epoch,
3209 light_client_state,
3210 next_stake_table_state,
3211 signature,
3212 v2_signature,
3213 auth_root,
3214 signed_state_digest,
3215 })
3216 }
3217
3218 fn handle_stored(&mut self, stored: StorageOutput<T>, outbox: &mut Outbox<ConsensusOutput<T>>) {
3219 let view = stored.view_number();
3220 match stored {
3221 StorageOutput::Proposal(view, commitment) => {
3222 self.stored_proposals
3223 .entry(view)
3224 .or_default()
3225 .push(commitment);
3226 },
3227 StorageOutput::Vid(view) => {
3228 self.stored_vids.insert(view);
3229 },
3230 StorageOutput::Action(view, kind) => {
3231 self.stored_actions.insert((view, kind));
3232 },
3233 StorageOutput::HighQc(lock) => {
3234 self.bump_stored_high_qc(lock);
3235 let pending: Vec<ViewEpoch> = self.pending_vote2.keys().copied().collect();
3237 for key in pending {
3238 self.release_vote2_at(key, outbox);
3239 }
3240 return;
3241 },
3242 }
3243 self.release_vote1(view, outbox);
3244 self.release_vote2(view, outbox);
3245 self.release_proposal(view, outbox);
3246 self.release_timeout_vote(view, outbox);
3247 }
3248
3249 fn release_vote1(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
3250 let pending: Vec<ViewEpoch> = self
3251 .pending_vote1
3252 .range(ViewEpoch::at_view(view))
3253 .map(|(key, _)| *key)
3254 .collect();
3255 for key in pending {
3256 self.release_vote1_at(key, outbox);
3257 }
3258 }
3259
3260 fn release_vote1_at(&mut self, key: ViewEpoch, outbox: &mut Outbox<ConsensusOutput<T>>) {
3261 let ViewEpoch(view, epoch) = key;
3262 let Some(vote1) = self.pending_vote1.get(&key) else {
3263 return;
3264 };
3265 let is_revote = self.revotes.get(&view).is_some_and(|revote| {
3267 revote.epoch == epoch && revote.cert1.data.leaf_commit == vote1.vote.data.leaf_commit
3268 });
3269 if !self.stored_actions.contains(&(view, ActionKind::Vote))
3270 || (!is_revote && !self.is_proposal_stored(view, &vote1.vote.data.leaf_commit))
3271 {
3272 return;
3273 }
3274 let vote1 = self.pending_vote1.remove(&key).expect("checked above");
3275 if self.upgrade_lock.certificate_rule(view) && epoch < self.current_epoch {
3276 debug!(%view, %epoch, "dropping pending vote1 of an epoch this node has left");
3277 return;
3278 }
3279 if view <= self.timeout_view {
3280 debug!(%view, "dropping pending vote1 for timed-out view");
3281 return;
3282 }
3283 outbox.push_back(ConsensusOutput::SendVote1(vote1));
3284 if is_revote {
3285 return;
3286 }
3287 if let Some(vid_share) = self.vid_shares.get(&view).cloned() {
3288 outbox.push_back(ConsensusOutput::BroadcastVidShare(vid_share));
3289 } else {
3290 debug!(%view, "vid share gone for released vote1; skipping broadcast");
3291 }
3292 }
3293
3294 fn release_vote2(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
3295 let pending: Vec<ViewEpoch> = self
3296 .pending_vote2
3297 .range(ViewEpoch::at_view(view))
3298 .map(|(key, _)| *key)
3299 .collect();
3300 for key in pending {
3301 self.release_vote2_at(key, outbox);
3302 }
3303 }
3304
3305 fn release_vote2_at(&mut self, key: ViewEpoch, outbox: &mut Outbox<ConsensusOutput<T>>) {
3306 let ViewEpoch(view, epoch) = key;
3307 let Some(pending) = self.pending_vote2.get(&key) else {
3308 return;
3309 };
3310 if self.certs2.contains_key(&key) {
3311 self.pending_vote2.remove(&key);
3312 return;
3313 }
3314 if !self.vote2_persisted(view, pending.block_view) || !self.high_qc_persisted(pending.lock)
3315 {
3316 return;
3317 }
3318 let pending = self.pending_vote2.remove(&key).expect("checked above");
3319 if self.upgrade_lock.certificate_rule(view) && epoch < self.current_epoch {
3320 debug!(%view, %epoch, "dropping pending vote2 of an epoch this node has left");
3321 return;
3322 }
3323 if self.timed_out_for_vote2(view) {
3324 warn!(%view, "dropping pending vote2 for a view this node has timed out");
3325 return;
3326 }
3327 if !self.upgrade_lock.certificate_rule(view) && self.voted_for_branch_excluding(view) {
3328 warn!(%view, "dropping pending vote2 for a view a later vote1 skips");
3329 return;
3330 }
3331 outbox.push_back(ConsensusOutput::SendVote2(pending.vote));
3332 }
3333
3334 fn vote2_persisted(&self, view: ViewNumber, block_view: ViewNumber) -> bool {
3339 view <= self.restart_barred_view
3340 || (self.stored_actions.contains(&(view, ActionKind::Vote))
3341 && (block_view != view || self.stored_vids.contains(&view)))
3342 }
3343
3344 fn timed_out_for_vote2(&self, view: ViewNumber) -> bool {
3346 self.timeout_vote_bar.is_some_and(|bar| view <= bar)
3347 }
3348
3349 fn hold_proposal(&mut self, proposal: Proposal<T>) -> Commitment<Leaf2<T>> {
3355 let commit = proposal_commitment(&proposal);
3356 self.hold_proposal_under(proposal, commit);
3357 commit
3358 }
3359
3360 fn hold_proposal_under(&mut self, proposal: Proposal<T>, commit: Commitment<Leaf2<T>>) {
3363 let view = proposal.view_number;
3364 let leaf = proposal.clone().into();
3365 self.proposals.insert_under(proposal, commit);
3366 self.leaves.insert((view, commit), leaf);
3367 }
3368
3369 fn voted_for_branch_excluding(&self, view: ViewNumber) -> bool {
3380 self.vote1_parent
3381 .range(view + 1..)
3382 .any(|(_, justified_at)| *justified_at < view)
3383 }
3384
3385 fn release_proposal(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
3386 if !self.stored_actions.contains(&(view, ActionKind::Propose)) {
3387 return;
3388 }
3389 if view <= self.timeout_view {
3390 debug!(%view, "dropping pending proposal and re-vote request for timed-out view");
3391 self.pending_revote.retain(|ViewEpoch(v, _), _| *v != view);
3392 self.pending_proposal
3393 .retain(|ViewEpoch(v, _), _| *v != view);
3394 return;
3395 }
3396 for (ViewEpoch(_, epoch), message) in self
3397 .pending_revote
3398 .extract_if(ViewEpoch::at_view(view), |_, _| true)
3399 {
3400 if epoch < self.current_epoch {
3401 debug!(%view, %epoch, "dropping pending re-vote request of an epoch this node has left");
3402 continue;
3403 }
3404 outbox.push_back(ConsensusOutput::SendReVote(message));
3405 }
3406 let stored = self.stored_proposals.get(&view);
3407 for (ViewEpoch(_, epoch), message) in
3408 self.pending_proposal
3409 .extract_if(ViewEpoch::at_view(view), |_, message| {
3410 stored.is_some_and(|commitments| {
3411 commitments.contains(&proposal_commitment(&message.data))
3412 })
3413 })
3414 {
3415 if self.upgrade_lock.certificate_rule(view) && epoch < self.current_epoch {
3416 debug!(%view, %epoch, "dropping pending proposal of an epoch this node has left");
3417 continue;
3418 }
3419 outbox.push_back(ConsensusOutput::SendProposal(message));
3420 }
3421 }
3422
3423 fn is_proposal_stored(&self, view: ViewNumber, commitment: &Commitment<Leaf2<T>>) -> bool {
3424 self.stored_proposals
3425 .get(&view)
3426 .is_some_and(|commitments| commitments.contains(commitment))
3427 }
3428
3429 fn request_action(
3430 &mut self,
3431 view: ViewNumber,
3432 kind: ActionKind,
3433 outbox: &mut Outbox<ConsensusOutput<T>>,
3434 ) {
3435 if self.requested_actions.insert((view, kind)) {
3436 outbox.push_back(ConsensusOutput::RecordAction(view, kind));
3437 }
3438 }
3439
3440 #[instrument(level = "debug", skip_all)]
3441 fn maybe_vote_1(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
3442 if view <= self.timeout_view {
3443 return;
3444 }
3445 if self
3448 .revotes
3449 .get(&view)
3450 .is_some_and(|revote| !self.voted_1_at(view, revote.epoch))
3451 {
3452 self.maybe_vote_revote(view, outbox);
3453 }
3454 if self
3455 .proposals
3456 .live(view)
3457 .is_none_or(|proposal| self.voted_1_at(view, proposal.epoch))
3458 {
3459 return;
3460 }
3461
3462 let Some(proposal) = self.proposals.live(view) else {
3463 debug!(%view, "proposal not available");
3464 return;
3465 };
3466 if self.lock_view().is_some_and(|lock| {
3468 lock > LockView {
3469 epoch: proposal.epoch,
3470 view,
3471 }
3472 }) {
3473 debug!(%view, "proposal behind the lock");
3474 return;
3475 }
3476 if !self
3477 .states_verified
3478 .contains(&(view, proposal_commitment(proposal)))
3479 {
3480 debug!(%view, "state commitment not available");
3481 return;
3482 }
3483 let Some(vid_share) = self.vid_shares.get(&view) else {
3484 debug!(%view, "vid share not available");
3485 return;
3486 };
3487
3488 let block_number = proposal.block_header.block_number();
3489 let epoch = proposal.epoch;
3490 let qc_view = proposal.justify_qc.view_number();
3491 let qc_epoch = proposal.justify_qc.epoch();
3492
3493 if self.upgrade_lock.certificate_rule(view) {
3494 if let Err(reason) = self.certificate_rule_vote1(proposal) {
3495 warn!(
3496 %view, block = %block_number, %epoch, %qc_view, ?qc_epoch, %reason,
3497 "certificate rule refuses vote1"
3498 );
3499 return;
3500 }
3501 } else if let Err(err) = self.is_safe(proposal) {
3508 warn!(
3509 %view, block = %block_number, %epoch, %qc_view, ?qc_epoch, %err,
3510 "proposal no longer safe against the current lock; refusing to vote1"
3511 );
3512 return;
3513 }
3514
3515 if proposal.epoch > EpochNumber::genesis()
3519 && is_epoch_transition(block_number, *self.epoch_height)
3520 {
3521 let Some(drb) = self.drb_results.get(&(proposal.epoch + 1)) else {
3522 debug!(%view, block = %block_number, %epoch, "DRB result not yet available, deferring vote");
3523 return;
3524 };
3525 if proposal
3526 .next_drb_result
3527 .is_none_or(|proposed_drb| drb != &proposed_drb)
3528 {
3529 warn!(
3530 %view, block = %block_number, %epoch, %qc_view, ?qc_epoch,
3531 "DRB result does not match proposal, refusing to vote"
3532 );
3533 return;
3534 }
3535 }
3536
3537 if !self.staked_in_epoch(proposal.epoch) {
3538 return;
3539 }
3540
3541 let parent_view = proposal.justify_qc.view_number();
3543
3544 if parent_view != ViewNumber::genesis()
3545 && !is_last_block(
3546 proposal.block_header.block_number().saturating_sub(1),
3547 *self.epoch_height,
3548 )
3549 {
3550 let Some(prev_proposal) = self
3551 .proposals
3552 .get(parent_view, proposal.justify_qc.data().leaf_commit)
3553 else {
3554 debug!(%view, %parent_view, "proposal not available");
3555 return;
3556 };
3557 let parent_block = prev_proposal.block_header.block_number();
3558 let parent_epoch = prev_proposal.epoch;
3559
3560 let VidCommitment::V2(prev_block_commitment) =
3561 prev_proposal.block_header.payload_commitment()
3562 else {
3563 warn! {
3564 %view, block = %block_number, %epoch,
3565 %parent_view, %parent_block, %parent_epoch,
3566 "prev. proposal payload commitment is not a V2 VID commitment"
3567 }
3568 return;
3569 };
3570 if !self.parent_reconstructed(
3572 parent_view,
3573 prev_block_commitment,
3574 proposal_commitment(prev_proposal),
3575 ) {
3576 debug!(
3577 %view, block = %block_number, %epoch,
3578 %parent_view, %parent_block, %parent_epoch,
3579 "no reconstructed block matching the parent block commitment"
3580 );
3581 return;
3582 }
3583 }
3584
3585 let proposal_commit = proposal_commitment(proposal);
3586
3587 let inner_vote = match SimpleVote::create_signed_vote(
3588 QuorumData2 {
3589 leaf_commit: proposal_commit,
3590 epoch: proposal.epoch(),
3591 block_number: Some(proposal.block_header.block_number()),
3592 },
3593 view,
3594 &self.public_key,
3595 &self.private_key,
3596 &self.upgrade_lock,
3597 ) {
3598 Ok(vote) => vote,
3599 Err(err) => {
3600 warn!(%view, %err, "failed to created signed vote for proposal");
3601 return;
3602 },
3603 };
3604
3605 let state_vote = if is_epoch_root(proposal.block_header.block_number(), *self.epoch_height)
3606 {
3607 match self.build_state_vote(proposal) {
3608 Ok(sv) => Some(sv),
3609 Err(err) => {
3610 warn!(%view, %err, "failed to build state vote for epoch-root leaf; skipping vote1");
3611 return;
3612 },
3613 }
3614 } else {
3615 None
3616 };
3617
3618 let vote = Vote1 {
3619 vote: inner_vote,
3620 state_vote,
3621 };
3622 let can_send = self.stored_actions.contains(&(view, ActionKind::Vote))
3623 && self.is_proposal_stored(view, &proposal_commit);
3624 let vid_share = can_send.then(|| vid_share.clone());
3625 self.voted_1_views.insert(ViewEpoch(view, epoch));
3626 self.vote1_parent.insert(view, qc_view);
3627 if let Some(vid_share) = vid_share {
3628 outbox.push_back(ConsensusOutput::SendVote1(vote));
3629 outbox.push_back(ConsensusOutput::BroadcastVidShare(vid_share));
3630 } else {
3631 self.request_action(view, ActionKind::Vote, outbox);
3632 self.pending_vote1.insert(ViewEpoch(view, epoch), vote);
3633 }
3634 }
3635
3636 #[instrument(level = "debug", skip_all)]
3639 fn maybe_vote_2_and_update_lock(
3640 &mut self,
3641 view: ViewNumber,
3642 outbox: &mut Outbox<ConsensusOutput<T>>,
3643 ) {
3644 if view == ViewNumber::genesis() {
3645 return;
3646 }
3647 let epochs: Vec<EpochNumber> = self
3648 .certs1
3649 .range(ViewEpoch::at_view(view))
3650 .map(|(ViewEpoch(_, epoch), _)| *epoch)
3651 .collect();
3652 if epochs.is_empty() {
3653 debug!(%view, "cert1 not available");
3654 }
3655 for epoch in epochs {
3656 self.maybe_vote_2_and_update_lock_at(ViewEpoch(view, epoch), outbox);
3657 }
3658 }
3659
3660 fn maybe_vote_2_and_update_lock_at(
3661 &mut self,
3662 key: ViewEpoch,
3663 outbox: &mut Outbox<ConsensusOutput<T>>,
3664 ) {
3665 let ViewEpoch(view, epoch) = key;
3666 let Some(cert1) = self.certs1.get(&key) else {
3667 return;
3668 };
3669 if self.voted_2_at(view, epoch)
3670 && self
3671 .lock_view()
3672 .is_some_and(|lock| lock >= LockView::of(cert1))
3673 {
3674 return;
3675 }
3676 let Some((block_view, proposal)) =
3679 self.certified_proposal(view, cert1.data.leaf_commit, cert1.data.block_number)
3680 else {
3681 if self.proposals.at(view).next().is_some() {
3682 warn!(%view, "cert1 commitment does not match proposal commitment");
3683 } else {
3684 debug!(%view, "proposal not available");
3685 }
3686 return;
3687 };
3688 let proposal_epoch = proposal.epoch;
3689 let block = proposal.block_header.block_number();
3690 let qc_view = proposal.justify_qc.view_number();
3691 let qc_epoch = proposal.justify_qc.epoch();
3692 let proposal_commit = proposal_commitment(proposal);
3693 let VidCommitment::V2(proposal_block_commitment) =
3694 proposal.block_header.payload_commitment()
3695 else {
3696 warn!(
3697 %view, %block, epoch = %proposal_epoch, %qc_view, ?qc_epoch,
3698 "proposal payload commitment is not a V2 VID commitment"
3699 );
3700 return;
3701 };
3702 if !self
3703 .blocks_reconstructed
3704 .contains(&(block_view, proposal_block_commitment))
3705 {
3706 debug!(
3707 %view, %block, epoch = %proposal_epoch, %qc_view, ?qc_epoch,
3708 "no reconstructed block matching the proposal commitment"
3709 );
3710 return;
3711 }
3712
3713 let own_boundary =
3714 (block_view == view && is_last_block(block, *self.epoch_height)).then(|| cert1.clone());
3715 if !self.behind_current_epoch(view, proposal_epoch)
3720 && self
3721 .lock_view()
3722 .is_none_or(|lock| lock < LockView::of(cert1))
3723 {
3724 let cert1 = cert1.clone();
3725 self.locked_cert = Some(cert1.clone());
3726 let curr_view = self.current_view;
3727 self.set_current_view_max(view + 1);
3728 self.set_current_epoch_max(proposal_epoch);
3729 outbox.push_back(ConsensusOutput::LockUpdated(block_view));
3731 if self.current_view != curr_view {
3732 outbox.push_back(ConsensusOutput::ViewChanged(
3733 self.current_view,
3734 self.current_epoch,
3735 ));
3736 }
3737 outbox.push_back(ConsensusOutput::SendCertificate1(cert1.clone()));
3738 outbox.push_back(ConsensusOutput::PersistHighQc(cert1));
3740 }
3741 if let Some(cert1) = own_boundary
3744 && self
3745 .locked_cert
3746 .as_ref()
3747 .is_some_and(|lock| lock.data.leaf_commit == cert1.data.leaf_commit)
3748 {
3749 outbox.push_back(ConsensusOutput::PersistBoundaryQc(cert1));
3750 }
3751
3752 if self.voted_2_at(view, epoch)
3753 || self.certs2.contains_key(&key)
3754 || self.decided_views.contains(&view)
3755 || view <= self.decide_floor()
3756 {
3757 return;
3758 }
3759
3760 if self.timed_out_for_vote2(view) {
3761 debug!(%view, %block, "timed out this view; refusing to vote2");
3762 return;
3763 }
3764
3765 if self.upgrade_lock.certificate_rule(view) && proposal_epoch < self.current_epoch {
3768 debug!(%view, %block, epoch = %proposal_epoch, "vote2 of an earlier epoch");
3769 return;
3770 }
3771
3772 if !self.upgrade_lock.certificate_rule(view) && self.voted_for_branch_excluding(view) {
3773 warn!(%view, %qc_view, %block, "a later vote1 skips this view; refusing to vote2");
3774 return;
3775 }
3776
3777 if !self.staked_in_epoch(proposal_epoch) {
3778 return;
3779 }
3780
3781 let vote = match SimpleVote::create_signed_vote(
3782 Vote2Data {
3783 leaf_commit: proposal_commit,
3784 epoch: proposal_epoch,
3785 block_number: block,
3786 },
3787 view,
3788 &self.public_key,
3789 &self.private_key,
3790 &self.upgrade_lock,
3791 ) {
3792 Ok(vote) => vote,
3793 Err(err) => {
3794 warn!(%view, %err, "failed to created signed vote2");
3795 return;
3796 },
3797 };
3798 self.voted_2_views.insert(key);
3799 let required = self
3801 .lock_view()
3802 .expect("locked_cert is set before voting in phase 2");
3803 if self.vote2_persisted(view, block_view) && self.high_qc_persisted(required) {
3804 outbox.push_back(ConsensusOutput::SendVote2(vote));
3805 } else {
3806 if view > self.restart_barred_view {
3807 self.request_action(view, ActionKind::Vote, outbox);
3808 }
3809 self.pending_vote2.insert(
3810 key,
3811 PendingVote2 {
3812 vote,
3813 lock: required,
3814 block_view,
3815 },
3816 );
3817 }
3818 crate::trace_leader_event!(
3819 self.tracer,
3820 view,
3821 crate::leader_trace::LeaderEvent::Vote2VMinus1Queued
3822 );
3823 }
3824
3825 fn certificate_rule_vote1(&self, proposal: &Proposal<T>) -> Result<(), &'static str> {
3833 let epoch = proposal.epoch;
3834 if epoch < self.current_epoch {
3835 return Err("proposal is of an epoch earlier than this node's");
3836 }
3837 if let Some(tc) = &proposal.view_change_evidence
3838 && !certificate_rule_admits(&proposal.justify_qc, tc, epoch)
3839 {
3840 return Err("parent is earlier than the timeout certificate's lock");
3841 }
3842 if proposal.next_epoch_justify_qc.is_some() {
3843 let parent_view = proposal.justify_qc.view_number();
3844 let holds_parent = self
3845 .proposals
3846 .contains(parent_view, proposal.justify_qc.data.leaf_commit);
3847 if !holds_parent {
3848 return Err("parent of the first block of the epoch is not held at its own view");
3849 }
3850 }
3851 Ok(())
3852 }
3853
3854 fn handle_revote(
3862 &mut self,
3863 message: ReVoteMessage<T, Validated>,
3864 outbox: &mut Outbox<ConsensusOutput<T>>,
3865 ) -> Protocol {
3866 let revote = message.revote;
3867 let view = revote.view;
3868 if view <= self.decide_floor() {
3869 return Protocol::Abort;
3870 }
3871 if revote.epoch < self.current_epoch {
3874 debug!(%view, epoch = %revote.epoch, "re-vote request of an earlier epoch");
3875 return Protocol::Abort;
3876 }
3877 self.advance_on(revote.cert1.clone(), revote.epoch, outbox);
3878 if let Some(timeout) = &revote.timeout
3879 && !self.timeout_certs.contains_key(&view)
3880 {
3881 let tc = ValidCert::new(TimeoutEvidence::V3(timeout.clone()), revote.epoch);
3882 self.handle_timeout_certificate(tc, outbox);
3883 }
3884 self.revotes.entry(view).or_insert(revote);
3885 Protocol::Continue
3886 }
3887
3888 fn maybe_vote_revote(&mut self, view: ViewNumber, outbox: &mut Outbox<ConsensusOutput<T>>) {
3894 let Some(revote) = self.revotes.get(&view) else {
3895 return;
3896 };
3897 if !self.upgrade_lock.certificate_rule(view) {
3898 return;
3899 }
3900 let epoch = revote.epoch;
3901 let cert_view = revote.cert1.view_number();
3902 let leaf_commit = revote.cert1.data.leaf_commit;
3903 if epoch < self.current_epoch {
3904 debug!(%view, %epoch, current_epoch = %self.current_epoch, "re-vote of an earlier epoch");
3905 return;
3906 }
3907 if let Some(timeout) = &revote.timeout
3908 && !certificate_rule_admits(&revote.cert1, &TimeoutEvidence::V3(timeout.clone()), epoch)
3909 {
3910 warn!(%view, %cert_view, "re-vote block is earlier than the timeout certificate's lock");
3911 return;
3912 }
3913 let data = revote.vote_data();
3914 let Some((block_view, block)) =
3917 self.certified_proposal(cert_view, leaf_commit, data.block_number)
3918 else {
3919 if self.revote_fetches.insert((view, RevoteFetch::Proposal)) {
3920 debug!(%view, %cert_view, "re-vote proposal missing; requesting fetch");
3921 outbox.push_back(ConsensusOutput::RequestMissingProposal {
3922 view: cert_view,
3923 leaf_commit,
3924 });
3925 }
3926 return;
3927 };
3928 if self.cert2_over(block_view, leaf_commit).is_some() {
3929 debug!(%view, %block_view, "re-vote on a block already committed");
3930 return;
3931 }
3932 let VidCommitment::V2(payload_commitment) = block.block_header.payload_commitment() else {
3933 return;
3934 };
3935 if !self.parent_reconstructed(block_view, payload_commitment, leaf_commit) {
3936 if self.revote_fetches.insert((view, RevoteFetch::Payload)) {
3937 debug!(%view, %block_view, "re-vote payload missing; requesting fetch");
3938 outbox.push_back(ConsensusOutput::RequestMissingPayload {
3939 view: block_view,
3940 payload_commitment,
3941 });
3942 }
3943 return;
3944 }
3945 if !self.staked_in_epoch(epoch) {
3946 return;
3947 }
3948 let vote = match SimpleVote::create_signed_vote(
3949 data,
3950 view,
3951 &self.public_key,
3952 &self.private_key,
3953 &self.upgrade_lock,
3954 ) {
3955 Ok(vote) => Vote1 {
3956 vote,
3957 state_vote: None,
3958 },
3959 Err(err) => {
3960 warn!(%view, %err, "failed to sign re-vote");
3961 return;
3962 },
3963 };
3964 info!(%view, %epoch, %block_view, "re-vote on the last block of the epoch");
3965 self.voted_1_views.insert(ViewEpoch(view, epoch));
3966 self.vote1_parent.insert(view, block_view);
3967 if self.stored_actions.contains(&(view, ActionKind::Vote)) {
3968 outbox.push_back(ConsensusOutput::SendVote1(vote));
3969 } else {
3970 self.request_action(view, ActionKind::Vote, outbox);
3971 self.pending_vote1.insert(ViewEpoch(view, epoch), vote);
3972 }
3973 }
3974
3975 fn retry_later_certificates(
3982 &mut self,
3983 block_view: ViewNumber,
3984 outbox: &mut Outbox<ConsensusOutput<T>>,
3985 ) {
3986 let last_blocks: Vec<_> = self
3987 .proposals
3988 .at(block_view)
3989 .filter(|block| is_last_block(block.block_header.block_number(), *self.epoch_height))
3990 .map(proposal_commitment)
3991 .collect();
3992 if last_blocks.is_empty() {
3993 return;
3994 }
3995 let from = ViewEpoch::start(block_view + 1);
3996 let certs: BTreeSet<ViewNumber> = self
3997 .certs1
3998 .range(from..)
3999 .filter(|(_, cert)| last_blocks.contains(&cert.data.leaf_commit))
4000 .map(|(ViewEpoch(view, _), _)| *view)
4001 .chain(
4002 self.certs2
4003 .range(from..)
4004 .filter(|(_, cert)| last_blocks.contains(&cert.data.leaf_commit))
4005 .map(|(ViewEpoch(view, _), _)| *view),
4006 )
4007 .collect();
4008 for view in certs {
4009 self.maybe_vote_2_and_update_lock(view, outbox);
4010 self.maybe_decide(view, outbox);
4011 }
4012 }
4013
4014 fn retry_revotes(&mut self, outbox: &mut Outbox<ConsensusOutput<T>>) {
4018 let pending: Vec<ViewNumber> = self
4019 .revotes
4020 .range(self.timeout_view + 1..)
4021 .filter(|(view, revote)| !self.voted_1_at(**view, revote.epoch))
4022 .map(|(view, _)| *view)
4023 .collect();
4024 for view in pending {
4025 self.maybe_vote_1(view, outbox);
4026 }
4027 let floor = self.decide_floor();
4028 let revoted: Vec<ViewNumber> = self
4029 .revotes
4030 .range(floor + 1..)
4031 .filter(|(view, revote)| {
4032 let key = ViewEpoch(**view, revote.epoch);
4033 (self.certs1.contains_key(&key) && !self.voted_2_at(key.0, key.1))
4034 || self.certs2.get(&key).is_some_and(|cert2| {
4035 self.revote_commits.get(view) != Some(&cert2.data.epoch)
4036 })
4037 })
4038 .map(|(view, _)| *view)
4039 .collect();
4040 for view in revoted {
4041 self.maybe_vote_2_and_update_lock(view, outbox);
4042 self.maybe_decide(view, outbox);
4043 }
4044 }
4045
4046 #[instrument(level = "trace", skip_all)]
4047 fn is_safe(&self, proposal: &Proposal<T>) -> Result<(), SafetyError> {
4048 let Some(locked_cert) = self.locked_cert.as_ref() else {
4049 debug!("at genesis");
4051 return Ok(());
4052 };
4053
4054 if locked_cert.view_number() == proposal.view_number() {
4056 let locked_commit = locked_cert.data.leaf_commit;
4057 let proposal_commit = proposal_commitment(proposal);
4058 if locked_commit != proposal_commit {
4059 return Err(SafetyError::LockedViewCommitmentMismatch {
4060 locked_commit: locked_commit.to_string(),
4061 proposal_commit: proposal_commit.to_string(),
4062 });
4063 }
4064 return Ok(());
4065 }
4066
4067 let parent_commit = proposal
4068 .justify_qc
4069 .data_commitment(&self.upgrade_lock)
4070 .map_err(SafetyError::JustifyQcCommitment)?;
4071 let locked_commit = locked_cert
4072 .data_commitment(&self.upgrade_lock)
4073 .map_err(SafetyError::LockedCertCommitment)?;
4074
4075 let safety = parent_commit == locked_commit;
4076 let liveness = proposal.justify_qc.view_number() > locked_cert.view_number();
4077 if safety || liveness {
4078 return Ok(());
4079 }
4080
4081 Err(SafetyError::UnsafeProposal {
4082 locked_view: locked_cert.view_number(),
4083 parent_commit: parent_commit.to_string(),
4084 locked_commit: locked_commit.to_string(),
4085 })
4086 }
4087
4088 fn leader_label(&self, view: ViewNumber, epoch: EpochNumber) -> String {
4092 match self
4093 .stake_table_coordinator
4094 .membership_for_epoch(Some(epoch))
4095 {
4096 Ok(stake_table) => match stake_table.leader(view) {
4097 Ok(leader) => KeyPrefix::from(&leader).to_string(),
4098 Err(_) => "unknown".to_string(),
4099 },
4100 Err(_) => "unknown".to_string(),
4101 }
4102 }
4103
4104 #[instrument(level = "trace", skip_all)]
4105 fn is_leader(&self, view: ViewNumber, epoch: EpochNumber) -> bool {
4106 self.leader_of(view, epoch).as_ref() == Some(&self.public_key)
4107 }
4108
4109 fn staked_in_epoch(&self, epoch: EpochNumber) -> bool {
4110 match self
4111 .stake_table_coordinator
4112 .membership_for_epoch(Some(epoch))
4113 {
4114 Ok(stake_table) => stake_table.has_stake(&self.public_key),
4115 Err(err) => {
4116 warn!(%epoch, %err, "failed to get stake table");
4117 false
4118 },
4119 }
4120 }
4121
4122 fn missing_for_vote1(&self, view: ViewNumber) -> Vec<&'static str> {
4124 let mut missing = Vec::new();
4125 let proposal = self.proposals.live(view);
4126 if proposal.is_some_and(|p| {
4127 !self
4128 .states_verified
4129 .contains(&(view, proposal_commitment(p)))
4130 }) {
4131 missing.push("state_validation");
4132 }
4133 if proposal.is_none() {
4134 missing.push("proposal");
4135 }
4136 if !self.vid_shares.contains_key(&view) {
4137 missing.push("vid_share");
4138 }
4139 if let Some(proposal) = proposal {
4140 let block_number = proposal.block_header.block_number();
4141 if proposal.epoch > EpochNumber::genesis()
4142 && is_epoch_transition(block_number, *self.epoch_height)
4143 && !self.drb_results.contains_key(&(proposal.epoch + 1))
4144 {
4145 missing.push("drb_result_for_next_epoch");
4146 }
4147 let parent_view = proposal.justify_qc.view_number();
4151 if parent_view != ViewNumber::genesis()
4152 && !is_last_block(block_number.saturating_sub(1), *self.epoch_height)
4153 {
4154 let parent = self
4155 .proposals
4156 .get(parent_view, proposal.justify_qc.data().leaf_commit);
4157 let reconstructed = parent.is_some_and(|p| {
4158 let VidCommitment::V2(c) = p.block_header.payload_commitment() else {
4159 return false;
4160 };
4161 self.parent_reconstructed(parent_view, c, proposal_commitment(p))
4162 });
4163 if !reconstructed {
4164 missing.push("parent_block_reconstructed");
4165 }
4166 }
4167 }
4168 missing
4169 }
4170
4171 fn missing_for_propose(&self, view: ViewNumber) -> Vec<&'static str> {
4173 let mut missing = Vec::new();
4174
4175 let view_change_evidence = self.timeout_certs.get(&view);
4176 let parent_cert = if view_change_evidence.is_some() {
4177 match self.locked_cert.as_ref() {
4178 Some(c) => c,
4179 None => {
4180 missing.push("locked_cert");
4181 return missing;
4182 },
4183 }
4184 } else {
4185 match self.cert1_at(ViewNumber::from(view.saturating_sub(1))) {
4186 Some(c) => c,
4187 None => {
4188 missing.push("parent_cert");
4189 return missing;
4190 },
4191 }
4192 };
4193 let parent_view = parent_cert.view_number();
4194 let Some(parent_proposal) = self
4195 .proposals
4196 .get(parent_view, parent_cert.data.leaf_commit)
4197 else {
4198 missing.push("parent_proposal");
4199 return missing;
4200 };
4201
4202 let parent_commitment = proposal_commitment(parent_proposal);
4203 let header = self.headers.get(&(view, parent_commitment));
4204 if header.is_none() {
4205 missing.push("block_header");
4206 }
4207 let block_present = header
4208 .and_then(|h| {
4209 if let VidCommitment::V2(c) = h.payload_commitment() {
4210 Some(c)
4211 } else {
4212 None
4213 }
4214 })
4215 .is_some_and(|c| self.blocks.contains_key(&(view, c)));
4216 if !block_present {
4217 missing.push("block_payload");
4218 }
4219
4220 if let Some(header) = header {
4223 let first_proposal_of_epoch =
4224 is_last_block(header.block_number().saturating_sub(1), *self.epoch_height);
4225
4226 if parent_proposal.epoch > EpochNumber::genesis()
4227 && is_epoch_transition(header.block_number(), *self.epoch_height)
4228 && !self
4229 .drb_results
4230 .contains_key(&EpochNumber::new(*parent_proposal.epoch + 1))
4231 {
4232 missing.push("drb_result_for_next_epoch");
4233 }
4234
4235 if first_proposal_of_epoch && self.cert2_at(parent_view).is_none() {
4236 missing.push("next_epoch_justify_qc");
4237 }
4238 }
4239
4240 let parent_block_number = parent_cert.data.block_number.unwrap_or(0);
4241 if is_epoch_root(parent_block_number, *self.epoch_height) {
4242 match parent_cert.data.epoch() {
4243 None => missing.push("parent_cert_epoch"),
4244 Some(parent_epoch) => {
4245 if !self.state_certs.contains_key(&parent_epoch) {
4246 missing.push("state_cert");
4247 }
4248 },
4249 }
4250 }
4251
4252 missing
4253 }
4254
4255 fn set_current_epoch_max(&mut self, e: EpochNumber) {
4256 self.current_epoch = self.current_epoch.max(e)
4257 }
4258
4259 fn set_current_view_max(&mut self, v: ViewNumber) {
4260 self.current_view = self.current_view.max(v)
4261 }
4262}
4263
4264pub(crate) fn certificate_rule_admits<T: NodeType>(
4275 parent: &Certificate1<T>,
4276 tc: &TimeoutEvidence<T>,
4277 epoch: EpochNumber,
4278) -> bool {
4279 if tc.epoch() != Some(epoch) {
4280 return false;
4281 }
4282 let Some((lock, lock_cert)) = tc.lock() else {
4283 return true;
4284 };
4285 LockView::of(parent) >= lock
4286 || parent.data.leaf_commit == lock_cert.data.leaf_commit
4287 || lock.epoch < epoch
4288}
4289
4290impl<T: NodeType> ConsensusInput<T> {
4291 pub fn epoch(&self) -> Option<EpochNumber> {
4293 match self {
4294 ConsensusInput::BlockBuilt { epoch, .. } => Some(*epoch),
4295 ConsensusInput::Certificate1(cert) => Some(cert.epoch()),
4296 ConsensusInput::Certificate2(cert) => Some(cert.epoch()),
4297 ConsensusInput::AdvanceView(cert) => Some(cert.epoch()),
4298 ConsensusInput::EpochRootCertificates { cert1, .. } => Some(cert1.epoch()),
4299 ConsensusInput::TimeoutCertificate(cert) => Some(cert.epoch()),
4300 ConsensusInput::Proposal(_, proposal) => Some(proposal.proposal.data.epoch),
4301 ConsensusInput::FetchedProposal(proposal) => Some(proposal.proposal.data.epoch),
4302 ConsensusInput::Timeout(..) | ConsensusInput::TimeoutOneHonest(..) => None,
4303 ConsensusInput::DrbResult(epoch, _) => Some(*epoch),
4304 ConsensusInput::EpochChange(message) => message.cert1.epoch(),
4305 ConsensusInput::UpgradeCertificateFormed(cert) => Some(cert.epoch()),
4306 ConsensusInput::ReVote(message) => Some(message.revote.epoch),
4307 ConsensusInput::BlockReconstructed { .. }
4308 | ConsensusInput::HeaderCreated(..)
4309 | ConsensusInput::VidShare(..)
4310 | ConsensusInput::StateValidated(..)
4311 | ConsensusInput::StateValidationFailed(..)
4312 | ConsensusInput::Stored(..)
4313 | ConsensusInput::VidDisperseCreated(..) => None,
4314 }
4315 }
4316
4317 pub fn view_number(&self) -> ViewNumber {
4318 match self {
4319 ConsensusInput::BlockBuilt { view, .. } => *view,
4320 ConsensusInput::BlockReconstructed { view, .. } => *view,
4321 ConsensusInput::Certificate1(cert) => cert.view_number(),
4322 ConsensusInput::Certificate2(cert) => cert.view_number(),
4323 ConsensusInput::AdvanceView(cert) => cert.view_number() + 1,
4325 ConsensusInput::EpochRootCertificates { cert1, .. } => cert1.view_number(),
4326 ConsensusInput::HeaderCreated(view, ..) => *view,
4327 ConsensusInput::Proposal(_, prop) => prop.view_number(),
4328 ConsensusInput::VidShare(share) => share.view_number(),
4329 ConsensusInput::FetchedProposal(prop) => prop.view_number(),
4330 ConsensusInput::StateValidated(response) => response.view,
4331 ConsensusInput::StateValidationFailed(request) => request.view,
4332 ConsensusInput::Stored(stored) => stored.view_number(),
4333 ConsensusInput::Timeout(view) => *view,
4334 ConsensusInput::TimeoutOneHonest(view) => *view,
4335 ConsensusInput::TimeoutCertificate(cert) => {
4336 cert.view_number() + 1
4339 },
4340 ConsensusInput::VidDisperseCreated(view, _) => *view,
4341 ConsensusInput::DrbResult(..) => ViewNumber::genesis(),
4345 ConsensusInput::UpgradeCertificateFormed(..) => ViewNumber::genesis(),
4346 ConsensusInput::EpochChange(epoch_change) => epoch_change.cert2.view_number(),
4348 ConsensusInput::ReVote(message) => message.revote.view,
4349 }
4350 }
4351}