Skip to main content

hotshot_new_protocol/
upgrade.rs

1//! The upgrade sub-protocol: proposing and voting on protocol version
2//! upgrades. Owns the wall clock and the window configuration so the pure
3//! consensus state machine never needs them.
4
5use 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
22/// Views after the upgrade proposal by which a carrying leaf must decide.
23/// The certificate forms within a view of the proposal and each following
24/// leader re-attaches it, so this is the number of leader turns allowed.
25pub const DECIDE_BY_OFFSET: u64 = 10;
26
27/// Views after the upgrade proposal at which the new version takes effect.
28/// The gap past `DECIDE_BY_OFFSET` is margin for nodes to decide the carrier
29/// before their own `UpgradeLock` flips the wire and header format.
30pub const FINISH_OFFSET: u64 = 20;
31
32/// The `UpgradeProposalData2` every honest node expects for an upgrade
33/// proposed at `view` in `epoch`. Voters require full equality, so a leader
34/// cannot pick rogue offsets (e.g. a `new_version_first_view` so close that
35/// the wire format would flip before the decided certificate propagates) or
36/// an epoch other than the voter's own.
37///
38/// Unlike the legacy task this ignores `T::UPGRADE_CONSTANTS`: one-view
39/// finality needs no 130-view runway or empty-block transition period.
40///
41/// `None` when an offset would overflow the view number. No honest node
42/// reaches such a view, so nothing is expected there and a peer supplying
43/// one is rejected instead of wrapping (release) or panicking (debug).
44pub(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    /// An upgrade proposal for `view` in `epoch`, which this node must lead.
88    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    /// A vote for a received upgrade proposal, when it is exactly the one
123    /// this node expects from the view's leader for its own `current_epoch`.
124    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    /// A trivial `Upgrade` (base == target) disables the sub-protocol
182    /// whatever the windows say; a decided upgrade ends it.
183    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        // Voters sign only their own epoch.
334        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}