Skip to main content

cuprate_consensus_context/
distribution.rs

1//! Output Distribution Module
2//!
3//! This module handles keeping track of the data required to serve the output distribution.
4//! This data is currently the cumulative number of RCT outputs in each block.
5//!
6use std::{num::NonZero, ops::Range};
7
8use tower::ServiceExt;
9use tracing::instrument;
10
11use cuprate_helper::cast::{u64_to_usize, usize_to_u64};
12use cuprate_types::{
13    blockchain::{BlockchainReadRequest, BlockchainResponse},
14    rpc::OutputDistributionData,
15    VerifiedBlockInformation,
16};
17
18use crate::{hardforks::HardForkConfig, ContextCacheError, Database, HardFork};
19
20/// A cache of the cumulative RCT output count per block.
21#[derive(Debug, Clone, Eq, PartialEq)]
22pub struct CumulativeRctOutsCache {
23    /// The HF v4 activation height.
24    pub start_height: usize,
25    /// The cumulative RCT output count for blocks `start_height..chain_height`.
26    pub cumulative_rct_outs: Vec<u64>,
27}
28
29impl CumulativeRctOutsCache {
30    /// Initialize the RCT outs cache from the specified chain height.
31    #[instrument(
32        name = "init_rct_outs_cache",
33        level = "info",
34        skip(hard_fork_cfg, database)
35    )]
36    pub async fn init_from_chain_height<D: Database + Clone>(
37        chain_height: usize,
38        hard_fork_cfg: HardForkConfig,
39        database: D,
40    ) -> Result<Self, ContextCacheError> {
41        tracing::info!("Initializing output distribution cache, this may take a while.");
42
43        // The first height an RCT output can appear at is the HF v4 activation height.
44        let rct_start_height = hard_fork_cfg.info.info_for_hf(&HardFork::V4).height();
45
46        let cumulative_rct_outs = if rct_start_height < chain_height {
47            get_cumulative_rct_outs(database, rct_start_height..chain_height).await?
48        } else {
49            Vec::new()
50        };
51
52        Ok(Self {
53            start_height: rct_start_height,
54            cumulative_rct_outs,
55        })
56    }
57
58    /// Add a new block to the RCT outs cache.
59    pub fn new_block(&mut self, height: usize, numb_rct_outputs: usize) {
60        if height < self.start_height {
61            debug_assert_eq!(numb_rct_outputs, 0);
62            return;
63        }
64        assert_eq!(self.start_height + self.cumulative_rct_outs.len(), height);
65
66        let last = self.cumulative_rct_outs.last().copied().unwrap_or(0);
67        self.cumulative_rct_outs
68            .push(last + usize_to_u64(numb_rct_outputs));
69    }
70
71    /// Pop some blocks from the top of the cache.
72    pub fn pop_blocks_main_chain(&mut self, numb_blocks: usize) {
73        self.cumulative_rct_outs
74            .truncate(self.cumulative_rct_outs.len().saturating_sub(numb_blocks));
75    }
76
77    /// Returns the RCT output distribution for the request.
78    pub fn distribution(
79        &self,
80        from_height: u64,
81        to_height: Option<NonZero<u64>>,
82        cumulative: bool,
83        chain_height: usize,
84    ) -> Result<OutputDistributionData, tower::BoxError> {
85        if to_height.is_some_and(|h| h.get() < from_height) {
86            return Err("`to_height` is below `from_height`".into());
87        }
88
89        let to_height = to_height.map_or(chain_height - 1, |h| {
90            let h = h.get();
91            u64_to_usize(h)
92        });
93
94        if to_height >= chain_height {
95            return Err("`to_height` is above the chain height".into());
96        }
97
98        // clamp the start to the start of RCT, like monerod.
99        let start_height = u64_to_usize(from_height).max(self.start_height);
100
101        if start_height > to_height {
102            return Ok(OutputDistributionData {
103                amount: 0,
104                distribution: Vec::new(),
105                start_height: usize_to_u64(start_height),
106                base: 0,
107            });
108        }
109
110        let idx = |height: usize| height - self.start_height;
111
112        // The value one block below the range, the base to calculate real values from
113        // cumulative ones.
114        let base = if start_height <= self.start_height {
115            0
116        } else {
117            self.cumulative_rct_outs[idx(start_height - 1)]
118        };
119
120        let mut distribution =
121            self.cumulative_rct_outs[idx(start_height)..=idx(to_height)].to_vec();
122
123        if !cumulative {
124            let mut prev = base;
125            for cumulative_outs in &mut distribution {
126                let delta = *cumulative_outs - prev;
127                prev = *cumulative_outs;
128                *cumulative_outs = delta;
129            }
130        }
131
132        Ok(OutputDistributionData {
133            amount: 0,
134            distribution,
135            start_height: usize_to_u64(start_height),
136            base,
137        })
138    }
139}
140
141/// The number of RCT outputs in a block.
142pub fn rct_output_count(block: &VerifiedBlockInformation) -> usize {
143    let miner_tx = block.block.miner_transaction();
144    let miner_tx_outputs = if miner_tx.version() == 2 {
145        miner_tx.prefix().outputs.len()
146    } else {
147        0
148    };
149
150    miner_tx_outputs
151        + block
152            .txs
153            .iter()
154            .filter(|tx| tx.tx.version() == 2)
155            .map(|tx| tx.tx.prefix().outputs.len())
156            .sum::<usize>()
157}
158
159/// Returns the cumulative RCT output count for the main-chain blocks with heights in the specified range.
160#[instrument(name = "get_cumulative_rct_outs", skip(database), level = "info")]
161async fn get_cumulative_rct_outs<D: Database>(
162    database: D,
163    block_heights: Range<usize>,
164) -> Result<Vec<u64>, ContextCacheError> {
165    let BlockchainResponse::CumulativeRctOutsInRange(cumulative_rct_outs) = database
166        .oneshot(BlockchainReadRequest::CumulativeRctOutsInRange(
167            block_heights,
168        ))
169        .await?
170    else {
171        panic!("Database sent incorrect response");
172    };
173
174    Ok(cumulative_rct_outs)
175}