diff --git a/Cargo.lock b/Cargo.lock index 7ccfc6be7..0035f074a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,6 +543,7 @@ dependencies = [ "monero-serai", "pretty_assertions", "proptest", + "rand", "rayon", "serde", "tempfile", diff --git a/helper/Cargo.toml b/helper/Cargo.toml index c74e40fd4..4bd17ca89 100644 --- a/helper/Cargo.toml +++ b/helper/Cargo.toml @@ -9,8 +9,9 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/consensus" [features] +# TODO: I don't think this is a good idea # All features on by default. -default = ["std", "atomic", "asynch", "cast", "fs", "num", "map", "time", "thread", "constants"] +default = ["std", "atomic", "asynch", "cast", "fs", "num", "map", "time", "thread", "constants", "tx-utils"] std = [] atomic = ["dep:crossbeam"] asynch = ["dep:futures", "dep:rayon"] @@ -21,6 +22,7 @@ num = [] map = ["cast", "dep:monero-serai"] time = ["dep:chrono", "std"] thread = ["std", "dep:target_os_lib"] +tx-utils = ["dep:monero-serai"] [dependencies] crossbeam = { workspace = true, optional = true } diff --git a/helper/src/lib.rs b/helper/src/lib.rs index de0d95558..e82ec827d 100644 --- a/helper/src/lib.rs +++ b/helper/src/lib.rs @@ -31,6 +31,8 @@ pub mod thread; #[cfg(feature = "time")] pub mod time; +#[cfg(feature = "tx-utils")] +pub mod tx_utils; //---------------------------------------------------------------------------------------------------- Private Usage //---------------------------------------------------------------------------------------------------- diff --git a/helper/src/tx_utils.rs b/helper/src/tx_utils.rs new file mode 100644 index 000000000..aeccf32b2 --- /dev/null +++ b/helper/src/tx_utils.rs @@ -0,0 +1,34 @@ +//! Utils for working with [`Transaction`] + +use monero_serai::transaction::{Input, Transaction}; + +/// Calculates the fee of the [`Transaction`]. +/// +/// # Panics +/// This will panic if the inputs overflow or the transaction outputs too much, so should only +/// be used on known to be valid txs. +pub fn tx_fee(tx: &Transaction) -> u64 { + let mut fee = 0_u64; + + match &tx { + Transaction::V1 { prefix, .. } => { + for input in &prefix.inputs { + match input { + Input::Gen(_) => return 0, + Input::ToKey { amount, .. } => { + fee = fee.checked_add(amount.unwrap_or(0)).unwrap(); + } + } + } + + for output in &prefix.outputs { + fee.checked_sub(output.amount.unwrap_or(0)).unwrap(); + } + } + Transaction::V2 { proofs, .. } => { + fee = proofs.as_ref().unwrap().base.fee; + } + }; + + fee +} diff --git a/storage/blockchain/Cargo.toml b/storage/blockchain/Cargo.toml index e0399033c..46b8414d6 100644 --- a/storage/blockchain/Cargo.toml +++ b/storage/blockchain/Cargo.toml @@ -25,11 +25,12 @@ cuprate-database = { path = "../database" } cuprate-database-service = { path = "../service" } cuprate-helper = { path = "../../helper", features = ["fs", "thread", "map"] } cuprate-types = { path = "../../types", features = ["blockchain"] } +cuprate-pruning = { path = "../../pruning" } bitflags = { workspace = true, features = ["std", "serde", "bytemuck"] } bytemuck = { workspace = true, features = ["must_cast", "derive", "min_const_generics", "extern_crate_alloc"] } curve25519-dalek = { workspace = true } -cuprate-pruning = { path = "../../pruning" } +rand = { workspace = true } monero-serai = { workspace = true, features = ["std"] } serde = { workspace = true, optional = true } diff --git a/storage/blockchain/src/ops/alt_block/block.rs b/storage/blockchain/src/ops/alt_block/block.rs new file mode 100644 index 000000000..5bc052b74 --- /dev/null +++ b/storage/blockchain/src/ops/alt_block/block.rs @@ -0,0 +1,317 @@ +use bytemuck::TransparentWrapper; +use monero_serai::block::{Block, BlockHeader}; + +use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; +use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; +use cuprate_types::{AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, HardFork}; + +use crate::{ + ops::{ + alt_block::{add_alt_transaction_blob, get_alt_transaction, update_alt_chain_info}, + block::get_block_info, + macros::doc_error, + }, + tables::{Tables, TablesMut}, + types::{AltBlockHeight, BlockHash, BlockHeight, CompactAltBlockInfo}, +}; + +/// Add a [`AltBlockInformation`] to the database. +/// +/// This extracts all the data from the input block and +/// maps/adds them to the appropriate database tables. +/// +#[doc = doc_error!()] +/// +/// # Panics +/// This function will panic if: +/// - `alt_block.height` is == `0` +/// - `alt_block.txs.len()` != `alt_block.block.transactions.len()` +/// +pub fn add_alt_block( + alt_block: &AltBlockInformation, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + let alt_block_height = AltBlockHeight { + chain_id: alt_block.chain_id.into(), + height: alt_block.height, + }; + + tables + .alt_block_heights_mut() + .put(&alt_block.block_hash, &alt_block_height)?; + + update_alt_chain_info(&alt_block_height, &alt_block.block.header.previous, tables)?; + + let (cumulative_difficulty_low, cumulative_difficulty_high) = + split_u128_into_low_high_bits(alt_block.cumulative_difficulty); + + let alt_block_info = CompactAltBlockInfo { + block_hash: alt_block.block_hash, + pow_hash: alt_block.pow_hash, + height: alt_block.height, + weight: alt_block.weight, + long_term_weight: alt_block.long_term_weight, + cumulative_difficulty_low, + cumulative_difficulty_high, + }; + + tables + .alt_blocks_info_mut() + .put(&alt_block_height, &alt_block_info)?; + + tables.alt_block_blobs_mut().put( + &alt_block_height, + StorableVec::wrap_ref(&alt_block.block_blob), + )?; + + assert_eq!(alt_block.txs.len(), alt_block.block.transactions.len()); + for tx in &alt_block.txs { + add_alt_transaction_blob(tx, tables)?; + } + + Ok(()) +} + +/// Retrieves an [`AltBlockInformation`] from the database. +/// +/// This function will look at only the blocks with the given [`AltBlockHeight::chain_id`], no others +/// even if they are technically part of this chain. +#[doc = doc_error!()] +pub fn get_alt_block( + alt_block_height: &AltBlockHeight, + tables: &impl Tables, +) -> Result { + let block_info = tables.alt_blocks_info().get(alt_block_height)?; + + let block_blob = tables.alt_block_blobs().get(alt_block_height)?.0; + + let block = Block::read(&mut block_blob.as_slice())?; + + let txs = block + .transactions + .iter() + .map(|tx_hash| get_alt_transaction(tx_hash, tables)) + .collect::>()?; + + Ok(AltBlockInformation { + block, + block_blob, + txs, + block_hash: block_info.block_hash, + pow_hash: block_info.pow_hash, + height: block_info.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: alt_block_height.chain_id.into(), + }) +} + +/// Retrieves the hash of the block at the given `block_height` on the alt chain with +/// the given [`ChainId`]. +/// +/// This function will get blocks from the whole chain, for example if you were to ask for height +/// `0` with any [`ChainId`] (as long that chain actually exists) you will get the main chain genesis. +/// +#[doc = doc_error!()] +pub fn get_alt_block_hash( + block_height: &BlockHeight, + alt_chain: ChainId, + tables: &impl Tables, +) -> Result { + let alt_chains = tables.alt_chain_infos(); + + // First find what [`ChainId`] this block would be stored under. + let original_chain = { + let mut chain = alt_chain.into(); + loop { + let chain_info = alt_chains.get(&chain)?; + + if chain_info.common_ancestor_height < *block_height { + break Chain::Alt(chain.into()); + } + + match chain_info.parent_chain.into() { + Chain::Main => break Chain::Main, + Chain::Alt(alt_chain_id) => { + chain = alt_chain_id.into(); + continue; + } + } + } + }; + + // Get the block hash. + match original_chain { + Chain::Main => { + get_block_info(block_height, tables.block_infos()).map(|info| info.block_hash) + } + Chain::Alt(chain_id) => tables + .alt_blocks_info() + .get(&AltBlockHeight { + chain_id: chain_id.into(), + height: *block_height, + }) + .map(|info| info.block_hash), + } +} + +/// Retrieves the [`ExtendedBlockHeader`] of the alt-block with an exact [`AltBlockHeight`]. +/// +/// This function will look at only the blocks with the given [`AltBlockHeight::chain_id`], no others +/// even if they are technically part of this chain. +/// +#[doc = doc_error!()] +pub fn get_alt_block_extended_header_from_height( + height: &AltBlockHeight, + table: &impl Tables, +) -> Result { + let block_info = table.alt_blocks_info().get(height)?; + + let block_blob = table.alt_block_blobs().get(height)?.0; + + let block_header = BlockHeader::read(&mut block_blob.as_slice())?; + + Ok(ExtendedBlockHeader { + version: HardFork::from_version(block_header.hardfork_version) + .expect("Block in DB must have correct version"), + vote: block_header.hardfork_version, + timestamp: block_header.timestamp, + cumulative_difficulty: combine_low_high_bits_to_u128( + block_info.cumulative_difficulty_low, + block_info.cumulative_difficulty_high, + ), + block_weight: block_info.weight, + long_term_weight: block_info.long_term_weight, + }) +} + +#[cfg(test)] +mod tests { + use std::num::NonZero; + + use cuprate_database::{Env, EnvInner, TxRw}; + use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3}; + use cuprate_types::{Chain, ChainId}; + + use crate::{ + ops::{ + alt_block::{ + add_alt_block, flush_alt_blocks, get_alt_block, + get_alt_block_extended_header_from_height, get_alt_block_hash, + get_alt_chain_history_ranges, + }, + block::{add_block, pop_block}, + }, + tables::{OpenTables, Tables}, + tests::{assert_all_tables_are_empty, map_verified_block_to_alt, tmp_concrete_env}, + types::AltBlockHeight, + }; + + #[allow(clippy::range_plus_one)] + #[test] + fn all_alt_blocks() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + let chain_id = ChainId(NonZero::new(1).unwrap()); + + // Add initial block. + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + let mut initial_block = BLOCK_V1_TX2.clone(); + initial_block.height = 0; + + add_block(&initial_block, &mut tables).unwrap(); + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + let alt_blocks = [ + map_verified_block_to_alt(BLOCK_V9_TX3.clone(), chain_id), + map_verified_block_to_alt(BLOCK_V16_TX0.clone(), chain_id), + ]; + + // Add alt-blocks + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + let mut prev_hash = BLOCK_V1_TX2.block_hash; + for (i, mut alt_block) in alt_blocks.into_iter().enumerate() { + let height = i + 1; + + alt_block.height = height; + alt_block.block.header.previous = prev_hash; + alt_block.block_blob = alt_block.block.serialize(); + + add_alt_block(&alt_block, &mut tables).unwrap(); + + let alt_height = AltBlockHeight { + chain_id: chain_id.into(), + height, + }; + + let alt_block_2 = get_alt_block(&alt_height, &tables).unwrap(); + assert_eq!(alt_block.block, alt_block_2.block); + + let headers = get_alt_chain_history_ranges( + 0..(height + 1), + chain_id, + tables.alt_chain_infos(), + ) + .unwrap(); + + assert_eq!(headers.len(), 2); + assert_eq!(headers[1], (Chain::Main, 0..1)); + assert_eq!(headers[0], (Chain::Alt(chain_id), 1..(height + 1))); + + prev_hash = alt_block.block_hash; + + let header = + get_alt_block_extended_header_from_height(&alt_height, &tables).unwrap(); + + assert_eq!(header.timestamp, alt_block.block.header.timestamp); + assert_eq!(header.block_weight, alt_block.weight); + assert_eq!(header.long_term_weight, alt_block.long_term_weight); + assert_eq!( + header.cumulative_difficulty, + alt_block.cumulative_difficulty + ); + assert_eq!( + header.version.as_u8(), + alt_block.block.header.hardfork_version + ); + assert_eq!(header.vote, alt_block.block.header.hardfork_signal); + + let block_hash = get_alt_block_hash(&height, chain_id, &tables).unwrap(); + + assert_eq!(block_hash, alt_block.block_hash); + } + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + { + let mut tx_rw = env_inner.tx_rw().unwrap(); + + flush_alt_blocks(&env_inner, &mut tx_rw).unwrap(); + + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + pop_block(None, &mut tables).unwrap(); + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + assert_all_tables_are_empty(&env); + } +} diff --git a/storage/blockchain/src/ops/alt_block/chain.rs b/storage/blockchain/src/ops/alt_block/chain.rs new file mode 100644 index 000000000..3e27d7d4b --- /dev/null +++ b/storage/blockchain/src/ops/alt_block/chain.rs @@ -0,0 +1,93 @@ +use std::cmp::max; + +use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError}; +use cuprate_types::{Chain, ChainId}; + +use crate::{ + ops::macros::{doc_add_alt_block_inner_invariant, doc_error}, + tables::{AltChainInfos, TablesMut}, + types::{AltBlockHeight, AltChainInfo, BlockHash, BlockHeight}, +}; + +/// Updates the [`AltChainInfo`] with information on a new alt-block. +/// +#[doc = doc_add_alt_block_inner_invariant!()] +#[doc = doc_error!()] +/// +/// # Panics +/// +/// This will panic if [`AltBlockHeight::height`] == `0`. +pub fn update_alt_chain_info( + alt_block_height: &AltBlockHeight, + prev_hash: &BlockHash, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + // try update the info if one exists for this chain. + let update = tables + .alt_chain_infos_mut() + .update(&alt_block_height.chain_id, |mut info| { + info.chain_height = alt_block_height.height + 1; + Some(info) + }); + + match update { + Ok(()) => return Ok(()), + Err(RuntimeError::KeyNotFound) => (), + Err(e) => return Err(e), + } + + // If one doesn't already exist add it. + + let parent_chain = match tables.alt_block_heights().get(prev_hash) { + Ok(alt_parent_height) => Chain::Alt(alt_parent_height.chain_id.into()), + Err(RuntimeError::KeyNotFound) => Chain::Main, + Err(e) => return Err(e), + }; + + tables.alt_chain_infos_mut().put( + &alt_block_height.chain_id, + &AltChainInfo { + parent_chain: parent_chain.into(), + common_ancestor_height: alt_block_height.height.checked_sub(1).unwrap(), + chain_height: alt_block_height.height + 1, + }, + ) +} + +/// Get the height history of an alt-chain in reverse chronological order. +/// +/// Height history is a list of height ranges with the corresponding [`Chain`] they are stored under. +/// For example if your range goes from height `0` the last entry in the list will be [`Chain::Main`] +/// upto the height where the first split occurs. +#[doc = doc_error!()] +pub fn get_alt_chain_history_ranges( + range: std::ops::Range, + alt_chain: ChainId, + alt_chain_infos: &impl DatabaseRo, +) -> Result)>, RuntimeError> { + let mut ranges = Vec::with_capacity(5); + + let mut i = range.end; + let mut current_chain_id = alt_chain.into(); + while i > range.start { + let chain_info = alt_chain_infos.get(¤t_chain_id)?; + + let start_height = max(range.start, chain_info.common_ancestor_height + 1); + + ranges.push((Chain::Alt(current_chain_id.into()), start_height..i)); + i = chain_info.common_ancestor_height + 1; + + match chain_info.parent_chain.into() { + Chain::Main => { + ranges.push((Chain::Main, range.start..i)); + break; + } + Chain::Alt(alt_chain_id) => { + current_chain_id = alt_chain_id.into(); + continue; + } + } + } + + Ok(ranges) +} diff --git a/storage/blockchain/src/ops/alt_block/mod.rs b/storage/blockchain/src/ops/alt_block/mod.rs new file mode 100644 index 000000000..96c1cecff --- /dev/null +++ b/storage/blockchain/src/ops/alt_block/mod.rs @@ -0,0 +1,75 @@ +//! Alternative Block/Chain Ops +//! +//! Alternative chains are chains that potentially have more proof-of-work than the main-chain +//! which we are tracking to potentially re-org to. +//! +//! Cuprate uses an ID system for alt-chains. When a split is made from the main-chain we generate +//! a random [`ChainID`](cuprate_types::ChainId) and assign it to the chain: +//! +//! ```text +//! | +//! | +//! | split +//! |------------- +//! | | +//! | | +//! \|/ \|/ +//! main-chain ChainID(X) +//! ``` +//! +//! In that example if we were to receive an alt-block which immediately follows the top block of `ChainID(X)` +//! then that block will also be stored under `ChainID(X)`. However, if it follows from another block from `ChainID(X)` +//! we will split into a chain with a different ID: +//! +//! ```text +//! | +//! | +//! | split +//! |------------- +//! | | split +//! | |-------------| +//! | | | +//! | | | +//! | | | +//! \|/ \|/ \|/ +//! main-chain ChainID(X) ChainID(Z) +//! ``` +//! +//! As you can see if we wanted to get all the alt-blocks in `ChainID(Z)` that now includes some blocks from `ChainID(X)` as well. +//! [`get_alt_chain_history_ranges`] covers this and is the method to get the ranges of heights needed from each [`ChainID`](cuprate_types::ChainId) +//! to get all the alt-blocks in a given [`ChainID`](cuprate_types::ChainId). +//! +//! Although this should be kept in mind as a possibility, because Cuprate's block downloader will only track a single chain it is +//! unlikely that we will be tracking [`ChainID`](cuprate_types::ChainId) that don't immediately connect to the main-chain. +//! +//! ## Why not use block's previous field? +//! +//! Although that would be easier, it makes getting a range of block extremely slow, as we have to build the weight cache to verify +//! blocks, roughly 100,000 block headers needed, this cost was seen as too high. +pub use block::*; +pub use chain::*; +pub use tx::*; + +mod block; +mod chain; +mod tx; + +/// Flush all alt-block data from all the alt-block tables. +/// +/// This function completely empties the alt block tables. +pub fn flush_alt_blocks<'a, E: cuprate_database::EnvInner<'a>>( + env_inner: &E, + tx_rw: &mut E::Rw<'_>, +) -> Result<(), cuprate_database::RuntimeError> { + use crate::tables::{ + AltBlockBlobs, AltBlockHeights, AltBlocksInfo, AltChainInfos, AltTransactionBlobs, + AltTransactionInfos, + }; + + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw) +} diff --git a/storage/blockchain/src/ops/alt_block/tx.rs b/storage/blockchain/src/ops/alt_block/tx.rs new file mode 100644 index 000000000..aa2e82f40 --- /dev/null +++ b/storage/blockchain/src/ops/alt_block/tx.rs @@ -0,0 +1,74 @@ +use bytemuck::TransparentWrapper; +use monero_serai::transaction::Transaction; + +use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; +use cuprate_types::VerifiedTransactionInformation; + +use crate::{ + ops::macros::{doc_add_alt_block_inner_invariant, doc_error}, + tables::{Tables, TablesMut}, + types::{AltTransactionInfo, TxHash}, +}; + +/// Adds a [`VerifiedTransactionInformation`] form an alt-block to the DB, if +/// that transaction is not already in the DB. +/// +/// If the transaction is in the main-chain this function will still fill in the +/// [`AltTransactionInfos`](crate::tables::AltTransactionInfos) table, as that +/// table holds data which we don't keep around for main-chain txs. +/// +#[doc = doc_add_alt_block_inner_invariant!()] +#[doc = doc_error!()] +pub fn add_alt_transaction_blob( + tx: &VerifiedTransactionInformation, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + tables.alt_transaction_infos_mut().put( + &tx.tx_hash, + &AltTransactionInfo { + tx_weight: tx.tx_weight, + fee: tx.fee, + tx_hash: tx.tx_hash, + }, + )?; + + if tables.tx_ids().get(&tx.tx_hash).is_ok() + || tables.alt_transaction_blobs().get(&tx.tx_hash).is_ok() + { + return Ok(()); + } + + tables + .alt_transaction_blobs_mut() + .put(&tx.tx_hash, StorableVec::wrap_ref(&tx.tx_blob)) +} + +/// Retrieve a [`VerifiedTransactionInformation`] from the database. +/// +#[doc = doc_error!()] +pub fn get_alt_transaction( + tx_hash: &TxHash, + tables: &impl Tables, +) -> Result { + let tx_info = tables.alt_transaction_infos().get(tx_hash)?; + + let tx_blob = match tables.alt_transaction_blobs().get(tx_hash) { + Ok(blob) => blob.0, + Err(RuntimeError::KeyNotFound) => { + let tx_id = tables.tx_ids().get(tx_hash)?; + + let blob = tables.tx_blobs().get(&tx_id)?; + + blob.0 + } + Err(e) => return Err(e), + }; + + Ok(VerifiedTransactionInformation { + tx: Transaction::read(&mut tx_blob.as_slice()).unwrap(), + tx_blob, + tx_weight: tx_info.tx_weight, + fee: tx_info.fee, + tx_hash: tx_info.tx_hash, + }) +} diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index d1b83a456..295869257 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -2,16 +2,23 @@ //---------------------------------------------------------------------------------------------------- Import use bytemuck::TransparentWrapper; -use monero_serai::block::Block; +use monero_serai::block::{Block, BlockHeader}; use cuprate_database::{ RuntimeError, StorableVec, {DatabaseRo, DatabaseRw}, }; -use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; -use cuprate_types::{ExtendedBlockHeader, HardFork, VerifiedBlockInformation}; +use cuprate_helper::{ + map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}, + tx_utils::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, @@ -33,11 +40,6 @@ use crate::{ /// This function will panic if: /// - `block.height > u32::MAX` (not normally possible) /// - `block.height` is not != [`chain_height`] -/// -/// # Already exists -/// This function will operate normally even if `block` already -/// exists, i.e., this function will not return `Err` even if you -/// call this function infinitely with the same block. // no inline, too big. pub fn add_block( block: &VerifiedBlockInformation, @@ -106,9 +108,8 @@ pub fn add_block( cumulative_rct_outs, timestamp: block.block.header.timestamp, block_hash: block.block_hash, - // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` - weight: block.weight as u64, - long_term_weight: block.long_term_weight as u64, + weight: block.weight, + long_term_weight: block.long_term_weight, }, )?; @@ -129,23 +130,24 @@ pub fn add_block( /// Remove the top/latest block from the database. /// /// The removed block's data is returned. +/// +/// If a [`ChainId`] is specified the popped block will be added to the alt block tables under +/// that [`ChainId`]. Otherwise, the block will be completely removed from the DB. #[doc = doc_error!()] /// /// In `pop_block()`'s case, [`RuntimeError::KeyNotFound`] /// will be returned if there are no blocks left. // no inline, too big pub fn pop_block( + move_to_alt_chain: Option, tables: &mut impl TablesMut, ) -> Result<(BlockHeight, BlockHash, Block), RuntimeError> { //------------------------------------------------------ Block Info // Remove block data from tables. - let (block_height, block_hash) = { - let (block_height, block_info) = tables.block_infos_mut().pop_last()?; - (block_height, block_info.block_hash) - }; + let (block_height, block_info) = tables.block_infos_mut().pop_last()?; // Block heights. - tables.block_heights_mut().delete(&block_hash)?; + tables.block_heights_mut().delete(&block_info.block_hash)?; // Block blobs. // We deserialize the block blob into a `Block`, such @@ -154,12 +156,47 @@ pub fn pop_block( let block = Block::read(&mut block_blob.as_slice())?; //------------------------------------------------------ Transaction / Outputs / Key Images + let mut txs = Vec::with_capacity(block.transactions.len()); + remove_tx(&block.miner_transaction.hash(), tables)?; for tx_hash in &block.transactions { - remove_tx(tx_hash, tables)?; + let (_, tx) = remove_tx(tx_hash, tables)?; + + if move_to_alt_chain.is_some() { + txs.push(VerifiedTransactionInformation { + tx_weight: tx.weight(), + tx_blob: tx.serialize(), + tx_hash: tx.hash(), + fee: tx_fee(&tx), + tx, + }); + } } - Ok((block_height, block_hash, block)) + if let Some(chain_id) = move_to_alt_chain { + alt_block::add_alt_block( + &AltBlockInformation { + block: block.clone(), + block_blob, + txs, + block_hash: block_info.block_hash, + // We know the PoW is valid for this block so just set it so it will always verify as + // valid. + 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, + )?; + } + + Ok((block_height, block_info.block_hash, block)) } //---------------------------------------------------------------------------------------------------- `get_block_extended_header_*` @@ -190,23 +227,21 @@ pub fn get_block_extended_header_from_height( ) -> Result { let block_info = tables.block_infos().get(block_height)?; let block_blob = tables.block_blobs().get(block_height)?.0; - let block = Block::read(&mut block_blob.as_slice())?; + let block_header = BlockHeader::read(&mut block_blob.as_slice())?; let cumulative_difficulty = combine_low_high_bits_to_u128( block_info.cumulative_difficulty_low, block_info.cumulative_difficulty_high, ); - // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` - #[allow(clippy::cast_possible_truncation)] Ok(ExtendedBlockHeader { cumulative_difficulty, - version: HardFork::from_version(block.header.hardfork_version) + 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 as usize, - long_term_weight: block_info.long_term_weight as usize, + vote: block_header.hardfork_signal, + timestamp: block_header.timestamp, + block_weight: block_info.weight, + long_term_weight: block_info.long_term_weight, }) } @@ -270,14 +305,14 @@ mod test { use cuprate_database::{Env, EnvInner, TxRw}; use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3}; - use super::*; - use crate::{ ops::tx::{get_tx, tx_exists}, tables::OpenTables, tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, }; + use super::*; + /// Tests all above block functions. /// /// Note that this doesn't test the correctness of values added, as the @@ -412,7 +447,8 @@ mod test { 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(&mut tables).unwrap(); + let (_popped_height, popped_hash, _popped_block) = + pop_block(None, &mut tables).unwrap(); assert_eq!(block_hash, popped_hash); diff --git a/storage/blockchain/src/ops/macros.rs b/storage/blockchain/src/ops/macros.rs index b7cdba474..e547c7f24 100644 --- a/storage/blockchain/src/ops/macros.rs +++ b/storage/blockchain/src/ops/macros.rs @@ -31,3 +31,24 @@ When calling this function, ensure that either: }; } pub(super) use doc_add_block_inner_invariant; + +// This is pretty much the same as [`doc_add_block_inner_invariant`], it's not worth the effort to reduce +// the duplication. +/// Generate `# Invariant` documentation for internal alt block `fn`'s +/// that should be called directly with caution. +macro_rules! doc_add_alt_block_inner_invariant { + () => { + r#"# ⚠️ Invariant ⚠️ +This function mainly exists to be used internally by the parent function [`crate::ops::alt_block::add_alt_block`]. + +`add_alt_block()` makes sure all data related to the input is mutated, while +this function _does not_, it specifically mutates _particular_ tables. + +This is usually undesired - although this function is still available to call directly. + +When calling this function, ensure that either: +1. This effect (incomplete database mutation) is what is desired, or that... +2. ...the other tables will also be mutated to a correct state"# + }; +} +pub(super) use doc_add_alt_block_inner_invariant; diff --git a/storage/blockchain/src/ops/mod.rs b/storage/blockchain/src/ops/mod.rs index 4ff7dff1e..285aa2440 100644 --- a/storage/blockchain/src/ops/mod.rs +++ b/storage/blockchain/src/ops/mod.rs @@ -94,7 +94,7 @@ //! // Read the data, assert it is correct. //! let tx_rw = env_inner.tx_rw()?; //! let mut tables = env_inner.open_tables_mut(&tx_rw)?; -//! let (height, hash, serai_block) = pop_block(&mut tables)?; +//! let (height, hash, serai_block) = pop_block(None, &mut tables)?; //! //! assert_eq!(height, 0); //! assert_eq!(serai_block, block.block); @@ -102,6 +102,7 @@ //! # Ok(()) } //! ``` +pub mod alt_block; pub mod block; pub mod blockchain; pub mod key_image; diff --git a/storage/blockchain/src/service/free.rs b/storage/blockchain/src/service/free.rs index 2e7c9086e..d8a878c93 100644 --- a/storage/blockchain/src/service/free.rs +++ b/storage/blockchain/src/service/free.rs @@ -4,11 +4,14 @@ use std::sync::Arc; use cuprate_database::{ConcreteEnv, InitError}; +use cuprate_types::{AltBlockInformation, VerifiedBlockInformation}; -use crate::service::{init_read_service, init_write_service}; use crate::{ config::Config, - service::types::{BlockchainReadHandle, BlockchainWriteHandle}, + service::{ + init_read_service, init_write_service, + types::{BlockchainReadHandle, BlockchainWriteHandle}, + }, }; //---------------------------------------------------------------------------------------------------- Init @@ -81,6 +84,44 @@ pub(super) const fn compact_history_genesis_not_included INITIAL_BLOCKS && !(top_block_height - INITIAL_BLOCKS + 2).is_power_of_two() } +//---------------------------------------------------------------------------------------------------- Map Block +/// Maps [`AltBlockInformation`] to [`VerifiedBlockInformation`] +/// +/// # Panics +/// This will panic if the block is invalid, so should only be used on blocks that have been popped from +/// the main-chain. +pub(super) fn map_valid_alt_block_to_verified_block( + alt_block: AltBlockInformation, +) -> VerifiedBlockInformation { + let total_fees = alt_block.txs.iter().map(|tx| tx.fee).sum::(); + let total_miner_output = alt_block + .block + .miner_transaction + .prefix() + .outputs + .iter() + .map(|out| out.amount.unwrap_or(0)) + .sum::(); + + VerifiedBlockInformation { + block: alt_block.block, + block_blob: alt_block.block_blob, + txs: alt_block + .txs + .into_iter() + .map(TryInto::try_into) + .collect::>() + .unwrap(), + block_hash: alt_block.block_hash, + pow_hash: alt_block.pow_hash, + height: alt_block.height, + generated_coins: total_miner_output - total_fees, + weight: alt_block.weight, + long_term_weight: alt_block.long_term_weight, + cumulative_difficulty: alt_block.cumulative_difficulty, + } +} + //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] diff --git a/storage/blockchain/src/service/mod.rs b/storage/blockchain/src/service/mod.rs index c774ee493..aa322d06e 100644 --- a/storage/blockchain/src/service/mod.rs +++ b/storage/blockchain/src/service/mod.rs @@ -98,7 +98,7 @@ //! //! // Block write was OK. //! let response = response_channel.await?; -//! assert_eq!(response, BlockchainResponse::WriteBlockOk); +//! assert_eq!(response, BlockchainResponse::Ok); //! //! // Now, let's try getting the block hash //! // of the block we just wrote. diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 207da4163..73b2d2205 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -8,6 +8,7 @@ use std::{ use rayon::{ iter::{IntoParallelIterator, ParallelIterator}, + prelude::*, ThreadPool, }; use thread_local::ThreadLocal; @@ -17,11 +18,15 @@ use cuprate_database_service::{init_thread_pool, DatabaseReadService, ReaderThre use cuprate_helper::map::combine_low_high_bits_to_u128; use cuprate_types::{ blockchain::{BlockchainReadRequest, BlockchainResponse}, - Chain, ExtendedBlockHeader, OutputOnChain, + Chain, ChainId, ExtendedBlockHeader, OutputOnChain, }; use crate::{ ops::{ + alt_block::{ + get_alt_block, get_alt_block_extended_header_from_height, get_alt_block_hash, + get_alt_chain_history_ranges, + }, block::{ block_exists, get_block_extended_header_from_height, get_block_height, get_block_info, }, @@ -33,8 +38,10 @@ use crate::{ free::{compact_history_genesis_not_included, compact_history_index_to_height_offset}, types::{BlockchainReadHandle, ResponseResult}, }, - tables::{BlockHeights, BlockInfos, OpenTables, Tables}, - types::{Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId}, + tables::{AltBlockHeights, BlockHeights, BlockInfos, OpenTables, Tables}, + types::{ + AltBlockHeight, Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId, + }, }; //---------------------------------------------------------------------------------------------------- init_read_service @@ -87,7 +94,7 @@ fn map_request( match request { R::BlockExtendedHeader(block) => block_extended_header(env, block), R::BlockHash(block, chain) => block_hash(env, block, chain), - R::FindBlock(_) => todo!("Add alt blocks to DB"), + R::FindBlock(block_hash) => find_block(env, block_hash), R::FilterUnknownHashes(hashes) => filter_unknown_hashes(env, hashes), R::BlockExtendedHeaderInRange(range, chain) => { block_extended_header_in_range(env, range, chain) @@ -99,6 +106,7 @@ fn map_request( R::KeyImagesSpent(set) => key_images_spent(env, set), R::CompactChainHistory => compact_chain_history(env), R::FindFirstUnknown(block_ids) => find_first_unknown(env, &block_ids), + R::AltBlocksInChain(chain_id) => alt_blocks_in_chain(env, chain_id), } /* SOMEDAY: post-request handling, run some code for each request? */ @@ -198,12 +206,39 @@ fn block_hash(env: &ConcreteEnv, block_height: BlockHeight, chain: Chain) -> Res let block_hash = match chain { Chain::Main => get_block_info(&block_height, &table_block_infos)?.block_hash, - Chain::Alt(_) => todo!("Add alt blocks to DB"), + Chain::Alt(chain) => { + get_alt_block_hash(&block_height, chain, &env_inner.open_tables(&tx_ro)?)? + } }; Ok(BlockchainResponse::BlockHash(block_hash)) } +/// [`BlockchainReadRequest::FindBlock`] +fn find_block(env: &ConcreteEnv, block_hash: BlockHash) -> ResponseResult { + // Single-threaded, no `ThreadLocal` required. + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + + let table_block_heights = env_inner.open_db_ro::(&tx_ro)?; + + // Check the main chain first. + match table_block_heights.get(&block_hash) { + Ok(height) => return Ok(BlockchainResponse::FindBlock(Some((Chain::Main, height)))), + Err(RuntimeError::KeyNotFound) => (), + Err(e) => return Err(e), + } + + let table_alt_block_heights = env_inner.open_db_ro::(&tx_ro)?; + + let height = table_alt_block_heights.get(&block_hash)?; + + Ok(BlockchainResponse::FindBlock(Some(( + Chain::Alt(height.chain_id.into()), + height.height, + )))) +} + /// [`BlockchainReadRequest::FilterUnknownHashes`]. #[inline] fn filter_unknown_hashes(env: &ConcreteEnv, mut hashes: HashSet) -> ResponseResult { @@ -254,7 +289,38 @@ fn block_extended_header_in_range( get_block_extended_header_from_height(&block_height, tables) }) .collect::, RuntimeError>>()?, - Chain::Alt(_) => todo!("Add alt blocks to DB"), + Chain::Alt(chain_id) => { + let ranges = { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + let alt_chains = tables.alt_chain_infos(); + + get_alt_chain_history_ranges(range, chain_id, alt_chains)? + }; + + ranges + .par_iter() + .rev() + .map(|(chain, range)| { + range.clone().into_par_iter().map(|height| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + + match *chain { + Chain::Main => get_block_extended_header_from_height(&height, tables), + Chain::Alt(chain_id) => get_alt_block_extended_header_from_height( + &AltBlockHeight { + chain_id: chain_id.into(), + height, + }, + tables, + ), + } + }) + }) + .flatten() + .collect::, _>>()? + } }; Ok(BlockchainResponse::BlockExtendedHeaderInRange(vec)) @@ -489,3 +555,45 @@ fn find_first_unknown(env: &ConcreteEnv, block_ids: &[BlockHash]) -> ResponseRes BlockchainResponse::FindFirstUnknown(Some((idx, last_known_height + 1))) }) } + +/// [`BlockchainReadRequest::AltBlocksInChain`] +fn alt_blocks_in_chain(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { + // Prepare tx/tables in `ThreadLocal`. + let env_inner = env.env_inner(); + let tx_ro = thread_local(env); + let tables = thread_local(env); + + // Get the history of this alt-chain. + let history = { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + get_alt_chain_history_ranges(0..usize::MAX, chain_id, tables.alt_chain_infos())? + }; + + // Get all the blocks until we join the main-chain. + let blocks = history + .par_iter() + .rev() + .skip(1) + .flat_map(|(chain_id, range)| { + let Chain::Alt(chain_id) = chain_id else { + panic!("Should not have main chain blocks here we skipped last range"); + }; + + range.clone().into_par_iter().map(|height| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + + get_alt_block( + &AltBlockHeight { + chain_id: (*chain_id).into(), + height, + }, + tables, + ) + }) + }) + .collect::>()?; + + Ok(BlockchainResponse::AltBlocksInChain(blocks)) +} diff --git a/storage/blockchain/src/service/tests.rs b/storage/blockchain/src/service/tests.rs index b68b5444c..78cb6944a 100644 --- a/storage/blockchain/src/service/tests.rs +++ b/storage/blockchain/src/service/tests.rs @@ -84,7 +84,7 @@ async fn test_template( let request = BlockchainWriteRequest::WriteBlock(block); let response_channel = writer.call(request); let response = response_channel.await.unwrap(); - assert_eq!(response, BlockchainResponse::WriteBlockOk); + assert_eq!(response, BlockchainResponse::Ok); } //----------------------------------------------------------------------- Reset the transaction diff --git a/storage/blockchain/src/service/write.rs b/storage/blockchain/src/service/write.rs index 816afc4f5..4038d94a5 100644 --- a/storage/blockchain/src/service/write.rs +++ b/storage/blockchain/src/service/write.rs @@ -1,18 +1,21 @@ //! Database writer thread definitions and logic. - //---------------------------------------------------------------------------------------------------- Import use std::sync::Arc; -use cuprate_database::{ConcreteEnv, Env, EnvInner, RuntimeError, TxRw}; +use cuprate_database::{ConcreteEnv, DatabaseRo, DatabaseRw, Env, EnvInner, RuntimeError, TxRw}; use cuprate_database_service::DatabaseWriteHandle; use cuprate_types::{ blockchain::{BlockchainResponse, BlockchainWriteRequest}, - VerifiedBlockInformation, + AltBlockInformation, Chain, ChainId, VerifiedBlockInformation, }; use crate::{ - service::types::{BlockchainWriteHandle, ResponseResult}, - tables::OpenTables, + service::{ + free::map_valid_alt_block_to_verified_block, + types::{BlockchainWriteHandle, ResponseResult}, + }, + tables::{OpenTables, Tables, TablesMut}, + types::{AltBlockHeight, AltChainInfo}, }; //---------------------------------------------------------------------------------------------------- init_write_service @@ -29,6 +32,12 @@ fn handle_blockchain_request( ) -> Result { match req { BlockchainWriteRequest::WriteBlock(block) => write_block(env, block), + BlockchainWriteRequest::WriteAltBlock(alt_block) => write_alt_block(env, alt_block), + BlockchainWriteRequest::PopBlocks(numb_blocks) => pop_blocks(env, *numb_blocks), + BlockchainWriteRequest::ReverseReorg(old_main_chain_id) => { + reverse_reorg(env, *old_main_chain_id) + } + BlockchainWriteRequest::FlushAltBlocks => flush_alt_blocks(env), } } @@ -55,7 +64,167 @@ fn write_block(env: &ConcreteEnv, block: &VerifiedBlockInformation) -> ResponseR match result { Ok(()) => { TxRw::commit(tx_rw)?; - Ok(BlockchainResponse::WriteBlockOk) + Ok(BlockchainResponse::Ok) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} + +/// [`BlockchainWriteRequest::WriteAltBlock`]. +#[inline] +fn write_alt_block(env: &ConcreteEnv, block: &AltBlockInformation) -> ResponseResult { + let env_inner = env.env_inner(); + let tx_rw = env_inner.tx_rw()?; + + let result = { + let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; + crate::ops::alt_block::add_alt_block(block, &mut tables_mut) + }; + + match result { + Ok(()) => { + TxRw::commit(tx_rw)?; + Ok(BlockchainResponse::Ok) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} + +/// [`BlockchainWriteRequest::PopBlocks`]. +fn pop_blocks(env: &ConcreteEnv, numb_blocks: usize) -> ResponseResult { + let env_inner = env.env_inner(); + let mut tx_rw = env_inner.tx_rw()?; + + // TODO: turn this function into a try block once stable. + let mut result = || { + // flush all the current alt blocks as they may reference blocks to be popped. + crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw)?; + + let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; + // generate a `ChainId` for the popped blocks. + let old_main_chain_id = ChainId(rand::random()); + + // pop the blocks + let mut last_block_height = 0; + for _ in 0..numb_blocks { + (last_block_height, _, _) = + crate::ops::block::pop_block(Some(old_main_chain_id), &mut tables_mut)?; + } + + // Update the alt_chain_info with the correct information. + tables_mut.alt_chain_infos_mut().put( + &old_main_chain_id.into(), + &AltChainInfo { + parent_chain: Chain::Main.into(), + common_ancestor_height: last_block_height - 1, + chain_height: last_block_height + numb_blocks, + }, + )?; + + Ok(old_main_chain_id) + }; + + match result() { + Ok(old_main_chain_id) => { + TxRw::commit(tx_rw)?; + Ok(BlockchainResponse::PopBlocks(old_main_chain_id)) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} + +/// [`BlockchainWriteRequest::ReverseReorg`]. +fn reverse_reorg(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { + let env_inner = env.env_inner(); + let tx_rw = env_inner.tx_rw()?; + + // TODO: turn this function into a try block once stable. + let result = || { + let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; + + let chain_info = tables_mut.alt_chain_infos().get(&chain_id.into())?; + // Although this doesn't guarantee the chain was popped from the main-chain, it's an easy + // thing for us to check. + assert_eq!(Chain::from(chain_info.parent_chain), Chain::Main); + + let tob_block_height = + crate::ops::blockchain::top_block_height(tables_mut.block_heights())?; + + // pop any blocks that were added as part of a re-org. + for _ in chain_info.common_ancestor_height..tob_block_height { + crate::ops::block::pop_block(None, &mut tables_mut)?; + } + + // Rust borrow rules requires us to collect into a Vec first before looping over the Vec. + let alt_blocks = (chain_info.common_ancestor_height..chain_info.chain_height) + .map(|height| { + crate::ops::alt_block::get_alt_block( + &AltBlockHeight { + chain_id: chain_id.into(), + height, + }, + &tables_mut, + ) + }) + .collect::>(); + + // Add the old main chain blocks back to the main chain. + for res_alt_block in alt_blocks { + let alt_block = res_alt_block?; + + let verified_block = map_valid_alt_block_to_verified_block(alt_block); + + crate::ops::block::add_block(&verified_block, &mut tables_mut)?; + } + + Ok(()) + }; + + match result() { + Ok(()) => { + TxRw::commit(tx_rw)?; + Ok(BlockchainResponse::Ok) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} + +/// [`BlockchainWriteRequest::FlushAltBlocks`]. +#[inline] +fn flush_alt_blocks(env: &ConcreteEnv) -> ResponseResult { + let env_inner = env.env_inner(); + let mut tx_rw = env_inner.tx_rw()?; + + let result = crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw); + + match result { + Ok(()) => { + TxRw::commit(tx_rw)?; + Ok(BlockchainResponse::Ok) } Err(e) => { // INVARIANT: ensure database atomicity by aborting diff --git a/storage/blockchain/src/tables.rs b/storage/blockchain/src/tables.rs index 122ac31b4..74d2e591e 100644 --- a/storage/blockchain/src/tables.rs +++ b/storage/blockchain/src/tables.rs @@ -17,8 +17,9 @@ //---------------------------------------------------------------------------------------------------- Import use crate::types::{ - Amount, AmountIndex, AmountIndices, BlockBlob, BlockHash, BlockHeight, BlockInfo, KeyImage, - Output, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RctOutput, TxBlob, TxHash, + AltBlockHeight, AltChainInfo, AltTransactionInfo, Amount, AmountIndex, AmountIndices, + BlockBlob, BlockHash, BlockHeight, BlockInfo, CompactAltBlockInfo, KeyImage, Output, + PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RawChainId, RctOutput, TxBlob, TxHash, TxId, UnlockTime, }; @@ -129,6 +130,40 @@ cuprate_database::define_tables! { /// Transactions without unlock times will not exist in this table. 14 => TxUnlockTime, TxId => UnlockTime, + + /// Information on alt-chains. + 15 => AltChainInfos, + RawChainId => AltChainInfo, + + /// Alt-block heights. + /// + /// Contains the height of all alt-blocks. + 16 => AltBlockHeights, + BlockHash => AltBlockHeight, + + /// Alt-block information. + /// + /// Contains information on all alt-blocks. + 17 => AltBlocksInfo, + AltBlockHeight => CompactAltBlockInfo, + + /// Alt-block blobs. + /// + /// Contains the raw bytes of all alt-blocks. + 18 => AltBlockBlobs, + AltBlockHeight => BlockBlob, + + /// Alt-Block transactions blobs. + /// + /// Contains the raw bytes of alt transactions, if those transactions are not in the main-chain. + 19 => AltTransactionBlobs, + TxHash => TxBlob, + + /// Alt-Block transactions information. + /// + /// Contains information on all alt transactions, even if they are in the main-chain. + 20 => AltTransactionInfos, + TxHash => AltTransactionInfo, } //---------------------------------------------------------------------------------------------------- Tests diff --git a/storage/blockchain/src/tests.rs b/storage/blockchain/src/tests.rs index 65527e102..d57a37154 100644 --- a/storage/blockchain/src/tests.rs +++ b/storage/blockchain/src/tests.rs @@ -10,6 +10,7 @@ use std::{borrow::Cow, fmt::Debug}; use pretty_assertions::assert_eq; use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner}; +use cuprate_types::{AltBlockInformation, ChainId, VerifiedBlockInformation}; use crate::{ config::ConfigBuilder, @@ -88,3 +89,21 @@ pub(crate) fn assert_all_tables_are_empty(env: &ConcreteEnv) { assert!(tables.all_tables_empty().unwrap()); assert_eq!(crate::ops::tx::get_num_tx(tables.tx_ids()).unwrap(), 0); } + +pub(crate) fn map_verified_block_to_alt( + verified_block: VerifiedBlockInformation, + chain_id: ChainId, +) -> AltBlockInformation { + AltBlockInformation { + block: verified_block.block, + block_blob: verified_block.block_blob, + txs: verified_block.txs, + block_hash: verified_block.block_hash, + pow_hash: verified_block.pow_hash, + height: verified_block.height, + weight: verified_block.weight, + long_term_weight: verified_block.long_term_weight, + cumulative_difficulty: verified_block.cumulative_difficulty, + chain_id, + } +} diff --git a/storage/blockchain/src/types.rs b/storage/blockchain/src/types.rs index eb1dc6479..e96656951 100644 --- a/storage/blockchain/src/types.rs +++ b/storage/blockchain/src/types.rs @@ -41,12 +41,14 @@ #![forbid(unsafe_code)] // if you remove this line i will steal your monero //---------------------------------------------------------------------------------------------------- Import -use bytemuck::{Pod, Zeroable}; +use std::num::NonZero; +use bytemuck::{Pod, Zeroable}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use cuprate_database::{Key, StorableVec}; +use cuprate_types::{Chain, ChainId}; //---------------------------------------------------------------------------------------------------- Aliases // These type aliases exist as many Monero-related types are the exact same. @@ -187,7 +189,7 @@ pub struct BlockInfo { /// The adjusted block size, in bytes. /// /// See [`block_weight`](https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#blocks-weight). - pub weight: u64, + pub weight: usize, /// Least-significant 64 bits of the 128-bit cumulative difficulty. pub cumulative_difficulty_low: u64, /// Most-significant 64 bits of the 128-bit cumulative difficulty. @@ -199,7 +201,7 @@ pub struct BlockInfo { /// The long term block weight, based on the median weight of the preceding `100_000` blocks. /// /// See [`long_term_weight`](https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#long-term-block-weight). - pub long_term_weight: u64, + pub long_term_weight: usize, } //---------------------------------------------------------------------------------------------------- OutputFlags @@ -324,6 +326,259 @@ pub struct RctOutput { } // TODO: local_index? +//---------------------------------------------------------------------------------------------------- RawChain +/// [`Chain`] in a format which can be stored in the DB. +/// +/// Implements [`Into`] and [`From`] for [`Chain`]. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// use cuprate_types::Chain; +/// +/// // Assert Storable is correct. +/// let a: RawChain = Chain::Main.into(); +/// let b = Storable::as_bytes(&a); +/// let c: RawChain = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 8); +/// assert_eq!(align_of::(), 8); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(transparent)] +pub struct RawChain(u64); + +impl From for RawChain { + fn from(value: Chain) -> Self { + match value { + Chain::Main => Self(0), + Chain::Alt(chain_id) => Self(chain_id.0.get()), + } + } +} + +impl From for Chain { + fn from(value: RawChain) -> Self { + NonZero::new(value.0).map_or(Self::Main, |id| Self::Alt(ChainId(id))) + } +} + +impl From for RawChain { + fn from(value: RawChainId) -> Self { + // A [`ChainID`] with an inner value of `0` is invalid. + assert_ne!(value.0, 0); + + Self(value.0) + } +} + +//---------------------------------------------------------------------------------------------------- RawChainId +/// [`ChainId`] in a format which can be stored in the DB. +/// +/// Implements [`Into`] and [`From`] for [`ChainId`]. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// use cuprate_types::ChainId; +/// +/// // Assert Storable is correct. +/// let a: RawChainId = ChainId(10.try_into().unwrap()).into(); +/// let b = Storable::as_bytes(&a); +/// let c: RawChainId = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 8); +/// assert_eq!(align_of::(), 8); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(transparent)] +pub struct RawChainId(u64); + +impl From for RawChainId { + fn from(value: ChainId) -> Self { + Self(value.0.get()) + } +} + +impl From for ChainId { + fn from(value: RawChainId) -> Self { + Self(NonZero::new(value.0).expect("RawChainId mut not have a value of `0`")) + } +} + +impl Key for RawChainId {} + +//---------------------------------------------------------------------------------------------------- AltChainInfo +/// Information on an alternative chain. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// use cuprate_types::Chain; +/// +/// // Assert Storable is correct. +/// let a: AltChainInfo = AltChainInfo { +/// parent_chain: Chain::Main.into(), +/// common_ancestor_height: 0, +/// chain_height: 1, +/// }; +/// let b = Storable::as_bytes(&a); +/// let c: AltChainInfo = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 24); +/// assert_eq!(align_of::(), 8); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct AltChainInfo { + /// The chain this alt chain forks from. + pub parent_chain: RawChain, + /// The height of the first block we share with the parent chain. + pub common_ancestor_height: usize, + /// The chain height of the blocks in this alt chain. + pub chain_height: usize, +} + +//---------------------------------------------------------------------------------------------------- AltBlockHeight +/// Represents the height of a block on an alt-chain. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// use cuprate_types::ChainId; +/// +/// // Assert Storable is correct. +/// let a: AltBlockHeight = AltBlockHeight { +/// chain_id: ChainId(1.try_into().unwrap()).into(), +/// height: 1, +/// }; +/// let b = Storable::as_bytes(&a); +/// let c: AltBlockHeight = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 16); +/// assert_eq!(align_of::(), 8); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct AltBlockHeight { + /// The [`ChainId`] of the chain this alt block is on, in raw form. + pub chain_id: RawChainId, + /// The height of this alt-block. + pub height: usize, +} + +impl Key for AltBlockHeight {} + +//---------------------------------------------------------------------------------------------------- CompactAltBlockInfo +/// Represents information on an alt-chain. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// +/// // Assert Storable is correct. +/// let a: CompactAltBlockInfo = CompactAltBlockInfo { +/// block_hash: [1; 32], +/// pow_hash: [2; 32], +/// height: 10, +/// weight: 20, +/// long_term_weight: 30, +/// cumulative_difficulty_low: 40, +/// cumulative_difficulty_high: 50, +/// }; +/// +/// let b = Storable::as_bytes(&a); +/// let c: CompactAltBlockInfo = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 104); +/// assert_eq!(align_of::(), 8); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct CompactAltBlockInfo { + /// The block's hash. + pub block_hash: [u8; 32], + /// The block's proof-of-work hash. + pub pow_hash: [u8; 32], + /// The block's height. + pub height: usize, + /// The adjusted block size, in bytes. + pub weight: usize, + /// The long term block weight, which is the weight factored in with previous block weights. + pub long_term_weight: usize, + /// The low 64 bits of the cumulative difficulty. + pub cumulative_difficulty_low: u64, + /// The high 64 bits of the cumulative difficulty. + pub cumulative_difficulty_high: u64, +} + +//---------------------------------------------------------------------------------------------------- AltTransactionInfo +/// Represents information on an alt transaction. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// +/// // Assert Storable is correct. +/// let a: AltTransactionInfo = AltTransactionInfo { +/// tx_weight: 1, +/// fee: 6, +/// tx_hash: [6; 32], +/// }; +/// +/// let b = Storable::as_bytes(&a); +/// let c: AltTransactionInfo = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 48); +/// assert_eq!(align_of::(), 8); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct AltTransactionInfo { + /// The transaction's weight. + pub tx_weight: usize, + /// The transaction's total fees. + pub fee: u64, + /// The transaction's hash. + pub tx_hash: [u8; 32], +} + //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] mod test { diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index a96a9cfcd..9c64bd81b 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Boog900", "hinto-janai"] [dependencies] cuprate-types = { path = "../types" } -cuprate-helper = { path = "../helper", features = ["map"] } +cuprate-helper = { path = "../helper", features = ["map", "tx-utils"] } cuprate-wire = { path = "../net/wire" } cuprate-p2p-core = { path = "../p2p/p2p-core", features = ["borsh"] } diff --git a/test-utils/src/data/mod.rs b/test-utils/src/data/mod.rs index b9d42fb86..3be409fe1 100644 --- a/test-utils/src/data/mod.rs +++ b/test-utils/src/data/mod.rs @@ -25,13 +25,11 @@ //! let tx: VerifiedTransactionInformation = TX_V1_SIG0.clone(); //! ``` -mod constants; pub use constants::{ BLOCK_43BD1F, BLOCK_5ECB7E, BLOCK_BBD604, BLOCK_F91043, TX_2180A8, TX_3BC7FF, TX_84D48D, TX_9E3F73, TX_B6B439, TX_D7FEBD, TX_E2D393, TX_E57440, }; +pub use statics::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3, TX_V1_SIG0, TX_V1_SIG2, TX_V2_RCT3}; +mod constants; mod statics; -pub use statics::{ - tx_fee, BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3, TX_V1_SIG0, TX_V1_SIG2, TX_V2_RCT3, -}; diff --git a/test-utils/src/data/statics.rs b/test-utils/src/data/statics.rs index 8b98171a2..a45cc13fa 100644 --- a/test-utils/src/data/statics.rs +++ b/test-utils/src/data/statics.rs @@ -8,12 +8,12 @@ //---------------------------------------------------------------------------------------------------- Import use std::sync::LazyLock; -use cuprate_helper::map::combine_low_high_bits_to_u128; -use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; use hex_literal::hex; -use monero_serai::transaction::Input; use monero_serai::{block::Block, transaction::Transaction}; +use cuprate_helper::{map::combine_low_high_bits_to_u128, tx_utils::tx_fee}; +use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; + use crate::data::constants::{ BLOCK_43BD1F, BLOCK_5ECB7E, BLOCK_F91043, TX_2180A8, TX_3BC7FF, TX_84D48D, TX_9E3F73, TX_B6B439, TX_D7FEBD, TX_E2D393, TX_E57440, @@ -110,36 +110,6 @@ fn to_tx_verification_data(tx_blob: impl AsRef<[u8]>) -> VerifiedTransactionInfo } } -/// Calculates the fee of the [`Transaction`]. -/// -/// # Panics -/// This will panic if the inputs overflow or the transaction outputs too much. -pub fn tx_fee(tx: &Transaction) -> u64 { - let mut fee = 0_u64; - - match &tx { - Transaction::V1 { prefix, .. } => { - for input in &prefix.inputs { - match input { - Input::Gen(_) => return 0, - Input::ToKey { amount, .. } => { - fee = fee.checked_add(amount.unwrap_or(0)).unwrap(); - } - } - } - - for output in &prefix.outputs { - fee.checked_sub(output.amount.unwrap_or(0)).unwrap(); - } - } - Transaction::V2 { proofs, .. } => { - fee = proofs.as_ref().unwrap().base.fee; - } - }; - - fee -} - //---------------------------------------------------------------------------------------------------- Blocks /// Generate a `static LazyLock`. /// @@ -311,12 +281,12 @@ transaction_verification_data! { //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod tests { - use super::*; - use pretty_assertions::assert_eq; use crate::rpc::client::HttpRpcClient; + use super::*; + /// Assert the defined blocks are the same compared to ones received from a local RPC call. #[ignore] // FIXME: doesn't work in CI, we need a real unrestricted node #[tokio::test] diff --git a/test-utils/src/rpc/client.rs b/test-utils/src/rpc/client.rs index fbe6fb9e4..3711334b7 100644 --- a/test-utils/src/rpc/client.rs +++ b/test-utils/src/rpc/client.rs @@ -1,18 +1,16 @@ //! HTTP RPC client. //---------------------------------------------------------------------------------------------------- Use -use serde::Deserialize; -use serde_json::json; -use tokio::task::spawn_blocking; - use monero_rpc::Rpc; use monero_serai::block::Block; use monero_simple_request_rpc::SimpleRequestRpc; +use serde::Deserialize; +use serde_json::json; +use tokio::task::spawn_blocking; +use cuprate_helper::tx_utils::tx_fee; use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; -use crate::data::tx_fee; - //---------------------------------------------------------------------------------------------------- Constants /// The default URL used for Monero RPC connections. pub const LOCALHOST_RPC_URL: &str = "http://127.0.0.1:18081"; @@ -184,9 +182,10 @@ impl HttpRpcClient { //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod tests { - use super::*; use hex_literal::hex; + use super::*; + /// Assert the default address is localhost. #[tokio::test] async fn localhost() { diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index b502c3fa8..f246e59a5 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -2,14 +2,17 @@ //! //! Tests that assert particular requests lead to particular //! responses are also tested in Cuprate's blockchain database crate. - +//! //---------------------------------------------------------------------------------------------------- Import use std::{ collections::{HashMap, HashSet}, ops::Range, }; -use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; +use crate::{ + types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}, + AltBlockInformation, ChainId, +}; //---------------------------------------------------------------------------------------------------- ReadRequest /// A read request to the blockchain database. @@ -92,26 +95,44 @@ pub enum BlockchainReadRequest { CompactChainHistory, /// A request to find the first unknown block ID in a list of block IDs. - //// + /// /// # Invariant /// The [`Vec`] containing the block IDs must be sorted in chronological block /// order, or else the returned response is unspecified and meaningless, /// as this request performs a binary search. FindFirstUnknown(Vec<[u8; 32]>), + /// A request for all alt blocks in the chain with the given [`ChainId`]. + AltBlocksInChain(ChainId), } //---------------------------------------------------------------------------------------------------- WriteRequest /// A write request to the blockchain database. -/// -/// There is currently only 1 write request to the database, -/// as such, the only valid [`BlockchainResponse`] to this request is -/// the proper response for a [`BlockchainResponse::WriteBlockOk`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum BlockchainWriteRequest { /// Request that a block be written to the database. /// /// Input is an already verified block. WriteBlock(VerifiedBlockInformation), + /// Write an alternative block to the database, + /// + /// Input is the alternative block. + WriteAltBlock(AltBlockInformation), + /// A request to pop some blocks from the top of the main chain + /// + /// Input is the amount of blocks to pop. + /// + /// This request flushes all alt-chains from the cache before adding the popped blocks to the + /// alt cache. + PopBlocks(usize), + /// A request to reverse the re-org process. + /// + /// The inner value is the [`ChainId`] of the old main chain. + /// + /// # Invariant + /// It is invalid to call this with a [`ChainId`] that was not returned from [`BlockchainWriteRequest::PopBlocks`]. + ReverseReorg(ChainId), + /// A request to flush all alternative blocks. + FlushAltBlocks, } //---------------------------------------------------------------------------------------------------- Response @@ -197,12 +218,24 @@ pub enum BlockchainResponse { /// This will be [`None`] if all blocks were known. FindFirstUnknown(Option<(usize, usize)>), - //------------------------------------------------------ Writes - /// Response to [`BlockchainWriteRequest::WriteBlock`]. + /// The response for [`BlockchainReadRequest::AltBlocksInChain`]. /// - /// This response indicates that the requested block has - /// successfully been written to the database without error. - WriteBlockOk, + /// Contains all the alt blocks in the alt-chain in chronological order. + AltBlocksInChain(Vec), + + //------------------------------------------------------ Writes + /// A generic Ok response to indicate a request was successfully handled. + /// + /// currently the response for: + /// - [`BlockchainWriteRequest::WriteBlock`] + /// - [`BlockchainWriteRequest::WriteAltBlock`] + /// - [`BlockchainWriteRequest::ReverseReorg`] + /// - [`BlockchainWriteRequest::FlushAltBlocks`] + Ok, + /// The response for [`BlockchainWriteRequest::PopBlocks`]. + /// + /// The inner value is the alt-chain ID for the old main chain blocks. + PopBlocks(ChainId), } //---------------------------------------------------------------------------------------------------- Tests diff --git a/types/src/types.rs b/types/src/types.rs index 4b6e2e126..a60ce6c60 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -1,6 +1,8 @@ //! Various shared data types in Cuprate. //---------------------------------------------------------------------------------------------------- Import +use std::num::NonZero; + use curve25519_dalek::edwards::EdwardsPoint; use monero_serai::{ block::Block, @@ -38,8 +40,7 @@ pub struct ExtendedBlockHeader { //---------------------------------------------------------------------------------------------------- VerifiedTransactionInformation /// Verified information of a transaction. /// -/// - If this is in a [`VerifiedBlockInformation`] this represents a valid transaction -/// - If this is in an [`AltBlockInformation`] this represents a potentially valid transaction +/// This represents a valid transaction #[derive(Clone, Debug, PartialEq, Eq)] pub struct VerifiedTransactionInformation { /// The transaction itself. @@ -79,6 +80,7 @@ pub struct VerifiedBlockInformation { /// [`Block::hash`]. pub block_hash: [u8; 32], /// The block's proof-of-work hash. + // TODO: make this an option. pub pow_hash: [u8; 32], /// The block's height. pub height: usize, @@ -97,7 +99,7 @@ pub struct VerifiedBlockInformation { /// /// The inner value is meaningless. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub struct ChainId(pub u64); +pub struct ChainId(pub NonZero); //---------------------------------------------------------------------------------------------------- Chain /// An identifier for a chain.