1use std::{
6 collections::BTreeSet,
7 time::{SystemTime, UNIX_EPOCH},
8};
9
10use committable::Committable;
11use hotshot_types::{
12 data::{EpochNumber, UpgradeProposal2, ViewNumber},
13 message::UpgradeLock,
14 simple_vote::{UpgradeProposalData2, UpgradeVote2},
15 traits::{node_implementation::NodeType, signature_key::SignatureKey},
16 upgrade_config::UpgradeConfig,
17};
18use tracing::{debug, info, warn};
19
20use crate::message::{UpgradeProposalMessage, UpgradeVoteMessage};
21
22pub const DECIDE_BY_OFFSET: u64 = 10;
26
27pub const FINISH_OFFSET: u64 = 20;
31
32pub(crate) fn expected_upgrade_data(
45 upgrade: &versions::Upgrade,
46 view: ViewNumber,
47 epoch: EpochNumber,
48) -> Option<UpgradeProposalData2> {
49 let offset = |n: u64| (*view).checked_add(n).map(ViewNumber::new);
50 Some(UpgradeProposalData2 {
51 old_version: upgrade.base,
52 new_version: upgrade.target,
53 decide_by: offset(DECIDE_BY_OFFSET)?,
54 new_version_hash: upgrade.hash().into(),
55 old_version_last_view: offset(FINISH_OFFSET - 1)?,
56 new_version_first_view: offset(FINISH_OFFSET)?,
57 epoch,
58 })
59}
60
61pub struct UpgradeProtocol<T: NodeType> {
62 config: UpgradeConfig,
63 upgrade_lock: UpgradeLock<T>,
64 public_key: T::SignatureKey,
65 private_key: <T::SignatureKey as SignatureKey>::PrivateKey,
66 proposed_views: BTreeSet<ViewNumber>,
67 voted_views: BTreeSet<ViewNumber>,
68}
69
70impl<T: NodeType> UpgradeProtocol<T> {
71 pub fn new(
72 config: UpgradeConfig,
73 upgrade_lock: UpgradeLock<T>,
74 public_key: T::SignatureKey,
75 private_key: <T::SignatureKey as SignatureKey>::PrivateKey,
76 ) -> Self {
77 Self {
78 config,
79 upgrade_lock,
80 public_key,
81 private_key,
82 proposed_views: BTreeSet::new(),
83 voted_views: BTreeSet::new(),
84 }
85 }
86
87 pub fn maybe_propose(
89 &mut self,
90 view: ViewNumber,
91 epoch: EpochNumber,
92 ) -> Option<UpgradeProposalMessage<T>> {
93 if !self.active() || self.proposed_views.contains(&view) || !self.proposing_open(view) {
94 return None;
95 }
96 let data = expected_upgrade_data(&self.upgrade_lock.upgrade(), view, epoch)?;
97 let signature = match T::SignatureKey::sign(&self.private_key, data.commit().as_ref()) {
98 Ok(signature) => signature,
99 Err(err) => {
100 warn!(%view, %err, "failed to sign upgrade proposal");
101 return None;
102 },
103 };
104 info!(
105 target: "announce",
106 %view,
107 new_version = %data.new_version,
108 decide_by = %data.decide_by,
109 first_view = %data.new_version_first_view,
110 "proposing upgrade"
111 );
112 self.proposed_views.insert(view);
113 Some(UpgradeProposalMessage::new(
114 UpgradeProposal2 {
115 upgrade_proposal: data,
116 view_number: view,
117 },
118 signature,
119 ))
120 }
121
122 pub fn maybe_vote(
125 &mut self,
126 message: &UpgradeProposalMessage<T>,
127 sender: &T::SignatureKey,
128 leader: Option<&T::SignatureKey>,
129 current_view: ViewNumber,
130 current_epoch: EpochNumber,
131 ) -> Option<UpgradeVoteMessage<T>> {
132 let view = message.data.view_number;
133 if !self.active() || self.voted_views.contains(&view) {
134 return None;
135 }
136 if !self.voting_open(view) {
137 debug!(%view, "upgrade proposal outside the voting window");
138 return None;
139 }
140 if view + 1 < current_view {
141 debug!(%view, %current_view, "upgrade proposal for stale view");
142 return None;
143 }
144 if leader != Some(sender) {
145 warn!(%view, "upgrade proposal not sent by the view leader");
146 return None;
147 }
148 let data = &message.data.upgrade_proposal;
149 if !sender.validate(&message.signature, data.commit().as_ref()) {
150 warn!(%view, "invalid upgrade proposal signature");
151 return None;
152 }
153 let expected = expected_upgrade_data(&self.upgrade_lock.upgrade(), view, current_epoch);
154 if expected.as_ref() != Some(data) {
155 warn!(%view, ?data, "upgrade proposal data differs from the expected data");
156 return None;
157 }
158 let vote = match UpgradeVote2::<T>::create_signed_vote(
159 data.clone(),
160 view,
161 &self.public_key,
162 &self.private_key,
163 &self.upgrade_lock,
164 ) {
165 Ok(vote) => vote,
166 Err(err) => {
167 warn!(%view, %err, "failed to sign upgrade vote");
168 return None;
169 },
170 };
171 info!(target: "announce", %view, new_version = %data.new_version, "voting for upgrade");
172 self.voted_views.insert(view);
173 Some(vote)
174 }
175
176 pub fn gc(&mut self, view: ViewNumber) {
177 self.proposed_views = self.proposed_views.split_off(&view);
178 self.voted_views = self.voted_views.split_off(&view);
179 }
180
181 fn active(&self) -> bool {
184 let upgrade = self.upgrade_lock.upgrade();
185 upgrade.base < upgrade.target && self.upgrade_lock.decided_upgrade_cert().is_none()
186 }
187
188 fn proposing_open(&self, view: ViewNumber) -> bool {
189 let c = &self.config;
190 in_window(*view, c.start_proposing_view, c.stop_proposing_view)
191 && in_window(unix_time(), c.start_proposing_time, c.stop_proposing_time)
192 }
193
194 fn voting_open(&self, view: ViewNumber) -> bool {
195 let c = &self.config;
196 in_window(*view, c.start_voting_view, c.stop_voting_view)
197 && in_window(unix_time(), c.start_voting_time, c.stop_voting_time)
198 }
199}
200
201fn in_window(value: u64, start: u64, stop: u64) -> bool {
202 start <= value && value < stop
203}
204
205fn unix_time() -> u64 {
206 SystemTime::now()
207 .duration_since(UNIX_EPOCH)
208 .map(|d| d.as_secs())
209 .unwrap_or(0)
210}
211
212#[cfg(test)]
213mod tests {
214 use hotshot_example_types::node_types::TestTypes;
215 use hotshot_types::{
216 message::UpgradeLock,
217 traits::{node_implementation::NodeType, signature_key::SignatureKey},
218 };
219 use versions::{Upgrade, version};
220
221 use super::*;
222
223 type Key = <TestTypes as NodeType>::SignatureKey;
224
225 fn upgrade_protocol(config: UpgradeConfig) -> UpgradeProtocol<TestTypes> {
226 let (public_key, private_key) = Key::generated_from_seed_indexed([0; 32], 0);
227 let lock = UpgradeLock::new(Upgrade::new(version(0, 6), version(0, 7)));
228 UpgradeProtocol::new(config, lock, public_key, private_key)
229 }
230
231 fn open_view_windows() -> UpgradeConfig {
232 UpgradeConfig {
233 start_proposing_view: 5,
234 stop_proposing_view: 15,
235 start_voting_view: 0,
236 stop_voting_view: u64::MAX,
237 start_proposing_time: 0,
238 stop_proposing_time: u64::MAX,
239 start_voting_time: 0,
240 stop_voting_time: u64::MAX,
241 }
242 }
243
244 #[test]
245 fn expected_data_offsets() {
246 let upgrade = Upgrade::new(version(0, 6), version(0, 7));
247 let view = ViewNumber::new(10);
248 let epoch = EpochNumber::new(3);
249 let data = expected_upgrade_data(&upgrade, view, epoch).unwrap();
250 assert_eq!(data.old_version, version(0, 6));
251 assert_eq!(data.new_version, version(0, 7));
252 assert_eq!(data.epoch, epoch);
253 assert_eq!(data.new_version_hash, Vec::<u8>::from(upgrade.hash()));
254 assert_eq!(data.decide_by, view + DECIDE_BY_OFFSET);
255 assert_eq!(data.new_version_first_view, view + FINISH_OFFSET);
256 assert!(data.decide_by < data.new_version_first_view);
257 assert_eq!(data.old_version_last_view + 1, data.new_version_first_view);
258 }
259
260 #[test]
261 fn expected_data_none_when_offsets_overflow() {
262 let upgrade = Upgrade::new(version(0, 6), version(0, 7));
263 let epoch = EpochNumber::genesis();
264 let last_valid = ViewNumber::new(u64::MAX - FINISH_OFFSET);
265 assert!(expected_upgrade_data(&upgrade, last_valid, epoch).is_some());
266 assert!(expected_upgrade_data(&upgrade, last_valid + 1, epoch).is_none());
267 }
268
269 #[test]
270 fn proposes_only_inside_window_and_once() {
271 let mut protocol = upgrade_protocol(open_view_windows());
272 let epoch = EpochNumber::genesis();
273 assert!(protocol.maybe_propose(ViewNumber::new(4), epoch).is_none());
274 assert!(protocol.maybe_propose(ViewNumber::new(15), epoch).is_none());
275 let proposal = protocol.maybe_propose(ViewNumber::new(7), epoch).unwrap();
276 assert_eq!(proposal.data.view_number, ViewNumber::new(7));
277 assert!(protocol.maybe_propose(ViewNumber::new(7), epoch).is_none());
278 }
279
280 #[test]
281 fn disabled_config_never_proposes() {
282 let mut protocol = upgrade_protocol(UpgradeConfig::default());
283 assert!(
284 protocol
285 .maybe_propose(ViewNumber::new(7), EpochNumber::genesis())
286 .is_none()
287 );
288 }
289
290 #[test]
291 fn trivial_upgrade_never_proposes() {
292 let (public_key, private_key) = Key::generated_from_seed_indexed([0; 32], 0);
293 let lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(version(0, 6)));
294 let mut protocol = UpgradeProtocol::new(open_view_windows(), lock, public_key, private_key);
295 assert!(
296 protocol
297 .maybe_propose(ViewNumber::new(7), EpochNumber::genesis())
298 .is_none()
299 );
300 }
301
302 #[test]
303 fn votes_only_for_expected_data_from_leader() {
304 let mut leader = upgrade_protocol(open_view_windows());
305 let mut voter = upgrade_protocol(open_view_windows());
306 let leader_key = leader.public_key;
307 let view = ViewNumber::new(7);
308 let epoch = EpochNumber::new(1);
309
310 let proposal = leader.maybe_propose(view, epoch).unwrap();
311
312 let (other_key, _) = Key::generated_from_seed_indexed([0; 32], 1);
313 assert!(
314 voter
315 .maybe_vote(&proposal, &leader_key, Some(&other_key), view, epoch)
316 .is_none()
317 );
318
319 let mut tampered = proposal.clone();
320 tampered.data.upgrade_proposal.new_version_first_view = view + 1;
321 assert!(
322 voter
323 .maybe_vote(&tampered, &leader_key, Some(&leader_key), view, epoch)
324 .is_none()
325 );
326
327 assert!(
328 voter
329 .maybe_vote(&proposal, &leader_key, Some(&leader_key), view + 2, epoch)
330 .is_none()
331 );
332
333 let mut other_epoch = proposal.clone();
335 other_epoch.data.upgrade_proposal.epoch = epoch + 1;
336 assert!(
337 voter
338 .maybe_vote(&other_epoch, &leader_key, Some(&leader_key), view, epoch)
339 .is_none()
340 );
341
342 let vote = voter
343 .maybe_vote(&proposal, &leader_key, Some(&leader_key), view, epoch)
344 .unwrap();
345 assert_eq!(vote.data.epoch, epoch);
346 assert_eq!(
347 vote.data,
348 expected_upgrade_data(&voter.upgrade_lock.upgrade(), view, epoch).unwrap()
349 );
350
351 assert!(
352 voter
353 .maybe_vote(&proposal, &leader_key, Some(&leader_key), view, epoch)
354 .is_none()
355 );
356 }
357}