cuprate_consensus_context/
hardforks.rs

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