cuprate_blockchain/ops/
block.rs

1//! Block functions.
2
3//---------------------------------------------------------------------------------------------------- Import
4use bytemuck::TransparentWrapper;
5use bytes::Bytes;
6use monero_serai::{
7    block::{Block, BlockHeader},
8    transaction::Transaction,
9};
10
11use cuprate_database::{
12    DbResult, RuntimeError, StorableVec, {DatabaseRo, DatabaseRw},
13};
14use cuprate_helper::cast::usize_to_u64;
15use cuprate_helper::{
16    map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits},
17    tx::tx_fee,
18};
19use cuprate_types::{
20    AltBlockInformation, BlockCompleteEntry, ChainId, ExtendedBlockHeader, HardFork,
21    TransactionBlobs, VerifiedBlockInformation, VerifiedTransactionInformation,
22};
23
24use crate::{
25    ops::{
26        alt_block,
27        blockchain::{chain_height, cumulative_generated_coins},
28        macros::doc_error,
29        output::get_rct_num_outputs,
30        tx::{add_tx, remove_tx},
31    },
32    tables::{BlockHeights, BlockInfos, Tables, TablesIter, TablesMut},
33    types::{BlockHash, BlockHeight, BlockInfo},
34};
35
36//---------------------------------------------------------------------------------------------------- `add_block_*`
37/// Add a [`VerifiedBlockInformation`] to the database.
38///
39/// This extracts all the data from the input block and
40/// maps/adds them to the appropriate database tables.
41///
42#[doc = doc_error!()]
43///
44/// # Panics
45/// This function will panic if:
46/// - `block.height > u32::MAX` (not normally possible)
47/// - `block.height` is != [`chain_height`]
48// no inline, too big.
49pub fn add_block(block: &VerifiedBlockInformation, tables: &mut impl TablesMut) -> DbResult<()> {
50    //------------------------------------------------------ Check preconditions first
51
52    // Cast height to `u32` for storage (handled at top of function).
53    // Panic (should never happen) instead of allowing DB corruption.
54    // <https://github.com/Cuprate/cuprate/pull/102#discussion_r1560020991>
55    assert!(
56        u32::try_from(block.height).is_ok(),
57        "block.height ({}) > u32::MAX",
58        block.height,
59    );
60
61    let chain_height = chain_height(tables.block_heights())?;
62    assert_eq!(
63        block.height, chain_height,
64        "block.height ({}) != chain_height ({})",
65        block.height, chain_height,
66    );
67
68    // Expensive checks - debug only.
69    #[cfg(debug_assertions)]
70    {
71        assert_eq!(block.block.serialize(), block.block_blob);
72        assert_eq!(block.block.transactions.len(), block.txs.len());
73        for (i, tx) in block.txs.iter().enumerate() {
74            assert_eq!(tx.tx_blob, tx.tx.serialize());
75            assert_eq!(tx.tx_hash, block.block.transactions[i]);
76        }
77    }
78
79    //------------------------------------------------------ Transaction / Outputs / Key Images
80    // Add the miner transaction first.
81    let mining_tx_index = {
82        let tx = &block.block.miner_transaction;
83        add_tx(tx, &tx.serialize(), &tx.hash(), &chain_height, tables)?
84    };
85
86    for tx in &block.txs {
87        add_tx(&tx.tx, &tx.tx_blob, &tx.tx_hash, &chain_height, tables)?;
88    }
89
90    //------------------------------------------------------ Block Info
91
92    // INVARIANT: must be below the above transaction loop since this
93    // RCT output count needs account for _this_ block's outputs.
94    let cumulative_rct_outs = get_rct_num_outputs(tables.rct_outputs())?;
95
96    // `saturating_add` is used here as cumulative generated coins overflows due to tail emission.
97    let cumulative_generated_coins =
98        cumulative_generated_coins(&block.height.saturating_sub(1), tables.block_infos())?
99            .saturating_add(block.generated_coins);
100
101    let (cumulative_difficulty_low, cumulative_difficulty_high) =
102        split_u128_into_low_high_bits(block.cumulative_difficulty);
103
104    // Block Info.
105    tables.block_infos_mut().put(
106        &block.height,
107        &BlockInfo {
108            cumulative_difficulty_low,
109            cumulative_difficulty_high,
110            cumulative_generated_coins,
111            cumulative_rct_outs,
112            timestamp: block.block.header.timestamp,
113            block_hash: block.block_hash,
114            weight: block.weight,
115            long_term_weight: block.long_term_weight,
116            mining_tx_index,
117        },
118    )?;
119
120    // Block header blob.
121    tables.block_header_blobs_mut().put(
122        &block.height,
123        StorableVec::wrap_ref(&block.block.header.serialize()),
124    )?;
125
126    // Block transaction hashes
127    tables.block_txs_hashes_mut().put(
128        &block.height,
129        StorableVec::wrap_ref(&block.block.transactions),
130    )?;
131
132    // Block heights.
133    tables
134        .block_heights_mut()
135        .put(&block.block_hash, &block.height)?;
136
137    Ok(())
138}
139
140//---------------------------------------------------------------------------------------------------- `pop_block`
141/// Remove the top/latest block from the database.
142///
143/// The removed block's data is returned.
144///
145/// If a [`ChainId`] is specified the popped block will be added to the alt block tables under
146/// that [`ChainId`]. Otherwise, the block will be completely removed from the DB.
147#[doc = doc_error!()]
148///
149/// In `pop_block()`'s case, [`RuntimeError::KeyNotFound`]
150/// will be returned if there are no blocks left.
151// no inline, too big
152pub fn pop_block(
153    move_to_alt_chain: Option<ChainId>,
154    tables: &mut impl TablesMut,
155) -> DbResult<(BlockHeight, BlockHash, Block)> {
156    //------------------------------------------------------ Block Info
157    // Remove block data from tables.
158    let (block_height, block_info) = tables.block_infos_mut().pop_last()?;
159
160    // Block heights.
161    tables.block_heights_mut().delete(&block_info.block_hash)?;
162
163    // Block blobs.
164    //
165    // We deserialize the block header blob and mining transaction blob
166    // to form a `Block`, such that we can remove the associated transactions
167    // later.
168    let block_header = tables.block_header_blobs_mut().take(&block_height)?.0;
169    let block_txs_hashes = tables.block_txs_hashes_mut().take(&block_height)?.0;
170    let miner_transaction = tables.tx_blobs().get(&block_info.mining_tx_index)?.0;
171    let block = Block {
172        header: BlockHeader::read(&mut block_header.as_slice())?,
173        miner_transaction: Transaction::read(&mut miner_transaction.as_slice())?,
174        transactions: block_txs_hashes,
175    };
176
177    //------------------------------------------------------ Transaction / Outputs / Key Images
178    remove_tx(&block.miner_transaction.hash(), tables)?;
179
180    let remove_tx_iter = block.transactions.iter().map(|tx_hash| {
181        let (_, tx) = remove_tx(tx_hash, tables)?;
182        Ok::<_, RuntimeError>(tx)
183    });
184
185    if let Some(chain_id) = move_to_alt_chain {
186        let txs = remove_tx_iter
187            .map(|result| {
188                let tx = result?;
189                Ok(VerifiedTransactionInformation {
190                    tx_weight: tx.weight(),
191                    tx_blob: tx.serialize(),
192                    tx_hash: tx.hash(),
193                    fee: tx_fee(&tx),
194                    tx,
195                })
196            })
197            .collect::<DbResult<Vec<VerifiedTransactionInformation>>>()?;
198
199        alt_block::add_alt_block(
200            &AltBlockInformation {
201                block: block.clone(),
202                block_blob: block.serialize(),
203                txs,
204                block_hash: block_info.block_hash,
205                // We know the PoW is valid for this block so just set it so it will always verify as valid.
206                pow_hash: [0; 32],
207                height: block_height,
208                weight: block_info.weight,
209                long_term_weight: block_info.long_term_weight,
210                cumulative_difficulty: combine_low_high_bits_to_u128(
211                    block_info.cumulative_difficulty_low,
212                    block_info.cumulative_difficulty_high,
213                ),
214                chain_id,
215            },
216            tables,
217        )?;
218    } else {
219        for result in remove_tx_iter {
220            drop(result?);
221        }
222    }
223
224    Ok((block_height, block_info.block_hash, block))
225}
226
227//---------------------------------------------------------------------------------------------------- `get_block_blob_with_tx_indexes`
228/// Retrieve a block's raw bytes, the index of the miner transaction and the number of non miner-txs in the block.
229///
230#[doc = doc_error!()]
231pub fn get_block_blob_with_tx_indexes(
232    block_height: &BlockHeight,
233    tables: &impl Tables,
234) -> Result<(Vec<u8>, u64, usize), RuntimeError> {
235    let miner_tx_idx = tables.block_infos().get(block_height)?.mining_tx_index;
236
237    let block_txs = tables.block_txs_hashes().get(block_height)?.0;
238    let numb_txs = block_txs.len();
239
240    // Get the block header
241    let mut block = tables.block_header_blobs().get(block_height)?.0;
242
243    // Add the miner tx to the blob.
244    let mut miner_tx_blob = tables.tx_blobs().get(&miner_tx_idx)?.0;
245    block.append(&mut miner_tx_blob);
246
247    // Add the blocks tx hashes.
248    monero_serai::io::write_varint(&block_txs.len(), &mut block)
249        .expect("The number of txs per block will not exceed u64::MAX");
250
251    let block_txs_bytes = bytemuck::must_cast_slice(&block_txs);
252    block.extend_from_slice(block_txs_bytes);
253
254    Ok((block, miner_tx_idx, numb_txs))
255}
256
257//---------------------------------------------------------------------------------------------------- `get_block_extended_header_*`
258/// Retrieve a [`BlockCompleteEntry`] from the database.
259///
260#[doc = doc_error!()]
261pub fn get_block_complete_entry(
262    block_hash: &BlockHash,
263    tables: &impl TablesIter,
264) -> Result<BlockCompleteEntry, RuntimeError> {
265    let block_height = tables.block_heights().get(block_hash)?;
266    let (block_blob, miner_tx_idx, numb_non_miner_txs) =
267        get_block_blob_with_tx_indexes(&block_height, tables)?;
268
269    let first_tx_idx = miner_tx_idx + 1;
270
271    let tx_blobs = (first_tx_idx..(usize_to_u64(numb_non_miner_txs) + first_tx_idx))
272        .map(|idx| {
273            let tx_blob = tables.tx_blobs().get(&idx)?.0;
274
275            Ok(Bytes::from(tx_blob))
276        })
277        .collect::<Result<_, RuntimeError>>()?;
278
279    Ok(BlockCompleteEntry {
280        block: Bytes::from(block_blob),
281        txs: TransactionBlobs::Normal(tx_blobs),
282        pruned: false,
283        block_weight: 0,
284    })
285}
286
287//---------------------------------------------------------------------------------------------------- `get_block_extended_header_*`
288/// Retrieve a [`ExtendedBlockHeader`] from the database.
289///
290/// This extracts all the data from the database tables
291/// needed to create a full `ExtendedBlockHeader`.
292///
293/// # Notes
294/// This is slightly more expensive than [`get_block_extended_header_from_height`]
295/// (1 more database lookup).
296#[doc = doc_error!()]
297#[inline]
298pub fn get_block_extended_header(
299    block_hash: &BlockHash,
300    tables: &impl Tables,
301) -> DbResult<ExtendedBlockHeader> {
302    get_block_extended_header_from_height(&tables.block_heights().get(block_hash)?, tables)
303}
304
305/// Same as [`get_block_extended_header`] but with a [`BlockHeight`].
306#[doc = doc_error!()]
307#[expect(
308    clippy::missing_panics_doc,
309    reason = "The panic is only possible with a corrupt DB"
310)]
311#[inline]
312pub fn get_block_extended_header_from_height(
313    block_height: &BlockHeight,
314    tables: &impl Tables,
315) -> DbResult<ExtendedBlockHeader> {
316    let block_info = tables.block_infos().get(block_height)?;
317    let block_header_blob = tables.block_header_blobs().get(block_height)?.0;
318    let block_header = BlockHeader::read(&mut block_header_blob.as_slice())?;
319
320    let cumulative_difficulty = combine_low_high_bits_to_u128(
321        block_info.cumulative_difficulty_low,
322        block_info.cumulative_difficulty_high,
323    );
324
325    Ok(ExtendedBlockHeader {
326        cumulative_difficulty,
327        version: HardFork::from_version(block_header.hardfork_version)
328            .expect("Stored block must have a valid hard-fork"),
329        vote: block_header.hardfork_signal,
330        timestamp: block_header.timestamp,
331        block_weight: block_info.weight,
332        long_term_weight: block_info.long_term_weight,
333    })
334}
335
336/// Return the top/latest [`ExtendedBlockHeader`] from the database.
337#[doc = doc_error!()]
338#[inline]
339pub fn get_block_extended_header_top(
340    tables: &impl Tables,
341) -> DbResult<(ExtendedBlockHeader, BlockHeight)> {
342    let height = chain_height(tables.block_heights())?.saturating_sub(1);
343    let header = get_block_extended_header_from_height(&height, tables)?;
344    Ok((header, height))
345}
346
347//---------------------------------------------------------------------------------------------------- Misc
348/// Retrieve a [`BlockInfo`] via its [`BlockHeight`].
349#[doc = doc_error!()]
350#[inline]
351pub fn get_block_info(
352    block_height: &BlockHeight,
353    table_block_infos: &impl DatabaseRo<BlockInfos>,
354) -> DbResult<BlockInfo> {
355    table_block_infos.get(block_height)
356}
357
358/// Retrieve a [`BlockHeight`] via its [`BlockHash`].
359#[doc = doc_error!()]
360#[inline]
361pub fn get_block_height(
362    block_hash: &BlockHash,
363    table_block_heights: &impl DatabaseRo<BlockHeights>,
364) -> DbResult<BlockHeight> {
365    table_block_heights.get(block_hash)
366}
367
368/// Check if a block exists in the database.
369///
370/// # Errors
371/// Note that this will never return `Err(RuntimeError::KeyNotFound)`,
372/// as in that case, `Ok(false)` will be returned.
373///
374/// Other errors may still occur.
375#[inline]
376pub fn block_exists(
377    block_hash: &BlockHash,
378    table_block_heights: &impl DatabaseRo<BlockHeights>,
379) -> DbResult<bool> {
380    table_block_heights.contains(block_hash)
381}
382
383//---------------------------------------------------------------------------------------------------- Tests
384#[cfg(test)]
385#[expect(clippy::too_many_lines)]
386mod test {
387    use pretty_assertions::assert_eq;
388
389    use cuprate_database::{Env, EnvInner, TxRw};
390    use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
391
392    use crate::{
393        ops::tx::{get_tx, tx_exists},
394        tables::OpenTables,
395        tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
396    };
397
398    use super::*;
399
400    /// Tests all above block functions.
401    ///
402    /// Note that this doesn't test the correctness of values added, as the
403    /// functions have a pre-condition that the caller handles this.
404    ///
405    /// It simply tests if the proper tables are mutated, and if the data
406    /// stored and retrieved is the same.
407    #[test]
408    fn all_block_functions() {
409        let (env, _tmp) = tmp_concrete_env();
410        let env_inner = env.env_inner();
411        assert_all_tables_are_empty(&env);
412
413        let mut blocks = [
414            BLOCK_V1_TX2.clone(),
415            BLOCK_V9_TX3.clone(),
416            BLOCK_V16_TX0.clone(),
417        ];
418        // HACK: `add_block()` asserts blocks with non-sequential heights
419        // cannot be added, to get around this, manually edit the block height.
420        for (height, block) in blocks.iter_mut().enumerate() {
421            block.height = height;
422            assert_eq!(block.block.serialize(), block.block_blob);
423        }
424        let generated_coins_sum = blocks
425            .iter()
426            .map(|block| block.generated_coins)
427            .sum::<u64>();
428
429        // Add blocks.
430        {
431            let tx_rw = env_inner.tx_rw().unwrap();
432            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
433
434            for block in &blocks {
435                // println!("add_block: {block:#?}");
436                add_block(block, &mut tables).unwrap();
437            }
438
439            drop(tables);
440            TxRw::commit(tx_rw).unwrap();
441        }
442
443        // Assert all reads are OK.
444        let block_hashes = {
445            let tx_ro = env_inner.tx_ro().unwrap();
446            let tables = env_inner.open_tables(&tx_ro).unwrap();
447
448            // Assert only the proper tables were added to.
449            AssertTableLen {
450                block_infos: 3,
451                block_header_blobs: 3,
452                block_txs_hashes: 3,
453                block_heights: 3,
454                key_images: 69,
455                num_outputs: 41,
456                pruned_tx_blobs: 0,
457                prunable_hashes: 0,
458                outputs: 111,
459                prunable_tx_blobs: 0,
460                rct_outputs: 8,
461                tx_blobs: 8,
462                tx_ids: 8,
463                tx_heights: 8,
464                tx_unlock_time: 3,
465            }
466            .assert(&tables);
467
468            // Check `cumulative` functions work.
469            assert_eq!(
470                cumulative_generated_coins(&2, tables.block_infos()).unwrap(),
471                generated_coins_sum,
472            );
473
474            // Both height and hash should result in getting the same data.
475            let mut block_hashes = vec![];
476            for block in &blocks {
477                println!("blocks.iter(): hash: {}", hex::encode(block.block_hash));
478
479                let height = get_block_height(&block.block_hash, tables.block_heights()).unwrap();
480
481                println!("blocks.iter(): height: {height}");
482
483                assert!(block_exists(&block.block_hash, tables.block_heights()).unwrap());
484
485                let block_header_from_height =
486                    get_block_extended_header_from_height(&height, &tables).unwrap();
487                let block_header_from_hash =
488                    get_block_extended_header(&block.block_hash, &tables).unwrap();
489
490                // Just an alias, these names are long.
491                let b1 = block_header_from_hash;
492                let b2 = block;
493                assert_eq!(b1, block_header_from_height);
494                assert_eq!(b1.version.as_u8(), b2.block.header.hardfork_version);
495                assert_eq!(b1.vote, b2.block.header.hardfork_signal);
496                assert_eq!(b1.timestamp, b2.block.header.timestamp);
497                assert_eq!(b1.cumulative_difficulty, b2.cumulative_difficulty);
498                assert_eq!(b1.block_weight, b2.weight);
499                assert_eq!(b1.long_term_weight, b2.long_term_weight);
500
501                block_hashes.push(block.block_hash);
502
503                // Assert transaction reads are OK.
504                for (i, tx) in block.txs.iter().enumerate() {
505                    println!("tx_hash: {:?}", hex::encode(tx.tx_hash));
506
507                    assert!(tx_exists(&tx.tx_hash, tables.tx_ids()).unwrap());
508
509                    let tx2 = get_tx(&tx.tx_hash, tables.tx_ids(), tables.tx_blobs()).unwrap();
510
511                    assert_eq!(tx.tx_blob, tx2.serialize());
512                    assert_eq!(tx.tx_weight, tx2.weight());
513                    assert_eq!(tx.tx_hash, block.block.transactions[i]);
514                    assert_eq!(tx.tx_hash, tx2.hash());
515                }
516            }
517
518            block_hashes
519        };
520
521        {
522            let len = block_hashes.len();
523            let hashes: Vec<String> = block_hashes.iter().map(hex::encode).collect();
524            println!("block_hashes: len: {len}, hashes: {hashes:?}");
525        }
526
527        // Remove the blocks.
528        {
529            let tx_rw = env_inner.tx_rw().unwrap();
530            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
531
532            for block_hash in block_hashes.into_iter().rev() {
533                println!("pop_block(): block_hash: {}", hex::encode(block_hash));
534
535                let (_popped_height, popped_hash, _popped_block) =
536                    pop_block(None, &mut tables).unwrap();
537
538                assert_eq!(block_hash, popped_hash);
539
540                assert!(matches!(
541                    get_block_extended_header(&block_hash, &tables),
542                    Err(RuntimeError::KeyNotFound)
543                ));
544            }
545
546            drop(tables);
547            TxRw::commit(tx_rw).unwrap();
548        }
549
550        assert_all_tables_are_empty(&env);
551    }
552
553    /// We should panic if: `block.height` > `u32::MAX`
554    #[test]
555    #[should_panic(expected = "block.height (4294967296) > u32::MAX")]
556    fn block_height_gt_u32_max() {
557        let (env, _tmp) = tmp_concrete_env();
558        let env_inner = env.env_inner();
559        assert_all_tables_are_empty(&env);
560
561        let tx_rw = env_inner.tx_rw().unwrap();
562        let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
563
564        let mut block = BLOCK_V9_TX3.clone();
565
566        block.height = cuprate_helper::cast::u32_to_usize(u32::MAX) + 1;
567        add_block(&block, &mut tables).unwrap();
568    }
569
570    /// We should panic if: `block.height` != the chain height
571    #[test]
572    #[should_panic(
573        expected = "assertion `left == right` failed: block.height (123) != chain_height (1)\n  left: 123\n right: 1"
574    )]
575    fn block_height_not_chain_height() {
576        let (env, _tmp) = tmp_concrete_env();
577        let env_inner = env.env_inner();
578        assert_all_tables_are_empty(&env);
579
580        let tx_rw = env_inner.tx_rw().unwrap();
581        let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
582
583        let mut block = BLOCK_V9_TX3.clone();
584        // HACK: `add_block()` asserts blocks with non-sequential heights
585        // cannot be added, to get around this, manually edit the block height.
586        block.height = 0;
587
588        // OK, `0 == 0`
589        assert_eq!(block.height, 0);
590        add_block(&block, &mut tables).unwrap();
591
592        // FAIL, `123 != 1`
593        block.height = 123;
594        add_block(&block, &mut tables).unwrap();
595    }
596}