Skip to main content

hotshot_new_protocol/
consensus.rs

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    /// This node obtained a proposal's payload, by reconstructing it from VID
73    /// shares or fetching it from a peer. Consensus keeps the payload so a
74    /// decide of that view carries it.
75    BlockReconstructed {
76        view: ViewNumber,
77        payload_commitment: VidCommitment2,
78        payload: T::BlockPayload,
79    },
80    Certificate1(ValidCert<Certificate1<T>>),
81    Certificate2(ValidCert<Certificate2<T>>),
82    /// A quorum certificate allows us to advance our view.
83    ///
84    /// Used to help divergent nodes re-converge on restart.
85    AdvanceView(ValidCert<Certificate1<T>>),
86    /// Atomic pair emitted by the `EpochRootTally` for epoch-root views:
87    /// a `Certificate1` and its matching `LightClientStateUpdateCertificateV2`.
88    /// Consensus never sees an epoch-root Cert1 without the matching state_cert.
89    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    /// A validated proposal. Consensus parks it until this node's VID share
96    /// for the same payload arrives ([`ConsensusInput::VidShare`]) and only
97    /// processes the two together.
98    Proposal(T::SignatureKey, ProposalMessage<T, Validated>),
99    /// This node's validated VID share.
100    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    /// An `UpgradeCertificate2` assembled from broadcast upgrade votes.
111    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    /// Persist the locked QC before the matching phase-2 vote is released.
128    PersistHighQc(Certificate1<T>),
129    PersistBoundaryQc(Certificate1<T>),
130    SendTimeoutCertificate(TimeoutEvidence<T>, ViewNumber, EpochNumber),
131    SendCertificate1(Certificate1<T>),
132    /// Broadcast a first-obtained Cert2 so peers that could not assemble it
133    /// from votes can still decide. Mirrors `SendCertificate1`.
134    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        /// Certificate1 (QC) that certifies the most recent (first) leaf in the chain.
146        /// Each older leaf's cert1 is available as the next leaf's `justify_qc`.
147        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    /// A view timed out with a timeout certificate.
154    ViewTimedOut(ViewNumber),
155    /// A validated proposal met this node's VID share.
156    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    /// A proposal taken in on a certificate's word rather than paired with
165    /// this node's VID share: fetched from a peer, or parked live until a
166    /// certificate vouched for it. The share the leader owed this node may
167    /// never arrive, so this is what lets the payload be rebuilt from the
168    /// shares the voters broadcast.
169    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    /// Emitted when a node has reconstructed a block payload from VID shares.
179    /// A payload obtained before its view decides rides along in
180    /// `LeafDecided`; this event covers the rest, so downstream consumers
181    /// (e.g. the query service) can store a payload that was still missing
182    /// when the view decided.
183    BlockPayloadReconstructed {
184        view: ViewNumber,
185        header: T::BlockHeader,
186        payload: Arc<T::BlockPayload>,
187    },
188    /// Broadcast our own VID share so peers can reconstruct the block. Emitted
189    /// right after `SendVote1` so it never delays the cert-forming vote.
190    BroadcastVidShare(VidDisperseShare2<T>),
191    /// The `UpgradeLock` has been updated; the certificate must be persisted.
192    UpgradeDecided(UpgradeCertificate<T>),
193}
194
195/// What a proposal and this node's VID share for it must agree on to pair.
196///
197/// A share's epoch is supplied by its disperser and covered by no signature,
198/// so keying on it means a share naming an epoch other than its proposal's
199/// never pairs: it cannot be the one this node votes on, stores, or
200/// broadcasts, and the honest share for the view can still arrive and pair.
201///
202/// That makes the share's epoch exactly as trustworthy as the proposal's, and
203/// no more.
204type 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
213/// Views to retain decide inputs (`proposals`, `certs`, `certs2`) behind the
214/// decided view, letting a late-broadcast Cert2 decide an older gap view.
215pub(crate) const DECIDE_BUFFER: u64 = 20;
216
217/// Views earlier than the current view at which the transport stops
218/// retransmitting: the bound the coordinator gives the network's collection,
219/// and so the earliest a missing payload is worth fetching.
220pub(crate) const GC_MARGIN_VIEWS: NonZeroU64 = NonZeroU64::new(2).expect("2 > 0");
221
222// The decide buffer retains the proposals the VID reconstructor reads.
223const _: () = 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    /// Payloads this node built or obtained, which a decide attaches to its
243    /// leaves. Also gates proposing: the leader's header must have its block.
244    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    /// Views actually emitted in a `LeafDecided`; once views can decide late,
252    /// `last_decided_view` is only a high-water mark.
253    decided_views: BTreeSet<ViewNumber>,
254    /// Hard lower bound for deciding, pinned to the anchor on restart:
255    /// `decided_views` is not persisted, so a replayed certificate pair could
256    /// otherwise re-decide pre-anchor views.
257    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    /// For each view this node cast a vote1 in, the view its proposal was justified at.
267    vote1_parent: BTreeMap<ViewNumber, ViewNumber>,
268
269    /// Storage confirmations; sends are gated on these facts.
270    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    /// Highest locked QC confirmed persisted; gates release of phase-2 votes.
275    stored_high_qc: Option<LockView>,
276
277    /// Messages constructed and accounted for in `voted_*_views` /
278    /// `proposed_views`, awaiting their storage confirmations. A pending vote2
279    /// also records the locked-QC view it must see persisted before release.
280    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    /// The highest view this node has sent, or may have sent before a
287    /// restart, a lock-carrying timeout vote for.
288    ///
289    /// Under the certificate rule a node never votes2 at or before it: its
290    /// timeout vote signed a lock earlier than the commit that vote2 would help
291    /// form, and a proposal skipping that commit could use the certificate.
292    timeout_vote_bar: Option<ViewNumber>,
293
294    /// The epoch of the re-vote commit processed at each view.
295    ///
296    /// Not in `decided_views`: the next epoch may have a block of its own at
297    /// a re-vote's view.
298    revote_commits: BTreeMap<ViewNumber, EpochNumber>,
299
300    /// Validated re-vote requests by view (see [`ReVote`]).
301    revotes: BTreeMap<ViewNumber, ReVote<T>>,
302
303    /// What was requested for each re-vote view, so it is requested once.
304    revote_fetches: BTreeSet<(ViewNumber, RevoteFetch)>,
305
306    /// The payloads of timeout certificate locks this node asked for, by the
307    /// block's view and payload commitment, with the view it asked in. Each is
308    /// asked for once per view: a silent peer is replaced in the next view.
309    lock_payload_fetches: BTreeMap<(ViewNumber, VidCommitment2), ViewNumber>,
310
311    /// An assembled or adopted upgrade certificate, kept until a leader turn
312    /// attaches it, the upgrade decides, or `decide_by` passes.
313    formed_upgrade_certificate: Option<ValidCert<UpgradeCertificate2<T>>>,
314
315    /// View of the decided leaf whose upgrade certificate is in the
316    /// `UpgradeLock` (see [`Self::maybe_decide_upgrade`]).
317    decided_upgrade_carrier: Option<ViewNumber>,
318
319    timeout_view: ViewNumber,
320    /// Highest view this node may have acted in before a restart (from the
321    /// persisted action log). Bars re-*recording* a view's Vote action, not
322    /// re-casting the phase-2 vote itself (see [`Self::vote2_persisted`]).
323    restart_barred_view: ViewNumber,
324    current_view: ViewNumber,
325    current_epoch: EpochNumber,
326
327    // TODO: We need a next epoch stake table to handle the transition
328    // And a way to set these stake tables, probably an event from coordinator
329    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/// What a re-vote can lack and fetch: the voted block's proposal, or its payload.
345#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
346enum RevoteFetch {
347    Proposal,
348    Payload,
349}
350
351/// A vote2 awaiting its storage confirmations.
352struct PendingVote2<T: NodeType> {
353    vote: Vote2<T>,
354    /// The lock that must be persisted before the vote is released.
355    lock: LockView,
356    /// The view the voted block was proposed at; a re-vote's is earlier.
357    block_view: ViewNumber,
358}
359
360/// A timeout vote awaiting its storage confirmation.
361type PendingTimeoutVote<T> = (TimeoutVote<T>, Option<CatchupEvidence<T>>);
362
363/// Protocol flow directive.
364enum Protocol {
365    /// Stop with further protocol steps.
366    Abort,
367    /// Continue with protocol.
368    Continue,
369}
370
371/// Reason a proposal failed the safety/liveness rule.
372#[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    /// Seed a parent certificate and proposal so the leader of the *next* view
480    /// can propose without any external bootstrap injection.
481    /// Sets the locked certificate and current epoch. After calling this, a
482    /// subsequent `apply` that triggers `maybe_propose` will find the
483    /// parent cert and proposal it needs.
484    ///
485    /// `reconstructed` are `(view, V2 commitment)` pairs to record as
486    /// already reconstructed blocks. During normal operation this set is
487    /// populated as VID shares arrive; on restart it starts empty, but the
488    /// persisted leaf and proposals correspond to blocks this node had already
489    /// reconstructed in the previous process. Seeding them lets a restarted
490    /// leader satisfy the `parent_block_reconstructed` check for its first
491    /// proposal/vote instead of stalling.
492    pub fn seed_parent(
493        &mut self,
494        cert1: Certificate1<T>,
495        proposal: Proposal<T>,
496        reconstructed: impl IntoIterator<Item = (ViewNumber, VidCommitment2)>,
497    ) {
498        // A decided last block ends its epoch.
499        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        // The seed cert comes from persistent storage, so its lock is already persisted.
505        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    /// Seed proposals loaded from storage on restart, keeping each one's signature.
521    ///
522    /// Fetches are answered only with signed proposals, such as the epoch's last block
523    /// a re-vote needs.
524    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    /// Seed proposals loaded from storage on restart so the decide chain-walk
536    /// can follow `justify_qc` back through views the node had already seen,
537    /// and so `maybe_vote_1`/`maybe_propose` can find the parent of the first
538    /// post-restart proposal (otherwise never re-fetched).
539    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    /// Restore the locked QC persisted on a prior run. Called after
546    /// `seed_parent`: the persisted lock can be newer than the decided-anchor
547    /// QC, and restoring it keeps the node from voting against a block it had
548    /// already locked. The loaded value is persisted, so it also advances the
549    /// persistence watermark.
550    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            // A node is at least in its lock's epoch, which may be later than the anchor's.
560            self.set_current_epoch_max(lock.epoch);
561            self.locked_cert = Some(cert1);
562        }
563    }
564
565    /// Seed the boundary certificate persisted on a prior run (see
566    /// [`ConsensusOutput::PersistBoundaryQc`]), so that a restarted node can
567    /// still lead a re-vote of the boundary block or the next epoch's first
568    /// block.
569    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    /// Seed the `Certificate2` persisted for the anchor on a prior run, so that
576    /// a node restarted on the last block of an epoch can lead the next
577    /// epoch's first block.
578    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    /// Insert a cert2 and return if it is new for its view and epoch.
597    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    /// Advance the locked-QC persistence watermark to `lock` if it is newer.
647    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    /// Whether the locked QC `required` is persisted.
654    fn high_qc_persisted(&self, required: LockView) -> bool {
655        self.stored_high_qc.is_some_and(|stored| stored >= required)
656    }
657
658    /// Whether a certificate at `view` is of an epoch earlier than this node's,
659    /// under the certificate rule: one of the outgoing committee's late
660    /// re-votes, which this node does not lock on.
661    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    /// The proposal a certificate at `view` over `leaf_commit` certifies, with
670    /// the view it was proposed at.
671    ///
672    /// That is `view` itself, except for a re-vote, which certifies the last
673    /// block of an epoch again at a later view.
674    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    /// A `Certificate2` this node holds over `leaf_commit`, at `from` or later.
696    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    /// Whether the parent block counts as reconstructed for voting: this node
708    /// holds its payload, or its lock certifies the parent leaf.
709    ///
710    /// A lock is taken only on a reconstructed block, so a restarted node can
711    /// vote on the first proposal built on its restored lock. The exception is
712    /// an epoch's last block taken from an epoch change, whose only child opens
713    /// the next epoch and needs no parent payload. The leaf, which covers the
714    /// block's view, is compared rather than the view, since a re-vote's
715    /// certificate is at a later view than its block.
716    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    /// Seed a state certificate loaded from storage on restart, so a leader
731    /// proposing on an epoch-root parent QC right after a restart does not
732    /// stall on a missing state_cert.
733    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    /// The proposal with the highest view below `view`, if any.
746    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    /// The `Certificate1` of the latest epoch this node holds one of at `view`.
752    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    /// The `Certificate2` of the latest epoch this node holds one of at `view`.
760    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    /// The highest certificate we hold: the locked QC or the latest timeout
768    /// certificate, whichever has the higher view (ties go to the timeout
769    /// certificate).
770    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    /// This node's signed VID share of `proposal`'s payload, if it holds one.
783    ///
784    /// The share held for a view is the one of the proposal the node paired it
785    /// with, which need not be `proposal` when the leader equivocated.
786    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    /// Return the timeout certificate that advanced consensus to `view`, if
804    /// any. Keyed by the view it advanced *into* (i.e. one greater than the
805    /// view it certified as timed out).
806    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    /// Newest view that can no longer be decided (and below which decide
816    /// inputs are dropped): slides [`DECIDE_BUFFER`] behind the watermark,
817    /// pinned at the restart anchor.
818    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    /// Whether `view` is decided or at or below the decide floor, so that no
826    /// block of it is wanted any more.
827    ///
828    /// A view between the floor and the last decided view that is not decided
829    /// is a gap, and its block is still wanted: a later decide fills it in
830    /// (`maybe_decide_at`).
831    fn decided_or_past(&self, view: ViewNumber) -> bool {
832        view <= self.decide_floor() || self.decided_views.contains(&view)
833    }
834
835    /// Apply consensus to the given input and collect protocol outputs.
836    #[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        // DRB results and formed upgrade certificates arrive asynchronously
839        // with no specific view attached. Use `current_view` so that the
840        // post-apply retries (`maybe_propose`, `maybe_vote_*`) target the view
841        // the node is actually on.
842        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                // Store state_cert first so the subsequent Cert1 handler / leader
898                // proposer has it on hand. Atomicity invariant: this pair always
899                // arrives together; Consensus never sees the Cert1 alone.
900                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                // Retry the votable children whose vote1 is gated on this
923                // parent's reconstruction. More than `view + 1` can be
924                // waiting: while a view's payload was missing, every later
925                // proposal extends it, and of those only the ones later than
926                // the timeout bar can still be voted. Latest first, stopping
927                // at the one we vote for: a vote at an earlier child adds
928                // nothing and costs its vote2, which the later vote1 skips.
929                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        // An event from the current view or the previous view can trigger a propose
1082        self.maybe_propose(view + 1, outbox);
1083        // A lock that moved, or a block that arrived, can unblock the leader
1084        // of the current view and a pending re-vote.
1085        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    /// On restart, bar voting and proposing in every view this node may have
1130    /// acted in by raising `timeout_view` (vote1, propose) and
1131    /// `restart_barred_view` (vote-action re-recording), pin the decide floor
1132    /// at the anchor, and place the view cursor just past the high QC.
1133    /// Forward-only: never regresses any view.
1134    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        // Which views this node submitted its vote1 in is not persisted, only how far
1155        // it acted. A vote1 needs its proposal stored, so treat every seeded proposal
1156        // at or below the bar as one, this node may have voted for. Without that, a
1157        // vote2 re-cast after the restart could land in a view whose branch this node
1158        // had already left behind. Where a view holds more than one, which of them the
1159        // vote was for is not known either, so the earliest justify view is kept: it
1160        // is the one that bars the most vote2s.
1161        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        // `Coordinator::start` enters `current_view + 1`, so parking the cursor
1169        // at the high QC makes the node re-enter at `high_qc + 1`.
1170        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    /// Whether a proposal for `view` in `epoch` can still matter to this node.
1177    ///
1178    /// One at or before the decide floor cannot be decided any more. Under the
1179    /// certificate rule, one behind the lock is kept: it gets no vote1
1180    /// (`maybe_vote_1`), but a `Certificate2` for its view may still arrive and
1181    /// decide it. Before the certificate rule, one behind the lock fails
1182    /// `is_safe` and is not wanted. Locks are ordered by epoch, then view, so a
1183    /// proposal of a later epoch is wanted even if a lock of an earlier one is at
1184    /// a later view.
1185    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        // A proposal may already be in `self.proposals` because we received
1197        // an EpochChangeMessage for it (which carries the proposal but no
1198        // vid_share and does not trigger state validation).  In that case we
1199        // still want to process the real proposal message so handle_proposal
1200        // runs — it populates vid_shares and emits RequestState.
1201        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    /// The proposal held at `view` that matters most, if any.
1209    ///
1210    /// The one a held Cert2 names, else the one a held Cert1 names, else the one
1211    /// this node paired with its own VID share, else any. A view normally holds
1212    /// one proposal; the order only matters when a leader equivocated.
1213    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    /// The epoch change that ended `epoch`, if this node holds it.
1248    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    /// The proposals to answer a fetch for `view` with.
1274    ///
1275    /// That is the one proposed at `view`, and the blocks `view`'s
1276    /// certificates certify if they are others. A re-vote's certificate
1277    /// certifies a block of an earlier view, and a node holding only the
1278    /// certificate knows no other view to ask for.
1279    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    /// Garbage-collect per-view state.
1306    ///
1307    /// The decide inputs (`proposals`, `certs`, `certs2`, deferred certs,
1308    /// `decided_views`) survive down to [`Self::decide_floor`] so a late
1309    /// Cert2 can still decide a gap view. Parked proposal halves survive as
1310    /// long, so such a Cert2 can adopt one instead of fetching.
1311    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    /// Whether a view that timed out keeps its payload and shares.
1386    ///
1387    /// A view holding a certificate is likely to decide soon. So is the last
1388    /// block of an epoch: a re-vote commits it at a later view.
1389    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    /// Test-only: forcibly replace the proposal stored at `view`.
1417    ///
1418    /// Used to simulate a node that holds only a proposal no certificate names
1419    /// at `view`, the certified one having never arrived. No production code
1420    /// should ever do this.
1421    #[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    /// Pair a validated proposal with this node's VID share for the same payload.
1428    ///
1429    /// The half arriving first is parked under its [`PairingKey`].
1430    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            // Under the certificate rule, a `Certificate2` that arrived first
1447            // vouches for it as one arriving later would (`handle_certificate2`).
1448            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    /// Pair this node's VID share with a validated proposal for the same payload.
1461    ///
1462    /// The half arriving first is parked under its [`PairingKey`].
1463    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        // Refuse the share before `ProposalPaired` persists it and seeds the
1488        // reconstructor with its common.
1489        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        // QC can be for a different epoch
1532        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        // Under the certificate rule a vote1 is checked against the proposal's
1541        // timeout certificate, not against this node's lock (`maybe_vote_1`).
1542        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        // Store the proposal before the DRB check so it is not lost when
1555        // the DRB is not yet available (e.g. a node catching up after a
1556        // restart).  Voting is deferred to `maybe_vote_1` which verifies
1557        // the DRB before casting a vote.
1558        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        // The certificates a proposal carries were verified with it and count
1566        // as received, at every version: a valid certificate is valid whatever
1567        // message carries it, and the vote2 rules still apply to it. This is
1568        // what lets a node that missed a certificate's broadcast catch up from
1569        // the next proposal.
1570        //
1571        // The first block of an epoch carries its parent's commit, which
1572        // decides the previous epoch's last block.
1573        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        // The parent's certificate and the timeout certificate were verified
1581        // with the proposal, and put this node in the proposal's view.
1582        let parent_epoch = LockView::of(&proposal.justify_qc).epoch;
1583        self.advance_on(proposal.justify_qc.clone(), parent_epoch, outbox);
1584        // Decided only now: a re-vote's commit is at a later view than the
1585        // block, and deciding it needs a `Certificate1` over the block, which
1586        // the parent's certificate may be the first of.
1587        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        // Request the DRB if we don't have it yet.  A mismatching DRB is
1605        // a hard failure (invalid leader), but a missing DRB is
1606        // recoverable — the proposal is stored and voting will proceed
1607        // once the DRB arrives.  Same epoch guard as `maybe_propose`:
1608        // transitions in epoch >= 2 (`> genesis`) carry `next_drb_result`
1609        // (the successor epoch's DRB lives in this leaf, so the successor
1610        // epoch's catchup path unwraps `leaf.next_drb_result`).
1611        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    /// Take in a proposal this node did not receive live together with its
1648    /// VID share: one fetched from a peer, or one parked in
1649    /// `unpaired_proposals` that a certificate has since vouched for. Its
1650    /// parked ancestors come with it, oldest first, so each finds its parent
1651    /// stored rather than fetching it.
1652    #[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            // The proposal itself may now be decidable (e.g. cert2 arrived first
1666            // and triggered the fetch).
1667            self.maybe_decide(view, outbox);
1668            // Views extending it may be blocked on it.
1669            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    /// `message` followed by its parked ancestors, nearest first, up to the
1686    /// first parent this node already holds or has nothing parked for.
1687    ///
1688    /// A loop, not recursion through `request_parent_proposal_if_missing`: the
1689    /// chain is as long as the run of views parked without a share.
1690    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        // Parked ancestors are stored ahead of this proposal
1735        // (`adopt_certified_proposal`), so this only fetches a parent nothing
1736        // parked can supply.
1737        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    /// Adopt the live proposal parked for `view` awaiting this node's VID
1748    /// share, if it is the leaf `leaf_commit` names. Returns whether one was adopted.
1749    ///
1750    /// The proposal stays parked: a share arriving later still pairs with it and
1751    /// runs the live path, the only one that stores the share for `maybe_vote_1`.
1752    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    /// The live proposal parked awaiting this node's VID share that a
1768    /// certificate at `view` over `leaf_commit` certifies.
1769    ///
1770    /// As for [`Self::certified_proposal`], that is one parked for `view`
1771    /// itself, except for a re-vote's certificate, which certifies the last
1772    /// block of an epoch again at a later view.
1773    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    /// The payload size of an adopted proposal, if a share for it has arrived.
1819    /// `None` makes state validation skip the checks that need the size; a
1820    /// quorum already certified the proposal, so they have been run elsewhere.
1821    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    /// Queue a block and header request, stamping when it enters the outbox.
1860    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    /// Ask for the payload of the view every proposal we could vote for is
1874    /// parented at.
1875    ///
1876    /// Leaders build on their lock, so the parent of the latest proposal we
1877    /// hold is that view, and it is the one view peers still retain: a node
1878    /// holding the payload of a certified later view would have locked there
1879    /// itself, so no peer keeps one. Its certificate is the proposal's
1880    /// `justify_qc`, which is also the only record of it for a node that
1881    /// missed the view's votes and the certificate broadcast that followed
1882    /// them.
1883    ///
1884    /// Views at or earlier than the lock cannot be what blocks us: safety
1885    /// pins the justify_qc of any proposal we may still vote for at or later
1886    /// than the lock, and a parent equal to the lock counts as reconstructed.
1887    /// Later than `current_view - GC_MARGIN_VIEWS` a missing share broadcast
1888    /// may still arrive on its own, and fetching would race it for a whole
1889    /// payload.
1890    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        // A view decided as an ancestor of a later one needs no vote from us,
1908        // so its payload no longer blocks anything. Its proposal outlives the
1909        // decide by the decide buffer, which would otherwise keep it a
1910        // candidate for as long as the lock stays put.
1911        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        // Only a parent behind the proposal is adopted; adopting for a forward
1948        // QC could re-enter adoption without end.
1949        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        // Relay a first-obtained Cert2 so peers that missed the vote2s can
1999        // still decide. Skip decided views so a GC'd-then-re-received Cert2
2000        // cannot ping-pong between nodes forever.
2001        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    /// Adopt the successor-epoch DRB carried by the transition leaf at `view`,
2022    /// once we hold both its proposal and a QC certifying it. A Cert1 over the
2023    /// leaf is a quorum's endorsement of its `next_drb_result`, so a catching-up
2024    /// node can use it without waiting on a separate successor-epoch catchup.
2025    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        // Only transition leaves in epoch >= 1 carry the next epoch's DRB.
2035        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    /// Lift the upgrade certificate attached to a stored proposal, so a node
2077    /// that never saw the upgrade votes can still re-attach it when leading.
2078    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    /// The upgrade certificate to attach to this node's proposal at `view`.
2101    /// The `Leaf2` carries it without its epoch, so validators require it to
2102    /// bind the carrying proposal's epoch.
2103    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    /// Decide the upgrade when a decided leaf carries a certificate within
2116    /// its `decide_by` deadline: flip the shared `UpgradeLock` and emit
2117    /// [`ConsensusOutput::UpgradeDecided`] so the certificate is persisted.
2118    ///
2119    /// The chain can carry more than one certificate and gap fills decide
2120    /// carriers out of order, so the *earliest* carrying leaf wins: an
2121    /// earlier carrier overrides a later one.
2122    fn maybe_decide_upgrade(
2123        &mut self,
2124        decided: &[Leaf2<T>],
2125        outbox: &mut Outbox<ConsensusOutput<T>>,
2126    ) {
2127        // A certificate restored from storage has no known carrier view; its
2128        // carrier is at or below the decided anchor, so never override it.
2129        if self.decided_upgrade_carrier.is_none()
2130            && self.upgrade_lock.decided_upgrade_cert().is_some()
2131        {
2132            return;
2133        }
2134        // `decided` is ordered newest first.
2135        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    /// Advance our view based on a quorum certificate.
2168    #[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    /// Keep `cert1` of `epoch`, vote2 and lock on it if this node can, and
2180    /// move to the view after it.
2181    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        // Ensure we submit a vote2 if we can:
2196        self.maybe_vote_2_and_update_lock(view, outbox);
2197        self.maybe_decide(view, outbox);
2198
2199        // Every `Cert1` is grounds for the view after it, an earlier epoch's
2200        // included: a node that ignored one would split from the nodes that took
2201        // it before the epoch change, and the two halves could time out
2202        // different views for ever.
2203        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        // Vote-side diagnostics fire when we weren't the leader, or when we
2255        // were the leader and did propose: in either case the next thing
2256        // expected of us was voting on a proposal we received.
2257        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 a cert1 already formed for this view, the holdup is one step
2280        // further along: we need the reconstructed block to match the
2281        // proposal's payload commitment before we can vote2 and update lock.
2282        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                    // Non-V2 commitment shouldn't happen at this point in the
2301                    // protocol; log it loudly if it does.
2302                    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    /// Sign a timeout vote that carries this node's lock, and send it once
2348    /// its action is recorded.
2349    ///
2350    /// The bar is raised first, so from now on this node does not vote2 at
2351    /// `view` or before it, and recording the action keeps the bar where it
2352    /// is across a restart (see [`Self::timeout_vote_bar`]).
2353    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        // The genesis QC is no lock: nothing earlier could be skipped.
2361        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        // Counted from signing: a vote held until its action is stored still goes out.
2381        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    /// Whether this node signed a timeout vote for `view` in `epoch`, or in any
2420    /// epoch for a certificate that does not bind one.
2421    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        // A node that voted in the view answered the one-honest indication
2471        // with its vote, which the others count.
2472        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    /// Take in the lock the timeout certificate entering `view` carries.
2497    ///
2498    /// The lock may be later than this node's own: a leader must build on a
2499    /// block no earlier than it, and this node may have to lock on it to vote.
2500    /// Its block and payload are fetched if missing. Only a recent lock
2501    /// (`TimeoutLock::Recent`), the one the certificate's verification
2502    /// checked, is taken in.
2503    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        // The block is at its own certificate's view; a re-vote commits it at a
2565        // later view, after which the next epoch starts.
2566        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        // Compare epochs (not views) so a node that timed out past a boundary
2578        // it never saw can still recover via a genuinely new epoch change.
2579        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        // A lock of the next epoch means this node is past the boundary, and
2589        // a later lock of the same epoch over another block means the message
2590        // is stale. A later lock over the block itself is a re-vote's.
2591        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        // Taken in on the certificates' word like a fetched proposal, and this
2604        // node may hold no share of it.
2605        outbox.push_back(ConsensusOutput::ProposalCertified(proposal.clone()));
2606
2607        // Locked on without the payload, which the commit shows a quorum had,
2608        // so that a node new to the incoming committee can lock.
2609        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        // The view after the boundary is relabelled with the next epoch even
2626        // if this node is in it already.
2627        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        // Request block and header if we're the first leader of the next epoch
2635        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            // Keyed by the certificate's leaf, not by view alone: another proposal
2679            // of the same view may be held, and a header built on it would carry a
2680            // wrong block number.
2681            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        // The first block of an epoch needs a commit of its parent, and names
2701        // the parent's own certificate.
2702        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            // Voters refuse a proposal of an epoch earlier than their own, and
2748            // one whose parent is earlier than its timeout certificate's lock.
2749            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            // The header request issued on the TC targeted the lock held at
2766            // that moment; if the lock moved since, re-request. The same goes
2767            // for the first block of an epoch, whose parent's commit may come
2768            // after the view started. The block builder dedups by (view, parent).
2769            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        // Epoch 1 is the genesis epoch and has no successor that needs a
2800        // DRB from a transition leaf — the membership seeds DRBs for
2801        // `first_epoch` and `first_epoch + 1`.  Every epoch beyond that
2802        // communicates its successor's DRB via `next_drb_result` on each
2803        // leaf in its transition zone; the successor epoch's catchup
2804        // unwraps that field, so leaving it `None` here would panic
2805        // peers that fetch this leaf.
2806        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                // Keep retrying — the epoch manager dedups pending requests,
2812                // but if an earlier catchup failed (e.g. Leaf2Fetcher
2813                // timeout under CPU load) nothing else kicks the request.
2814                outbox.push_back(ConsensusOutput::RequestDrbResult(proposal.epoch + 1));
2815                return;
2816            };
2817            Some(*drb)
2818        } else {
2819            None
2820        };
2821        // If the parent QC is for an epoch-root block, attach the state_cert.
2822        // By the atomicity invariant (enforced by `EpochRootTally`),
2823        // if we hold the epoch-root Cert1 then `state_certs` also holds the
2824        // matching cert.
2825        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        // Sign the proposal
2860        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    /// The certificate a proposal at `view` would name as its parent.
2894    ///
2895    /// Without a timeout that is the certificate of the view before. After
2896    /// one it is this node's lock, except under the certificate rule a leader
2897    /// holding a commit of an epoch's last block builds the next epoch's
2898    /// first block on it, even when locked on a re-vote's certificate.
2899    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    /// The own `Certificate1` of the latest last block of an epoch at or
2919    /// after `epoch` that this node holds a commit of.
2920    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    /// The commit of the last block of an epoch, at `block_view` over
2942    /// `leaf_commit`, that the next epoch's first block at `view` can carry:
2943    /// one at the block's view or later, and before `view`.
2944    ///
2945    /// Without a timeout certificate the first block follows its parent
2946    /// directly, so only the block's own commit can come before it.
2947    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    /// Send a re-vote request for the last block of an epoch at `view`, if
2965    /// this node leads `view` in that epoch.
2966    ///
2967    /// `block`, proposed at `block_view`, has a `Certificate1` but no commit
2968    /// this node knows of, and `cert1` is the one over it this node builds on:
2969    /// the block's own or a re-vote's. The request names `cert1`.
2970    ///
2971    /// The request goes out in the view after `cert1`'s, not only after a
2972    /// timeout `tc`. Faulty nodes can assemble a re-vote's `Certificate1` from
2973    /// a minority of honest vote1s, hand it to the next leader before it times
2974    /// the view out and to the others after they did. No timeout certificate
2975    /// for that view then forms, and the others can no longer vote2 in it. A
2976    /// leader that waited for a timeout certificate could not ask again, and
2977    /// with faulty and honest leaders alternating the boundary would stall
2978    /// for ever.
2979    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        // A re-vote's `Certificate2` commits the block too, and voters refuse
3006        // to vote on it again.
3007        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    /// Decide on each `Certificate2` at `view`, earliest epoch first.
3045    #[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        // Any still-undecided view above the floor can decide, even one older
3063        // than the watermark (a gap).
3064        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            // `view` is not marked decided: the next epoch may have a block
3079            // of its own there.
3080            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        // A Cert2 can arrive before its Cert1; require both before mutating any
3092        // decided state. A re-vote's Cert1 stands in for the block's own.
3093        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        // Handle Epoch Change by broadcasting the epoch change message if we have
3104        // all the data we need.
3105        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        // we have a second certificate, and matching proposal, it is decided.
3113        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        // A missing ancestor is a gap; a later Cert2 for it fills it in.
3126        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        // A gap-fill decide of an older view must not move the watermark backward.
3147        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    /// Build a `LightClientStateUpdateVote2` for an epoch-root leaf.
3166    ///
3167    /// Computes the `LightClientState` from the header, fetches the next-epoch
3168    /// stake-table commitment, and signs both the LCV2 (pre-upgrade, for
3169    /// backward compatibility with existing relay infrastructure) and LCV3
3170    /// (current) Schnorr signatures.
3171    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                // A newly persisted lock can unblock vote2 across many views; re-check all.
3236                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        // A re-vote's block was proposed, and stored, at an earlier view.
3266        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    /// Whether the view's Vote action and VID share are persisted, gating the
3335    /// phase-2 vote. A restart-barred view persisted both before the crash:
3336    /// its vote is re-cast, but its Vote action is never re-recorded. A
3337    /// re-vote disperses nothing, so it has no share of its own to wait for.
3338    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    /// See [`Self::timeout_vote_bar`].
3345    fn timed_out_for_vote2(&self, view: ViewNumber) -> bool {
3346        self.timeout_vote_bar.is_some_and(|bar| view <= bar)
3347    }
3348
3349    /// Hold `proposal` together with its leaf, and return its commitment.
3350    ///
3351    /// Every held proposal above the decided view has a leaf, which is what
3352    /// `undecided_leaves` reads. Leaves are pruned at the decided view,
3353    /// proposals only at the decide floor.
3354    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    /// Hold `proposal` and its leaf under `commit`, which need not be the
3361    /// proposal's own commitment; only for the anchor, see `seed_parent`.
3362    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    /// Whether a vote1 in a later view endorsed a branch with no block at `view`.
3370    ///
3371    /// This is what keeps one node out of two conflicting quorums. A vote2 certifies
3372    /// `view` for good, but the certificate and the reconstructed block it waits on
3373    /// can arrive after the node has submitted vote1 elsewhere. Casting it anyway
3374    /// would count the node towards a quorum committing `view` and towards one
3375    /// certifying a branch without it.
3376    ///
3377    /// This covers the order vote1-then-vote2. The reverse is the `is_safe`
3378    /// re-check in `maybe_vote_1`; neither alone is enough.
3379    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        // A re-vote of an epoch's last block and the next epoch's first block
3446        // may share a view; each takes the vote1 of its own epoch.
3447        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        // A proposal behind the lock is kept only to be decided.
3467        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        // The counterpart of `voted_for_branch_excluding`, for the other order.
3502        // `handle_proposal_with_vid_share` checked this proposal against the lock
3503        // as it stood on arrival, and state validation can complete after that.
3504        // A vote2 in the meantime locks this node on a view the proposal's
3505        // branch may skip, and voting here would then count it towards a quorum
3506        // certifying that branch and towards the one committing the view.
3507        } 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        // Don't vote for epoch-transition proposals until we can verify
3516        // the attached DRB result.  Same guard as `maybe_propose`:
3517        // transitions in epoch >= 2 must carry `next_drb_result`.
3518        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        // Verify parent chain unless justify_qc is the genesis QC
3542        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            // Parent must be reconstructed (see `parent_reconstructed`).
3571            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    /// Vote2 and lock on each `Certificate1` at `view`, earliest epoch first,
3637    /// so that the lock ends on the latest.
3638    #[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        // A re-vote certifies a block of an earlier view; its payload is the
3677        // one at the block's own view.
3678        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        // We have a valid certificate, proposal, and reconstructed block
3716        // We can now update the lock, change view and vote. A node does not
3717        // lock on a late re-vote of an epoch it has left: its timeout votes
3718        // would carry a lock later than their view, which peers drop.
3719        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            // The view the block was proposed at, which payloads are kept by.
3730            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            // Persist the new lock; `release_vote2` gates the phase-2 vote on it.
3739            outbox.push_back(ConsensusOutput::PersistHighQc(cert1));
3740        }
3741        // The block's own certificate, which opens the next epoch, also when the
3742        // lock is already a re-vote's later certificate over the block.
3743        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        // A node votes for no epoch earlier than its own: a re-vote of the
3766        // epoch it left would otherwise still take its vote2.
3767        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        // Lock is set above and >= view; the vote waits until it is persisted.
3800        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    /// The certificate rule's checks on a vote1, in place of the voter's own votes:
3826    /// - a node votes for no epoch earlier than its own;
3827    /// - after a timeout, the parent must be admitted by the timeout
3828    ///   certificate's lock ([`certificate_rule_admits`]);
3829    /// - the first block of an epoch names its parent's own `Certificate1`,
3830    ///   and the node must hold that parent. The commit of it the proposal
3831    ///   carries was verified with the proposal.
3832    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    /// Take in a validated re-vote request (see [`ReVote`]).
3855    ///
3856    /// Its certificate, the block's own or a re-vote's, is taken in as any
3857    /// `Cert1` is (`advance_on`): grounds for the view after it, kept, and locked
3858    /// on. Its timeout certificate, if any, moves this node into the re-vote's
3859    /// view. The vote is cast by [`Self::maybe_vote_revote`] once the node holds
3860    /// the block.
3861    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        // A node votes for no epoch earlier than its own, and an earlier
3872        // epoch's timeout certificate must not fill the view for its own.
3873        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    /// Vote1 on the re-vote at `view`, with the last block's own vote data.
3889    ///
3890    /// The rules are those of any vote1 under the certificate rule. The node
3891    /// must also hold the block and its payload, which it fetches once if
3892    /// missing.
3893    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        // The request may name a re-vote's certificate, at a later view than
3915        // the block.
3916        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    /// Retry the vote2, lock and decide on the re-vote certificates over the
3976    /// block proposed at `block_view`, whose block, payload or own
3977    /// `Certificate1` just arrived.
3978    ///
3979    /// A re-vote's `Certificate2` commits the block at a later view, so the
3980    /// post-apply decide at `block_view` does not reach it.
3981    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    /// Retry what a block or payload arriving can unblock for re-votes: the
4015    /// vote1 on a pending request, and the vote2, lock and decide on a
4016    /// re-vote's certificates.
4017    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            // Locked certificate is not set which means it is at genesis
4050            debug!("at genesis");
4051            return Ok(());
4052        };
4053
4054        // cert1 + block arrived before proposal
4055        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    /// Format the leader's key prefix for the given view/epoch, or `"unknown"`
4089    /// when the stake table is not available.  Used by timeout logging so a
4090    /// reader can immediately see which validator failed to make progress.
4091    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    /// Used for logging.  Returns a list of checks that failed for the given trying to vote1
4123    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            // Parent-chain reconstruction is only required for non-genesis,
4148            // non-last-block-of-epoch proposals (matches the gate in
4149            // `maybe_vote_1`).
4150            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    /// Used for logging.  Returns a list of checks that failed for the given trying to propose
4172    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        // The epoch-transition checks below all key off the proposed block
4221        // number, so we can only evaluate them once we have the header.
4222        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
4264/// Whether a proposal of `epoch` naming `parent` may follow `tc` under the
4265/// certificate rule.
4266///
4267/// The timeout certificate must be of the proposal's epoch, and the parent no
4268/// earlier than the certificate's lock, ordered by epoch, then view. A parent
4269/// over the same block as the lock passes at any view: a re-vote certifies a
4270/// block again at a later view, and the first block of the next epoch names
4271/// the block's own certificate. So does any parent if the lock is of an
4272/// earlier epoch than the proposal, which lets the first view of an epoch
4273/// recover from a timeout.
4274pub(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    /// The epoch this input carries, where it carries one.
4292    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            // We advance from the certificate's view v to v + 1:
4324            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                // Add one because we are moving to the next view so all event
4337                // processing is for the next view
4338                cert.view_number() + 1
4339            },
4340            ConsensusInput::VidDisperseCreated(view, _) => *view,
4341            // DRB results and formed upgrade certificates arrive
4342            // asynchronously and don't belong to any particular view; `apply`
4343            // handles routing by using `current_view` for these variants.
4344            ConsensusInput::DrbResult(..) => ViewNumber::genesis(),
4345            ConsensusInput::UpgradeCertificateFormed(..) => ViewNumber::genesis(),
4346            // The view of the commit, so that the post-apply steps decide it.
4347            ConsensusInput::EpochChange(epoch_change) => epoch_change.cert2.view_number(),
4348            ConsensusInput::ReVote(message) => message.revote.view,
4349        }
4350    }
4351}