1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
//! # Hard-Forks
//!
//! Monero use hard-forks to update it's protocol, this module contains a [`HFVotes`] struct which
//! keeps track of current blockchain voting, and has a method [`HFVotes::current_fork`] to check
//! if the next hard-fork should be activated.
use std::{
    collections::VecDeque,
    fmt::{Display, Formatter},
};

pub use cuprate_types::{HardFork, HardForkError};

#[cfg(test)]
mod tests;

pub const NUMB_OF_HARD_FORKS: usize = 16;

/// Checks a blocks version and vote, assuming that `hf` is the current hard-fork.
///
/// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#blocks-version-and-vote>
pub fn check_block_version_vote(
    hf: &HardFork,
    version: &HardFork,
    vote: &HardFork,
) -> Result<(), HardForkError> {
    // self = current hf
    if hf != version {
        Err(HardForkError::VersionIncorrect)?;
    }
    if hf > vote {
        Err(HardForkError::VoteTooLow)?;
    }

    Ok(())
}

/// Information about a given hard-fork.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct HFInfo {
    height: usize,
    threshold: usize,
}
impl HFInfo {
    pub const fn new(height: usize, threshold: usize) -> HFInfo {
        HFInfo { height, threshold }
    }
}

/// Information about every hard-fork Monero has had.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct HFsInfo([HFInfo; NUMB_OF_HARD_FORKS]);

impl HFsInfo {
    pub fn info_for_hf(&self, hf: &HardFork) -> HFInfo {
        self.0[*hf as usize - 1]
    }

    pub const fn new(hfs: [HFInfo; NUMB_OF_HARD_FORKS]) -> Self {
        Self(hfs)
    }

    /// Returns the main-net hard-fork information.
    ///
    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Mainnet-Hard-Forks>
    pub const fn main_net() -> HFsInfo {
        Self([
            HFInfo::new(0, 0),
            HFInfo::new(1009827, 0),
            HFInfo::new(1141317, 0),
            HFInfo::new(1220516, 0),
            HFInfo::new(1288616, 0),
            HFInfo::new(1400000, 0),
            HFInfo::new(1546000, 0),
            HFInfo::new(1685555, 0),
            HFInfo::new(1686275, 0),
            HFInfo::new(1788000, 0),
            HFInfo::new(1788720, 0),
            HFInfo::new(1978433, 0),
            HFInfo::new(2210000, 0),
            HFInfo::new(2210720, 0),
            HFInfo::new(2688888, 0),
            HFInfo::new(2689608, 0),
        ])
    }

    /// Returns the test-net hard-fork information.
    ///
    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Testnet-Hard-Forks>
    pub const fn test_net() -> HFsInfo {
        Self([
            HFInfo::new(0, 0),
            HFInfo::new(624634, 0),
            HFInfo::new(800500, 0),
            HFInfo::new(801219, 0),
            HFInfo::new(802660, 0),
            HFInfo::new(971400, 0),
            HFInfo::new(1057027, 0),
            HFInfo::new(1057058, 0),
            HFInfo::new(1057778, 0),
            HFInfo::new(1154318, 0),
            HFInfo::new(1155038, 0),
            HFInfo::new(1308737, 0),
            HFInfo::new(1543939, 0),
            HFInfo::new(1544659, 0),
            HFInfo::new(1982800, 0),
            HFInfo::new(1983520, 0),
        ])
    }

    /// Returns the test-net hard-fork information.
    ///
    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#Stagenet-Hard-Forks>
    pub const fn stage_net() -> HFsInfo {
        Self([
            HFInfo::new(0, 0),
            HFInfo::new(32000, 0),
            HFInfo::new(33000, 0),
            HFInfo::new(34000, 0),
            HFInfo::new(35000, 0),
            HFInfo::new(36000, 0),
            HFInfo::new(37000, 0),
            HFInfo::new(176456, 0),
            HFInfo::new(177176, 0),
            HFInfo::new(269000, 0),
            HFInfo::new(269720, 0),
            HFInfo::new(454721, 0),
            HFInfo::new(675405, 0),
            HFInfo::new(676125, 0),
            HFInfo::new(1151000, 0),
            HFInfo::new(1151720, 0),
        ])
    }
}

/// A struct holding the current voting state of the blockchain.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct HFVotes {
    votes: [usize; NUMB_OF_HARD_FORKS],
    vote_list: VecDeque<HardFork>,
    window_size: usize,
}

impl Display for HFVotes {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HFVotes")
            .field("total", &self.total_votes())
            .field("V1", &self.votes_for_hf(&HardFork::V1))
            .field("V2", &self.votes_for_hf(&HardFork::V2))
            .field("V3", &self.votes_for_hf(&HardFork::V3))
            .field("V4", &self.votes_for_hf(&HardFork::V4))
            .field("V5", &self.votes_for_hf(&HardFork::V5))
            .field("V6", &self.votes_for_hf(&HardFork::V6))
            .field("V7", &self.votes_for_hf(&HardFork::V7))
            .field("V8", &self.votes_for_hf(&HardFork::V8))
            .field("V9", &self.votes_for_hf(&HardFork::V9))
            .field("V10", &self.votes_for_hf(&HardFork::V10))
            .field("V11", &self.votes_for_hf(&HardFork::V11))
            .field("V12", &self.votes_for_hf(&HardFork::V12))
            .field("V13", &self.votes_for_hf(&HardFork::V13))
            .field("V14", &self.votes_for_hf(&HardFork::V14))
            .field("V15", &self.votes_for_hf(&HardFork::V15))
            .field("V16", &self.votes_for_hf(&HardFork::V16))
            .finish()
    }
}

impl HFVotes {
    pub fn new(window_size: usize) -> HFVotes {
        HFVotes {
            votes: [0; NUMB_OF_HARD_FORKS],
            vote_list: VecDeque::with_capacity(window_size),
            window_size,
        }
    }

    /// Add a vote for a hard-fork, this function removes votes outside of the window.
    pub fn add_vote_for_hf(&mut self, hf: &HardFork) {
        self.vote_list.push_back(*hf);
        self.votes[*hf as usize - 1] += 1;
        if self.vote_list.len() > self.window_size {
            let hf = self.vote_list.pop_front().unwrap();
            self.votes[hf as usize - 1] -= 1;
        }
    }

    /// Pop a number of blocks from the top of the cache and push some values into the front of the cache,
    /// i.e. the oldest blocks.
    ///
    /// `old_block_votes` should contain the HFs below the window that now will be in the window after popping
    /// blocks from the top.
    ///
    /// # Panics
    ///
    /// This will panic if `old_block_votes` contains more HFs than `numb_blocks`.
    pub fn reverse_blocks(&mut self, numb_blocks: usize, old_block_votes: Self) {
        assert!(old_block_votes.vote_list.len() <= numb_blocks);

        for hf in self.vote_list.drain(self.vote_list.len() - numb_blocks..) {
            self.votes[hf as usize - 1] -= 1;
        }

        for old_vote in old_block_votes.vote_list.into_iter().rev() {
            self.vote_list.push_front(old_vote);
            self.votes[old_vote as usize - 1] += 1;
        }
    }

    /// Returns the total votes for a hard-fork.
    ///
    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
    pub fn votes_for_hf(&self, hf: &HardFork) -> usize {
        self.votes[*hf as usize - 1..].iter().sum()
    }

    /// Returns the total amount of votes being tracked
    pub fn total_votes(&self) -> usize {
        self.vote_list.len()
    }

    /// Checks if a future hard fork should be activated, returning the next hard-fork that should be
    /// activated.
    ///
    /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
    pub fn current_fork(
        &self,
        current_hf: &HardFork,
        current_height: usize,
        window: usize,
        hfs_info: &HFsInfo,
    ) -> HardFork {
        let mut current_hf = *current_hf;

        while let Some(next_hf) = current_hf.next_fork() {
            let hf_info = hfs_info.info_for_hf(&next_hf);
            if current_height >= hf_info.height
                && self.votes_for_hf(&next_hf) >= votes_needed(hf_info.threshold, window)
            {
                current_hf = next_hf;
            } else {
                // if we don't have enough votes for this fork any future fork won't have enough votes
                // as votes are cumulative.
                // TODO: If a future fork has a lower threshold that could not be true, but as all current forks
                // have threshold 0 it is ok for now.
                return current_hf;
            }
        }
        current_hf
    }
}

/// Returns the votes needed for a hard-fork.
///
/// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork>
pub fn votes_needed(threshold: usize, window: usize) -> usize {
    (threshold * window).div_ceil(100)
}