diff --git a/consensus/core/src/errors/block.rs b/consensus/core/src/errors/block.rs index 9aab18905..92645eba7 100644 --- a/consensus/core/src/errors/block.rs +++ b/consensus/core/src/errors/block.rs @@ -147,6 +147,9 @@ pub enum RuleError { #[error("DAA window data has only {0} entries")] InsufficientDaaWindowSize(usize), + + #[error("cannot add block body to a pruned block")] + PrunedBlock, } pub type BlockProcessResult = std::result::Result; diff --git a/consensus/src/consensus/mod.rs b/consensus/src/consensus/mod.rs index 80babbef0..037fb01e6 100644 --- a/consensus/src/consensus/mod.rs +++ b/consensus/src/consensus/mod.rs @@ -235,6 +235,7 @@ impl Consensus { storage.headers_store.clone(), storage.block_transactions_store.clone(), storage.body_tips_store.clone(), + storage.pruning_point_store.clone(), services.reachability_service.clone(), services.coinbase_manager.clone(), services.mass_calculator.clone(), diff --git a/consensus/src/consensus/test_consensus.rs b/consensus/src/consensus/test_consensus.rs index c626e00ff..7f0d77137 100644 --- a/consensus/src/consensus/test_consensus.rs +++ b/consensus/src/consensus/test_consensus.rs @@ -117,7 +117,9 @@ impl TestConsensus { } pub fn build_header_with_parents(&self, hash: Hash, parents: Vec) -> Header { - let mut header = header_from_precomputed_hash(hash, parents); + let mut header = header_from_precomputed_hash(hash, Default::default()); + let parents_by_level = self.consensus.services.parents_manager.calc_block_parents(self.pruning_point(), &parents); + header.parents_by_level = parents_by_level; let ghostdag_data = self.consensus.services.ghostdag_primary_manager.ghostdag(header.direct_parents()); header.pruning_point = self .consensus @@ -134,8 +136,12 @@ impl TestConsensus { header } - pub fn add_block_with_parents(&self, hash: Hash, parents: Vec) -> impl Future> { - self.validate_and_insert_block(self.build_block_with_parents(hash, parents).to_immutable()).virtual_state_task + pub fn add_header_only_block_with_parents( + &self, + hash: Hash, + parents: Vec, + ) -> impl Future> { + self.validate_and_insert_block(self.build_header_only_block_with_parents(hash, parents).to_immutable()).virtual_state_task } pub fn add_utxo_valid_block_with_parents( @@ -149,6 +155,14 @@ impl TestConsensus { .virtual_state_task } + pub fn add_empty_utxo_valid_block_with_parents( + &self, + hash: Hash, + parents: Vec, + ) -> impl Future> { + self.add_utxo_valid_block_with_parents(hash, parents, vec![]) + } + pub fn build_utxo_valid_block_with_parents( &self, hash: Hash, @@ -180,7 +194,7 @@ impl TestConsensus { MutableBlock::new(header, txs) } - pub fn build_block_with_parents(&self, hash: Hash, parents: Vec) -> MutableBlock { + pub fn build_header_only_block_with_parents(&self, hash: Hash, parents: Vec) -> MutableBlock { MutableBlock::from_header(self.build_header_with_parents(hash, parents)) } diff --git a/consensus/src/pipeline/body_processor/body_validation_in_context.rs b/consensus/src/pipeline/body_processor/body_validation_in_context.rs index 2425556d0..44e76873b 100644 --- a/consensus/src/pipeline/body_processor/body_validation_in_context.rs +++ b/consensus/src/pipeline/body_processor/body_validation_in_context.rs @@ -1,7 +1,10 @@ use super::BlockBodyProcessor; use crate::{ errors::{BlockProcessResult, RuleError}, - model::stores::{ghostdag::GhostdagStoreReader, statuses::StatusesStoreReader}, + model::{ + services::reachability::ReachabilityService, + stores::{ghostdag::GhostdagStoreReader, pruning::PruningStoreReader, statuses::StatusesStoreReader}, + }, processes::window::WindowManager, }; use kaspa_consensus_core::block::Block; @@ -12,16 +15,27 @@ use std::sync::Arc; impl BlockBodyProcessor { pub fn validate_body_in_context(self: &Arc, block: &Block) -> BlockProcessResult<()> { + self.check_block_is_not_pruned(block)?; self.check_parent_bodies_exist(block)?; self.check_coinbase_blue_score_and_subsidy(block)?; - self.check_block_transactions_in_context(block)?; - self.check_block_is_not_pruned(block) + self.check_block_transactions_in_context(block) } fn check_block_is_not_pruned(self: &Arc, _block: &Block) -> BlockProcessResult<()> { - // TODO: In kaspad code it checks that the block is not in the past of the current tips. - // We should decide what's the best indication that a block was pruned. - Ok(()) + match self.statuses_store.read().get(_block.hash()).unwrap_option() { + Some(_) => { + let Some(pp) = self.pruning_point_store.read().pruning_point().unwrap_option() else { + return Ok(()); + }; + + if self.reachability_service.is_dag_ancestor_of(_block.hash(), pp) { + Err(RuleError::PrunedBlock) + } else { + Ok(()) + } + } + None => Ok(()), + } } fn check_block_transactions_in_context(self: &Arc, block: &Block) -> BlockProcessResult<()> { @@ -111,7 +125,7 @@ mod tests { let wait_handles = consensus.init(); let body_processor = consensus.block_body_processor(); - consensus.add_block_with_parents(1.into(), vec![config.genesis.hash]).await.unwrap(); + consensus.add_header_only_block_with_parents(1.into(), vec![config.genesis.hash]).await.unwrap(); { let block = consensus.build_block_with_parents_and_transactions(2.into(), vec![1.into()], vec![]); diff --git a/consensus/src/pipeline/body_processor/processor.rs b/consensus/src/pipeline/body_processor/processor.rs index 1ea674263..f575ddad5 100644 --- a/consensus/src/pipeline/body_processor/processor.rs +++ b/consensus/src/pipeline/body_processor/processor.rs @@ -7,6 +7,7 @@ use crate::{ block_transactions::DbBlockTransactionsStore, ghostdag::DbGhostdagStore, headers::DbHeadersStore, + pruning::DbPruningStore, reachability::DbReachabilityStore, statuses::{DbStatusesStore, StatusesStore, StatusesStoreBatchExtensions, StatusesStoreReader}, tips::{DbTipsStore, TipsStore}, @@ -59,6 +60,7 @@ pub struct BlockBodyProcessor { pub(super) headers_store: Arc, pub(super) block_transactions_store: Arc, pub(super) body_tips_store: Arc>, + pub(super) pruning_point_store: Arc>, // Managers and services pub(super) reachability_service: MTReachabilityService, @@ -96,6 +98,7 @@ impl BlockBodyProcessor { headers_store: Arc, block_transactions_store: Arc, body_tips_store: Arc>, + pruning_point_store: Arc>, reachability_service: MTReachabilityService, coinbase_manager: CoinbaseManager, @@ -120,6 +123,7 @@ impl BlockBodyProcessor { headers_store, block_transactions_store, body_tips_store, + pruning_point_store, coinbase_manager, mass_calculator, transaction_validator, diff --git a/testing/integration/src/consensus_integration_tests.rs b/testing/integration/src/consensus_integration_tests.rs index e66baaf69..136300031 100644 --- a/testing/integration/src/consensus_integration_tests.rs +++ b/testing/integration/src/consensus_integration_tests.rs @@ -62,6 +62,7 @@ use std::cmp::{max, Ordering}; use std::collections::HashSet; use std::path::Path; use std::sync::Arc; +use std::time::Duration; use std::{ collections::HashMap, fs::File, @@ -194,7 +195,9 @@ async fn consensus_sanity_test() { let wait_handles = consensus.init(); consensus - .validate_and_insert_block(consensus.build_block_with_parents(genesis_child, vec![MAINNET_PARAMS.genesis.hash]).to_immutable()) + .validate_and_insert_block( + consensus.build_header_only_block_with_parents(genesis_child, vec![MAINNET_PARAMS.genesis.hash]).to_immutable(), + ) .virtual_state_task .await .unwrap(); @@ -359,7 +362,7 @@ async fn block_window_test() { for test_block in test_blocks { info!("Processing block {}", test_block.id); let block_id = string_to_hash(test_block.id); - let block = consensus.build_block_with_parents( + let block = consensus.build_header_only_block_with_parents( block_id, strings_to_hashes(&test_block.parents.iter().map(|parent| String::from(*parent)).collect()), ); @@ -396,7 +399,7 @@ async fn header_in_isolation_validation_test() { let config = Config::new(MAINNET_PARAMS); let consensus = TestConsensus::new(&config); let wait_handles = consensus.init(); - let block = consensus.build_block_with_parents(1.into(), vec![config.genesis.hash]); + let block = consensus.build_header_only_block_with_parents(1.into(), vec![config.genesis.hash]); { let mut block = block.clone(); @@ -465,12 +468,12 @@ async fn incest_test() { let config = ConfigBuilder::new(MAINNET_PARAMS).skip_proof_of_work().build(); let consensus = TestConsensus::new(&config); let wait_handles = consensus.init(); - let block = consensus.build_block_with_parents(1.into(), vec![config.genesis.hash]); + let block = consensus.build_header_only_block_with_parents(1.into(), vec![config.genesis.hash]); let BlockValidationFutures { block_task, virtual_state_task } = consensus.validate_and_insert_block(block.to_immutable()); block_task.await.unwrap(); // Assert that block task completes as well virtual_state_task.await.unwrap(); - let mut block = consensus.build_block_with_parents(2.into(), vec![config.genesis.hash]); + let mut block = consensus.build_header_only_block_with_parents(2.into(), vec![config.genesis.hash]); block.header.parents_by_level[0] = vec![1.into(), config.genesis.hash]; let BlockValidationFutures { block_task, virtual_state_task } = consensus.validate_and_insert_block(block.to_immutable()); match virtual_state_task.await { @@ -494,7 +497,7 @@ async fn missing_parents_test() { let config = ConfigBuilder::new(MAINNET_PARAMS).skip_proof_of_work().build(); let consensus = TestConsensus::new(&config); let wait_handles = consensus.init(); - let mut block = consensus.build_block_with_parents(1.into(), vec![config.genesis.hash]); + let mut block = consensus.build_header_only_block_with_parents(1.into(), vec![config.genesis.hash]); block.header.parents_by_level[0] = vec![0.into()]; let BlockValidationFutures { block_task, virtual_state_task } = consensus.validate_and_insert_block(block.to_immutable()); match virtual_state_task.await { @@ -519,7 +522,7 @@ async fn known_invalid_test() { let config = ConfigBuilder::new(MAINNET_PARAMS).skip_proof_of_work().build(); let consensus = TestConsensus::new(&config); let wait_handles = consensus.init(); - let mut block = consensus.build_block_with_parents(1.into(), vec![config.genesis.hash]); + let mut block = consensus.build_header_only_block_with_parents(1.into(), vec![config.genesis.hash]); block.header.timestamp -= 1; match consensus.validate_and_insert_block(block.clone().to_immutable()).virtual_state_task.await { @@ -579,12 +582,12 @@ async fn median_time_test() { let timestamp_deviation_tolerance = test.config.timestamp_deviation_tolerance(0); for i in 1..(num_blocks + 1) { let parent = if i == 1 { test.config.genesis.hash } else { (i - 1).into() }; - let mut block = consensus.build_block_with_parents(i.into(), vec![parent]); + let mut block = consensus.build_header_only_block_with_parents(i.into(), vec![parent]); block.header.timestamp = test.config.genesis.timestamp + i; consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await.unwrap(); } - let mut block = consensus.build_block_with_parents((num_blocks + 2).into(), vec![num_blocks.into()]); + let mut block = consensus.build_header_only_block_with_parents((num_blocks + 2).into(), vec![num_blocks.into()]); // We set the timestamp to be less than the median time and expect the block to be rejected block.header.timestamp = test.config.genesis.timestamp + num_blocks - timestamp_deviation_tolerance - 1; match consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await { @@ -594,7 +597,7 @@ async fn median_time_test() { } } - let mut block = consensus.build_block_with_parents((num_blocks + 3).into(), vec![num_blocks.into()]); + let mut block = consensus.build_header_only_block_with_parents((num_blocks + 3).into(), vec![num_blocks.into()]); // We set the timestamp to be the exact median time and expect the block to be rejected block.header.timestamp = test.config.genesis.timestamp + num_blocks - timestamp_deviation_tolerance; match consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await { @@ -604,7 +607,7 @@ async fn median_time_test() { } } - let mut block = consensus.build_block_with_parents((num_blocks + 4).into(), vec![(num_blocks).into()]); + let mut block = consensus.build_header_only_block_with_parents((num_blocks + 4).into(), vec![(num_blocks).into()]); // We set the timestamp to be bigger than the median time and expect the block to be inserted successfully. block.header.timestamp = test.config.genesis.timestamp + timestamp_deviation_tolerance + 1; consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await.unwrap(); @@ -624,19 +627,19 @@ async fn mergeset_size_limit_test() { let mut tip1_hash = config.genesis.hash; for i in 1..(num_blocks_per_chain + 1) { - let block = consensus.build_block_with_parents(i.into(), vec![tip1_hash]); + let block = consensus.build_header_only_block_with_parents(i.into(), vec![tip1_hash]); tip1_hash = block.header.hash; consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await.unwrap(); } let mut tip2_hash = config.genesis.hash; for i in (num_blocks_per_chain + 2)..(2 * num_blocks_per_chain + 1) { - let block = consensus.build_block_with_parents(i.into(), vec![tip2_hash]); + let block = consensus.build_header_only_block_with_parents(i.into(), vec![tip2_hash]); tip2_hash = block.header.hash; consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await.unwrap(); } - let block = consensus.build_block_with_parents((3 * num_blocks_per_chain + 1).into(), vec![tip1_hash, tip2_hash]); + let block = consensus.build_header_only_block_with_parents((3 * num_blocks_per_chain + 1).into(), vec![tip1_hash, tip2_hash]); match consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await { Err(RuleError::MergeSetTooBig(a, b)) => { assert_eq!(a, config.mergeset_size_limit + 1); @@ -1267,7 +1270,7 @@ async fn bounded_merge_depth_test() { let mut selected_chain = vec![config.genesis.hash]; for i in 1..(config.merge_depth + 3) { let hash: Hash = (i + 1).into(); - consensus.add_block_with_parents(hash, vec![*selected_chain.last().unwrap()]).await.unwrap(); + consensus.add_header_only_block_with_parents(hash, vec![*selected_chain.last().unwrap()]).await.unwrap(); selected_chain.push(hash); } @@ -1275,19 +1278,22 @@ async fn bounded_merge_depth_test() { let mut block_chain_2 = vec![config.genesis.hash]; for i in 1..(config.merge_depth + 2) { let hash: Hash = (i + config.merge_depth + 3).into(); - consensus.add_block_with_parents(hash, vec![*block_chain_2.last().unwrap()]).await.unwrap(); + consensus.add_header_only_block_with_parents(hash, vec![*block_chain_2.last().unwrap()]).await.unwrap(); block_chain_2.push(hash); } // The merge depth root belongs to selected_chain, and block_chain_2[1] is red and doesn't have it in its past, and is not in the // past of any kosherizing block, so we expect the next block to be rejected. - match consensus.add_block_with_parents(100.into(), vec![block_chain_2[1], *selected_chain.last().unwrap()]).await { + match consensus.add_header_only_block_with_parents(100.into(), vec![block_chain_2[1], *selected_chain.last().unwrap()]).await { Err(RuleError::ViolatingBoundedMergeDepth) => {} res => panic!("Unexpected result: {res:?}"), } // A block that points to tip of both chains will be rejected for similar reasons (since block_chain_2 tip is also red). - match consensus.add_block_with_parents(101.into(), vec![*block_chain_2.last().unwrap(), *selected_chain.last().unwrap()]).await { + match consensus + .add_header_only_block_with_parents(101.into(), vec![*block_chain_2.last().unwrap(), *selected_chain.last().unwrap()]) + .await + { Err(RuleError::ViolatingBoundedMergeDepth) => {} res => panic!("Unexpected result: {res:?}"), } @@ -1295,7 +1301,7 @@ async fn bounded_merge_depth_test() { let kosherizing_hash: Hash = 102.into(); // This will pass since now genesis is the mutual merge depth root. consensus - .add_block_with_parents( + .add_header_only_block_with_parents( kosherizing_hash, vec![block_chain_2[block_chain_2.len() - 3], selected_chain[selected_chain.len() - 3]], ) @@ -1305,25 +1311,101 @@ async fn bounded_merge_depth_test() { let point_at_blue_kosherizing: Hash = 103.into(); // We expect it to pass because all of the reds are in the past of a blue kosherizing block. consensus - .add_block_with_parents(point_at_blue_kosherizing, vec![kosherizing_hash, *selected_chain.last().unwrap()]) + .add_header_only_block_with_parents(point_at_blue_kosherizing, vec![kosherizing_hash, *selected_chain.last().unwrap()]) .await .unwrap(); // We extend the selected chain until kosherizing_hash will be red from the virtual POV. for i in 0..config.ghostdag_k { let hash = Hash::from_u64_word((i + 1) as u64 * 1000); - consensus.add_block_with_parents(hash, vec![*selected_chain.last().unwrap()]).await.unwrap(); + consensus.add_header_only_block_with_parents(hash, vec![*selected_chain.last().unwrap()]).await.unwrap(); selected_chain.push(hash); } // Since kosherizing_hash is now red, we expect this to fail. - match consensus.add_block_with_parents(1200.into(), vec![kosherizing_hash, *selected_chain.last().unwrap()]).await { + match consensus.add_header_only_block_with_parents(1200.into(), vec![kosherizing_hash, *selected_chain.last().unwrap()]).await { Err(RuleError::ViolatingBoundedMergeDepth) => {} res => panic!("Unexpected result: {res:?}"), } // point_at_blue_kosherizing is kosherizing kosherizing_hash, so this should pass. - consensus.add_block_with_parents(1201.into(), vec![point_at_blue_kosherizing, *selected_chain.last().unwrap()]).await.unwrap(); + consensus + .add_header_only_block_with_parents(1201.into(), vec![point_at_blue_kosherizing, *selected_chain.last().unwrap()]) + .await + .unwrap(); + + consensus.shutdown(wait_handles); +} + +#[tokio::test] +async fn pruning_test() { + init_allocator_with_default_settings(); + let config = ConfigBuilder::new(MAINNET_PARAMS) + .skip_proof_of_work() + .edit_consensus_params(|p| { + p.finality_depth = 2; + p.mergeset_size_limit = 2; + p.ghostdag_k = 2; + p.merge_depth = 3; + p.pruning_depth = 100; + }) + .build(); + + assert!((config.ghostdag_k as u64) < config.merge_depth, "K must be smaller than merge depth for this test to run"); + + let consensus = TestConsensus::new(&config); + let wait_handles = consensus.init(); + + let mut selected_chain = vec![config.genesis.hash]; + + let genesis_child = 1.into(); + consensus.add_empty_utxo_valid_block_with_parents(genesis_child, vec![*selected_chain.last().unwrap()]).await.unwrap(); + selected_chain.push(genesis_child); + let genesis_child_block = consensus.get_block(genesis_child).unwrap(); + + let genesis_child_child = 2.into(); + consensus.add_empty_utxo_valid_block_with_parents(genesis_child_child, vec![*selected_chain.last().unwrap()]).await.unwrap(); + selected_chain.push(genesis_child_child); + let genesis_child_child_block = consensus.get_block(genesis_child_child).unwrap(); + + for i in 3..config.pruning_depth + config.finality_depth + 100 { + let hash: Hash = i.into(); + consensus.add_empty_utxo_valid_block_with_parents(hash, vec![*selected_chain.last().unwrap()]).await.unwrap(); + selected_chain.push(hash); + } + + // Waiting for genesis_child to get pruned + while consensus.get_block_status(genesis_child).unwrap() == BlockStatus::StatusUTXOValid { + tokio::time::sleep(Duration::from_millis(100)).await; + } + + assert!(consensus.validate_and_insert_block(genesis_child_block).virtual_state_task.await.is_err()); + assert!(consensus.validate_and_insert_block(genesis_child_child_block).virtual_state_task.await.is_err()); + + consensus.shutdown(wait_handles); +} + +#[tokio::test] +async fn indirect_parents_test() { + init_allocator_with_default_settings(); + let config = ConfigBuilder::new(DEVNET_PARAMS).skip_proof_of_work().build(); + let consensus = TestConsensus::new(&config); + let wait_handles = consensus.init(); + + let mut level_3_count = 3; + let mut selected_chain = vec![config.genesis.hash]; + for i in 1.. { + let hash: Hash = i.into(); + consensus.add_header_only_block_with_parents(hash, vec![*selected_chain.last().unwrap()]).await.unwrap(); + selected_chain.push(hash); + if consensus.get_header(*selected_chain.last().unwrap()).unwrap().parents_by_level.len() >= 3 { + level_3_count += 1; + } + + if level_3_count > 5 { + break; + } + } consensus.shutdown(wait_handles); } diff --git a/testing/integration/src/consensus_pipeline_tests.rs b/testing/integration/src/consensus_pipeline_tests.rs index 0b252c381..634d7565a 100644 --- a/testing/integration/src/consensus_pipeline_tests.rs +++ b/testing/integration/src/consensus_pipeline_tests.rs @@ -34,7 +34,7 @@ async fn test_concurrent_pipeline() { for (hash, parents) in blocks { // Submit to consensus twice to make sure duplicates are handled - let b: kaspa_consensus_core::block::Block = consensus.build_block_with_parents(hash, parents).to_immutable(); + let b: kaspa_consensus_core::block::Block = consensus.build_header_only_block_with_parents(hash, parents).to_immutable(); let results = join!( consensus.validate_and_insert_block(b.clone()).virtual_state_task, consensus.validate_and_insert_block(b).virtual_state_task