cuprate_blockchain/ops/alt_block/
block.rs

1use bytemuck::TransparentWrapper;
2use monero_serai::block::{Block, BlockHeader};
3
4use cuprate_database::{DatabaseRo, DatabaseRw, DbResult, StorableVec};
5use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits};
6use cuprate_types::{AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, HardFork};
7
8use crate::{
9    ops::{
10        alt_block::{add_alt_transaction_blob, get_alt_transaction, update_alt_chain_info},
11        block::get_block_info,
12        macros::doc_error,
13    },
14    tables::{Tables, TablesMut},
15    types::{AltBlockHeight, BlockHash, BlockHeight, CompactAltBlockInfo},
16};
17
18/// Flush all alt-block data from all the alt-block tables.
19///
20/// This function completely empties the alt block tables.
21pub fn flush_alt_blocks<'a, E: cuprate_database::EnvInner<'a>>(
22    env_inner: &E,
23    tx_rw: &mut E::Rw<'_>,
24) -> DbResult<()> {
25    use crate::tables::{
26        AltBlockBlobs, AltBlockHeights, AltBlocksInfo, AltChainInfos, AltTransactionBlobs,
27        AltTransactionInfos,
28    };
29
30    env_inner.clear_db::<AltChainInfos>(tx_rw)?;
31    env_inner.clear_db::<AltBlockHeights>(tx_rw)?;
32    env_inner.clear_db::<AltBlocksInfo>(tx_rw)?;
33    env_inner.clear_db::<AltBlockBlobs>(tx_rw)?;
34    env_inner.clear_db::<AltTransactionBlobs>(tx_rw)?;
35    env_inner.clear_db::<AltTransactionInfos>(tx_rw)
36}
37
38/// Add a [`AltBlockInformation`] to the database.
39///
40/// This extracts all the data from the input block and
41/// maps/adds them to the appropriate database tables.
42///
43#[doc = doc_error!()]
44///
45/// # Panics
46/// This function will panic if:
47/// - `alt_block.height` is == `0`
48/// - `alt_block.txs.len()` != `alt_block.block.transactions.len()`
49///
50pub fn add_alt_block(alt_block: &AltBlockInformation, tables: &mut impl TablesMut) -> DbResult<()> {
51    let alt_block_height = AltBlockHeight {
52        chain_id: alt_block.chain_id.into(),
53        height: alt_block.height,
54    };
55
56    tables
57        .alt_block_heights_mut()
58        .put(&alt_block.block_hash, &alt_block_height)?;
59
60    update_alt_chain_info(&alt_block_height, &alt_block.block.header.previous, tables)?;
61
62    let (cumulative_difficulty_low, cumulative_difficulty_high) =
63        split_u128_into_low_high_bits(alt_block.cumulative_difficulty);
64
65    let alt_block_info = CompactAltBlockInfo {
66        block_hash: alt_block.block_hash,
67        pow_hash: alt_block.pow_hash,
68        height: alt_block.height,
69        weight: alt_block.weight,
70        long_term_weight: alt_block.long_term_weight,
71        cumulative_difficulty_low,
72        cumulative_difficulty_high,
73    };
74
75    tables
76        .alt_blocks_info_mut()
77        .put(&alt_block_height, &alt_block_info)?;
78
79    tables.alt_block_blobs_mut().put(
80        &alt_block_height,
81        StorableVec::wrap_ref(&alt_block.block_blob),
82    )?;
83
84    assert_eq!(alt_block.txs.len(), alt_block.block.transactions.len());
85    for tx in &alt_block.txs {
86        add_alt_transaction_blob(tx, tables)?;
87    }
88
89    Ok(())
90}
91
92/// Retrieves an [`AltBlockInformation`] from the database.
93///
94/// This function will look at only the blocks with the given [`AltBlockHeight::chain_id`], no others
95/// even if they are technically part of this chain.
96#[doc = doc_error!()]
97pub fn get_alt_block(
98    alt_block_height: &AltBlockHeight,
99    tables: &impl Tables,
100) -> DbResult<AltBlockInformation> {
101    let block_info = tables.alt_blocks_info().get(alt_block_height)?;
102
103    let block_blob = tables.alt_block_blobs().get(alt_block_height)?.0;
104
105    let block = Block::read(&mut block_blob.as_slice())?;
106
107    let txs = block
108        .transactions
109        .iter()
110        .map(|tx_hash| get_alt_transaction(tx_hash, tables))
111        .collect::<DbResult<_>>()?;
112
113    Ok(AltBlockInformation {
114        block,
115        block_blob,
116        txs,
117        block_hash: block_info.block_hash,
118        pow_hash: block_info.pow_hash,
119        height: block_info.height,
120        weight: block_info.weight,
121        long_term_weight: block_info.long_term_weight,
122        cumulative_difficulty: combine_low_high_bits_to_u128(
123            block_info.cumulative_difficulty_low,
124            block_info.cumulative_difficulty_high,
125        ),
126        chain_id: alt_block_height.chain_id.into(),
127    })
128}
129
130/// Retrieves the hash of the block at the given `block_height` on the alt chain with
131/// the given [`ChainId`].
132///
133/// This function will get blocks from the whole chain, for example if you were to ask for height
134/// `0` with any [`ChainId`] (as long that chain actually exists) you will get the main chain genesis.
135///
136#[doc = doc_error!()]
137pub fn get_alt_block_hash(
138    block_height: &BlockHeight,
139    alt_chain: ChainId,
140    tables: &impl Tables,
141) -> DbResult<BlockHash> {
142    let alt_chains = tables.alt_chain_infos();
143
144    // First find what [`ChainId`] this block would be stored under.
145    let original_chain = {
146        let mut chain = alt_chain.into();
147        loop {
148            let chain_info = alt_chains.get(&chain)?;
149
150            if chain_info.common_ancestor_height < *block_height {
151                break Chain::Alt(chain.into());
152            }
153
154            match chain_info.parent_chain.into() {
155                Chain::Main => break Chain::Main,
156                Chain::Alt(alt_chain_id) => {
157                    chain = alt_chain_id.into();
158                    continue;
159                }
160            }
161        }
162    };
163
164    // Get the block hash.
165    match original_chain {
166        Chain::Main => {
167            get_block_info(block_height, tables.block_infos()).map(|info| info.block_hash)
168        }
169        Chain::Alt(chain_id) => tables
170            .alt_blocks_info()
171            .get(&AltBlockHeight {
172                chain_id: chain_id.into(),
173                height: *block_height,
174            })
175            .map(|info| info.block_hash),
176    }
177}
178
179/// Retrieves the [`ExtendedBlockHeader`] of the alt-block with an exact [`AltBlockHeight`].
180///
181/// This function will look at only the blocks with the given [`AltBlockHeight::chain_id`], no others
182/// even if they are technically part of this chain.
183///
184#[doc = doc_error!()]
185pub fn get_alt_block_extended_header_from_height(
186    height: &AltBlockHeight,
187    table: &impl Tables,
188) -> DbResult<ExtendedBlockHeader> {
189    let block_info = table.alt_blocks_info().get(height)?;
190
191    let block_blob = table.alt_block_blobs().get(height)?.0;
192
193    let block_header = BlockHeader::read(&mut block_blob.as_slice())?;
194
195    Ok(ExtendedBlockHeader {
196        version: HardFork::from_version(block_header.hardfork_version)
197            .expect("Block in DB must have correct version"),
198        vote: block_header.hardfork_version,
199        timestamp: block_header.timestamp,
200        cumulative_difficulty: combine_low_high_bits_to_u128(
201            block_info.cumulative_difficulty_low,
202            block_info.cumulative_difficulty_high,
203        ),
204        block_weight: block_info.weight,
205        long_term_weight: block_info.long_term_weight,
206    })
207}
208
209#[cfg(test)]
210mod tests {
211    use std::num::NonZero;
212
213    use cuprate_database::{Env, EnvInner, TxRw};
214    use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
215    use cuprate_types::{Chain, ChainId};
216
217    use crate::{
218        ops::{
219            alt_block::{
220                add_alt_block, flush_alt_blocks, get_alt_block,
221                get_alt_block_extended_header_from_height, get_alt_block_hash,
222                get_alt_chain_history_ranges,
223            },
224            block::{add_block, pop_block},
225        },
226        tables::{OpenTables, Tables},
227        tests::{assert_all_tables_are_empty, map_verified_block_to_alt, tmp_concrete_env},
228        types::AltBlockHeight,
229    };
230
231    #[expect(clippy::range_plus_one)]
232    #[test]
233    fn all_alt_blocks() {
234        let (env, _tmp) = tmp_concrete_env();
235        let env_inner = env.env_inner();
236        assert_all_tables_are_empty(&env);
237
238        let chain_id = ChainId(NonZero::new(1).unwrap());
239
240        // Add initial block.
241        {
242            let tx_rw = env_inner.tx_rw().unwrap();
243            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
244
245            let mut initial_block = BLOCK_V1_TX2.clone();
246            initial_block.height = 0;
247
248            add_block(&initial_block, &mut tables).unwrap();
249
250            drop(tables);
251            TxRw::commit(tx_rw).unwrap();
252        }
253
254        let alt_blocks = [
255            map_verified_block_to_alt(BLOCK_V9_TX3.clone(), chain_id),
256            map_verified_block_to_alt(BLOCK_V16_TX0.clone(), chain_id),
257        ];
258
259        // Add alt-blocks
260        {
261            let tx_rw = env_inner.tx_rw().unwrap();
262            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
263
264            let mut prev_hash = BLOCK_V1_TX2.block_hash;
265            for (i, mut alt_block) in alt_blocks.into_iter().enumerate() {
266                let height = i + 1;
267
268                alt_block.height = height;
269                alt_block.block.header.previous = prev_hash;
270                alt_block.block_blob = alt_block.block.serialize();
271
272                add_alt_block(&alt_block, &mut tables).unwrap();
273
274                let alt_height = AltBlockHeight {
275                    chain_id: chain_id.into(),
276                    height,
277                };
278
279                let alt_block_2 = get_alt_block(&alt_height, &tables).unwrap();
280                assert_eq!(alt_block.block, alt_block_2.block);
281
282                let headers = get_alt_chain_history_ranges(
283                    0..(height + 1),
284                    chain_id,
285                    tables.alt_chain_infos(),
286                )
287                .unwrap();
288
289                assert_eq!(headers.len(), 2);
290                assert_eq!(headers[1], (Chain::Main, 0..1));
291                assert_eq!(headers[0], (Chain::Alt(chain_id), 1..(height + 1)));
292
293                prev_hash = alt_block.block_hash;
294
295                let header =
296                    get_alt_block_extended_header_from_height(&alt_height, &tables).unwrap();
297
298                assert_eq!(header.timestamp, alt_block.block.header.timestamp);
299                assert_eq!(header.block_weight, alt_block.weight);
300                assert_eq!(header.long_term_weight, alt_block.long_term_weight);
301                assert_eq!(
302                    header.cumulative_difficulty,
303                    alt_block.cumulative_difficulty
304                );
305                assert_eq!(
306                    header.version.as_u8(),
307                    alt_block.block.header.hardfork_version
308                );
309                assert_eq!(header.vote, alt_block.block.header.hardfork_signal);
310
311                let block_hash = get_alt_block_hash(&height, chain_id, &tables).unwrap();
312
313                assert_eq!(block_hash, alt_block.block_hash);
314            }
315
316            drop(tables);
317            TxRw::commit(tx_rw).unwrap();
318        }
319
320        {
321            let mut tx_rw = env_inner.tx_rw().unwrap();
322
323            flush_alt_blocks(&env_inner, &mut tx_rw).unwrap();
324
325            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
326            pop_block(None, &mut tables).unwrap();
327
328            drop(tables);
329            TxRw::commit(tx_rw).unwrap();
330        }
331
332        assert_all_tables_are_empty(&env);
333    }
334}