Skip to main content

cuprate_blockchain/ops/
block.rs

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