use bytemuck::TransparentWrapper;
use monero_serai::{
block::{Block, BlockHeader},
transaction::Transaction,
};
use cuprate_database::{
RuntimeError, StorableVec, {DatabaseRo, DatabaseRw},
};
use cuprate_helper::{
map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits},
tx::tx_fee,
};
use cuprate_types::{
AltBlockInformation, ChainId, ExtendedBlockHeader, HardFork, VerifiedBlockInformation,
VerifiedTransactionInformation,
};
use crate::{
ops::{
alt_block,
blockchain::{chain_height, cumulative_generated_coins},
macros::doc_error,
output::get_rct_num_outputs,
tx::{add_tx, remove_tx},
},
tables::{BlockHeights, BlockInfos, Tables, TablesMut},
types::{BlockHash, BlockHeight, BlockInfo},
};
#[doc = doc_error!()]
pub fn add_block(
block: &VerifiedBlockInformation,
tables: &mut impl TablesMut,
) -> Result<(), RuntimeError> {
assert!(
u32::try_from(block.height).is_ok(),
"block.height ({}) > u32::MAX",
block.height,
);
let chain_height = chain_height(tables.block_heights())?;
assert_eq!(
block.height, chain_height,
"block.height ({}) != chain_height ({})",
block.height, chain_height,
);
#[cfg(debug_assertions)]
{
assert_eq!(block.block.serialize(), block.block_blob);
assert_eq!(block.block.transactions.len(), block.txs.len());
for (i, tx) in block.txs.iter().enumerate() {
assert_eq!(tx.tx_blob, tx.tx.serialize());
assert_eq!(tx.tx_hash, block.block.transactions[i]);
}
}
let mining_tx_index = {
let tx = &block.block.miner_transaction;
add_tx(tx, &tx.serialize(), &tx.hash(), &chain_height, tables)?
};
for tx in &block.txs {
add_tx(&tx.tx, &tx.tx_blob, &tx.tx_hash, &chain_height, tables)?;
}
let cumulative_rct_outs = get_rct_num_outputs(tables.rct_outputs())?;
let cumulative_generated_coins =
cumulative_generated_coins(&block.height.saturating_sub(1), tables.block_infos())?
.saturating_add(block.generated_coins);
let (cumulative_difficulty_low, cumulative_difficulty_high) =
split_u128_into_low_high_bits(block.cumulative_difficulty);
tables.block_infos_mut().put(
&block.height,
&BlockInfo {
cumulative_difficulty_low,
cumulative_difficulty_high,
cumulative_generated_coins,
cumulative_rct_outs,
timestamp: block.block.header.timestamp,
block_hash: block.block_hash,
weight: block.weight,
long_term_weight: block.long_term_weight,
mining_tx_index,
},
)?;
tables.block_header_blobs_mut().put(
&block.height,
StorableVec::wrap_ref(&block.block.header.serialize()),
)?;
tables.block_txs_hashes_mut().put(
&block.height,
StorableVec::wrap_ref(&block.block.transactions),
)?;
tables
.block_heights_mut()
.put(&block.block_hash, &block.height)?;
Ok(())
}
#[doc = doc_error!()]
pub fn pop_block(
move_to_alt_chain: Option<ChainId>,
tables: &mut impl TablesMut,
) -> Result<(BlockHeight, BlockHash, Block), RuntimeError> {
let (block_height, block_info) = tables.block_infos_mut().pop_last()?;
tables.block_heights_mut().delete(&block_info.block_hash)?;
let block_header = tables.block_header_blobs_mut().take(&block_height)?.0;
let block_txs_hashes = tables.block_txs_hashes_mut().take(&block_height)?.0;
let miner_transaction = tables.tx_blobs().get(&block_info.mining_tx_index)?.0;
let block = Block {
header: BlockHeader::read(&mut block_header.as_slice())?,
miner_transaction: Transaction::read(&mut miner_transaction.as_slice())?,
transactions: block_txs_hashes,
};
remove_tx(&block.miner_transaction.hash(), tables)?;
let remove_tx_iter = block.transactions.iter().map(|tx_hash| {
let (_, tx) = remove_tx(tx_hash, tables)?;
Ok::<_, RuntimeError>(tx)
});
if let Some(chain_id) = move_to_alt_chain {
let txs = remove_tx_iter
.map(|result| {
let tx = result?;
Ok(VerifiedTransactionInformation {
tx_weight: tx.weight(),
tx_blob: tx.serialize(),
tx_hash: tx.hash(),
fee: tx_fee(&tx),
tx,
})
})
.collect::<Result<Vec<VerifiedTransactionInformation>, RuntimeError>>()?;
alt_block::add_alt_block(
&AltBlockInformation {
block: block.clone(),
block_blob: block.serialize(),
txs,
block_hash: block_info.block_hash,
pow_hash: [0; 32],
height: block_height,
weight: block_info.weight,
long_term_weight: block_info.long_term_weight,
cumulative_difficulty: combine_low_high_bits_to_u128(
block_info.cumulative_difficulty_low,
block_info.cumulative_difficulty_high,
),
chain_id,
},
tables,
)?;
} else {
for result in remove_tx_iter {
drop(result?);
}
}
Ok((block_height, block_info.block_hash, block))
}
#[doc = doc_error!()]
#[inline]
pub fn get_block_extended_header(
block_hash: &BlockHash,
tables: &impl Tables,
) -> Result<ExtendedBlockHeader, RuntimeError> {
get_block_extended_header_from_height(&tables.block_heights().get(block_hash)?, tables)
}
#[doc = doc_error!()]
#[expect(
clippy::missing_panics_doc,
reason = "The panic is only possible with a corrupt DB"
)]
#[inline]
pub fn get_block_extended_header_from_height(
block_height: &BlockHeight,
tables: &impl Tables,
) -> Result<ExtendedBlockHeader, RuntimeError> {
let block_info = tables.block_infos().get(block_height)?;
let block_header_blob = tables.block_header_blobs().get(block_height)?.0;
let block_header = BlockHeader::read(&mut block_header_blob.as_slice())?;
let cumulative_difficulty = combine_low_high_bits_to_u128(
block_info.cumulative_difficulty_low,
block_info.cumulative_difficulty_high,
);
Ok(ExtendedBlockHeader {
cumulative_difficulty,
version: HardFork::from_version(block_header.hardfork_version)
.expect("Stored block must have a valid hard-fork"),
vote: block_header.hardfork_signal,
timestamp: block_header.timestamp,
block_weight: block_info.weight,
long_term_weight: block_info.long_term_weight,
})
}
#[doc = doc_error!()]
#[inline]
pub fn get_block_extended_header_top(
tables: &impl Tables,
) -> Result<(ExtendedBlockHeader, BlockHeight), RuntimeError> {
let height = chain_height(tables.block_heights())?.saturating_sub(1);
let header = get_block_extended_header_from_height(&height, tables)?;
Ok((header, height))
}
#[doc = doc_error!()]
#[inline]
pub fn get_block_info(
block_height: &BlockHeight,
table_block_infos: &impl DatabaseRo<BlockInfos>,
) -> Result<BlockInfo, RuntimeError> {
table_block_infos.get(block_height)
}
#[doc = doc_error!()]
#[inline]
pub fn get_block_height(
block_hash: &BlockHash,
table_block_heights: &impl DatabaseRo<BlockHeights>,
) -> Result<BlockHeight, RuntimeError> {
table_block_heights.get(block_hash)
}
#[inline]
pub fn block_exists(
block_hash: &BlockHash,
table_block_heights: &impl DatabaseRo<BlockHeights>,
) -> Result<bool, RuntimeError> {
table_block_heights.contains(block_hash)
}
#[cfg(test)]
#[expect(clippy::too_many_lines)]
mod test {
use pretty_assertions::assert_eq;
use cuprate_database::{Env, EnvInner, TxRw};
use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
use crate::{
ops::tx::{get_tx, tx_exists},
tables::OpenTables,
tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
};
use super::*;
#[test]
fn all_block_functions() {
let (env, _tmp) = tmp_concrete_env();
let env_inner = env.env_inner();
assert_all_tables_are_empty(&env);
let mut blocks = [
BLOCK_V1_TX2.clone(),
BLOCK_V9_TX3.clone(),
BLOCK_V16_TX0.clone(),
];
for (height, block) in blocks.iter_mut().enumerate() {
block.height = height;
assert_eq!(block.block.serialize(), block.block_blob);
}
let generated_coins_sum = blocks
.iter()
.map(|block| block.generated_coins)
.sum::<u64>();
{
let tx_rw = env_inner.tx_rw().unwrap();
let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
for block in &blocks {
add_block(block, &mut tables).unwrap();
}
drop(tables);
TxRw::commit(tx_rw).unwrap();
}
let block_hashes = {
let tx_ro = env_inner.tx_ro().unwrap();
let tables = env_inner.open_tables(&tx_ro).unwrap();
AssertTableLen {
block_infos: 3,
block_header_blobs: 3,
block_txs_hashes: 3,
block_heights: 3,
key_images: 69,
num_outputs: 41,
pruned_tx_blobs: 0,
prunable_hashes: 0,
outputs: 111,
prunable_tx_blobs: 0,
rct_outputs: 8,
tx_blobs: 8,
tx_ids: 8,
tx_heights: 8,
tx_unlock_time: 3,
}
.assert(&tables);
assert_eq!(
cumulative_generated_coins(&2, tables.block_infos()).unwrap(),
generated_coins_sum,
);
let mut block_hashes = vec![];
for block in &blocks {
println!("blocks.iter(): hash: {}", hex::encode(block.block_hash));
let height = get_block_height(&block.block_hash, tables.block_heights()).unwrap();
println!("blocks.iter(): height: {height}");
assert!(block_exists(&block.block_hash, tables.block_heights()).unwrap());
let block_header_from_height =
get_block_extended_header_from_height(&height, &tables).unwrap();
let block_header_from_hash =
get_block_extended_header(&block.block_hash, &tables).unwrap();
let b1 = block_header_from_hash;
let b2 = block;
assert_eq!(b1, block_header_from_height);
assert_eq!(b1.version.as_u8(), b2.block.header.hardfork_version);
assert_eq!(b1.vote, b2.block.header.hardfork_signal);
assert_eq!(b1.timestamp, b2.block.header.timestamp);
assert_eq!(b1.cumulative_difficulty, b2.cumulative_difficulty);
assert_eq!(b1.block_weight, b2.weight);
assert_eq!(b1.long_term_weight, b2.long_term_weight);
block_hashes.push(block.block_hash);
for (i, tx) in block.txs.iter().enumerate() {
println!("tx_hash: {:?}", hex::encode(tx.tx_hash));
assert!(tx_exists(&tx.tx_hash, tables.tx_ids()).unwrap());
let tx2 = get_tx(&tx.tx_hash, tables.tx_ids(), tables.tx_blobs()).unwrap();
assert_eq!(tx.tx_blob, tx2.serialize());
assert_eq!(tx.tx_weight, tx2.weight());
assert_eq!(tx.tx_hash, block.block.transactions[i]);
assert_eq!(tx.tx_hash, tx2.hash());
}
}
block_hashes
};
{
let len = block_hashes.len();
let hashes: Vec<String> = block_hashes.iter().map(hex::encode).collect();
println!("block_hashes: len: {len}, hashes: {hashes:?}");
}
{
let tx_rw = env_inner.tx_rw().unwrap();
let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
for block_hash in block_hashes.into_iter().rev() {
println!("pop_block(): block_hash: {}", hex::encode(block_hash));
let (_popped_height, popped_hash, _popped_block) =
pop_block(None, &mut tables).unwrap();
assert_eq!(block_hash, popped_hash);
assert!(matches!(
get_block_extended_header(&block_hash, &tables),
Err(RuntimeError::KeyNotFound)
));
}
drop(tables);
TxRw::commit(tx_rw).unwrap();
}
assert_all_tables_are_empty(&env);
}
#[test]
#[should_panic(expected = "block.height (4294967296) > u32::MAX")]
fn block_height_gt_u32_max() {
let (env, _tmp) = tmp_concrete_env();
let env_inner = env.env_inner();
assert_all_tables_are_empty(&env);
let tx_rw = env_inner.tx_rw().unwrap();
let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
let mut block = BLOCK_V9_TX3.clone();
block.height = cuprate_helper::cast::u32_to_usize(u32::MAX) + 1;
add_block(&block, &mut tables).unwrap();
}
#[test]
#[should_panic(
expected = "assertion `left == right` failed: block.height (123) != chain_height (1)\n left: 123\n right: 1"
)]
fn block_height_not_chain_height() {
let (env, _tmp) = tmp_concrete_env();
let env_inner = env.env_inner();
assert_all_tables_are_empty(&env);
let tx_rw = env_inner.tx_rw().unwrap();
let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
let mut block = BLOCK_V9_TX3.clone();
block.height = 0;
assert_eq!(block.height, 0);
add_block(&block, &mut tables).unwrap();
block.height = 123;
add_block(&block, &mut tables).unwrap();
}
}