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"); }
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"); }
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 pub async fn membership_coordinator(&self) -> EpochMembershipCoordinator<T> {
314 self.legacy_handle
315 .read()
316 .await
317 .membership_coordinator
318 .clone()
319 }
320
321 pub async fn upgrade_lock(&self) -> UpgradeLock<T> {
323 self.legacy_handle.read().await.hotshot.upgrade_lock.clone()
324 }
325
326 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 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
632fn 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
711async 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}