From b44c6b045be5ece83a539f06be69f0f8622e3f51 Mon Sep 17 00:00:00 2001 From: Boog900 Date: Mon, 29 Jul 2024 00:13:08 +0000 Subject: [PATCH] Consensus: add alt-chain handling (#214) * add `pop_blocks` to the difficulty cache * add a rolling median struct * use RollingMedian in weight cache * add pop_blocks to weight cache * add alt context cache * add getting alt RX vms * rework alt cache * add alt block verify function * keep alt caches around * add alt checked alt blocks to the cache * check the alt blocks timestamp * add docs + cleanup code * add popping blocks from the context cache * finish popping blocks + fix tests * fix doc * add a test popping blocks from HF cache * add a request to clear alt caches * add back lint * Apply suggestions from code review Co-authored-by: hinto-janai * review fixes * small changes * change panic doc --------- Co-authored-by: hinto-janai --- Cargo.lock | 2 + consensus/Cargo.toml | 2 + consensus/fast-sync/src/create.rs | 7 +- consensus/rules/src/blocks.rs | 4 +- consensus/rules/src/hard_forks.rs | 28 +- consensus/src/block.rs | 259 +++--------------- consensus/src/block/alt_block.rs | 304 ++++++++++++++++++++++ consensus/src/block/batch_prepare.rs | 207 +++++++++++++++ consensus/src/block/free.rs | 32 +++ consensus/src/context.rs | 88 ++++++- consensus/src/context/alt_chains.rs | 215 +++++++++++++++ consensus/src/context/difficulty.rs | 86 +++++- consensus/src/context/hardforks.rs | 58 ++++- consensus/src/context/rx_vms.rs | 52 +++- consensus/src/context/task.rs | 150 +++++++++-- consensus/src/context/weight.rs | 186 ++++++------- consensus/src/tests/context/difficulty.rs | 77 +++++- consensus/src/tests/context/hardforks.rs | 43 +++ consensus/src/tests/context/weight.rs | 81 +++++- consensus/src/tests/mock_db.rs | 12 +- helper/src/num.rs | 6 + helper/src/num/rolling_median.rs | 150 +++++++++++ storage/blockchain/src/service/mod.rs | 6 +- storage/blockchain/src/service/read.rs | 49 ++-- storage/blockchain/src/service/tests.rs | 32 ++- types/src/blockchain.rs | 24 +- types/src/lib.rs | 3 +- types/src/types.rs | 50 +++- 28 files changed, 1801 insertions(+), 412 deletions(-) create mode 100644 consensus/src/block/alt_block.rs create mode 100644 consensus/src/block/batch_prepare.rs create mode 100644 consensus/src/block/free.rs create mode 100644 consensus/src/context/alt_chains.rs create mode 100644 helper/src/num/rolling_median.rs diff --git a/Cargo.lock b/Cargo.lock index 55f17a1..32a5cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,11 +535,13 @@ dependencies = [ "multiexp", "proptest", "proptest-derive", + "rand", "randomx-rs", "rayon", "thiserror", "thread_local", "tokio", + "tokio-test", "tokio-util", "tower", "tracing", diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index 624eb63..521b98c 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -29,6 +29,7 @@ tokio = { workspace = true, features = ["rt"] } tokio-util = { workspace = true } hex = { workspace = true } +rand = { workspace = true } [dev-dependencies] cuprate-test-utils = { path = "../test-utils" } @@ -37,5 +38,6 @@ cuprate-consensus-rules = {path = "./rules", features = ["proptest"]} hex-literal = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"]} +tokio-test = { workspace = true } proptest = { workspace = true } proptest-derive = { workspace = true } \ No newline at end of file diff --git a/consensus/fast-sync/src/create.rs b/consensus/fast-sync/src/create.rs index dc2311f..adae100 100644 --- a/consensus/fast-sync/src/create.rs +++ b/consensus/fast-sync/src/create.rs @@ -6,7 +6,10 @@ use tower::{Service, ServiceExt}; use cuprate_blockchain::{ config::ConfigBuilder, cuprate_database::RuntimeError, service::DatabaseReadHandle, }; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use cuprate_fast_sync::{hash_of_hashes, BlockId, HashOfHashes}; @@ -19,7 +22,7 @@ async fn read_batch( let mut block_ids = Vec::::with_capacity(BATCH_SIZE as usize); for height in height_from..(height_from + BATCH_SIZE) { - let request = BCReadRequest::BlockHash(height); + let request = BCReadRequest::BlockHash(height, Chain::Main); let response_channel = handle.ready().await?.call(request); let response = response_channel.await?; diff --git a/consensus/rules/src/blocks.rs b/consensus/rules/src/blocks.rs index cb0e3e4..ecd6a11 100644 --- a/consensus/rules/src/blocks.rs +++ b/consensus/rules/src/blocks.rs @@ -148,7 +148,7 @@ fn block_size_sanity_check( /// Sanity check on the block weight. /// /// ref: -fn check_block_weight( +pub fn check_block_weight( block_weight: usize, median_for_block_reward: usize, ) -> Result<(), BlockError> { @@ -184,7 +184,7 @@ fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> { /// Checks the blocks timestamp is in the valid range. /// /// ref: -fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> { +pub fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> { if block.header.timestamp < median_timestamp || block.header.timestamp > current_unix_timestamp() + BLOCK_FUTURE_TIME_LIMIT { diff --git a/consensus/rules/src/hard_forks.rs b/consensus/rules/src/hard_forks.rs index b34b93d..016a51f 100644 --- a/consensus/rules/src/hard_forks.rs +++ b/consensus/rules/src/hard_forks.rs @@ -38,7 +38,7 @@ pub enum HardForkError { } /// Information about a given hard-fork. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct HFInfo { height: u64, threshold: u64, @@ -50,7 +50,7 @@ impl HFInfo { } /// Information about every hard-fork Monero has had. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct HFsInfo([HFInfo; NUMB_OF_HARD_FORKS]); impl HFsInfo { @@ -243,7 +243,7 @@ impl HardFork { } /// A struct holding the current voting state of the blockchain. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct HFVotes { votes: [u64; NUMB_OF_HARD_FORKS], vote_list: VecDeque, @@ -293,6 +293,28 @@ impl HFVotes { } } + /// Pop a number of blocks from the top of the cache and push some values into the front of the cache, + /// i.e. the oldest blocks. + /// + /// `old_block_votes` should contain the HFs below the window that now will be in the window after popping + /// blocks from the top. + /// + /// # Panics + /// + /// This will panic if `old_block_votes` contains more HFs than `numb_blocks`. + pub fn reverse_blocks(&mut self, numb_blocks: usize, old_block_votes: Self) { + assert!(old_block_votes.vote_list.len() <= numb_blocks); + + for hf in self.vote_list.drain(self.vote_list.len() - numb_blocks..) { + self.votes[hf as usize - 1] -= 1; + } + + for old_vote in old_block_votes.vote_list.into_iter().rev() { + self.vote_list.push_front(old_vote); + self.votes[old_vote as usize - 1] += 1; + } + } + /// Returns the total votes for a hard-fork. /// /// ref: diff --git a/consensus/src/block.rs b/consensus/src/block.rs index d3d0672..1b36eb9 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -12,31 +12,35 @@ use monero_serai::{ block::Block, transaction::{Input, Transaction}, }; -use rayon::prelude::*; use tower::{Service, ServiceExt}; -use tracing::instrument; + +use cuprate_helper::asynch::rayon_spawn_async; +use cuprate_types::{ + AltBlockInformation, VerifiedBlockInformation, VerifiedTransactionInformation, +}; use cuprate_consensus_rules::{ blocks::{ - calculate_pow_hash, check_block, check_block_pow, is_randomx_seed_height, - randomx_seed_height, BlockError, RandomX, + calculate_pow_hash, check_block, check_block_pow, randomx_seed_height, BlockError, RandomX, }, - hard_forks::HardForkError, miner_tx::MinerTxError, ConsensusError, HardFork, }; -use cuprate_helper::asynch::rayon_spawn_async; -use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; use crate::{ - context::{ - rx_vms::RandomXVM, BlockChainContextRequest, BlockChainContextResponse, - RawBlockChainContext, - }, + context::{BlockChainContextRequest, BlockChainContextResponse, RawBlockChainContext}, transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse}, Database, ExtendedConsensusError, }; +mod alt_block; +mod batch_prepare; +mod free; + +use alt_block::sanity_check_alt_block; +use batch_prepare::batch_prepare_main_chain_block; +use free::pull_ordered_transactions; + /// A pre-prepared block with all data needed to verify it, except the block's proof of work. #[derive(Debug)] pub struct PreparedBlockExPow { @@ -124,7 +128,7 @@ impl PreparedBlock { let (hf_version, hf_vote) = HardFork::from_block_header(&block.header).map_err(BlockError::HardForkError)?; - let Some(Input::Gen(height)) = block.miner_tx.prefix.inputs.first() else { + let [Input::Gen(height)] = &block.miner_tx.prefix.inputs[..] else { Err(ConsensusError::Block(BlockError::MinerTxError( MinerTxError::InputNotOfTypeGen, )))? @@ -191,6 +195,7 @@ pub enum VerifyBlockRequest { /// The already prepared block. block: PreparedBlock, /// The full list of transactions for this block, in the order given in `block`. + // TODO: Remove the Arc here txs: Vec>, }, /// Batch prepares a list of blocks and transactions for verification. @@ -198,6 +203,16 @@ pub enum VerifyBlockRequest { /// The list of blocks and their transactions (not necessarily in the order given in the block). blocks: Vec<(Block, Vec)>, }, + /// A request to sanity check an alt block, also returning the cumulative difficulty of the alt chain. + /// + /// Unlike requests to verify main chain blocks, you do not need to add the returned block to the context + /// service, you will still have to add it to the database though. + AltChain { + /// The alt block to sanity check. + block: Block, + /// The alt transactions. + prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, + }, } /// A response from a verify block request. @@ -205,6 +220,8 @@ pub enum VerifyBlockRequest { pub enum VerifyBlockResponse { /// This block is valid. MainChain(VerifiedBlockInformation), + /// The sanity checked alt block. + AltChain(AltBlockInformation), /// A list of prepared blocks for verification, you should call [`VerifyBlockRequest::MainChainPrepped`] on each of the returned /// blocks to fully verify them. MainChainBatchPrepped(Vec<(PreparedBlock, Vec>)>), @@ -296,206 +313,20 @@ where verify_prepped_main_chain_block(block, txs, context_svc, tx_verifier_svc, None) .await } + VerifyBlockRequest::AltChain { + block, + prepared_txs, + } => sanity_check_alt_block(block, prepared_txs, context_svc).await, } } .boxed() } } -/// Batch prepares a list of blocks for verification. -#[instrument(level = "debug", name = "batch_prep_blocks", skip_all, fields(amt = blocks.len()))] -async fn batch_prepare_main_chain_block( - blocks: Vec<(Block, Vec)>, - mut context_svc: C, -) -> Result -where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Send - + 'static, - C::Future: Send + 'static, -{ - let (blocks, txs): (Vec<_>, Vec<_>) = blocks.into_iter().unzip(); - - tracing::debug!("Calculating block hashes."); - let blocks: Vec = rayon_spawn_async(|| { - blocks - .into_iter() - .map(PreparedBlockExPow::new) - .collect::, _>>() - }) - .await?; - - let Some(last_block) = blocks.last() else { - return Err(ExtendedConsensusError::NoBlocksToVerify); - }; - - // hard-forks cannot be reversed, so the last block will contain the highest hard fork (provided the - // batch is valid). - let top_hf_in_batch = last_block.hf_version; - - // A Vec of (timestamp, HF) for each block to calculate the expected difficulty for each block. - let mut timestamps_hfs = Vec::with_capacity(blocks.len()); - let mut new_rx_vm = None; - - tracing::debug!("Checking blocks follow each other."); - - // For every block make sure they have the correct height and previous ID - for window in blocks.windows(2) { - let block_0 = &window[0]; - let block_1 = &window[1]; - - // Make sure no blocks in the batch have a higher hard fork than the last block. - if block_0.hf_version > top_hf_in_batch { - Err(ConsensusError::Block(BlockError::HardForkError( - HardForkError::VersionIncorrect, - )))?; - } - - if block_0.block_hash != block_1.block.header.previous - || block_0.height != block_1.height - 1 - { - tracing::debug!("Blocks do not follow each other, verification failed."); - Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))?; - } - - // Cache any potential RX VM seeds as we may need them for future blocks in the batch. - if is_randomx_seed_height(block_0.height) && top_hf_in_batch >= HardFork::V12 { - new_rx_vm = Some((block_0.height, block_0.block_hash)); - } - - timestamps_hfs.push((block_0.block.header.timestamp, block_0.hf_version)) - } - - // Get the current blockchain context. - let BlockChainContextResponse::Context(checked_context) = context_svc - .ready() - .await? - .call(BlockChainContextRequest::GetContext) - .await - .map_err(Into::::into)? - else { - panic!("Context service returned wrong response!"); - }; - - // Calculate the expected difficulties for each block in the batch. - let BlockChainContextResponse::BatchDifficulties(difficulties) = context_svc - .ready() - .await? - .call(BlockChainContextRequest::BatchGetDifficulties( - timestamps_hfs, - )) - .await - .map_err(Into::::into)? - else { - panic!("Context service returned wrong response!"); - }; - - let context = checked_context.unchecked_blockchain_context().clone(); - - // Make sure the blocks follow the main chain. - - if context.chain_height != blocks[0].height { - tracing::debug!("Blocks do not follow main chain, verification failed."); - - Err(ConsensusError::Block(BlockError::MinerTxError( - MinerTxError::InputsHeightIncorrect, - )))?; - } - - if context.top_hash != blocks[0].block.header.previous { - tracing::debug!("Blocks do not follow main chain, verification failed."); - - Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))?; - } - - let mut rx_vms = if top_hf_in_batch < HardFork::V12 { - HashMap::new() - } else { - let BlockChainContextResponse::RxVms(rx_vms) = context_svc - .ready() - .await? - .call(BlockChainContextRequest::GetCurrentRxVm) - .await? - else { - panic!("Blockchain context service returned wrong response!"); - }; - - rx_vms - }; - - // If we have a RX seed in the batch calculate it. - if let Some((new_vm_height, new_vm_seed)) = new_rx_vm { - tracing::debug!("New randomX seed in batch, initialising VM"); - - let new_vm = rayon_spawn_async(move || { - Arc::new(RandomXVM::new(&new_vm_seed).expect("RandomX VM gave an error on set up!")) - }) - .await; - - context_svc - .oneshot(BlockChainContextRequest::NewRXVM(( - new_vm_seed, - new_vm.clone(), - ))) - .await - .map_err(Into::::into)?; - - rx_vms.insert(new_vm_height, new_vm); - } - - tracing::debug!("Calculating PoW and prepping transaction"); - - let blocks = rayon_spawn_async(move || { - blocks - .into_par_iter() - .zip(difficulties) - .zip(txs) - .map(|((block, difficultly), txs)| { - // Calculate the PoW for the block. - let height = block.height; - let block = PreparedBlock::new_prepped( - block, - rx_vms.get(&randomx_seed_height(height)).map(AsRef::as_ref), - )?; - - // Check the PoW - check_block_pow(&block.pow_hash, difficultly).map_err(ConsensusError::Block)?; - - // Now setup the txs. - let mut txs = txs - .into_par_iter() - .map(|tx| { - let tx = TransactionVerificationData::new(tx)?; - Ok::<_, ConsensusError>((tx.tx_hash, tx)) - }) - .collect::, _>>()?; - - // Order the txs correctly. - let mut ordered_txs = Vec::with_capacity(txs.len()); - - for tx_hash in &block.block.txs { - let tx = txs - .remove(tx_hash) - .ok_or(ExtendedConsensusError::TxsIncludedWithBlockIncorrect)?; - ordered_txs.push(Arc::new(tx)); - } - - Ok((block, ordered_txs)) - }) - .collect::, ExtendedConsensusError>>() - }) - .await?; - - Ok(VerifyBlockResponse::MainChainBatchPrepped(blocks)) -} - /// Verifies a prepared block. async fn verify_main_chain_block( block: Block, - mut txs: HashMap<[u8; 32], TransactionVerificationData>, + txs: HashMap<[u8; 32], TransactionVerificationData>, mut context_svc: C, tx_verifier_svc: TxV, ) -> Result @@ -557,20 +388,11 @@ where .map_err(ConsensusError::Block)?; // Check that the txs included are what we need and that there are not any extra. - - let mut ordered_txs = Vec::with_capacity(txs.len()); - - tracing::debug!("Ordering transactions for block."); - - if !prepped_block.block.txs.is_empty() { - for tx_hash in &prepped_block.block.txs { - let tx = txs - .remove(tx_hash) - .ok_or(ExtendedConsensusError::TxsIncludedWithBlockIncorrect)?; - ordered_txs.push(Arc::new(tx)); - } - drop(txs); - } + // TODO: Remove the Arc here + let ordered_txs = pull_ordered_transactions(&prepped_block.block, txs)? + .into_iter() + .map(Arc::new) + .collect(); verify_prepped_main_chain_block( prepped_block, @@ -604,8 +426,7 @@ where } else { let BlockChainContextResponse::Context(checked_context) = context_svc .oneshot(BlockChainContextRequest::GetContext) - .await - .map_err(Into::::into)? + .await? else { panic!("Context service returned wrong response!"); }; diff --git a/consensus/src/block/alt_block.rs b/consensus/src/block/alt_block.rs new file mode 100644 index 0000000..cf6f213 --- /dev/null +++ b/consensus/src/block/alt_block.rs @@ -0,0 +1,304 @@ +//! Alt Blocks +//! +//! Alt blocks are sanity checked by [`sanity_check_alt_block`], that function will also compute the cumulative +//! difficulty of the alt chain so callers will know if they should re-org to the alt chain. +use std::{collections::HashMap, sync::Arc}; + +use monero_serai::{block::Block, transaction::Input}; +use tower::{Service, ServiceExt}; + +use cuprate_consensus_rules::{ + blocks::{ + check_block_pow, check_block_weight, check_timestamp, randomx_seed_height, BlockError, + }, + miner_tx::MinerTxError, + ConsensusError, +}; +use cuprate_helper::asynch::rayon_spawn_async; +use cuprate_types::{AltBlockInformation, Chain, ChainId, VerifiedTransactionInformation}; + +use crate::{ + block::{free::pull_ordered_transactions, PreparedBlock}, + context::{ + difficulty::DifficultyCache, + rx_vms::RandomXVM, + weight::{self, BlockWeightsCache}, + AltChainContextCache, AltChainRequestToken, BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW, + }, + transactions::TransactionVerificationData, + BlockChainContextRequest, BlockChainContextResponse, ExtendedConsensusError, + VerifyBlockResponse, +}; + +/// This function sanity checks an alt-block. +/// +/// Returns [`AltBlockInformation`], which contains the cumulative difficulty of the alt chain. +/// +/// This function only checks the block's PoW and its weight. +pub async fn sanity_check_alt_block( + block: Block, + txs: HashMap<[u8; 32], TransactionVerificationData>, + mut context_svc: C, +) -> Result +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send + + 'static, + C::Future: Send + 'static, +{ + // Fetch the alt-chains context cache. + let BlockChainContextResponse::AltChainContextCache(mut alt_context_cache) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::AltChainContextCache { + prev_id: block.header.previous, + _token: AltChainRequestToken, + }) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + // Check if the block's miner input is formed correctly. + let [Input::Gen(height)] = &block.miner_tx.prefix.inputs[..] else { + Err(ConsensusError::Block(BlockError::MinerTxError( + MinerTxError::InputNotOfTypeGen, + )))? + }; + + if *height != alt_context_cache.chain_height { + Err(ConsensusError::Block(BlockError::MinerTxError( + MinerTxError::InputsHeightIncorrect, + )))? + } + + // prep the alt block. + let prepped_block = { + let rx_vm = alt_rx_vm( + alt_context_cache.chain_height, + block.header.major_version, + alt_context_cache.parent_chain, + &mut alt_context_cache, + &mut context_svc, + ) + .await?; + + rayon_spawn_async(move || PreparedBlock::new(block, rx_vm.as_deref())).await? + }; + + // get the difficulty cache for this alt chain. + let difficulty_cache = alt_difficulty_cache( + prepped_block.block.header.previous, + &mut alt_context_cache, + &mut context_svc, + ) + .await?; + + // Check the alt block timestamp is in the correct range. + if let Some(median_timestamp) = + difficulty_cache.median_timestamp(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW.try_into().unwrap()) + { + check_timestamp(&prepped_block.block, median_timestamp).map_err(ConsensusError::Block)? + }; + + let next_difficulty = difficulty_cache.next_difficulty(&prepped_block.hf_version); + // make sure the block's PoW is valid for this difficulty. + check_block_pow(&prepped_block.pow_hash, next_difficulty).map_err(ConsensusError::Block)?; + + let cumulative_difficulty = difficulty_cache.cumulative_difficulty() + next_difficulty; + + let ordered_txs = pull_ordered_transactions(&prepped_block.block, txs)?; + + let block_weight = + prepped_block.miner_tx_weight + ordered_txs.iter().map(|tx| tx.tx_weight).sum::(); + + let alt_weight_cache = alt_weight_cache( + prepped_block.block.header.previous, + &mut alt_context_cache, + &mut context_svc, + ) + .await?; + + // Check the block weight is below the limit. + check_block_weight( + block_weight, + alt_weight_cache.median_for_block_reward(&prepped_block.hf_version), + ) + .map_err(ConsensusError::Block)?; + + let long_term_weight = weight::calculate_block_long_term_weight( + &prepped_block.hf_version, + block_weight, + alt_weight_cache.median_long_term_weight(), + ); + + // Get the chainID or generate a new one if this is the first alt block in this alt chain. + let chain_id = *alt_context_cache + .chain_id + .get_or_insert_with(|| ChainId(rand::random())); + + // Create the alt block info. + let block_info = AltBlockInformation { + block_hash: prepped_block.block_hash, + block: prepped_block.block, + block_blob: prepped_block.block_blob, + txs: ordered_txs + .into_iter() + .map(|tx| VerifiedTransactionInformation { + tx_blob: tx.tx_blob, + tx_weight: tx.tx_weight, + fee: tx.fee, + tx_hash: tx.tx_hash, + tx: tx.tx, + }) + .collect(), + pow_hash: prepped_block.pow_hash, + weight: block_weight, + height: alt_context_cache.chain_height, + long_term_weight, + cumulative_difficulty, + chain_id, + }; + + // Add this block to the cache. + alt_context_cache.add_new_block( + block_info.height, + block_info.block_hash, + block_info.weight, + block_info.long_term_weight, + block_info.block.header.timestamp, + ); + + // Add this alt cache back to the context service. + context_svc + .oneshot(BlockChainContextRequest::AddAltChainContextCache { + prev_id: block_info.block.header.previous, + cache: alt_context_cache, + _token: AltChainRequestToken, + }) + .await?; + + Ok(VerifyBlockResponse::AltChain(block_info)) +} + +/// Retrieves the alt RX VM for the chosen block height. +/// +/// If the `hf` is less than 12 (the height RX activates), then [`None`] is returned. +async fn alt_rx_vm( + block_height: u64, + hf: u8, + parent_chain: Chain, + alt_chain_context: &mut AltChainContextCache, + context_svc: C, +) -> Result>, ExtendedConsensusError> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send, + C::Future: Send + 'static, +{ + if hf < 12 { + return Ok(None); + } + + let seed_height = randomx_seed_height(block_height); + + let cached_vm = match alt_chain_context.cached_rx_vm.take() { + // If the VM is cached and the height is the height we need, we can use this VM. + Some((cached_seed_height, vm)) if seed_height == cached_seed_height => { + (cached_seed_height, vm) + } + // Otherwise we need to make a new VM. + _ => { + let BlockChainContextResponse::AltChainRxVM(vm) = context_svc + .oneshot(BlockChainContextRequest::AltChainRxVM { + height: block_height, + chain: parent_chain, + _token: AltChainRequestToken, + }) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + (seed_height, vm) + } + }; + + Ok(Some( + alt_chain_context.cached_rx_vm.insert(cached_vm).1.clone(), + )) +} + +/// Returns the [`DifficultyCache`] for the alt chain. +async fn alt_difficulty_cache( + prev_id: [u8; 32], + alt_chain_context: &mut AltChainContextCache, + context_svc: C, +) -> Result<&mut DifficultyCache, ExtendedConsensusError> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send, + C::Future: Send + 'static, +{ + // First look to see if the difficulty cache for this alt chain is already cached. + match &mut alt_chain_context.difficulty_cache { + Some(cache) => Ok(cache), + // Otherwise make a new one. + difficulty_cache => { + let BlockChainContextResponse::AltChainDifficultyCache(cache) = context_svc + .oneshot(BlockChainContextRequest::AltChainDifficultyCache { + prev_id, + _token: AltChainRequestToken, + }) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + Ok(difficulty_cache.insert(cache)) + } + } +} + +/// Returns the [`BlockWeightsCache`] for the alt chain. +async fn alt_weight_cache( + prev_id: [u8; 32], + alt_chain_context: &mut AltChainContextCache, + context_svc: C, +) -> Result<&mut BlockWeightsCache, ExtendedConsensusError> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send, + C::Future: Send + 'static, +{ + // First look to see if the weight cache for this alt chain is already cached. + match &mut alt_chain_context.weight_cache { + Some(cache) => Ok(cache), + // Otherwise make a new one. + weight_cache => { + let BlockChainContextResponse::AltChainWeightCache(cache) = context_svc + .oneshot(BlockChainContextRequest::AltChainWeightCache { + prev_id, + _token: AltChainRequestToken, + }) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + Ok(weight_cache.insert(cache)) + } + } +} diff --git a/consensus/src/block/batch_prepare.rs b/consensus/src/block/batch_prepare.rs new file mode 100644 index 0000000..64d1ccb --- /dev/null +++ b/consensus/src/block/batch_prepare.rs @@ -0,0 +1,207 @@ +use std::{collections::HashMap, sync::Arc}; + +use monero_serai::{block::Block, transaction::Transaction}; +use rayon::prelude::*; +use tower::{Service, ServiceExt}; +use tracing::instrument; + +use cuprate_consensus_rules::{ + blocks::{check_block_pow, is_randomx_seed_height, randomx_seed_height, BlockError}, + hard_forks::HardForkError, + miner_tx::MinerTxError, + ConsensusError, HardFork, +}; +use cuprate_helper::asynch::rayon_spawn_async; + +use crate::{ + block::{free::pull_ordered_transactions, PreparedBlock, PreparedBlockExPow}, + context::rx_vms::RandomXVM, + transactions::TransactionVerificationData, + BlockChainContextRequest, BlockChainContextResponse, ExtendedConsensusError, + VerifyBlockResponse, +}; + +/// Batch prepares a list of blocks for verification. +#[instrument(level = "debug", name = "batch_prep_blocks", skip_all, fields(amt = blocks.len()))] +pub(crate) async fn batch_prepare_main_chain_block( + blocks: Vec<(Block, Vec)>, + mut context_svc: C, +) -> Result +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send + + 'static, + C::Future: Send + 'static, +{ + let (blocks, txs): (Vec<_>, Vec<_>) = blocks.into_iter().unzip(); + + tracing::debug!("Calculating block hashes."); + let blocks: Vec = rayon_spawn_async(|| { + blocks + .into_iter() + .map(PreparedBlockExPow::new) + .collect::, _>>() + }) + .await?; + + let Some(last_block) = blocks.last() else { + return Err(ExtendedConsensusError::NoBlocksToVerify); + }; + + // hard-forks cannot be reversed, so the last block will contain the highest hard fork (provided the + // batch is valid). + let top_hf_in_batch = last_block.hf_version; + + // A Vec of (timestamp, HF) for each block to calculate the expected difficulty for each block. + let mut timestamps_hfs = Vec::with_capacity(blocks.len()); + let mut new_rx_vm = None; + + tracing::debug!("Checking blocks follow each other."); + + // For every block make sure they have the correct height and previous ID + for window in blocks.windows(2) { + let block_0 = &window[0]; + let block_1 = &window[1]; + + // Make sure no blocks in the batch have a higher hard fork than the last block. + if block_0.hf_version > top_hf_in_batch { + Err(ConsensusError::Block(BlockError::HardForkError( + HardForkError::VersionIncorrect, + )))?; + } + + if block_0.block_hash != block_1.block.header.previous + || block_0.height != block_1.height - 1 + { + tracing::debug!("Blocks do not follow each other, verification failed."); + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))?; + } + + // Cache any potential RX VM seeds as we may need them for future blocks in the batch. + if is_randomx_seed_height(block_0.height) && top_hf_in_batch >= HardFork::V12 { + new_rx_vm = Some((block_0.height, block_0.block_hash)); + } + + timestamps_hfs.push((block_0.block.header.timestamp, block_0.hf_version)) + } + + // Get the current blockchain context. + let BlockChainContextResponse::Context(checked_context) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::GetContext) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + // Calculate the expected difficulties for each block in the batch. + let BlockChainContextResponse::BatchDifficulties(difficulties) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::BatchGetDifficulties( + timestamps_hfs, + )) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + let context = checked_context.unchecked_blockchain_context().clone(); + + // Make sure the blocks follow the main chain. + + if context.chain_height != blocks[0].height { + tracing::debug!("Blocks do not follow main chain, verification failed."); + + Err(ConsensusError::Block(BlockError::MinerTxError( + MinerTxError::InputsHeightIncorrect, + )))?; + } + + if context.top_hash != blocks[0].block.header.previous { + tracing::debug!("Blocks do not follow main chain, verification failed."); + + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))?; + } + + let mut rx_vms = if top_hf_in_batch < HardFork::V12 { + HashMap::new() + } else { + let BlockChainContextResponse::RxVms(rx_vms) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::GetCurrentRxVm) + .await? + else { + panic!("Blockchain context service returned wrong response!"); + }; + + rx_vms + }; + + // If we have a RX seed in the batch calculate it. + if let Some((new_vm_height, new_vm_seed)) = new_rx_vm { + tracing::debug!("New randomX seed in batch, initialising VM"); + + let new_vm = rayon_spawn_async(move || { + Arc::new(RandomXVM::new(&new_vm_seed).expect("RandomX VM gave an error on set up!")) + }) + .await; + + // Give the new VM to the context service, so it can cache it. + context_svc + .oneshot(BlockChainContextRequest::NewRXVM(( + new_vm_seed, + new_vm.clone(), + ))) + .await?; + + rx_vms.insert(new_vm_height, new_vm); + } + + tracing::debug!("Calculating PoW and prepping transaction"); + + let blocks = rayon_spawn_async(move || { + blocks + .into_par_iter() + .zip(difficulties) + .zip(txs) + .map(|((block, difficultly), txs)| { + // Calculate the PoW for the block. + let height = block.height; + let block = PreparedBlock::new_prepped( + block, + rx_vms.get(&randomx_seed_height(height)).map(AsRef::as_ref), + )?; + + // Check the PoW + check_block_pow(&block.pow_hash, difficultly).map_err(ConsensusError::Block)?; + + // Now setup the txs. + let txs = txs + .into_par_iter() + .map(|tx| { + let tx = TransactionVerificationData::new(tx)?; + Ok::<_, ConsensusError>((tx.tx_hash, tx)) + }) + .collect::, _>>()?; + + // Order the txs correctly. + // TODO: Remove the Arc here + let ordered_txs = pull_ordered_transactions(&block.block, txs)? + .into_iter() + .map(Arc::new) + .collect(); + + Ok((block, ordered_txs)) + }) + .collect::, ExtendedConsensusError>>() + }) + .await?; + + Ok(VerifyBlockResponse::MainChainBatchPrepped(blocks)) +} diff --git a/consensus/src/block/free.rs b/consensus/src/block/free.rs new file mode 100644 index 0000000..8a61e80 --- /dev/null +++ b/consensus/src/block/free.rs @@ -0,0 +1,32 @@ +//! Free functions for block verification +use std::collections::HashMap; + +use monero_serai::block::Block; + +use crate::{transactions::TransactionVerificationData, ExtendedConsensusError}; + +/// Returns a list of transactions, pulled from `txs` in the order they are in the [`Block`]. +/// +/// Will error if a tx need is not in `txs` or if `txs` contain more txs than needed. +pub(crate) fn pull_ordered_transactions( + block: &Block, + mut txs: HashMap<[u8; 32], TransactionVerificationData>, +) -> Result, ExtendedConsensusError> { + if block.txs.len() != txs.len() { + return Err(ExtendedConsensusError::TxsIncludedWithBlockIncorrect); + } + + let mut ordered_txs = Vec::with_capacity(txs.len()); + + if !block.txs.is_empty() { + for tx_hash in &block.txs { + let tx = txs + .remove(tx_hash) + .ok_or(ExtendedConsensusError::TxsIncludedWithBlockIncorrect)?; + ordered_txs.push(tx); + } + drop(txs); + } + + Ok(ordered_txs) +} diff --git a/consensus/src/context.rs b/consensus/src/context.rs index 0752b8b..fffbe90 100644 --- a/consensus/src/context.rs +++ b/consensus/src/context.rs @@ -27,16 +27,22 @@ pub(crate) mod hardforks; pub(crate) mod rx_vms; pub(crate) mod weight; +mod alt_chains; mod task; mod tokens; +use cuprate_types::Chain; +use difficulty::DifficultyCache; +use rx_vms::RandomXVM; +use weight::BlockWeightsCache; + +pub(crate) use alt_chains::{sealed::AltChainRequestToken, AltChainContextCache}; pub use difficulty::DifficultyCacheConfig; pub use hardforks::HardForkConfig; -use rx_vms::RandomXVM; pub use tokens::*; pub use weight::BlockWeightsCacheConfig; -const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; +pub(crate) const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; /// Config for the context service. pub struct ContextConfig { @@ -233,6 +239,74 @@ pub enum BlockChainContextRequest { NewRXVM(([u8; 32], Arc)), /// A request to add a new block to the cache. Update(NewBlockData), + /// Pop blocks from the cache to the specified height. + PopBlocks { + /// The number of blocks to pop from the top of the chain. + /// + /// # Panics + /// + /// This will panic if the number of blocks will pop the genesis block. + numb_blocks: u64, + }, + /// Clear the alt chain context caches. + ClearAltCache, + //----------------------------------------------------------------------------------------------------------- AltChainRequests + /// A request for an alt chain context cache. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle getting the alt cache. + AltChainContextCache { + /// The previous block field in a [`BlockHeader`](monero_serai::block::BlockHeader). + prev_id: [u8; 32], + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, + /// A request for a difficulty cache of an alternative chin. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle getting the difficulty cache of an alt chain. + AltChainDifficultyCache { + /// The previous block field in a [`BlockHeader`](monero_serai::block::BlockHeader). + prev_id: [u8; 32], + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, + /// A request for a block weight cache of an alternative chin. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle getting the weight cache of an alt chain. + AltChainWeightCache { + /// The previous block field in a [`BlockHeader`](monero_serai::block::BlockHeader). + prev_id: [u8; 32], + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, + /// A request for a RX VM for an alternative chin. + /// + /// Response variant: [`BlockChainContextResponse::AltChainRxVM`]. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle getting the randomX VM of an alt chain. + AltChainRxVM { + /// The height the RandomX VM is needed for. + height: u64, + /// The chain to look in for the seed. + chain: Chain, + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, + /// A request to add an alt chain context cache to the context cache. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle returning the alt cache to the context service. + AddAltChainContextCache { + /// The previous block field in a [`BlockHeader`](monero_serai::block::BlockHeader). + prev_id: [u8; 32], + /// The cache. + cache: Box, + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, } pub enum BlockChainContextResponse { @@ -242,7 +316,15 @@ pub enum BlockChainContextResponse { RxVms(HashMap>), /// A list of difficulties. BatchDifficulties(Vec), - /// Ok response. + /// An alt chain context cache. + AltChainContextCache(Box), + /// A difficulty cache for an alt chain. + AltChainDifficultyCache(DifficultyCache), + /// A randomX VM for an alt chain. + AltChainRxVM(Arc), + /// A weight cache for an alt chain + AltChainWeightCache(BlockWeightsCache), + /// A generic Ok response. Ok, } diff --git a/consensus/src/context/alt_chains.rs b/consensus/src/context/alt_chains.rs new file mode 100644 index 0000000..71af8a1 --- /dev/null +++ b/consensus/src/context/alt_chains.rs @@ -0,0 +1,215 @@ +use std::{collections::HashMap, sync::Arc}; + +use tower::ServiceExt; + +use cuprate_consensus_rules::{blocks::BlockError, ConsensusError}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, ChainId, +}; + +use crate::{ + ExtendedConsensusError, + __private::Database, + context::{difficulty::DifficultyCache, rx_vms::RandomXVM, weight::BlockWeightsCache}, +}; + +pub(crate) mod sealed { + /// A token that should be hard to create from outside this crate. + /// + /// It is currently possible to safely create this from outside this crate, **DO NOT** rely on this + /// as it will be broken once we find a way to completely seal this. + #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub struct AltChainRequestToken; +} + +/// The context cache of an alternative chain. +#[derive(Debug, Clone)] +pub struct AltChainContextCache { + /// The alt chain weight cache, [`None`] if it has not been built yet. + pub weight_cache: Option, + /// The alt chain difficulty cache, [`None`] if it has not been built yet. + pub difficulty_cache: Option, + + /// A cached RX VM. + pub cached_rx_vm: Option<(u64, Arc)>, + + /// The chain height of the alt chain. + pub chain_height: u64, + /// The top hash of the alt chain. + pub top_hash: [u8; 32], + /// The [`ChainID`] of the alt chain. + pub chain_id: Option, + /// The parent [`Chain`] of this alt chain. + pub parent_chain: Chain, +} + +impl AltChainContextCache { + /// Add a new block to the cache. + pub fn add_new_block( + &mut self, + height: u64, + block_hash: [u8; 32], + block_weight: usize, + long_term_block_weight: usize, + timestamp: u64, + ) { + if let Some(difficulty_cache) = &mut self.difficulty_cache { + difficulty_cache.new_block(height, timestamp, difficulty_cache.cumulative_difficulty()); + } + + if let Some(weight_cache) = &mut self.weight_cache { + weight_cache.new_block(height, block_weight, long_term_block_weight); + } + + self.chain_height += 1; + self.top_hash = block_hash; + } +} + +/// A map of top IDs to alt chains. +pub struct AltChainMap { + alt_cache_map: HashMap<[u8; 32], Box>, +} + +impl AltChainMap { + pub fn new() -> Self { + Self { + alt_cache_map: HashMap::new(), + } + } + + pub fn clear(&mut self) { + self.alt_cache_map.clear(); + } + + /// Add an alt chain cache to the map. + pub fn add_alt_cache(&mut self, prev_id: [u8; 32], alt_cache: Box) { + self.alt_cache_map.insert(prev_id, alt_cache); + } + + /// Attempts to take an [`AltChainContextCache`] from the map, returning [`None`] if no cache is + /// present. + pub async fn get_alt_chain_context( + &mut self, + prev_id: [u8; 32], + database: D, + ) -> Result, ExtendedConsensusError> { + if let Some(cache) = self.alt_cache_map.remove(&prev_id) { + return Ok(cache); + } + + // find the block with hash == prev_id. + let BCResponse::FindBlock(res) = + database.oneshot(BCReadRequest::FindBlock(prev_id)).await? + else { + panic!("Database returned wrong response"); + }; + + let Some((parent_chain, top_height)) = res else { + // Couldn't find prev_id + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))? + }; + + Ok(Box::new(AltChainContextCache { + weight_cache: None, + difficulty_cache: None, + cached_rx_vm: None, + chain_height: top_height, + top_hash: prev_id, + chain_id: None, + parent_chain, + })) + } +} + +/// Builds a [`DifficultyCache`] for an alt chain. +pub async fn get_alt_chain_difficulty_cache( + prev_id: [u8; 32], + main_chain_difficulty_cache: &DifficultyCache, + mut database: D, +) -> Result { + // find the block with hash == prev_id. + let BCResponse::FindBlock(res) = database + .ready() + .await? + .call(BCReadRequest::FindBlock(prev_id)) + .await? + else { + panic!("Database returned wrong response"); + }; + + let Some((chain, top_height)) = res else { + // Can't find prev_id + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))? + }; + + Ok(match chain { + Chain::Main => { + // prev_id is in main chain, we can use the fast path and clone the main chain cache. + let mut difficulty_cache = main_chain_difficulty_cache.clone(); + difficulty_cache + .pop_blocks_main_chain( + difficulty_cache.last_accounted_height - top_height, + database, + ) + .await?; + + difficulty_cache + } + Chain::Alt(_) => { + // prev_id is in an alt chain, completely rebuild the cache. + DifficultyCache::init_from_chain_height( + top_height + 1, + main_chain_difficulty_cache.config, + database, + chain, + ) + .await? + } + }) +} + +/// Builds a [`BlockWeightsCache`] for an alt chain. +pub async fn get_alt_chain_weight_cache( + prev_id: [u8; 32], + main_chain_weight_cache: &BlockWeightsCache, + mut database: D, +) -> Result { + // find the block with hash == prev_id. + let BCResponse::FindBlock(res) = database + .ready() + .await? + .call(BCReadRequest::FindBlock(prev_id)) + .await? + else { + panic!("Database returned wrong response"); + }; + + let Some((chain, top_height)) = res else { + // Can't find prev_id + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))? + }; + + Ok(match chain { + Chain::Main => { + // prev_id is in main chain, we can use the fast path and clone the main chain cache. + let mut weight_cache = main_chain_weight_cache.clone(); + weight_cache + .pop_blocks_main_chain(weight_cache.tip_height - top_height, database) + .await?; + + weight_cache + } + Chain::Alt(_) => { + // prev_id is in an alt chain, completely rebuild the cache. + BlockWeightsCache::init_from_chain_height( + top_height + 1, + main_chain_weight_cache.config, + database, + chain, + ) + .await? + } + }) +} diff --git a/consensus/src/context/difficulty.rs b/consensus/src/context/difficulty.rs index 9c8321f..b025dfc 100644 --- a/consensus/src/context/difficulty.rs +++ b/consensus/src/context/difficulty.rs @@ -12,7 +12,10 @@ use tower::ServiceExt; use tracing::instrument; use cuprate_helper::num::median; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use crate::{Database, ExtendedConsensusError, HardFork}; @@ -28,7 +31,7 @@ const DIFFICULTY_LAG: usize = 15; /// Configuration for the difficulty cache. /// -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct DifficultyCacheConfig { pub(crate) window: usize, pub(crate) cut: usize, @@ -68,7 +71,7 @@ impl DifficultyCacheConfig { /// This struct is able to calculate difficulties from blockchain information. /// #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) struct DifficultyCache { +pub struct DifficultyCache { /// The list of timestamps in the window. /// len <= [`DIFFICULTY_BLOCKS_COUNT`] pub(crate) timestamps: VecDeque, @@ -87,6 +90,7 @@ impl DifficultyCache { chain_height: u64, config: DifficultyCacheConfig, database: D, + chain: Chain, ) -> Result { tracing::info!("Initializing difficulty cache this may take a while."); @@ -98,7 +102,9 @@ impl DifficultyCache { } let (timestamps, cumulative_difficulties) = - get_blocks_in_pow_info(database.clone(), block_start..chain_height).await?; + get_blocks_in_pow_info(database.clone(), block_start..chain_height, chain).await?; + + debug_assert_eq!(timestamps.len() as u64, chain_height - block_start); tracing::info!( "Current chain height: {}, accounting for {} blocks timestamps", @@ -116,6 +122,70 @@ impl DifficultyCache { Ok(diff) } + /// Pop some blocks from the top of the cache. + /// + /// The cache will be returned to the state it would have been in `numb_blocks` ago. + /// + /// # Invariant + /// + /// This _must_ only be used on a main-chain cache. + #[instrument(name = "pop_blocks_diff_cache", skip_all, fields(numb_blocks = numb_blocks))] + pub async fn pop_blocks_main_chain( + &mut self, + numb_blocks: u64, + database: D, + ) -> Result<(), ExtendedConsensusError> { + let Some(retained_blocks) = self + .timestamps + .len() + .checked_sub(usize::try_from(numb_blocks).unwrap()) + else { + // More blocks to pop than we have in the cache, so just restart a new cache. + *self = Self::init_from_chain_height( + self.last_accounted_height - numb_blocks + 1, + self.config, + database, + Chain::Main, + ) + .await?; + + return Ok(()); + }; + + let current_chain_height = self.last_accounted_height + 1; + + let mut new_start_height = current_chain_height + .saturating_sub(self.config.total_block_count()) + .saturating_sub(numb_blocks); + + // skip the genesis block. + if new_start_height == 0 { + new_start_height = 1; + } + + let (mut timestamps, mut cumulative_difficulties) = get_blocks_in_pow_info( + database, + new_start_height + // current_chain_height - self.timestamps.len() blocks are already in the cache. + ..(current_chain_height - u64::try_from(self.timestamps.len()).unwrap()), + Chain::Main, + ) + .await?; + + self.timestamps.drain(retained_blocks..); + self.cumulative_difficulties.drain(retained_blocks..); + timestamps.append(&mut self.timestamps); + cumulative_difficulties.append(&mut self.cumulative_difficulties); + + self.timestamps = timestamps; + self.cumulative_difficulties = cumulative_difficulties; + self.last_accounted_height -= numb_blocks; + + assert_eq!(self.timestamps.len(), self.cumulative_difficulties.len()); + + Ok(()) + } + /// Add a new block to the difficulty cache. pub fn new_block(&mut self, height: u64, timestamp: u64, cumulative_difficulty: u128) { assert_eq!(self.last_accounted_height + 1, height); @@ -200,7 +270,7 @@ impl DifficultyCache { if self.last_accounted_height + 1 == u64::try_from(numb_blocks).unwrap() { // if the chain height is equal to `numb_blocks` add the genesis block. // otherwise if the chain height is less than `numb_blocks` None is returned - // and if its more than it would be excluded from calculations. + // and if it's more it would be excluded from calculations. let mut timestamps = self.timestamps.clone(); // all genesis blocks have a timestamp of 0. // https://cuprate.github.io/monero-book/consensus_rules/genesis_block.html @@ -299,11 +369,15 @@ fn get_window_start_and_end( async fn get_blocks_in_pow_info( database: D, block_heights: Range, + chain: Chain, ) -> Result<(VecDeque, VecDeque), ExtendedConsensusError> { tracing::info!("Getting blocks timestamps"); let BCResponse::BlockExtendedHeaderInRange(ext_header) = database - .oneshot(BCReadRequest::BlockExtendedHeaderInRange(block_heights)) + .oneshot(BCReadRequest::BlockExtendedHeaderInRange( + block_heights, + chain, + )) .await? else { panic!("Database sent incorrect response"); diff --git a/consensus/src/context/hardforks.rs b/consensus/src/context/hardforks.rs index 92182c7..2243350 100644 --- a/consensus/src/context/hardforks.rs +++ b/consensus/src/context/hardforks.rs @@ -4,7 +4,10 @@ use tower::ServiceExt; use tracing::instrument; use cuprate_consensus_rules::{HFVotes, HFsInfo, HardFork}; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use crate::{Database, ExtendedConsensusError}; @@ -15,7 +18,7 @@ const DEFAULT_WINDOW_SIZE: u64 = 10080; // supermajority window check length - a /// Configuration for hard-forks. /// -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct HardForkConfig { /// The network we are on. pub(crate) info: HFsInfo, @@ -50,7 +53,7 @@ impl HardForkConfig { } /// A struct that keeps track of the current hard-fork and current votes. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct HardForkState { /// The current active hard-fork. pub(crate) current_hardfork: HardFork, @@ -117,6 +120,50 @@ impl HardForkState { Ok(hfs) } + /// Pop some blocks from the top of the cache. + /// + /// The cache will be returned to the state it would have been in `numb_blocks` ago. + /// + /// # Invariant + /// + /// This _must_ only be used on a main-chain cache. + pub async fn pop_blocks_main_chain( + &mut self, + numb_blocks: u64, + database: D, + ) -> Result<(), ExtendedConsensusError> { + let Some(retained_blocks) = self.votes.total_votes().checked_sub(self.config.window) else { + *self = Self::init_from_chain_height( + self.last_height + 1 - numb_blocks, + self.config, + database, + ) + .await?; + + return Ok(()); + }; + + let current_chain_height = self.last_height + 1; + + let oldest_votes = get_votes_in_range( + database, + current_chain_height + .saturating_sub(self.config.window) + .saturating_sub(numb_blocks) + ..current_chain_height + .saturating_sub(numb_blocks) + .saturating_sub(retained_blocks), + usize::try_from(numb_blocks).unwrap(), + ) + .await?; + + self.votes + .reverse_blocks(usize::try_from(numb_blocks).unwrap(), oldest_votes); + self.last_height -= numb_blocks; + + Ok(()) + } + /// Add a new block to the cache. pub fn new_block(&mut self, vote: HardFork, height: u64) { // We don't _need_ to take in `height` but it's for safety, so we don't silently loose track @@ -168,7 +215,10 @@ async fn get_votes_in_range( let mut votes = HFVotes::new(window_size); let BCResponse::BlockExtendedHeaderInRange(vote_list) = database - .oneshot(BCReadRequest::BlockExtendedHeaderInRange(block_heights)) + .oneshot(BCReadRequest::BlockExtendedHeaderInRange( + block_heights, + Chain::Main, + )) .await? else { panic!("Database sent incorrect response!"); diff --git a/consensus/src/context/rx_vms.rs b/consensus/src/context/rx_vms.rs index 08ecb95..3154648 100644 --- a/consensus/src/context/rx_vms.rs +++ b/consensus/src/context/rx_vms.rs @@ -15,12 +15,16 @@ use thread_local::ThreadLocal; use tower::ServiceExt; use tracing::instrument; +use cuprate_consensus_rules::blocks::randomx_seed_height; use cuprate_consensus_rules::{ blocks::{is_randomx_seed_height, RandomX, RX_SEEDHASH_EPOCH_BLOCKS}, HardFork, }; use cuprate_helper::asynch::rayon_spawn_async; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use crate::{Database, ExtendedConsensusError}; @@ -124,7 +128,39 @@ impl RandomXVMCache { self.cached_vm.replace(vm); } - /// Get the RandomX VMs. + /// Creates a RX VM for an alt chain, looking at the main chain RX VMs to see if we can use one + /// of them first. + pub async fn get_alt_vm( + &mut self, + height: u64, + chain: Chain, + database: D, + ) -> Result, ExtendedConsensusError> { + let seed_height = randomx_seed_height(height); + + let BCResponse::BlockHash(seed_hash) = database + .oneshot(BCReadRequest::BlockHash(seed_height, chain)) + .await? + else { + panic!("Database returned wrong response!"); + }; + + for (vm_main_chain_height, vm_seed_hash) in &self.seeds { + if vm_seed_hash == &seed_hash { + let Some(vm) = self.vms.get(vm_main_chain_height) else { + break; + }; + + return Ok(vm.clone()); + } + } + + let alt_vm = rayon_spawn_async(move || Arc::new(RandomXVM::new(&seed_hash).unwrap())).await; + + Ok(alt_vm) + } + + /// Get the main-chain RandomX VMs. pub async fn get_vms(&mut self) -> HashMap> { match self.seeds.len().checked_sub(self.vms.len()) { // No difference in the amount of seeds to VMs. @@ -176,6 +212,12 @@ impl RandomXVMCache { self.vms.clone() } + /// Removes all the RandomX VMs above the `new_height`. + pub fn pop_blocks_main_chain(&mut self, new_height: u64) { + self.seeds.retain(|(height, _)| *height < new_height); + self.vms.retain(|height, _| *height < new_height); + } + /// Add a new block to the VM cache. /// /// hash is the block hash not the blocks PoW hash. @@ -231,8 +273,10 @@ async fn get_block_hashes( for height in heights { let db = database.clone(); fut.push_back(async move { - let BCResponse::BlockHash(hash) = - db.clone().oneshot(BCReadRequest::BlockHash(height)).await? + let BCResponse::BlockHash(hash) = db + .clone() + .oneshot(BCReadRequest::BlockHash(height, Chain::Main)) + .await? else { panic!("Database sent incorrect response!"); }; diff --git a/consensus/src/context/task.rs b/consensus/src/context/task.rs index 108922d..1fa68a2 100644 --- a/consensus/src/context/task.rs +++ b/consensus/src/context/task.rs @@ -9,14 +9,20 @@ use tower::ServiceExt; use tracing::Instrument; use cuprate_consensus_rules::blocks::ContextToVerifyBlock; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; - -use super::{ - difficulty, hardforks, rx_vms, weight, BlockChainContext, BlockChainContextRequest, - BlockChainContextResponse, ContextConfig, RawBlockChainContext, ValidityToken, - BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW, +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; + +use crate::{ + context::{ + alt_chains::{get_alt_chain_difficulty_cache, get_alt_chain_weight_cache, AltChainMap}, + difficulty, hardforks, rx_vms, weight, BlockChainContext, BlockChainContextRequest, + BlockChainContextResponse, ContextConfig, RawBlockChainContext, ValidityToken, + BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW, + }, + Database, ExtendedConsensusError, }; -use crate::{Database, ExtendedConsensusError}; /// A request from the context service to the context task. pub(super) struct ContextTaskRequest { @@ -29,7 +35,7 @@ pub(super) struct ContextTaskRequest { } /// The Context task that keeps the blockchain context and handles requests. -pub struct ContextTask { +pub struct ContextTask { /// A token used to invalidate previous contexts when a new /// block is added to the chain. current_validity_token: ValidityToken, @@ -43,25 +49,25 @@ pub struct ContextTask { /// The hard-fork state cache. hardfork_state: hardforks::HardForkState, + alt_chain_cache_map: AltChainMap, + /// The current chain height. chain_height: u64, /// The top block hash. top_block_hash: [u8; 32], /// The total amount of coins generated. already_generated_coins: u64, + + database: D, } -impl ContextTask { +impl ContextTask { /// Initialize the [`ContextTask`], this will need to pull a lot of data from the database so may take a /// while to complete. - pub async fn init_context( + pub async fn init_context( cfg: ContextConfig, mut database: D, - ) -> Result - where - D: Database + Clone + Send + Sync + 'static, - D::Future: Send + 'static, - { + ) -> Result { let ContextConfig { difficulty_cfg, weights_config, @@ -82,7 +88,7 @@ impl ContextTask { let BCResponse::GeneratedCoins(already_generated_coins) = database .ready() .await? - .call(BCReadRequest::GeneratedCoins) + .call(BCReadRequest::GeneratedCoins(chain_height - 1)) .await? else { panic!("Database sent incorrect response!"); @@ -95,14 +101,24 @@ impl ContextTask { let db = database.clone(); let difficulty_cache_handle = tokio::spawn(async move { - difficulty::DifficultyCache::init_from_chain_height(chain_height, difficulty_cfg, db) - .await + difficulty::DifficultyCache::init_from_chain_height( + chain_height, + difficulty_cfg, + db, + Chain::Main, + ) + .await }); let db = database.clone(); let weight_cache_handle = tokio::spawn(async move { - weight::BlockWeightsCache::init_from_chain_height(chain_height, weights_config, db) - .await + weight::BlockWeightsCache::init_from_chain_height( + chain_height, + weights_config, + db, + Chain::Main, + ) + .await }); // Wait for the hardfork state to finish first as we need it to start the randomX VM cache. @@ -120,9 +136,11 @@ impl ContextTask { weight_cache: weight_cache_handle.await.unwrap()?, rx_vm_cache: rx_seed_handle.await.unwrap()?, hardfork_state, + alt_chain_cache_map: AltChainMap::new(), chain_height, already_generated_coins, top_block_hash, + database, }; Ok(context_svc) @@ -211,6 +229,98 @@ impl ContextTask { BlockChainContextResponse::Ok } + BlockChainContextRequest::PopBlocks { numb_blocks } => { + assert!(numb_blocks < self.chain_height); + + self.difficulty_cache + .pop_blocks_main_chain(numb_blocks, self.database.clone()) + .await?; + self.weight_cache + .pop_blocks_main_chain(numb_blocks, self.database.clone()) + .await?; + self.rx_vm_cache + .pop_blocks_main_chain(self.chain_height - numb_blocks - 1); + self.hardfork_state + .pop_blocks_main_chain(numb_blocks, self.database.clone()) + .await?; + + self.alt_chain_cache_map.clear(); + + self.chain_height -= numb_blocks; + + let BCResponse::GeneratedCoins(already_generated_coins) = self + .database + .ready() + .await? + .call(BCReadRequest::GeneratedCoins(self.chain_height - 1)) + .await? + else { + panic!("Database sent incorrect response!"); + }; + + let BCResponse::BlockHash(top_block_hash) = self + .database + .ready() + .await? + .call(BCReadRequest::BlockHash(self.chain_height - 1, Chain::Main)) + .await? + else { + panic!("Database returned incorrect response!"); + }; + + self.already_generated_coins = already_generated_coins; + self.top_block_hash = top_block_hash; + + std::mem::replace(&mut self.current_validity_token, ValidityToken::new()) + .set_data_invalid(); + + BlockChainContextResponse::Ok + } + BlockChainContextRequest::ClearAltCache => { + self.alt_chain_cache_map.clear(); + + BlockChainContextResponse::Ok + } + BlockChainContextRequest::AltChainContextCache { prev_id, _token } => { + BlockChainContextResponse::AltChainContextCache( + self.alt_chain_cache_map + .get_alt_chain_context(prev_id, &mut self.database) + .await?, + ) + } + BlockChainContextRequest::AltChainDifficultyCache { prev_id, _token } => { + BlockChainContextResponse::AltChainDifficultyCache( + get_alt_chain_difficulty_cache( + prev_id, + &self.difficulty_cache, + self.database.clone(), + ) + .await?, + ) + } + BlockChainContextRequest::AltChainWeightCache { prev_id, _token } => { + BlockChainContextResponse::AltChainWeightCache( + get_alt_chain_weight_cache(prev_id, &self.weight_cache, self.database.clone()) + .await?, + ) + } + BlockChainContextRequest::AltChainRxVM { + height, + chain, + _token, + } => BlockChainContextResponse::AltChainRxVM( + self.rx_vm_cache + .get_alt_vm(height, chain, &mut self.database) + .await?, + ), + BlockChainContextRequest::AddAltChainContextCache { + prev_id, + cache, + _token, + } => { + self.alt_chain_cache_map.add_alt_cache(prev_id, cache); + BlockChainContextResponse::Ok + } }) } diff --git a/consensus/src/context/weight.rs b/consensus/src/context/weight.rs index 2511c59..1084086 100644 --- a/consensus/src/context/weight.rs +++ b/consensus/src/context/weight.rs @@ -8,17 +8,18 @@ //! use std::{ cmp::{max, min}, - collections::VecDeque, ops::Range, }; -use rayon::prelude::*; use tower::ServiceExt; use tracing::instrument; use cuprate_consensus_rules::blocks::{penalty_free_zone, PENALTY_FREE_ZONE_5}; -use cuprate_helper::{asynch::rayon_spawn_async, num::median}; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_helper::{asynch::rayon_spawn_async, num::RollingMedian}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use crate::{Database, ExtendedConsensusError, HardFork}; @@ -29,7 +30,7 @@ const LONG_TERM_WINDOW: u64 = 100000; /// Configuration for the block weight cache. /// -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct BlockWeightsCacheConfig { short_term_window: u64, long_term_window: u64, @@ -58,25 +59,17 @@ impl BlockWeightsCacheConfig { /// /// These calculations require a lot of data from the database so by caching /// this data it reduces the load on the database. -#[derive(Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct BlockWeightsCache { /// The short term block weights. - short_term_block_weights: VecDeque, + short_term_block_weights: RollingMedian, /// The long term block weights. - long_term_weights: VecDeque, - - /// The short term block weights sorted so we don't have to sort them every time we need - /// the median. - cached_sorted_long_term_weights: Vec, - /// The long term block weights sorted so we don't have to sort them every time we need - /// the median. - cached_sorted_short_term_weights: Vec, + long_term_weights: RollingMedian, /// The height of the top block. - tip_height: u64, + pub(crate) tip_height: u64, - /// The block weight config. - config: BlockWeightsCacheConfig, + pub(crate) config: BlockWeightsCacheConfig, } impl BlockWeightsCache { @@ -86,45 +79,109 @@ impl BlockWeightsCache { chain_height: u64, config: BlockWeightsCacheConfig, database: D, + chain: Chain, ) -> Result { tracing::info!("Initializing weight cache this may take a while."); let long_term_weights = get_long_term_weight_in_range( chain_height.saturating_sub(config.long_term_window)..chain_height, database.clone(), + chain, ) .await?; let short_term_block_weights = get_blocks_weight_in_range( chain_height.saturating_sub(config.short_term_window)..chain_height, database, + chain, ) .await?; tracing::info!("Initialized block weight cache, chain-height: {:?}, long term weights length: {:?}, short term weights length: {:?}", chain_height, long_term_weights.len(), short_term_block_weights.len()); - let mut cloned_short_term_weights = short_term_block_weights.clone(); - let mut cloned_long_term_weights = long_term_weights.clone(); Ok(BlockWeightsCache { - short_term_block_weights: short_term_block_weights.into(), - long_term_weights: long_term_weights.into(), - - cached_sorted_long_term_weights: rayon_spawn_async(|| { - cloned_long_term_weights.par_sort_unstable(); - cloned_long_term_weights + short_term_block_weights: rayon_spawn_async(move || { + RollingMedian::from_vec( + short_term_block_weights, + usize::try_from(config.short_term_window).unwrap(), + ) }) .await, - cached_sorted_short_term_weights: rayon_spawn_async(|| { - cloned_short_term_weights.par_sort_unstable(); - cloned_short_term_weights + long_term_weights: rayon_spawn_async(move || { + RollingMedian::from_vec( + long_term_weights, + usize::try_from(config.long_term_window).unwrap(), + ) }) .await, - tip_height: chain_height - 1, config, }) } + /// Pop some blocks from the top of the cache. + /// + /// The cache will be returned to the state it would have been in `numb_blocks` ago. + #[instrument(name = "pop_blocks_weight_cache", skip_all, fields(numb_blocks = numb_blocks))] + pub async fn pop_blocks_main_chain( + &mut self, + numb_blocks: u64, + database: D, + ) -> Result<(), ExtendedConsensusError> { + if self.long_term_weights.window_len() <= usize::try_from(numb_blocks).unwrap() { + // More blocks to pop than we have in the cache, so just restart a new cache. + *self = Self::init_from_chain_height( + self.tip_height - numb_blocks + 1, + self.config, + database, + Chain::Main, + ) + .await?; + + return Ok(()); + } + + let chain_height = self.tip_height + 1; + + let new_long_term_start_height = chain_height + .saturating_sub(self.config.long_term_window) + .saturating_sub(numb_blocks); + + let old_long_term_weights = get_long_term_weight_in_range( + new_long_term_start_height + // current_chain_height - self.long_term_weights.len() blocks are already in the cache. + ..(chain_height - u64::try_from(self.long_term_weights.window_len()).unwrap()), + database.clone(), + Chain::Main, + ) + .await?; + + let new_short_term_start_height = chain_height + .saturating_sub(self.config.short_term_window) + .saturating_sub(numb_blocks); + + let old_short_term_weights = get_blocks_weight_in_range( + new_short_term_start_height + // current_chain_height - self.long_term_weights.len() blocks are already in the cache. + ..(chain_height - u64::try_from(self.short_term_block_weights.window_len()).unwrap()), + database, + Chain::Main + ) + .await?; + + for _ in 0..numb_blocks { + self.short_term_block_weights.pop_back(); + self.long_term_weights.pop_back(); + } + + self.long_term_weights.append_front(old_long_term_weights); + self.short_term_block_weights + .append_front(old_short_term_weights); + self.tip_height -= numb_blocks; + + Ok(()) + } + /// Add a new block to the cache. /// /// The block_height **MUST** be one more than the last height the cache has @@ -139,74 +196,19 @@ impl BlockWeightsCache { long_term_weight ); - // add the new block to the `long_term_weights` list and the sorted `cached_sorted_long_term_weights` list. - self.long_term_weights.push_back(long_term_weight); - match self - .cached_sorted_long_term_weights - .binary_search(&long_term_weight) - { - Ok(idx) | Err(idx) => self - .cached_sorted_long_term_weights - .insert(idx, long_term_weight), - } + self.long_term_weights.push(long_term_weight); - // If the list now has too many entries remove the oldest. - if u64::try_from(self.long_term_weights.len()).unwrap() > self.config.long_term_window { - let val = self - .long_term_weights - .pop_front() - .expect("long term window can't be negative"); - - match self.cached_sorted_long_term_weights.binary_search(&val) { - Ok(idx) => self.cached_sorted_long_term_weights.remove(idx), - Err(_) => panic!("Long term cache has incorrect values!"), - }; - } - - // add the block to the short_term_block_weights and the sorted cached_sorted_short_term_weights list. - self.short_term_block_weights.push_back(block_weight); - match self - .cached_sorted_short_term_weights - .binary_search(&block_weight) - { - Ok(idx) | Err(idx) => self - .cached_sorted_short_term_weights - .insert(idx, block_weight), - } - - // If there are now too many entries remove the oldest. - if u64::try_from(self.short_term_block_weights.len()).unwrap() - > self.config.short_term_window - { - let val = self - .short_term_block_weights - .pop_front() - .expect("short term window can't be negative"); - - match self.cached_sorted_short_term_weights.binary_search(&val) { - Ok(idx) => self.cached_sorted_short_term_weights.remove(idx), - Err(_) => panic!("Short term cache has incorrect values"), - }; - } - - debug_assert_eq!( - self.cached_sorted_long_term_weights.len(), - self.long_term_weights.len() - ); - debug_assert_eq!( - self.cached_sorted_short_term_weights.len(), - self.short_term_block_weights.len() - ); + self.short_term_block_weights.push(block_weight); } /// Returns the median long term weight over the last [`LONG_TERM_WINDOW`] blocks, or custom amount of blocks in the config. pub fn median_long_term_weight(&self) -> usize { - median(&self.cached_sorted_long_term_weights) + self.long_term_weights.median() } /// Returns the median weight over the last [`SHORT_TERM_WINDOW`] blocks, or custom amount of blocks in the config. pub fn median_short_term_weight(&self) -> usize { - median(&self.cached_sorted_short_term_weights) + self.short_term_block_weights.median() } /// Returns the effective median weight, used for block reward calculations and to calculate @@ -290,11 +292,12 @@ pub fn calculate_block_long_term_weight( async fn get_blocks_weight_in_range( range: Range, database: D, + chain: Chain, ) -> Result, ExtendedConsensusError> { tracing::info!("getting block weights."); let BCResponse::BlockExtendedHeaderInRange(ext_headers) = database - .oneshot(BCReadRequest::BlockExtendedHeaderInRange(range)) + .oneshot(BCReadRequest::BlockExtendedHeaderInRange(range, chain)) .await? else { panic!("Database sent incorrect response!") @@ -311,11 +314,12 @@ async fn get_blocks_weight_in_range( async fn get_long_term_weight_in_range( range: Range, database: D, + chain: Chain, ) -> Result, ExtendedConsensusError> { tracing::info!("getting block long term weights."); let BCResponse::BlockExtendedHeaderInRange(ext_headers) = database - .oneshot(BCReadRequest::BlockExtendedHeaderInRange(range)) + .oneshot(BCReadRequest::BlockExtendedHeaderInRange(range, chain)) .await? else { panic!("Database sent incorrect response!") diff --git a/consensus/src/tests/context/difficulty.rs b/consensus/src/tests/context/difficulty.rs index c9886f3..b59f62e 100644 --- a/consensus/src/tests/context/difficulty.rs +++ b/consensus/src/tests/context/difficulty.rs @@ -1,15 +1,15 @@ use std::collections::VecDeque; -use proptest::collection::size_range; +use proptest::collection::{size_range, vec}; use proptest::{prelude::*, prop_assert_eq, prop_compose, proptest}; -use cuprate_helper::num::median; - use crate::{ context::difficulty::*, tests::{context::data::DIF_3000000_3002000, mock_db::*}, HardFork, }; +use cuprate_helper::num::median; +use cuprate_types::Chain; const TEST_WINDOW: usize = 72; const TEST_CUT: usize = 6; @@ -26,9 +26,13 @@ async fn first_3_blocks_fixed_difficulty() -> Result<(), tower::BoxError> { let genesis = DummyBlockExtendedHeader::default().with_difficulty_info(0, 1); db_builder.add_block(genesis); - let mut difficulty_cache = - DifficultyCache::init_from_chain_height(1, TEST_DIFFICULTY_CONFIG, db_builder.finish(None)) - .await?; + let mut difficulty_cache = DifficultyCache::init_from_chain_height( + 1, + TEST_DIFFICULTY_CONFIG, + db_builder.finish(None), + Chain::Main, + ) + .await?; for height in 1..3 { assert_eq!(difficulty_cache.next_difficulty(&HardFork::V1), 1); @@ -42,9 +46,13 @@ async fn genesis_block_skipped() -> Result<(), tower::BoxError> { let mut db_builder = DummyDatabaseBuilder::default(); let genesis = DummyBlockExtendedHeader::default().with_difficulty_info(0, 1); db_builder.add_block(genesis); - let diff_cache = - DifficultyCache::init_from_chain_height(1, TEST_DIFFICULTY_CONFIG, db_builder.finish(None)) - .await?; + let diff_cache = DifficultyCache::init_from_chain_height( + 1, + TEST_DIFFICULTY_CONFIG, + db_builder.finish(None), + Chain::Main, + ) + .await?; assert!(diff_cache.cumulative_difficulties.is_empty()); assert!(diff_cache.timestamps.is_empty()); Ok(()) @@ -66,8 +74,9 @@ async fn calculate_diff_3000000_3002000() -> Result<(), tower::BoxError> { let mut diff_cache = DifficultyCache::init_from_chain_height( 3_000_720, - cfg.clone(), + cfg, db_builder.finish(Some(3_000_720)), + Chain::Main, ) .await?; @@ -208,4 +217,52 @@ proptest! { } } + + #[test] + fn pop_blocks_below_total_blocks( + mut database in arb_dummy_database(20), + new_blocks in vec(any::<(u64, u128)>(), 0..500) + ) { + tokio_test::block_on(async move { + let old_cache = DifficultyCache::init_from_chain_height(19, TEST_DIFFICULTY_CONFIG, database.clone(), Chain::Main).await.unwrap(); + + let blocks_to_pop = new_blocks.len(); + + let mut new_cache = old_cache.clone(); + for (timestamp, cumulative_difficulty) in new_blocks.into_iter() { + database.add_block(DummyBlockExtendedHeader::default().with_difficulty_info(timestamp, cumulative_difficulty)); + new_cache.new_block(new_cache.last_accounted_height+1, timestamp, cumulative_difficulty); + } + + new_cache.pop_blocks_main_chain(blocks_to_pop as u64, database).await?; + + prop_assert_eq!(new_cache, old_cache); + + Ok::<_, TestCaseError>(()) + })?; + } + + #[test] + fn pop_blocks_above_total_blocks( + mut database in arb_dummy_database(2000), + new_blocks in vec(any::<(u64, u128)>(), 0..5_000) + ) { + tokio_test::block_on(async move { + let old_cache = DifficultyCache::init_from_chain_height(1999, TEST_DIFFICULTY_CONFIG, database.clone(), Chain::Main).await.unwrap(); + + let blocks_to_pop = new_blocks.len(); + + let mut new_cache = old_cache.clone(); + for (timestamp, cumulative_difficulty) in new_blocks.into_iter() { + database.add_block(DummyBlockExtendedHeader::default().with_difficulty_info(timestamp, cumulative_difficulty)); + new_cache.new_block(new_cache.last_accounted_height+1, timestamp, cumulative_difficulty); + } + + new_cache.pop_blocks_main_chain(blocks_to_pop as u64, database).await?; + + prop_assert_eq!(new_cache, old_cache); + + Ok::<_, TestCaseError>(()) + })?; + } } diff --git a/consensus/src/tests/context/hardforks.rs b/consensus/src/tests/context/hardforks.rs index f6f0f23..d003b3c 100644 --- a/consensus/src/tests/context/hardforks.rs +++ b/consensus/src/tests/context/hardforks.rs @@ -1,3 +1,5 @@ +use proptest::{collection::vec, prelude::*}; + use cuprate_consensus_rules::hard_forks::{HFInfo, HFsInfo, HardFork, NUMB_OF_HARD_FORKS}; use crate::{ @@ -82,3 +84,44 @@ async fn hf_v15_v16_correct() { assert_eq!(state.current_hardfork, HardFork::V16); } + +proptest! { + fn pop_blocks( + hfs in vec(any::(), 0..100), + extra_hfs in vec(any::(), 0..100) + ) { + tokio_test::block_on(async move { + let numb_hfs = hfs.len() as u64; + let numb_pop_blocks = extra_hfs.len() as u64; + + let mut db_builder = DummyDatabaseBuilder::default(); + + for hf in hfs { + db_builder.add_block( + DummyBlockExtendedHeader::default().with_hard_fork_info(hf, hf), + ); + } + + let db = db_builder.finish(Some(numb_hfs as usize)); + + let mut state = HardForkState::init_from_chain_height( + numb_hfs, + TEST_HARD_FORK_CONFIG, + db.clone(), + ) + .await?; + + let state_clone = state.clone(); + + for (i, hf) in extra_hfs.into_iter().enumerate() { + state.new_block(hf, state.last_height + u64::try_from(i).unwrap() + 1); + } + + state.pop_blocks_main_chain(numb_pop_blocks, db).await?; + + prop_assert_eq!(state_clone, state); + + Ok::<(), TestCaseError>(()) + })?; + } +} diff --git a/consensus/src/tests/context/weight.rs b/consensus/src/tests/context/weight.rs index 902d446..83c8bb9 100644 --- a/consensus/src/tests/context/weight.rs +++ b/consensus/src/tests/context/weight.rs @@ -6,6 +6,7 @@ use crate::{ tests::{context::data::BW_2850000_3050000, mock_db::*}, HardFork, }; +use cuprate_types::Chain; pub const TEST_WEIGHT_CONFIG: BlockWeightsCacheConfig = BlockWeightsCacheConfig::new(100, 5000); @@ -21,6 +22,7 @@ async fn blocks_out_of_window_not_counted() -> Result<(), tower::BoxError> { 5000, TEST_WEIGHT_CONFIG, db_builder.finish(None), + Chain::Main, ) .await?; assert_eq!(weight_cache.median_long_term_weight(), 2500); @@ -37,6 +39,74 @@ async fn blocks_out_of_window_not_counted() -> Result<(), tower::BoxError> { Ok(()) } +#[tokio::test] +async fn pop_blocks_greater_than_window() -> Result<(), tower::BoxError> { + let mut db_builder = DummyDatabaseBuilder::default(); + for weight in 1..=5000 { + let block = DummyBlockExtendedHeader::default().with_weight_into(weight, weight); + db_builder.add_block(block); + } + + let database = db_builder.finish(None); + + let mut weight_cache = BlockWeightsCache::init_from_chain_height( + 5000, + TEST_WEIGHT_CONFIG, + database.clone(), + Chain::Main, + ) + .await?; + + let old_cache = weight_cache.clone(); + + weight_cache.new_block(5000, 0, 0); + weight_cache.new_block(5001, 0, 0); + weight_cache.new_block(5002, 0, 0); + + weight_cache + .pop_blocks_main_chain(3, database) + .await + .unwrap(); + + assert_eq!(weight_cache, old_cache); + + Ok(()) +} + +#[tokio::test] +async fn pop_blocks_less_than_window() -> Result<(), tower::BoxError> { + let mut db_builder = DummyDatabaseBuilder::default(); + for weight in 1..=500 { + let block = DummyBlockExtendedHeader::default().with_weight_into(weight, weight); + db_builder.add_block(block); + } + + let database = db_builder.finish(None); + + let mut weight_cache = BlockWeightsCache::init_from_chain_height( + 500, + TEST_WEIGHT_CONFIG, + database.clone(), + Chain::Main, + ) + .await?; + + let old_cache = weight_cache.clone(); + + weight_cache.new_block(500, 0, 0); + weight_cache.new_block(501, 0, 0); + weight_cache.new_block(502, 0, 0); + + weight_cache + .pop_blocks_main_chain(3, database) + .await + .unwrap(); + + assert_eq!(weight_cache, old_cache); + + Ok(()) +} + #[tokio::test] async fn weight_cache_calculates_correct_median() -> Result<(), tower::BoxError> { let mut db_builder = DummyDatabaseBuilder::default(); @@ -44,9 +114,13 @@ async fn weight_cache_calculates_correct_median() -> Result<(), tower::BoxError> let block = DummyBlockExtendedHeader::default().with_weight_into(0, 0); db_builder.add_block(block); - let mut weight_cache = - BlockWeightsCache::init_from_chain_height(1, TEST_WEIGHT_CONFIG, db_builder.finish(None)) - .await?; + let mut weight_cache = BlockWeightsCache::init_from_chain_height( + 1, + TEST_WEIGHT_CONFIG, + db_builder.finish(None), + Chain::Main, + ) + .await?; for height in 1..=100 { weight_cache.new_block(height as u64, height, height); @@ -76,6 +150,7 @@ async fn calc_bw_ltw_2850000_3050000() { 2950000, TEST_WEIGHT_CONFIG, db_builder.finish(Some(2950000)), + Chain::Main, ) .await .unwrap(); diff --git a/consensus/src/tests/mock_db.rs b/consensus/src/tests/mock_db.rs index d1c6255..c4fd75d 100644 --- a/consensus/src/tests/mock_db.rs +++ b/consensus/src/tests/mock_db.rs @@ -127,6 +127,12 @@ pub struct DummyDatabase { dummy_height: Option, } +impl DummyDatabase { + pub fn add_block(&mut self, block: DummyBlockExtendedHeader) { + self.blocks.write().unwrap().push(block) + } +} + impl Service for DummyDatabase { type Response = BCResponse; type Error = BoxError; @@ -161,12 +167,12 @@ impl Service for DummyDatabase { .ok_or("block not in database!")?, ) } - BCReadRequest::BlockHash(id) => { + BCReadRequest::BlockHash(id, _) => { let mut hash = [0; 32]; hash[0..8].copy_from_slice(&id.to_le_bytes()); BCResponse::BlockHash(hash) } - BCReadRequest::BlockExtendedHeaderInRange(range) => { + BCReadRequest::BlockExtendedHeaderInRange(range, _) => { let mut end = usize::try_from(range.end).unwrap(); let mut start = usize::try_from(range.start).unwrap(); @@ -200,7 +206,7 @@ impl Service for DummyDatabase { BCResponse::ChainHeight(height, top_hash) } - BCReadRequest::GeneratedCoins => BCResponse::GeneratedCoins(0), + BCReadRequest::GeneratedCoins(_) => BCResponse::GeneratedCoins(0), _ => unimplemented!("the context svc should not need these requests!"), }) } diff --git a/helper/src/num.rs b/helper/src/num.rs index cc1feb1..f90357e 100644 --- a/helper/src/num.rs +++ b/helper/src/num.rs @@ -8,6 +8,9 @@ use core::{ ops::{Add, Div, Mul, Sub}, }; +#[cfg(feature = "std")] +mod rolling_median; + //---------------------------------------------------------------------------------------------------- Types // INVARIANT: must be private. // Protects against outside-crate implementations. @@ -15,6 +18,9 @@ mod private { pub trait Sealed: Copy + PartialOrd + core::fmt::Display {} } +#[cfg(feature = "std")] +pub use rolling_median::RollingMedian; + /// Non-floating point numbers /// /// This trait is sealed and is only implemented on: diff --git a/helper/src/num/rolling_median.rs b/helper/src/num/rolling_median.rs new file mode 100644 index 0000000..2babda2 --- /dev/null +++ b/helper/src/num/rolling_median.rs @@ -0,0 +1,150 @@ +use std::{ + collections::VecDeque, + ops::{Add, Div, Mul, Sub}, +}; + +use crate::num::median; + +/// A rolling median type. +/// +/// This keeps track of a window of items and allows calculating the [`RollingMedian::median`] of them. +/// +/// Example: +/// ```rust +/// # use cuprate_helper::num::RollingMedian; +/// let mut rolling_median = RollingMedian::new(2); +/// +/// rolling_median.push(1); +/// assert_eq!(rolling_median.median(), 1); +/// assert_eq!(rolling_median.window_len(), 1); +/// +/// rolling_median.push(3); +/// assert_eq!(rolling_median.median(), 2); +/// assert_eq!(rolling_median.window_len(), 2); +/// +/// rolling_median.push(5); +/// assert_eq!(rolling_median.median(), 4); +/// assert_eq!(rolling_median.window_len(), 2); +/// ``` +/// +// TODO: a more efficient structure is probably possible. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] +pub struct RollingMedian { + /// The window of items, in order of insertion. + window: VecDeque, + /// The window of items, sorted. + sorted_window: Vec, + + /// The target window length. + target_window: usize, +} + +impl RollingMedian +where + T: Ord + + PartialOrd + + Add + + Sub + + Div + + Mul + + Copy + + From, +{ + /// Creates a new [`RollingMedian`] with a certain target window length. + /// + /// `target_window` is the maximum amount of items to keep in the rolling window. + pub fn new(target_window: usize) -> Self { + Self { + window: VecDeque::with_capacity(target_window), + sorted_window: Vec::with_capacity(target_window), + target_window, + } + } + + /// Creates a new [`RollingMedian`] from a [`Vec`] with a certain target window length. + /// + /// `target_window` is the maximum amount of items to keep in the rolling window. + /// + /// # Panics + /// This function panics if `vec.len() > target_window`. + pub fn from_vec(vec: Vec, target_window: usize) -> Self { + assert!(vec.len() <= target_window); + + let mut sorted_window = vec.clone(); + sorted_window.sort_unstable(); + + Self { + window: vec.into(), + sorted_window, + target_window, + } + } + + /// Pops the front of the window, i.e. the oldest item. + /// + /// This is often not needed as [`RollingMedian::push`] will handle popping old values when they fall + /// out of the window. + pub fn pop_front(&mut self) { + if let Some(item) = self.window.pop_front() { + match self.sorted_window.binary_search(&item) { + Ok(idx) => { + self.sorted_window.remove(idx); + } + Err(_) => panic!("Value expected to be in sorted_window was not there"), + } + } + } + + /// Pops the back of the window, i.e. the youngest item. + pub fn pop_back(&mut self) { + if let Some(item) = self.window.pop_back() { + match self.sorted_window.binary_search(&item) { + Ok(idx) => { + self.sorted_window.remove(idx); + } + Err(_) => panic!("Value expected to be in sorted_window was not there"), + } + } + } + + /// Push an item to the _back_ of the window. + /// + /// This will pop the oldest item in the window if the target length has been exceeded. + pub fn push(&mut self, item: T) { + if self.window.len() >= self.target_window { + self.pop_front(); + } + + self.window.push_back(item); + match self.sorted_window.binary_search(&item) { + Ok(idx) | Err(idx) => self.sorted_window.insert(idx, item), + } + } + + /// Append some values to the _front_ of the window. + /// + /// These new values will be the oldest items in the window. The order of the inputted items will be + /// kept, i.e. the first item in the [`Vec`] will be the oldest item in the queue. + pub fn append_front(&mut self, items: Vec) { + for item in items.into_iter().rev() { + self.window.push_front(item); + match self.sorted_window.binary_search(&item) { + Ok(idx) | Err(idx) => self.sorted_window.insert(idx, item), + } + + if self.window.len() > self.target_window { + self.pop_back(); + } + } + } + + /// Returns the number of items currently in the [`RollingMedian`]. + pub fn window_len(&self) -> usize { + self.window.len() + } + + /// Calculates the median of the values currently in the [`RollingMedian`]. + pub fn median(&self) -> T { + median(&self.sorted_window) + } +} diff --git a/storage/blockchain/src/service/mod.rs b/storage/blockchain/src/service/mod.rs index 1d9d10b..bf2d8e7 100644 --- a/storage/blockchain/src/service/mod.rs +++ b/storage/blockchain/src/service/mod.rs @@ -63,7 +63,7 @@ //! use hex_literal::hex; //! use tower::{Service, ServiceExt}; //! -//! use cuprate_types::blockchain::{BCReadRequest, BCWriteRequest, BCResponse}; +//! use cuprate_types::{blockchain::{BCReadRequest, BCWriteRequest, BCResponse}, Chain}; //! use cuprate_test_utils::data::block_v16_tx0; //! //! use cuprate_blockchain::{ @@ -85,7 +85,7 @@ //! //! // Prepare a request to write block. //! let mut block = block_v16_tx0().clone(); -//! # block.height = 0 as u64; // must be 0th height or panic in `add_block()` +//! # block.height = 0_u64; // must be 0th height or panic in `add_block()` //! let request = BCWriteRequest::WriteBlock(block); //! //! // Send the request. @@ -100,7 +100,7 @@ //! //! // Now, let's try getting the block hash //! // of the block we just wrote. -//! let request = BCReadRequest::BlockHash(0); +//! let request = BCReadRequest::BlockHash(0, Chain::Main); //! let response_channel = read_handle.ready().await?.call(request); //! let response = response_channel.await?; //! assert_eq!( diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 3f0b263..a5d51f1 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -17,7 +17,7 @@ use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError}; use cuprate_helper::{asynch::InfallibleOneshotReceiver, map::combine_low_high_bits_to_u128}; use cuprate_types::{ blockchain::{BCReadRequest, BCResponse}, - ExtendedBlockHeader, OutputOnChain, + Chain, ExtendedBlockHeader, OutputOnChain, }; use crate::{ @@ -206,11 +206,14 @@ fn map_request( let response = match request { R::BlockExtendedHeader(block) => block_extended_header(env, block), - R::BlockHash(block) => block_hash(env, block), + R::BlockHash(block, chain) => block_hash(env, block, chain), + R::FindBlock(_) => todo!("Add alt blocks to DB"), R::FilterUnknownHashes(hashes) => filter_unknown_hashes(env, hashes), - R::BlockExtendedHeaderInRange(range) => block_extended_header_in_range(env, range), + R::BlockExtendedHeaderInRange(range, chain) => { + block_extended_header_in_range(env, range, chain) + } R::ChainHeight => chain_height(env), - R::GeneratedCoins => generated_coins(env), + R::GeneratedCoins(height) => generated_coins(env, height), R::Outputs(map) => outputs(env, map), R::NumberOutputsWithAmount(vec) => number_outputs_with_amount(env, vec), R::KeyImagesSpent(set) => key_images_spent(env, set), @@ -312,15 +315,18 @@ fn block_extended_header(env: &ConcreteEnv, block_height: BlockHeight) -> Respon /// [`BCReadRequest::BlockHash`]. #[inline] -fn block_hash(env: &ConcreteEnv, block_height: BlockHeight) -> ResponseResult { +fn block_hash(env: &ConcreteEnv, block_height: BlockHeight, chain: Chain) -> ResponseResult { // Single-threaded, no `ThreadLocal` required. let env_inner = env.env_inner(); let tx_ro = env_inner.tx_ro()?; let table_block_infos = env_inner.open_db_ro::(&tx_ro)?; - Ok(BCResponse::BlockHash( - get_block_info(&block_height, &table_block_infos)?.block_hash, - )) + 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"), + }; + + Ok(BCResponse::BlockHash(block_hash)) } /// [`BCReadRequest::FilterUnknownHashes`]. @@ -356,6 +362,7 @@ fn filter_unknown_hashes(env: &ConcreteEnv, mut hashes: HashSet) -> R fn block_extended_header_in_range( env: &ConcreteEnv, range: std::ops::Range, + chain: Chain, ) -> ResponseResult { // Prepare tx/tables in `ThreadLocal`. let env_inner = env.env_inner(); @@ -363,14 +370,17 @@ fn block_extended_header_in_range( let tables = thread_local(env); // Collect results using `rayon`. - let vec = range - .into_par_iter() - .map(|block_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_block_extended_header_from_height(&block_height, tables) - }) - .collect::, RuntimeError>>()?; + let vec = match chain { + Chain::Main => range + .into_par_iter() + .map(|block_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_block_extended_header_from_height(&block_height, tables) + }) + .collect::, RuntimeError>>()?, + Chain::Alt(_) => todo!("Add alt blocks to DB"), + }; Ok(BCResponse::BlockExtendedHeaderInRange(vec)) } @@ -393,17 +403,14 @@ fn chain_height(env: &ConcreteEnv) -> ResponseResult { /// [`BCReadRequest::GeneratedCoins`]. #[inline] -fn generated_coins(env: &ConcreteEnv) -> ResponseResult { +fn generated_coins(env: &ConcreteEnv, height: u64) -> 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)?; let table_block_infos = env_inner.open_db_ro::(&tx_ro)?; - let top_height = top_block_height(&table_block_heights)?; - Ok(BCResponse::GeneratedCoins(cumulative_generated_coins( - &top_height, + &height, &table_block_infos, )?)) } diff --git a/storage/blockchain/src/service/tests.rs b/storage/blockchain/src/service/tests.rs index 4f3fbe4..c00e32f 100644 --- a/storage/blockchain/src/service/tests.rs +++ b/storage/blockchain/src/service/tests.rs @@ -19,7 +19,7 @@ use cuprate_database::{ConcreteEnv, DatabaseIter, DatabaseRo, Env, EnvInner, Run use cuprate_test_utils::data::{block_v16_tx0, block_v1_tx2, block_v9_tx3}; use cuprate_types::{ blockchain::{BCReadRequest, BCResponse, BCWriteRequest}, - OutputOnChain, VerifiedBlockInformation, + Chain, OutputOnChain, VerifiedBlockInformation, }; use crate::{ @@ -138,10 +138,15 @@ async fn test_template( Err(RuntimeError::KeyNotFound) }; + let test_chain_height = chain_height(tables.block_heights()).unwrap(); + let chain_height = { - let height = chain_height(tables.block_heights()).unwrap(); - let block_info = get_block_info(&height.saturating_sub(1), tables.block_infos()).unwrap(); - Ok(BCResponse::ChainHeight(height, block_info.block_hash)) + let block_info = + get_block_info(&test_chain_height.saturating_sub(1), tables.block_infos()).unwrap(); + Ok(BCResponse::ChainHeight( + test_chain_height, + block_info.block_hash, + )) }; let cumulative_generated_coins = Ok(BCResponse::GeneratedCoins(cumulative_generated_coins)); @@ -182,12 +187,21 @@ async fn test_template( BCReadRequest::BlockExtendedHeader(1), extended_block_header_1, ), - (BCReadRequest::BlockHash(0), block_hash_0), - (BCReadRequest::BlockHash(1), block_hash_1), - (BCReadRequest::BlockExtendedHeaderInRange(0..1), range_0_1), - (BCReadRequest::BlockExtendedHeaderInRange(0..2), range_0_2), + (BCReadRequest::BlockHash(0, Chain::Main), block_hash_0), + (BCReadRequest::BlockHash(1, Chain::Main), block_hash_1), + ( + BCReadRequest::BlockExtendedHeaderInRange(0..1, Chain::Main), + range_0_1, + ), + ( + BCReadRequest::BlockExtendedHeaderInRange(0..2, Chain::Main), + range_0_2, + ), (BCReadRequest::ChainHeight, chain_height), - (BCReadRequest::GeneratedCoins, cumulative_generated_coins), + ( + BCReadRequest::GeneratedCoins(test_chain_height), + cumulative_generated_coins, + ), (BCReadRequest::NumberOutputsWithAmount(num_req), num_resp), (BCReadRequest::KeyImagesSpent(ki_req), ki_resp), ] { diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index 4a280be..1ff06c2 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -9,7 +9,7 @@ use std::{ ops::Range, }; -use crate::types::{ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; +use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; //---------------------------------------------------------------------------------------------------- ReadRequest /// A read request to the blockchain database. @@ -29,8 +29,13 @@ pub enum BCReadRequest { /// Request a block's hash. /// - /// The input is the block's height. - BlockHash(u64), + /// The input is the block's height and the chain it is on. + BlockHash(u64, Chain), + + /// Request to check if we have a block and which [`Chain`] it is on. + /// + /// The input is the block's hash. + FindBlock([u8; 32]), /// Removes the block hashes that are not in the _main_ chain. /// @@ -40,15 +45,15 @@ pub enum BCReadRequest { /// Request a range of block extended headers. /// /// The input is a range of block heights. - BlockExtendedHeaderInRange(Range), + BlockExtendedHeaderInRange(Range, Chain), /// Request the current chain height. /// /// Note that this is not the top-block height. ChainHeight, - /// Request the total amount of generated coins (atomic units) so far. - GeneratedCoins, + /// Request the total amount of generated coins (atomic units) at this height. + GeneratedCoins(u64), /// Request data for multiple outputs. /// @@ -129,6 +134,11 @@ pub enum BCResponse { /// Inner value is the hash of the requested block. BlockHash([u8; 32]), + /// Response to [`BCReadRequest::FindBlock`]. + /// + /// Inner value is the chain and height of the block if found. + FindBlock(Option<(Chain, u64)>), + /// Response to [`BCReadRequest::FilterUnknownHashes`]. /// /// Inner value is the list of hashes that were in the main chain. @@ -146,7 +156,7 @@ pub enum BCResponse { /// Response to [`BCReadRequest::GeneratedCoins`]. /// - /// Inner value is the total amount of generated coins so far, in atomic units. + /// Inner value is the total amount of generated coins up to and including the chosen height, in atomic units. GeneratedCoins(u64), /// Response to [`BCReadRequest::Outputs`]. diff --git a/types/src/lib.rs b/types/src/lib.rs index 1cdb9d5..bcf6a45 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -84,7 +84,8 @@ mod types; pub use block_complete_entry::{BlockCompleteEntry, PrunedTxBlobEntry, TransactionBlobs}; pub use types::{ - ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation, VerifiedTransactionInformation, + AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, OutputOnChain, + VerifiedBlockInformation, VerifiedTransactionInformation, }; //---------------------------------------------------------------------------------------------------- Feature-gated diff --git a/types/src/types.rs b/types/src/types.rs index 76ffd57..db31507 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -38,7 +38,8 @@ pub struct ExtendedBlockHeader { //---------------------------------------------------------------------------------------------------- VerifiedTransactionInformation /// Verified information of a transaction. /// -/// This represents a transaction in a valid block. +/// - If this is in a [`VerifiedBlockInformation`] this represents a valid transaction +/// - If this is in an [`AltBlockInformation`] this represents a potentially valid transaction #[derive(Clone, Debug, PartialEq, Eq)] pub struct VerifiedTransactionInformation { /// The transaction itself. @@ -91,6 +92,53 @@ pub struct VerifiedBlockInformation { pub cumulative_difficulty: u128, } +//---------------------------------------------------------------------------------------------------- ChainID +/// A unique ID for an alt chain. +/// +/// The inner value is meaningless. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct ChainId(pub u64); + +//---------------------------------------------------------------------------------------------------- Chain +/// An identifier for a chain. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum Chain { + /// The main chain. + Main, + /// An alt chain. + Alt(ChainId), +} + +//---------------------------------------------------------------------------------------------------- AltBlockInformation +/// A block on an alternative chain. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AltBlockInformation { + /// The block itself. + pub block: Block, + /// The serialized byte form of [`Self::block`]. + /// + /// [`Block::serialize`]. + pub block_blob: Vec, + /// All the transactions in the block, excluding the [`Block::miner_tx`]. + pub txs: Vec, + /// The block's hash. + /// + /// [`Block::hash`]. + pub block_hash: [u8; 32], + /// The block's proof-of-work hash. + pub pow_hash: [u8; 32], + /// The block's height. + pub height: u64, + /// 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 cumulative difficulty of all blocks up until and including this block. + pub cumulative_difficulty: u128, + /// The [`ChainId`] of the chain this alt block is on. + pub chain_id: ChainId, +} + //---------------------------------------------------------------------------------------------------- OutputOnChain /// An already existing transaction output. #[derive(Clone, Copy, Debug, PartialEq, Eq)]