cuprate_blockchain/ops/
blockchain.rs

1//! Blockchain functions - chain height, generated coins, etc.
2
3//---------------------------------------------------------------------------------------------------- Import
4use cuprate_database::{DatabaseRo, DbResult, RuntimeError};
5
6use crate::{
7    ops::{block, macros::doc_error},
8    tables::{AltBlockHeights, BlockHeights, BlockInfos},
9    types::{BlockHash, BlockHeight},
10};
11
12//---------------------------------------------------------------------------------------------------- Free Functions
13/// Retrieve the height of the chain.
14///
15/// This returns the chain-tip, not the [`top_block_height`].
16///
17/// For example:
18/// - The blockchain has 0 blocks => this returns `0`
19/// - The blockchain has 1 block (height 0) => this returns `1`
20/// - The blockchain has 2 blocks (height 1) => this returns `2`
21///
22/// So the height of a new block would be `chain_height()`.
23#[doc = doc_error!()]
24#[inline]
25pub fn chain_height(table_block_heights: &impl DatabaseRo<BlockHeights>) -> DbResult<BlockHeight> {
26    #[expect(clippy::cast_possible_truncation, reason = "we enforce 64-bit")]
27    table_block_heights.len().map(|height| height as usize)
28}
29
30/// Retrieve the height of the top block.
31///
32/// This returns the height of the top block, not the [`chain_height`].
33///
34/// For example:
35/// - The blockchain has 0 blocks => this returns `Err(RuntimeError::KeyNotFound)`
36/// - The blockchain has 1 block (height 0) => this returns `Ok(0)`
37/// - The blockchain has 2 blocks (height 1) => this returns `Ok(1)`
38///
39/// Note that in cases where no blocks have been written to the
40/// database yet, an error is returned: `Err(RuntimeError::KeyNotFound)`.
41///
42#[doc = doc_error!()]
43#[inline]
44pub fn top_block_height(
45    table_block_heights: &impl DatabaseRo<BlockHeights>,
46) -> DbResult<BlockHeight> {
47    match table_block_heights.len()? {
48        0 => Err(RuntimeError::KeyNotFound),
49        #[expect(clippy::cast_possible_truncation, reason = "we enforce 64-bit")]
50        height => Ok(height as usize - 1),
51    }
52}
53
54/// Check how many cumulative generated coins there have been until a certain [`BlockHeight`].
55///
56/// This returns the total amount of Monero generated up to `block_height`
57/// (including the block itself) in atomic units.
58///
59/// For example:
60/// - on the genesis block `0`, this returns the amount block `0` generated
61/// - on the next block `1`, this returns the amount block `0` and `1` generated
62///
63/// If no blocks have been added and `block_height == 0`
64/// (i.e., the cumulative generated coins before genesis block is being calculated),
65/// this returns `Ok(0)`.
66#[doc = doc_error!()]
67#[inline]
68pub fn cumulative_generated_coins(
69    block_height: &BlockHeight,
70    table_block_infos: &impl DatabaseRo<BlockInfos>,
71) -> DbResult<u64> {
72    match table_block_infos.get(block_height) {
73        Ok(block_info) => Ok(block_info.cumulative_generated_coins),
74        Err(RuntimeError::KeyNotFound) if block_height == &0 => Ok(0),
75        Err(e) => Err(e),
76    }
77}
78
79/// Find the split point between our chain and a list of [`BlockHash`]s from another chain.
80///
81/// This function accepts chains in chronological and reverse chronological order, however
82/// if the wrong order is specified the return value is meaningless.
83///
84/// For chronologically ordered chains this will return the index of the first unknown, for reverse
85/// chronologically ordered chains this will return the index of the first known.
86///
87/// If all blocks are known for chronologically ordered chains or unknown for reverse chronologically
88/// ordered chains then the length of the chain will be returned.
89#[doc = doc_error!()]
90#[inline]
91pub fn find_split_point(
92    block_ids: &[BlockHash],
93    chronological_order: bool,
94    include_alt_blocks: bool,
95    table_block_heights: &impl DatabaseRo<BlockHeights>,
96    table_alt_block_heights: &impl DatabaseRo<AltBlockHeights>,
97) -> Result<usize, RuntimeError> {
98    let mut err = None;
99
100    let block_exists = |block_id| {
101        block::block_exists(&block_id, table_block_heights).and_then(|exists| {
102            Ok(exists | (include_alt_blocks & table_alt_block_heights.contains(&block_id)?))
103        })
104    };
105
106    // Do a binary search to find the first unknown/known block in the batch.
107    let idx = block_ids.partition_point(|block_id| {
108        match block_exists(*block_id) {
109            Ok(exists) => exists == chronological_order,
110            Err(e) => {
111                err.get_or_insert(e);
112                // if this happens the search is scrapped, just return `false` back.
113                false
114            }
115        }
116    });
117
118    if let Some(e) = err {
119        return Err(e);
120    }
121
122    Ok(idx)
123}
124
125//---------------------------------------------------------------------------------------------------- Tests
126#[cfg(test)]
127mod test {
128    use pretty_assertions::assert_eq;
129
130    use cuprate_database::{Env, EnvInner, TxRw};
131    use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
132
133    use super::*;
134
135    use crate::{
136        ops::block::add_block,
137        tables::{OpenTables, Tables},
138        tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
139    };
140
141    /// Tests all above functions.
142    ///
143    /// Note that this doesn't test the correctness of values added, as the
144    /// functions have a pre-condition that the caller handles this.
145    ///
146    /// It simply tests if the proper tables are mutated, and if the data
147    /// stored and retrieved is the same.
148    #[test]
149    fn all_blockchain_functions() {
150        let (env, _tmp) = tmp_concrete_env();
151        let env_inner = env.env_inner();
152        assert_all_tables_are_empty(&env);
153
154        let mut blocks = [
155            BLOCK_V1_TX2.clone(),
156            BLOCK_V9_TX3.clone(),
157            BLOCK_V16_TX0.clone(),
158        ];
159        let blocks_len = blocks.len();
160
161        // Add blocks.
162        {
163            let tx_rw = env_inner.tx_rw().unwrap();
164            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
165
166            assert!(matches!(
167                top_block_height(tables.block_heights()),
168                Err(RuntimeError::KeyNotFound),
169            ));
170            assert_eq!(
171                0,
172                cumulative_generated_coins(&0, tables.block_infos()).unwrap()
173            );
174
175            for (i, block) in blocks.iter_mut().enumerate() {
176                // HACK: `add_block()` asserts blocks with non-sequential heights
177                // cannot be added, to get around this, manually edit the block height.
178                block.height = i;
179                add_block(block, &mut tables).unwrap();
180            }
181
182            // Assert reads are correct.
183            AssertTableLen {
184                block_infos: 3,
185                block_header_blobs: 3,
186                block_txs_hashes: 3,
187                block_heights: 3,
188                key_images: 69,
189                num_outputs: 41,
190                pruned_tx_blobs: 0,
191                prunable_hashes: 0,
192                outputs: 111,
193                prunable_tx_blobs: 0,
194                rct_outputs: 8,
195                tx_blobs: 8,
196                tx_ids: 8,
197                tx_heights: 8,
198                tx_unlock_time: 3,
199            }
200            .assert(&tables);
201
202            assert_eq!(blocks_len, chain_height(tables.block_heights()).unwrap());
203            assert_eq!(
204                blocks_len - 1,
205                top_block_height(tables.block_heights()).unwrap()
206            );
207            assert_eq!(
208                cumulative_generated_coins(&0, tables.block_infos()).unwrap(),
209                14_535_350_982_449,
210            );
211            assert_eq!(
212                cumulative_generated_coins(&1, tables.block_infos()).unwrap(),
213                17_939_125_004_612,
214            );
215            assert_eq!(
216                cumulative_generated_coins(&2, tables.block_infos()).unwrap(),
217                18_539_125_004_612,
218            );
219            assert!(matches!(
220                cumulative_generated_coins(&3, tables.block_infos()),
221                Err(RuntimeError::KeyNotFound),
222            ));
223
224            drop(tables);
225            TxRw::commit(tx_rw).unwrap();
226        }
227    }
228}