hotshot_new_protocol/
cutover.rs1use std::{
14 collections::BTreeMap,
15 sync::atomic::{AtomicBool, Ordering},
16};
17
18use async_broadcast::InactiveReceiver;
19use futures::StreamExt;
20use hotshot::{traits::NodeImplementation, types::SystemContextHandle};
21use hotshot_types::{
22 data::{EpochNumber, Leaf2},
23 event::{Event, EventType},
24 traits::{block_contents::BlockHeader, node_implementation::NodeType},
25 utils::epoch_from_block_number,
26};
27use versions::NEW_PROTOCOL_VERSION;
28
29use crate::{client::ClientApi, consensus::PreCutoverSeed};
30
31pub async fn extract_pre_cutover_seed<T, I>(
34 handle: &SystemContextHandle<T, I>,
35) -> Option<PreCutoverSeed<T>>
36where
37 T: NodeType,
38 I: NodeImplementation<T>,
39{
40 let cutover_view = match handle.hotshot.upgrade_lock.decided_upgrade_cert() {
41 Some(cert) => cert.data.new_version_first_view,
42 None => {
43 tracing::warn!("no decided upgrade certificate; aborting seed extraction");
44 return None;
45 },
46 };
47
48 let consensus_arc = handle.hotshot.consensus();
49 let consensus = consensus_arc.read().await;
50 let decided_anchor = consensus.decided_leaf();
51 let decided_view = decided_anchor.view_number();
52
53 let high_qc = consensus.high_qc().clone();
54 let saved = consensus.saved_leaves();
55
56 let mut undecided: Vec<Leaf2<T>> = saved
60 .values()
61 .filter(|leaf| leaf.view_number() > decided_view)
62 .cloned()
63 .collect();
64 undecided.sort_by_key(|leaf| leaf.view_number());
65
66 let mut validated_states = BTreeMap::new();
67 if let Some(state) = consensus.state(decided_view) {
68 validated_states.insert(decided_view, state.clone());
69 } else {
70 tracing::warn!(%decided_view, "no validated state for decided anchor");
71 }
72 for leaf in &undecided {
73 let view = leaf.view_number();
74 if let Some(state) = consensus.state(view) {
75 validated_states.insert(view, state.clone());
76 } else {
77 tracing::warn!(%view, "no validated state for undecided leaf");
78 }
79 }
80
81 Some(PreCutoverSeed {
82 decided_anchor,
83 undecided,
84 high_qc: Some(high_qc),
85 validated_states,
86 cutover_view,
87 })
88}
89
90#[derive(Debug, Default)]
95pub struct CutoverGate {
96 active: AtomicBool,
97}
98
99impl CutoverGate {
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn is_active(&self) -> bool {
107 self.active.load(Ordering::Relaxed)
108 }
109
110 pub async fn check<T, I>(
115 &self,
116 legacy: &SystemContextHandle<T, I>,
117 client_api: &ClientApi<T>,
118 ) -> bool
119 where
120 T: NodeType,
121 I: NodeImplementation<T>,
122 {
123 if self.is_active() {
124 return true;
125 }
126 let cur_view = legacy.cur_view().await;
127 let crossed =
128 legacy.hotshot.upgrade_lock.version_infallible(cur_view) >= NEW_PROTOCOL_VERSION;
129 if !crossed {
130 return false;
131 }
132
133 if let Some(seed) = extract_pre_cutover_seed(legacy).await {
134 if let Err(err) = client_api.seed_pre_cutover(seed).await {
135 tracing::warn!(%err, "seed_pre_cutover client request failed");
136 }
137 } else {
138 tracing::warn!("seed extraction returned None; coordinator will not be seeded");
139 }
140
141 self.active.store(true, Ordering::Relaxed);
142 true
143 }
144}
145
146pub async fn forward_legacy_timeout_votes<T: NodeType>(
149 legacy_event_rx: InactiveReceiver<Event<T>>,
150 client_api: ClientApi<T>,
151) {
152 let mut rx = legacy_event_rx.activate_cloned();
153 while let Some(event) = rx.next().await {
154 if let EventType::LegacyTimeoutVoteEmitted { vote } = event.event
155 && let Err(err) = client_api.submit_timeout_vote(vote).await
156 {
157 tracing::warn!(%err, "failed to forward legacy TimeoutVote2 to new-protocol coordinator");
158 }
159 }
160}
161
162pub async fn forward_legacy_epoch_changes<T: NodeType>(
165 legacy_event_rx: InactiveReceiver<Event<T>>,
166 client_api: ClientApi<T>,
167 epoch_height: u64,
168) {
169 if epoch_height == 0 {
170 return;
171 }
172 let mut rx = legacy_event_rx.activate_cloned();
173 let mut last_forwarded: Option<EpochNumber> = None;
174 while let Some(event) = rx.next().await {
175 let EventType::Decide { leaf_chain, .. } = &event.event else {
176 continue;
177 };
178 let Some(newest) = leaf_chain.first() else {
179 continue;
180 };
181 let block_number = newest.leaf.block_header().block_number();
182 let epoch = EpochNumber::new(epoch_from_block_number(block_number, epoch_height));
183 if last_forwarded.is_some_and(|prev| epoch <= prev) {
184 continue;
185 }
186 if let Err(err) = client_api.bump_network_epoch(epoch).await {
187 tracing::warn!(%epoch, %err, "failed to forward legacy epoch change to new-protocol coordinator");
188 continue;
189 }
190 last_forwarded = Some(epoch);
191 }
192}