diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index 874d5a8c..846c1ac1 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -45,6 +45,7 @@ cryptonight-cuprate = {path = "../cryptonight"} rayon = "1" tokio = "1" +tokio-util = "0.7" # used in binaries monero-wire = {path="../net/monero-wire", optional = true} diff --git a/consensus/src/block.rs b/consensus/src/block.rs index e114293c..bfc69615 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -122,11 +122,14 @@ where Tx: Service, { tracing::debug!("getting blockchain context"); - let context = context_svc + let checked_context = context_svc .oneshot(BlockChainContextRequest) .await .map_err(Into::::into)?; + // TODO: should we unwrap here, we did just get the data so it should be ok. + let context = checked_context.blockchain_context().unwrap(); + tracing::debug!("got blockchain context: {:?}", context); let block_weight = block.miner_tx.weight() + txs.iter().map(|tx| tx.tx_weight).sum::(); @@ -208,11 +211,14 @@ where Tx: Service, { tracing::debug!("getting blockchain context"); - let context = context_svc + let checked_context = context_svc .oneshot(BlockChainContextRequest) .await .map_err(Into::::into)?; + // TODO: should we unwrap here, we did just get the data so it should be ok. + let context = checked_context.blockchain_context().unwrap(); + tracing::debug!("got blockchain context: {:?}", context); // TODO: reorder these tests so we do the cheap tests first. diff --git a/consensus/src/context.rs b/consensus/src/context.rs index 6f6e7ffe..da248741 100644 --- a/consensus/src/context.rs +++ b/consensus/src/context.rs @@ -16,13 +16,17 @@ use std::{ use futures::FutureExt; use tokio::sync::RwLock; +use tokio_util::sync::CancellationToken; use tower::{Service, ServiceExt}; use crate::{helper::current_time, ConsensusError, Database, DatabaseRequest, DatabaseResponse}; -pub mod difficulty; -pub mod hardforks; -pub mod weight; +mod difficulty; +mod hardforks; +mod weight; + +#[cfg(test)] +mod tests; pub use difficulty::DifficultyCacheConfig; pub use hardforks::{HardFork, HardForkConfig}; @@ -31,9 +35,9 @@ pub use weight::BlockWeightsCacheConfig; const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; pub struct ContextConfig { - hard_fork_cfg: HardForkConfig, - difficulty_cfg: DifficultyCacheConfig, - weights_config: BlockWeightsCacheConfig, + pub hard_fork_cfg: HardForkConfig, + pub difficulty_cfg: DifficultyCacheConfig, + pub weights_config: BlockWeightsCacheConfig, } impl ContextConfig { @@ -114,6 +118,7 @@ where let context_svc = BlockChainContextService { internal_blockchain_context: Arc::new( InternalBlockChainContext { + current_validity_token: CancellationToken::new(), difficulty_cache: difficulty_cache_handle.await.unwrap()?, weight_cache: weight_cache_handle.await.unwrap()?, hardfork_state: hardfork_state_handle.await.unwrap()?, @@ -130,8 +135,10 @@ where Ok((context_svc_update.clone(), context_svc_update)) } +/// Raw blockchain context, gotten from [`BlockChainContext`]. This data may turn invalid so is not ok to keep +/// around. You should keep around [`BlockChainContext`] instead. #[derive(Debug, Clone, Copy)] -pub struct BlockChainContext { +pub struct RawBlockChainContext { /// The next blocks difficulty. pub next_difficulty: u128, /// The current cumulative difficulty. @@ -156,7 +163,7 @@ pub struct BlockChainContext { pub current_hard_fork: HardFork, } -impl BlockChainContext { +impl RawBlockChainContext { /// Returns the timestamp the should be used when checking locked outputs. /// /// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time @@ -197,11 +204,44 @@ impl BlockChainContext { } } +/// Blockchain context which keeps a token of validity so users will know when the data is no longer valid. +#[derive(Debug, Clone)] +pub struct BlockChainContext { + /// A token representing this data's validity. + validity_token: CancellationToken, + /// The actual block chain context. + raw: RawBlockChainContext, +} + +#[derive(Debug, Clone, Copy, thiserror::Error)] +#[error("data is no longer valid")] +pub struct DataNoLongerValid; + +impl BlockChainContext { + /// Checks if the data is still valid. + pub fn is_still_valid(&self) -> bool { + !self.validity_token.is_cancelled() + } + + /// Checks if the data is valid returning an Err if not and a reference to the blockchain context if + /// it is. + pub fn blockchain_context(&self) -> Result { + if !self.is_still_valid() { + return Err(DataNoLongerValid); + } + Ok(self.raw) + } +} + #[derive(Debug, Clone)] pub struct BlockChainContextRequest; #[derive(Clone)] struct InternalBlockChainContext { + /// A token used to invalidate previous contexts when a new + /// block is added to the chain. + current_validity_token: CancellationToken, + difficulty_cache: difficulty::DifficultyCache, weight_cache: weight::BlockWeightsCache, hardfork_state: hardforks::HardForkState, @@ -233,6 +273,7 @@ impl Service for BlockChainContextService { let internal_blockchain_context_lock = internal_blockchain_context.read().await; let InternalBlockChainContext { + current_validity_token, difficulty_cache, weight_cache, hardfork_state, @@ -244,18 +285,24 @@ impl Service for BlockChainContextService { let current_hf = hardfork_state.current_hardfork(); Ok(BlockChainContext { - next_difficulty: difficulty_cache.next_difficulty(¤t_hf), - cumulative_difficulty: difficulty_cache.cumulative_difficulty(), - effective_median_weight: weight_cache.effective_median_block_weight(¤t_hf), - median_long_term_weight: weight_cache.median_long_term_weight(), - median_weight_for_block_reward: weight_cache.median_for_block_reward(¤t_hf), - already_generated_coins: *already_generated_coins, - top_block_timestamp: difficulty_cache.top_block_timestamp(), - median_block_timestamp: difficulty_cache - .median_timestamp(usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap()), - chain_height: *chain_height, - top_hash: *top_block_hash, - current_hard_fork: current_hf, + validity_token: current_validity_token.child_token(), + raw: RawBlockChainContext { + next_difficulty: difficulty_cache.next_difficulty(¤t_hf), + cumulative_difficulty: difficulty_cache.cumulative_difficulty(), + effective_median_weight: weight_cache + .effective_median_block_weight(¤t_hf), + median_long_term_weight: weight_cache.median_long_term_weight(), + median_weight_for_block_reward: weight_cache + .median_for_block_reward(¤t_hf), + already_generated_coins: *already_generated_coins, + top_block_timestamp: difficulty_cache.top_block_timestamp(), + median_block_timestamp: difficulty_cache.median_timestamp( + usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap(), + ), + chain_height: *chain_height, + top_hash: *top_block_hash, + current_hard_fork: current_hf, + }, }) } .boxed() @@ -291,6 +338,7 @@ impl tower::Service for BlockChainContextService { let mut internal_blockchain_context_lock = internal_blockchain_context.write().await; let InternalBlockChainContext { + current_validity_token, difficulty_cache, weight_cache, hardfork_state, @@ -299,11 +347,14 @@ impl tower::Service for BlockChainContextService { already_generated_coins, } = internal_blockchain_context_lock.deref_mut(); + // Cancel the validity token and replace it with a new one. + std::mem::replace(current_validity_token, CancellationToken::new()).cancel(); + difficulty_cache.new_block(new.height, new.timestamp, new.cumulative_difficulty); weight_cache.new_block(new.height, new.weight, new.long_term_weight); - hardfork_state.new_block(new.vote, new.height).await?; + hardfork_state.new_block(new.vote, new.height); *chain_height = new.height + 1; *top_block_hash = new.new_top_hash; diff --git a/consensus/src/context/difficulty.rs b/consensus/src/context/difficulty.rs index 6a39295c..f8b380d3 100644 --- a/consensus/src/context/difficulty.rs +++ b/consensus/src/context/difficulty.rs @@ -8,7 +8,7 @@ use crate::{ }; #[cfg(test)] -mod tests; +pub(super) mod tests; /// The amount of blocks we account for to calculate difficulty const DIFFICULTY_WINDOW: usize = 720; @@ -69,22 +69,6 @@ pub struct DifficultyCache { } impl DifficultyCache { - pub async fn init( - config: DifficultyCacheConfig, - mut database: D, - ) -> Result { - let DatabaseResponse::ChainHeight(chain_height, _) = database - .ready() - .await? - .call(DatabaseRequest::ChainHeight) - .await? - else { - panic!("Database sent incorrect response") - }; - - DifficultyCache::init_from_chain_height(chain_height, config, database).await - } - #[instrument(name = "init_difficulty_cache", level = "info", skip(database, config))] pub async fn init_from_chain_height( chain_height: u64, diff --git a/consensus/src/context/difficulty/tests.rs b/consensus/src/context/difficulty/tests.rs index cfabdc6f..71e59e14 100644 --- a/consensus/src/context/difficulty/tests.rs +++ b/consensus/src/context/difficulty/tests.rs @@ -11,7 +11,7 @@ const TEST_LAG: usize = 2; const TEST_TOTAL_ACCOUNTED_BLOCKS: usize = TEST_WINDOW + TEST_LAG; -const TEST_DIFFICULTY_CONFIG: DifficultyCacheConfig = +pub const TEST_DIFFICULTY_CONFIG: DifficultyCacheConfig = DifficultyCacheConfig::new(TEST_WINDOW, TEST_CUT, TEST_LAG); #[tokio::test] @@ -21,7 +21,8 @@ async fn first_3_blocks_fixed_difficulty() -> Result<(), tower::BoxError> { db_builder.add_block(genesis); let mut difficulty_cache = - DifficultyCache::init(TEST_DIFFICULTY_CONFIG, db_builder.finish()).await?; + DifficultyCache::init_from_chain_height(1, TEST_DIFFICULTY_CONFIG, db_builder.finish()) + .await?; for height in 1..3 { assert_eq!(difficulty_cache.next_difficulty(&HardFork::V1), 1); @@ -35,7 +36,9 @@ 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(TEST_DIFFICULTY_CONFIG, db_builder.finish()).await?; + let diff_cache = + DifficultyCache::init_from_chain_height(1, TEST_DIFFICULTY_CONFIG, db_builder.finish()) + .await?; assert!(diff_cache.cumulative_difficulties.is_empty()); assert!(diff_cache.timestamps.is_empty()); Ok(()) diff --git a/consensus/src/context/hardforks.rs b/consensus/src/context/hardforks.rs index 49ef0868..30bfffb0 100644 --- a/consensus/src/context/hardforks.rs +++ b/consensus/src/context/hardforks.rs @@ -12,7 +12,7 @@ use tracing::instrument; use crate::{ConsensusError, Database, DatabaseRequest, DatabaseResponse}; #[cfg(test)] -mod tests; +pub(super) mod tests; // https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork const DEFAULT_WINDOW_SIZE: u64 = 10080; // supermajority window check length - a week @@ -263,24 +263,6 @@ pub struct HardForkState { } impl HardForkState { - pub async fn init( - config: HardForkConfig, - mut database: D, - ) -> Result { - let DatabaseResponse::ChainHeight(chain_height, _) = database - .ready() - .await? - .call(DatabaseRequest::ChainHeight) - .await? - else { - panic!("Database sent incorrect response") - }; - - let hfs = HardForkState::init_from_chain_height(chain_height, config, database).await?; - - Ok(hfs) - } - #[instrument(name = "init_hardfork_state", skip(config, database), level = "info")] pub async fn init_from_chain_height( chain_height: u64, @@ -336,7 +318,7 @@ impl HardForkState { Ok(hfs) } - pub async fn new_block(&mut self, vote: HardFork, height: u64) -> Result<(), ConsensusError> { + pub fn new_block(&mut self, vote: HardFork, height: u64) { assert_eq!(self.last_height + 1, height); self.last_height += 1; @@ -353,7 +335,6 @@ impl HardForkState { } self.check_set_new_hf(); - Ok(()) } /// Checks if the next hard-fork should be activated and activates it if it should. diff --git a/consensus/src/context/hardforks/tests.rs b/consensus/src/context/hardforks/tests.rs index 417a19d3..21c8688d 100644 --- a/consensus/src/context/hardforks/tests.rs +++ b/consensus/src/context/hardforks/tests.rs @@ -26,7 +26,7 @@ const TEST_HFS: [HFInfo; NUMB_OF_HARD_FORKS] = [ HFInfo::new(150, 0), ]; -const TEST_HARD_FORK_CONFIG: HardForkConfig = HardForkConfig { +pub const TEST_HARD_FORK_CONFIG: HardForkConfig = HardForkConfig { window: TEST_WINDOW_SIZE, forks: TEST_HFS, }; @@ -62,9 +62,13 @@ async fn hard_fork_set_depends_on_top_block() { DummyBlockExtendedHeader::default().with_hard_fork_info(HardFork::V14, HardFork::V16), ); - let state = HardForkState::init(TEST_HARD_FORK_CONFIG, db_builder.finish()) - .await - .unwrap(); + let state = HardForkState::init_from_chain_height( + TEST_WINDOW_SIZE + 1, + TEST_HARD_FORK_CONFIG, + db_builder.finish(), + ) + .await + .unwrap(); assert_eq!(state.current_hardfork, HardFork::V14); } diff --git a/consensus/src/context/tests.rs b/consensus/src/context/tests.rs new file mode 100644 index 00000000..e70c3ecb --- /dev/null +++ b/consensus/src/context/tests.rs @@ -0,0 +1,74 @@ +use proptest::strategy::ValueTree; +use proptest::{strategy::Strategy, test_runner::TestRunner}; +use tower::ServiceExt; + +use super::{ + difficulty::tests::TEST_DIFFICULTY_CONFIG, hardforks::tests::TEST_HARD_FORK_CONFIG, + initialize_blockchain_context, weight::tests::TEST_WEIGHT_CONFIG, BlockChainContextRequest, + ContextConfig, UpdateBlockchainCacheRequest, +}; +use crate::{test_utils::mock_db::*, HardFork}; + +const TEST_CONTEXT_CONFIG: ContextConfig = ContextConfig { + hard_fork_cfg: TEST_HARD_FORK_CONFIG, + difficulty_cfg: TEST_DIFFICULTY_CONFIG, + weights_config: TEST_WEIGHT_CONFIG, +}; + +#[tokio::test] +async fn context_invalidated_on_new_block() -> Result<(), tower::BoxError> { + const BLOCKCHAIN_HEIGHT: u64 = 6000; + + let mut runner = TestRunner::default(); + let db = arb_dummy_database(BLOCKCHAIN_HEIGHT.try_into().unwrap()) + .new_tree(&mut runner) + .unwrap() + .current(); + + let (ctx_svc, updater) = initialize_blockchain_context(TEST_CONTEXT_CONFIG, db).await?; + + let context = ctx_svc.oneshot(BlockChainContextRequest).await?; + + assert!(context.is_still_valid()); + assert!(context.is_still_valid()); + assert!(context.is_still_valid()); + + updater + .oneshot(UpdateBlockchainCacheRequest { + new_top_hash: [0; 32], + height: BLOCKCHAIN_HEIGHT, + timestamp: 0, + weight: 0, + long_term_weight: 0, + generated_coins: 0, + vote: HardFork::V1, + cumulative_difficulty: 0, + }) + .await?; + + assert!(!context.is_still_valid()); + + Ok(()) +} + +#[tokio::test] +async fn context_height_correct() -> Result<(), tower::BoxError> { + const BLOCKCHAIN_HEIGHT: u64 = 6000; + + let mut runner = TestRunner::default(); + let db = arb_dummy_database(BLOCKCHAIN_HEIGHT.try_into().unwrap()) + .new_tree(&mut runner) + .unwrap() + .current(); + + let (ctx_svc, _) = initialize_blockchain_context(TEST_CONTEXT_CONFIG, db).await?; + + let context = ctx_svc.oneshot(BlockChainContextRequest).await?; + + assert_eq!( + context.blockchain_context().unwrap().chain_height, + BLOCKCHAIN_HEIGHT + ); + + Ok(()) +} diff --git a/consensus/src/context/weight.rs b/consensus/src/context/weight.rs index 35ce29d5..a4c46015 100644 --- a/consensus/src/context/weight.rs +++ b/consensus/src/context/weight.rs @@ -12,7 +12,6 @@ use std::{ ops::Range, }; -use monero_serai::{block::Block, transaction::Transaction}; use rayon::prelude::*; use tower::ServiceExt; use tracing::instrument; @@ -22,7 +21,7 @@ use crate::{ }; #[cfg(test)] -mod tests; +pub(super) mod tests; const PENALTY_FREE_ZONE_1: usize = 20000; const PENALTY_FREE_ZONE_2: usize = 60000; @@ -31,16 +30,6 @@ const PENALTY_FREE_ZONE_5: usize = 300000; const SHORT_TERM_WINDOW: u64 = 100; const LONG_TERM_WINDOW: u64 = 100000; -/// Calculates the blocks weight. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#blocks-weight -pub fn block_weight(block: &Block, txs: &[Transaction]) -> usize { - txs.iter() - .chain([&block.miner_tx]) - .map(|tx| tx.weight()) - .sum() -} - /// Returns the penalty free zone /// /// https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#penalty-free-zone @@ -94,23 +83,6 @@ pub struct BlockWeightsCache { } impl BlockWeightsCache { - /// Initialize the [`BlockWeightsCache`] at the the height of the database. - pub async fn init( - config: BlockWeightsCacheConfig, - mut database: D, - ) -> Result { - let DatabaseResponse::ChainHeight(chain_height, _) = database - .ready() - .await? - .call(DatabaseRequest::ChainHeight) - .await? - else { - panic!("Database sent incorrect response!"); - }; - - Self::init_from_chain_height(chain_height, config, database).await - } - /// Initialize the [`BlockWeightsCache`] at the the given chain height. #[instrument(name = "init_weight_cache", level = "info", skip(database, config))] pub async fn init_from_chain_height( diff --git a/consensus/src/context/weight/tests.rs b/consensus/src/context/weight/tests.rs index f726f9b0..73e9c267 100644 --- a/consensus/src/context/weight/tests.rs +++ b/consensus/src/context/weight/tests.rs @@ -1,7 +1,7 @@ use super::{BlockWeightsCache, BlockWeightsCacheConfig}; use crate::test_utils::mock_db::*; -const TEST_WEIGHT_CONFIG: BlockWeightsCacheConfig = BlockWeightsCacheConfig::new(100, 5000); +pub const TEST_WEIGHT_CONFIG: BlockWeightsCacheConfig = BlockWeightsCacheConfig::new(100, 5000); #[tokio::test] async fn blocks_out_of_window_not_counted() -> Result<(), tower::BoxError> { @@ -11,7 +11,9 @@ async fn blocks_out_of_window_not_counted() -> Result<(), tower::BoxError> { db_builder.add_block(block); } - let mut weight_cache = BlockWeightsCache::init(TEST_WEIGHT_CONFIG, db_builder.finish()).await?; + let mut weight_cache = + BlockWeightsCache::init_from_chain_height(5000, TEST_WEIGHT_CONFIG, db_builder.finish()) + .await?; assert_eq!(weight_cache.median_long_term_weight(), 2500); assert_eq!(weight_cache.median_short_term_weight(), 4950); @@ -33,7 +35,9 @@ 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(TEST_WEIGHT_CONFIG, db_builder.finish()).await?; + let mut weight_cache = + BlockWeightsCache::init_from_chain_height(1, TEST_WEIGHT_CONFIG, db_builder.finish()) + .await?; for height in 1..=100 { weight_cache.new_block(height as u64, height, height); diff --git a/consensus/src/test_utils/mock_db.rs b/consensus/src/test_utils/mock_db.rs index 237324c2..6ca33e14 100644 --- a/consensus/src/test_utils/mock_db.rs +++ b/consensus/src/test_utils/mock_db.rs @@ -1,4 +1,3 @@ -use futures::FutureExt; use std::{ future::Future, pin::Pin, @@ -6,20 +5,53 @@ use std::{ task::{Context, Poll}, }; -use cuprate_common::BlockID; +use futures::FutureExt; +use proptest::{ + arbitrary::{any, any_with}, + prop_compose, + sample::size_range, + strategy::Strategy, +}; +use proptest_derive::Arbitrary; use tower::{BoxError, Service}; +use cuprate_common::BlockID; + use crate::{DatabaseRequest, DatabaseResponse, ExtendedBlockHeader, HardFork}; -#[derive(Default, Debug, Clone, Copy)] +prop_compose! { + /// Generates an arbitrary full [`DummyDatabase`], it is not safe to do consensus checks on the returned database + /// but is ok for testing certain parts of the code with. + pub fn arb_dummy_database(height: usize) + ( + mut blocks in any_with::>(size_range(height).lift()) + ) -> DummyDatabase { + let mut builder = DummyDatabaseBuilder::default(); + + blocks.sort_by(|a, b| a.cumulative_difficulty.cmp(&b.cumulative_difficulty)); + + for block in blocks { + builder.add_block(block); + } + builder.finish() + } +} + +#[derive(Default, Debug, Clone, Copy, Arbitrary)] pub struct DummyBlockExtendedHeader { + #[proptest(strategy = "any::().prop_map(Some)")] pub version: Option, + #[proptest(strategy = "any::().prop_map(Some)")] pub vote: Option, + #[proptest(strategy = "any::().prop_map(Some)")] pub timestamp: Option, + #[proptest(strategy = "any::().prop_map(|x| Some(x % u128::from(u64::MAX)))")] pub cumulative_difficulty: Option, + #[proptest(strategy = "any::().prop_map(|x| Some(x % 100_000_000))")] pub block_weight: Option, + #[proptest(strategy = "any::().prop_map(|x| Some(x % 100_000_000))")] pub long_term_weight: Option, } @@ -85,7 +117,7 @@ impl DummyDatabaseBuilder { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DummyDatabase { blocks: Arc>>, }