1use 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#[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 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 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 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 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 for blocks in [first_stripe, next_stripe] {
147 if blocks.is_empty() {
148 continue;
149 }
150
151 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 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 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 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, 0, tx.serialize().len(),
192 0,
193 &block.height,
194 &mut numb_rct_outs,
195 append_tx,
196 db,
197 )?
198 };
199 pruned_tape_index += usize_to_u64(block.block_blob.len());
201
202 for tx in &block.txs {
204 add_tx_info_to_tapes(
205 &tx.tx,
206 pruned_tape_index,
207 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 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 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 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
275pub 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 assert!(
295 u32::try_from(block.number()).is_ok(),
296 "block.height ({}) > u32::MAX",
297 block.number(),
298 );
299
300 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 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
343pub 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 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 let block = get_block(&block_height, Some(&block_info), tapes, db)?;
373 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 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 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
452pub 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
473pub 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 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 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 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); 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 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#[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#[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#[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#[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 Some(tx_info) if tx_info.height == *block_height => tx_info.pruned_blob_idx,
737 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 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#[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#[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#[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
805pub(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}