Skip to main content

espresso_node/
context.rs

1use std::{
2    collections::BTreeMap,
3    fmt::{Debug, Display},
4    future::Future,
5    path::PathBuf,
6    sync::Arc,
7    time::{Duration, Instant},
8};
9
10use anyhow::{Context, ensure};
11use async_lock::RwLock;
12use derivative::Derivative;
13use espresso_types::{
14    NodeState, Transaction, ValidatedState,
15    v0::traits::{EventConsumer as PersistenceEventConsumer, SequencerPersistence},
16};
17use futures::{
18    future::join_all,
19    stream::{BoxStream, Stream, StreamExt},
20};
21use hotshot_new_protocol::{
22    coordinator::Coordinator,
23    leader_trace::{CsvLeaderTracer, LeaderTracerHandle},
24    network::{Cliquenet, NetworkError},
25};
26use hotshot_orchestrator::client::OrchestratorClient;
27use hotshot_types::{
28    HotShotConfig, PeerConfig, ValidatorConfig,
29    consensus::ConsensusMetricsValue,
30    constants::EXTERNAL_EVENT_CHANNEL_SIZE,
31    data::{Leaf2, ViewNumber},
32    drb::drb_difficulty_selector,
33    epoch_membership::EpochMembershipCoordinator,
34    initializer::{HotShotInitializer, load_start_epoch_info},
35    message::UpgradeLock,
36    network::NetworkConfig,
37    new_protocol::CoordinatorEvent,
38    traits::{
39        election::Membership,
40        metrics::{Counter, Gauge, Histogram, Metrics},
41    },
42    upgrade_config::UpgradeConfig,
43};
44use parking_lot::Mutex;
45use request_response::RequestResponseConfig;
46use tokio::{
47    select, spawn,
48    sync::{
49        broadcast,
50        mpsc::{channel, unbounded_channel},
51        watch,
52    },
53    task::JoinHandle,
54};
55use tracing::{Instrument, Level, info};
56use url::Url;
57use versions::{NEW_PROTOCOL_VERSION, Version};
58
59use crate::{
60    SeqTypes, SequencerApiVersion,
61    catchup::ParallelStateCatchup,
62    consensus_handle::ConsensusHandle,
63    external_event_handler::ExternalEventHandler,
64    proposal_fetcher::ProposalFetcherConfig,
65    request_response::{
66        RequestResponseProtocol,
67        data_source::{DataSource, Storage as RequestResponseStorage},
68        network::Sender as RequestResponseSender,
69        recipient_source::RecipientSource,
70    },
71    startup_catchup::bootstrap_epoch_window,
72    state_signature::{self, DecidedLeaf, StateSignatureMemStorage, StateSigner},
73};
74
75/// Inbound request-response messages are dropped (not queued) when this channel is full. A node
76/// catching up has many batched requests in flight, each answered by up to `request_batch_size`
77/// peers per batch interval, so this must comfortably exceed that fan-in.
78const REQUEST_RESPONSE_CHANNEL_CAPACITY: usize = 256;
79
80/// Responders block on the outbound channel while holding request-response admission permits.
81const OUTBOUND_MESSAGE_CHANNEL_CAPACITY: usize = 128;
82
83/// The sequencer context contains a consensus handle and other sequencer specific information.
84#[derive(Derivative, Clone)]
85#[derivative(Debug(bound = ""))]
86pub struct SequencerContext<P: SequencerPersistence> {
87    #[derivative(Debug = "ignore")]
88    consensus_handle: Arc<ConsensusHandle<SeqTypes, Arc<P>>>,
89
90    #[derivative(Debug = "ignore")]
91    persistence: Arc<P>,
92
93    /// The request-response protocol
94    #[derivative(Debug = "ignore")]
95    #[allow(dead_code)]
96    pub request_response_protocol: RequestResponseProtocol<P>,
97
98    /// Light client state signatures produced by the state signer task.
99    state_signatures: Arc<RwLock<StateSignatureMemStorage>>,
100
101    /// An orchestrator to wait for before starting consensus, with the peer config this node
102    /// registered there.
103    #[derivative(Debug = "ignore")]
104    wait_for_orchestrator: Option<(Arc<OrchestratorClient>, PeerConfig<SeqTypes>)>,
105
106    /// Background tasks to shut down when the node is dropped.
107    tasks: TaskList,
108
109    detached: bool,
110
111    node_state: NodeState,
112
113    network_config: NetworkConfig<SeqTypes>,
114
115    #[derivative(Debug = "ignore")]
116    validator_config: ValidatorConfig<SeqTypes>,
117}
118
119impl<P> SequencerContext<P>
120where
121    P: SequencerPersistence,
122{
123    #[tracing::instrument(skip_all, fields(node_id = initializer.instance_state().node_id))]
124    #[allow(clippy::too_many_arguments)]
125    pub async fn init<F>(
126        network_config: NetworkConfig<SeqTypes>,
127        upgrade: versions::Upgrade,
128        validator_config: ValidatorConfig<SeqTypes>,
129        membership_coordinator: EpochMembershipCoordinator<SeqTypes>,
130        initializer: HotShotInitializer<SeqTypes>,
131        anchor_view: Option<ViewNumber>,
132        storage: Option<RequestResponseStorage>,
133        state_catchup: ParallelStateCatchup,
134        persistence: Arc<P>,
135        coordinator_network: F,
136        state_relay_server: Option<Url>,
137        metrics: &dyn Metrics,
138        stake_table_capacity: usize,
139        event_consumer: impl PersistenceEventConsumer + 'static,
140        proposal_fetcher_cfg: ProposalFetcherConfig,
141        bootstrap_epoch_catchup_timeout: Duration,
142        empty_block_delay: Duration,
143        block_sizes: BTreeMap<Version, u64>,
144        leader_trace_dir: Option<PathBuf>,
145    ) -> anyhow::Result<Self>
146    where
147        F: AsyncFnOnce(UpgradeLock<SeqTypes>) -> Result<Cliquenet<SeqTypes>, NetworkError>,
148    {
149        let config = &network_config.config;
150        let pub_key = validator_config.public_key;
151        tracing::info!(%pub_key, "initializing consensus");
152        let upgrade_lock = restored_upgrade_lock(upgrade, &initializer);
153        let version = running_version(&upgrade_lock);
154        ensure!(
155            version >= NEW_PROTOCOL_VERSION,
156            "the chain runs version {version}, which predates the new protocol \
157             ({NEW_PROTOCOL_VERSION}), the only consensus this node runs"
158        );
159
160        let instance_state = initializer.instance_state().clone();
161
162        // Stick our node ID in `metrics` so it is easily accessible via the status API.
163        metrics
164            .create_gauge("node_index".into(), None)
165            .set(instance_state.node_id as usize);
166
167        // Start L1 client if it isn't already.
168        instance_state.l1_client.spawn_tasks().await;
169
170        info!(target: "announce", ?initializer, "starting up sequencer context with initializer");
171
172        let stake_table = config.hotshot_stake_table();
173        let stake_table_commit = stake_table.commitment(stake_table_capacity)?;
174        let stake_table_epoch = None;
175        let should_vote =
176            state_signature::should_vote(&stake_table, &validator_config.state_public_key);
177
178        let epoch_height = initializer.epoch_height();
179
180        seed_membership(&membership_coordinator, config, &upgrade_lock, &initializer).await;
181
182        let mut coordinator_network = coordinator_network(upgrade_lock.clone()).await?;
183
184        let current_epoch = bootstrap_epoch_window(
185            &membership_coordinator,
186            epoch_height,
187            bootstrap_epoch_catchup_timeout,
188        )
189        .await
190        .context("startup stake-table catchup failed")?;
191        tracing::info!(%current_epoch, "Startup catchup complete");
192
193        // For cliquenet this dials the N-1/N/N+1 epoch window before consensus starts.
194        if let Err(err) = coordinator_network.apply_epoch(current_epoch, &membership_coordinator) {
195            tracing::warn!(%current_epoch, %err, "coordinator network apply_epoch failed at startup");
196        }
197
198        // Restore the persisted lock so the new protocol resumes with the lock
199        // it actually held, not the older decided-anchor QC.
200        let locked_qc = persistence
201            .load_high_qc2()
202            .await
203            .context("loading persisted locked QC")?;
204        // And the epoch boundary certificate a re-vote and the next epoch's
205        // first block name.
206        let boundary_qc = persistence
207            .load_boundary_qc2()
208            .await
209            .context("loading persisted boundary QC")?;
210        let anchor_cert2 = persistence
211            .load_cert2(initializer.anchor_leaf().view_number())
212            .await
213            .context("loading the anchor's cert2")?;
214
215        let leader_tracer = leader_trace_dir
216            .map(|dir| {
217                let node_id = instance_state.node_id;
218                let path = dir.join(format!("leader_trace_node{node_id}.csv"));
219                CsvLeaderTracer::new(node_id, &path)
220                    .map(|tracer| Arc::new(tracer) as LeaderTracerHandle)
221                    .with_context(|| format!("creating leader trace {}", path.display()))
222            })
223            .transpose()?;
224
225        let (external_tx, external_rx) = unbounded_channel();
226        let coordinator = Coordinator::maker()
227            .membership_coordinator(membership_coordinator.clone())
228            .network(coordinator_network)
229            .initializer(&initializer)
230            .upgrade_lock(upgrade_lock.clone())
231            .public_key(validator_config.public_key)
232            .private_key(validator_config.private_key.clone())
233            .state_private_key(validator_config.state_private_key.clone())
234            .stake_table_capacity(stake_table_capacity)
235            .timeout_duration(Duration::from_secs(10))
236            .empty_block_delay(empty_block_delay)
237            .block_sizes(block_sizes)
238            .storage(Arc::clone(&persistence))
239            .metrics(metrics)
240            .consensus_metrics(ConsensusMetricsValue::new(metrics))
241            .maybe_locked_qc(locked_qc)
242            .maybe_boundary_qc(boundary_qc)
243            .maybe_anchor_cert2(anchor_cert2)
244            .maybe_leader_tracer(leader_tracer)
245            .upgrade_config(UpgradeConfig {
246                start_proposing_view: config.start_proposing_view,
247                stop_proposing_view: config.stop_proposing_view,
248                start_voting_view: config.start_voting_view,
249                stop_voting_view: config.stop_voting_view,
250                start_proposing_time: config.start_proposing_time,
251                stop_proposing_time: config.stop_proposing_time,
252                start_voting_time: config.start_voting_time,
253                stop_voting_time: config.stop_voting_time,
254            })
255            .external_tx(external_tx)
256            .make();
257
258        let consensus_handle = Arc::new(ConsensusHandle::new(
259            coordinator,
260            external_rx,
261            upgrade_lock,
262            membership_coordinator.clone(),
263            &initializer,
264            EXTERNAL_EVENT_CHANNEL_SIZE,
265            metrics,
266        ));
267
268        let mut state_signer = StateSigner::new(
269            validator_config.state_private_key.clone(),
270            validator_config.state_public_key.clone(),
271            stake_table_commit,
272            stake_table_epoch,
273            stake_table_capacity,
274            should_vote,
275        );
276        if let Some(url) = state_relay_server {
277            state_signer = state_signer.with_relay_server(url);
278        }
279
280        // Create the channel for sending outbound messages from the external event handler
281        let (outbound_message_sender, outbound_message_receiver) =
282            channel(OUTBOUND_MESSAGE_CHANNEL_CAPACITY);
283        let (request_response_sender, request_response_receiver) =
284            channel(REQUEST_RESPONSE_CHANNEL_CAPACITY);
285
286        // Configure the request-response protocol
287        let request_response_config = RequestResponseConfig {
288            incoming_request_ttl: Duration::from_secs(40),
289            incoming_request_timeout: Duration::from_secs(5),
290            request_batch_size: 5,
291            request_batch_interval: Duration::from_secs(2),
292            // Permits are held while a response is derived (possibly from SQL), and a peer
293            // catching up legitimately issues several distinct requests concurrently
294            max_incoming_requests: 32,
295            max_incoming_requests_per_key: 4,
296        };
297
298        // Create the request-response protocol
299        let request_response_protocol = RequestResponseProtocol::new(
300            request_response_config,
301            RequestResponseSender::new(outbound_message_sender),
302            request_response_receiver,
303            RecipientSource {
304                memberships: membership_coordinator,
305                consensus_handle: consensus_handle.clone(),
306                public_key: validator_config.public_key,
307            },
308            DataSource {
309                node_state: instance_state.clone(),
310                storage,
311                persistence: persistence.clone(),
312                consensus_handle: consensus_handle.clone(),
313            },
314            validator_config.public_key,
315            validator_config.private_key.clone(),
316        );
317
318        // Add the request-response protocol to the list of providers for state catchup. Since the interior is mutable,
319        // the request-response protocol will now retroactively be used anywhere we passed in the original struct (e.g. in consensus
320        // itself)
321        state_catchup.add_provider(Arc::new(request_response_protocol.clone()));
322
323        // Create the external event handler
324        let mut tasks = TaskList::default();
325        let external_event_handler = ExternalEventHandler::new(
326            &mut tasks,
327            request_response_sender,
328            outbound_message_receiver,
329            consensus_handle.clone(),
330            pub_key,
331            metrics
332                .subgroup("request_response".into())
333                .create_counter("inbound_dropped".into(), None)
334                .into(),
335        )
336        .await
337        .with_context(|| "Failed to create external event handler")?;
338
339        Ok(Self::new(
340            consensus_handle,
341            persistence,
342            state_signer,
343            external_event_handler,
344            request_response_protocol,
345            instance_state,
346            network_config,
347            validator_config,
348            event_consumer,
349            anchor_view,
350            proposal_fetcher_cfg,
351            metrics,
352        )
353        .with_task_list(tasks))
354    }
355
356    /// Constructor
357    #[allow(clippy::too_many_arguments)]
358    fn new(
359        consensus_handle: Arc<ConsensusHandle<SeqTypes, Arc<P>>>,
360        persistence: Arc<P>,
361        state_signer: StateSigner<SequencerApiVersion>,
362        external_event_handler: ExternalEventHandler,
363        request_response_protocol: RequestResponseProtocol<P>,
364        node_state: NodeState,
365        network_config: NetworkConfig<SeqTypes>,
366        validator_config: ValidatorConfig<SeqTypes>,
367        event_consumer: impl PersistenceEventConsumer + 'static,
368        anchor_view: Option<ViewNumber>,
369        proposal_fetcher_cfg: ProposalFetcherConfig,
370        metrics: &dyn Metrics,
371    ) -> Self {
372        let events = consensus_handle.event_stream();
373
374        let node_id = node_state.node_id;
375        let mut ctx = Self {
376            consensus_handle,
377            persistence: persistence.clone(),
378            state_signatures: Arc::clone(&state_signer.signatures),
379            request_response_protocol,
380            tasks: Default::default(),
381            detached: false,
382            wait_for_orchestrator: None,
383            node_state,
384            network_config,
385            validator_config,
386        };
387
388        // Spawn proposal fetching tasks.
389        proposal_fetcher_cfg.spawn(
390            &mut ctx.tasks,
391            ctx.consensus_handle.clone(),
392            persistence.clone(),
393            metrics,
394        );
395
396        // Shared between the event loop and the background decide processor.
397        let event_consumer = Arc::new(event_consumer);
398
399        // Wakes the background decide processor. `watch` coalesces: the processor is cursor-driven,
400        // so it only needs the latest decided view.
401        let (decide_tx, decide_rx) = watch::channel::<DecideSignal>(None);
402
403        // Background decide processor: query-service ingestion + GC, decoupled from the event loop.
404        ctx.spawn(
405            "decide processor",
406            process_decided_events_task(
407                persistence.clone(),
408                event_consumer.clone(),
409                decide_rx,
410                anchor_view,
411                DecideProcessorMetrics::new(metrics),
412            ),
413        );
414
415        let (signer_tx, signer_rx) = broadcast::channel(SIGNER_QUEUE_CAPACITY);
416        ctx.spawn(
417            "state signer",
418            sign_decided_leaves(ctx.consensus_handle.clone(), state_signer, signer_rx),
419        );
420
421        // Event loop. On a decide this only does the leaf write, then signals `decide_tx`.
422        ctx.spawn(
423            "event handler",
424            handle_events(
425                node_id,
426                events,
427                persistence,
428                signer_tx,
429                external_event_handler,
430                event_consumer,
431                decide_tx,
432            ),
433        );
434
435        ctx
436    }
437
438    /// Wait for a signal from the orchestrator before starting consensus.
439    ///
440    /// `peer_config` is what this node registered with the orchestrator. `/ready` posts it
441    /// again, and the orchestrator equality-checks it against `known_nodes_with_stake`.
442    pub fn wait_for_orchestrator(
443        mut self,
444        client: OrchestratorClient,
445        peer_config: PeerConfig<SeqTypes>,
446    ) -> Self {
447        self.wait_for_orchestrator = Some((Arc::new(client), peer_config));
448        self
449    }
450
451    /// Add a list of tasks to the given context.
452    pub(crate) fn with_task_list(mut self, tasks: TaskList) -> Self {
453        self.tasks.extend(tasks);
454        self
455    }
456
457    /// Return the light client state signatures this node has produced.
458    pub fn state_signatures(&self) -> Arc<RwLock<StateSignatureMemStorage>> {
459        self.state_signatures.clone()
460    }
461
462    /// Stream consensus events.
463    pub fn event_stream(&self) -> BoxStream<'static, CoordinatorEvent<SeqTypes>> {
464        self.consensus_handle.event_stream()
465    }
466
467    pub async fn submit_transaction(&self, tx: Transaction) -> anyhow::Result<()> {
468        self.consensus_handle.submit_transaction(tx).await
469    }
470
471    pub fn consensus_handle(&self) -> Arc<ConsensusHandle<SeqTypes, Arc<P>>> {
472        self.consensus_handle.clone()
473    }
474
475    pub fn validator_config(&self) -> &ValidatorConfig<SeqTypes> {
476        &self.validator_config
477    }
478
479    pub fn upgrade_lock(&self) -> UpgradeLock<SeqTypes> {
480        self.consensus_handle.upgrade_lock()
481    }
482
483    pub async fn shutdown_consensus(&self) {
484        self.consensus_handle.shut_down().await
485    }
486
487    pub async fn decided_leaf(&self) -> Leaf2<SeqTypes> {
488        self.consensus_handle.decided_leaf().await
489    }
490
491    pub async fn state(&self, view: ViewNumber) -> Option<Arc<ValidatedState>> {
492        self.consensus_handle.state(view).await
493    }
494
495    pub async fn decided_state(&self) -> Option<Arc<ValidatedState>> {
496        self.consensus_handle.decided_state().await
497    }
498
499    pub fn node_id(&self) -> u64 {
500        self.node_state.node_id
501    }
502
503    pub fn node_state(&self) -> NodeState {
504        self.node_state.clone()
505    }
506
507    pub fn persistence(&self) -> Arc<P> {
508        self.persistence.clone()
509    }
510
511    /// Start participating in consensus.
512    pub async fn start_consensus(&self) {
513        if let Some((orchestrator_client, peer_config)) = &self.wait_for_orchestrator {
514            tracing::warn!("waiting for orchestrated start");
515            orchestrator_client
516                .wait_for_all_nodes_ready(PeerConfig::to_bytes(peer_config))
517                .await;
518        } else {
519            // the network config was loaded from storage or fetched from
520            // peers, so there is no need of orchestrator
521            // This is the normal path for a node rejoining an existing network.
522            tracing::info!("no orchestrator configured");
523        }
524        tracing::warn!("starting consensus");
525        self.consensus_handle.start_consensus().await;
526    }
527
528    /// Spawn a background task attached to this context.
529    ///
530    /// When this context is dropped or [`shut_down`](Self::shut_down), background tasks will be
531    /// cancelled in the reverse order that they were spawned.
532    pub fn spawn(&mut self, name: impl Display, task: impl Future<Output: Debug> + Send + 'static) {
533        self.tasks.spawn(name, task);
534    }
535
536    /// Spawn a short-lived background task attached to this context.
537    ///
538    /// When this context is dropped or [`shut_down`](Self::shut_down), background tasks will be
539    /// cancelled in the reverse order that they were spawned.
540    ///
541    /// The only difference between a short-lived background task and a [long-lived](Self::spawn)
542    /// one is how urgently logging related to the task is treated.
543    pub fn spawn_short_lived(
544        &mut self,
545        name: impl Display,
546        task: impl Future<Output: Debug> + Send + 'static,
547    ) {
548        self.tasks.spawn_short_lived(name, task);
549    }
550
551    /// Stop participating in consensus.
552    pub async fn shut_down(&mut self) {
553        tracing::info!("shutting down SequencerContext");
554        self.consensus_handle.shut_down().await;
555        self.tasks.shut_down();
556        self.node_state.l1_client.shut_down_tasks().await;
557
558        // Since we've already shut down, we can set `detached` so the drop
559        // handler doesn't call `shut_down` again.
560        self.detached = true;
561    }
562
563    /// Wait for consensus to complete.
564    ///
565    /// Under normal conditions, this function will block forever, which is a convenient way of
566    /// keeping the main thread from exiting as long as there are still active background tasks.
567    /// It returns once the coordinator has stopped, so a node whose coordinator died exits
568    /// instead of serving its last state.
569    pub async fn join(&mut self) {
570        select! {
571            () = self.tasks.join() => {},
572            () = self.consensus_handle.stopped() => tracing::error!("coordinator stopped"),
573        }
574    }
575
576    /// Allow this node to continue participating in consensus even after it is dropped.
577    pub fn detach(&mut self) {
578        // Set `detached` so the drop handler doesn't call `shut_down`.
579        self.detached = true;
580    }
581
582    /// Get the network config
583    pub fn network_config(&self) -> NetworkConfig<SeqTypes> {
584        self.network_config.clone()
585    }
586}
587
588/// A decided certificate at or below the configured base is from a completed upgrade; kept, it
589/// would make `UpgradeLock::version` fail once the target moves on.
590fn restored_upgrade_lock(
591    upgrade: versions::Upgrade,
592    initializer: &HotShotInitializer<SeqTypes>,
593) -> UpgradeLock<SeqTypes> {
594    let decided_upgrade_certificate = initializer
595        .decided_upgrade_certificate()
596        .filter(|cert| cert.data.new_version > upgrade.base)
597        .cloned();
598    tracing::info!(?upgrade, ?decided_upgrade_certificate, "resuming consensus");
599    UpgradeLock::from_certificate(upgrade, &decided_upgrade_certificate)
600}
601
602/// The version the chain runs now: the decided upgrade's, or the base when none was decided.
603fn running_version(upgrade_lock: &UpgradeLock<SeqTypes>) -> Version {
604    upgrade_lock
605        .decided_upgrade_cert()
606        .map(|cert| cert.data.new_version)
607        .unwrap_or(upgrade_lock.upgrade().base)
608}
609
610async fn seed_membership(
611    membership_coordinator: &EpochMembershipCoordinator<SeqTypes>,
612    config: &HotShotConfig<SeqTypes>,
613    upgrade_lock: &UpgradeLock<SeqTypes>,
614    initializer: &HotShotInitializer<SeqTypes>,
615) {
616    membership_coordinator.set_drb_difficulty_selector(drb_difficulty_selector(config));
617
618    let running_version = running_version(upgrade_lock);
619    let membership = membership_coordinator.membership();
620    for da_committee in &config.da_committees {
621        if running_version >= da_committee.start_version {
622            membership.add_da_committee(
623                da_committee.start_epoch.into(),
624                da_committee.committee.clone(),
625            );
626        }
627    }
628
629    load_start_epoch_info(membership_coordinator, initializer.start_epoch_info()).await;
630}
631
632impl<P: SequencerPersistence> Drop for SequencerContext<P> {
633    fn drop(&mut self) {
634        if !self.detached {
635            // Spawn a task to shut down the context
636            let consensus_handle = self.consensus_handle.clone();
637            let tasks_clone = self.tasks.clone();
638            let node_state_clone = self.node_state.clone();
639
640            spawn(async move {
641                tracing::info!("shutting down SequencerContext");
642                consensus_handle.shut_down().await;
643                tasks_clone.shut_down();
644                node_state_clone.l1_client.shut_down_tasks().await;
645            });
646
647            // Set `detached` so the drop handler doesn't call `shut_down` again.
648            self.detached = true;
649        }
650    }
651}
652
653/// Latest decided view, sent from the event loop to the background decide processor. `None` is
654/// the initial/no-op value of the `watch` channel.
655type DecideSignal = Option<ViewNumber>;
656
657/// Metrics for the background decide processor. `backlog` (decided - processed) is the key signal:
658/// sustained growth means staging tables accumulate (no data lost, but disk grows).
659struct DecideProcessorMetrics {
660    last_decided: Arc<dyn Gauge>,
661    last_processed: Arc<dyn Gauge>,
662    backlog: Arc<dyn Gauge>,
663    duration: Arc<dyn Histogram>,
664    failures: Arc<dyn Counter>,
665}
666
667impl DecideProcessorMetrics {
668    fn new(metrics: &(impl Metrics + ?Sized)) -> Self {
669        let metrics = metrics.subgroup("decide_processor".into());
670        Self {
671            last_decided: metrics
672                .create_gauge("last_decided".into(), Some("view".into()))
673                .into(),
674            last_processed: metrics
675                .create_gauge("last_processed".into(), Some("view".into()))
676                .into(),
677            backlog: metrics
678                .create_gauge("backlog".into(), Some("view".into()))
679                .into(),
680            duration: metrics
681                .create_histogram("process_duration".into(), Some("seconds".into()))
682                .into(),
683            failures: metrics.create_counter("failures".into(), None).into(),
684        }
685    }
686}
687
688#[tracing::instrument(skip_all, fields(node_id))]
689async fn handle_events<P, C>(
690    node_id: u64,
691    mut events: impl Stream<Item = CoordinatorEvent<SeqTypes>> + Unpin,
692    persistence: Arc<P>,
693    signer_tx: broadcast::Sender<DecidedLeaf>,
694    external_event_handler: ExternalEventHandler,
695    event_consumer: Arc<C>,
696    decide_tx: watch::Sender<DecideSignal>,
697) where
698    P: SequencerPersistence,
699    C: PersistenceEventConsumer + 'static,
700{
701    while let Some(event) = events.next().await {
702        tracing::debug!(node_id, ?event, "consensus event");
703
704        match &event {
705            CoordinatorEvent::ExternalMessageReceived { data, .. } => {
706                if let Err(err) = external_event_handler.handle_event(data).await {
707                    tracing::warn!("Failed to handle external message: {:?}", err);
708                }
709            },
710            CoordinatorEvent::BlockPayloadReconstructed { .. } => {
711                // Forward straight to the consumer: reconstructed payloads might not yet
712                // have been stored by consensus storage,
713                // Query service verifies the block against a decided leaf before storing it.
714                if let Err(err) = event_consumer.handle_event(&event).await {
715                    tracing::warn!("failed to handle reconstructed payload: {err:#}");
716                }
717            },
718            _ => {},
719        }
720
721        if let Some(leaf) = DecidedLeaf::from_event(&event) {
722            let _ = signer_tx.send(leaf);
723        }
724
725        // Critical path: only persist the decided leaves, then signal the background processor.
726        // Signalling after the persist means it never reads ahead of committed state.
727        if let Some(signal) = persistence
728            .persist_event(&event, event_consumer.as_ref())
729            .await
730        {
731            // Keep the max view: a gap-fill decide signals an *older* view
732            // and must not hide a newer, unconsumed tip signal.
733            decide_tx.send_modify(|current| match current {
734                Some(view) if *view > signal => {},
735                _ => *current = Some(signal),
736            });
737        }
738    }
739}
740
741/// Decides the signer may fall behind by before it drops the oldest ones.
742const SIGNER_QUEUE_CAPACITY: usize = 128;
743
744/// Signs and relays the newest leaf of every decide, in order, off the event loop, because a
745/// relay post can take as long as its HTTP timeout. The relay needs a threshold of signatures on
746/// one height, so every node must sign the same heights. Signing every decide keeps that while
747/// posts fall behind, and dropping only the oldest once the queue is full keeps it as well.
748async fn sign_decided_leaves<P>(
749    consensus_handle: Arc<ConsensusHandle<SeqTypes, Arc<P>>>,
750    mut state_signer: StateSigner<SequencerApiVersion>,
751    mut leaves: broadcast::Receiver<DecidedLeaf>,
752) where
753    P: SequencerPersistence,
754{
755    loop {
756        match leaves.recv().await {
757            Ok(leaf) => {
758                state_signer
759                    .handle_decide(&leaf, consensus_handle.as_ref())
760                    .await;
761            },
762            Err(broadcast::error::RecvError::Lagged(skipped)) => {
763                tracing::warn!(skipped, "state signer fell behind, dropped oldest decides");
764            },
765            Err(broadcast::error::RecvError::Closed) => break,
766        }
767    }
768}
769
770const PROCESS_RETRY_INTERVAL: Duration = Duration::from_secs(30);
771
772/// Turns persisted decided leaves into query-service decide events and GCs processed data.
773/// Decoupled from [`handle_events`] so slow ingestion/GC can't stall (or drop) consensus events;
774/// cursor-driven, so it can lag without losing data.
775#[tracing::instrument(skip_all)]
776async fn process_decided_events_task<P, C>(
777    persistence: Arc<P>,
778    consumer: Arc<C>,
779    mut decide_rx: watch::Receiver<DecideSignal>,
780    anchor_view: Option<ViewNumber>,
781    metrics: DecideProcessorMetrics,
782) where
783    P: SequencerPersistence,
784    C: PersistenceEventConsumer + 'static,
785{
786    // Highest view confirmed processed, for the backlog gauge. Floored at the anchor view; the
787    // cursor reported below raises it.
788    let mut last_processed = anchor_view.map(|v| v.u64()).unwrap_or(0);
789
790    // Process leaves persisted before a previous shutdown but not yet handled.
791    if let Some(view) = anchor_view {
792        match persistence
793            .process_decided_events(view, consumer.as_ref())
794            .await
795        {
796            Ok(processed) => {
797                if let Some(v) = processed {
798                    last_processed = last_processed.max(v.u64());
799                }
800            },
801            Err(err) => tracing::warn!(
802                "failed to process decided leaves on startup, chain may not be up to date: {err:#}"
803            ),
804        }
805    }
806
807    // Reused on a timeout to re-attempt the most recent decide when no new one has arrived.
808    let mut latest: DecideSignal = None;
809
810    loop {
811        // Wait for the next decide, retrying the most recent one if none arrives within the timeout.
812        match tokio::time::timeout(PROCESS_RETRY_INTERVAL, decide_rx.changed()).await {
813            Ok(Ok(())) => latest = *decide_rx.borrow_and_update(),
814            Ok(Err(_)) => {
815                tracing::info!("decide signal channel closed, stopping decide processor");
816                return;
817            },
818            Err(_) => {}, // Timed out; fall through to retry `latest`.
819        }
820
821        let Some(view) = latest else {
822            continue;
823        };
824        let decided = view.u64();
825        metrics.last_decided.set(decided as usize);
826        metrics
827            .backlog
828            .set(decided.saturating_sub(last_processed) as usize);
829
830        let start = Instant::now();
831        let result = persistence
832            .process_decided_events(view, consumer.as_ref())
833            .await;
834        metrics.duration.add_point(start.elapsed().as_secs_f64());
835
836        match result {
837            Ok(processed) => {
838                // Advance from the real cursor, not `decided`: if ingestion/GC lagged, `processed`
839                // stays behind and the backlog gauge reflects it.
840                if let Some(v) = processed {
841                    last_processed = last_processed.max(v.u64());
842                }
843                // reset latest if we have processed all the decided leaves
844                if let Some(view) = latest
845                    && last_processed >= view.u64()
846                {
847                    latest = None;
848                }
849                metrics.last_processed.set(last_processed as usize);
850                metrics
851                    .backlog
852                    .set(decided.saturating_sub(last_processed) as usize);
853            },
854            Err(err) => {
855                // Cursor not advanced, so this range is retried next iteration; no data is lost.
856                metrics.failures.add(1);
857                tracing::warn!(?view, "deferred decide processing failed: {err:#}");
858            },
859        }
860    }
861}
862
863#[derive(Debug, Default, Clone)]
864#[allow(clippy::type_complexity)]
865pub struct TaskList(Arc<Mutex<Vec<(String, JoinHandle<()>)>>>);
866
867macro_rules! spawn_with_log_level {
868    ($this:expr, $lvl:expr, $name:expr, $task: expr) => {
869        let name = $name.to_string();
870        let task = {
871            let name = name.clone();
872            let span = tracing::span!($lvl, "background task", name);
873            spawn(
874                async move {
875                    tracing::event!($lvl, "spawning background task");
876                    let res = $task.await;
877                    tracing::event!($lvl, ?res, "background task exited");
878                }
879                .instrument(span),
880            )
881        };
882        $this.0.lock().push((name, task));
883    };
884}
885
886impl TaskList {
887    /// Spawn a background task attached to this [`TaskList`].
888    ///
889    /// When this [`TaskList`] is dropped or [`shut_down`](Self::shut_down), background tasks will
890    /// be cancelled in the reverse order that they were spawned.
891    pub fn spawn(&mut self, name: impl Display, task: impl Future<Output: Debug> + Send + 'static) {
892        spawn_with_log_level!(self, Level::INFO, name, task);
893    }
894
895    /// Spawn a short-lived background task attached to this [`TaskList`].
896    ///
897    /// When this [`TaskList`] is dropped or [`shut_down`](Self::shut_down), background tasks will
898    /// be cancelled in the reverse order that they were spawned.
899    ///
900    /// The only difference between a short-lived background task and a [long-lived](Self::spawn)
901    /// one is how urgently logging related to the task is treated.
902    pub fn spawn_short_lived(
903        &mut self,
904        name: impl Display,
905        task: impl Future<Output: Debug> + Send + 'static,
906    ) {
907        spawn_with_log_level!(self, Level::DEBUG, name, task);
908    }
909
910    /// Stop all background tasks.
911    pub fn shut_down(&self) {
912        let tasks: Vec<(String, JoinHandle<()>)> = self.0.lock().drain(..).collect();
913        for (name, task) in tasks.into_iter().rev() {
914            tracing::info!(name, "cancelling background task");
915            task.abort();
916        }
917    }
918
919    /// Wait for all background tasks to complete.
920    pub async fn join(&mut self) {
921        let tasks: Vec<(String, JoinHandle<()>)> = self.0.lock().drain(..).collect();
922        join_all(tasks.into_iter().map(|(_, task)| task)).await;
923    }
924
925    pub fn extend(&mut self, tasks: TaskList) {
926        self.0.lock().extend(
927            tasks
928                .0
929                .lock()
930                .drain(..)
931                .collect::<Vec<(String, JoinHandle<()>)>>(),
932        );
933    }
934}
935
936impl Drop for TaskList {
937    fn drop(&mut self) {
938        self.shut_down()
939    }
940}