Skip to main content

commonware_glue/dkg/orchestrator/
actor.rs

1//! Consensus engine orchestration for threshold reshare epoch transitions.
2
3use crate::dkg::{
4    ReshareBlock,
5    fence::Gate,
6    network::{Directory, Manager},
7    orchestrator::{Mailbox, mailbox::Message},
8    state_sync::{self, Plan as StateSyncPlan},
9    types::{EpochInfo, Payload},
10};
11use commonware_actor::mailbox;
12use commonware_consensus::{
13    CertifiableAutomaton, Heightable, Relay,
14    marshal::core::{Mailbox as MarshalMailbox, Variant as MarshalVariant},
15    simplex::{
16        self, Floor, ForwardPolicy, Plan, SkipPolicy, elector::Config as Elector, scheme,
17        types::Context,
18    },
19    types::{Epoch, Epocher, FixedEpocher, Height, ViewDelta},
20};
21use commonware_cryptography::{
22    Digest, PublicKey, Signer,
23    bls12381::primitives::variant::Variant as BlsVariant,
24    certificate::{Provider, Verifier},
25};
26use commonware_macros::{select, select_loop};
27use commonware_p2p::{
28    Blocker, Channel, Message as P2pMessage, Receiver, Sender,
29    utils::mux::{Builder, MuxHandle, Muxer},
30};
31use commonware_parallel::Strategy;
32use commonware_runtime::{
33    BufferPooler, Clock, ContextCell, Handle, Metrics, Network, Spawner, Storage,
34    buffer::paged::CacheRef,
35    spawn_cell,
36    telemetry::metrics::{Gauge, GaugeExt, MetricsExt as _},
37};
38use commonware_utils::{Acknowledgement, acknowledgement::Exact, channel::mpsc, vec::NonEmptyVec};
39use rand_core::CryptoRng;
40use std::{
41    marker::PhantomData,
42    num::{NonZeroU16, NonZeroU64, NonZeroUsize},
43    sync::Arc,
44    time::Duration,
45};
46use tracing::{debug, info, warn};
47
48struct Channels<C, S, R>
49where
50    C: Verifier,
51    S: Sender<PublicKey = C::PublicKey>,
52    R: Receiver<PublicKey = C::PublicKey>,
53{
54    vote: MuxHandle<S, R>,
55    vote_backup: mpsc::Receiver<(Channel, P2pMessage<C::PublicKey>)>,
56    certificate: MuxHandle<S, R>,
57    certificate_backup: mpsc::Receiver<(Channel, P2pMessage<C::PublicKey>)>,
58    resolver: MuxHandle<S, R>,
59}
60
61struct ActiveEpoch {
62    epoch: Epoch,
63    handle: Handle<()>,
64}
65
66impl Drop for ActiveEpoch {
67    fn drop(&mut self) {
68        self.handle.abort();
69    }
70}
71
72enum EnterEpochError<E> {
73    GateClosed,
74    PeerSet(E),
75    MuxClosed,
76    Stopped,
77}
78
79struct ResolvedStart<S, D, V, P, Dir>
80where
81    S: scheme::Scheme<D, PublicKey = P>,
82    D: Digest,
83    V: BlsVariant,
84    P: PublicKey,
85    Dir: Directory<P>,
86{
87    epoch: Epoch,
88    floor: Floor<S, D>,
89    info: EpochInfo<V, P, Dir>,
90}
91
92/// Simplex configuration applied to each epoch engine.
93#[derive(Clone)]
94pub struct SimplexConfig<L> {
95    /// Leader election configuration.
96    pub elector: L,
97
98    /// Maximum number of messages to buffer on channels inside each consensus engine.
99    pub mailbox_size: NonZeroUsize,
100
101    /// Number of bytes to buffer when replaying consensus state during startup.
102    pub replay_buffer: NonZeroUsize,
103
104    /// Number of bytes to buffer when writing consensus journal blobs.
105    pub write_buffer: NonZeroUsize,
106
107    /// Page size used by the consensus journal page cache.
108    pub page_cache_page_size: NonZeroU16,
109
110    /// Number of pages retained by the consensus journal page cache.
111    pub page_cache_pages: NonZeroUsize,
112
113    /// Time to wait for a leader proposal in a view.
114    pub leader_timeout: Duration,
115
116    /// Time to wait for certification progress before attempting to skip a view.
117    pub certification_timeout: Duration,
118
119    /// Time to wait before retrying a nullify broadcast while stuck in a view.
120    pub timeout_retry: Duration,
121
122    /// Time to wait for a peer to respond to a resolver request.
123    pub fetch_timeout: Duration,
124
125    /// Number of views behind the finalized tip to retain validator activity.
126    pub view_retention: ViewDelta,
127
128    /// Policy governing whether `nullify(v)` may be broadcast before the normal round deadlines.
129    pub skip: SkipPolicy,
130
131    /// Track individual votes after certification.
132    ///
133    /// By default, full vote evidence is released when the corresponding certificate
134    /// is constructed or received, making later conflict reporting and peer blocking
135    /// best effort. Enabling this retains each recorded vote until its round is
136    /// pruned, increasing memory usage.
137    pub track_historical_votes: bool,
138
139    /// Policy for proactively forwarding certified blocks.
140    pub forward: ForwardPolicy,
141}
142
143/// Configuration for the [`Actor`].
144pub struct Config<B, M, P, MV, DV, A, L, T>
145where
146    P: Provider<Scope = Epoch>,
147    P::Scheme: scheme::Scheme<MV::Commitment>,
148    MV: MarshalVariant,
149    MV::ApplicationBlock: ReshareBlock,
150    <MV::ApplicationBlock as ReshareBlock>::Signer:
151        Signer<PublicKey = <P::Scheme as Verifier>::PublicKey>,
152    DV: BlsVariant,
153{
154    /// Network blocker shared with each epoch consensus engine.
155    pub oracle: B,
156
157    /// P2P manager used to track the active consensus peer set.
158    pub manager: M,
159
160    /// Provider of epoch-scoped consensus signing schemes.
161    pub provider: P,
162
163    /// Marshal mailbox used to report consensus output and read finalized blocks.
164    pub marshal: MarshalMailbox<P::Scheme, MV>,
165
166    /// Application automaton and relay used by each epoch consensus engine.
167    pub application: A,
168
169    /// Strategy for parallel verification and signing work.
170    pub strategy: T,
171
172    /// Simplex settings applied to every epoch engine.
173    pub simplex: SimplexConfig<L>,
174
175    /// Gate for waiting for the signature scheme to be configured prior to
176    /// entering an epoch.
177    pub gate: Gate,
178
179    /// Shared DKG state-sync startup recovery plan.
180    pub state_sync: StateSyncPlan<
181        P::Scheme,
182        MV::Commitment,
183        DV,
184        <MV::ApplicationBlock as ReshareBlock>::Directory,
185    >,
186
187    /// Number of blocks in each epoch.
188    pub blocks_per_epoch: NonZeroU64,
189
190    /// Maximum number of messages to buffer in each network muxer.
191    pub muxer_size: usize,
192
193    /// Maximum number of finalized-block reports to buffer.
194    pub mailbox_size: NonZeroUsize,
195
196    /// Partition prefix used for per-epoch consensus persistence.
197    pub partition_prefix: String,
198}
199
200/// Consensus engine orchestrator.
201pub struct Actor<E, B, M, P, MV, DV, C, A, L, T, ACK = Exact>
202where
203    E: BufferPooler + Spawner + Metrics + CryptoRng + Clock + Storage + Network,
204    B: Blocker<PublicKey = <P::Scheme as Verifier>::PublicKey>,
205    M: Manager<
206            PublicKey = <P::Scheme as Verifier>::PublicKey,
207            Directory = <MV::ApplicationBlock as ReshareBlock>::Directory,
208        >,
209    P: Provider<Scope = Epoch>,
210    P::Scheme: scheme::Scheme<MV::Commitment>,
211    MV: MarshalVariant,
212    MV::ApplicationBlock: ReshareBlock<Variant = DV, Signer = C>,
213    DV: BlsVariant,
214    C: Signer<PublicKey = <P::Scheme as Verifier>::PublicKey>,
215    A: CertifiableAutomaton<
216            Context = Context<MV::Commitment, <P::Scheme as Verifier>::PublicKey>,
217            Digest = MV::Commitment,
218        > + Relay<
219            Digest = MV::Commitment,
220            PublicKey = <P::Scheme as Verifier>::PublicKey,
221            Plan = Plan<<P::Scheme as Verifier>::PublicKey>,
222        >,
223    L: Elector<P::Scheme>,
224    T: Strategy,
225    ACK: Acknowledgement,
226{
227    context: ContextCell<E>,
228    mailbox: mailbox::Receiver<Message<MV::ApplicationBlock, ACK>>,
229    oracle: B,
230    manager: M,
231    provider: P,
232    marshal: MarshalMailbox<P::Scheme, MV>,
233    application: A,
234    strategy: T,
235    simplex: SimplexConfig<L>,
236    gate: Gate,
237    state_sync: StateSyncPlan<
238        P::Scheme,
239        MV::Commitment,
240        DV,
241        <MV::ApplicationBlock as ReshareBlock>::Directory,
242    >,
243    blocks_per_epoch: NonZeroU64,
244    muxer_size: usize,
245    partition_prefix: String,
246    page_cache_ref: CacheRef,
247    latest_epoch: Gauge,
248    _payload: PhantomData<(DV, C)>,
249}
250
251impl<E, B, M, P, MV, DV, C, A, L, T, ACK> Actor<E, B, M, P, MV, DV, C, A, L, T, ACK>
252where
253    E: BufferPooler + Spawner + Metrics + CryptoRng + Clock + Storage + Network,
254    B: Blocker<PublicKey = <P::Scheme as Verifier>::PublicKey>,
255    M: Manager<
256            PublicKey = <P::Scheme as Verifier>::PublicKey,
257            Directory = <MV::ApplicationBlock as ReshareBlock>::Directory,
258        >,
259    P: Provider<Scope = Epoch>,
260    P::Scheme: scheme::Scheme<MV::Commitment>,
261    MV: MarshalVariant,
262    MV::ApplicationBlock: ReshareBlock<Variant = DV, Signer = C>,
263    DV: BlsVariant,
264    C: Signer<PublicKey = <P::Scheme as Verifier>::PublicKey>,
265    A: CertifiableAutomaton<
266            Context = Context<MV::Commitment, <P::Scheme as Verifier>::PublicKey>,
267            Digest = MV::Commitment,
268        > + Relay<
269            Digest = MV::Commitment,
270            PublicKey = <P::Scheme as Verifier>::PublicKey,
271            Plan = Plan<<P::Scheme as Verifier>::PublicKey>,
272        >,
273    L: Elector<P::Scheme>,
274    T: Strategy,
275    ACK: Acknowledgement,
276{
277    /// Build an orchestrator and the mailbox that receives finalized blocks.
278    ///
279    /// The returned [`Mailbox`] should be installed as a marshal reporter. The
280    /// actor uses those finalized-block reports to advance epochs after it is
281    /// spawned with [`Actor::start`].
282    pub fn new(
283        context: E,
284        config: Config<B, M, P, MV, DV, A, L, T>,
285    ) -> (Self, Mailbox<MV::ApplicationBlock, ACK>) {
286        let (sender, mailbox) = mailbox::new(context.child("mailbox"), config.mailbox_size);
287        let page_cache_ref = CacheRef::from_pooler(
288            &context,
289            config.simplex.page_cache_page_size,
290            config.simplex.page_cache_pages,
291        );
292        let latest_epoch = context.gauge("latest_epoch", "current epoch");
293
294        (
295            Self {
296                context: ContextCell::new(context),
297                mailbox,
298                oracle: config.oracle,
299                manager: config.manager,
300                provider: config.provider,
301                marshal: config.marshal,
302                application: config.application,
303                strategy: config.strategy,
304                simplex: config.simplex,
305                gate: config.gate,
306                state_sync: config.state_sync,
307                blocks_per_epoch: config.blocks_per_epoch,
308                muxer_size: config.muxer_size,
309                partition_prefix: config.partition_prefix,
310                page_cache_ref,
311                latest_epoch,
312                _payload: PhantomData,
313            },
314            Mailbox::new(sender),
315        )
316    }
317
318    /// Spawn the orchestrator with the consensus network channels.
319    ///
320    /// Vote, certificate, and resolver channels are multiplexed by epoch
321    /// inside the actor.
322    pub fn start<S, R>(
323        mut self,
324        votes: (S, R),
325        certificates: (S, R),
326        resolver: (S, R),
327    ) -> Handle<()>
328    where
329        S: Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
330        R: Receiver<PublicKey = <P::Scheme as Verifier>::PublicKey>,
331    {
332        spawn_cell!(self.context, self.run(votes, certificates, resolver,))
333    }
334
335    /// Run the actor event loop.
336    ///
337    /// The loop owns one active Simplex engine at a time. It listens for
338    /// finalized boundary blocks from marshal and for backup vote and
339    /// certificate traffic from future epochs, which is used only to ask
340    /// marshal for the missing boundary finalization.
341    async fn run<S, R>(
342        mut self,
343        (vote_sender, vote_receiver): (S, R),
344        (certificate_sender, certificate_receiver): (S, R),
345        (resolver_sender, resolver_receiver): (S, R),
346    ) where
347        S: Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
348        R: Receiver<PublicKey = <P::Scheme as Verifier>::PublicKey>,
349    {
350        let mut channels = self.create_channels(
351            (vote_sender, vote_receiver),
352            (certificate_sender, certificate_receiver),
353            (resolver_sender, resolver_receiver),
354        );
355        let epocher = FixedEpocher::new(self.blocks_per_epoch);
356        let Some(start) = self.resolve_start(&epocher).await else {
357            debug!("context shutdown while resolving startup epoch");
358            return;
359        };
360        let mut active = match self
361            .enter_epoch(start.epoch, start.floor, &start.info, &mut channels)
362            .await
363        {
364            Ok(active) => active,
365            Err(EnterEpochError::GateClosed) => {
366                debug!(
367                    epoch = start.epoch.get(),
368                    "epoch gate closed before startup"
369                );
370                return;
371            }
372            Err(EnterEpochError::PeerSet(error)) => {
373                warn!(epoch = %start.epoch, %error, "failed to activate startup peer set");
374                return;
375            }
376            Err(EnterEpochError::MuxClosed) => {
377                debug!(
378                    epoch = start.epoch.get(),
379                    "consensus mux closed before startup epoch"
380                );
381                return;
382            }
383            Err(EnterEpochError::Stopped) => {
384                debug!("context shutdown before startup epoch");
385                return;
386            }
387        };
388
389        select_loop! {
390            self.context,
391            on_stopped => {
392                debug!("context shutdown, stopping orchestrator");
393            },
394            Some((their_epoch, (from, _))) = channels.vote_backup.recv() else {
395                debug!("vote mux backup channel closed, shutting down orchestrator");
396                break;
397            } => {
398                self.handle_backup(&epocher, active.epoch, their_epoch, from);
399            },
400            Some((their_epoch, (from, _))) = channels.certificate_backup.recv() else {
401                debug!("certificate mux backup channel closed, shutting down orchestrator");
402                break;
403            } => {
404                self.handle_backup(&epocher, active.epoch, their_epoch, from);
405            },
406            result = &mut active.handle => match result {
407                Ok(()) => {
408                    debug!(epoch = active.epoch.get(), "simplex engine stopped, shutting down orchestrator");
409                    break;
410                }
411                Err(error) => {
412                    panic!("simplex engine for epoch {} stopped unexpectedly: {error}", active.epoch);
413                }
414            },
415            Some(message) = self.mailbox.recv() else {
416                debug!("mailbox closed, shutting down orchestrator");
417                break;
418            } => match message {
419                Message::Finalized {
420                    block,
421                    acknowledgement,
422                } => {
423                    let keep_running = self
424                        .handle_finalized(
425                            &epocher,
426                            &mut active,
427                            block,
428                            acknowledgement,
429                            &mut channels,
430                        )
431                        .await;
432                    if !keep_running {
433                        break;
434                    }
435                }
436            },
437        }
438    }
439
440    /// Resolve the first epoch this process should run.
441    ///
442    /// Normal startup resolves from marshal's local boundary blocks. State-sync
443    /// startup and recovery are exceptions: the node may know a recent public
444    /// boundary from `dkg::probe` without having the previous boundary block in
445    /// local marshal storage.
446    ///
447    /// Returns `None` when startup data cannot be fetched from marshal, which
448    /// requires the orchestrator to shut down.
449    async fn resolve_start(
450        &mut self,
451        epocher: &FixedEpocher,
452    ) -> Option<
453        ResolvedStart<
454            P::Scheme,
455            MV::Commitment,
456            DV,
457            <P::Scheme as Verifier>::PublicKey,
458            <MV::ApplicationBlock as ReshareBlock>::Directory,
459        >,
460    > {
461        let recovered_epoch = state_sync::recovered_epoch(&self.marshal, epocher).await;
462        if let Some(state_sync) = self
463            .state_sync
464            .resolve(
465                self.context.as_present().child("state_sync"),
466                recovered_epoch,
467            )
468            .await
469        {
470            return Some(ResolvedStart {
471                epoch: state_sync.info.epoch,
472                floor: Floor::Finalized(state_sync.floor),
473                info: state_sync.info,
474            });
475        }
476
477        self.resolve_boundary(recovered_epoch.unwrap_or_else(Epoch::zero), epocher)
478            .await
479    }
480
481    /// Resolve a locally recovered epoch from marshal's finalized boundary block.
482    ///
483    /// Ordinary restarts should not re-enter the configured bootstrap epoch if
484    /// marshal has already delivered finalized blocks to the application. The
485    /// processed height names the next block marshal will deliver; from that
486    /// height we derive the active epoch, then read the boundary block that
487    /// carried that epoch's public [`EpochInfo`]. That boundary block supplies
488    /// both the Simplex floor commitment and the peer set to track for the
489    /// recovered epoch.
490    ///
491    /// This is intentionally not used for state-sync startup: during one-time
492    /// state sync, marshal is anchored at the probe-sampled floor while the
493    /// previous epoch boundary block is not locally available yet. In that
494    /// startup path, the probe artifact is the trusted source of boundary
495    /// epoch info.
496    ///
497    /// Returns `None` when the boundary block cannot be fetched from marshal,
498    /// which requires the orchestrator to shut down.
499    async fn resolve_boundary(
500        &mut self,
501        epoch: Epoch,
502        epocher: &FixedEpocher,
503    ) -> Option<
504        ResolvedStart<
505            P::Scheme,
506            MV::Commitment,
507            DV,
508            <P::Scheme as Verifier>::PublicKey,
509            <MV::ApplicationBlock as ReshareBlock>::Directory,
510        >,
511    > {
512        let height = epoch
513            .previous()
514            .and_then(|epoch| epocher.last(epoch))
515            .unwrap_or_else(Height::zero);
516        let Some(boundary) = self.marshal.get_block(height).await else {
517            debug!(%height, "boundary block unavailable, shutting down orchestrator");
518            return None;
519        };
520        let commitment = MV::commitment(&boundary);
521        let block = MV::into_inner(boundary);
522        let Some(Payload::EpochInfo(info)) = block.payload() else {
523            panic!("boundary block {height} missing epoch info");
524        };
525        if info.epoch != epoch {
526            panic!(
527                "boundary block {height} carries epoch info for {}, expected {epoch}",
528                info.epoch
529            );
530        }
531
532        Some(ResolvedStart {
533            epoch,
534            floor: Floor::Genesis(commitment),
535            info,
536        })
537    }
538
539    /// Start the consensus channel muxers and return handles used to open
540    /// epoch-specific subchannels.
541    ///
542    /// The vote mux includes a backup receiver so the orchestrator can detect
543    /// messages for epochs it has not registered locally.
544    fn create_channels<S, R>(
545        &self,
546        (vote_sender, vote_receiver): (S, R),
547        (certificate_sender, certificate_receiver): (S, R),
548        (resolver_sender, resolver_receiver): (S, R),
549    ) -> Channels<P::Scheme, S, R>
550    where
551        S: Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
552        R: Receiver<PublicKey = <P::Scheme as Verifier>::PublicKey>,
553    {
554        let (mux, vote, vote_backup) = Muxer::builder(
555            self.context.child("vote_mux"),
556            vote_sender,
557            vote_receiver,
558            self.muxer_size,
559        )
560        .with_backup()
561        .build();
562        mux.start();
563
564        let (mux, certificate, certificate_backup) = Muxer::builder(
565            self.context.child("certificate_mux"),
566            certificate_sender,
567            certificate_receiver,
568            self.muxer_size,
569        )
570        .with_backup()
571        .build();
572        mux.start();
573
574        let (mux, resolver) = Muxer::new(
575            self.context.child("resolver_mux"),
576            resolver_sender,
577            resolver_receiver,
578            self.muxer_size,
579        );
580        mux.start();
581
582        Channels {
583            vote,
584            vote_backup,
585            certificate,
586            certificate_backup,
587            resolver,
588        }
589    }
590
591    /// Handle traffic for an epoch whose vote or certificate subchannel is not
592    /// registered.
593    ///
594    /// Messages from past or current epochs are ignored. A future-epoch
595    /// message is evidence that peers have crossed an epoch boundary locally,
596    /// so the actor hints marshal to fetch the current epoch's boundary
597    /// finalization from the sender.
598    fn handle_backup(
599        &self,
600        epocher: &FixedEpocher,
601        our_epoch: Epoch,
602        their_epoch: u64,
603        from: <P::Scheme as Verifier>::PublicKey,
604    ) {
605        let their_epoch = Epoch::new(their_epoch);
606        if their_epoch <= our_epoch {
607            debug!(%their_epoch, %our_epoch, ?from, "received message from past epoch");
608            return;
609        }
610
611        let boundary_height = epocher
612            .last(our_epoch)
613            .expect("our epoch should be covered by epoch strategy");
614        debug!(
615            ?from,
616            %their_epoch,
617            %our_epoch,
618            %boundary_height,
619            "received backup message from future epoch, ensuring boundary finalization"
620        );
621        self.marshal
622            .hint_finalized(boundary_height, NonEmptyVec::new(from));
623    }
624
625    /// Handle one finalized block delivered by marshal.
626    ///
627    /// Non-boundary blocks are acknowledged immediately. A boundary block must
628    /// carry the next epoch's public [`Payload::EpochInfo`]; once it does, the
629    /// actor stops the current Simplex engine and enters the next epoch using
630    /// that public peer set.
631    async fn handle_finalized<S, R>(
632        &mut self,
633        epocher: &FixedEpocher,
634        active: &mut ActiveEpoch,
635        block: Arc<MV::ApplicationBlock>,
636        acknowledgement: ACK,
637        channels: &mut Channels<P::Scheme, S, R>,
638    ) -> bool
639    where
640        S: Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
641        R: Receiver<PublicKey = <P::Scheme as Verifier>::PublicKey>,
642    {
643        let height = block.height();
644        let current = active.epoch;
645        if epocher.last(current) != Some(height) {
646            acknowledgement.acknowledge();
647            return true;
648        }
649
650        let next_epoch = current.next();
651        let Some(Payload::EpochInfo(info)) = block.payload() else {
652            panic!("boundary block of epoch {current} missing EpochInfo");
653        };
654        if info.epoch != next_epoch {
655            panic!(
656                "boundary block of epoch {current} carries epoch info for wrong epoch (got: {}, expected: {next_epoch})",
657                info.epoch
658            );
659        }
660
661        let Some(boundary) = self.marshal.get_block(height).await else {
662            debug!(%height, "boundary block unavailable, shutting down orchestrator");
663            return false;
664        };
665        let floor = Floor::Genesis(MV::commitment(&boundary));
666
667        let next = self.enter_epoch(next_epoch, floor, &info, channels).await;
668        let next = match next {
669            Ok(next) => next,
670            Err(EnterEpochError::GateClosed) => {
671                debug!(%next_epoch, "epoch gate closed before boundary transition");
672                return false;
673            }
674            Err(EnterEpochError::PeerSet(error)) => {
675                warn!(%next_epoch, %error, "failed to activate boundary peer set");
676                return false;
677            }
678            Err(EnterEpochError::MuxClosed) => {
679                debug!(%next_epoch, "consensus mux closed before boundary transition");
680                return false;
681            }
682            Err(EnterEpochError::Stopped) => {
683                debug!(%next_epoch, "context shutdown while waiting to enter epoch");
684                return false;
685            }
686        };
687
688        *active = next;
689        acknowledgement.acknowledge();
690        true
691    }
692
693    /// Enter an epoch and return the active engine handle.
694    ///
695    /// This is the only path that tracks consensus peers, opens epoch-scoped
696    /// mux subchannels, constructs the Simplex engine, and updates the current
697    /// epoch metric. Callers must abort the previous [`ActiveEpoch`] before
698    /// replacing it with the returned value.
699    async fn enter_epoch<S, R>(
700        &mut self,
701        epoch: Epoch,
702        floor: Floor<P::Scheme, MV::Commitment>,
703        info: &EpochInfo<
704            DV,
705            <P::Scheme as Verifier>::PublicKey,
706            <MV::ApplicationBlock as ReshareBlock>::Directory,
707        >,
708        channels: &mut Channels<P::Scheme, S, R>,
709    ) -> Result<ActiveEpoch, EnterEpochError<M::Error>>
710    where
711        S: Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
712        R: Receiver<PublicKey = <P::Scheme as Verifier>::PublicKey>,
713    {
714        // Shutdown is polled first so a stop signal wins over an
715        // already-marked gate.
716        let mut shutdown = self.context.stopped();
717        select! {
718            _ = &mut shutdown => {
719                return Err(EnterEpochError::Stopped);
720            },
721            result = self.gate.wait(epoch) => {
722                if result.is_err() {
723                    return Err(EnterEpochError::GateClosed);
724                }
725            },
726        };
727        drop(shutdown);
728
729        self.manager
730            .track(epoch, info.participants().tracked_peers(), &info.directory)
731            .map_err(EnterEpochError::PeerSet)?;
732        let scheme = self
733            .provider
734            .scheme(epoch)
735            .unwrap_or_else(|| panic!("missing consensus scheme for epoch {epoch}"));
736        let context = self
737            .context
738            .child("consensus_engine")
739            .with_attribute("epoch", epoch);
740        let engine = simplex::Engine::new(
741            context,
742            simplex::Config {
743                scheme: scheme.as_ref().clone(),
744                elector: self.simplex.elector.clone(),
745                blocker: self.oracle.clone(),
746                automaton: self.application.clone(),
747                relay: self.application.clone(),
748                reporter: self.marshal.clone(),
749                strategy: self.strategy.clone(),
750                partition: format!("{}_consensus_{epoch}", self.partition_prefix),
751                mailbox_size: self.simplex.mailbox_size,
752                epoch,
753                floor,
754                replay_buffer: self.simplex.replay_buffer,
755                write_buffer: self.simplex.write_buffer,
756                page_cache: self.page_cache_ref.clone(),
757                leader_timeout: self.simplex.leader_timeout,
758                certification_timeout: self.simplex.certification_timeout,
759                timeout_retry: self.simplex.timeout_retry,
760                fetch_timeout: self.simplex.fetch_timeout,
761                view_retention: self.simplex.view_retention,
762                skip: self.simplex.skip,
763                forward: self.simplex.forward,
764                track_historical_votes: self.simplex.track_historical_votes,
765            },
766        );
767
768        // Each epoch is registered exactly once, so a registration failure
769        // means the muxer has stopped: the vote, certificate, and resolver
770        // muxers all exit with this context, which is a clean-stop condition.
771        let Ok(vote) = channels.vote.register(epoch.get()).await else {
772            return Err(EnterEpochError::MuxClosed);
773        };
774        let Ok(certificate) = channels.certificate.register(epoch.get()).await else {
775            return Err(EnterEpochError::MuxClosed);
776        };
777        let Ok(resolver) = channels.resolver.register(epoch.get()).await else {
778            return Err(EnterEpochError::MuxClosed);
779        };
780        let handle = engine.start(vote, certificate, resolver);
781        let _ = self.latest_epoch.try_set(epoch.get());
782
783        info!(%epoch, "entered epoch");
784        Ok(ActiveEpoch { epoch, handle })
785    }
786}