Skip to main content

espresso_node/
consensus_handle.rs

1use std::{collections::HashMap, mem, sync::Arc};
2
3use async_broadcast::{InactiveReceiver, Sender, broadcast};
4use async_lock::RwLock as AsyncRwLock;
5use committable::Commitment;
6use futures::{
7    FutureExt, StreamExt,
8    future::BoxFuture,
9    stream::{self, BoxStream},
10};
11use hotshot::{traits::NodeImplementation, types::SystemContextHandle};
12use hotshot_new_protocol::{
13    client::ClientApi,
14    consensus::{ConsensusInput, ConsensusOutput, PreCutoverSeed},
15    coordinator::{
16        Coordinator,
17        error::{CoordinatorError, Severity},
18    },
19    cutover::{extract_pre_cutover_seed, forward_legacy_high_qc, forward_legacy_timeout_votes},
20    state::UpdateLeaf,
21    storage::NewProtocolStorage,
22};
23use hotshot_types::{
24    data::{BlockNumber, EpochNumber, Leaf2, QuorumProposalWrapper, VidDisperseShare, ViewNumber},
25    epoch_membership::EpochMembershipCoordinator,
26    event::{Event, EventType, LeafInfo},
27    message::{Proposal as SignedProposal, UpgradeLock, convert_proposal},
28    new_protocol::CoordinatorEvent,
29    traits::{
30        ValidatedState,
31        block_contents::BlockHeader,
32        metrics::{Gauge, Metrics},
33        node_implementation::NodeType,
34        signature_key::SignatureKey,
35    },
36    utils::{StateAndDelta, epoch_from_block_number},
37};
38use parking_lot::RwLock;
39use tokio::{select, spawn};
40use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle};
41use tracing::{error, warn};
42
43pub struct ConsensusHandle<T: NodeType, I: NodeImplementation<T>> {
44    legacy_handle: Arc<AsyncRwLock<SystemContextHandle<T, I>>>,
45    epoch_height: BlockNumber,
46    legacy_event_rx: InactiveReceiver<Event<T>>,
47    event_rx: InactiveReceiver<CoordinatorEvent<T>>,
48    upgrade_lock: UpgradeLock<T>,
49    new_proto: Arc<RwLock<NewProtocol<T, I::Storage>>>,
50    tasks: Vec<AbortOnDropHandle<()>>,
51}
52
53#[allow(clippy::large_enum_variant)]
54enum NewProtocol<T: NodeType, S> {
55    Empty,
56    Init {
57        coordinator: Coordinator<T, S>,
58        event_tx: Sender<CoordinatorEvent<T>>,
59        queue_len: Option<Arc<dyn Gauge>>,
60    },
61    Running {
62        coordinator: AbortOnDropHandle<()>,
63        client_api: ClientApi<T>,
64        shutdown: CancellationToken,
65    },
66}
67
68impl<T: NodeType, S> NewProtocol<T, S> {
69    fn take(&mut self) -> Self {
70        mem::replace(self, Self::Empty)
71    }
72}
73
74impl<T, I> ConsensusHandle<T, I>
75where
76    T: NodeType,
77    I: NodeImplementation<T>,
78    I::Storage: NewProtocolStorage<T>,
79{
80    pub async fn new(
81        ctx: Arc<AsyncRwLock<SystemContextHandle<T, I>>>,
82        coordinator: Coordinator<T, I::Storage>,
83        epoch_height: BlockNumber,
84        rx: InactiveReceiver<Event<T>>,
85        event_channel_capacity: usize,
86        metrics: &dyn Metrics,
87    ) -> Self {
88        let (mut event_tx, mut event_rx) = broadcast(event_channel_capacity);
89        event_tx.set_await_active(false);
90        event_rx.set_overflow(true);
91
92        let coordinator_event_queue_len = metrics.is_recording().then(|| {
93            metrics
94                .create_gauge("coordinator_event_queue_len".into(), None)
95                .into()
96        });
97        let external_event_queue_len = metrics.is_recording().then(|| {
98            metrics
99                .create_gauge("external_event_queue_len".into(), None)
100                .into()
101        });
102
103        let upgrade_lock = ctx.read().await.hotshot.upgrade_lock.clone();
104
105        let client_api = coordinator.client_api().clone();
106
107        let new_proto = Arc::new(RwLock::new(NewProtocol::Init {
108            coordinator,
109            event_tx,
110            queue_len: coordinator_event_queue_len,
111        }));
112
113        let tasks = vec![
114            AbortOnDropHandle::new(spawn(forward_legacy_timeout_votes(
115                rx.clone(),
116                client_api.clone(),
117                upgrade_lock.clone(),
118                external_event_queue_len,
119            ))),
120            AbortOnDropHandle::new(spawn(forward_legacy_high_qc(
121                rx.clone(),
122                client_api,
123                upgrade_lock.clone(),
124            ))),
125            AbortOnDropHandle::new(spawn(forward_legacy_epoch_changes(
126                rx.clone(),
127                new_proto.clone(),
128                epoch_height.into(),
129            ))),
130        ];
131
132        Self {
133            upgrade_lock,
134            legacy_handle: ctx,
135            epoch_height,
136            legacy_event_rx: rx,
137            event_rx: event_rx.deactivate(),
138            new_proto,
139            tasks,
140        }
141    }
142
143    pub async fn activate(&self) {
144        if matches!(*self.new_proto.read(), NewProtocol::Running { .. }) {
145            return;
146        }
147
148        let view = self.legacy_handle.read().await.cur_view().await;
149
150        if !self.upgrade_lock.new_protocol_active(view) {
151            return;
152        }
153
154        let seed = {
155            let legacy = self.legacy_handle.read().await;
156            extract_pre_cutover_seed(&legacy).await
157        };
158
159        if seed.is_none() {
160            warn!("seed extraction returned None; coordinator will not be seeded");
161        }
162
163        let mut new_proto = self.new_proto.write();
164
165        match new_proto.take() {
166            NewProtocol::Init {
167                coordinator,
168                event_tx,
169                queue_len,
170            } => {
171                let client_api = coordinator.client_api().clone();
172                let shutdown = CancellationToken::new();
173                *new_proto = NewProtocol::Running {
174                    coordinator: AbortOnDropHandle::new(spawn(run_coordinator(
175                        coordinator,
176                        event_tx,
177                        queue_len,
178                        seed,
179                        shutdown.clone(),
180                    ))),
181                    client_api,
182                    shutdown,
183                };
184            },
185            other => *new_proto = other,
186        }
187    }
188
189    pub fn legacy_consensus(&self) -> Arc<AsyncRwLock<SystemContextHandle<T, I>>> {
190        self.legacy_handle.clone()
191    }
192
193    pub fn event_stream(&self) -> BoxStream<'static, CoordinatorEvent<T>> {
194        let old_stream = self
195            .legacy_event_rx
196            .activate_cloned()
197            .map(CoordinatorEvent::LegacyEvent);
198        let new_stream = self.event_rx.activate_cloned();
199        stream::select(old_stream, new_stream).boxed()
200    }
201
202    pub async fn current_view(&self) -> ViewNumber {
203        if let Some(client_api) = self.client_api().await {
204            return client_api
205                .current_view()
206                .await
207                .expect("coordinator channel closed"); // FIXME
208        }
209        self.legacy_handle.read().await.cur_view().await
210    }
211
212    pub async fn decided_leaf(&self) -> Leaf2<T> {
213        if let Some(client_api) = self.client_api().await {
214            return client_api
215                .decided_leaf()
216                .await
217                .expect("coordinator channel closed"); // FIXME
218        }
219        self.legacy_handle.read().await.decided_leaf().await
220    }
221
222    pub async fn decided_state(&self) -> Option<Arc<T::ValidatedState>> {
223        if let Some(client_api) = self.client_api().await {
224            return match client_api.decided_state().await {
225                Ok(state) => state,
226                Err(err) => {
227                    warn!(%err, "coordinator unavailable for decided_state");
228                    None
229                },
230            };
231        }
232        Some(self.legacy_handle.read().await.decided_state().await)
233    }
234
235    pub async fn state(&self, view: ViewNumber) -> Option<Arc<T::ValidatedState>> {
236        if !self.upgrade_lock.new_protocol_active(view) {
237            return self.legacy_handle.read().await.state(view).await;
238        }
239        match self.client_api().await?.state(view).await {
240            Ok(state) => state,
241            Err(err) => {
242                warn!(%view, %err, "coordinator unavailable for state");
243                None
244            },
245        }
246    }
247
248    pub async fn state_and_delta(&self, view: ViewNumber) -> StateAndDelta<T> {
249        if !self.upgrade_lock.new_protocol_active(view) {
250            return self
251                .legacy_handle
252                .read()
253                .await
254                .hotshot
255                .consensus()
256                .read()
257                .await
258                .state_and_delta(view);
259        }
260        let Some(client_api) = self.client_api().await else {
261            return (None, None);
262        };
263        match client_api.state_and_delta(view).await {
264            Ok(state_and_delta) => state_and_delta,
265            Err(err) => {
266                warn!(%view, %err, "coordinator unavailable for state_and_delta");
267                (None, None)
268            },
269        }
270    }
271
272    pub async fn undecided_leaves(&self) -> Vec<Leaf2<T>> {
273        if let Some(client_api) = self.client_api().await {
274            return match client_api.undecided_leaves().await {
275                Ok(leaves) => leaves,
276                Err(err) => {
277                    warn!(%err, "coordinator unavailable for undecided_leaves");
278                    Vec::new()
279                },
280            };
281        }
282        self.legacy_handle
283            .read()
284            .await
285            .hotshot
286            .consensus()
287            .read()
288            .await
289            .undecided_leaves()
290    }
291
292    pub async fn current_epoch(&self) -> Option<EpochNumber> {
293        if let Some(client_api) = self.client_api().await {
294            return match client_api.current_epoch().await {
295                Ok(epoch) => epoch,
296                Err(err) => {
297                    warn!(%err, "coordinator unavailable for current_epoch");
298                    None
299                },
300            };
301        }
302        self.legacy_handle.read().await.cur_epoch().await
303    }
304
305    pub async fn epoch_height(&self) -> BlockNumber {
306        if self.is_new_proto_running() {
307            return self.epoch_height;
308        }
309        self.legacy_handle.read().await.epoch_height.into()
310    }
311
312    // TODO: implement for new protocol
313    pub async fn membership_coordinator(&self) -> EpochMembershipCoordinator<T> {
314        self.legacy_handle
315            .read()
316            .await
317            .membership_coordinator
318            .clone()
319    }
320
321    // TODO: implement for new protocol
322    pub async fn upgrade_lock(&self) -> UpgradeLock<T> {
323        self.legacy_handle.read().await.hotshot.upgrade_lock.clone()
324    }
325
326    // TODO: implement for new protocol
327    pub async fn storage(&self) -> I::Storage {
328        self.legacy_handle.read().await.storage()
329    }
330
331    pub async fn current_proposal_participation(&self) -> HashMap<T::SignatureKey, f64> {
332        if let Some(client_api) = self.client_api().await {
333            return client_api
334                .proposal_participation(None)
335                .await
336                .inspect_err(|err| {
337                    warn!(%err, "coordinator unavailable for current_proposal_participation");
338                })
339                .unwrap_or_default();
340        }
341        self.legacy_handle
342            .read()
343            .await
344            .consensus()
345            .read()
346            .await
347            .current_proposal_participation()
348    }
349
350    pub async fn proposal_participation(
351        &self,
352        epoch: EpochNumber,
353    ) -> HashMap<T::SignatureKey, f64> {
354        if let Some(client_api) = self.client_api().await {
355            match client_api.proposal_participation(Some(epoch)).await {
356                Ok(participation) if !participation.is_empty() => return participation,
357                Ok(_) => {},
358                Err(err) => {
359                    warn!(%err, "coordinator unavailable for proposal_participation");
360                },
361            }
362        }
363        self.legacy_handle
364            .read()
365            .await
366            .consensus()
367            .read()
368            .await
369            .proposal_participation(epoch)
370    }
371
372    pub async fn current_vote_participation(
373        &self,
374    ) -> HashMap<<T::SignatureKey as SignatureKey>::VerificationKeyType, f64> {
375        if let Some(client_api) = self.client_api().await {
376            return client_api
377                .vote_participation(None)
378                .await
379                .inspect_err(|err| {
380                    warn!(%err, "coordinator unavailable for current_vote_participation");
381                })
382                .unwrap_or_default();
383        }
384        self.legacy_handle
385            .read()
386            .await
387            .consensus()
388            .read()
389            .await
390            .current_vote_participation()
391    }
392
393    pub async fn vote_participation(
394        &self,
395        epoch: EpochNumber,
396    ) -> HashMap<<T::SignatureKey as SignatureKey>::VerificationKeyType, f64> {
397        if let Some(client_api) = self.client_api().await {
398            match client_api.vote_participation(Some(epoch)).await {
399                Ok(participation) if !participation.is_empty() => return participation,
400                Ok(_) => {},
401                Err(err) => {
402                    warn!(%err, "coordinator unavailable for vote_participation");
403                },
404            }
405        }
406        self.legacy_handle
407            .read()
408            .await
409            .consensus()
410            .read()
411            .await
412            .vote_participation(Some(epoch))
413    }
414
415    pub async fn request_proposal(
416        &self,
417        view: ViewNumber,
418        leaf_commitment: Commitment<Leaf2<T>>,
419    ) -> anyhow::Result<
420        BoxFuture<'static, anyhow::Result<SignedProposal<T, QuorumProposalWrapper<T>>>>,
421    > {
422        if self.upgrade_lock.new_protocol_active(view)
423            && let Some(client_api) = self.client_api().await
424        {
425            return Ok(async move {
426                client_api
427                    .request_proposal(view, leaf_commitment)
428                    .await
429                    .map(convert_proposal)
430                    .map_err(anyhow::Error::new)
431            }
432            .boxed());
433        }
434
435        let future = self
436            .legacy_handle
437            .read()
438            .await
439            .request_proposal(view, leaf_commitment)
440            .map_err(|e| anyhow::anyhow!("{e}"))?;
441
442        Ok(future.boxed())
443    }
444
445    pub async fn submit_transaction(&self, tx: T::Transaction) -> anyhow::Result<()> {
446        if let Some(client_api) = self.client_api().await {
447            return client_api
448                .submit_transaction(tx)
449                .await
450                .map_err(|e| anyhow::anyhow!("{e}"));
451        }
452        self.legacy_handle
453            .read()
454            .await
455            .submit_transaction(tx)
456            .await
457            .map_err(|e| anyhow::anyhow!("{e}"))
458    }
459
460    pub async fn update_leaf(
461        &self,
462        leaf: Leaf2<T>,
463        state: Arc<T::ValidatedState>,
464        delta: Option<Arc<<T::ValidatedState as ValidatedState<T>>::Delta>>,
465    ) -> anyhow::Result<()> {
466        let view = leaf.view_number();
467        if self.upgrade_lock.new_protocol_active(view)
468            && let Some(client_api) = self.client_api().await
469        {
470            return client_api
471                .update_leaf(UpdateLeaf {
472                    view,
473                    leaf,
474                    state,
475                    delta,
476                })
477                .await
478                .map_err(|e| anyhow::anyhow!("{e}"));
479        }
480        self.legacy_handle
481            .read()
482            .await
483            .hotshot
484            .consensus()
485            .write()
486            .await
487            .update_leaf(leaf, state, delta)
488            .map_err(|e| anyhow::anyhow!("{e}"))
489    }
490
491    pub async fn start_consensus(&self) {
492        self.activate().await;
493        if self.is_new_proto_running() {
494            if self.upgrade_lock.upgrade().base >= versions::NEW_PROTOCOL_VERSION {
495                tracing::info!("base version starts at the cutover, shutting down legacy stack");
496                self.shut_down_legacy().await;
497            }
498            return;
499        }
500        self.legacy_handle
501            .read()
502            .await
503            .hotshot
504            .start_consensus()
505            .await;
506    }
507
508    pub async fn shut_down(&self) {
509        for t in &self.tasks {
510            t.abort()
511        }
512        self.legacy_handle.write().await.shut_down().await;
513        let NewProtocol::Running {
514            shutdown,
515            coordinator,
516            ..
517        } = self.new_proto.write().take()
518        else {
519            return;
520        };
521        shutdown.cancel();
522        let _ = coordinator.await;
523    }
524
525    /// Permanently tear down the legacy consensus stack: its tasks and the
526    /// legacy network.
527    ///
528    /// The in-memory legacy consensus state stays readable for pre-cutover
529    /// queries, and in-flight DRB computations on the shared membership
530    /// coordinator keep running (the new protocol needs them for upcoming
531    /// epochs).
532    pub async fn shut_down_legacy(&self) {
533        for t in &self.tasks {
534            t.abort()
535        }
536        self.legacy_handle
537            .write()
538            .await
539            .shut_down_tasks_and_network()
540            .await;
541    }
542
543    fn is_new_proto_running(&self) -> bool {
544        matches!(*self.new_proto.read(), NewProtocol::Running { .. })
545    }
546
547    pub(crate) async fn client_api(&self) -> Option<ClientApi<T>> {
548        if let NewProtocol::Running { client_api, .. } = &*self.new_proto.read() {
549            return Some(client_api.clone());
550        }
551        self.activate().await;
552        if let NewProtocol::Running { client_api, .. } = &*self.new_proto.read() {
553            Some(client_api.clone())
554        } else {
555            None
556        }
557    }
558}
559
560async fn run_coordinator<T, S>(
561    mut coord: Coordinator<T, S>,
562    tx: Sender<CoordinatorEvent<T>>,
563    queue_len: Option<Arc<dyn Gauge>>,
564    seed: Option<PreCutoverSeed<T>>,
565    shutdown: CancellationToken,
566) where
567    T: NodeType,
568    S: NewProtocolStorage<T>,
569{
570    coord.start(seed);
571
572    loop {
573        select! {
574            () = shutdown.cancelled() => break,
575            it = coord.next_consensus_input() => {
576                if let Err(err) = apply_input(&mut coord, &tx, it).await {
577                    error!(%err, "coordinator: critical error");
578                    break;
579                }
580                if let Some(m) = &queue_len {
581                    m.set(tx.len())
582                }
583            }
584        }
585    }
586
587    coord.stop().await;
588}
589
590async fn apply_input<T, S>(
591    coord: &mut Coordinator<T, S>,
592    tx: &Sender<CoordinatorEvent<T>>,
593    it: Result<ConsensusInput<T>, CoordinatorError>,
594) -> Result<(), CoordinatorError>
595where
596    T: NodeType,
597    S: NewProtocolStorage<T>,
598{
599    match it {
600        Ok(it) => coord.apply_consensus(it),
601        Err(err) => {
602            if err.severity == Severity::Critical {
603                return Err(err);
604            }
605            warn!(%err, "coordinator: non-critical error");
606        },
607    }
608
609    while let Some(out) = coord.outbox_mut().pop_front() {
610        if let Some(e) = consensus_event(coord, &out) {
611            broadcast_event(tx, e).await;
612        }
613        if let Err(err) = coord.process_consensus_output(out) {
614            if err.severity == Severity::Critical {
615                return Err(err);
616            }
617            warn!(%err, "coordinator: error processing output");
618        }
619    }
620
621    while let Some(m) = coord.coordinator_outbox_mut().pop_front() {
622        let e = CoordinatorEvent::ExternalMessageReceived {
623            sender: m.sender,
624            data: m.data,
625        };
626        broadcast_event(tx, e).await;
627    }
628
629    Ok(())
630}
631
632// TODO: `ConsensusOutput::LeafDecided` still carries fields (leaves +
633// vid_shares) rather than a `Vec<LeafInfo>`. This is because `Consensus` doesn't own `StateManager`
634// state and delta only become available one level up, in `Coordinator`.
635fn consensus_event<T, S>(
636    coordinator: &Coordinator<T, S>,
637    output: &ConsensusOutput<T>,
638) -> Option<CoordinatorEvent<T>>
639where
640    T: NodeType,
641    S: NewProtocolStorage<T>,
642{
643    match output {
644        ConsensusOutput::LeafDecided {
645            leaves,
646            cert1,
647            cert2,
648            vid_shares,
649        } => {
650            if leaves.is_empty() {
651                tracing::error!("coordinator emitted LeafDecided with empty leaves");
652                return None;
653            }
654            let leaf_infos = leaves
655                .iter()
656                .zip(vid_shares.iter())
657                .map(|(leaf, vid_share)| {
658                    let (state, delta) = match coordinator.state(leaf.view_number()) {
659                        Some(s) => (s.state.clone(), s.delta.clone()),
660                        None => {
661                            let s = Arc::new(T::ValidatedState::from_header(leaf.block_header()));
662                            (s, None)
663                        },
664                    };
665                    let vid_share = vid_share
666                        .as_ref()
667                        .map(|share| VidDisperseShare::V2(share.data.clone()));
668                    LeafInfo::new(leaf.clone(), state, delta, vid_share, None)
669                })
670                .collect();
671            Some(CoordinatorEvent::NewDecide {
672                leaf_infos,
673                cert1: cert1.clone(),
674                cert2: cert2.clone(),
675            })
676        },
677        ConsensusOutput::ProposalValidated { proposal, sender } => {
678            Some(CoordinatorEvent::QuorumProposal {
679                proposal: proposal.clone(),
680                sender: sender.clone(),
681            })
682        },
683        ConsensusOutput::BlockPayloadReconstructed {
684            view,
685            header,
686            payload,
687        } => Some(CoordinatorEvent::BlockPayloadReconstructed {
688            view: *view,
689            header: header.clone(),
690            payload: payload.clone(),
691        }),
692        _ => None,
693    }
694}
695
696async fn broadcast_event<T>(sender: &Sender<CoordinatorEvent<T>>, event: CoordinatorEvent<T>)
697where
698    T: NodeType,
699{
700    match sender.broadcast_direct(event).await {
701        Ok(None) => {},
702        Ok(Some(overflowed)) => {
703            warn!(%overflowed, "coordinator event channel overflow, oldest event dropped");
704        },
705        Err(err) => {
706            warn!(%err, "failed to broadcast consensus event");
707        },
708    }
709}
710
711/// Forward legacy epoch transitions into the coordinator so cliquenet keeps
712/// dialing the current validator set before cutover.
713///
714/// The parked coordinator's event loop is not running yet, so its network is
715/// bumped directly under the state lock. Once the coordinator runs, it
716/// refreshes peers itself whenever a proposal validates, so this task ends.
717/// `epoch_height == 0` disables forwarding.
718async fn forward_legacy_epoch_changes<T, S>(
719    legacy_event_rx: InactiveReceiver<Event<T>>,
720    new_proto: Arc<RwLock<NewProtocol<T, S>>>,
721    epoch_height: u64,
722) where
723    T: NodeType,
724    S: NewProtocolStorage<T>,
725{
726    if epoch_height == 0 {
727        return;
728    }
729    let mut rx = legacy_event_rx.activate_cloned();
730    let mut last_forwarded: Option<EpochNumber> = None;
731    while let Some(event) = rx.next().await {
732        let EventType::Decide { leaf_chain, .. } = &event.event else {
733            continue;
734        };
735        let Some(newest) = leaf_chain.first() else {
736            continue;
737        };
738        let block_number = newest.leaf.block_header().block_number();
739        let epoch = EpochNumber::new(epoch_from_block_number(block_number, epoch_height));
740        if last_forwarded.is_some_and(|prev| epoch <= prev) {
741            continue;
742        }
743        match &mut *new_proto.write() {
744            NewProtocol::Init { coordinator, .. } => coordinator.bump_network_epoch(epoch),
745            _ => return,
746        }
747        last_forwarded = Some(epoch);
748    }
749}