Skip to main content

hotshot_new_protocol/
cutover.rs

1//! Legacy → new-protocol cutover machinery.
2//!
3//! Two concerns live here:
4//! - [`extract_pre_cutover_seed`] walks a live legacy [`SystemContextHandle`]
5//!   and produces a [`PreCutoverSeed`].
6//! - [`forward_legacy_timeout_votes`] and [`forward_legacy_high_qc`] tail the
7//!   legacy event stream and bridge those events into the coordinator's
8//!   client API so the new protocol can form TC2s and propose at the
9//!   boundary.
10
11use std::{collections::BTreeMap, sync::Arc};
12
13use async_broadcast::InactiveReceiver;
14use futures::StreamExt;
15use hotshot::{traits::NodeImplementation, types::SystemContextHandle};
16use hotshot_types::{
17    data::Leaf2,
18    event::{Event, EventType},
19    message::UpgradeLock,
20    traits::{metrics::Gauge, node_implementation::NodeType},
21};
22use versions::NEW_PROTOCOL_VERSION;
23
24use crate::{client::ClientApi, consensus::PreCutoverSeed};
25
26/// Walk legacy state to produce a [`PreCutoverSeed`]; `None` on
27/// a broken walk.
28pub async fn extract_pre_cutover_seed<T, I>(
29    handle: &SystemContextHandle<T, I>,
30) -> Option<PreCutoverSeed<T>>
31where
32    T: NodeType,
33    I: NodeImplementation<T>,
34{
35    let cutover_view = match handle.hotshot.upgrade_lock.decided_upgrade_cert() {
36        Some(cert) => cert.data.new_version_first_view,
37        None => {
38            tracing::warn!("no decided upgrade certificate; aborting seed extraction");
39            return None;
40        },
41    };
42
43    let consensus_arc = handle.hotshot.consensus();
44    let consensus = consensus_arc.read().await;
45    let decided_anchor = consensus.decided_leaf();
46    let decided_view = decided_anchor.view_number();
47
48    let high_qc = consensus.high_qc().clone();
49    let saved = consensus.saved_leaves();
50
51    // `saved_leaves` is canonical — a non-canonical entry would break legacy
52    // decide — so we can take every leaf above `decided_view` without
53    // re-validating via a `justify_qc` walk.
54    let mut undecided: Vec<Leaf2<T>> = saved
55        .values()
56        .filter(|leaf| leaf.view_number() > decided_view)
57        .cloned()
58        .collect();
59    undecided.sort_by_key(|leaf| leaf.view_number());
60
61    let mut validated_states = BTreeMap::new();
62    if let Some(state) = consensus.state(decided_view) {
63        validated_states.insert(decided_view, state.clone());
64    } else {
65        tracing::warn!(%decided_view, "no validated state for decided anchor");
66    }
67    for leaf in &undecided {
68        let view = leaf.view_number();
69        if let Some(state) = consensus.state(view) {
70            validated_states.insert(view, state.clone());
71        } else {
72            tracing::warn!(%view, "no validated state for undecided leaf");
73        }
74    }
75
76    Some(PreCutoverSeed {
77        decided_anchor,
78        undecided,
79        high_qc: Some(high_qc),
80        validated_states,
81        cutover_view,
82    })
83}
84
85/// Bridged requests only matter at the `NEW_PROTOCOL_VERSION` cutover;
86/// forwarding earlier fills the bounded request queue the parked coordinator
87/// can't drain.
88fn cutover_decided<T: NodeType>(upgrade_lock: &UpgradeLock<T>) -> bool {
89    upgrade_lock
90        .decided_upgrade_cert()
91        .is_some_and(|cert| cert.data.new_version >= NEW_PROTOCOL_VERSION)
92}
93
94/// Forward legacy `TimeoutVote2` events into the new-protocol timeout
95/// collectors so the first new leader can form TC2 at the boundary.
96pub async fn forward_legacy_timeout_votes<T: NodeType>(
97    legacy_event_rx: InactiveReceiver<Event<T>>,
98    client_api: ClientApi<T>,
99    upgrade_lock: UpgradeLock<T>,
100    queue_len: Option<Arc<dyn Gauge>>,
101) {
102    let mut rx = legacy_event_rx.activate_cloned();
103    while let Some(event) = rx.next().await {
104        if let Some(m) = &queue_len {
105            m.set(rx.len())
106        }
107        if let EventType::LegacyTimeoutVoteEmitted { vote } = event.event
108            && cutover_decided(&upgrade_lock)
109            && let Err(err) = client_api.try_submit_legacy_timeout_vote(vote)
110        {
111            tracing::warn!(%err, "failed to forward legacy TimeoutVote2 to new-protocol coordinator");
112        }
113    }
114}
115
116/// Forward the last legacy view's QC into the coordinator, so the cutover-view
117/// leader can propose on it instead of waiting out a timeout when the cutover
118/// seed was snapshotted before the QC formed.
119pub async fn forward_legacy_high_qc<T: NodeType>(
120    legacy_event_rx: InactiveReceiver<Event<T>>,
121    client_api: ClientApi<T>,
122    upgrade_lock: UpgradeLock<T>,
123) {
124    let mut rx = legacy_event_rx.activate_cloned();
125    while let Some(event) = rx.next().await {
126        if let EventType::LegacyHighQcFormed { qc } = event.event
127            && cutover_decided(&upgrade_lock)
128            && let Err(err) = client_api.try_submit_legacy_high_qc(qc)
129        {
130            tracing::warn!(%err, "failed to forward legacy high QC to new-protocol coordinator");
131        }
132    }
133}