Skip to main content

cuprate_blockchain/ops/
block.rs

1//! Block functions.
2use std::{borrow::Cow, cmp::min, collections::HashMap, io};
3
4use cuprate_helper::{
5    cast::{u64_to_usize, usize_to_u64},
6    map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits},
7    tx::tx_fee,
8};
9use cuprate_pruning::{CRYPTONOTE_PRUNING_LOG_STRIPES, CRYPTONOTE_PRUNING_STRIPE_SIZE};
10use cuprate_types::{
11    AltBlockInformation, BlockCompleteEntry, ChainId, ExtendedBlockHeader, HardFork,
12    PrunedTxBlobEntry, TransactionBlobs, VerifiedBlockInformation, VerifiedTransactionInformation,
13};
14
15use bytes::{Buf, Bytes};
16use fjall::Readable;
17use monero_oxide::{
18    block::{Block, BlockHeader},
19    transaction::{Pruned, Transaction},
20};
21use tapes::{TapesAppend, TapesRead, TapesTruncate};
22use tracing::instrument;
23
24use crate::{
25    database::CHAIN_TIP_KEY,
26    error::{BlockchainError, DbResult},
27    ops::{
28        alt_block,
29        tx::{add_tx_info_to_dynamic_tables, add_tx_info_to_tapes, remove_tx_from_dynamic_tables},
30    },
31    types::{Amount, BlockHash, BlockHeight, BlockInfo},
32    BlockchainDatabase,
33};
34
35/// Write a slice of contiguous blocks to the tapes database.
36///
37/// The blocks must follow directly from the top of the chain.
38///
39/// # Panic
40///
41/// This will panic if the given blocks cross 3 pruning stripes.
42#[instrument(skip_all, level = "info")]
43pub fn add_blocks_to_tapes(
44    blocks: &[VerifiedBlockInformation],
45    db: &BlockchainDatabase,
46    append_tx: &mut tapes::TapesAppendTransaction,
47) -> DbResult<()> {
48    /// A helper function to write the v2 prunable data, returning the index of the first v2 prunable byte.
49    fn write_v2_prunable_data(
50        append_tx: &mut tapes::TapesAppendTransaction,
51        tape: &tapes::BlobTape,
52        blocks: &[VerifiedBlockInformation],
53    ) -> io::Result<u64> {
54        let first_idx = append_tx
55            .blob_tape_len(tape)
56            .expect("Required tape not found");
57        for block in blocks {
58            for tx in &block.txs {
59                if tx.tx.version() != 1 {
60                    append_tx.append_bytes(tape, tx.tx_prunable_blob.as_slice())?;
61                }
62            }
63        }
64
65        Ok(first_idx)
66    }
67
68    /// A helper function to write the v1 prunable data, returning the index of the first v1 prunable byte.
69    fn write_v1_prunable_data(
70        append_tx: &mut tapes::TapesAppendTransaction,
71        db: &BlockchainDatabase,
72        blocks: &[VerifiedBlockInformation],
73    ) -> io::Result<u64> {
74        let first_idx = append_tx
75            .blob_tape_len(&db.v1_prunable_blobs)
76            .expect("Required tape not found");
77
78        for block in blocks {
79            for tx in &block.txs {
80                if tx.tx.version() == 1 {
81                    append_tx.append_bytes(&db.v1_prunable_blobs, &tx.tx_prunable_blob)?;
82                }
83            }
84        }
85
86        Ok(first_idx)
87    }
88
89    // First fill in the `pruned_blobs` tape.
90    let mut pruned_tape_index = append_tx.blob_tape_len(&db.pruned_blobs).unwrap_or(0);
91    for block in blocks {
92        append_tx.append_bytes(&db.pruned_blobs, &block.block_blob)?;
93
94        for tx in &block.txs {
95            append_tx.append_bytes(&db.pruned_blobs, tx.tx_pruned.as_slice())?;
96
97            let prunable_hash = if tx.tx_prunable_blob.is_empty() || tx.tx.version() == 1 {
98                [0; 32]
99            } else {
100                monero_oxide::primitives::keccak256(&tx.tx_prunable_blob)
101            };
102            append_tx.append_bytes(&db.pruned_blobs, &prunable_hash)?;
103        }
104    }
105
106    tracing::trace!("pruned_tape_index: {}", pruned_tape_index);
107
108    // Split the blocks at the point the pruning stripe changes.
109    let start_height = blocks[0].height;
110    let first_block_pruning_seed = cuprate_pruning::DecompressedPruningSeed::new(
111        cuprate_pruning::get_block_pruning_stripe(
112            start_height,
113            usize::MAX,
114            CRYPTONOTE_PRUNING_LOG_STRIPES,
115        )
116        .unwrap(),
117        CRYPTONOTE_PRUNING_LOG_STRIPES,
118    )
119    .unwrap();
120    let next_stripe_height = first_block_pruning_seed
121        .get_next_pruned_block(start_height, 500_000_000)
122        .unwrap()
123        .unwrap();
124
125    let (first_stripe, next_stripe) =
126        blocks.split_at(min(next_stripe_height - start_height, blocks.len()));
127
128    assert!(
129        next_stripe.len() <= CRYPTONOTE_PRUNING_STRIPE_SIZE,
130        "blocks cross more than 2 pruning stripes"
131    );
132
133    tracing::debug!(
134        start_height,
135        ?first_block_pruning_seed,
136        next_stripe_height,
137        first_stripe_len = first_stripe.len(),
138        next_stripe_len = next_stripe.len()
139    );
140
141    let mut numb_rct_outs = append_tx
142        .fixed_sized_tape_len(&db.rct_outputs)
143        .expect("Required tape not found");
144
145    // Now iterate over chunks of blocks with the same stripe.
146    for blocks in [first_stripe, next_stripe] {
147        if blocks.is_empty() {
148            continue;
149        }
150
151        // Get the stripe and write to the correct v2 prunable tape.
152        let stripe = cuprate_pruning::get_block_pruning_stripe(
153            blocks[0].height,
154            usize::MAX,
155            CRYPTONOTE_PRUNING_LOG_STRIPES,
156        )
157        .unwrap();
158        let mut v2_prunable_index = write_v2_prunable_data(
159            append_tx,
160            &db.prunable_blobs
161                [usize::try_from(stripe).expect("stripe will not exceed usize::MAX") - 1],
162            blocks,
163        )?;
164
165        // Write the v1 prunable tape for any v1 txs.
166        let mut v1_prunable_index = write_v1_prunable_data(append_tx, db, blocks)?;
167
168        tracing::debug!(
169            chunk_start = blocks[0].height,
170            stripe,
171            v1_prunable_index,
172            v2_prunable_index,
173            numb_rct_outs
174        );
175
176        // Now loop over every block in the stripe.
177        for block in blocks {
178            let block_pruned_blob_idx = pruned_tape_index;
179            let block_v1_prunable_idx = v1_prunable_index;
180            let block_v2_prunable_idx = v2_prunable_index;
181
182            let header_len = usize_to_u64(block.block.header.serialize().len());
183
184            // Add the miner tx info to the tapes, recording the index of the miner tx.
185            let mining_tx_index = {
186                let tx = block.block.miner_transaction();
187                add_tx_info_to_tapes(
188                    &tx.clone().into(),
189                    pruned_tape_index + header_len, // The miner tx will be stored after the block header.
190                    0,                              // miner txs will never be pruned
191                    tx.serialize().len(),
192                    0,
193                    &block.height,
194                    &mut numb_rct_outs,
195                    append_tx,
196                    db,
197                )?
198            };
199            // Move the pruned tape index forward by the block size (header, miner tx, tx hash list).
200            pruned_tape_index += usize_to_u64(block.block_blob.len());
201
202            // Loop over all the transactions in the block, adding their info to the tapes.
203            for tx in &block.txs {
204                add_tx_info_to_tapes(
205                    &tx.tx,
206                    pruned_tape_index,
207                    // Pruned part of v1 txs will be stored in a different tape.
208                    if tx.tx.version() == 1 {
209                        v1_prunable_index
210                    } else {
211                        v2_prunable_index
212                    },
213                    tx.tx_pruned.len(),
214                    tx.tx_prunable_blob.len(),
215                    &block.height,
216                    &mut numb_rct_outs,
217                    append_tx,
218                    db,
219                )?;
220
221                // Move the blob tapes forward by the transaction size(s). We add 32 for the pruned tape
222                // as we store the prunable hash in the pruned tape.
223                pruned_tape_index += usize_to_u64(tx.tx_pruned.len()) + 32;
224                if tx.tx.version() == 1 {
225                    v1_prunable_index += usize_to_u64(tx.tx_prunable_blob.len());
226                } else {
227                    v2_prunable_index += usize_to_u64(tx.tx_prunable_blob.len());
228                }
229            }
230
231            // `saturating_add` is used here as cumulative generated coins overflows due to tail emission.
232            let cumulative_generated_coins = append_tx
233                .read_entry(
234                    &db.block_infos,
235                    usize_to_u64(block.height.saturating_sub(1)),
236                )?
237                .map_or(0, |prev| prev.cumulative_generated_coins)
238                .saturating_add(block.generated_coins);
239
240            let (cumulative_difficulty_low, cumulative_difficulty_high) =
241                split_u128_into_low_high_bits(block.cumulative_difficulty);
242
243            // Add the block info to the tapes.
244            append_tx.append_entries(
245                &db.block_infos,
246                &[BlockInfo {
247                    cumulative_difficulty_low,
248                    cumulative_difficulty_high,
249                    cumulative_generated_coins,
250                    cumulative_rct_outs: numb_rct_outs,
251                    block_hash: block.block_hash,
252                    weight: block.weight,
253                    long_term_weight: block.long_term_weight,
254                    mining_tx_index,
255                    pruned_blob_idx: block_pruned_blob_idx,
256                    v1_prunable_blob_idx: block_v1_prunable_idx,
257                    prunable_blob_idx: block_v2_prunable_idx,
258                }],
259            )?;
260
261            tracing::debug!(
262                height = block.height,
263                block_pruned_blob_idx,
264                block_v1_prunable_idx,
265                block_v2_prunable_idx,
266                cumulative_generated_coins,
267                "added block to tapes"
268            );
269        }
270    }
271
272    Ok(())
273}
274
275/// Add a [`VerifiedBlockInformation`] to the dynamic database (fjall).
276///
277/// This extracts all the data from the input block and
278/// maps/adds them to the appropriate database tables.
279///
280/// # Panics
281/// This function will panic if the block is invalid.
282// no inline, too big.
283pub fn add_block_to_dynamic_tables<'a, I: Iterator<Item = Cow<'a, Transaction<Pruned>>>>(
284    db: &BlockchainDatabase,
285    block: &Block,
286    block_hash: &BlockHash,
287    txs: I,
288    numb_transactions: &mut u64,
289    w: &mut fjall::OwnedWriteBatch,
290    pre_rct_numb_outputs_cache: &mut HashMap<Amount, u64>,
291) -> DbResult<()> {
292    // Panic (should never happen) instead of allowing DB corruption.
293    // <https://github.com/Cuprate/cuprate/pull/102#discussion_r1560020991>
294    assert!(
295        u32::try_from(block.number()).is_ok(),
296        "block.height ({}) > u32::MAX",
297        block.number(),
298    );
299
300    // Add the miner transaction first.
301    let tx = block.miner_transaction();
302    add_tx_info_to_dynamic_tables(
303        db,
304        &tx.clone().into(),
305        *numb_transactions,
306        &tx.hash(),
307        &block.number(),
308        w,
309        pre_rct_numb_outputs_cache,
310    )?;
311    *numb_transactions += 1;
312
313    for (tx_hash, tx) in block.transactions.iter().zip(txs) {
314        #[cfg(debug_assertions)]
315        {
316            // Make sure the given tx is correct.
317            let tx_full =
318                crate::ops::tx::get_tx_from_id(numb_transactions, &db.linear_tapes.reader(), db)?;
319
320            let (pruned, _) = tx_full.pruned_with_prunable();
321
322            assert_eq!(tx.as_ref(), &pruned);
323        }
324
325        add_tx_info_to_dynamic_tables(
326            db,
327            &tx,
328            *numb_transactions,
329            tx_hash,
330            &block.number(),
331            w,
332            pre_rct_numb_outputs_cache,
333        )?;
334        *numb_transactions += 1;
335    }
336
337    w.insert(&db.block_heights, block_hash, block.number().to_le_bytes());
338    w.insert(&db.chain_tip, CHAIN_TIP_KEY, block_hash);
339
340    Ok(())
341}
342
343//---------------------------------------------------------------------------------------------------- `pop_block`
344/// Remove the top/latest block from the database.
345///
346/// The removed block's data is returned.
347///
348/// If a [`ChainId`] is specified the popped block will be added to the alt block tables under
349/// that [`ChainId`]. Otherwise, the block will be completely removed from the DB.
350// no inline, too big
351pub fn pop_block(
352    db: &BlockchainDatabase,
353    move_to_alt_chain: Option<ChainId>,
354    tx_rw: &mut fjall::OwnedWriteBatch,
355    tapes: &mut tapes::TapesTruncateTransaction,
356) -> DbResult<(BlockHeight, BlockHash, Block)> {
357    // Pop the last block info.
358    let (block_height, block_info) = tapes
359        .pop_fixed_sized_tape(&db.block_infos)?
360        .ok_or(BlockchainError::NotFound)?;
361
362    let block_height = usize::try_from(block_height).unwrap();
363
364    tx_rw.remove(&db.block_heights, block_info.block_hash);
365
366    let new_top = tapes
367        .read_entry(&db.block_infos, usize_to_u64(block_height - 1))?
368        .ok_or(BlockchainError::NotFound)?;
369    tx_rw.insert(&db.chain_tip, CHAIN_TIP_KEY, new_top.block_hash);
370
371    // Get the block from the database, so we know what txs to remove.
372    let block = get_block(&block_height, Some(&block_info), tapes, db)?;
373    //------------------------------------------------------ Transaction / Outputs / Key Images
374    remove_tx_from_dynamic_tables(db, &block.miner_transaction().hash(), tx_rw, tapes)?;
375
376    let remove_tx_iter = block.transactions.iter().map(|tx_hash| {
377        let (_, tx) = remove_tx_from_dynamic_tables(db, tx_hash, tx_rw, tapes)?;
378        Ok::<_, BlockchainError>(tx)
379    });
380
381    if let Some(chain_id) = move_to_alt_chain {
382        let txs = remove_tx_iter
383            .map(|result| {
384                let tx = result?;
385                let tx_weight = tx.weight();
386                let tx_hash = tx.hash();
387                let fee = tx_fee(&tx);
388                let (tx_pruned, prunable) = tx.pruned_with_prunable();
389
390                Ok(VerifiedTransactionInformation {
391                    tx_weight,
392                    tx_pruned: tx_pruned.serialize(),
393                    tx_prunable_blob: prunable,
394                    tx_hash,
395                    fee,
396                    tx: tx_pruned,
397                })
398            })
399            .collect::<DbResult<Vec<VerifiedTransactionInformation>>>()?;
400
401        alt_block::add_alt_block(
402            db,
403            &AltBlockInformation {
404                block: block.clone(),
405                block_blob: block.serialize(),
406                txs,
407                block_hash: block_info.block_hash,
408                // We know the PoW is valid for this block so just set it so it will always verify as valid.
409                pow_hash: [0; 32],
410                height: block_height,
411                weight: block_info.weight,
412                long_term_weight: block_info.long_term_weight,
413                cumulative_difficulty: combine_low_high_bits_to_u128(
414                    block_info.cumulative_difficulty_low,
415                    block_info.cumulative_difficulty_high,
416                ),
417                chain_id,
418            },
419            tx_rw,
420        )?;
421    } else {
422        for result in remove_tx_iter {
423            drop(result?);
424        }
425    }
426
427    // Truncate the tapes.
428    tapes.truncate_blob_tape(&db.pruned_blobs, block_info.pruned_blob_idx);
429    tapes.truncate_blob_tape(&db.v1_prunable_blobs, block_info.v1_prunable_blob_idx);
430    let stripe = cuprate_pruning::get_block_pruning_stripe(
431        block_height,
432        usize::MAX,
433        CRYPTONOTE_PRUNING_LOG_STRIPES,
434    )
435    .unwrap();
436    tapes.truncate_blob_tape(
437        &db.prunable_blobs[usize::try_from(stripe).expect("stripe will not exceed usize::MAX") - 1],
438        block_info.prunable_blob_idx,
439    );
440
441    tapes.truncate_fixed_sized_tape(&db.tx_infos, block_info.mining_tx_index);
442
443    let cumulative_rct_outs = tapes
444        .read_entry(&db.block_infos, usize_to_u64(block_height) - 1)?
445        .map_or(0, |info| info.cumulative_rct_outs);
446
447    tapes.truncate_fixed_sized_tape(&db.rct_outputs, cumulative_rct_outs);
448
449    Ok((block_height, block_info.block_hash, block))
450}
451
452//---------------------------------------------------------------------------------------------------- `get_block_complete_entry_*`
453/// Retrieve a [`BlockCompleteEntry`] from the database.
454///
455pub fn get_block_complete_entry(
456    db: &BlockchainDatabase,
457    block_hash: &BlockHash,
458    pruned: bool,
459    tx_ro: &fjall::Snapshot,
460    tapes: &tapes::TapesReadTransaction,
461) -> DbResult<BlockCompleteEntry> {
462    let block_height = tx_ro
463        .get(&db.block_heights, block_hash)?
464        .ok_or(BlockchainError::NotFound)?;
465    get_block_complete_entry_from_height(
466        usize::from_le_bytes(block_height.as_ref().try_into().unwrap()),
467        pruned,
468        tapes,
469        db,
470    )
471}
472
473/// Retrieve a [`BlockCompleteEntry`] from the database.
474///
475pub fn get_block_complete_entry_from_height(
476    block_height: BlockHeight,
477    pruned: bool,
478    tapes: &tapes::TapesReadTransaction,
479    db: &BlockchainDatabase,
480) -> DbResult<BlockCompleteEntry> {
481    /// A helper function to read a span of bytes from a tape into an owned buffer.
482    fn read_blob(
483        tapes: &tapes::TapesReadTransaction,
484        tape: &tapes::BlobTape,
485        start: u64,
486        len: usize,
487    ) -> DbResult<Bytes> {
488        if len == 0 {
489            return Ok(Bytes::new());
490        }
491
492        let mut buf = vec![0; len];
493        tapes.read_bytes(tape, start, &mut buf)?;
494        Ok(Bytes::from(buf))
495    }
496
497    let block_info = tapes
498        .read_entry(&db.block_infos, usize_to_u64(block_height))?
499        .ok_or(BlockchainError::NotFound)?;
500
501    let block_blob_start_idx = block_info.pruned_blob_idx;
502    let mut block_blob_end_idx = None;
503
504    let mut txs = Vec::with_capacity(32);
505
506    for tx_info in tapes.iter_from(&db.tx_infos, block_info.mining_tx_index + 1)? {
507        let tx_info = tx_info?;
508
509        if tx_info.height != block_height {
510            break;
511        }
512
513        // Set the `block_blob_end_idx` to the first tx in the block.
514        block_blob_end_idx.get_or_insert(tx_info.pruned_blob_idx);
515
516        txs.push(tx_info);
517    }
518
519    let txs = if txs.is_empty() {
520        TransactionBlobs::None
521    } else {
522        // We do just one big read of the pruned tape to get all txs blobs as they are contiguous.
523        let first_blob_idx = txs.first().unwrap().pruned_blob_idx;
524        let mut bytes = read_blob(
525            tapes,
526            &db.pruned_blobs,
527            first_blob_idx,
528            u64_to_usize(txs.last().unwrap().pruned_blob_idx - first_blob_idx)
529                + txs.last().unwrap().pruned_size
530                + 32,
531        )?;
532
533        if pruned {
534            TransactionBlobs::Pruned(
535                txs.iter()
536                    .map(|tx_info| PrunedTxBlobEntry {
537                        blob: bytes.split_to(tx_info.pruned_size),
538                        prunable_hash: bytes.split_to(32).try_into().unwrap(),
539                    })
540                    .collect(),
541            )
542        } else {
543            let (mut v1_len, mut v2_len) = (0, 0);
544            for t in &txs {
545                if t.is_v1_tx() {
546                    v1_len += t.prunable_size;
547                } else {
548                    v2_len += t.prunable_size;
549                }
550            }
551            let mut v1 = read_blob(
552                tapes,
553                &db.v1_prunable_blobs,
554                block_info.v1_prunable_blob_idx,
555                v1_len,
556            )?;
557            let pruning_stripe = cuprate_pruning::get_block_pruning_stripe(
558                block_height,
559                usize::MAX,
560                CRYPTONOTE_PRUNING_LOG_STRIPES,
561            )
562            .unwrap();
563            let mut v2 = read_blob(
564                tapes,
565                &db.prunable_blobs[usize::try_from(pruning_stripe)
566                    .expect("stripe will not exceed usize::MAX")
567                    - 1],
568                block_info.prunable_blob_idx,
569                v2_len,
570            )?;
571
572            TransactionBlobs::Normal(
573                txs.iter()
574                    .map(|tx_info| {
575                        let pruned = bytes.split_to(tx_info.pruned_size);
576                        bytes.advance(32); // skip the interleaved prunable hash
577                        let prunable = if tx_info.is_v1_tx() {
578                            v1.split_to(tx_info.prunable_size)
579                        } else {
580                            v2.split_to(tx_info.prunable_size)
581                        };
582                        Bytes::from([pruned.as_ref(), prunable.as_ref()].concat())
583                    })
584                    .collect(),
585            )
586        }
587    };
588
589    let block_blob = {
590        let block_blob_end_idx = block_blob_end_idx.map_or_else(
591            || {
592                // If the `block_blob_end_idx` has not been set then there were no txs in the block.
593                // Get the next block's start index or the end of the pruned tape for the end index.
594                let next_block_info =
595                    tapes.read_entry(&db.block_infos, usize_to_u64(block_height + 1))?;
596
597                if let Some(info) = next_block_info {
598                    return Ok::<_, BlockchainError>(info.pruned_blob_idx);
599                }
600
601                Ok(tapes
602                    .blob_tape_len(&db.pruned_blobs)
603                    .expect("Required tape not found"))
604            },
605            Ok,
606        )?;
607
608        read_blob(
609            tapes,
610            &db.pruned_blobs,
611            block_blob_start_idx,
612            u64_to_usize(block_blob_end_idx - block_blob_start_idx),
613        )?
614    };
615
616    Ok(BlockCompleteEntry {
617        block: block_blob,
618        txs,
619        pruned,
620        block_weight: if pruned {
621            usize_to_u64(block_info.weight)
622        } else {
623            0
624        },
625    })
626}
627
628//---------------------------------------------------------------------------------------------------- `get_block_extended_header_*`
629/// Retrieve a [`ExtendedBlockHeader`] from the database.
630///
631/// This extracts all the data from the database tables
632/// needed to create a full `ExtendedBlockHeader`.
633///
634/// # Notes
635/// This is more expensive than [`get_block_extended_header_from_height`]
636#[inline]
637pub fn get_block_extended_header(
638    db: &BlockchainDatabase,
639    block_hash: &BlockHash,
640    tx_ro: &fjall::Snapshot,
641    tapes: &tapes::TapesReadTransaction,
642) -> DbResult<ExtendedBlockHeader> {
643    let block_height = tx_ro
644        .get(&db.block_heights, block_hash)?
645        .ok_or(BlockchainError::NotFound)?;
646
647    get_block_extended_header_from_height(
648        usize::from_le_bytes(block_height.as_ref().try_into().unwrap()),
649        tapes,
650        db,
651    )
652}
653
654/// Same as [`get_block_extended_header`] but with a [`BlockHeight`].
655#[expect(
656    clippy::missing_panics_doc,
657    reason = "The panic is only possible with a corrupt DB"
658)]
659#[inline]
660pub fn get_block_extended_header_from_height(
661    block_height: BlockHeight,
662    tapes: &tapes::TapesReadTransaction,
663    db: &BlockchainDatabase,
664) -> DbResult<ExtendedBlockHeader> {
665    let block_info = tapes
666        .read_entry(&db.block_infos, usize_to_u64(block_height))?
667        .ok_or(BlockchainError::NotFound)?;
668    let miner_tx_info = tapes
669        .read_entry(&db.tx_infos, block_info.mining_tx_index)?
670        .ok_or(BlockchainError::NotFound)?;
671
672    let mut block_header_blob =
673        vec![0; u64_to_usize(miner_tx_info.pruned_blob_idx - block_info.pruned_blob_idx)];
674
675    tapes.read_bytes(
676        &db.pruned_blobs,
677        block_info.pruned_blob_idx,
678        &mut block_header_blob,
679    )?;
680
681    let block_header = BlockHeader::read(&mut block_header_blob.as_slice()).unwrap();
682
683    let cumulative_difficulty = combine_low_high_bits_to_u128(
684        block_info.cumulative_difficulty_low,
685        block_info.cumulative_difficulty_high,
686    );
687
688    Ok(ExtendedBlockHeader {
689        cumulative_difficulty,
690        version: HardFork::from_version(block_header.hardfork_version)
691            .expect("Stored block must have a valid hard-fork"),
692        vote: block_header.hardfork_signal,
693        timestamp: block_header.timestamp,
694        block_weight: block_info.weight,
695        long_term_weight: block_info.long_term_weight,
696    })
697}
698
699/// Return the top/latest [`ExtendedBlockHeader`] from the database.
700#[inline]
701pub fn get_block_extended_header_top(
702    db: &BlockchainDatabase,
703    tapes: &tapes::TapesReadTransaction,
704) -> DbResult<(ExtendedBlockHeader, BlockHeight)> {
705    let height = u64_to_usize(
706        tapes
707            .fixed_sized_tape_len(&db.block_infos)
708            .expect("Required tape not found")
709            .saturating_sub(1),
710    );
711    let header = get_block_extended_header_from_height(height, tapes, db)?;
712    Ok((header, height))
713}
714
715//---------------------------------------------------------------------------------------------------- Block
716/// Retrieve a [`Block`] via its [`BlockHeight`].
717///
718/// If the block info is given, it will not be looked up, which can be useful when the info is popped.
719#[inline]
720pub fn get_block(
721    block_height: &BlockHeight,
722    blocks_info: Option<&BlockInfo>,
723    tapes: &impl TapesRead,
724    db: &BlockchainDatabase,
725) -> DbResult<Block> {
726    let block_info = match blocks_info {
727        Some(blocks_info) => *blocks_info,
728        None => tapes
729            .read_entry(&db.block_infos, usize_to_u64(*block_height))?
730            .ok_or(BlockchainError::NotFound)?,
731    };
732
733    let pruned_end_blob_idx =
734        match tapes.read_entry(&db.tx_infos, block_info.mining_tx_index + 1)? {
735            // First check if the block has a tx then use that start as the block header end.
736            Some(tx_info) if tx_info.height == *block_height => tx_info.pruned_blob_idx,
737            // Otherwise the next tx is in a different block, so use the start of that block header.
738            Some(_) => {
739                tapes
740                    .read_entry(&db.block_infos, usize_to_u64(*block_height + 1))?
741                    .ok_or(BlockchainError::NotFound)?
742                    .pruned_blob_idx
743            }
744            // If this is the top block, and it doesn't have a tx, then use the end of the pruned tape.
745            None => tapes
746                .blob_tape_len(&db.pruned_blobs)
747                .expect("Required tape not found"),
748        };
749
750    let mut blob =
751        vec![0; usize::try_from(pruned_end_blob_idx - block_info.pruned_blob_idx).unwrap()];
752
753    tapes.read_bytes(&db.pruned_blobs, block_info.pruned_blob_idx, &mut blob)?;
754
755    Ok(Block::read(&mut blob.as_slice())?)
756}
757
758/// Retrieve a [`Block`] via its [`BlockHash`].
759#[inline]
760pub fn get_block_by_hash(
761    db: &BlockchainDatabase,
762    block_hash: &BlockHash,
763    tx_ro: &fjall::Snapshot,
764    tapes: &tapes::TapesReadTransaction,
765) -> DbResult<Block> {
766    let block_height = tx_ro
767        .get(&db.block_heights, block_hash)?
768        .ok_or(BlockchainError::NotFound)?;
769
770    get_block(
771        &usize::from_le_bytes(block_height.as_ref().try_into().unwrap()),
772        None,
773        tapes,
774        db,
775    )
776}
777
778/// Retrieve a [`BlockHeight`] via its [`BlockHash`].
779#[inline]
780pub fn get_block_height(
781    db: &BlockchainDatabase,
782    block_hash: &BlockHash,
783    tx_ro: &fjall::Snapshot,
784) -> DbResult<BlockHeight> {
785    let block_height = tx_ro
786        .get(&db.block_heights, block_hash)?
787        .ok_or(BlockchainError::NotFound)?;
788
789    Ok(usize::from_le_bytes(
790        block_height.as_ref().try_into().unwrap(),
791    ))
792}
793
794/// Check if a block exists in the database.
795///
796#[inline]
797pub fn block_exists(
798    db: &BlockchainDatabase,
799    block_hash: &BlockHash,
800    tx_ro: &fjall::Snapshot,
801) -> DbResult<bool> {
802    Ok(tx_ro.contains_key(&db.block_heights, block_hash)?)
803}
804
805/// Returns the height of a block from its hash, only if it is in the main chain.
806///
807pub(crate) fn block_height(
808    db: &BlockchainDatabase,
809    tx_ro: &fjall::Snapshot,
810    hash: &BlockHash,
811) -> DbResult<Option<usize>> {
812    let Some(block_height) = tx_ro.get(&db.block_heights, hash)? else {
813        return Ok(None);
814    };
815
816    Ok(Some(usize::from_le_bytes(
817        block_height.as_ref().try_into().unwrap(),
818    )))
819}