Skip to main content

cuprate_consensus_context/
hardforks.rs

1use std::ops::Range;
2
3use strum::VariantArray;
4use tower::ServiceExt;
5use tracing::instrument;
6
7use cuprate_consensus_rules::{HFVotes, HFsInfo, HardFork};
8use cuprate_helper::time::current_unix_timestamp;
9use cuprate_types::{
10    blockchain::{BlockchainReadRequest, BlockchainResponse},
11    rpc::HardForkInfo,
12    Chain,
13};
14
15use crate::{ContextCacheError, Database};
16
17/// The default amount of hard-fork votes to track to decide on activation of a hard-fork.
18///
19/// ref: <https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork>
20const DEFAULT_WINDOW_SIZE: usize = 10080; // supermajority window check length - a week
21
22/// Configuration for hard-forks.
23///
24#[derive(Debug, Clone, Copy, Eq, PartialEq)]
25pub struct HardForkConfig {
26    /// The network we are on.
27    pub info: HFsInfo,
28    /// The amount of votes we are taking into account to decide on a fork activation.
29    pub window: usize,
30}
31
32impl HardForkConfig {
33    /// Config for main-net.
34    pub const fn main_net() -> Self {
35        Self {
36            info: HFsInfo::main_net(),
37            window: DEFAULT_WINDOW_SIZE,
38        }
39    }
40
41    /// Config for stage-net.
42    pub const fn stage_net() -> Self {
43        Self {
44            info: HFsInfo::stage_net(),
45            window: DEFAULT_WINDOW_SIZE,
46        }
47    }
48
49    /// Config for test-net.
50    pub const fn test_net() -> Self {
51        Self {
52            info: HFsInfo::test_net(),
53            window: DEFAULT_WINDOW_SIZE,
54        }
55    }
56
57    /// Config for fake-chain (regtest).
58    pub const fn fake_chain() -> Self {
59        Self {
60            info: HFsInfo::fake_chain(),
61            window: DEFAULT_WINDOW_SIZE,
62        }
63    }
64}
65
66/// A struct that keeps track of the current hard-fork and current votes.
67#[derive(Debug, Clone, Eq, PartialEq)]
68pub struct HardForkState {
69    /// The current active hard-fork.
70    pub current_hardfork: HardFork,
71
72    /// The hard-fork config.
73    pub config: HardForkConfig,
74    /// The votes in the current window.
75    pub votes: HFVotes,
76
77    /// The last block height accounted for.
78    pub last_height: usize,
79}
80
81impl HardForkState {
82    /// Initialize the [`HardForkState`] from the specified chain height.
83    #[instrument(name = "init_hardfork_state", skip(config, database), level = "info")]
84    pub async fn init_from_chain_height<D: Database + Clone>(
85        chain_height: usize,
86        config: HardForkConfig,
87        mut database: D,
88    ) -> Result<Self, ContextCacheError> {
89        tracing::info!("Initializing hard-fork state this may take a while.");
90
91        let block_start = chain_height.saturating_sub(config.window);
92
93        let votes =
94            get_votes_in_range(database.clone(), block_start..chain_height, config.window).await?;
95
96        if chain_height > config.window {
97            debug_assert_eq!(votes.total_votes(), config.window);
98        }
99
100        let BlockchainResponse::BlockExtendedHeader(ext_header) = database
101            .ready()
102            .await?
103            .call(BlockchainReadRequest::BlockExtendedHeader(chain_height - 1))
104            .await?
105        else {
106            panic!("Database sent incorrect response!");
107        };
108
109        let current_hardfork = ext_header.version;
110
111        let mut hfs = Self {
112            config,
113            current_hardfork,
114            votes,
115            last_height: chain_height - 1,
116        };
117
118        hfs.check_set_new_hf();
119
120        tracing::info!(
121            "Initialized Hfs, current fork: {:?}, {}",
122            hfs.current_hardfork,
123            hfs.votes
124        );
125
126        Ok(hfs)
127    }
128
129    /// Pop some blocks from the top of the cache.
130    ///
131    /// The cache will be returned to the state it would have been in `numb_blocks` ago.
132    ///
133    /// # Invariant
134    ///
135    /// This _must_ only be used on a main-chain cache.
136    pub async fn pop_blocks_main_chain<D: Database + Clone>(
137        &mut self,
138        numb_blocks: usize,
139        database: D,
140    ) -> Result<(), ContextCacheError> {
141        let Some(retained_blocks) = self.votes.total_votes().checked_sub(self.config.window) else {
142            *self = Self::init_from_chain_height(
143                self.last_height + 1 - numb_blocks,
144                self.config,
145                database,
146            )
147            .await?;
148
149            return Ok(());
150        };
151
152        let current_chain_height = self.last_height + 1;
153
154        let oldest_votes = get_votes_in_range(
155            database,
156            current_chain_height
157                .saturating_sub(self.config.window)
158                .saturating_sub(numb_blocks)
159                ..current_chain_height
160                    .saturating_sub(numb_blocks)
161                    .saturating_sub(retained_blocks),
162            numb_blocks,
163        )
164        .await?;
165
166        self.votes.reverse_blocks(numb_blocks, oldest_votes);
167        self.last_height -= numb_blocks;
168
169        Ok(())
170    }
171
172    /// Add a new block to the cache.
173    pub fn new_block(&mut self, vote: HardFork, height: usize) {
174        // We don't _need_ to take in `height` but it's for safety, so we don't silently lose track
175        // of blocks.
176        assert_eq!(self.last_height + 1, height);
177        self.last_height += 1;
178
179        tracing::debug!(
180            "Accounting for new block's vote, height: {}, vote: {:?}",
181            self.last_height,
182            vote
183        );
184
185        // This function remove votes outside the window as well.
186        self.votes.add_vote_for_hf(&vote);
187
188        if height > self.config.window {
189            debug_assert_eq!(self.votes.total_votes(), self.config.window);
190        }
191
192        self.check_set_new_hf();
193    }
194
195    /// Checks if the next hard-fork should be activated and activates it if it should.
196    ///
197    /// <https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork>
198    fn check_set_new_hf(&mut self) {
199        self.current_hardfork = self.votes.current_fork(
200            &self.current_hardfork,
201            self.last_height + 1,
202            self.config.window,
203            &self.config.info,
204        );
205    }
206
207    /// Returns info on all hard-forks.
208    pub fn hardfork_infos(&self) -> Vec<HardForkInfo> {
209        let current = self.current_hardfork;
210        // `voting` is the highest version blocks can vote for.
211        // ref: <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_basic/hardfork.cpp#L421>
212        let voting = HardFork::LATEST.as_u8();
213        let state = u32::from(self.config.info.hard_fork_state(current_unix_timestamp()));
214        let window = u32::try_from(self.votes.total_votes()).unwrap();
215
216        let threshold = u32::try_from(
217            (self.votes.total_votes() * self.config.info.info_for_hf(&current).threshold())
218                .div_ceil(100),
219        )
220        .unwrap();
221
222        HardFork::VARIANTS
223            .iter()
224            .map(|hf| {
225                let info = self.config.info.info_for_hf(hf);
226                HardForkInfo {
227                    earliest_height: info.height() as u64,
228                    enabled: current >= *hf,
229                    state,
230                    threshold,
231                    version: hf.as_u8(),
232                    votes: u32::try_from(self.votes.votes_for_hf(hf)).unwrap(),
233                    voting,
234                    window,
235                }
236            })
237            .collect()
238    }
239
240    /// Returns the current hard-fork.
241    pub const fn current_hardfork(&self) -> HardFork {
242        self.current_hardfork
243    }
244}
245
246/// Returns the block votes for blocks in the specified range.
247#[instrument(name = "get_votes", skip(database))]
248async fn get_votes_in_range<D: Database>(
249    database: D,
250    block_heights: Range<usize>,
251    window_size: usize,
252) -> Result<HFVotes, ContextCacheError> {
253    let mut votes = HFVotes::new(window_size);
254
255    let BlockchainResponse::BlockExtendedHeaderInRange(vote_list) = database
256        .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
257            block_heights,
258            Chain::Main,
259        ))
260        .await?
261    else {
262        panic!("Database sent incorrect response!");
263    };
264
265    for hf_info in vote_list {
266        votes.add_vote_for_hf(&HardFork::from_vote(hf_info.vote));
267    }
268
269    Ok(votes)
270}