diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index 632591f4cb3..e26cf8dafbe 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -46,6 +46,18 @@ pub type BoxError = Box; #[error("block is not contextually valid: {}", .0)] pub struct CommitSemanticallyVerifiedError(#[from] ValidateContextError); +/// An error describing the reason a block or its descendants could not be reconsidered after +/// potentially being invalidated from the chain_set. +#[derive(Debug, Error)] +pub enum ReconsiderError { + #[error("Block with hash {0} was not previously invalidated")] + NonPreviouslyInvalidatedBlock(block::Hash), + #[error("Parent chain not found for block {0}")] + ParentChainNotFound(block::Hash), + #[error("{0}")] + ValidationError(#[from] ValidateContextError), +} + /// An error describing why a block failed contextual validation. #[derive(Debug, Error, Clone, PartialEq, Eq)] #[non_exhaustive] diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 91ae30ae23d..453360fbde1 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -11,11 +11,13 @@ use std::{ use zebra_chain::{ block::{self, Block, Hash}, parameters::Network, - sprout, transparent, + sprout::{self}, + transparent, }; use crate::{ constants::MAX_NON_FINALIZED_CHAIN_FORKS, + error::ReconsiderError, request::{ContextuallyVerifiedBlock, FinalizableBlock}, service::{check, finalized_state::ZebraDb}, SemanticallyVerifiedBlock, ValidateContextError, @@ -301,6 +303,57 @@ impl NonFinalizedState { self.update_metrics_bars(); } + /// Reconsiders a previously invalidated block and its descendants into the non-finalized state + /// based on a block_hash. Reconsidered blocks are inserted into the previous chain and re-inserted + /// into the chain_set. + pub fn reconsider_block(&mut self, block_hash: block::Hash) -> Result<(), ReconsiderError> { + // Get the invalidated blocks that were invalidated by the given block_hash + let Some(invalidated_blocks) = self.invalidated_blocks.get(&block_hash) else { + return Err(ReconsiderError::NonPreviouslyInvalidatedBlock(block_hash)); + }; + + let mut blocks = invalidated_blocks.clone(); + let mut_blocks = Arc::make_mut(&mut blocks); + + let Some(invalidated_root) = mut_blocks.first() else { + return Err(ReconsiderError::NonPreviouslyInvalidatedBlock(block_hash)); + }; + + // Find and fork the parent chain of the invalidated_root. Update the parent chain + // with the invalidated_descendants + let prev_block_hash = invalidated_root.block.header.previous_block_hash; + let Ok(parent_chain) = self.parent_chain(prev_block_hash) else { + return Err(ReconsiderError::ParentChainNotFound(block_hash)); + }; + + let Some(new_chain) = parent_chain.fork(parent_chain.non_finalized_tip_hash()) else { + return Err(ReconsiderError::ValidationError( + ValidateContextError::NonSequentialBlock { + candidate_height: invalidated_root.height, + parent_height: parent_chain.non_finalized_tip_height(), + }, + )); + }; + + let mut chain = new_chain + .push(mut_blocks.remove(0)) + .map_err(ReconsiderError::ValidationError)?; + for block in mut_blocks { + chain = chain + .push(block.clone()) + .map_err(ReconsiderError::ValidationError)?; + } + + self.insert_with(Arc::new(chain), |chain_set| { + chain_set.retain(|c| c != &parent_chain) + }); + + self.update_metrics_for_chains(); + self.update_metrics_bars(); + + Ok(()) + } + /// Commit block to the non-finalized state as a new chain where its parent /// is the finalized tip. #[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))] diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index c7d0d2877c6..e017a742c0d 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -359,7 +359,7 @@ impl Chain { (block, treestate) } - // Returns the block at the provided height and all of its descendant blocks. + /// Returns the block at the provided height and all of its descendant blocks. pub fn child_blocks(&self, block_height: &block::Height) -> Vec { self.blocks .range(block_height..) @@ -367,7 +367,7 @@ impl Chain { .collect() } - // Returns a new chain without the invalidated block or its descendants. + /// Returns a new chain without the invalidated block or its descendants. pub fn invalidate_block( &self, block_hash: block::Hash, diff --git a/zebra-state/src/service/non_finalized_state/tests/vectors.rs b/zebra-state/src/service/non_finalized_state/tests/vectors.rs index 5b392e4a0b9..a1303b09ebc 100644 --- a/zebra-state/src/service/non_finalized_state/tests/vectors.rs +++ b/zebra-state/src/service/non_finalized_state/tests/vectors.rs @@ -216,6 +216,17 @@ fn finalize_pops_from_best_chain_for_network(network: Network) -> Result<()> { Ok(()) } +#[test] +fn invalidate_block_removes_block_and_descendants_from_chain() -> Result<()> { + let _init_guard = zebra_test::init(); + + for network in Network::iter() { + invalidate_block_removes_block_and_descendants_from_chain_for_network(network)?; + } + + Ok(()) +} + fn invalidate_block_removes_block_and_descendants_from_chain_for_network( network: Network, ) -> Result<()> { @@ -294,16 +305,88 @@ fn invalidate_block_removes_block_and_descendants_from_chain_for_network( } #[test] -fn invalidate_block_removes_block_and_descendants_from_chain() -> Result<()> { +fn reconsider_block_and_reconsider_chain_correctly_reconsiders_blocks_and_descendants() -> Result<()> +{ let _init_guard = zebra_test::init(); for network in Network::iter() { - invalidate_block_removes_block_and_descendants_from_chain_for_network(network)?; + reconsider_block_inserts_block_and_descendants_into_chain_for_network(network.clone())?; } Ok(()) } +fn reconsider_block_inserts_block_and_descendants_into_chain_for_network( + network: Network, +) -> Result<()> { + let block1: Arc = Arc::new(network.test_block(653599, 583999).unwrap()); + let block2 = block1.make_fake_child().set_work(10); + let block3 = block2.make_fake_child().set_work(1); + + let mut state = NonFinalizedState::new(&network); + let finalized_state = FinalizedState::new( + &Config::ephemeral(), + &network, + #[cfg(feature = "elasticsearch")] + false, + ); + + let fake_value_pool = ValueBalance::::fake_populated_pool(); + finalized_state.set_finalized_value_pool(fake_value_pool); + + state.commit_new_chain(block1.clone().prepare(), &finalized_state)?; + state.commit_block(block2.clone().prepare(), &finalized_state)?; + state.commit_block(block3.clone().prepare(), &finalized_state)?; + + assert_eq!( + state + .best_chain() + .unwrap_or(&Arc::new(Chain::default())) + .blocks + .len(), + 3 + ); + + // Invalidate block2 to update the invalidated_blocks NonFinalizedState + state.invalidate_block(block2.hash()); + + // Perform checks to ensure the invalidated_block and descendants were added to the invalidated_block + // state + let post_invalidated_chain = state.best_chain().unwrap(); + + assert_eq!(post_invalidated_chain.blocks.len(), 1); + assert!( + post_invalidated_chain.contains_block_hash(block1.hash()), + "the new modified chain should contain block1" + ); + + assert!( + !post_invalidated_chain.contains_block_hash(block2.hash()), + "the new modified chain should not contain block2" + ); + assert!( + !post_invalidated_chain.contains_block_hash(block3.hash()), + "the new modified chain should not contain block3" + ); + + // Reconsider block2 and check that both block2 and block3 were `reconsidered` into the + // best chain + state.reconsider_block(block2.hash())?; + + let best_chain = state.best_chain().unwrap(); + + assert!( + best_chain.contains_block_hash(block2.hash()), + "the best chain should again contain block2" + ); + assert!( + best_chain.contains_block_hash(block3.hash()), + "the best chain should again contain block3" + ); + + Ok(()) +} + #[test] // This test gives full coverage for `take_chain_if` fn commit_block_extending_best_chain_doesnt_drop_worst_chains() -> Result<()> {