cuprate_consensus_context/
weight.rs1use std::{
10 cmp::{max, min},
11 ops::Range,
12};
13
14use tower::ServiceExt;
15use tracing::instrument;
16
17use cuprate_consensus_rules::{
18 blocks::{penalty_free_zone, PENALTY_FREE_ZONE_5},
19 miner_tx::calculate_block_reward,
20};
21use cuprate_helper::{asynch::rayon_spawn_async, num::RollingMedian};
22use cuprate_types::{
23 blockchain::{BlockchainReadRequest, BlockchainResponse},
24 rpc::FeeEstimate,
25 Chain,
26};
27
28const DYNAMIC_FEE_REFERENCE_TX_WEIGHT: u64 = 3_000;
30const FEE_ROUNDING_PLACES: u32 = 2;
32const FEE_QUANTIZATION_MASK: u64 = 10_000;
35
36use crate::{ContextCacheError, Database, HardFork};
37
38pub const SHORT_TERM_WINDOW: usize = 100;
40pub const LONG_TERM_WINDOW: usize = 100000;
42
43#[derive(Debug, Clone, Copy, Eq, PartialEq)]
46pub struct BlockWeightsCacheConfig {
47 short_term_window: usize,
48 long_term_window: usize,
49}
50
51impl BlockWeightsCacheConfig {
52 pub const fn new(short_term_window: usize, long_term_window: usize) -> Self {
54 Self {
55 short_term_window,
56 long_term_window,
57 }
58 }
59
60 pub const fn main_net() -> Self {
62 Self {
63 short_term_window: SHORT_TERM_WINDOW,
64 long_term_window: LONG_TERM_WINDOW,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Eq, PartialEq)]
75pub struct BlockWeightsCache {
76 short_term_block_weights: RollingMedian<usize>,
78 long_term_weights: RollingMedian<usize>,
80
81 pub(crate) tip_height: usize,
83
84 pub(crate) config: BlockWeightsCacheConfig,
85}
86
87impl BlockWeightsCache {
88 #[instrument(name = "init_weight_cache", level = "info", skip(database, config))]
90 pub async fn init_from_chain_height<D: Database + Clone>(
91 chain_height: usize,
92 config: BlockWeightsCacheConfig,
93 database: D,
94 chain: Chain,
95 ) -> Result<Self, ContextCacheError> {
96 tracing::info!("Initializing weight cache this may take a while.");
97
98 let long_term_weights = get_long_term_weight_in_range(
99 chain_height.saturating_sub(config.long_term_window)..chain_height,
100 database.clone(),
101 chain,
102 )
103 .await?;
104
105 let short_term_block_weights = get_blocks_weight_in_range(
106 chain_height.saturating_sub(config.short_term_window)..chain_height,
107 database,
108 chain,
109 )
110 .await?;
111
112 tracing::info!("Initialized block weight cache, chain-height: {:?}, long term weights length: {:?}, short term weights length: {:?}", chain_height, long_term_weights.len(), short_term_block_weights.len());
113
114 Ok(Self {
115 short_term_block_weights: rayon_spawn_async(move || {
116 RollingMedian::from_vec(short_term_block_weights, config.short_term_window)
117 })
118 .await,
119 long_term_weights: rayon_spawn_async(move || {
120 RollingMedian::from_vec(long_term_weights, config.long_term_window)
121 })
122 .await,
123 tip_height: chain_height - 1,
124 config,
125 })
126 }
127
128 #[instrument(name = "pop_blocks_weight_cache", skip_all, fields(numb_blocks = numb_blocks))]
132 pub async fn pop_blocks_main_chain<D: Database + Clone>(
133 &mut self,
134 numb_blocks: usize,
135 database: D,
136 ) -> Result<(), ContextCacheError> {
137 if self.long_term_weights.window_len() <= numb_blocks {
138 *self = Self::init_from_chain_height(
140 self.tip_height - numb_blocks + 1,
141 self.config,
142 database,
143 Chain::Main,
144 )
145 .await?;
146
147 return Ok(());
148 }
149
150 let chain_height = self.tip_height + 1;
151
152 let new_long_term_start_height =
153 chain_height.saturating_sub(self.config.long_term_window + numb_blocks);
154
155 let old_long_term_weights = get_long_term_weight_in_range(
156 new_long_term_start_height..
157 (chain_height - self.long_term_weights.window_len()),
160 database.clone(),
161 Chain::Main,
162 )
163 .await?;
164
165 let new_short_term_start_height =
166 chain_height.saturating_sub(self.config.short_term_window + numb_blocks);
167
168 let old_short_term_weights = get_blocks_weight_in_range(
169 new_short_term_start_height
170 ..(
171 min(
173 chain_height - self.short_term_block_weights.window_len(),
175 chain_height - numb_blocks,
177 )
178 ),
179 database,
180 Chain::Main,
181 )
182 .await?;
183
184 for _ in 0..numb_blocks {
185 self.short_term_block_weights.pop_back();
186 self.long_term_weights.pop_back();
187 }
188
189 self.long_term_weights.append_front(old_long_term_weights);
190 self.short_term_block_weights
191 .append_front(old_short_term_weights);
192 self.tip_height -= numb_blocks;
193
194 Ok(())
195 }
196
197 pub fn new_block(&mut self, block_height: usize, block_weight: usize, long_term_weight: usize) {
202 assert_eq!(self.tip_height + 1, block_height);
203 self.tip_height += 1;
204 tracing::debug!(
205 "Adding new block's {} weights to block cache, weight: {}, long term weight: {}",
206 self.tip_height,
207 block_weight,
208 long_term_weight
209 );
210
211 self.long_term_weights.push(long_term_weight);
212
213 self.short_term_block_weights.push(block_weight);
214 }
215
216 pub fn median_long_term_weight(&self) -> usize {
218 self.long_term_weights.median()
219 }
220
221 pub fn median_short_term_weight(&self) -> usize {
223 self.short_term_block_weights.median()
224 }
225
226 pub fn effective_median_block_weight(&self, hf: HardFork) -> usize {
231 calculate_effective_median_block_weight(
232 hf,
233 self.median_short_term_weight(),
234 self.median_long_term_weight(),
235 )
236 }
237
238 pub fn median_for_block_reward(&self, hf: HardFork) -> usize {
242 if hf < HardFork::V12 {
243 self.median_short_term_weight()
244 } else {
245 self.effective_median_block_weight(hf)
246 }
247 .max(penalty_free_zone(hf))
248 }
249
250 pub fn fee_estimate_2021(
254 &self,
255 grace_blocks: u64,
256 hf: HardFork,
257 already_generated_coins: u64,
258 ) -> Result<FeeEstimate, tower::BoxError> {
259 const fn round_money_up(amount: u64, significant_digits: u32) -> u64 {
261 if amount == 0 {
262 return 0;
263 }
264 let digits = amount.ilog10() + 1;
265 if digits <= significant_digits {
266 return amount;
267 }
268 let scale = 10_u64.pow(digits - significant_digits);
269 amount.div_ceil(scale) * scale
270 }
271
272 let grace = usize::try_from(grace_blocks).unwrap_or(usize::MAX);
273
274 if grace > SHORT_TERM_WINDOW {
275 return Err("Amount of grace blocks exceeds SHORT_TERM_WINDOW".into());
276 }
277
278 let mlw = max(
279 self.long_term_weights.median_with_grace(grace),
280 PENALTY_FREE_ZONE_5,
281 );
282
283 let msw = max(self.short_term_block_weights.median_with_grace(grace), mlw);
284
285 let mnw = min(msw, 50 * mlw);
286
287 let base_reward = calculate_block_reward(1, mlw, already_generated_coins, hf);
288
289 let mfw = min(mnw, mlw);
290
291 let fl = base_reward * DYNAMIC_FEE_REFERENCE_TX_WEIGHT / (mfw * mfw) as u64;
292 let fn_ = 4 * base_reward * DYNAMIC_FEE_REFERENCE_TX_WEIGHT / (mfw * mfw) as u64;
293 let fm =
294 16 * base_reward * DYNAMIC_FEE_REFERENCE_TX_WEIGHT / (PENALTY_FREE_ZONE_5 * mfw) as u64;
295 let fh = max(
296 4 * fm,
297 4 * fm * mfw as u64
298 / (32 * DYNAMIC_FEE_REFERENCE_TX_WEIGHT * mnw as u64 / PENALTY_FREE_ZONE_5 as u64),
299 );
300
301 let fees = [fl, fn_, fm, fh].map(|f| round_money_up(f, FEE_ROUNDING_PLACES));
302
303 Ok(FeeEstimate {
304 fee: fees[0],
305 fees: fees.to_vec(),
306 quantization_mask: FEE_QUANTIZATION_MASK,
307 })
308 }
309}
310
311fn calculate_effective_median_block_weight(
313 hf: HardFork,
314 median_short_term_weight: usize,
315 median_long_term_weight: usize,
316) -> usize {
317 if hf < HardFork::V10 {
318 return median_short_term_weight.max(penalty_free_zone(hf));
319 }
320
321 let long_term_median = median_long_term_weight.max(PENALTY_FREE_ZONE_5);
322 let short_term_median = median_short_term_weight;
323 let effective_median = if hf >= HardFork::V10 && hf < HardFork::V15 {
324 min(
325 max(PENALTY_FREE_ZONE_5, short_term_median),
326 50 * long_term_median,
327 )
328 } else {
329 min(
330 max(long_term_median, short_term_median),
331 50 * long_term_median,
332 )
333 };
334
335 effective_median.max(penalty_free_zone(hf))
336}
337
338pub fn calculate_block_long_term_weight(
340 hf: HardFork,
341 block_weight: usize,
342 long_term_median: usize,
343) -> usize {
344 if hf < HardFork::V10 {
345 return block_weight;
346 }
347
348 let long_term_median = max(penalty_free_zone(hf), long_term_median);
349
350 let (short_term_constraint, adjusted_block_weight) =
351 if hf >= HardFork::V10 && hf < HardFork::V15 {
352 let stc = long_term_median + long_term_median * 2 / 5;
353 (stc, block_weight)
354 } else {
355 let stc = long_term_median + long_term_median * 7 / 10;
356 (stc, max(block_weight, long_term_median * 10 / 17))
357 };
358
359 min(short_term_constraint, adjusted_block_weight)
360}
361
362#[instrument(name = "get_block_weights", skip(database))]
364async fn get_blocks_weight_in_range<D: Database + Clone>(
365 range: Range<usize>,
366 database: D,
367 chain: Chain,
368) -> Result<Vec<usize>, ContextCacheError> {
369 tracing::info!("getting block weights.");
370
371 let BlockchainResponse::BlockExtendedHeaderInRange(ext_headers) = database
372 .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
373 range, chain,
374 ))
375 .await?
376 else {
377 panic!("Database sent incorrect response!")
378 };
379
380 Ok(ext_headers
381 .into_iter()
382 .map(|info| info.block_weight)
383 .collect())
384}
385
386#[instrument(name = "get_long_term_weights", skip(database), level = "info")]
388async fn get_long_term_weight_in_range<D: Database + Clone>(
389 range: Range<usize>,
390 database: D,
391 chain: Chain,
392) -> Result<Vec<usize>, ContextCacheError> {
393 tracing::info!("getting block long term weights.");
394
395 let BlockchainResponse::BlockExtendedHeaderInRange(ext_headers) = database
396 .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
397 range, chain,
398 ))
399 .await?
400 else {
401 panic!("Database sent incorrect response!")
402 };
403
404 Ok(ext_headers
405 .into_iter()
406 .map(|info| info.long_term_weight)
407 .collect())
408}