Skip to main content

espresso_node/
consensus_handle.rs

1use std::{collections::HashMap, mem, sync::Arc};
2
3use anyhow::bail;
4use async_broadcast::{InactiveReceiver, Sender, broadcast};
5use committable::Commitment;
6use espresso_api::error::SubmitError;
7use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
8use hotshot_new_protocol::{
9    block,
10    client::{ClientApi, QueryError},
11    consensus::{ConsensusInput, ConsensusOutput},
12    coordinator::{
13        Coordinator,
14        error::{CoordinatorError, Severity},
15    },
16    message::OpaqueMessage,
17    state::UpdateLeaf,
18    storage::NewProtocolStorage,
19};
20use hotshot_types::{
21    data::{BlockNumber, EpochNumber, Leaf2, QuorumProposalWrapper, VidDisperseShare, ViewNumber},
22    epoch_membership::EpochMembershipCoordinator,
23    event::LeafInfo,
24    initializer::HotShotInitializer,
25    message::{Proposal as SignedProposal, UpgradeLock, convert_proposal},
26    new_protocol::CoordinatorEvent,
27    traits::{
28        ValidatedState,
29        metrics::{Gauge, Metrics},
30        node_implementation::NodeType,
31        signature_key::SignatureKey,
32    },
33    utils::StateAndDelta,
34};
35use parking_lot::RwLock;
36use tokio::{select, spawn, sync::mpsc::UnboundedReceiver};
37use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle};
38use tracing::{error, warn};
39
40/// The node's handle on consensus: the [`Coordinator`], run on its own task from
41/// [`start_consensus`](Self::start_consensus) on, and what the rest of the node reads from it.
42pub struct ConsensusHandle<T: NodeType, S> {
43    epoch_height: BlockNumber,
44    event_rx: InactiveReceiver<CoordinatorEvent<T>>,
45    upgrade_lock: UpgradeLock<T>,
46    membership_coordinator: EpochMembershipCoordinator<T>,
47    resting: Arc<RwLock<Resting<T>>>,
48    coordinator: Arc<RwLock<CoordinatorState<T, S>>>,
49    /// Cancelled once the coordinator task has ended, however it ended.
50    exited: CancellationToken,
51}
52
53/// What the readers serve once the coordinator has stopped: what it held when it stopped, written
54/// by its task on the way out, or the anchor if it never ran.
55struct Resting<T: NodeType> {
56    view: ViewNumber,
57    leaf: Leaf2<T>,
58    state: Arc<T::ValidatedState>,
59}
60
61#[allow(clippy::large_enum_variant)]
62enum CoordinatorState<T: NodeType, S> {
63    Empty,
64    Init {
65        coordinator: Coordinator<T, S>,
66        external_rx: UnboundedReceiver<OpaqueMessage<T::SignatureKey>>,
67        event_tx: Sender<CoordinatorEvent<T>>,
68        queue_len: Option<Arc<dyn Gauge>>,
69    },
70    Running {
71        /// Held so that dropping the handle aborts the task; `shut_down` waits on `exited`.
72        #[cfg_attr(not(test), allow(dead_code))]
73        coordinator: AbortOnDropHandle<()>,
74        client_api: ClientApi<T>,
75        shutdown: CancellationToken,
76    },
77}
78
79impl<T: NodeType, S> CoordinatorState<T, S> {
80    fn take(&mut self) -> Self {
81        mem::replace(self, Self::Empty)
82    }
83}
84
85impl<T, S> ConsensusHandle<T, S>
86where
87    T: NodeType,
88    S: NewProtocolStorage<T>,
89{
90    pub fn new(
91        coordinator: Coordinator<T, S>,
92        external_rx: UnboundedReceiver<OpaqueMessage<T::SignatureKey>>,
93        upgrade_lock: UpgradeLock<T>,
94        membership_coordinator: EpochMembershipCoordinator<T>,
95        initializer: &HotShotInitializer<T>,
96        event_channel_capacity: usize,
97        metrics: &dyn Metrics,
98    ) -> Self {
99        let (mut event_tx, mut event_rx) = broadcast(event_channel_capacity);
100        event_tx.set_await_active(false);
101        event_rx.set_overflow(true);
102
103        let coordinator_event_queue_len = metrics.is_recording().then(|| {
104            metrics
105                .create_gauge("coordinator_event_queue_len".into(), None)
106                .into()
107        });
108
109        Self {
110            epoch_height: initializer.epoch_height().into(),
111            event_rx: event_rx.deactivate(),
112            upgrade_lock,
113            membership_coordinator,
114            resting: Arc::new(RwLock::new(Resting {
115                view: initializer.anchor_leaf().view_number(),
116                leaf: initializer.anchor_leaf().clone(),
117                state: initializer.anchor_state().clone(),
118            })),
119            coordinator: Arc::new(RwLock::new(CoordinatorState::Init {
120                coordinator,
121                external_rx,
122                event_tx,
123                queue_len: coordinator_event_queue_len,
124            })),
125            exited: CancellationToken::new(),
126        }
127    }
128
129    pub async fn start_consensus(&self) {
130        if matches!(*self.coordinator.read(), CoordinatorState::Running { .. }) {
131            return;
132        }
133
134        let mut state = self.coordinator.write();
135
136        match state.take() {
137            CoordinatorState::Init {
138                coordinator,
139                external_rx,
140                event_tx,
141                queue_len,
142            } => {
143                let client_api = coordinator.client_api().clone();
144                let shutdown = CancellationToken::new();
145                *state = CoordinatorState::Running {
146                    coordinator: AbortOnDropHandle::new(spawn(run_coordinator(
147                        coordinator,
148                        external_rx,
149                        event_tx,
150                        queue_len,
151                        shutdown.clone(),
152                        self.exited.clone(),
153                        Arc::clone(&self.resting),
154                    ))),
155                    client_api,
156                    shutdown,
157                };
158            },
159            other => *state = other,
160        }
161    }
162
163    pub fn event_stream(&self) -> BoxStream<'static, CoordinatorEvent<T>> {
164        self.event_rx.activate_cloned().boxed()
165    }
166
167    /// Resolves once the coordinator task has ended, whether `shut_down` stopped it or it died
168    /// on a critical error or a panic. Never resolves if consensus never started.
169    pub async fn stopped(&self) {
170        self.exited.cancelled().await
171    }
172
173    /// Serves the resting state once the coordinator has been told to shut down, waiting for
174    /// the state it leaves on the way out if a request fails meanwhile. Panics if the
175    /// coordinator died instead: the node must not answer as if the chain had stopped.
176    pub async fn current_view(&self) -> ViewNumber {
177        let Some(client_api) = self.client_api().await else {
178            return self.resting.read().view;
179        };
180        match client_api.current_view().await {
181            Ok(view) => view,
182            Err(err) => self.resting_unless_dead(err).await.view,
183        }
184    }
185
186    /// See [`Self::current_view`].
187    pub async fn decided_leaf(&self) -> Leaf2<T> {
188        let Some(client_api) = self.client_api().await else {
189            return self.resting.read().leaf.clone();
190        };
191        match client_api.decided_leaf().await {
192            Ok(leaf) => leaf,
193            Err(err) => self.resting_unless_dead(err).await.leaf.clone(),
194        }
195    }
196
197    /// See [`Self::current_view`].
198    pub async fn decided_state(&self) -> Option<Arc<T::ValidatedState>> {
199        let Some(client_api) = self.client_api().await else {
200            return Some(self.resting.read().state.clone());
201        };
202        match client_api.decided_state().await {
203            Ok(state) => state,
204            Err(err) => Some(self.resting_unless_dead(err).await.state.clone()),
205        }
206    }
207
208    /// Where a failed request leaves a reader: the coordinator was told to shut down and the
209    /// resting state it leaves behind answers, or no shutdown was asked for, so it died.
210    async fn resting_unless_dead(
211        &self,
212        err: QueryError,
213    ) -> parking_lot::RwLockReadGuard<'_, Resting<T>> {
214        let stopping = match &*self.coordinator.read() {
215            CoordinatorState::Running { shutdown, .. } => shutdown.is_cancelled(),
216            _ => true,
217        };
218        if !stopping {
219            panic!("coordinator died: {err}");
220        }
221        // The coordinator writes the resting state before it exits.
222        self.exited.cancelled().await;
223        self.resting.read()
224    }
225
226    pub async fn state(&self, view: ViewNumber) -> Option<Arc<T::ValidatedState>> {
227        match self.client_api().await?.state(view).await {
228            Ok(state) => state,
229            Err(err) => {
230                warn!(%view, %err, "coordinator unavailable for state");
231                None
232            },
233        }
234    }
235
236    pub async fn state_and_delta(&self, view: ViewNumber) -> StateAndDelta<T> {
237        let Some(client_api) = self.client_api().await else {
238            return (None, None);
239        };
240        match client_api.state_and_delta(view).await {
241            Ok(state_and_delta) => state_and_delta,
242            Err(err) => {
243                warn!(%view, %err, "coordinator unavailable for state_and_delta");
244                (None, None)
245            },
246        }
247    }
248
249    pub async fn undecided_leaves(&self) -> Vec<Leaf2<T>> {
250        let Some(client_api) = self.client_api().await else {
251            return Vec::new();
252        };
253        match client_api.undecided_leaves().await {
254            Ok(leaves) => leaves,
255            Err(err) => {
256                warn!(%err, "coordinator unavailable for undecided_leaves");
257                Vec::new()
258            },
259        }
260    }
261
262    pub async fn current_epoch(&self) -> Option<EpochNumber> {
263        let Some(client_api) = self.client_api().await else {
264            return self.resting.read().leaf.epoch(*self.epoch_height);
265        };
266        match client_api.current_epoch().await {
267            Ok(epoch) => Some(epoch),
268            Err(err) => {
269                warn!(%err, "coordinator unavailable for current_epoch");
270                None
271            },
272        }
273    }
274
275    pub fn epoch_height(&self) -> BlockNumber {
276        self.epoch_height
277    }
278
279    pub fn membership_coordinator(&self) -> EpochMembershipCoordinator<T> {
280        self.membership_coordinator.clone()
281    }
282
283    pub fn upgrade_lock(&self) -> UpgradeLock<T> {
284        self.upgrade_lock.clone()
285    }
286
287    pub async fn current_proposal_participation(&self) -> HashMap<T::SignatureKey, f64> {
288        let Some(client_api) = self.client_api().await else {
289            return HashMap::new();
290        };
291        client_api
292            .proposal_participation(None)
293            .await
294            .inspect_err(|err| {
295                warn!(%err, "coordinator unavailable for current_proposal_participation");
296            })
297            .unwrap_or_default()
298    }
299
300    pub async fn proposal_participation(
301        &self,
302        epoch: EpochNumber,
303    ) -> HashMap<T::SignatureKey, f64> {
304        let Some(client_api) = self.client_api().await else {
305            return HashMap::new();
306        };
307        client_api
308            .proposal_participation(Some(epoch))
309            .await
310            .inspect_err(|err| {
311                warn!(%err, "coordinator unavailable for proposal_participation");
312            })
313            .unwrap_or_default()
314    }
315
316    pub async fn current_vote_participation(
317        &self,
318    ) -> HashMap<<T::SignatureKey as SignatureKey>::VerificationKeyType, f64> {
319        let Some(client_api) = self.client_api().await else {
320            return HashMap::new();
321        };
322        client_api
323            .vote_participation(None)
324            .await
325            .inspect_err(|err| {
326                warn!(%err, "coordinator unavailable for current_vote_participation");
327            })
328            .unwrap_or_default()
329    }
330
331    pub async fn vote_participation(
332        &self,
333        epoch: EpochNumber,
334    ) -> HashMap<<T::SignatureKey as SignatureKey>::VerificationKeyType, f64> {
335        let Some(client_api) = self.client_api().await else {
336            return HashMap::new();
337        };
338        client_api
339            .vote_participation(Some(epoch))
340            .await
341            .inspect_err(|err| {
342                warn!(%err, "coordinator unavailable for vote_participation");
343            })
344            .unwrap_or_default()
345    }
346
347    pub async fn request_proposal(
348        &self,
349        view: ViewNumber,
350        leaf_commitment: Commitment<Leaf2<T>>,
351    ) -> anyhow::Result<
352        BoxFuture<'static, anyhow::Result<SignedProposal<T, QuorumProposalWrapper<T>>>>,
353    > {
354        let Some(client_api) = self.client_api().await else {
355            bail!("consensus is not running");
356        };
357        Ok(async move {
358            client_api
359                .request_proposal(view, leaf_commitment)
360                .await
361                .map(convert_proposal)
362                .map_err(anyhow::Error::new)
363        }
364        .boxed())
365    }
366
367    pub async fn submit_transaction(&self, tx: T::Transaction) -> anyhow::Result<()> {
368        let Some(client_api) = self.client_api().await else {
369            bail!("consensus is not running");
370        };
371        client_api
372            .submit_transaction(tx)
373            .await
374            .map_err(|err| match err {
375                QueryError::Rejected(rejection @ block::SubmitError::TooLarge { .. }) => {
376                    SubmitError::Invalid(rejection.to_string()).into()
377                },
378                QueryError::Rejected(rejection @ block::SubmitError::RetryBufferFull) => {
379                    SubmitError::Overloaded(rejection.to_string()).into()
380                },
381                err => anyhow::Error::new(err),
382            })
383    }
384
385    pub async fn update_leaf(
386        &self,
387        leaf: Leaf2<T>,
388        state: Arc<T::ValidatedState>,
389        delta: Option<Arc<<T::ValidatedState as ValidatedState<T>>::Delta>>,
390    ) -> anyhow::Result<()> {
391        let view = leaf.view_number();
392        let Some(client_api) = self.client_api().await else {
393            bail!("consensus is not running");
394        };
395        client_api
396            .update_leaf(UpdateLeaf {
397                view,
398                leaf,
399                state,
400                delta,
401            })
402            .await
403            .map_err(|e| anyhow::anyhow!("{e}"))
404    }
405
406    /// Stops the coordinator if it runs, and drops it if it never started so that nothing can
407    /// start it afterwards; the readers serve the resting state from here on.
408    pub async fn shut_down(&self) {
409        self.membership_coordinator.cancel_all_drb();
410        let running = match &*self.coordinator.read() {
411            CoordinatorState::Running { shutdown, .. } => {
412                shutdown.cancel();
413                true
414            },
415            _ => false,
416        };
417        if running {
418            // The coordinator writes the resting state before it exits. Take it only afterwards,
419            // so a reader whose request fails meanwhile waits for that state instead of taking
420            // the coordinator for dead.
421            self.exited.cancelled().await;
422        }
423        drop(self.coordinator.write().take());
424    }
425
426    /// Aborts the coordinator task without touching the handle's state, as a panic in it would.
427    #[cfg(test)]
428    pub(crate) fn kill_coordinator(&self) {
429        if let CoordinatorState::Running { coordinator, .. } = &*self.coordinator.read() {
430            coordinator.abort();
431        }
432    }
433
434    pub(crate) async fn client_api(&self) -> Option<ClientApi<T>> {
435        if let CoordinatorState::Running { client_api, .. } = &*self.coordinator.read() {
436            return Some(client_api.clone());
437        }
438        self.start_consensus().await;
439        if let CoordinatorState::Running { client_api, .. } = &*self.coordinator.read() {
440            Some(client_api.clone())
441        } else {
442            None
443        }
444    }
445}
446
447async fn run_coordinator<T, S>(
448    mut coord: Coordinator<T, S>,
449    mut external_rx: UnboundedReceiver<OpaqueMessage<T::SignatureKey>>,
450    tx: Sender<CoordinatorEvent<T>>,
451    queue_len: Option<Arc<dyn Gauge>>,
452    shutdown: CancellationToken,
453    exited: CancellationToken,
454    resting: Arc<RwLock<Resting<T>>>,
455) where
456    T: NodeType,
457    S: NewProtocolStorage<T>,
458{
459    let _exited = exited.drop_guard();
460    coord.start();
461    let _forward = AbortOnDropHandle::new(spawn({
462        let tx = tx.clone();
463        async move {
464            while let Some(m) = external_rx.recv().await {
465                let e = CoordinatorEvent::ExternalMessageReceived {
466                    sender: m.sender,
467                    data: m.data,
468                };
469                broadcast_event(&tx, e).await;
470            }
471        }
472    }));
473
474    loop {
475        select! {
476            () = shutdown.cancelled() => break,
477            it = coord.next_consensus_input() => {
478                if let Err(err) = apply_input(&mut coord, &tx, it).await {
479                    error!(%err, "coordinator: critical error");
480                    break;
481                }
482                if let Some(m) = &queue_len {
483                    m.set(tx.len())
484                }
485            }
486        }
487    }
488
489    // Leave the readers what the coordinator holds now. No request reaches it from here on, so
490    // nothing it still has queued moves the chain past this.
491    let leaf = coord.last_decided_leaf().clone();
492    let state = coord
493        .state(leaf.view_number())
494        .map(|entry| entry.state.clone())
495        .unwrap_or_else(|| Arc::new(T::ValidatedState::from_header(leaf.block_header())));
496    *resting.write() = Resting {
497        view: coord.current_view(),
498        leaf,
499        state,
500    };
501    coord.stop().await;
502}
503
504async fn apply_input<T, S>(
505    coord: &mut Coordinator<T, S>,
506    tx: &Sender<CoordinatorEvent<T>>,
507    it: Result<ConsensusInput<T>, CoordinatorError>,
508) -> Result<(), CoordinatorError>
509where
510    T: NodeType,
511    S: NewProtocolStorage<T>,
512{
513    match it {
514        Ok(it) => coord.apply_consensus(it),
515        Err(err) => {
516            if err.severity == Severity::Critical {
517                return Err(err);
518            }
519            warn!(%err, "coordinator: non-critical error");
520        },
521    }
522
523    while let Some(out) = coord.outbox_mut().pop_front() {
524        if let Some(e) = consensus_event(coord, &out) {
525            broadcast_event(tx, e).await;
526        }
527        if let Err(err) = coord.process_consensus_output(out) {
528            if err.severity == Severity::Critical {
529                return Err(err);
530            }
531            warn!(%err, "coordinator: error processing output");
532        }
533    }
534
535    Ok(())
536}
537
538// TODO: `ConsensusOutput::LeafDecided` still carries fields (leaves +
539// vid_shares) rather than a `Vec<LeafInfo>`. This is because `Consensus` doesn't own `StateManager`
540// state and delta only become available one level up, in `Coordinator`.
541fn consensus_event<T, S>(
542    coordinator: &Coordinator<T, S>,
543    output: &ConsensusOutput<T>,
544) -> Option<CoordinatorEvent<T>>
545where
546    T: NodeType,
547    S: NewProtocolStorage<T>,
548{
549    match output {
550        ConsensusOutput::LeafDecided {
551            leaves,
552            cert1,
553            cert2,
554            vid_shares,
555        } => {
556            if leaves.is_empty() {
557                tracing::error!("coordinator emitted LeafDecided with empty leaves");
558                return None;
559            }
560            let leaf_infos = leaves
561                .iter()
562                .zip(vid_shares.iter())
563                .map(|(leaf, vid_share)| {
564                    let (state, delta) = match coordinator.state(leaf.view_number()) {
565                        Some(s) => (s.state.clone(), s.delta.clone()),
566                        None => {
567                            let s = Arc::new(T::ValidatedState::from_header(leaf.block_header()));
568                            (s, None)
569                        },
570                    };
571                    let vid_share = vid_share
572                        .as_ref()
573                        .map(|share| VidDisperseShare::V2(share.data.clone()));
574                    LeafInfo::new(leaf.clone(), state, delta, vid_share, None)
575                })
576                .collect();
577            Some(CoordinatorEvent::NewDecide {
578                leaf_infos,
579                cert1: cert1.clone(),
580                cert2: cert2.clone(),
581            })
582        },
583        ConsensusOutput::ProposalValidated { proposal, sender } => {
584            Some(CoordinatorEvent::QuorumProposal {
585                proposal: Box::new(proposal.clone()),
586                sender: sender.clone(),
587            })
588        },
589        ConsensusOutput::BlockPayloadReconstructed {
590            view,
591            header,
592            payload,
593        } => Some(CoordinatorEvent::BlockPayloadReconstructed {
594            view: *view,
595            header: header.clone(),
596            payload: Arc::clone(payload),
597        }),
598        _ => None,
599    }
600}
601
602async fn broadcast_event<T>(sender: &Sender<CoordinatorEvent<T>>, event: CoordinatorEvent<T>)
603where
604    T: NodeType,
605{
606    match sender.broadcast_direct(event).await {
607        Ok(None) => {},
608        Ok(Some(overflowed)) => {
609            warn!(%overflowed, "coordinator event channel overflow, oldest event dropped");
610        },
611        Err(err) => {
612            warn!(%err, "failed to broadcast consensus event");
613        },
614    }
615}