Skip to main content

cuprate_consensus_rules/
hard_forks.rs

1//! # Hard-Forks
2//!
3//! Monero use hard-forks to update it's protocol, this module contains a [`HFVotes`] struct which
4//! keeps track of current blockchain voting, and has a method [`HFVotes::current_fork`] to check
5//! if the next hard-fork should be activated.
6use std::{
7    collections::VecDeque,
8    fmt::{Display, Formatter},
9};
10
11pub use cuprate_types::{HardFork, HardForkError};
12
13#[cfg(test)]
14mod tests;
15
16pub const NUMB_OF_HARD_FORKS: usize = 16;
17
18/// Checks a blocks version and vote, assuming that `hf` is the current hard-fork.
19///
20/// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#blocks-version-and-vote>
21pub fn check_block_version_vote(
22    hf: &HardFork,
23    version: &HardFork,
24    vote: &HardFork,
25) -> Result<(), HardForkError> {
26    // self = current hf
27    if hf != version {
28        return Err(HardForkError::VersionIncorrect);
29    }
30    if hf > vote {
31        return Err(HardForkError::VoteTooLow);
32    }
33
34    Ok(())
35}
36
37/// The amount of time in seconds after the last fork's scheduled timestamp before we
38/// consider this node likely forked from the network.
39///
40/// ref: <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_basic/hardfork.h#L49>
41const FORKED_TIME: u64 = 31_557_600; // one year in seconds
42
43/// The amount of time in seconds after the last fork's scheduled timestamp before we
44/// warn that an update is needed.
45///
46/// ref: <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_basic/hardfork.h#L50>
47const UPDATE_TIME: u64 = FORKED_TIME / 2;
48
49/// The state of the daemon with respect to the latest scheduled hard-fork.
50///
51/// ref: <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_basic/hardfork.h#L46>
52#[derive(Debug, Clone, Copy, Eq, PartialEq)]
53#[repr(u32)]
54pub enum HardForkState {
55    LikelyForked = 0,
56    UpdateNeeded = 1,
57    Ready = 2,
58}
59
60impl From<HardForkState> for u32 {
61    fn from(state: HardForkState) -> Self {
62        state as Self
63    }
64}
65
66/// Information about a given hard-fork.
67#[derive(Debug, Clone, Copy, Eq, PartialEq)]
68pub struct HFInfo {
69    height: usize,
70    threshold: usize,
71    time: u64,
72}
73impl HFInfo {
74    pub const fn height(&self) -> usize {
75        self.height
76    }
77
78    pub const fn threshold(&self) -> usize {
79        self.threshold
80    }
81
82    pub const fn time(&self) -> u64 {
83        self.time
84    }
85
86    pub const fn new(height: usize, threshold: usize, time: u64) -> Self {
87        Self {
88            height,
89            threshold,
90            time,
91        }
92    }
93}
94
95/// Information about every hard-fork Monero has had.
96#[derive(Debug, Clone, Copy, Eq, PartialEq)]
97pub struct HFsInfo([HFInfo; NUMB_OF_HARD_FORKS]);
98
99impl HFsInfo {
100    pub const fn info_for_hf(&self, hf: &HardFork) -> HFInfo {
101        self.0[*hf as usize - 1]
102    }
103
104    pub const fn new(hfs: [HFInfo; NUMB_OF_HARD_FORKS]) -> Self {
105        Self(hfs)
106    }
107
108    /// Returns the hard-fork state based on the current time.
109    ///
110    /// Mirrors `HardFork::get_state()` in monerod:
111    /// - [`HardForkState::LikelyForked`]: more than one year past the last scheduled fork time.
112    /// - [`HardForkState::UpdateNeeded`]: more than six months past the last scheduled fork time.
113    /// - [`HardForkState::Ready`]: within six months of the last scheduled fork time.
114    ///
115    /// ref: <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_basic/hardfork.cpp#L326-L345>
116    pub const fn hard_fork_state(&self, current_time: u64) -> HardForkState {
117        let last_fork_time = self.0[NUMB_OF_HARD_FORKS - 1].time;
118        if current_time >= last_fork_time.saturating_add(FORKED_TIME) {
119            HardForkState::LikelyForked
120        } else if current_time >= last_fork_time.saturating_add(UPDATE_TIME) {
121            HardForkState::UpdateNeeded
122        } else {
123            HardForkState::Ready
124        }
125    }
126
127    /// Returns the main-net hard-fork information.
128    ///
129    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Mainnet-Hard-Forks>
130    pub const fn main_net() -> Self {
131        Self([
132            HFInfo::new(0, 0, 1341378000),
133            HFInfo::new(1009827, 0, 1442763710),
134            HFInfo::new(1141317, 0, 1458558528),
135            HFInfo::new(1220516, 0, 1483574400),
136            HFInfo::new(1288616, 0, 1489520158),
137            HFInfo::new(1400000, 0, 1503046577),
138            HFInfo::new(1546000, 0, 1521303150),
139            HFInfo::new(1685555, 0, 1535889547),
140            HFInfo::new(1686275, 0, 1535889548),
141            HFInfo::new(1788000, 0, 1549792439),
142            HFInfo::new(1788720, 0, 1550225678),
143            HFInfo::new(1978433, 0, 1571419280),
144            HFInfo::new(2210000, 0, 1598180817),
145            HFInfo::new(2210720, 0, 1598180818),
146            HFInfo::new(2688888, 0, 1656629117),
147            HFInfo::new(2689608, 0, 1656629118),
148        ])
149    }
150
151    /// Returns the test-net hard-fork information.
152    ///
153    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Testnet-Hard-Forks>
154    pub const fn test_net() -> Self {
155        Self([
156            HFInfo::new(0, 0, 1341378000),
157            HFInfo::new(624634, 0, 1445355000),
158            HFInfo::new(800500, 0, 1472415034),
159            HFInfo::new(801219, 0, 1472415035),
160            HFInfo::new(802660, 0, 1472415036 + 86400 * 180),
161            HFInfo::new(971400, 0, 1501709789),
162            HFInfo::new(1057027, 0, 1512211236),
163            HFInfo::new(1057058, 0, 1533211200),
164            HFInfo::new(1057778, 0, 1533297600),
165            HFInfo::new(1154318, 0, 1550153694),
166            HFInfo::new(1155038, 0, 1550225678),
167            HFInfo::new(1308737, 0, 1569582000),
168            HFInfo::new(1543939, 0, 1599069376),
169            HFInfo::new(1544659, 0, 1599069377),
170            HFInfo::new(1982800, 0, 1652727000),
171            HFInfo::new(1983520, 0, 1652813400),
172        ])
173    }
174
175    /// Returns the fake-chain (regtest) hard-fork information.
176    ///
177    /// ref: <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/cryptonote_core.cpp#L670>
178    pub const fn fake_chain() -> Self {
179        let mut hfs = [HFInfo::new(1, 0, 0); NUMB_OF_HARD_FORKS];
180        hfs[0] = HFInfo::new(0, 0, 0);
181        Self(hfs)
182    }
183
184    /// Returns the stagenet hard-fork information.
185    ///
186    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Stagenet-Hard-Forks>
187    pub const fn stage_net() -> Self {
188        Self([
189            HFInfo::new(0, 0, 1341378000),
190            HFInfo::new(32000, 0, 1521000000),
191            HFInfo::new(33000, 0, 1521120000),
192            HFInfo::new(34000, 0, 1521240000),
193            HFInfo::new(35000, 0, 1521360000),
194            HFInfo::new(36000, 0, 1521480000),
195            HFInfo::new(37000, 0, 1521600000),
196            HFInfo::new(176456, 0, 1537821770),
197            HFInfo::new(177176, 0, 1537821771),
198            HFInfo::new(269000, 0, 1550153694),
199            HFInfo::new(269720, 0, 1550225678),
200            HFInfo::new(454721, 0, 1571419280),
201            HFInfo::new(675405, 0, 1598180817),
202            HFInfo::new(676125, 0, 1598180818),
203            HFInfo::new(1151000, 0, 1656629117),
204            HFInfo::new(1151720, 0, 1656629118),
205        ])
206    }
207}
208
209/// A struct holding the current voting state of the blockchain.
210#[derive(Debug, Clone, Eq, PartialEq)]
211pub struct HFVotes {
212    votes: [usize; NUMB_OF_HARD_FORKS],
213    vote_list: VecDeque<HardFork>,
214    window_size: usize,
215}
216
217impl Display for HFVotes {
218    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
219        f.debug_struct("HFVotes")
220            .field("total", &self.total_votes())
221            .field("V1", &self.votes_for_hf(&HardFork::V1))
222            .field("V2", &self.votes_for_hf(&HardFork::V2))
223            .field("V3", &self.votes_for_hf(&HardFork::V3))
224            .field("V4", &self.votes_for_hf(&HardFork::V4))
225            .field("V5", &self.votes_for_hf(&HardFork::V5))
226            .field("V6", &self.votes_for_hf(&HardFork::V6))
227            .field("V7", &self.votes_for_hf(&HardFork::V7))
228            .field("V8", &self.votes_for_hf(&HardFork::V8))
229            .field("V9", &self.votes_for_hf(&HardFork::V9))
230            .field("V10", &self.votes_for_hf(&HardFork::V10))
231            .field("V11", &self.votes_for_hf(&HardFork::V11))
232            .field("V12", &self.votes_for_hf(&HardFork::V12))
233            .field("V13", &self.votes_for_hf(&HardFork::V13))
234            .field("V14", &self.votes_for_hf(&HardFork::V14))
235            .field("V15", &self.votes_for_hf(&HardFork::V15))
236            .field("V16", &self.votes_for_hf(&HardFork::V16))
237            .finish()
238    }
239}
240
241impl HFVotes {
242    pub fn new(window_size: usize) -> Self {
243        Self {
244            votes: [0; NUMB_OF_HARD_FORKS],
245            vote_list: VecDeque::with_capacity(window_size),
246            window_size,
247        }
248    }
249
250    /// Add a vote for a hard-fork, this function removes votes outside of the window.
251    pub fn add_vote_for_hf(&mut self, hf: &HardFork) {
252        self.vote_list.push_back(*hf);
253        self.votes[*hf as usize - 1] += 1;
254        if self.vote_list.len() > self.window_size {
255            let hf = self.vote_list.pop_front().unwrap();
256            self.votes[hf as usize - 1] -= 1;
257        }
258    }
259
260    /// Pop a number of blocks from the top of the cache and push some values into the front of the cache,
261    /// i.e. the oldest blocks.
262    ///
263    /// `old_block_votes` should contain the HFs below the window that now will be in the window after popping
264    /// blocks from the top.
265    ///
266    /// # Panics
267    ///
268    /// This will panic if `old_block_votes` contains more HFs than `numb_blocks`.
269    pub fn reverse_blocks(&mut self, numb_blocks: usize, old_block_votes: Self) {
270        assert!(old_block_votes.vote_list.len() <= numb_blocks);
271
272        for hf in self.vote_list.drain(self.vote_list.len() - numb_blocks..) {
273            self.votes[hf as usize - 1] -= 1;
274        }
275
276        for old_vote in old_block_votes.vote_list.into_iter().rev() {
277            self.vote_list.push_front(old_vote);
278            self.votes[old_vote as usize - 1] += 1;
279        }
280    }
281
282    /// Returns the total votes for a hard-fork.
283    ///
284    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
285    pub fn votes_for_hf(&self, hf: &HardFork) -> usize {
286        self.votes[*hf as usize - 1..].iter().sum()
287    }
288
289    /// Returns the total amount of votes being tracked
290    pub fn total_votes(&self) -> usize {
291        self.vote_list.len()
292    }
293
294    /// Checks if a future hard fork should be activated, returning the next hard-fork that should be
295    /// activated.
296    ///
297    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
298    pub fn current_fork(
299        &self,
300        current_hf: &HardFork,
301        current_height: usize,
302        window: usize,
303        hfs_info: &HFsInfo,
304    ) -> HardFork {
305        let mut current_hf = *current_hf;
306
307        while let Some(next_hf) = current_hf.next_fork() {
308            let hf_info = hfs_info.info_for_hf(&next_hf);
309            if current_height >= hf_info.height
310                && self.votes_for_hf(&next_hf) >= votes_needed(hf_info.threshold, window)
311            {
312                current_hf = next_hf;
313            } else {
314                // if we don't have enough votes for this fork any future fork won't have enough votes
315                // as votes are cumulative.
316                // TODO: If a future fork has a lower threshold that could not be true, but as all current forks
317                // have threshold 0 it is ok for now.
318                return current_hf;
319            }
320        }
321        current_hf
322    }
323}
324
325/// Returns the votes needed for a hard-fork.
326///
327/// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
328pub const fn votes_needed(threshold: usize, window: usize) -> usize {
329    (threshold * window).div_ceil(100)
330}