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_complete_entry_*`
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    get_block_complete_entry_from_height(&block_height, tables)
267}
268
269/// Retrieve a [`BlockCompleteEntry`] from the database.
270///
271#[doc = doc_error!()]
272pub fn get_block_complete_entry_from_height(
273    block_height: &BlockHeight,
274    tables: &impl TablesIter,
275) -> Result<BlockCompleteEntry, RuntimeError> {
276    let (block_blob, miner_tx_idx, numb_non_miner_txs) =
277        get_block_blob_with_tx_indexes(block_height, tables)?;
278
279    let first_tx_idx = miner_tx_idx + 1;
280
281    let tx_blobs = (first_tx_idx..(usize_to_u64(numb_non_miner_txs) + first_tx_idx))
282        .map(|idx| {
283            let tx_blob = tables.tx_blobs().get(&idx)?.0;
284
285            Ok(Bytes::from(tx_blob))
286        })
287        .collect::<Result<_, RuntimeError>>()?;
288
289    Ok(BlockCompleteEntry {
290        block: Bytes::from(block_blob),
291        txs: TransactionBlobs::Normal(tx_blobs),
292        pruned: false,
293        block_weight: 0,
294    })
295}
296
297//---------------------------------------------------------------------------------------------------- `get_block_extended_header_*`
298/// Retrieve a [`ExtendedBlockHeader`] from the database.
299///
300/// This extracts all the data from the database tables
301/// needed to create a full `ExtendedBlockHeader`.
302///
303/// # Notes
304/// This is slightly more expensive than [`get_block_extended_header_from_height`]
305/// (1 more database lookup).
306#[doc = doc_error!()]
307#[inline]
308pub fn get_block_extended_header(
309    block_hash: &BlockHash,
310    tables: &impl Tables,
311) -> DbResult<ExtendedBlockHeader> {
312    get_block_extended_header_from_height(&tables.block_heights().get(block_hash)?, tables)
313}
314
315/// Same as [`get_block_extended_header`] but with a [`BlockHeight`].
316#[doc = doc_error!()]
317#[expect(
318    clippy::missing_panics_doc,
319    reason = "The panic is only possible with a corrupt DB"
320)]
321#[inline]
322pub fn get_block_extended_header_from_height(
323    block_height: &BlockHeight,
324    tables: &impl Tables,
325) -> DbResult<ExtendedBlockHeader> {
326    let block_info = tables.block_infos().get(block_height)?;
327    let block_header_blob = tables.block_header_blobs().get(block_height)?.0;
328    let block_header = BlockHeader::read(&mut block_header_blob.as_slice())?;
329
330    let cumulative_difficulty = combine_low_high_bits_to_u128(
331        block_info.cumulative_difficulty_low,
332        block_info.cumulative_difficulty_high,
333    );
334
335    Ok(ExtendedBlockHeader {
336        cumulative_difficulty,
337        version: HardFork::from_version(block_header.hardfork_version)
338            .expect("Stored block must have a valid hard-fork"),
339        vote: block_header.hardfork_signal,
340        timestamp: block_header.timestamp,
341        block_weight: block_info.weight,
342        long_term_weight: block_info.long_term_weight,
343    })
344}
345
346/// Return the top/latest [`ExtendedBlockHeader`] from the database.
347#[doc = doc_error!()]
348#[inline]
349pub fn get_block_extended_header_top(
350    tables: &impl Tables,
351) -> DbResult<(ExtendedBlockHeader, BlockHeight)> {
352    let height = chain_height(tables.block_heights())?.saturating_sub(1);
353    let header = get_block_extended_header_from_height(&height, tables)?;
354    Ok((header, height))
355}
356
357//---------------------------------------------------------------------------------------------------- Block
358/// Retrieve a [`Block`] via its [`BlockHeight`].
359#[doc = doc_error!()]
360#[inline]
361pub fn get_block(tables: &impl Tables, block_height: &BlockHeight) -> DbResult<Block> {
362    let header_blob = tables.block_header_blobs().get(block_height)?.0;
363    let header = BlockHeader::read(&mut header_blob.as_slice())?;
364
365    let transactions = tables.block_txs_hashes().get(block_height)?.0;
366    let miner_tx_id = tables.block_infos().get(block_height)?.mining_tx_index;
367    let miner_transaction = crate::ops::tx::get_tx_from_id(&miner_tx_id, tables.tx_blobs())?;
368
369    Ok(Block {
370        header,
371        miner_transaction,
372        transactions,
373    })
374}
375
376/// Retrieve a [`Block`] via its [`BlockHash`].
377#[doc = doc_error!()]
378#[inline]
379pub fn get_block_by_hash(tables: &impl Tables, block_hash: &BlockHash) -> DbResult<Block> {
380    let block_height = tables.block_heights().get(block_hash)?;
381    get_block(tables, &block_height)
382}
383
384//---------------------------------------------------------------------------------------------------- Misc
385/// Retrieve a [`BlockInfo`] via its [`BlockHeight`].
386#[doc = doc_error!()]
387#[inline]
388pub fn get_block_info(
389    block_height: &BlockHeight,
390    table_block_infos: &impl DatabaseRo<BlockInfos>,
391) -> DbResult<BlockInfo> {
392    table_block_infos.get(block_height)
393}
394
395/// Retrieve a [`BlockHeight`] via its [`BlockHash`].
396#[doc = doc_error!()]
397#[inline]
398pub fn get_block_height(
399    block_hash: &BlockHash,
400    table_block_heights: &impl DatabaseRo<BlockHeights>,
401) -> DbResult<BlockHeight> {
402    table_block_heights.get(block_hash)
403}
404
405/// Check if a block exists in the database.
406///
407/// # Errors
408/// Note that this will never return `Err(RuntimeError::KeyNotFound)`,
409/// as in that case, `Ok(false)` will be returned.
410///
411/// Other errors may still occur.
412#[inline]
413pub fn block_exists(
414    block_hash: &BlockHash,
415    table_block_heights: &impl DatabaseRo<BlockHeights>,
416) -> DbResult<bool> {
417    table_block_heights.contains(block_hash)
418}
419
420//---------------------------------------------------------------------------------------------------- Tests
421#[cfg(test)]
422#[expect(clippy::too_many_lines)]
423mod test {
424    use pretty_assertions::assert_eq;
425
426    use cuprate_database::{Env, EnvInner, TxRw};
427    use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
428
429    use crate::{
430        ops::tx::{get_tx, tx_exists},
431        tables::OpenTables,
432        tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
433    };
434
435    use super::*;
436
437    /// Tests all above block functions.
438    ///
439    /// Note that this doesn't test the correctness of values added, as the
440    /// functions have a pre-condition that the caller handles this.
441    ///
442    /// It simply tests if the proper tables are mutated, and if the data
443    /// stored and retrieved is the same.
444    #[test]
445    fn all_block_functions() {
446        let (env, _tmp) = tmp_concrete_env();
447        let env_inner = env.env_inner();
448        assert_all_tables_are_empty(&env);
449
450        let mut blocks = [
451            BLOCK_V1_TX2.clone(),
452            BLOCK_V9_TX3.clone(),
453            BLOCK_V16_TX0.clone(),
454        ];
455        // HACK: `add_block()` asserts blocks with non-sequential heights
456        // cannot be added, to get around this, manually edit the block height.
457        for (height, block) in blocks.iter_mut().enumerate() {
458            block.height = height;
459            assert_eq!(block.block.serialize(), block.block_blob);
460        }
461        let generated_coins_sum = blocks
462            .iter()
463            .map(|block| block.generated_coins)
464            .sum::<u64>();
465
466        // Add blocks.
467        {
468            let tx_rw = env_inner.tx_rw().unwrap();
469            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
470
471            for block in &blocks {
472                // println!("add_block: {block:#?}");
473                add_block(block, &mut tables).unwrap();
474            }
475
476            drop(tables);
477            TxRw::commit(tx_rw).unwrap();
478        }
479
480        // Assert all reads are OK.
481        let block_hashes = {
482            let tx_ro = env_inner.tx_ro().unwrap();
483            let tables = env_inner.open_tables(&tx_ro).unwrap();
484
485            // Assert only the proper tables were added to.
486            AssertTableLen {
487                block_infos: 3,
488                block_header_blobs: 3,
489                block_txs_hashes: 3,
490                block_heights: 3,
491                key_images: 69,
492                num_outputs: 41,
493                pruned_tx_blobs: 0,
494                prunable_hashes: 0,
495                outputs: 111,
496                prunable_tx_blobs: 0,
497                rct_outputs: 8,
498                tx_blobs: 8,
499                tx_ids: 8,
500                tx_heights: 8,
501                tx_unlock_time: 3,
502            }
503            .assert(&tables);
504
505            // Check `cumulative` functions work.
506            assert_eq!(
507                cumulative_generated_coins(&2, tables.block_infos()).unwrap(),
508                generated_coins_sum,
509            );
510
511            // Both height and hash should result in getting the same data.
512            let mut block_hashes = vec![];
513            for block in &blocks {
514                println!("blocks.iter(): hash: {}", hex::encode(block.block_hash));
515
516                let height = get_block_height(&block.block_hash, tables.block_heights()).unwrap();
517
518                println!("blocks.iter(): height: {height}");
519
520                assert!(block_exists(&block.block_hash, tables.block_heights()).unwrap());
521
522                let block_header_from_height =
523                    get_block_extended_header_from_height(&height, &tables).unwrap();
524                let block_header_from_hash =
525                    get_block_extended_header(&block.block_hash, &tables).unwrap();
526
527                // Just an alias, these names are long.
528                let b1 = block_header_from_hash;
529                let b2 = block;
530                assert_eq!(b1, block_header_from_height);
531                assert_eq!(b1.version.as_u8(), b2.block.header.hardfork_version);
532                assert_eq!(b1.vote, b2.block.header.hardfork_signal);
533                assert_eq!(b1.timestamp, b2.block.header.timestamp);
534                assert_eq!(b1.cumulative_difficulty, b2.cumulative_difficulty);
535                assert_eq!(b1.block_weight, b2.weight);
536                assert_eq!(b1.long_term_weight, b2.long_term_weight);
537
538                block_hashes.push(block.block_hash);
539
540                // Assert transaction reads are OK.
541                for (i, tx) in block.txs.iter().enumerate() {
542                    println!("tx_hash: {:?}", hex::encode(tx.tx_hash));
543
544                    assert!(tx_exists(&tx.tx_hash, tables.tx_ids()).unwrap());
545
546                    let tx2 = get_tx(&tx.tx_hash, tables.tx_ids(), tables.tx_blobs()).unwrap();
547
548                    assert_eq!(tx.tx_blob, tx2.serialize());
549                    assert_eq!(tx.tx_weight, tx2.weight());
550                    assert_eq!(tx.tx_hash, block.block.transactions[i]);
551                    assert_eq!(tx.tx_hash, tx2.hash());
552                }
553            }
554
555            block_hashes
556        };
557
558        {
559            let len = block_hashes.len();
560            let hashes: Vec<String> = block_hashes.iter().map(hex::encode).collect();
561            println!("block_hashes: len: {len}, hashes: {hashes:?}");
562        }
563
564        // Remove the blocks.
565        {
566            let tx_rw = env_inner.tx_rw().unwrap();
567            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
568
569            for block_hash in block_hashes.into_iter().rev() {
570                println!("pop_block(): block_hash: {}", hex::encode(block_hash));
571
572                let (_popped_height, popped_hash, _popped_block) =
573                    pop_block(None, &mut tables).unwrap();
574
575                assert_eq!(block_hash, popped_hash);
576
577                assert!(matches!(
578                    get_block_extended_header(&block_hash, &tables),
579                    Err(RuntimeError::KeyNotFound)
580                ));
581            }
582
583            drop(tables);
584            TxRw::commit(tx_rw).unwrap();
585        }
586
587        assert_all_tables_are_empty(&env);
588    }
589
590    /// We should panic if: `block.height` > `u32::MAX`
591    #[test]
592    #[should_panic(expected = "block.height (4294967296) > u32::MAX")]
593    fn block_height_gt_u32_max() {
594        let (env, _tmp) = tmp_concrete_env();
595        let env_inner = env.env_inner();
596        assert_all_tables_are_empty(&env);
597
598        let tx_rw = env_inner.tx_rw().unwrap();
599        let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
600
601        let mut block = BLOCK_V9_TX3.clone();
602
603        block.height = cuprate_helper::cast::u32_to_usize(u32::MAX) + 1;
604        add_block(&block, &mut tables).unwrap();
605    }
606
607    /// We should panic if: `block.height` != the chain height
608    #[test]
609    #[should_panic(
610        expected = "assertion `left == right` failed: block.height (123) != chain_height (1)\n  left: 123\n right: 1"
611    )]
612    fn block_height_not_chain_height() {
613        let (env, _tmp) = tmp_concrete_env();
614        let env_inner = env.env_inner();
615        assert_all_tables_are_empty(&env);
616
617        let tx_rw = env_inner.tx_rw().unwrap();
618        let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
619
620        let mut block = BLOCK_V9_TX3.clone();
621        // HACK: `add_block()` asserts blocks with non-sequential heights
622        // cannot be added, to get around this, manually edit the block height.
623        block.height = 0;
624
625        // OK, `0 == 0`
626        assert_eq!(block.height, 0);
627        add_block(&block, &mut tables).unwrap();
628
629        // FAIL, `123 != 1`
630        block.height = 123;
631        add_block(&block, &mut tables).unwrap();
632    }
633}