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/// Information about a given hard-fork.
38#[derive(Debug, Clone, Copy, Eq, PartialEq)]
39pub struct HFInfo {
40    height: usize,
41    threshold: usize,
42}
43impl HFInfo {
44    pub const fn new(height: usize, threshold: usize) -> Self {
45        Self { height, threshold }
46    }
47}
48
49/// Information about every hard-fork Monero has had.
50#[derive(Debug, Clone, Copy, Eq, PartialEq)]
51pub struct HFsInfo([HFInfo; NUMB_OF_HARD_FORKS]);
52
53impl HFsInfo {
54    pub const fn info_for_hf(&self, hf: &HardFork) -> HFInfo {
55        self.0[*hf as usize - 1]
56    }
57
58    pub const fn new(hfs: [HFInfo; NUMB_OF_HARD_FORKS]) -> Self {
59        Self(hfs)
60    }
61
62    /// Returns the main-net hard-fork information.
63    ///
64    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Mainnet-Hard-Forks>
65    pub const fn main_net() -> Self {
66        Self([
67            HFInfo::new(0, 0),
68            HFInfo::new(1009827, 0),
69            HFInfo::new(1141317, 0),
70            HFInfo::new(1220516, 0),
71            HFInfo::new(1288616, 0),
72            HFInfo::new(1400000, 0),
73            HFInfo::new(1546000, 0),
74            HFInfo::new(1685555, 0),
75            HFInfo::new(1686275, 0),
76            HFInfo::new(1788000, 0),
77            HFInfo::new(1788720, 0),
78            HFInfo::new(1978433, 0),
79            HFInfo::new(2210000, 0),
80            HFInfo::new(2210720, 0),
81            HFInfo::new(2688888, 0),
82            HFInfo::new(2689608, 0),
83        ])
84    }
85
86    /// Returns the test-net hard-fork information.
87    ///
88    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Testnet-Hard-Forks>
89    pub const fn test_net() -> Self {
90        Self([
91            HFInfo::new(0, 0),
92            HFInfo::new(624634, 0),
93            HFInfo::new(800500, 0),
94            HFInfo::new(801219, 0),
95            HFInfo::new(802660, 0),
96            HFInfo::new(971400, 0),
97            HFInfo::new(1057027, 0),
98            HFInfo::new(1057058, 0),
99            HFInfo::new(1057778, 0),
100            HFInfo::new(1154318, 0),
101            HFInfo::new(1155038, 0),
102            HFInfo::new(1308737, 0),
103            HFInfo::new(1543939, 0),
104            HFInfo::new(1544659, 0),
105            HFInfo::new(1982800, 0),
106            HFInfo::new(1983520, 0),
107        ])
108    }
109
110    /// Returns the test-net hard-fork information.
111    ///
112    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Stagenet-Hard-Forks>
113    pub const fn stage_net() -> Self {
114        Self([
115            HFInfo::new(0, 0),
116            HFInfo::new(32000, 0),
117            HFInfo::new(33000, 0),
118            HFInfo::new(34000, 0),
119            HFInfo::new(35000, 0),
120            HFInfo::new(36000, 0),
121            HFInfo::new(37000, 0),
122            HFInfo::new(176456, 0),
123            HFInfo::new(177176, 0),
124            HFInfo::new(269000, 0),
125            HFInfo::new(269720, 0),
126            HFInfo::new(454721, 0),
127            HFInfo::new(675405, 0),
128            HFInfo::new(676125, 0),
129            HFInfo::new(1151000, 0),
130            HFInfo::new(1151720, 0),
131        ])
132    }
133}
134
135/// A struct holding the current voting state of the blockchain.
136#[derive(Debug, Clone, Eq, PartialEq)]
137pub struct HFVotes {
138    votes: [usize; NUMB_OF_HARD_FORKS],
139    vote_list: VecDeque<HardFork>,
140    window_size: usize,
141}
142
143impl Display for HFVotes {
144    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
145        f.debug_struct("HFVotes")
146            .field("total", &self.total_votes())
147            .field("V1", &self.votes_for_hf(&HardFork::V1))
148            .field("V2", &self.votes_for_hf(&HardFork::V2))
149            .field("V3", &self.votes_for_hf(&HardFork::V3))
150            .field("V4", &self.votes_for_hf(&HardFork::V4))
151            .field("V5", &self.votes_for_hf(&HardFork::V5))
152            .field("V6", &self.votes_for_hf(&HardFork::V6))
153            .field("V7", &self.votes_for_hf(&HardFork::V7))
154            .field("V8", &self.votes_for_hf(&HardFork::V8))
155            .field("V9", &self.votes_for_hf(&HardFork::V9))
156            .field("V10", &self.votes_for_hf(&HardFork::V10))
157            .field("V11", &self.votes_for_hf(&HardFork::V11))
158            .field("V12", &self.votes_for_hf(&HardFork::V12))
159            .field("V13", &self.votes_for_hf(&HardFork::V13))
160            .field("V14", &self.votes_for_hf(&HardFork::V14))
161            .field("V15", &self.votes_for_hf(&HardFork::V15))
162            .field("V16", &self.votes_for_hf(&HardFork::V16))
163            .finish()
164    }
165}
166
167impl HFVotes {
168    pub fn new(window_size: usize) -> Self {
169        Self {
170            votes: [0; NUMB_OF_HARD_FORKS],
171            vote_list: VecDeque::with_capacity(window_size),
172            window_size,
173        }
174    }
175
176    /// Add a vote for a hard-fork, this function removes votes outside of the window.
177    pub fn add_vote_for_hf(&mut self, hf: &HardFork) {
178        self.vote_list.push_back(*hf);
179        self.votes[*hf as usize - 1] += 1;
180        if self.vote_list.len() > self.window_size {
181            let hf = self.vote_list.pop_front().unwrap();
182            self.votes[hf as usize - 1] -= 1;
183        }
184    }
185
186    /// Pop a number of blocks from the top of the cache and push some values into the front of the cache,
187    /// i.e. the oldest blocks.
188    ///
189    /// `old_block_votes` should contain the HFs below the window that now will be in the window after popping
190    /// blocks from the top.
191    ///
192    /// # Panics
193    ///
194    /// This will panic if `old_block_votes` contains more HFs than `numb_blocks`.
195    pub fn reverse_blocks(&mut self, numb_blocks: usize, old_block_votes: Self) {
196        assert!(old_block_votes.vote_list.len() <= numb_blocks);
197
198        for hf in self.vote_list.drain(self.vote_list.len() - numb_blocks..) {
199            self.votes[hf as usize - 1] -= 1;
200        }
201
202        for old_vote in old_block_votes.vote_list.into_iter().rev() {
203            self.vote_list.push_front(old_vote);
204            self.votes[old_vote as usize - 1] += 1;
205        }
206    }
207
208    /// Returns the total votes for a hard-fork.
209    ///
210    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
211    pub fn votes_for_hf(&self, hf: &HardFork) -> usize {
212        self.votes[*hf as usize - 1..].iter().sum()
213    }
214
215    /// Returns the total amount of votes being tracked
216    pub fn total_votes(&self) -> usize {
217        self.vote_list.len()
218    }
219
220    /// Checks if a future hard fork should be activated, returning the next hard-fork that should be
221    /// activated.
222    ///
223    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
224    pub fn current_fork(
225        &self,
226        current_hf: &HardFork,
227        current_height: usize,
228        window: usize,
229        hfs_info: &HFsInfo,
230    ) -> HardFork {
231        let mut current_hf = *current_hf;
232
233        while let Some(next_hf) = current_hf.next_fork() {
234            let hf_info = hfs_info.info_for_hf(&next_hf);
235            if current_height >= hf_info.height
236                && self.votes_for_hf(&next_hf) >= votes_needed(hf_info.threshold, window)
237            {
238                current_hf = next_hf;
239            } else {
240                // if we don't have enough votes for this fork any future fork won't have enough votes
241                // as votes are cumulative.
242                // TODO: If a future fork has a lower threshold that could not be true, but as all current forks
243                // have threshold 0 it is ok for now.
244                return current_hf;
245            }
246        }
247        current_hf
248    }
249}
250
251/// Returns the votes needed for a hard-fork.
252///
253/// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
254pub const fn votes_needed(threshold: usize, window: usize) -> usize {
255    (threshold * window).div_ceil(100)
256}