1use 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#[doc = doc_error!()]
43pub fn add_block(block: &VerifiedBlockInformation, tables: &mut impl TablesMut) -> DbResult<()> {
50 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 #[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 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 let cumulative_rct_outs = get_rct_num_outputs(tables.rct_outputs())?;
95
96 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 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 tables.block_header_blobs_mut().put(
122 &block.height,
123 StorableVec::wrap_ref(&block.block.header.serialize()),
124 )?;
125
126 tables.block_txs_hashes_mut().put(
128 &block.height,
129 StorableVec::wrap_ref(&block.block.transactions),
130 )?;
131
132 tables
134 .block_heights_mut()
135 .put(&block.block_hash, &block.height)?;
136
137 Ok(())
138}
139
140#[doc = doc_error!()]
148pub fn pop_block(
153 move_to_alt_chain: Option<ChainId>,
154 tables: &mut impl TablesMut,
155) -> DbResult<(BlockHeight, BlockHash, Block)> {
156 let (block_height, block_info) = tables.block_infos_mut().pop_last()?;
159
160 tables.block_heights_mut().delete(&block_info.block_hash)?;
162
163 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 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 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#[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 let mut block = tables.block_header_blobs().get(block_height)?.0;
242
243 let mut miner_tx_blob = tables.tx_blobs().get(&miner_tx_idx)?.0;
245 block.append(&mut miner_tx_blob);
246
247 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 #[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 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 {
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 add_block(block, &mut tables).unwrap();
474 }
475
476 drop(tables);
477 TxRw::commit(tx_rw).unwrap();
478 }
479
480 let block_hashes = {
482 let tx_ro = env_inner.tx_ro().unwrap();
483 let tables = env_inner.open_tables(&tx_ro).unwrap();
484
485 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 assert_eq!(
507 cumulative_generated_coins(&2, tables.block_infos()).unwrap(),
508 generated_coins_sum,
509 );
510
511 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 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 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 {
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 #[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 #[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 block.height = 0;
624
625 assert_eq!(block.height, 0);
627 add_block(&block, &mut tables).unwrap();
628
629 block.height = 123;
631 add_block(&block, &mut tables).unwrap();
632 }
633}