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 =
141            chain_height.saturating_sub(self.config.long_term_window + numb_blocks);
142
143        let old_long_term_weights = get_long_term_weight_in_range(
144            new_long_term_start_height..
145                // We don't need to handle the case where this is above the top block like with the
146                // short term cache as we check at the top of this function and just create a new 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 =
154            chain_height.saturating_sub(self.config.short_term_window + numb_blocks);
155
156        let old_short_term_weights = get_blocks_weight_in_range(
157            new_short_term_start_height
158                ..(
159                    // the smallest between ...
160                    min(
161                        // the blocks we already have in the cache.
162                        chain_height - self.short_term_block_weights.window_len(),
163                        // the new chain height.
164                        chain_height - numb_blocks,
165                    )
166                ),
167            database,
168            Chain::Main,
169        )
170        .await?;
171
172        for _ in 0..numb_blocks {
173            self.short_term_block_weights.pop_back();
174            self.long_term_weights.pop_back();
175        }
176
177        self.long_term_weights.append_front(old_long_term_weights);
178        self.short_term_block_weights
179            .append_front(old_short_term_weights);
180        self.tip_height -= numb_blocks;
181
182        Ok(())
183    }
184
185    /// Add a new block to the cache.
186    ///
187    /// The `block_height` **MUST** be one more than the last height the cache has
188    /// seen.
189    pub fn new_block(&mut self, block_height: usize, block_weight: usize, long_term_weight: usize) {
190        assert_eq!(self.tip_height + 1, block_height);
191        self.tip_height += 1;
192        tracing::debug!(
193            "Adding new block's {} weights to block cache, weight: {}, long term weight: {}",
194            self.tip_height,
195            block_weight,
196            long_term_weight
197        );
198
199        self.long_term_weights.push(long_term_weight);
200
201        self.short_term_block_weights.push(block_weight);
202    }
203
204    /// Returns the median long term weight over the last [`LONG_TERM_WINDOW`] blocks, or custom amount of blocks in the config.
205    pub fn median_long_term_weight(&self) -> usize {
206        self.long_term_weights.median()
207    }
208
209    /// Returns the median weight over the last [`SHORT_TERM_WINDOW`] blocks, or custom amount of blocks in the config.
210    pub fn median_short_term_weight(&self) -> usize {
211        self.short_term_block_weights.median()
212    }
213
214    /// Returns the effective median weight, used for block reward calculations and to calculate
215    /// the block weight limit.
216    ///
217    /// See: <https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#calculating-effective-median-weight>
218    pub fn effective_median_block_weight(&self, hf: HardFork) -> usize {
219        calculate_effective_median_block_weight(
220            hf,
221            self.median_short_term_weight(),
222            self.median_long_term_weight(),
223        )
224    }
225
226    /// Returns the median weight used to calculate block reward punishment.
227    ///
228    /// <https://cuprate.github.io/monero-book/consensus_rules/blocks/reward.html#calculating-block-reward>
229    pub fn median_for_block_reward(&self, hf: HardFork) -> usize {
230        if hf < HardFork::V12 {
231            self.median_short_term_weight()
232        } else {
233            self.effective_median_block_weight(hf)
234        }
235        .max(penalty_free_zone(hf))
236    }
237}
238
239/// Calculates the effective median with the long term and short term median.
240fn calculate_effective_median_block_weight(
241    hf: HardFork,
242    median_short_term_weight: usize,
243    median_long_term_weight: usize,
244) -> usize {
245    if hf < HardFork::V10 {
246        return median_short_term_weight.max(penalty_free_zone(hf));
247    }
248
249    let long_term_median = median_long_term_weight.max(PENALTY_FREE_ZONE_5);
250    let short_term_median = median_short_term_weight;
251    let effective_median = if hf >= HardFork::V10 && hf < HardFork::V15 {
252        min(
253            max(PENALTY_FREE_ZONE_5, short_term_median),
254            50 * long_term_median,
255        )
256    } else {
257        min(
258            max(long_term_median, short_term_median),
259            50 * long_term_median,
260        )
261    };
262
263    effective_median.max(penalty_free_zone(hf))
264}
265
266/// Calculates a blocks long term weight.
267pub fn calculate_block_long_term_weight(
268    hf: HardFork,
269    block_weight: usize,
270    long_term_median: usize,
271) -> usize {
272    if hf < HardFork::V10 {
273        return block_weight;
274    }
275
276    let long_term_median = max(penalty_free_zone(hf), long_term_median);
277
278    let (short_term_constraint, adjusted_block_weight) =
279        if hf >= HardFork::V10 && hf < HardFork::V15 {
280            let stc = long_term_median + long_term_median * 2 / 5;
281            (stc, block_weight)
282        } else {
283            let stc = long_term_median + long_term_median * 7 / 10;
284            (stc, max(block_weight, long_term_median * 10 / 17))
285        };
286
287    min(short_term_constraint, adjusted_block_weight)
288}
289
290/// Gets the block weights from the blocks with heights in the range provided.
291#[instrument(name = "get_block_weights", skip(database))]
292async fn get_blocks_weight_in_range<D: Database + Clone>(
293    range: Range<usize>,
294    database: D,
295    chain: Chain,
296) -> Result<Vec<usize>, ContextCacheError> {
297    tracing::info!("getting block weights.");
298
299    let BlockchainResponse::BlockExtendedHeaderInRange(ext_headers) = database
300        .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
301            range, chain,
302        ))
303        .await?
304    else {
305        panic!("Database sent incorrect response!")
306    };
307
308    Ok(ext_headers
309        .into_iter()
310        .map(|info| info.block_weight)
311        .collect())
312}
313
314/// Gets the block long term weights from the blocks with heights in the range provided.
315#[instrument(name = "get_long_term_weights", skip(database), level = "info")]
316async fn get_long_term_weight_in_range<D: Database + Clone>(
317    range: Range<usize>,
318    database: D,
319    chain: Chain,
320) -> Result<Vec<usize>, ContextCacheError> {
321    tracing::info!("getting block long term weights.");
322
323    let BlockchainResponse::BlockExtendedHeaderInRange(ext_headers) = database
324        .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
325            range, chain,
326        ))
327        .await?
328    else {
329        panic!("Database sent incorrect response!")
330    };
331
332    Ok(ext_headers
333        .into_iter()
334        .map(|info| info.long_term_weight)
335        .collect())
336}