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}