Skip to main content

hotshot_new_protocol/
coordinator.rs

1pub mod error;
2pub(crate) mod metrics;
3pub mod timer;
4
5use std::{
6    collections::{BTreeMap, HashMap},
7    sync::Arc,
8    time::{Duration, Instant},
9};
10
11use bon::{Builder, bon};
12use committable::Commitment;
13use hotshot_types::{
14    consensus::{ConsensusMetricsValue, ParticipationTracker},
15    data::{EpochNumber, Leaf2, VidCommitment, VidCommitment2, ViewChangeEvidence2, ViewNumber},
16    epoch_membership::EpochMembershipCoordinator,
17    initializer::HotShotInitializer,
18    message::{Proposal as SignedProposal, UpgradeLock},
19    simple_certificate::{
20        OneHonestThreshold, SuccessThreshold, TimeoutCertificate2, TimeoutCertificate3,
21        TimeoutEvidence, UpgradeCertificate2,
22    },
23    simple_vote::{HasEpoch, QuorumVote2, TimeoutVote2},
24    traits::{
25        BlockPayload,
26        block_contents::BlockHeader,
27        metrics::Metrics,
28        node_implementation::NodeType,
29        signature_key::{SignatureKey, StateSignatureKey},
30    },
31    upgrade_config::UpgradeConfig,
32    utils::{is_epoch_root, is_last_block},
33    vote::{HasViewNumber, Vote},
34};
35use time::OffsetDateTime;
36use tokio::{
37    select,
38    sync::{mpsc, oneshot},
39};
40use tracing::{debug, error, info, warn};
41use versions::Version;
42
43use crate::{
44    block::{BlockAndHeaderRequest, BlockBuilder, BlockBuilderConfig},
45    cert_verifier::CertVerifiers,
46    client::{ClientApi, ClientRequest, CoordinatorClient, QueryError},
47    consensus::{Consensus, ConsensusInput, ConsensusOutput, GC_MARGIN_VIEWS},
48    coordinator::{
49        error::{CoordinatorError, ErrorSource, Severity},
50        timer::Timer,
51    },
52    epoch::{EpochManager, EpochRootResult},
53    fetch::{Fetcher, Retry},
54    helpers::{ViewEpoch, proposal_commitment, validated_state_cert},
55    leader_trace::LeaderTracerHandle,
56    logging::KeyPrefix,
57    message::{
58        self, BlockMessage, CatchupEvidence, Certificate1, Certificate2, ConsensusMessage, Message,
59        MessageType, OpaqueMessage, Proposal, ProposalFetchMessage, ProposalMessage, TimeoutVote,
60        TransactionMessage, Unchecked, Validated, Vote2, payload::PayloadFetchMessage,
61    },
62    network::Cliquenet,
63    outbox::Outbox,
64    proposal::{ProposalValidator, VidShareValidator},
65    serve::Server,
66    state::{HeaderRequest, StateEntry, StateManager, StateManagerOutput},
67    storage::{NewProtocolStorage, Storage},
68    upgrade::UpgradeProtocol,
69    vid::{
70        BuiltDispersal, ObtainedPayload, PinnedBy, VidDisperseRequest, VidDisperser,
71        VidFragmentAccumulator, VidReconstructor, expected_vid_param,
72    },
73    vote::{EpochRootTally, SimpleTally, TimeoutTally, UpgradeTally, VoteCollector},
74};
75
76/// Views to retain in the VID reconstructor behind the decided view
77///
78/// A decide can land while an earlier view's payload is still being
79/// reconstructed, and GC at the decided view would abort that task.
80/// A decide proves a quorum reconstructed the payload
81/// so it can be fetched later assuming the quorum includes at
82/// least one query node serving catchup.
83/// The margin gives in flight reconstruction tasks time to finish, which is
84/// cheaper than fetching the payload through catchup.
85///
86/// Proposals are retained with the same margin: when a reconstruction
87/// finishes, `BlockPayloadReconstructed` is only emitted if the proposal
88/// (the block header) for that view is still available.
89pub(crate) const VID_RECONSTRUCT_GC_MARGIN: u64 = 5;
90
91/// Views to retain in storage GC behind the decided view, so in-flight
92/// storage writes for recent views aren't aborted before they persist.
93const STORAGE_GC_MARGIN: u64 = 5;
94
95/// Messages naming an epoch further ahead than this are dropped at intake. We
96/// could not verify them anyway: an epoch's stake table only materializes by
97/// walking the DRB chain, so parking such a message and driving catchup for an
98/// arbitrary claimed epoch just burns resources.
99/// Within the ceiling, deferred changes verify progressively as catchup
100/// advances ([`CertVerifiers::retry_pending`] runs on every DRB arrival).
101pub(crate) const EPOCH_CHANGE_LOOKAHEAD: u64 = 3;
102
103/// How many epochs before the node's current one are still accepted.
104const EPOCH_CHANGE_LOOKBEHIND: u64 = 1;
105
106pub(crate) const MAX_VIEWS_AHEAD: ViewNumber = ViewNumber::new(30);
107
108#[derive(Builder)]
109pub struct Coordinator<T: NodeType, S> {
110    membership_coordinator: EpochMembershipCoordinator<T>,
111    consensus: Consensus<T>,
112    network: Cliquenet<T>,
113    state_manager: StateManager<T>,
114    #[builder(default)]
115    client: CoordinatorClient<T>,
116    vid_disperser: VidDisperser<T>,
117    vid_reconstructor: VidReconstructor<T>,
118    #[builder(default)]
119    vid_fragment_accumulator: VidFragmentAccumulator<T>,
120    vote1_collector: VoteCollector<T, SimpleTally<T, QuorumVote2<T>, SuccessThreshold>>,
121    vote2_collector: VoteCollector<T, SimpleTally<T, Vote2<T>, SuccessThreshold>>,
122    timeout_collector: VoteCollector<T, SimpleTally<T, TimeoutVote2<T>, SuccessThreshold>>,
123    timeout_one_honest_collector:
124        VoteCollector<T, SimpleTally<T, TimeoutVote2<T>, OneHonestThreshold>>,
125    timeout3_collector: VoteCollector<T, TimeoutTally<T, SuccessThreshold>>,
126    timeout_one_honest3_collector: VoteCollector<T, TimeoutTally<T, OneHonestThreshold>>,
127    epoch_root_collector: VoteCollector<T, EpochRootTally<T>>,
128    upgrade_vote_collector: VoteCollector<T, UpgradeTally<T>>,
129    upgrade_protocol: UpgradeProtocol<T>,
130    cert_verifiers: CertVerifiers<T>,
131    epoch_manager: EpochManager<T>,
132    block_builder: BlockBuilder<T>,
133    proposal_validator: ProposalValidator<T>,
134    share_validator: VidShareValidator<T>,
135    storage: Storage<T, S>,
136    #[builder(default)]
137    outbox: Outbox<ConsensusOutput<T>>,
138    /// Forwards external messages on arrival, independent of consensus inputs.
139    external_tx: mpsc::UnboundedSender<OpaqueMessage<T::SignatureKey>>,
140    public_key: T::SignatureKey,
141    #[builder(default = KeyPrefix::from(&public_key))]
142    node_id: KeyPrefix,
143    timer: Timer,
144    #[builder(skip)]
145    pending_proposal_fetches: PendingProposalFetches<T>,
146    #[builder(skip)]
147    /// Proposals asked for, with the view this node was in when it last asked.
148    requested_missing_proposals: HashMap<ProposalFetchKey<T>, ViewNumber>,
149    #[builder(default = Fetcher::new(public_key.clone()))]
150    fetcher: Fetcher<T>,
151    #[builder(default = Server::new(public_key.clone()))]
152    server: Server<T>,
153    #[builder(skip)]
154    da_payloads: BTreeMap<(ViewNumber, VidCommitment2), PendingDa<T>>,
155    /// Dispersals the block builder computed, until consensus asks to disperse their block.
156    #[builder(skip)]
157    built_dispersals: BTreeMap<(ViewNumber, EpochNumber, VidCommitment2), BuiltDispersal<T>>,
158    metrics: Option<metrics::Metrics>,
159    #[builder(default)]
160    participation: ParticipationTracker<T>,
161    #[builder(skip)]
162    voted_view: Option<ViewNumber>,
163    /// View of the last timer fire that counted towards participation and
164    /// timeout metrics, so a re-fire for the same (stuck) view doesn't
165    /// double-count it.
166    #[builder(skip)]
167    last_timeout_view: Option<ViewNumber>,
168    #[builder(skip)]
169    view_started: Option<(ViewNumber, EpochNumber, Instant)>,
170    #[builder(skip)]
171    proposal_received_at: Option<(ViewNumber, Instant)>,
172    #[builder(skip)]
173    invalid_certs_at_decide: u64,
174    #[builder(skip)]
175    payload_txn_bytes: BTreeMap<ViewNumber, usize>,
176}
177
178#[bon]
179impl<T, S> Coordinator<T, S>
180where
181    T: NodeType,
182    S: NewProtocolStorage<T>,
183{
184    #[builder(builder_type = CoordinatorMaker, finish_fn = make)]
185    #[allow(clippy::too_many_arguments)]
186    pub fn maker(
187        membership_coordinator: EpochMembershipCoordinator<T>,
188        network: Cliquenet<T>,
189        initializer: &HotShotInitializer<T>,
190        upgrade_lock: UpgradeLock<T>,
191        public_key: T::SignatureKey,
192        private_key: <T::SignatureKey as SignatureKey>::PrivateKey,
193        state_private_key: <T::StateSignatureKey as StateSignatureKey>::StatePrivateKey,
194        stake_table_capacity: usize,
195        timeout_duration: Duration,
196        empty_block_delay: Duration,
197        block_sizes: BTreeMap<Version, u64>,
198        storage: S,
199        metrics: &dyn Metrics,
200        consensus_metrics: ConsensusMetricsValue,
201        /// Locked QC persisted on a prior run; restored so the lock survives restart.
202        locked_qc: Option<Certificate1<T>>,
203        /// Boundary certificate persisted on a prior run (see
204        /// [`ConsensusOutput::PersistBoundaryQc`]).
205        boundary_qc: Option<Certificate1<T>>,
206        /// `Certificate2` of the anchor persisted on a prior run.
207        anchor_cert2: Option<Certificate2<T>>,
208        upgrade_config: UpgradeConfig,
209        leader_tracer: Option<LeaderTracerHandle>,
210        external_tx: mpsc::UnboundedSender<OpaqueMessage<T::SignatureKey>>,
211    ) -> Self {
212        let mut consensus = Consensus::new(
213            membership_coordinator.clone(),
214            public_key.clone(),
215            private_key.clone(),
216            state_private_key,
217            stake_table_capacity,
218            upgrade_lock.clone(),
219            initializer.anchor_leaf().clone(),
220            initializer.epoch_height(),
221        );
222        consensus.set_tracer(leader_tracer.clone());
223
224        let anchor_leaf = initializer.anchor_leaf();
225        let anchor_view = anchor_leaf.view_number();
226        let anchor_epoch = anchor_leaf
227            .epoch(initializer.epoch_height())
228            .unwrap_or(EpochNumber::genesis());
229        let cert1 = initializer.high_qc().clone();
230        let parent_proposal = message::Proposal {
231            block_header: anchor_leaf.block_header().clone(),
232            view_number: anchor_view,
233            epoch: anchor_epoch,
234            justify_qc: anchor_leaf.justify_qc().clone(),
235            next_epoch_justify_qc: None,
236            upgrade_certificate: anchor_leaf
237                .upgrade_certificate()
238                .cloned()
239                .map(|cert| UpgradeCertificate2::restore_epoch(cert, anchor_epoch)),
240            view_change_evidence: anchor_leaf
241                .view_change_evidence
242                .clone()
243                .and_then(ViewChangeEvidence2::timeout_evidence),
244            next_drb_result: anchor_leaf.next_drb_result,
245            state_cert: None,
246        };
247
248        let coordinator_metrics = metrics
249            .is_recording()
250            .then(|| metrics::Metrics::new(consensus_metrics));
251
252        let mut state_manager = StateManager::new(
253            Arc::new(initializer.instance_state().clone()),
254            upgrade_lock.clone(),
255        )
256        .with_metrics(
257            coordinator_metrics.as_ref().map(|m| {
258                m.consensus
259                    .validate_and_apply_header_duration
260                    .clone()
261                    .into()
262            }),
263            coordinator_metrics
264                .as_ref()
265                .map(|m| m.consensus.update_leaf_duration.clone().into()),
266        );
267        // Seed `from_header` stubs for restored undecided proposals so a child
268        // proposal can be validated; anchor seeded last so its state wins.
269        for p in initializer.saved_proposals().values() {
270            state_manager.seed_from_header(message::Proposal::from(p.data.clone()));
271        }
272        state_manager.seed_state(
273            anchor_view,
274            initializer.anchor_state().clone(),
275            anchor_leaf.clone(),
276        );
277        // The anchor leaf and persisted proposals are blocks this node had
278        // reconstructed before it went down, so treat them as reconstructed on
279        // restart
280        let reconstructed_blocks =
281            std::iter::once((anchor_view, anchor_leaf.block_header().clone()))
282                .chain(
283                    initializer
284                        .saved_proposals()
285                        .iter()
286                        .map(|(view, p)| (*view, p.data.block_header().clone())),
287                )
288                .filter_map(|(view, header)| match header.payload_commitment() {
289                    VidCommitment::V2(commitment) => Some((view, commitment)),
290                    _ => None,
291                });
292        // Seed every persisted proposal before `seed_parent` so its authoritative anchor wins.
293        // They are kept signed, so this node can serve them to a peer fetching
294        // them, as one fetching an epoch's boundary block for a re-vote.
295        let saved_proposals = initializer
296            .saved_proposals()
297            .values()
298            .map(|p| SignedProposal {
299                data: message::Proposal::from(p.data.clone()),
300                signature: p.signature.clone(),
301                _pd: std::marker::PhantomData,
302            });
303        consensus.seed_signed_proposals(saved_proposals);
304        // `seed_parent` sets the current epoch from the anchor proposal;
305        // `resume_from_restart` positions the view so the node never
306        // re-enters a view it may have voted or proposed in before it went
307        // down.
308        consensus.seed_parent(cert1, parent_proposal, reconstructed_blocks);
309        // Restore the persisted lock; it can be newer than the anchor QC, so
310        // this must run after `seed_parent`.
311        if let Some(locked_qc) = locked_qc {
312            consensus.seed_locked_cert(locked_qc);
313        }
314        if let Some(boundary_qc) = boundary_qc {
315            consensus.seed_boundary_cert(boundary_qc);
316        }
317        if let Some(anchor_cert2) = anchor_cert2 {
318            consensus.seed_anchor_cert2(anchor_cert2);
319        }
320        // Covers a crash between deciding an upgrade and persisting its
321        // certificate. A certificate for another target would make
322        // `UpgradeLock::version` fail on every view. Restored before
323        // `resume_from_restart`, which reads the version of the views it bars.
324        if upgrade_lock.decided_upgrade_cert().is_none()
325            && let Some(cert) = anchor_leaf.upgrade_certificate()
326            && anchor_view <= cert.data.decide_by
327            && cert.data.new_version == upgrade_lock.upgrade().target
328        {
329            info!(
330                view = %anchor_view,
331                new_version = %cert.data.new_version,
332                "restoring decided upgrade certificate from the anchor leaf"
333            );
334            upgrade_lock.set_decided_upgrade_cert(cert.clone());
335        }
336
337        consensus.resume_from_restart(
338            anchor_view,
339            initializer.start_view(),
340            initializer.last_actioned_view(),
341        );
342        if let Some(state_cert) = initializer.state_cert().cloned() {
343            consensus.seed_state_cert(state_cert);
344        }
345
346        let participation = ParticipationTracker::new(&membership_coordinator, anchor_epoch);
347
348        let mut vid_disperser = VidDisperser::new(
349            membership_coordinator.clone(),
350            network.sender().clone(),
351            public_key.clone(),
352            private_key.clone(),
353        )
354        .with_metrics(
355            coordinator_metrics
356                .as_ref()
357                .map(|m| m.consensus.vid_disperse_duration.clone().into()),
358        );
359        vid_disperser.set_tracer(leader_tracer.clone());
360        let mut vid_reconstructor = VidReconstructor::new();
361        vid_reconstructor.set_tracer(leader_tracer.clone());
362
363        let lock = upgrade_lock.clone();
364        let genesis_qc = consensus.cert1_at(ViewNumber::genesis()).cloned();
365
366        Self::builder()
367            .consensus(consensus)
368            .network(network)
369            .state_manager(state_manager)
370            .vid_disperser(vid_disperser)
371            .vid_reconstructor(vid_reconstructor)
372            .vote1_collector(VoteCollector::new(
373                membership_coordinator.clone(),
374                lock.clone(),
375            ))
376            .vote2_collector(VoteCollector::new(
377                membership_coordinator.clone(),
378                lock.clone(),
379            ))
380            .timeout_collector(VoteCollector::new(
381                membership_coordinator.clone(),
382                lock.clone(),
383            ))
384            .timeout_one_honest_collector(VoteCollector::new(
385                membership_coordinator.clone(),
386                lock.clone(),
387            ))
388            .timeout3_collector(VoteCollector::new(
389                membership_coordinator.clone(),
390                lock.clone(),
391            ))
392            .timeout_one_honest3_collector(VoteCollector::new(
393                membership_coordinator.clone(),
394                lock.clone(),
395            ))
396            .epoch_root_collector(VoteCollector::new(
397                membership_coordinator.clone(),
398                lock.clone(),
399            ))
400            .upgrade_vote_collector(VoteCollector::new(
401                membership_coordinator.clone(),
402                lock.clone(),
403            ))
404            .upgrade_protocol(UpgradeProtocol::new(
405                upgrade_config,
406                lock.clone(),
407                public_key.clone(),
408                private_key.clone(),
409            ))
410            .cert_verifiers(CertVerifiers::new(
411                membership_coordinator.clone(),
412                lock.clone(),
413            ))
414            .epoch_manager(EpochManager::new(
415                initializer.epoch_height(),
416                membership_coordinator.clone(),
417            ))
418            .block_builder(
419                BlockBuilder::new(
420                    Arc::new(initializer.instance_state().clone()),
421                    membership_coordinator.clone(),
422                    BlockBuilderConfig {
423                        empty_block_delay,
424                        block_sizes,
425                        ..BlockBuilderConfig::default()
426                    },
427                    upgrade_lock.clone(),
428                )
429                .with_metrics(
430                    coordinator_metrics
431                        .as_ref()
432                        .map(|m| m.consensus.block_commitments_duration.clone().into()),
433                )
434                .with_tracer(leader_tracer),
435            )
436            .proposal_validator(ProposalValidator::new(
437                membership_coordinator.clone(),
438                initializer.epoch_height(),
439                upgrade_lock.clone(),
440                genesis_qc.as_ref(),
441            ))
442            .share_validator(VidShareValidator::new(
443                membership_coordinator.clone(),
444                initializer.epoch_height(),
445                upgrade_lock,
446            ))
447            .storage(Storage::new(storage, private_key).with_metrics(metrics))
448            .membership_coordinator(membership_coordinator)
449            .timer(Timer::new(timeout_duration, anchor_view))
450            .public_key(public_key)
451            .maybe_metrics(coordinator_metrics)
452            .participation(participation)
453            .external_tx(external_tx)
454            .build()
455    }
456
457    /// Emit `ViewChanged(current_view + 1)` and, if leader, a
458    /// `RequestBlockAndHeader`.
459    pub fn start(&mut self) {
460        let cur_view = self.consensus.current_view();
461        let next_view = cur_view + 1;
462        // Client requests are served before the outbox reaches the builder, so it must know the
463        // view before that first `ViewChanged`.
464        self.block_builder.start_at(next_view);
465        let epoch = self.consensus.current_epoch();
466
467        if self.consensus.last_decided_leaf().view_number() == ViewNumber::genesis() {
468            // Genesis DA never flows through the normal block-builder path.
469            let genesis_leaf = self.consensus.last_decided_leaf().clone();
470            let (payload, metadata) = T::BlockPayload::empty();
471            self.storage.append_da(
472                ViewNumber::genesis(),
473                EpochNumber::genesis(),
474                payload,
475                metadata,
476                genesis_leaf.payload_commitment(),
477            );
478
479            // Emit `LeafDecided` for genesis so persistence sees the header.
480            self.outbox.push_back(ConsensusOutput::LeafDecided {
481                leaves: vec![genesis_leaf],
482                cert1: self
483                    .consensus
484                    .cert1_at(ViewNumber::genesis())
485                    .cloned()
486                    .expect("genesis cert1 must be seeded"),
487                cert2: None,
488                vid_shares: vec![None],
489            });
490        }
491
492        self.outbox
493            .push_back(ConsensusOutput::ViewChanged(next_view, epoch));
494
495        if let Some(leader) = self.leader(next_view, epoch)
496            && leader == self.public_key
497        {
498            // No parent proposal when restarting past the anchor view: the
499            // node cannot propose off the anchor for a later view; the
500            // timeout path takes over instead.
501            if let Some(parent_proposal) = self.consensus.proposal_at(cur_view).cloned() {
502                self.consensus.push_block_request(
503                    BlockAndHeaderRequest {
504                        view: next_view,
505                        epoch,
506                        parent_proposal,
507                    },
508                    &mut self.outbox,
509                );
510            }
511        }
512    }
513
514    pub async fn stop(mut self) {
515        futures::join!(self.network.shutdown(), self.storage.flush());
516    }
517
518    pub async fn next_consensus_input(&mut self) -> Result<ConsensusInput<T>, CoordinatorError> {
519        loop {
520            select! {
521                message = self.network.receive() => match message {
522                    Ok(m) => {
523                        if let Some(input) = self.on_network_message(m) {
524                            return Ok(input)
525                        }
526                    }
527                    Err(e) => {
528                        return Err(CoordinatorError::from(e).context("network receive"))
529                    }
530                },
531                () = &mut self.timer => {
532                    let view = self.timer.view();
533                    let epoch = self.consensus.current_epoch();
534                    // Re-arm for the same view: a node stuck exactly at TC2
535                    // threshold can lose its only timeout-vote broadcast, so
536                    // the vote is re-sent every timeout period until the
537                    // view advances.
538                    self.timer.reset();
539                    let stats = self.vote1_collector.stats(view);
540                    if stats.is_empty() {
541                        warn!(%view, %epoch, "timeout: no vote1 received for this view");
542                    }
543                    for (voted_epoch, stats) in stats {
544                        match stats.stake {
545                            Some((stake, threshold)) => warn!(
546                                %view, %epoch, %voted_epoch,
547                                signers = %stats.signers, %stake, %threshold,
548                                "timeout: vote1 stake observed (deduped by signer)"
549                            ),
550                            None => warn!(
551                                %view, %epoch, %voted_epoch,
552                                signers = %stats.signers,
553                                "timeout: vote1 received, but the epoch's stake table is gone"
554                            ),
555                        }
556                    }
557                    let input = ConsensusInput::Timeout(view);
558                    if self.last_timeout_view != Some(view) {
559                        self.last_timeout_view = Some(view);
560                        let leader = self.leader(view, epoch);
561                        if let Some(leader) = leader.clone() {
562                            self.participation.leader_missed(leader, epoch);
563                        }
564                        if let Some(m) = &self.metrics {
565                            m.consensus.number_of_timeouts.add(1);
566                            if leader.as_ref() == Some(&self.public_key) {
567                                m.consensus.number_of_timeouts_as_leader.add(1);
568                            }
569                        }
570                    }
571                    return Ok(input)
572                }
573                Some(output) = self.state_manager.next() => {
574                    if let Some(input) = self.on_state_manager_output(output) {
575                        return Ok(input)
576                    }
577                }
578                Some(request) = self.client.next_request() => {
579                    if let Err(err) = self.on_client_request(request) {
580                        error!(%err, "error while handling client request");
581                    }
582                }
583                Some(tcert) = self.timeout_collector.next() => {
584                    self.cert_verifiers
585                        .timeout
586                        .mark_completed(tcert.view_number(), tcert.epoch());
587                    return Ok(ConsensusInput::TimeoutCertificate(tcert.map(TimeoutEvidence::V2)))
588                }
589                Some(tcert) = self.timeout3_collector.next() => {
590                    self.cert_verifiers
591                        .timeout3
592                        .mark_completed(tcert.view_number(), tcert.epoch());
593                    return Ok(ConsensusInput::TimeoutCertificate(tcert.map(TimeoutEvidence::V3)))
594                }
595                // The epoch these certificates name is the one the remote
596                // stake voted under, and is deliberately dropped: what the
597                // threshold attests to is that the view timed out, and the
598                // node answers under its own committee.
599                Some(out) = self.timeout_one_honest_collector.next() => {
600                    return Ok(ConsensusInput::TimeoutOneHonest(out.view_number()))
601                }
602                Some(out) = self.timeout_one_honest3_collector.next() => {
603                    return Ok(ConsensusInput::TimeoutOneHonest(out.view_number()))
604                }
605                Some(cert1) = self.vote1_collector.next() => {
606                    self.cert_verifiers
607                        .cert1
608                        .mark_completed(ViewEpoch(cert1.view_number(), cert1.epoch()));
609                    return Ok(ConsensusInput::Certificate1(cert1))
610                }
611                Some(cert2) = self.vote2_collector.next() => {
612                    self.cert_verifiers
613                        .cert2
614                        .mark_completed(ViewEpoch(cert2.view_number(), cert2.epoch()));
615                    return Ok(ConsensusInput::Certificate2(cert2))
616                }
617                Some(cert1) = self.cert_verifiers.cert1.next() => {
618                    // The epoch-root collector keeps tallying: its state
619                    // certificate only forms locally, and consensus dedupes
620                    // the cert1 it carries.
621                    self.vote1_collector.mark_completed(cert1.view_number(), cert1.epoch());
622                    return Ok(ConsensusInput::Certificate1(cert1))
623                }
624                Some(cert2) = self.cert_verifiers.cert2.next() => {
625                    self.vote2_collector.mark_completed(cert2.view_number(), cert2.epoch());
626                    return Ok(ConsensusInput::Certificate2(cert2))
627                }
628                Some(tc) = self.cert_verifiers.timeout.next() => {
629                    self.timeout_collector.mark_completed(tc.view_number(), tc.epoch());
630                    return Ok(ConsensusInput::TimeoutCertificate(tc.map(TimeoutEvidence::V2)))
631                }
632                Some(tc) = self.cert_verifiers.timeout3.next() => {
633                    self.timeout3_collector.mark_completed(tc.view_number(), tc.epoch());
634                    return Ok(ConsensusInput::TimeoutCertificate(tc.map(TimeoutEvidence::V3)))
635                }
636                Some(cert1) = self.cert_verifiers.advance.next() => {
637                    return Ok(ConsensusInput::AdvanceView(cert1))
638                }
639                Some(revote) = self.cert_verifiers.revote.next() => {
640                    return Ok(ConsensusInput::ReVote(revote.into_cert()))
641                }
642                Some(epoch_change) = self.cert_verifiers.epoch_change.next() => {
643                    let epoch_change = epoch_change.into_cert();
644                    // The boundary leaf is final (Cert1 + Cert2), so seed a
645                    // commitment-only state for it: a validator whose
646                    // membership starts at this boundary can then build and
647                    // validate the new epoch's first proposals via catchup.
648                    self.state_manager
649                        .seed_from_header(epoch_change.proposal.clone());
650                    return Ok(ConsensusInput::EpochChange(epoch_change))
651                }
652                Some(cert) = self.upgrade_vote_collector.next() => {
653                    return Ok(ConsensusInput::UpgradeCertificateFormed(cert))
654                }
655                Some((cert1, state_cert)) = self.epoch_root_collector.next() => {
656                    self.cert_verifiers
657                        .cert1
658                        .mark_completed(ViewEpoch(cert1.view_number(), cert1.epoch()));
659                    self.storage
660                        .append_state_cert(state_cert.view_number(), state_cert.clone());
661                    return Ok(ConsensusInput::EpochRootCertificates { cert1, state_cert })
662                }
663                Some(item) = self.share_validator.next() => match item {
664                    Ok(vid_share) => {
665                        return Ok(ConsensusInput::VidShare(vid_share))
666                    },
667                    Err(e) => {
668                        return Err(CoordinatorError::regular(e).context("vid share validation"))
669                    }
670                },
671                Some(item) = self.proposal_validator.next() => match item {
672                    Ok(validated) if validated.fetched => {
673                        return Ok(ConsensusInput::FetchedProposal(validated.message))
674                    }
675                    Ok(validated) => {
676                        // Refresh the network's peer set when a proposal is validated.
677                        let epoch = validated.message.proposal.data.epoch;
678                        self.bump_network_epoch(epoch);
679                        return Ok(ConsensusInput::Proposal(validated.sender, validated.message))
680                    }
681                    Err(e) => {
682                        return Err(CoordinatorError::regular(e).context("proposal validation"))
683                    }
684                },
685                Some(item) = self.block_builder.next() => match item {
686                    Ok(mut block) => {
687                        self.state_manager.request_header(HeaderRequest::from(&block));
688                        let epoch = block.epoch;
689                        let manifest = block.manifest.clone();
690                        // Retain the payload and persist it when consensus proposes this
691                        // exact block (cf. SendProposal):
692                        if let VidCommitment::V2(commit) = block.payload_commitment {
693                            self.da_payloads.insert(
694                                (block.view, commit),
695                                PendingDa {
696                                    epoch: block.epoch,
697                                    payload: block.payload.payload.clone(),
698                                    metadata: block.payload.metadata.clone(),
699                                    hashes: manifest.hashes.clone(),
700                                },
701                            );
702                        } else {
703                            warn!(view = %block.view, "block payload commitment is not V2");
704                        }
705                        // We built this block; skip reconstructing it from our own loopback share.
706                        self.vid_reconstructor.retire_view(block.view);
707                        // A leader of several views builds a copy sent for a later one into this
708                        // block, so a transaction here can be sent from `block.view - 1`, and its
709                        // other copies can sit with the leaders of the next `fanout` views.
710                        let holders = self.upcoming_leaders(
711                            block.view + 1,
712                            self.block_builder.fanout().get(),
713                            epoch,
714                        );
715                        for (_, leader) in holders {
716                            self.unicast_in(
717                                self.slot_for(block.view),
718                                &leader,
719                                MessageType::Block(BlockMessage::DedupManifest(manifest.clone())),
720                                "leader unicast",
721                            )?;
722                        }
723                        // Held only now, as an error above drops the block: consensus asks to
724                        // disperse every block it is given, and that request takes it back.
725                        if let VidCommitment::V2(commit) = block.payload_commitment
726                            && let Some(dispersal) = block.dispersal.take()
727                        {
728                            self.built_dispersals
729                                .insert((block.view, epoch, commit), dispersal);
730                        }
731                        return Ok(block.into())
732                    }
733                    Err(err) => {
734                        return Err(CoordinatorError::regular(err).context("block building"))
735                    }
736                },
737                Some(item) = self.vid_disperser.next() => match item {
738                    Ok(out) => {
739                        return Ok(ConsensusInput::VidDisperseCreated(out.view, out.payload_commitment))
740                    }
741                    Err(err) => {
742                        return Err(CoordinatorError::from(err).context("vid disperse"))
743                    }
744                },
745                Some(item) = self.vid_reconstructor.next() => match item {
746                    Ok(out) => {
747                        return Ok(self.on_payload_obtained(out))
748                    }
749                    Err(err) => {
750                        return Err(CoordinatorError::regular(err).context("vid reconstruction"))
751                    }
752                },
753                Some(out) = self.fetcher.next() => {
754                    if self.consensus.is_reconstructed(out.view, out.payload_commitment) {
755                        continue
756                    }
757                    self.vid_reconstructor.retire_view(out.view);
758                    return Ok(self.on_payload_obtained(out))
759                },
760                Some(stored) = self.storage.next() => {
761                    return Ok(ConsensusInput::Stored(stored))
762                },
763                Some(result) = self.epoch_manager.next() => match result {
764                    Ok(EpochRootResult::DrbResult(epoch, drb_result)) => {
765                        self.vote1_collector.retry_pending_votes();
766                        self.vote2_collector.retry_pending_votes();
767                        self.timeout_collector.retry_pending_votes();
768                        self.timeout_one_honest_collector.retry_pending_votes();
769                        self.timeout3_collector.retry_pending_votes();
770                        self.timeout_one_honest3_collector.retry_pending_votes();
771                        self.epoch_root_collector.retry_pending_votes();
772                        self.upgrade_vote_collector.retry_pending_votes();
773                        self.cert_verifiers.retry_pending(|e| self.epoch_manager.request_drb_result(e));
774                        return Ok(ConsensusInput::DrbResult(epoch, drb_result))
775                    }
776                    Err(failure) => {
777                        // Catchup/compute failed. The epoch manager clears
778                        // the pending guard; consensus's `maybe_propose`
779                        // will re-request the DRB when it next tries to
780                        // build a transition proposal and finds it missing.
781                        warn!(err = %failure.error, epoch = %failure.epoch, "DRB request failed");
782                        continue;
783                    }
784                },
785                else => {
786                    return Err(CoordinatorError::critical(ErrorSource::NoInput))
787                }
788            }
789        }
790    }
791
792    fn on_payload_obtained(&mut self, out: ObtainedPayload<T>) -> ConsensusInput<T> {
793        self.server
794            .retain(out.view, out.payload_commitment, &out.payload);
795        self.payload_txn_bytes
796            .insert(out.view, out.payload.txn_bytes());
797        self.block_builder.on_block_reconstructed(
798            out.view,
799            out.payload_commitment,
800            out.tx_commitments,
801        );
802        self.storage.append_da(
803            out.view,
804            out.epoch,
805            out.payload.clone(),
806            out.metadata.clone(),
807            VidCommitment::V2(out.payload_commitment),
808        );
809        // Only pair the payload with a header that commits to it.
810        if let Some(proposal) = self
811            .consensus
812            .proposal_with_payload(out.view, out.payload_commitment)
813        {
814            self.outbox
815                .push_back(ConsensusOutput::BlockPayloadReconstructed {
816                    view: out.view,
817                    header: proposal.block_header.clone(),
818                    payload: Arc::new(out.payload.clone()),
819                });
820        } else if self.consensus.proposal_at(out.view).is_some() {
821            warn!(
822                view = %out.view,
823                obtained = %out.payload_commitment,
824                "payload commitment does not match a proposal header"
825            );
826        }
827        ConsensusInput::BlockReconstructed {
828            view: out.view,
829            payload_commitment: out.payload_commitment,
830            payload: out.payload,
831        }
832    }
833
834    pub fn apply_consensus(&mut self, input: ConsensusInput<T>) {
835        self.consensus.apply(input, &mut self.outbox)
836    }
837
838    pub fn process_consensus_output(
839        &mut self,
840        output: ConsensusOutput<T>,
841    ) -> Result<(), CoordinatorError> {
842        let node = self.node_id;
843        match output {
844            ConsensusOutput::RequestState(state_request) => {
845                debug!(
846                    %node,
847                    view = %state_request.view,
848                    epoch = %state_request.epoch,
849                    block = %state_request.block,
850                    "request state validation"
851                );
852                self.state_manager.request_state(state_request);
853            },
854            ConsensusOutput::RequestVidDisperse {
855                view,
856                epoch,
857                payload,
858                metadata,
859                payload_commitment,
860            } => {
861                debug!(%node, %view, %epoch, "request vid disperse");
862                let built = self
863                    .built_dispersals
864                    .remove(&(view, epoch, payload_commitment));
865                self.vid_disperser.request_vid_disperse(
866                    VidDisperseRequest {
867                        view,
868                        epoch,
869                        block: payload,
870                        metadata,
871                        payload_commitment,
872                    },
873                    built,
874                );
875            },
876            ConsensusOutput::RequestDrbResult(epoch) => {
877                debug!(%node, %epoch, "request drb result");
878                self.epoch_manager.request_drb_result(epoch);
879            },
880            ConsensusOutput::LeafDecided { leaves, cert1, .. } => {
881                info!(
882                    %node,
883                    view = %cert1.view_number(),
884                    epoch = ?cert1.epoch().map(|e| *e),
885                    leaves = leaves.len(),
886                    "leaves decided"
887                );
888                self.on_decide_metrics(&leaves);
889                self.block_builder
890                    .on_blocks_decided(leaves.iter().filter_map(|leaf| {
891                        // New-protocol headers always carry a V2 commitment.
892                        match leaf.block_header().payload_commitment() {
893                            VidCommitment::V2(commit) => Some((leaf.view_number(), commit)),
894                            VidCommitment::V0(_) | VidCommitment::V1(_) => None,
895                        }
896                    }));
897                // `leaves` is ordered newest first.
898                //  Garbage collect the data for views < decided view
899                if let Some(newest) = leaves.first() {
900                    let gc_view = newest.view_number();
901                    let gc_epoch = newest.justify_qc().epoch().unwrap_or_default();
902                    self.gc(gc_epoch, GcScope::Decided(gc_view))?;
903                    // A gap-fill decide trails the decided view; gc already covered its leaves.
904                    if gc_view == self.consensus.last_decided_view() {
905                        self.state_manager.seed_decided(newest.clone());
906                    }
907                }
908                for leaf in leaves.into_iter().rev() {
909                    self.participation
910                        .on_leaf_decided(&leaf, &self.membership_coordinator);
911                    self.epoch_manager.handle_leaf_decided(leaf);
912                }
913            },
914            ConsensusOutput::LockUpdated(view) => {
915                debug!(%node, %view, "lock updated");
916                self.server.lock_moved(view);
917            },
918            ConsensusOutput::RequestMissingProposal { view, leaf_commit } => {
919                debug!(%node, %view, "request missing proposal");
920                if let Err(err) = self.request_missing_proposal(view, leaf_commit) {
921                    warn!(%node, %view, %err, "failed to request missing proposal");
922                }
923            },
924            ConsensusOutput::RequestMissingPayload {
925                view,
926                payload_commitment,
927            } => {
928                debug!(%node, %view, "request missing payload");
929                if let Some((peer, message)) = self.fetcher.request(
930                    view,
931                    payload_commitment,
932                    Retry::NewRound,
933                    &self.consensus,
934                    &self.membership_coordinator,
935                ) {
936                    self.unicast_in(
937                        self.slot_for(view),
938                        &peer,
939                        message.message_type,
940                        "payload request",
941                    )?;
942                }
943            },
944            ConsensusOutput::RequestBlockAndHeader(request) => {
945                debug!(
946                    %node,
947                    view = %request.view,
948                    epoch = %request.epoch,
949                    "request block and header"
950                );
951                self.block_builder.request_block(request);
952            },
953            ConsensusOutput::RecordAction(view, kind) => {
954                debug!(%node, %view, ?kind, "record action");
955                self.storage.record_action(view, kind);
956            },
957            ConsensusOutput::PersistProposal(proposal) => {
958                let view = proposal.data.view_number;
959                debug!(%node, %view, "persist proposal");
960                self.storage.append_proposal(proposal.data.clone());
961                // Two blocks can be built for one view. Here we know which one
962                // wins and we persist just that one:
963                if let VidCommitment::V2(commit) = proposal.data.block_header.payload_commitment() {
964                    if let Some(da) = self.da_payloads.remove(&(view, commit)) {
965                        self.payload_txn_bytes.insert(view, da.payload.txn_bytes());
966                        if let Some(m) = &self.metrics
967                            && da.payload.transactions(&da.metadata).next().is_none()
968                        {
969                            m.consensus.number_of_empty_blocks_proposed.add(1);
970                        }
971                        // A leader never reconstructs its own block, and a block it built
972                        // but did not propose puts nothing on the chain, so this is where
973                        // it records the block's transactions.
974                        self.block_builder
975                            .on_block_reconstructed(view, commit, da.hashes);
976                        self.storage.append_da(
977                            view,
978                            da.epoch,
979                            da.payload,
980                            da.metadata,
981                            VidCommitment::V2(commit),
982                        );
983                    } else {
984                        warn!(%node, %view, "no payload for proposed block");
985                    }
986                }
987            },
988            ConsensusOutput::ProposalPaired {
989                proposal,
990                vid_share,
991            } => {
992                let view = proposal.data.view_number;
993                debug!(%node, %view, "proposal paired with vid share");
994                self.storage.append_vid(vid_share.clone());
995                self.storage.append_proposal(proposal.data.clone());
996                // Same gate as the in-memory store in `Consensus::apply`: persisting
997                // unconditionally writes a `state_cert` row under a view the sender
998                // chose, which decide promotes into `finalized_state_cert` under the
999                // cert's own epoch. That table is what `load_state_cert` seeds consensus
1000                // from on restart (highest epoch wins) and what `load_state_cert_by_epoch`
1001                // serves.
1002                if let Some(state_cert) =
1003                    validated_state_cert(&proposal.data, *self.consensus.epoch_height)
1004                {
1005                    self.storage
1006                        .append_state_cert(state_cert.view_number(), state_cert.clone());
1007                }
1008                self.notify_vid_reconstructor(&proposal.data, PinnedBy::Leader);
1009                self.vid_reconstructor
1010                    .handle_vid_share(self.public_key.clone(), vid_share);
1011            },
1012            ConsensusOutput::SendProposal(proposal) => {
1013                let view = proposal.data.view_number;
1014                let epoch = proposal.data.epoch;
1015                let block = proposal.data.block_header.block_number();
1016                info!(%node, %view, %epoch, %block, "send proposal");
1017                if let Some(m) = &self.metrics
1018                    && proposal.data.view_change_evidence.is_none()
1019                    && let Some((prev_view, received_at)) = self.proposal_received_at
1020                    && (*prev_view).checked_add(1) == Some(*view)
1021                {
1022                    m.consensus
1023                        .previous_proposal_to_proposal_time
1024                        .add_point(received_at.elapsed().as_millis() as f64);
1025                }
1026                crate::trace_leader_event!(
1027                    self.consensus.tracer,
1028                    view,
1029                    crate::leader_trace::LeaderEvent::ProposalBroadcastStart
1030                );
1031                if let Err(err) = self.broadcast_in(
1032                    self.slot_for(view),
1033                    ConsensusMessage::Proposal(ProposalMessage::validated(proposal.clone())),
1034                    "proposal broadcast",
1035                ) {
1036                    if err.severity == Severity::Critical {
1037                        return Err(err);
1038                    } else {
1039                        warn!(%node, %err, "network error while broadcasting proposal")
1040                    }
1041                }
1042                crate::trace_leader_event!(
1043                    self.consensus.tracer,
1044                    view,
1045                    crate::leader_trace::LeaderEvent::ProposalBroadcastEnd
1046                );
1047            },
1048            ConsensusOutput::SendTimeoutVote(vote, evidence) => {
1049                let view = vote.view_number();
1050                debug!(
1051                    %node, %view,
1052                    has_evidence = evidence.is_some(),
1053                    "send timeout vote"
1054                );
1055                let message = match vote {
1056                    TimeoutVote::V2(vote) => {
1057                        ConsensusMessage::TimeoutVote(message::TimeoutVoteMessage {
1058                            vote,
1059                            evidence,
1060                        })
1061                    },
1062                    TimeoutVote::V3(ballot) => ConsensusMessage::TimeoutVote3(
1063                        message::TimeoutVoteMessage3::new(ballot, evidence),
1064                    ),
1065                };
1066                self.broadcast_in(self.slot_for(view), message, "broadcast timeout vote")?
1067            },
1068            ConsensusOutput::SendReVote(revote) => {
1069                info!(
1070                    %node,
1071                    view = %revote.revote.view,
1072                    epoch = %revote.revote.epoch,
1073                    block_view = %revote.revote.cert1.view_number(),
1074                    "send re-vote request"
1075                );
1076                self.broadcast_in(
1077                    self.slot_for(revote.revote.view),
1078                    ConsensusMessage::ReVote(revote),
1079                    "broadcast re-vote request",
1080                )?
1081            },
1082            ConsensusOutput::SendTimeoutCertificate(tc, view, epoch) => {
1083                debug!(
1084                    %node, %view, %epoch,
1085                    cert_view = %tc.view_number(),
1086                    "send timeout certificate"
1087                );
1088                let consensus_message = match tc {
1089                    TimeoutEvidence::V2(cert) => ConsensusMessage::TimeoutCertificate(cert),
1090                    TimeoutEvidence::V3(cert) => ConsensusMessage::TimeoutCertificate3(cert),
1091                };
1092                // To every node: consensus sends on only a certificate that took
1093                // this node out of the view without its own vote, which the nodes
1094                // still there may lack.
1095                self.broadcast_in(
1096                    self.slot_for(view),
1097                    consensus_message,
1098                    "broadcast timeout certificate",
1099                )?
1100            },
1101            ConsensusOutput::SendVote1(vote1) => {
1102                let view = vote1.vote.view_number();
1103                debug!(
1104                    %node, %view,
1105                    epoch_root = vote1.state_vote.is_some(),
1106                    "send vote1"
1107                );
1108                self.record_voted_view(view);
1109                if let Some(epoch) = vote1.vote.data.epoch
1110                    && let Some(leader) = self.leader(view, epoch)
1111                {
1112                    self.participation.leader_proposed(leader, epoch);
1113                }
1114                crate::trace_leader_event!(
1115                    self.consensus.tracer,
1116                    view,
1117                    crate::leader_trace::LeaderEvent::Vote1VMinus1BroadcastStart
1118                );
1119                let r = self.broadcast_in(
1120                    self.slot_for(view),
1121                    ConsensusMessage::Vote1(vote1),
1122                    "broadcast vote1",
1123                );
1124                crate::trace_leader_event!(
1125                    self.consensus.tracer,
1126                    view,
1127                    crate::leader_trace::LeaderEvent::Vote1VMinus1BroadcastEnd
1128                );
1129                r?
1130            },
1131            ConsensusOutput::BroadcastVidShare(share) => {
1132                let view = share.view_number();
1133                debug!(%node, %view, "send vid share");
1134                self.broadcast_in(
1135                    self.slot_for(view),
1136                    ConsensusMessage::VidShareBroadcast(share),
1137                    "broadcast vid share",
1138                )?
1139            },
1140            ConsensusOutput::SendVote2(vote2) => {
1141                let view = vote2.view_number();
1142                debug!(%node, %view, "send vote2");
1143                self.record_voted_view(view);
1144                crate::trace_leader_event!(
1145                    self.consensus.tracer,
1146                    view,
1147                    crate::leader_trace::LeaderEvent::Vote2VMinus1BroadcastStart
1148                );
1149                let r = self.broadcast_in(
1150                    self.slot_for(view),
1151                    ConsensusMessage::Vote2(vote2),
1152                    "broadcast vote2",
1153                );
1154                crate::trace_leader_event!(
1155                    self.consensus.tracer,
1156                    view,
1157                    crate::leader_trace::LeaderEvent::Vote2VMinus1BroadcastEnd
1158                );
1159                r?
1160            },
1161            ConsensusOutput::PersistBoundaryQc(qc) => {
1162                debug!(%node, view = %qc.view_number(), "persist boundary qc");
1163                self.storage.append_boundary_qc2(qc);
1164            },
1165            ConsensusOutput::PersistHighQc(high_qc) => {
1166                debug!(%node, view = %high_qc.view_number(), "persist high qc");
1167                self.storage.append_high_qc2(high_qc);
1168            },
1169            ConsensusOutput::SendEpochChange(epoch_change) => {
1170                let view = epoch_change.cert1.view_number();
1171                info!(
1172                    %node,
1173                    %view,
1174                    epoch = ?epoch_change.cert1.epoch().map(|e| *e),
1175                    "send epoch change"
1176                );
1177                // A node that decided the boundary from broadcast Cert2s needs
1178                // the boundary state seeded just like a receiver of an
1179                // EpochChangeMessage; for members this is a no-op.
1180                self.state_manager
1181                    .seed_from_header(epoch_change.proposal.clone());
1182                self.broadcast_in(
1183                    self.slot_for(view),
1184                    ConsensusMessage::EpochChange(epoch_change),
1185                    "broadcast epoch change",
1186                )?
1187            },
1188            ConsensusOutput::SendCertificate1(cert1) => {
1189                let view = cert1.view_number();
1190                debug!(
1191                    %node,
1192                    %view,
1193                    epoch = ?cert1.epoch().map(|e| *e),
1194                    "send certificate1"
1195                );
1196                crate::trace_leader_event!(
1197                    self.consensus.tracer,
1198                    view,
1199                    crate::leader_trace::LeaderEvent::Cert1VMinus1BroadcastStart
1200                );
1201                let r = self.broadcast_in(
1202                    self.slot_for(view),
1203                    ConsensusMessage::Certificate1(cert1, self.public_key.clone()),
1204                    "broadcast certificate1",
1205                );
1206                crate::trace_leader_event!(
1207                    self.consensus.tracer,
1208                    view,
1209                    crate::leader_trace::LeaderEvent::Cert1VMinus1BroadcastEnd
1210                );
1211                r?
1212            },
1213            ConsensusOutput::SendCertificate2(cert2) => {
1214                let view = cert2.view_number();
1215                debug!(
1216                    %node,
1217                    %view,
1218                    epoch = ?cert2.epoch().map(|e| *e),
1219                    "send certificate2"
1220                );
1221                self.broadcast_in(
1222                    self.slot_for(view),
1223                    ConsensusMessage::Certificate2(cert2, self.public_key.clone()),
1224                    "broadcast certificate2",
1225                )?
1226            },
1227            ConsensusOutput::ProposalValidated { proposal, sender } => {
1228                debug!(
1229                    %node,
1230                    view = %proposal.data.view_number,
1231                    sender = %KeyPrefix::from(&sender),
1232                    "proposal validated"
1233                );
1234            },
1235            ConsensusOutput::ViewChanged(view, epoch) => {
1236                let current_view = self.consensus.current_view();
1237                if view < current_view {
1238                    warn!(
1239                        %node, %view, %epoch, %current_view,
1240                        "ignoring view change to stale view"
1241                    );
1242                    return Ok(());
1243                }
1244                info!(%node, %view, %epoch, "view changed");
1245                self.timer.reset_with(view);
1246                self.gc(epoch, GcScope::Local(view))?;
1247                let resend = self.block_builder.on_view_changed(view);
1248                self.participation.on_view_changed(epoch);
1249                self.on_view_changed_metrics(view, epoch);
1250                if let Some(resend) = resend {
1251                    self.send_transactions(resend, epoch)?;
1252                }
1253
1254                // Proactively fetch the DRB for the next epoch so
1255                // late-starting nodes have it before they need it
1256                let next_epoch = epoch + 1;
1257                if next_epoch > EpochNumber::genesis() + 1 {
1258                    self.epoch_manager.request_drb_result(next_epoch);
1259                }
1260
1261                if self.leader(view, epoch).as_ref() == Some(&self.public_key)
1262                    && let Some(upgrade_proposal) = self.upgrade_protocol.maybe_propose(view, epoch)
1263                {
1264                    self.broadcast_in(
1265                        self.slot_for(view),
1266                        ConsensusMessage::UpgradeProposal(upgrade_proposal),
1267                        "broadcast upgrade proposal",
1268                    )?;
1269                }
1270            },
1271            ConsensusOutput::ViewTimedOut(view) => {
1272                debug!(%node, %view, "view timed out");
1273                self.server.view_timed_out(view);
1274                let epoch = self.consensus.current_epoch();
1275                self.gc(epoch, GcScope::Timeout(view))?;
1276            },
1277            ConsensusOutput::ProposalCertified(proposal) => {
1278                // Without this node's own share nothing pairs, so pin the
1279                // view here instead: the voters' broadcast shares are held
1280                // until a commitment admits them, and the header carries it.
1281                debug!(
1282                    %node, view = %proposal.view_number,
1283                    "pinning reconstructor from certified proposal"
1284                );
1285                self.notify_vid_reconstructor(&proposal, PinnedBy::Certificate);
1286            },
1287            ConsensusOutput::BlockPayloadReconstructed { .. } => {},
1288            ConsensusOutput::UpgradeDecided(cert) => {
1289                info!(
1290                    %node,
1291                    view = %cert.view_number(),
1292                    new_version = %cert.data.new_version,
1293                    first_view = %cert.data.new_version_first_view,
1294                    "persisting decided upgrade certificate"
1295                );
1296                self.storage.update_decided_upgrade_certificate(cert);
1297            },
1298        }
1299        Ok(())
1300    }
1301
1302    pub fn node_id(&self) -> &KeyPrefix {
1303        &self.node_id
1304    }
1305
1306    pub fn outbox(&self) -> &Outbox<ConsensusOutput<T>> {
1307        &self.outbox
1308    }
1309
1310    pub fn outbox_mut(&mut self) -> &mut Outbox<ConsensusOutput<T>> {
1311        &mut self.outbox
1312    }
1313
1314    pub fn current_view(&self) -> ViewNumber {
1315        self.consensus.current_view()
1316    }
1317
1318    pub fn last_decided_leaf(&self) -> &Leaf2<T> {
1319        self.consensus.last_decided_leaf()
1320    }
1321
1322    pub fn state(&self, v: ViewNumber) -> Option<&StateEntry<T>> {
1323        self.state_manager.get_state(v)
1324    }
1325
1326    pub fn client_api(&self) -> &ClientApi<T> {
1327        self.client.handle()
1328    }
1329
1330    #[cfg(test)]
1331    pub(crate) fn consensus(&self) -> &Consensus<T> {
1332        &self.consensus
1333    }
1334
1335    #[cfg(test)]
1336    pub(crate) fn consensus_mut(&mut self) -> &mut Consensus<T> {
1337        &mut self.consensus
1338    }
1339
1340    #[cfg(test)]
1341    pub(crate) fn holds_built_dispersal(
1342        &self,
1343        view: ViewNumber,
1344        epoch: EpochNumber,
1345        commit: VidCommitment2,
1346    ) -> bool {
1347        self.built_dispersals.contains_key(&(view, epoch, commit))
1348    }
1349
1350    #[cfg(test)]
1351    pub(crate) fn block_builder_mut(&mut self) -> &mut BlockBuilder<T> {
1352        &mut self.block_builder
1353    }
1354
1355    #[cfg(test)]
1356    pub(crate) fn network(&self) -> &Cliquenet<T> {
1357        &self.network
1358    }
1359
1360    #[cfg(test)]
1361    pub(crate) fn network_mut(&mut self) -> &mut Cliquenet<T> {
1362        &mut self.network
1363    }
1364
1365    /// Refresh the network's peer window for `epoch`.
1366    ///
1367    /// The coordinator does this itself whenever a proposal validates, but
1368    /// before its event loop is started callers can trigger this explicitly
1369    /// to keep the network up to date.
1370    pub fn bump_network_epoch(&mut self, epoch: EpochNumber) {
1371        if let Err(err) = self
1372            .network
1373            .apply_epoch(epoch, &self.membership_coordinator)
1374        {
1375            error!(%epoch, %err, "network apply_epoch failed");
1376        }
1377    }
1378
1379    pub(crate) fn on_network_message(
1380        &mut self,
1381        message: Message<T, Unchecked>,
1382    ) -> Option<ConsensusInput<T>> {
1383        let sender = KeyPrefix::from(&message.sender);
1384        let node = self.node_id;
1385        match message.message_type {
1386            MessageType::Consensus(msg) => match msg {
1387                ConsensusMessage::Proposal(p) => {
1388                    let view = p.view_number();
1389                    let epoch = p.proposal.data.epoch;
1390                    let block = p.proposal.data.block_header.block_number();
1391                    debug!(%node, %sender, %view, %epoch, %block, "recv proposal");
1392                    if self.is_epoch_too_far_ahead(Some(epoch)) {
1393                        warn!(%node, %sender, %view, %epoch, "proposal epoch is too far ahead");
1394                        return None;
1395                    }
1396                    if !self.is_view_too_far_ahead(view)
1397                        && self.proposal_received_at.is_none_or(|(v, _)| v < view)
1398                    {
1399                        self.proposal_received_at = Some((view, Instant::now()));
1400                    }
1401                    if self.consensus.wants_proposal_for_view(&view, epoch) {
1402                        self.proposal_validator.validate(p);
1403                    }
1404                    None
1405                },
1406                ConsensusMessage::VidShareFragment(fragment) => {
1407                    let view = fragment.data.view_number();
1408                    debug!(%node, %sender, %view, "received vid share fragment");
1409                    if self.is_view_too_far_ahead(view) {
1410                        warn!(%node, %sender, %view, "vid share fragment is too far ahead");
1411                        return None;
1412                    }
1413                    if fragment.data.recipient_key != self.public_key {
1414                        warn!(
1415                            %node,
1416                            %sender,
1417                            %view,
1418                            "ignoring vid share fragment not addressed to this node"
1419                        );
1420                        return None;
1421                    }
1422                    let Some(epoch) = fragment.data.epoch else {
1423                        warn!(%node, %sender, %view, "ignoring vid share fragment without an epoch");
1424                        return None;
1425                    };
1426                    if !self.is_fragment_epoch_admissible(epoch) {
1427                        warn!(
1428                            %node,
1429                            %sender,
1430                            %view,
1431                            %epoch,
1432                            "vid share fragment epoch is outside the admissible window"
1433                        );
1434                        return None;
1435                    }
1436                    if self.leader(view, epoch).as_ref() != Some(&message.sender) {
1437                        warn!(
1438                            %node,
1439                            %sender,
1440                            %view,
1441                            %epoch,
1442                            "ignoring vid share fragment not from the view leader"
1443                        );
1444                        return None;
1445                    }
1446                    if self.consensus.wants_proposal_for_view(&view, epoch) {
1447                        let signature = fragment.signature.clone();
1448                        match self
1449                            .vid_fragment_accumulator
1450                            .accept(&message.sender, fragment.data)
1451                        {
1452                            Ok(Some(share)) => self
1453                                .share_validator
1454                                .validate(SignedProposal::new(share, signature)),
1455                            Ok(None) => {}, // Still missing some fragments.
1456                            Err(err) => {
1457                                warn!(
1458                                    %node,
1459                                    %sender,
1460                                    %view,
1461                                    %err, "rejecting malformed vid share fragment"
1462                                );
1463                            },
1464                        }
1465                    }
1466                    None
1467                },
1468                ConsensusMessage::Vote1(vote1) => {
1469                    let view = vote1.vote.view_number();
1470                    if self.is_view_too_far_ahead(view) {
1471                        warn!(%node, %sender, %view, "vote1 is too far ahead");
1472                        return None;
1473                    }
1474                    if !self.is_vote_epoch_admissible(vote1.vote.epoch()) {
1475                        warn!(
1476                            %node,
1477                            %sender,
1478                            %view,
1479                            epoch = ?vote1.vote.epoch(),
1480                            "vote1 epoch is out of range"
1481                        );
1482                        return None;
1483                    }
1484                    if vote1.vote.signing_key() != message.sender {
1485                        warn!(%node, %sender, %view, "vote1 signing key != sender");
1486                        return None;
1487                    }
1488                    if !vote1.vote.data.is_well_formed(*self.consensus.epoch_height) {
1489                        warn!(%node, %sender, %view, "vote1 is not well formed");
1490                        return None;
1491                    }
1492                    crate::trace_leader_event!(
1493                        self.consensus.tracer,
1494                        view,
1495                        crate::leader_trace::LeaderEvent::Vote1VMinus1Arrived
1496                    );
1497                    let bn = vote1.vote.data.block_number.unwrap_or(0);
1498                    let epoch_height = *self.consensus.epoch_height;
1499                    let is_epoch_root_vote = is_epoch_root(bn, epoch_height);
1500                    debug!(
1501                        %node, %sender, %view,
1502                        epoch_root = is_epoch_root_vote,
1503                        has_state_vote = vote1.state_vote.is_some(),
1504                        "recv vote1"
1505                    );
1506                    if is_epoch_root_vote {
1507                        // An epoch-root Vote1 MUST carry a state_vote.
1508                        // Reject otherwise.
1509                        vote1.state_vote.as_ref()?;
1510                        self.epoch_root_collector.accumulate_vote(vote1);
1511                    } else {
1512                        self.vote1_collector.accumulate_vote(vote1.vote);
1513                    }
1514                    None
1515                },
1516                ConsensusMessage::VidShareBroadcast(share) => {
1517                    let view = share.view_number();
1518                    if self.is_view_too_far_ahead(view) {
1519                        warn!(%node, %sender, %view, "vid share broadcast is too far ahead");
1520                        return None;
1521                    }
1522                    debug!(%node, %sender, %view, "recv vid share broadcast");
1523                    // The share belongs to the sender (`recipient_key == sender`,
1524                    // enforced by `handle_vid_share`); it is verified lazily
1525                    // against the pinned commitment at reconstruction time.
1526                    self.vid_reconstructor
1527                        .handle_vid_share(message.sender.clone(), share);
1528                    None
1529                },
1530                ConsensusMessage::Vote2(vote2) => {
1531                    let view = vote2.view_number();
1532                    if self.is_view_too_far_ahead(view) {
1533                        warn!(%node, %sender, %view, "vote2 is too far ahead");
1534                        return None;
1535                    }
1536                    if !self.is_vote_epoch_admissible(vote2.epoch()) {
1537                        warn!(
1538                            %node,
1539                            %sender,
1540                            %view,
1541                            epoch = ?vote2.epoch(),
1542                            "vote2 epoch is out of range"
1543                        );
1544                        return None;
1545                    }
1546                    if vote2.signing_key() != message.sender {
1547                        warn!(%node, %sender, %view, "vote2 signing key != sender");
1548                        return None;
1549                    }
1550                    if !vote2.data.is_well_formed(*self.consensus.epoch_height) {
1551                        warn!(%node, %sender, %view, "vote2 is not well formed");
1552                        return None;
1553                    }
1554                    debug!(%node, %sender, %view, "recv vote2");
1555                    self.vote2_collector.accumulate_vote(vote2);
1556                    None
1557                },
1558                ConsensusMessage::Certificate1(certificate1, _key) => {
1559                    let view = certificate1.view_number();
1560                    debug!(
1561                        %node, %sender, %view,
1562                        epoch = ?certificate1.epoch().map(|e| *e),
1563                        "recv certificate1"
1564                    );
1565                    if self.is_view_too_far_ahead(view) {
1566                        warn!(%node, %sender, %view, "certificate1 is too far ahead");
1567                        return None;
1568                    }
1569                    if self.is_epoch_too_far_ahead(certificate1.epoch()) {
1570                        warn!(%node, %sender, %view, "certificate1 epoch is too far ahead");
1571                        return None;
1572                    }
1573                    if view == ViewNumber::genesis() {
1574                        warn!(%node, %sender, "certificate1 at the genesis view");
1575                        return None;
1576                    }
1577                    if let Some(epoch) = self
1578                        .cert_verifiers
1579                        .cert1
1580                        .verify(message.sender, certificate1)
1581                    {
1582                        self.epoch_manager.request_drb_result(epoch);
1583                    }
1584                    None
1585                },
1586                ConsensusMessage::Certificate2(certificate2, _key) => {
1587                    let view = certificate2.view_number();
1588                    debug!(
1589                        %node, %sender, %view,
1590                        epoch = ?certificate2.epoch().map(|e| *e),
1591                        "recv certificate2"
1592                    );
1593                    if self.is_view_too_far_ahead(view) {
1594                        warn!(%node, %sender, %view, "certificate2 is too far ahead");
1595                        return None;
1596                    }
1597                    if self.is_epoch_too_far_ahead(certificate2.epoch()) {
1598                        warn!(%node, %sender, %view, "certificate2 epoch is too far ahead");
1599                        return None;
1600                    }
1601                    if let Some(epoch) = self
1602                        .cert_verifiers
1603                        .cert2
1604                        .verify(message.sender, certificate2)
1605                    {
1606                        self.epoch_manager.request_drb_result(epoch);
1607                    }
1608                    None
1609                },
1610                ConsensusMessage::TimeoutVote(timeout_msg) => {
1611                    self.on_timeout_vote(
1612                        &message.sender,
1613                        TimeoutVote::V2(timeout_msg.vote),
1614                        timeout_msg.evidence,
1615                    );
1616                    None
1617                },
1618                ConsensusMessage::TimeoutVote3(timeout_msg) => {
1619                    match timeout_msg.ballot() {
1620                        Ok(ballot) => self.on_timeout_vote(
1621                            &message.sender,
1622                            TimeoutVote::V3(ballot),
1623                            timeout_msg.evidence(),
1624                        ),
1625                        Err(reason) => warn!(
1626                            %node, %sender, view = %timeout_msg.view, %reason,
1627                            "malformed timeout vote"
1628                        ),
1629                    }
1630                    None
1631                },
1632                ConsensusMessage::ReVote(revote) => {
1633                    let view = revote.view_number();
1634                    let epoch = revote.revote.epoch;
1635                    debug!(%node, %sender, %view, %epoch, "recv re-vote request");
1636                    if self.is_view_too_far_ahead(view) {
1637                        warn!(%node, %sender, %view, "re-vote is too far ahead");
1638                        return None;
1639                    }
1640                    if !self.is_vote_epoch_admissible(Some(epoch)) {
1641                        warn!(%node, %sender, %view, %epoch, "re-vote epoch is out of range");
1642                        return None;
1643                    }
1644                    // Checked before the lock's epoch can start a catchup.
1645                    if let Err(err) = revote.revote.well_formed(*self.consensus.epoch_height) {
1646                        warn!(%node, %sender, %view, %err, "malformed re-vote request");
1647                        return None;
1648                    }
1649                    if let Some(epoch) = self.cert_verifiers.revote.verify(message.sender, revote) {
1650                        self.epoch_manager.request_drb_result(epoch);
1651                    }
1652                    None
1653                },
1654                ConsensusMessage::TimeoutCertificate(tc) => {
1655                    debug!(
1656                        %node, %sender,
1657                        view = %tc.view_number(),
1658                        epoch = ?tc.data.epoch.map(|e| *e),
1659                        "recv timeout certificate"
1660                    );
1661                    if let Some(epoch) = self.verify_timeout_cert(&message.sender, tc) {
1662                        self.epoch_manager.request_drb_result(epoch);
1663                    }
1664                    None
1665                },
1666                ConsensusMessage::TimeoutCertificate3(tc) => {
1667                    debug!(
1668                        %node, %sender,
1669                        view = %tc.view_number(),
1670                        epoch = %tc.epoch,
1671                        "recv timeout certificate"
1672                    );
1673                    if let Some(epoch) = self.verify_timeout_cert3(&message.sender, tc) {
1674                        self.epoch_manager.request_drb_result(epoch);
1675                    }
1676                    None
1677                },
1678                ConsensusMessage::HighQc(qc) => {
1679                    debug!(
1680                        %node, %sender,
1681                        view = %qc.view_number(),
1682                        epoch = ?qc.epoch().map(|e| *e),
1683                        "recv high qc"
1684                    );
1685                    if let Some(epoch) = self.verify_advance(&message.sender, qc) {
1686                        self.epoch_manager.request_drb_result(epoch);
1687                    }
1688                    None
1689                },
1690                ConsensusMessage::EpochChange(epoch_change) => {
1691                    let view = epoch_change.cert1.view_number();
1692                    let epoch = epoch_change.cert1.epoch();
1693                    debug!(%node, %sender, %view, epoch = ?epoch.map(|e| *e), "recv epoch change");
1694                    if !epoch
1695                        .is_some_and(|e| is_epoch_admissible(e, self.consensus.current_epoch()))
1696                    {
1697                        warn!(%node, %sender, %view, ?epoch, "epoch change is out of range");
1698                        return None;
1699                    }
1700                    if let Some(epoch) = self
1701                        .cert_verifiers
1702                        .epoch_change
1703                        .verify(message.sender, epoch_change)
1704                    {
1705                        self.epoch_manager.request_drb_result(epoch);
1706                    }
1707                    None
1708                },
1709                ConsensusMessage::UpgradeProposal(upgrade_proposal) => {
1710                    let view = upgrade_proposal.data.view_number();
1711                    debug!(%node, %sender, %view, "recv upgrade proposal");
1712                    if self.is_view_too_far_ahead(view) {
1713                        warn!(%node, %sender, %view, "upgrade proposal is too far ahead");
1714                        return None;
1715                    }
1716                    let current_view = self.consensus.current_view();
1717                    let current_epoch = self.consensus.current_epoch();
1718                    let leader = self.leader(view, current_epoch);
1719                    if let Some(vote) = self.upgrade_protocol.maybe_vote(
1720                        &upgrade_proposal,
1721                        &message.sender,
1722                        leader.as_ref(),
1723                        current_view,
1724                        current_epoch,
1725                    ) && let Err(err) = self.broadcast_in(
1726                        self.slot_for(view),
1727                        ConsensusMessage::UpgradeVote(vote),
1728                        "upgrade vote",
1729                    ) {
1730                        warn!(%node, %err, "network error while broadcasting upgrade vote");
1731                    }
1732                    None
1733                },
1734                ConsensusMessage::UpgradeVote(vote) => {
1735                    let view = vote.view_number();
1736                    if self.is_view_too_far_ahead(view) {
1737                        warn!(%node, %sender, %view, "upgrade vote is too far ahead");
1738                        return None;
1739                    }
1740                    if !self.is_vote_epoch_admissible(vote.epoch()) {
1741                        warn!(
1742                            %node,
1743                            %sender,
1744                            %view,
1745                            epoch = ?vote.epoch(),
1746                            "upgrade vote epoch is out of range"
1747                        );
1748                        return None;
1749                    }
1750                    if vote.signing_key() != message.sender {
1751                        warn!(%node, %sender, %view, "upgrade vote signing key != sender");
1752                        return None;
1753                    }
1754                    debug!(%node, %sender, %view, "recv upgrade vote");
1755                    self.upgrade_vote_collector.accumulate_vote(vote);
1756                    None
1757                },
1758            },
1759            MessageType::Block(msg) => {
1760                match msg {
1761                    BlockMessage::Transactions(msg) => {
1762                        debug!(
1763                            %node, %sender,
1764                            view = %msg.view,
1765                            count = msg.transactions.len(),
1766                            "recv transactions"
1767                        );
1768                        if !self.is_view_too_far_ahead(msg.view) {
1769                            self.block_builder.on_transactions(msg)
1770                        }
1771                    },
1772                    BlockMessage::DedupManifest(manifest) => {
1773                        debug!(
1774                            %node, %sender,
1775                            view = %manifest.view,
1776                            epoch = %manifest.epoch,
1777                            hashes = manifest.hashes.len(),
1778                            "recv dedup manifest"
1779                        );
1780                        if !self.is_view_too_far_ahead(manifest.view)
1781                            && let Some(view_leader) = self.leader(manifest.view, manifest.epoch)
1782                            && view_leader == message.sender
1783                        {
1784                            self.block_builder.on_dedup_manifest(manifest)
1785                        }
1786                    },
1787                }
1788                None
1789            },
1790            MessageType::ProposalFetch(ProposalFetchMessage::Request(request)) => {
1791                let view = request.view_number();
1792                debug!(%node, %sender, %view, "recv proposal fetch request");
1793                if !request.validate_sender(&message.sender) {
1794                    warn!(
1795                        %node,
1796                        sender = %message.sender,
1797                        %view,
1798                        "ignoring invalid proposal fetch request signature"
1799                    );
1800                    return None;
1801                }
1802                let proposals: Vec<_> = self
1803                    .consensus
1804                    .signed_proposals_for_fetch(&view)
1805                    .into_iter()
1806                    .cloned()
1807                    .collect();
1808                for proposal in proposals {
1809                    if let Err(err) = self.unicast_in(
1810                        self.slot_for(view),
1811                        &message.sender,
1812                        MessageType::ProposalFetch(ProposalFetchMessage::Response(Box::new(
1813                            proposal,
1814                        ))),
1815                        "proposal response",
1816                    ) {
1817                        warn!(%node, %err, "network error while sending proposal response");
1818                    }
1819                }
1820                None
1821            },
1822            MessageType::ProposalFetch(ProposalFetchMessage::Response(proposal)) => {
1823                debug!(
1824                    %node, %sender,
1825                    view = %proposal.data.view_number,
1826                    "recv proposal fetch response"
1827                );
1828                self.pending_proposal_fetches.resolve(&proposal);
1829                self.maybe_validate_fetched_proposal(*proposal);
1830                None
1831            },
1832            MessageType::PayloadFetch(PayloadFetchMessage::Req(request)) => {
1833                let view = request.view_number();
1834                debug!(%node, %sender, %view, "received payload fetch request");
1835                if let Err(err) = self.server.handle_request(
1836                    &request,
1837                    &message.sender,
1838                    self.consensus.current_view(),
1839                    self.network.sender(),
1840                ) {
1841                    warn!(%node, %sender, %view, %err, "failed to send payload response");
1842                }
1843                None
1844            },
1845            MessageType::PayloadFetch(PayloadFetchMessage::Res(response)) => {
1846                let view = response.view_number();
1847                debug!(%node, %sender, %view, "received payload fetch response");
1848                let refused = self.fetcher.response(
1849                    response,
1850                    &message.sender,
1851                    &self.consensus,
1852                    &self.membership_coordinator,
1853                );
1854                for commit in refused {
1855                    if !self.consensus.is_reconstructed(view, commit)
1856                        && let Some((peer, message)) = self.fetcher.request(
1857                            view,
1858                            commit,
1859                            Retry::SameRound,
1860                            &self.consensus,
1861                            &self.membership_coordinator,
1862                        )
1863                        && let Err(err) = self.unicast_in(
1864                            self.slot_for(view),
1865                            &peer,
1866                            message.message_type,
1867                            "payload request",
1868                        )
1869                    {
1870                        warn!(%node, %sender, %view, %err, "failed to send payload request");
1871                    }
1872                }
1873                None
1874            },
1875            MessageType::External(data) => {
1876                debug!(%node, %sender, bytes = data.len(), "recv external message");
1877                let _ = self.external_tx.send(OpaqueMessage {
1878                    sender: message.sender,
1879                    data,
1880                });
1881                None
1882            },
1883        }
1884    }
1885
1886    fn on_state_manager_output(
1887        &mut self,
1888        output: StateManagerOutput<T>,
1889    ) -> Option<ConsensusInput<T>> {
1890        match output {
1891            StateManagerOutput::State {
1892                response,
1893                validated: true,
1894            } => Some(ConsensusInput::StateValidated(response)),
1895            StateManagerOutput::State {
1896                response,
1897                validated: false,
1898            } => Some(ConsensusInput::StateValidationFailed(response)),
1899            StateManagerOutput::Header {
1900                response,
1901                header: Some(hdr),
1902            } => Some(ConsensusInput::HeaderCreated(
1903                response.view,
1904                proposal_commitment(&response.parent_proposal),
1905                hdr,
1906            )),
1907            StateManagerOutput::Header {
1908                response,
1909                header: None,
1910            } => {
1911                warn!(view = %response.view, "header creation failed");
1912                None
1913            },
1914        }
1915    }
1916
1917    /// The slot for a message about `view`.
1918    ///
1919    /// A node can send a message about a later view than its own: it enters a view only after
1920    /// rebuilding the parent block. Every view change drops queued messages more than one slot
1921    /// below the new view, so such a message takes its own view to survive the view changes the
1922    /// node still has to make. A message about an earlier view takes the current view, which is
1923    /// never below that bound.
1924    fn slot_for(&self, view: ViewNumber) -> ViewNumber {
1925        view.max(self.consensus.current_view())
1926    }
1927
1928    fn broadcast_in(
1929        &self,
1930        slot: ViewNumber,
1931        message_type: impl Into<MessageType<T, Validated>>,
1932        ctx: &'static str,
1933    ) -> Result<(), CoordinatorError> {
1934        let message = Message {
1935            sender: self.public_key.clone(),
1936            message_type: message_type.into(),
1937        };
1938        self.network
1939            .sender()
1940            .broadcast(slot, &message)
1941            .map_err(|e| CoordinatorError::from(e).context(ctx))
1942    }
1943
1944    fn unicast_in(
1945        &self,
1946        slot: ViewNumber,
1947        recipient: &T::SignatureKey,
1948        message_type: impl Into<MessageType<T, Validated>>,
1949        ctx: &'static str,
1950    ) -> Result<(), CoordinatorError> {
1951        let message = Message {
1952            sender: self.public_key.clone(),
1953            message_type: message_type.into(),
1954        };
1955        self.network
1956            .sender()
1957            .unicast(slot, recipient, &message)
1958            .map_err(|e| CoordinatorError::from(e).context(ctx))
1959    }
1960
1961    /// Sends `forward` to the leaders of the `fanout` views from its own, each leader once with
1962    /// the earliest of its views.
1963    fn send_transactions(
1964        &mut self,
1965        forward: TransactionMessage<T>,
1966        epoch: EpochNumber,
1967    ) -> Result<(), CoordinatorError> {
1968        let leaders = self.upcoming_leaders(forward.view, self.block_builder.fanout().get(), epoch);
1969        for (view, leader) in leaders {
1970            let message = TransactionMessage {
1971                view,
1972                transactions: forward.transactions.clone(),
1973            };
1974            self.unicast_in(
1975                self.slot_for(view),
1976                &leader,
1977                MessageType::Block(BlockMessage::Transactions(message)),
1978                "unicast transactions",
1979            )?;
1980        }
1981        Ok(())
1982    }
1983
1984    /// The leaders of the `count` views from `first`, each once with the earliest of its views.
1985    fn upcoming_leaders(
1986        &mut self,
1987        first: ViewNumber,
1988        count: u64,
1989        epoch: EpochNumber,
1990    ) -> Vec<(ViewNumber, T::SignatureKey)> {
1991        let mut leaders = Vec::<(ViewNumber, T::SignatureKey)>::new();
1992        for view in (0..count).map(|ahead| first + ahead) {
1993            let Some(leader) = self.upcoming_leader(view, epoch) else {
1994                warn!(%view, %epoch, "failed to resolve an upcoming leader");
1995                continue;
1996            };
1997            if leaders.iter().all(|(_, known)| *known != leader) {
1998                leaders.push((view, leader));
1999            }
2000        }
2001        leaders
2002    }
2003
2004    /// The leader of an upcoming `view`. Consensus picks it with the epoch of the block it
2005    /// proposes, which is the next epoch when that block builds on its epoch's last block. Timed
2006    /// out views add no block, so the latest proposal is the best guess for that parent. `epoch`
2007    /// is the fallback when there is no proposal yet or its epoch's stake table is unknown.
2008    fn upcoming_leader(&mut self, view: ViewNumber, epoch: EpochNumber) -> Option<T::SignatureKey> {
2009        let expected = self.consensus.last_proposal_before(view).map(|p| {
2010            if is_last_block(p.block_header.block_number(), *self.consensus.epoch_height) {
2011                p.epoch + 1
2012            } else {
2013                p.epoch
2014            }
2015        });
2016        expected
2017            .and_then(|e| self.leader(view, e))
2018            .or_else(|| self.leader(view, epoch))
2019    }
2020
2021    fn leader(&mut self, view: ViewNumber, epoch: EpochNumber) -> Option<T::SignatureKey> {
2022        let membership = self
2023            .membership_coordinator
2024            .membership_for_epoch(Some(epoch))
2025            .ok()?;
2026        membership.leader(view).ok()
2027    }
2028
2029    fn on_client_request(&mut self, request: ClientRequest<T>) -> Result<(), CoordinatorError> {
2030        match request {
2031            ClientRequest::CurrentView(tx) => {
2032                let _ = tx.send(self.consensus.current_view());
2033            },
2034            ClientRequest::CurrentEpoch(tx) => {
2035                let _ = tx.send(self.consensus.current_epoch());
2036            },
2037            ClientRequest::DecidedLeaf(tx) => {
2038                let _ = tx.send(self.consensus.last_decided_leaf().clone());
2039            },
2040            ClientRequest::DecidedState(tx) => {
2041                let view = self.consensus.last_decided_leaf().view_number();
2042                let _ = tx.send(self.state(view).map(|s| s.state.clone()));
2043            },
2044            ClientRequest::UndecidedLeaves(tx) => {
2045                let _ = tx.send(self.consensus.undecided_leaves().cloned().collect());
2046            },
2047            ClientRequest::GetState { view, respond } => {
2048                let _ = respond.send(self.state(view).map(|s| s.state.clone()));
2049            },
2050            ClientRequest::GetStateAndDelta { view, respond } => {
2051                let _ = respond.send(match self.state(view) {
2052                    Some(s) => (Some(s.state.clone()), s.delta.clone()),
2053                    None => (None, None),
2054                });
2055            },
2056            ClientRequest::ProposalParticipation { epoch, respond } => {
2057                let _ = respond.send(match epoch {
2058                    Some(epoch) => self.participation.proposal_participation(epoch),
2059                    None => self.participation.current_proposal_participation(),
2060                });
2061            },
2062            ClientRequest::VoteParticipation { epoch, respond } => {
2063                let _ = respond.send(match epoch {
2064                    Some(epoch) => self.participation.vote_participation(epoch),
2065                    None => self.participation.current_vote_participation(),
2066                });
2067            },
2068            ClientRequest::SubmitTransaction { tx, respond } => {
2069                let forward = match self.block_builder.on_submit_transaction(tx) {
2070                    Ok(forward) => forward,
2071                    Err(err) => {
2072                        let _ = respond.send(Err(err));
2073                        return Ok(());
2074                    },
2075                };
2076                // The transaction is queued, and the retry buffer resends it if no leader
2077                // resolves now.
2078                let _ = respond.send(Ok(()));
2079                if let Some(forward) = forward {
2080                    self.send_transactions(forward, self.consensus.current_epoch())?;
2081                }
2082            },
2083            ClientRequest::UpdateLeaf { update, respond } => {
2084                self.state_manager.update_state(update);
2085                let _ = respond.send(());
2086            },
2087            ClientRequest::RequestProposal {
2088                view,
2089                leaf_commitment,
2090                respond,
2091            } => {
2092                if let Some(proposal) = self.consensus.signed_proposal(view, leaf_commitment) {
2093                    let _ = respond.send(Ok(proposal.clone()));
2094                    return Ok(());
2095                }
2096                if !self
2097                    .pending_proposal_fetches
2098                    .contains_request(view, leaf_commitment)
2099                {
2100                    self.broadcast_proposal_fetch(view)?;
2101                }
2102                self.pending_proposal_fetches
2103                    .push(view, leaf_commitment, respond);
2104            },
2105            ClientRequest::SendExternalMessage {
2106                payload,
2107                recipient,
2108                respond,
2109            } => {
2110                let message = Message {
2111                    sender: self.public_key.clone(),
2112                    message_type: MessageType::External(payload),
2113                };
2114                let result = self
2115                    .network
2116                    .sender()
2117                    .unicast(self.consensus.current_view(), &recipient, &message)
2118                    .map_err(|err| {
2119                        CoordinatorError::from(err)
2120                            .context("send external message")
2121                            .into()
2122                    });
2123                let _ = respond.send(result);
2124            },
2125        }
2126
2127        Ok(())
2128    }
2129
2130    fn record_voted_view(&mut self, view: ViewNumber) {
2131        if self.voted_view.is_some_and(|v| v >= view) {
2132            return;
2133        }
2134        self.voted_view = Some(view);
2135        if let Some(m) = &self.metrics {
2136            m.consensus.last_voted_view.set(*view as usize);
2137        }
2138    }
2139
2140    fn on_view_changed_metrics(&mut self, view: ViewNumber, epoch: EpochNumber) {
2141        if let Some((started_view, started_epoch, started_at)) = self.view_started
2142            && started_view == view
2143        {
2144            if epoch > started_epoch {
2145                self.view_started = Some((view, epoch, started_at));
2146            }
2147            return;
2148        }
2149        let prev = self.view_started.replace((view, epoch, Instant::now()));
2150        let duration_as_leader = prev.and_then(|(prev_view, prev_epoch, entered)| {
2151            (self.leader(prev_view, prev_epoch).as_ref() == Some(&self.public_key))
2152                .then(|| entered.elapsed())
2153        });
2154        let Some(m) = &self.metrics else { return };
2155        let consensus = &m.consensus;
2156        consensus.current_view.set(*view as usize);
2157        let invalid_certs = self
2158            .cert_verifiers
2159            .num_invalid_certs()
2160            .saturating_sub(self.invalid_certs_at_decide);
2161        consensus.invalid_qc.set(invalid_certs as usize);
2162        let last_decided_view = self.consensus.last_decided_view();
2163        if view > last_decided_view {
2164            consensus
2165                .number_of_views_since_last_decide
2166                .set((*view - *last_decided_view) as usize);
2167        }
2168        if let Some(duration) = duration_as_leader {
2169            consensus
2170                .view_duration_as_leader
2171                .add_point(duration.as_secs_f64());
2172        }
2173        let (outstanding_txns, outstanding_bytes) = self.block_builder.outstanding_transactions();
2174        consensus.outstanding_transactions.set(outstanding_txns);
2175        consensus
2176            .outstanding_transactions_memory_size
2177            .set(outstanding_bytes);
2178    }
2179
2180    fn on_decide_metrics(&mut self, leaves: &[Leaf2<T>]) {
2181        let Some(newest) = leaves.first() else { return };
2182        // The consensus watermark already includes this batch, so the batch
2183        // advanced the decide frontier iff its newest view is the watermark;
2184        // a gap-fill decide of older views must not regress the gauges.
2185        let advanced = newest.view_number() == self.consensus.last_decided_view();
2186        if advanced {
2187            self.invalid_certs_at_decide = self.cert_verifiers.num_invalid_certs();
2188        }
2189        let Some(m) = &self.metrics else { return };
2190        let consensus = &m.consensus;
2191        let now = OffsetDateTime::now_utc().unix_timestamp();
2192        for leaf in leaves {
2193            let txn_bytes = self
2194                .payload_txn_bytes
2195                .get(&leaf.view_number())
2196                .copied()
2197                .or_else(|| leaf.block_payload_ref().map(|p| p.txn_bytes()));
2198            if let Some(txn_bytes) = txn_bytes {
2199                consensus.finalized_bytes.add_point(txn_bytes as f64);
2200            }
2201            if advanced {
2202                match (now as u64).checked_sub(leaf.block_header().timestamp()) {
2203                    Some(age) => consensus.proposal_to_decide_time.add_point(age as f64),
2204                    None => error!(
2205                        timestamp = leaf.block_header().timestamp(),
2206                        "failed to calculate proposal to decide time: timestamp in the future"
2207                    ),
2208                }
2209            }
2210        }
2211        if !advanced {
2212            return;
2213        }
2214        consensus.last_decided_time.set(now as usize);
2215        consensus.invalid_qc.set(0);
2216        consensus
2217            .last_decided_view
2218            .set(*newest.view_number() as usize);
2219        consensus
2220            .last_synced_block_height
2221            .set(newest.block_header().block_number() as usize);
2222        if let Some(views_in_flight) =
2223            (*self.consensus.current_view()).checked_sub(*newest.view_number())
2224        {
2225            consensus
2226                .number_of_views_per_decide_event
2227                .add_point(views_in_flight as f64);
2228        }
2229    }
2230
2231    /// Broadcast a signed proposal fetch request for `view` to all peers.
2232    fn broadcast_proposal_fetch(&mut self, view: ViewNumber) -> Result<(), CoordinatorError> {
2233        let request = self
2234            .consensus
2235            .signed_proposal_fetch_request(view)
2236            .map_err(|err| {
2237                let err = format!("failed to sign proposal request: {err}");
2238                CoordinatorError::regular(err).context("sign proposal request")
2239            })?;
2240        self.broadcast_in(
2241            self.slot_for(view),
2242            MessageType::ProposalFetch(ProposalFetchMessage::Request(request)),
2243            "broadcast proposal request",
2244        )
2245    }
2246
2247    fn request_missing_proposal(
2248        &mut self,
2249        view: ViewNumber,
2250        leaf_commit: Commitment<Leaf2<T>>,
2251    ) -> Result<(), CoordinatorError> {
2252        // Asked again once the view has moved on: nothing may have been able
2253        // to answer, and while a boundary is stuck nothing is decided that
2254        // would clear the request.
2255        let current = self.consensus.current_view();
2256        let key = ProposalFetchKey::new(view, leaf_commit);
2257        if self
2258            .requested_missing_proposals
2259            .get(&key)
2260            .is_some_and(|asked_in| *asked_in >= current)
2261        {
2262            return Ok(());
2263        }
2264        self.requested_missing_proposals.insert(key, current);
2265        self.broadcast_proposal_fetch(view)
2266    }
2267
2268    fn maybe_validate_fetched_proposal(&mut self, proposal: SignedProposal<T, Proposal<T>>) {
2269        let view = proposal.data.view_number;
2270        // Matched by leaf: a block fetched by a re-vote certificate's view
2271        // comes back proposed at an earlier one.
2272        let leaf_commitment = proposal_commitment(&proposal.data);
2273        let Some(key) = self
2274            .requested_missing_proposals
2275            .keys()
2276            .find(|key| key.leaf_commitment == leaf_commitment && key.view >= view)
2277            .copied()
2278        else {
2279            return;
2280        };
2281        self.requested_missing_proposals.remove(&key);
2282        if self.consensus.proposals().contains(view, leaf_commitment) {
2283            return;
2284        }
2285        self.proposal_validator
2286            .validate_fetched(ProposalMessage::unchecked(proposal));
2287    }
2288
2289    /// Pin the reconstructor to `proposal`'s payload commitment, so the shares
2290    /// the voters broadcast for its view can rebuild the payload.
2291    fn notify_vid_reconstructor(&mut self, proposal: &Proposal<T>, pinned_by: PinnedBy) {
2292        let view = proposal.view_number;
2293        let VidCommitment::V2(payload_commitment) = proposal.block_header.payload_commitment()
2294        else {
2295            warn!(node = %self.node_id, %view, "proposal payload commitment is not V2");
2296            return;
2297        };
2298        let expected_param = expected_vid_param(&self.membership_coordinator, proposal.epoch);
2299        self.vid_reconstructor.handle_proposal(
2300            view,
2301            payload_commitment,
2302            proposal.block_header.metadata().clone(),
2303            proposal.epoch,
2304            expected_param,
2305            pinned_by,
2306        );
2307    }
2308
2309    fn gc(&mut self, epoch: EpochNumber, scope: GcScope) -> Result<(), CoordinatorError> {
2310        self.consensus.gc(scope);
2311        match scope {
2312            GcScope::Local(view) => {
2313                // The previous view is kept: its messages may still arrive, and
2314                // at an epoch boundary a re-vote's certificate can move this node
2315                // past the view of the next epoch's first block before it votes
2316                // on it or, as its leader, proposes it.
2317                let view = view.saturating_sub(GC_MARGIN_VIEWS.get() - 1).into();
2318                self.block_builder.gc(view);
2319                self.vid_disperser.gc(view);
2320                self.vid_fragment_accumulator.gc(view);
2321                self.network.gc(view)?;
2322                self.timeout_collector.gc(view);
2323                self.timeout_one_honest_collector.gc(view);
2324                self.timeout3_collector.gc(view);
2325                self.timeout_one_honest3_collector.gc(view);
2326                self.vote1_collector.gc(view);
2327                self.vote2_collector.gc(view);
2328                self.upgrade_vote_collector.gc(view);
2329                self.upgrade_protocol.gc(view);
2330            },
2331            GcScope::Decided(view) => {
2332                let decide_floor = self.consensus.decide_floor();
2333                self.epoch_manager.gc(epoch);
2334                self.epoch_root_collector.gc(view);
2335                self.cert_verifiers.gc(decide_floor, epoch);
2336                self.pending_proposal_fetches.gc(view);
2337                self.requested_missing_proposals
2338                    .retain(|key, _| key.view > view);
2339                self.fetcher.gc(view);
2340                self.state_manager.gc(view);
2341                self.storage
2342                    .gc(view.saturating_sub(STORAGE_GC_MARGIN).into());
2343                self.vid_reconstructor
2344                    .gc(view.saturating_sub(VID_RECONSTRUCT_GC_MARGIN).into());
2345                let vc = VidCommitment2::default();
2346                self.da_payloads = self.da_payloads.split_off(&(view, vc));
2347                self.built_dispersals =
2348                    self.built_dispersals
2349                        .split_off(&(view, EpochNumber::new(0), vc));
2350                self.payload_txn_bytes = self.payload_txn_bytes.split_off(&(decide_floor + 1));
2351            },
2352            GcScope::Timeout(view) => {
2353                // The shares voters broadcast for the view before stay until now, so a
2354                // node a timeout certificate moved past a view before any certificate of
2355                // it arrived can still rebuild the payload once it fetches the proposal.
2356                let before = ViewNumber::from(view.saturating_sub(1));
2357                if !self.consensus.keeps_payload_on_timeout(before) {
2358                    self.vid_reconstructor.retire_view(before);
2359                }
2360                // The same guard keeps `Consensus::gc` from dropping its half.
2361                if self.consensus.keeps_payload_on_timeout(view) {
2362                    return Ok(());
2363                }
2364                let vc = VidCommitment2::default();
2365                self.da_payloads
2366                    .extract_if((view, vc)..(view + 1, vc), |_, _| true)
2367                    .for_each(drop);
2368                let e = EpochNumber::new(0);
2369                self.built_dispersals
2370                    .extract_if((view, e, vc)..(view + 1, e, vc), |_, _| true)
2371                    .for_each(drop);
2372            },
2373        }
2374        Ok(())
2375    }
2376
2377    /// We ignore votes more than `MAX_VIEWS_AHEAD` ahead of ours.
2378    fn is_view_too_far_ahead(&self, v: ViewNumber) -> bool {
2379        v > self.consensus.current_view() + *MAX_VIEWS_AHEAD
2380    }
2381
2382    /// Is `epoch` close enough to the node's own to tally a vote naming it?
2383    fn is_vote_epoch_admissible(&self, epoch: Option<EpochNumber>) -> bool {
2384        let current = self.consensus.current_epoch();
2385        epoch.is_none_or(|e| is_epoch_admissible(e, current))
2386    }
2387
2388    /// We ignore messages more than `EPOCH_CHANGE_LOOKAHEAD` ahead of ours.
2389    fn is_epoch_too_far_ahead(&self, epoch: Option<EpochNumber>) -> bool {
2390        let current = self.consensus.current_epoch();
2391        epoch.is_some_and(|e| e > current + EPOCH_CHANGE_LOOKAHEAD)
2392    }
2393
2394    /// We ignore messages more than `EPOCH_CHANGE_LOOKBEHIND` behind ours.
2395    fn is_epoch_too_far_behind(&self, epoch: EpochNumber) -> bool {
2396        let current = self.consensus.current_epoch();
2397        epoch.u64() < current.saturating_sub(EPOCH_CHANGE_LOOKBEHIND)
2398    }
2399
2400    /// Is `epoch` close enough to the node's own to buffer a fragment naming it?
2401    fn is_fragment_epoch_admissible(&self, epoch: EpochNumber) -> bool {
2402        let current = self.consensus.current_epoch();
2403        is_epoch_admissible(epoch, current)
2404    }
2405
2406    pub(crate) fn catchup_evidence(&self) -> Option<ConsensusMessage<T, Validated>> {
2407        Some(match self.consensus.catchup_evidence()? {
2408            CatchupEvidence::Qc(qc) => ConsensusMessage::HighQc(qc),
2409            CatchupEvidence::Tc(tc) => ConsensusMessage::TimeoutCertificate(tc),
2410            CatchupEvidence::Tc3(tc) => ConsensusMessage::TimeoutCertificate3(tc),
2411        })
2412    }
2413
2414    fn on_timeout_vote(
2415        &mut self,
2416        sender: &T::SignatureKey,
2417        mut vote: TimeoutVote<T>,
2418        evidence: Option<CatchupEvidence<T>>,
2419    ) {
2420        let node = self.node_id;
2421        let view = vote.view_number();
2422        if vote.signing_key() != *sender {
2423            warn!(%node, %sender, %view, "timeout vote signing key != sender");
2424            return;
2425        }
2426
2427        let current_view = self.consensus.current_view();
2428        let has_evidence = evidence.is_some();
2429
2430        if let Some(e) = evidence.filter(|e| e.view_number() >= current_view) {
2431            let epoch = match e {
2432                CatchupEvidence::Qc(qc) => self.verify_advance(sender, qc),
2433                CatchupEvidence::Tc(tc) => self.verify_timeout_cert(sender, tc),
2434                CatchupEvidence::Tc3(tc) => self.verify_timeout_cert3(sender, tc),
2435            };
2436            if let Some(epoch) = epoch {
2437                self.epoch_manager.request_drb_result(epoch);
2438            }
2439        }
2440
2441        if self.is_view_too_far_ahead(view) {
2442            warn!(%node, %sender, %view, "timeout vote is too far ahead");
2443            return;
2444        }
2445
2446        if !vote.is_well_formed() {
2447            warn!(%node, %sender, %view, "timeout vote not well formed");
2448            return;
2449        }
2450
2451        // A vote of an epoch this node has left comes from a node that has not
2452        // learnt it ended; the epoch change brings it across.
2453        if vote.binds_epoch()
2454            && let Some(epoch) = vote.epoch()
2455            && epoch < self.consensus.current_epoch()
2456        {
2457            self.send_epoch_change(sender, epoch);
2458        }
2459
2460        if view < current_view {
2461            debug!(
2462                %node, %sender, %view, %current_view,
2463                "timeout vote for stale view; replying with catchup evidence"
2464            );
2465            self.send_catchup_evidence(sender, view);
2466            return;
2467        }
2468
2469        debug!(%node, %sender, %view, has_evidence, "recv timeout vote");
2470
2471        // A vote that does not bind its epoch names no committee: its
2472        // signature covers only the view, so the epoch field is whatever the
2473        // sender wrote there. Tallying it under this node's own epoch keeps
2474        // every such vote for a view in one tally, and leaves the committee it
2475        // is verified against to this node rather than to whoever sent the
2476        // first vote. Votes that do bind their epoch are tallied under it.
2477        //
2478        // The node's epoch can still move while a view is collecting, which
2479        // leaves the votes it has taken so far under the epoch it left. They
2480        // are not merged into the new tally; what recovers them is that a node
2481        // re-signs and re-sends its timeout vote on every re-arm of the timer,
2482        // so the new tally fills from the next round.
2483        if let TimeoutVote::V2(vote) = &mut vote {
2484            vote.data.epoch = Some(self.consensus.current_epoch());
2485        }
2486
2487        if !self.is_vote_epoch_admissible(vote.epoch()) {
2488            warn!(
2489                %node,
2490                %sender,
2491                %view,
2492                epoch = ?vote.epoch(),
2493                "timeout vote epoch is out of range"
2494            );
2495            return;
2496        }
2497
2498        if vote.binds_epoch() != self.consensus.upgrade_lock().timeout_epoch_bound(view) {
2499            warn!(%node, %sender, %view, "timeout vote has the wrong form for its version");
2500            return;
2501        }
2502
2503        match vote {
2504            TimeoutVote::V2(vote) => {
2505                self.timeout_collector.accumulate_vote(vote.clone());
2506                self.timeout_one_honest_collector.accumulate_vote(vote);
2507            },
2508            TimeoutVote::V3(ballot) => {
2509                // A vote whose lock is of an epoch whose committee is not
2510                // known yet waits in the collector (`Ballot::epochs`).
2511                if let Some(epoch) = self.timeout3_collector.accumulate_vote(ballot.clone()) {
2512                    debug!(%node, %sender, %view, %epoch, "timeout vote waits for a committee");
2513                    self.epoch_manager.request_drb_result(epoch);
2514                }
2515                self.timeout_one_honest3_collector.accumulate_vote(ballot);
2516            },
2517        }
2518    }
2519
2520    /// Submit a `Cert1` that would advance our view for verification.
2521    ///
2522    /// A certificate at the genesis view passes the verifier unsigned, which is
2523    /// right only for the genesis QC, so any other one claiming that view is
2524    /// dropped here. Dropping it before verification rather than in consensus
2525    /// also keeps it out of the verifier's completed keys, where a forgery
2526    /// naming the genesis epoch would shadow the genesis QC itself.
2527    fn verify_advance(
2528        &mut self,
2529        sender: &T::SignatureKey,
2530        qc: Certificate1<T>,
2531    ) -> Option<EpochNumber> {
2532        let genesis = ViewNumber::genesis();
2533        if qc.view_number() == genesis
2534            && self
2535                .consensus
2536                .cert1_at(genesis)
2537                .is_none_or(|seeded| seeded.data != qc.data)
2538        {
2539            warn!(node = %self.node_id, %sender, "cert1 at the genesis view is not the genesis QC");
2540            return None;
2541        }
2542        self.cert_verifiers.advance.verify(sender.clone(), qc)
2543    }
2544
2545    fn verify_timeout_cert(
2546        &mut self,
2547        sender: &T::SignatureKey,
2548        tc: TimeoutCertificate2<T>,
2549    ) -> Option<EpochNumber> {
2550        let view = tc.view_number();
2551        if self.consensus.upgrade_lock().timeout_epoch_bound(view) {
2552            warn!(node = %self.node_id, %sender, %view, "inadmissible timeout certificate");
2553            return None;
2554        }
2555        self.cert_verifiers.timeout.verify(sender.clone(), tc)
2556    }
2557
2558    fn verify_timeout_cert3(
2559        &mut self,
2560        sender: &T::SignatureKey,
2561        tc: TimeoutCertificate3<T>,
2562    ) -> Option<EpochNumber> {
2563        let view = tc.view_number();
2564        let epoch = tc.epoch;
2565        if self.is_epoch_too_far_behind(epoch) {
2566            warn!(node = %self.node_id, %sender, %view, %epoch, "timeout certificate too far behind");
2567            return None;
2568        }
2569        if !self.consensus.upgrade_lock().timeout_epoch_bound(view) {
2570            warn!(node = %self.node_id, %sender, %view, "inadmissible timeout certificate");
2571            return None;
2572        }
2573        if view < self.consensus.decide_floor() {
2574            debug!(node = %self.node_id, %sender, %view, "stale timeout certificate");
2575            return None;
2576        }
2577        if let Err(err) = tc.check_lock() {
2578            warn!(node = %self.node_id, %sender, %view, %err, "malformed timeout certificate");
2579            return None;
2580        }
2581        self.cert_verifiers.timeout3.verify(sender.clone(), tc)
2582    }
2583
2584    fn send_epoch_change(&mut self, peer: &T::SignatureKey, epoch: EpochNumber) {
2585        let Some(epoch_change) = self.consensus.epoch_change_ending(epoch) else {
2586            return;
2587        };
2588        let message = Message {
2589            sender: self.public_key.clone(),
2590            message_type: MessageType::Consensus(ConsensusMessage::EpochChange(epoch_change)),
2591        };
2592        if let Err(err) =
2593            self.network
2594                .sender()
2595                .unicast(self.consensus.current_view(), peer, &message)
2596        {
2597            warn!(%epoch, %err, "failed to send epoch change");
2598        }
2599    }
2600
2601    fn send_catchup_evidence(&mut self, peer: &T::SignatureKey, stale_view: ViewNumber) {
2602        let Some(evidence) = self.catchup_evidence() else {
2603            return;
2604        };
2605        let view = evidence.view_number();
2606        if view < stale_view {
2607            return;
2608        }
2609        if let Err(err) = self.unicast_in(self.slot_for(view), peer, evidence, "catchup evidence") {
2610            warn!(%stale_view, %err, "failed to send catchup evidence");
2611        }
2612    }
2613}
2614
2615/// Garbage collection scope.
2616#[derive(Debug, Clone, Copy)]
2617pub enum GcScope {
2618    /// GC is invoked on local view changes.
2619    Local(ViewNumber),
2620    /// GC is invoked on local decided views.
2621    Decided(ViewNumber),
2622    /// GC is invoked on a view that advanced via timeout certificate.
2623    Timeout(ViewNumber),
2624}
2625
2626/// The admissible window around `current`.
2627pub(crate) fn is_epoch_admissible(epoch: EpochNumber, current: EpochNumber) -> bool {
2628    current.saturating_sub(EPOCH_CHANGE_LOOKBEHIND) <= *epoch
2629        && *epoch <= current.saturating_add(EPOCH_CHANGE_LOOKAHEAD)
2630}
2631
2632/// A payload built locally and awaiting DA persistence.
2633struct PendingDa<T: NodeType> {
2634    epoch: EpochNumber,
2635    payload: T::BlockPayload,
2636    metadata: <T::BlockPayload as BlockPayload<T>>::Metadata,
2637    hashes: Vec<Commitment<T::Transaction>>,
2638}
2639
2640type ProposalFetchResponseSender<T> =
2641    oneshot::Sender<Result<SignedProposal<T, Proposal<T>>, QueryError>>;
2642
2643#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
2644struct ProposalFetchKey<T: NodeType> {
2645    view: ViewNumber,
2646    leaf_commitment: Commitment<Leaf2<T>>,
2647}
2648
2649impl<T: NodeType> ProposalFetchKey<T> {
2650    fn new(view: ViewNumber, leaf_commitment: Commitment<Leaf2<T>>) -> Self {
2651        Self {
2652            view,
2653            leaf_commitment,
2654        }
2655    }
2656}
2657
2658#[derive(Default)]
2659struct PendingProposalFetches<T: NodeType> {
2660    pending: HashMap<ProposalFetchKey<T>, Vec<ProposalFetchResponseSender<T>>>,
2661}
2662
2663impl<T: NodeType> PendingProposalFetches<T> {
2664    fn prune_closed(&mut self) {
2665        self.pending.retain(|_, responders| {
2666            responders.retain(|respond| !respond.is_closed());
2667            !responders.is_empty()
2668        });
2669    }
2670
2671    fn contains_request(
2672        &mut self,
2673        view: ViewNumber,
2674        leaf_commitment: Commitment<Leaf2<T>>,
2675    ) -> bool {
2676        self.prune_closed();
2677        self.pending
2678            .contains_key(&ProposalFetchKey::new(view, leaf_commitment))
2679    }
2680
2681    fn push(
2682        &mut self,
2683        view: ViewNumber,
2684        leaf_commitment: Commitment<Leaf2<T>>,
2685        respond: ProposalFetchResponseSender<T>,
2686    ) {
2687        self.pending
2688            .entry(ProposalFetchKey::new(view, leaf_commitment))
2689            .or_default()
2690            .push(respond);
2691    }
2692
2693    fn gc(&mut self, view: ViewNumber) {
2694        self.pending.retain(|key, responders| {
2695            responders.retain(|respond| !respond.is_closed());
2696            key.view >= view && !responders.is_empty()
2697        });
2698    }
2699
2700    fn resolve(&mut self, proposal: &SignedProposal<T, Proposal<T>>) {
2701        self.prune_closed();
2702        let view = proposal.data.view_number;
2703        let leaf_commitment = proposal_commitment(&proposal.data);
2704        let key = ProposalFetchKey::new(view, leaf_commitment);
2705
2706        if let Some(responders) = self.pending.remove(&key) {
2707            for respond in responders {
2708                let _ = respond.send(Ok(proposal.clone()));
2709            }
2710        }
2711    }
2712}