cuprate_consensus_context/
weight.rs

1//! # Block Weights
2//!
3//! This module contains calculations for block weights, including calculating block weight
4//! limits, effective medians and long term block weights.
5//!
6//! For more information please see the [block weights chapter](https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html)
7//! in the Monero Book.
8//!
9use std::{
10    cmp::{max, min},
11    ops::Range,
12};
13
14use tower::ServiceExt;
15use tracing::instrument;
16
17use cuprate_consensus_rules::blocks::{penalty_free_zone, PENALTY_FREE_ZONE_5};
18use cuprate_helper::{asynch::rayon_spawn_async, num::RollingMedian};
19use cuprate_types::{
20    blockchain::{BlockchainReadRequest, BlockchainResponse},
21    Chain,
22};
23
24use crate::{ContextCacheError, Database, HardFork};
25
26/// The short term block weight window.
27pub const SHORT_TERM_WINDOW: usize = 100;
28/// The long term block weight window.
29pub const LONG_TERM_WINDOW: usize = 100000;
30
31/// Configuration for the block weight cache.
32///
33#[derive(Debug, Clone, Copy, Eq, PartialEq)]
34pub struct BlockWeightsCacheConfig {
35    short_term_window: usize,
36    long_term_window: usize,
37}
38
39impl BlockWeightsCacheConfig {
40    /// Creates a new [`BlockWeightsCacheConfig`]
41    pub const fn new(short_term_window: usize, long_term_window: usize) -> Self {
42        Self {
43            short_term_window,
44            long_term_window,
45        }
46    }
47
48    /// Returns the [`BlockWeightsCacheConfig`] for all networks (They are all the same as mainnet).
49    pub const fn main_net() -> Self {
50        Self {
51            short_term_window: SHORT_TERM_WINDOW,
52            long_term_window: LONG_TERM_WINDOW,
53        }
54    }
55}
56
57/// A cache used to calculate block weight limits, the effective median and
58/// long term block weights.
59///
60/// These calculations require a lot of data from the database so by caching
61/// this data it reduces the load on the database.
62#[derive(Debug, Clone, Eq, PartialEq)]
63pub struct BlockWeightsCache {
64    /// The short term block weights.
65    short_term_block_weights: RollingMedian<usize>,
66    /// The long term block weights.
67    long_term_weights: RollingMedian<usize>,
68
69    /// The height of the top block.
70    pub(crate) tip_height: usize,
71
72    pub(crate) config: BlockWeightsCacheConfig,
73}
74
75impl BlockWeightsCache {
76    /// Initialize the [`BlockWeightsCache`] at the the given chain height.
77    #[instrument(name = "init_weight_cache", level = "info", skip(database, config))]
78    pub async fn init_from_chain_height<D: Database + Clone>(
79        chain_height: usize,
80        config: BlockWeightsCacheConfig,
81        database: D,
82        chain: Chain,
83    ) -> Result<Self, ContextCacheError> {
84        tracing::info!("Initializing weight cache this may take a while.");
85
86        let long_term_weights = get_long_term_weight_in_range(
87            chain_height.saturating_sub(config.long_term_window)..chain_height,
88            database.clone(),
89            chain,
90        )
91        .await?;
92
93        let short_term_block_weights = get_blocks_weight_in_range(
94            chain_height.saturating_sub(config.short_term_window)..chain_height,
95            database,
96            chain,
97        )
98        .await?;
99
100        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());
101
102        Ok(Self {
103            short_term_block_weights: rayon_spawn_async(move || {
104                RollingMedian::from_vec(short_term_block_weights, config.short_term_window)
105            })
106            .await,
107            long_term_weights: rayon_spawn_async(move || {
108                RollingMedian::from_vec(long_term_weights, config.long_term_window)
109            })
110            .await,
111            tip_height: chain_height - 1,
112            config,
113        })
114    }
115
116    /// Pop some blocks from the top of the cache.
117    ///
118    /// The cache will be returned to the state it would have been in `numb_blocks` ago.
119    #[instrument(name = "pop_blocks_weight_cache", skip_all, fields(numb_blocks = numb_blocks))]
120    pub async fn pop_blocks_main_chain<D: Database + Clone>(
121        &mut self,
122        numb_blocks: usize,
123        database: D,
124    ) -> Result<(), ContextCacheError> {
125        if self.long_term_weights.window_len() <= numb_blocks {
126            // More blocks to pop than we have in the cache, so just restart a new cache.
127            *self = Self::init_from_chain_height(
128                self.tip_height - numb_blocks + 1,
129                self.config,
130                database,
131                Chain::Main,
132            )
133            .await?;
134
135            return Ok(());
136        }
137
138        let chain_height = self.tip_height + 1;
139
140        let new_long_term_start_height = chain_height
141            .saturating_sub(self.config.long_term_window)
142            .saturating_sub(numb_blocks);
143
144        let old_long_term_weights = get_long_term_weight_in_range(
145            new_long_term_start_height
146                // current_chain_height - self.long_term_weights.len() blocks are already in the cache.
147                ..(chain_height - self.long_term_weights.window_len()),
148            database.clone(),
149            Chain::Main,
150        )
151        .await?;
152
153        let new_short_term_start_height = chain_height
154            .saturating_sub(self.config.short_term_window)
155            .saturating_sub(numb_blocks);
156
157        let old_short_term_weights = get_blocks_weight_in_range(
158            new_short_term_start_height
159                // current_chain_height - self.long_term_weights.len() blocks are already in the cache.
160                ..(chain_height - self.short_term_block_weights.window_len()),
161            database,
162            Chain::Main,
163        )
164        .await?;
165
166        for _ in 0..numb_blocks {
167            self.short_term_block_weights.pop_back();
168            self.long_term_weights.pop_back();
169        }
170
171        self.long_term_weights.append_front(old_long_term_weights);
172        self.short_term_block_weights
173            .append_front(old_short_term_weights);
174        self.tip_height -= numb_blocks;
175
176        Ok(())
177    }
178
179    /// Add a new block to the cache.
180    ///
181    /// The `block_height` **MUST** be one more than the last height the cache has
182    /// seen.
183    pub fn new_block(&mut self, block_height: usize, block_weight: usize, long_term_weight: usize) {
184        assert_eq!(self.tip_height + 1, block_height);
185        self.tip_height += 1;
186        tracing::debug!(
187            "Adding new block's {} weights to block cache, weight: {}, long term weight: {}",
188            self.tip_height,
189            block_weight,
190            long_term_weight
191        );
192
193        self.long_term_weights.push(long_term_weight);
194
195        self.short_term_block_weights.push(block_weight);
196    }
197
198    /// Returns the median long term weight over the last [`LONG_TERM_WINDOW`] blocks, or custom amount of blocks in the config.
199    pub fn median_long_term_weight(&self) -> usize {
200        self.long_term_weights.median()
201    }
202
203    /// Returns the median weight over the last [`SHORT_TERM_WINDOW`] blocks, or custom amount of blocks in the config.
204    pub fn median_short_term_weight(&self) -> usize {
205        self.short_term_block_weights.median()
206    }
207
208    /// Returns the effective median weight, used for block reward calculations and to calculate
209    /// the block weight limit.
210    ///
211    /// See: <https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#calculating-effective-median-weight>
212    pub fn effective_median_block_weight(&self, hf: HardFork) -> usize {
213        calculate_effective_median_block_weight(
214            hf,
215            self.median_short_term_weight(),
216            self.median_long_term_weight(),
217        )
218    }
219
220    /// Returns the median weight used to calculate block reward punishment.
221    ///
222    /// <https://cuprate.github.io/monero-book/consensus_rules/blocks/reward.html#calculating-block-reward>
223    pub fn median_for_block_reward(&self, hf: HardFork) -> usize {
224        if hf < HardFork::V12 {
225            self.median_short_term_weight()
226        } else {
227            self.effective_median_block_weight(hf)
228        }
229        .max(penalty_free_zone(hf))
230    }
231}
232
233/// Calculates the effective median with the long term and short term median.
234fn calculate_effective_median_block_weight(
235    hf: HardFork,
236    median_short_term_weight: usize,
237    median_long_term_weight: usize,
238) -> usize {
239    if hf < HardFork::V10 {
240        return median_short_term_weight.max(penalty_free_zone(hf));
241    }
242
243    let long_term_median = median_long_term_weight.max(PENALTY_FREE_ZONE_5);
244    let short_term_median = median_short_term_weight;
245    let effective_median = if hf >= HardFork::V10 && hf < HardFork::V15 {
246        min(
247            max(PENALTY_FREE_ZONE_5, short_term_median),
248            50 * long_term_median,
249        )
250    } else {
251        min(
252            max(long_term_median, short_term_median),
253            50 * long_term_median,
254        )
255    };
256
257    effective_median.max(penalty_free_zone(hf))
258}
259
260/// Calculates a blocks long term weight.
261pub fn calculate_block_long_term_weight(
262    hf: HardFork,
263    block_weight: usize,
264    long_term_median: usize,
265) -> usize {
266    if hf < HardFork::V10 {
267        return block_weight;
268    }
269
270    let long_term_median = max(penalty_free_zone(hf), long_term_median);
271
272    let (short_term_constraint, adjusted_block_weight) =
273        if hf >= HardFork::V10 && hf < HardFork::V15 {
274            let stc = long_term_median + long_term_median * 2 / 5;
275            (stc, block_weight)
276        } else {
277            let stc = long_term_median + long_term_median * 7 / 10;
278            (stc, max(block_weight, long_term_median * 10 / 17))
279        };
280
281    min(short_term_constraint, adjusted_block_weight)
282}
283
284/// Gets the block weights from the blocks with heights in the range provided.
285#[instrument(name = "get_block_weights", skip(database))]
286async fn get_blocks_weight_in_range<D: Database + Clone>(
287    range: Range<usize>,
288    database: D,
289    chain: Chain,
290) -> Result<Vec<usize>, ContextCacheError> {
291    tracing::info!("getting block weights.");
292
293    let BlockchainResponse::BlockExtendedHeaderInRange(ext_headers) = database
294        .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
295            range, chain,
296        ))
297        .await?
298    else {
299        panic!("Database sent incorrect response!")
300    };
301
302    Ok(ext_headers
303        .into_iter()
304        .map(|info| info.block_weight)
305        .collect())
306}
307
308/// Gets the block long term weights from the blocks with heights in the range provided.
309#[instrument(name = "get_long_term_weights", skip(database), level = "info")]
310async fn get_long_term_weight_in_range<D: Database + Clone>(
311    range: Range<usize>,
312    database: D,
313    chain: Chain,
314) -> Result<Vec<usize>, ContextCacheError> {
315    tracing::info!("getting block long term weights.");
316
317    let BlockchainResponse::BlockExtendedHeaderInRange(ext_headers) = database
318        .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
319            range, chain,
320        ))
321        .await?
322    else {
323        panic!("Database sent incorrect response!")
324    };
325
326    Ok(ext_headers
327        .into_iter()
328        .map(|info| info.long_term_weight)
329        .collect())
330}