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
40pub 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 exited: CancellationToken,
51}
52
53struct 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 #[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 pub async fn stopped(&self) {
170 self.exited.cancelled().await
171 }
172
173 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 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 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 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 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 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 self.exited.cancelled().await;
422 }
423 drop(self.coordinator.write().take());
424 }
425
426 #[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 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
538fn 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}