Skip to main content

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::{
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
28/// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_config.h#L75>
29const DYNAMIC_FEE_REFERENCE_TX_WEIGHT: u64 = 3_000;
30/// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_config.h#L193>
31const FEE_ROUNDING_PLACES: u32 = 2;
32/// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_config.h#L192>
33/// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/blockchain.h#L629>
34const FEE_QUANTIZATION_MASK: u64 = 10_000;
35
36use crate::{ContextCacheError, Database, HardFork};
37
38/// The short term block weight window.
39pub const SHORT_TERM_WINDOW: usize = 100;
40/// The long term block weight window.
41pub const LONG_TERM_WINDOW: usize = 100000;
42
43/// Configuration for the block weight cache.
44///
45#[derive(Debug, Clone, Copy, Eq, PartialEq)]
46pub struct BlockWeightsCacheConfig {
47    short_term_window: usize,
48    long_term_window: usize,
49}
50
51impl BlockWeightsCacheConfig {
52    /// Creates a new [`BlockWeightsCacheConfig`]
53    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    /// Returns the [`BlockWeightsCacheConfig`] for all networks (They are all the same as mainnet).
61    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/// A cache used to calculate block weight limits, the effective median and
70/// long term block weights.
71///
72/// These calculations require a lot of data from the database so by caching
73/// this data it reduces the load on the database.
74#[derive(Debug, Clone, Eq, PartialEq)]
75pub struct BlockWeightsCache {
76    /// The short term block weights.
77    short_term_block_weights: RollingMedian<usize>,
78    /// The long term block weights.
79    long_term_weights: RollingMedian<usize>,
80
81    /// The height of the top block.
82    pub(crate) tip_height: usize,
83
84    pub(crate) config: BlockWeightsCacheConfig,
85}
86
87impl BlockWeightsCache {
88    /// Initialize the [`BlockWeightsCache`] at the the given chain height.
89    #[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    /// Pop some blocks from the top of the cache.
129    ///
130    /// The cache will be returned to the state it would have been in `numb_blocks` ago.
131    #[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            // More blocks to pop than we have in the cache, so just restart a new cache.
139            *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                // We don't need to handle the case where this is above the top block like with the
158                // short term cache as we check at the top of this function and just create a new cache.
159                (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                    // the smallest between ...
172                    min(
173                        // the blocks we already have in the cache.
174                        chain_height - self.short_term_block_weights.window_len(),
175                        // the new chain height.
176                        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    /// Add a new block to the cache.
198    ///
199    /// The `block_height` **MUST** be one more than the last height the cache has
200    /// seen.
201    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    /// Returns the median long term weight over the last [`LONG_TERM_WINDOW`] blocks, or custom amount of blocks in the config.
217    pub fn median_long_term_weight(&self) -> usize {
218        self.long_term_weights.median()
219    }
220
221    /// Returns the median weight over the last [`SHORT_TERM_WINDOW`] blocks, or custom amount of blocks in the config.
222    pub fn median_short_term_weight(&self) -> usize {
223        self.short_term_block_weights.median()
224    }
225
226    /// Returns the effective median weight, used for block reward calculations and to calculate
227    /// the block weight limit.
228    ///
229    /// See: <https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#calculating-effective-median-weight>
230    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    /// Returns the median weight used to calculate block reward punishment.
239    ///
240    /// <https://cuprate.github.io/monero-book/consensus_rules/blocks/reward.html#calculating-block-reward>
241    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    /// Computes the 2021 fee estimates.
251    ///
252    /// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/blockchain.cpp#L3751>
253    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        /// Round an amount up to `significant_digits` significant decimal digits.
260        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
311/// Calculates the effective median with the long term and short term median.
312fn 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
338/// Calculates a block's long term weight.
339pub 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/// Gets the block weights from the blocks with heights in the range provided.
363#[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/// Gets the block long term weights from the blocks with heights in the range provided.
387#[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}