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 let (block_blob, miner_tx_idx, numb_non_miner_txs) =
267 get_block_blob_with_tx_indexes(&block_height, tables)?;
268
269 let first_tx_idx = miner_tx_idx + 1;
270
271 let tx_blobs = (first_tx_idx..(usize_to_u64(numb_non_miner_txs) + first_tx_idx))
272 .map(|idx| {
273 let tx_blob = tables.tx_blobs().get(&idx)?.0;
274
275 Ok(Bytes::from(tx_blob))
276 })
277 .collect::<Result<_, RuntimeError>>()?;
278
279 Ok(BlockCompleteEntry {
280 block: Bytes::from(block_blob),
281 txs: TransactionBlobs::Normal(tx_blobs),
282 pruned: false,
283 block_weight: 0,
284 })
285}
286
287#[doc = doc_error!()]
297#[inline]
298pub fn get_block_extended_header(
299 block_hash: &BlockHash,
300 tables: &impl Tables,
301) -> DbResult<ExtendedBlockHeader> {
302 get_block_extended_header_from_height(&tables.block_heights().get(block_hash)?, tables)
303}
304
305#[doc = doc_error!()]
307#[expect(
308 clippy::missing_panics_doc,
309 reason = "The panic is only possible with a corrupt DB"
310)]
311#[inline]
312pub fn get_block_extended_header_from_height(
313 block_height: &BlockHeight,
314 tables: &impl Tables,
315) -> DbResult<ExtendedBlockHeader> {
316 let block_info = tables.block_infos().get(block_height)?;
317 let block_header_blob = tables.block_header_blobs().get(block_height)?.0;
318 let block_header = BlockHeader::read(&mut block_header_blob.as_slice())?;
319
320 let cumulative_difficulty = combine_low_high_bits_to_u128(
321 block_info.cumulative_difficulty_low,
322 block_info.cumulative_difficulty_high,
323 );
324
325 Ok(ExtendedBlockHeader {
326 cumulative_difficulty,
327 version: HardFork::from_version(block_header.hardfork_version)
328 .expect("Stored block must have a valid hard-fork"),
329 vote: block_header.hardfork_signal,
330 timestamp: block_header.timestamp,
331 block_weight: block_info.weight,
332 long_term_weight: block_info.long_term_weight,
333 })
334}
335
336#[doc = doc_error!()]
338#[inline]
339pub fn get_block_extended_header_top(
340 tables: &impl Tables,
341) -> DbResult<(ExtendedBlockHeader, BlockHeight)> {
342 let height = chain_height(tables.block_heights())?.saturating_sub(1);
343 let header = get_block_extended_header_from_height(&height, tables)?;
344 Ok((header, height))
345}
346
347#[doc = doc_error!()]
350#[inline]
351pub fn get_block_info(
352 block_height: &BlockHeight,
353 table_block_infos: &impl DatabaseRo<BlockInfos>,
354) -> DbResult<BlockInfo> {
355 table_block_infos.get(block_height)
356}
357
358#[doc = doc_error!()]
360#[inline]
361pub fn get_block_height(
362 block_hash: &BlockHash,
363 table_block_heights: &impl DatabaseRo<BlockHeights>,
364) -> DbResult<BlockHeight> {
365 table_block_heights.get(block_hash)
366}
367
368#[inline]
376pub fn block_exists(
377 block_hash: &BlockHash,
378 table_block_heights: &impl DatabaseRo<BlockHeights>,
379) -> DbResult<bool> {
380 table_block_heights.contains(block_hash)
381}
382
383#[cfg(test)]
385#[expect(clippy::too_many_lines)]
386mod test {
387 use pretty_assertions::assert_eq;
388
389 use cuprate_database::{Env, EnvInner, TxRw};
390 use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
391
392 use crate::{
393 ops::tx::{get_tx, tx_exists},
394 tables::OpenTables,
395 tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
396 };
397
398 use super::*;
399
400 #[test]
408 fn all_block_functions() {
409 let (env, _tmp) = tmp_concrete_env();
410 let env_inner = env.env_inner();
411 assert_all_tables_are_empty(&env);
412
413 let mut blocks = [
414 BLOCK_V1_TX2.clone(),
415 BLOCK_V9_TX3.clone(),
416 BLOCK_V16_TX0.clone(),
417 ];
418 for (height, block) in blocks.iter_mut().enumerate() {
421 block.height = height;
422 assert_eq!(block.block.serialize(), block.block_blob);
423 }
424 let generated_coins_sum = blocks
425 .iter()
426 .map(|block| block.generated_coins)
427 .sum::<u64>();
428
429 {
431 let tx_rw = env_inner.tx_rw().unwrap();
432 let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
433
434 for block in &blocks {
435 add_block(block, &mut tables).unwrap();
437 }
438
439 drop(tables);
440 TxRw::commit(tx_rw).unwrap();
441 }
442
443 let block_hashes = {
445 let tx_ro = env_inner.tx_ro().unwrap();
446 let tables = env_inner.open_tables(&tx_ro).unwrap();
447
448 AssertTableLen {
450 block_infos: 3,
451 block_header_blobs: 3,
452 block_txs_hashes: 3,
453 block_heights: 3,
454 key_images: 69,
455 num_outputs: 41,
456 pruned_tx_blobs: 0,
457 prunable_hashes: 0,
458 outputs: 111,
459 prunable_tx_blobs: 0,
460 rct_outputs: 8,
461 tx_blobs: 8,
462 tx_ids: 8,
463 tx_heights: 8,
464 tx_unlock_time: 3,
465 }
466 .assert(&tables);
467
468 assert_eq!(
470 cumulative_generated_coins(&2, tables.block_infos()).unwrap(),
471 generated_coins_sum,
472 );
473
474 let mut block_hashes = vec![];
476 for block in &blocks {
477 println!("blocks.iter(): hash: {}", hex::encode(block.block_hash));
478
479 let height = get_block_height(&block.block_hash, tables.block_heights()).unwrap();
480
481 println!("blocks.iter(): height: {height}");
482
483 assert!(block_exists(&block.block_hash, tables.block_heights()).unwrap());
484
485 let block_header_from_height =
486 get_block_extended_header_from_height(&height, &tables).unwrap();
487 let block_header_from_hash =
488 get_block_extended_header(&block.block_hash, &tables).unwrap();
489
490 let b1 = block_header_from_hash;
492 let b2 = block;
493 assert_eq!(b1, block_header_from_height);
494 assert_eq!(b1.version.as_u8(), b2.block.header.hardfork_version);
495 assert_eq!(b1.vote, b2.block.header.hardfork_signal);
496 assert_eq!(b1.timestamp, b2.block.header.timestamp);
497 assert_eq!(b1.cumulative_difficulty, b2.cumulative_difficulty);
498 assert_eq!(b1.block_weight, b2.weight);
499 assert_eq!(b1.long_term_weight, b2.long_term_weight);
500
501 block_hashes.push(block.block_hash);
502
503 for (i, tx) in block.txs.iter().enumerate() {
505 println!("tx_hash: {:?}", hex::encode(tx.tx_hash));
506
507 assert!(tx_exists(&tx.tx_hash, tables.tx_ids()).unwrap());
508
509 let tx2 = get_tx(&tx.tx_hash, tables.tx_ids(), tables.tx_blobs()).unwrap();
510
511 assert_eq!(tx.tx_blob, tx2.serialize());
512 assert_eq!(tx.tx_weight, tx2.weight());
513 assert_eq!(tx.tx_hash, block.block.transactions[i]);
514 assert_eq!(tx.tx_hash, tx2.hash());
515 }
516 }
517
518 block_hashes
519 };
520
521 {
522 let len = block_hashes.len();
523 let hashes: Vec<String> = block_hashes.iter().map(hex::encode).collect();
524 println!("block_hashes: len: {len}, hashes: {hashes:?}");
525 }
526
527 {
529 let tx_rw = env_inner.tx_rw().unwrap();
530 let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
531
532 for block_hash in block_hashes.into_iter().rev() {
533 println!("pop_block(): block_hash: {}", hex::encode(block_hash));
534
535 let (_popped_height, popped_hash, _popped_block) =
536 pop_block(None, &mut tables).unwrap();
537
538 assert_eq!(block_hash, popped_hash);
539
540 assert!(matches!(
541 get_block_extended_header(&block_hash, &tables),
542 Err(RuntimeError::KeyNotFound)
543 ));
544 }
545
546 drop(tables);
547 TxRw::commit(tx_rw).unwrap();
548 }
549
550 assert_all_tables_are_empty(&env);
551 }
552
553 #[test]
555 #[should_panic(expected = "block.height (4294967296) > u32::MAX")]
556 fn block_height_gt_u32_max() {
557 let (env, _tmp) = tmp_concrete_env();
558 let env_inner = env.env_inner();
559 assert_all_tables_are_empty(&env);
560
561 let tx_rw = env_inner.tx_rw().unwrap();
562 let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
563
564 let mut block = BLOCK_V9_TX3.clone();
565
566 block.height = cuprate_helper::cast::u32_to_usize(u32::MAX) + 1;
567 add_block(&block, &mut tables).unwrap();
568 }
569
570 #[test]
572 #[should_panic(
573 expected = "assertion `left == right` failed: block.height (123) != chain_height (1)\n left: 123\n right: 1"
574 )]
575 fn block_height_not_chain_height() {
576 let (env, _tmp) = tmp_concrete_env();
577 let env_inner = env.env_inner();
578 assert_all_tables_are_empty(&env);
579
580 let tx_rw = env_inner.tx_rw().unwrap();
581 let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
582
583 let mut block = BLOCK_V9_TX3.clone();
584 block.height = 0;
587
588 assert_eq!(block.height, 0);
590 add_block(&block, &mut tables).unwrap();
591
592 block.height = 123;
594 add_block(&block, &mut tables).unwrap();
595 }
596}