cuprate_consensus_context/
hardforks.rs1use 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
17const DEFAULT_WINDOW_SIZE: usize = 10080; #[derive(Debug, Clone, Copy, Eq, PartialEq)]
25pub struct HardForkConfig {
26 pub info: HFsInfo,
28 pub window: usize,
30}
31
32impl HardForkConfig {
33 pub const fn main_net() -> Self {
35 Self {
36 info: HFsInfo::main_net(),
37 window: DEFAULT_WINDOW_SIZE,
38 }
39 }
40
41 pub const fn stage_net() -> Self {
43 Self {
44 info: HFsInfo::stage_net(),
45 window: DEFAULT_WINDOW_SIZE,
46 }
47 }
48
49 pub const fn test_net() -> Self {
51 Self {
52 info: HFsInfo::test_net(),
53 window: DEFAULT_WINDOW_SIZE,
54 }
55 }
56
57 pub const fn fake_chain() -> Self {
59 Self {
60 info: HFsInfo::fake_chain(),
61 window: DEFAULT_WINDOW_SIZE,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Eq, PartialEq)]
68pub struct HardForkState {
69 pub current_hardfork: HardFork,
71
72 pub config: HardForkConfig,
74 pub votes: HFVotes,
76
77 pub last_height: usize,
79}
80
81impl HardForkState {
82 #[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 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 pub fn new_block(&mut self, vote: HardFork, height: usize) {
174 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 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 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 pub fn hardfork_infos(&self) -> Vec<HardForkInfo> {
209 let current = self.current_hardfork;
210 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(¤t).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 pub const fn current_hardfork(&self) -> HardFork {
242 self.current_hardfork
243 }
244}
245
246#[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}