Skip to content

Commit 00fa3b4

Browse files
authored
Merge of #9260
2 parents 29ed501 + d71f7ec commit 00fa3b4

File tree

5 files changed

+239
-19
lines changed

5 files changed

+239
-19
lines changed

zebra-state/src/constants.rs

+6
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ pub const MAX_FIND_BLOCK_HEADERS_RESULTS: u32 = 160;
117117
/// These database versions can be recreated from their directly preceding versions.
118118
pub const RESTORABLE_DB_VERSIONS: [u64; 1] = [26];
119119

120+
/// The maximum number of invalidated block records.
121+
///
122+
/// This limits the memory use to around:
123+
/// `100 entries * up to 99 blocks * 2 MB per block = 20 GB`
124+
pub const MAX_INVALIDATED_BLOCKS: usize = 100;
125+
120126
lazy_static! {
121127
/// Regex that matches the RocksDB error when its lock file is already open.
122128
pub static ref LOCK_FILE_ERROR: Regex = Regex::new("(lock file).*(temporarily unavailable)|(in use)|(being used by another process)").expect("regex is valid");

zebra-state/src/error.rs

+21
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,32 @@ pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
4646
#[error("block is not contextually valid: {}", .0)]
4747
pub struct CommitSemanticallyVerifiedError(#[from] ValidateContextError);
4848

49+
/// An error describing the reason a block or its descendants could not be reconsidered after
50+
/// potentially being invalidated from the chain_set.
51+
#[derive(Debug, Error)]
52+
pub enum ReconsiderError {
53+
#[error("Block with hash {0} was not previously invalidated")]
54+
MissingInvalidatedBlock(block::Hash),
55+
56+
#[error("Parent chain not found for block {0}")]
57+
ParentChainNotFound(block::Hash),
58+
59+
#[error("Invalidated blocks list is empty when it should contain at least one block")]
60+
InvalidatedBlocksEmpty,
61+
62+
#[error("{0}")]
63+
ValidationError(#[from] ValidateContextError),
64+
}
65+
4966
/// An error describing why a block failed contextual validation.
5067
#[derive(Debug, Error, Clone, PartialEq, Eq)]
5168
#[non_exhaustive]
5269
#[allow(missing_docs)]
5370
pub enum ValidateContextError {
71+
#[error("block hash {block_hash} was previously invalidated")]
72+
#[non_exhaustive]
73+
BlockPreviouslyInvalidated { block_hash: block::Hash },
74+
5475
#[error("block parent not found in any chain, or not enough blocks in chain")]
5576
#[non_exhaustive]
5677
NotReadyToBeCommitted,

zebra-state/src/service/non_finalized_state.rs

+107-7
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ use std::{
88
sync::Arc,
99
};
1010

11+
use indexmap::IndexMap;
1112
use zebra_chain::{
12-
block::{self, Block, Hash},
13+
block::{self, Block, Hash, Height},
1314
parameters::Network,
14-
sprout, transparent,
15+
sprout::{self},
16+
transparent,
1517
};
1618

1719
use crate::{
18-
constants::MAX_NON_FINALIZED_CHAIN_FORKS,
20+
constants::{MAX_INVALIDATED_BLOCKS, MAX_NON_FINALIZED_CHAIN_FORKS},
21+
error::ReconsiderError,
1922
request::{ContextuallyVerifiedBlock, FinalizableBlock},
2023
service::{check, finalized_state::ZebraDb},
2124
SemanticallyVerifiedBlock, ValidateContextError,
@@ -47,7 +50,7 @@ pub struct NonFinalizedState {
4750

4851
/// Blocks that have been invalidated in, and removed from, the non finalized
4952
/// state.
50-
invalidated_blocks: HashMap<Hash, Arc<Vec<ContextuallyVerifiedBlock>>>,
53+
invalidated_blocks: IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>>,
5154

5255
// Configuration
5356
//
@@ -233,6 +236,10 @@ impl NonFinalizedState {
233236
self.insert(side_chain);
234237
}
235238

239+
// Remove all invalidated_blocks at or below the finalized height
240+
self.invalidated_blocks
241+
.retain(|height, _blocks| *height >= best_chain_root.height);
242+
236243
self.update_metrics_for_chains();
237244

238245
// Add the treestate to the finalized block.
@@ -294,13 +301,100 @@ impl NonFinalizedState {
294301
invalidated_blocks
295302
};
296303

297-
self.invalidated_blocks
298-
.insert(block_hash, Arc::new(invalidated_blocks));
304+
self.invalidated_blocks.insert(
305+
invalidated_blocks.first().unwrap().clone().height,
306+
Arc::new(invalidated_blocks),
307+
);
308+
309+
while self.invalidated_blocks.len() > MAX_INVALIDATED_BLOCKS {
310+
self.invalidated_blocks.shift_remove_index(0);
311+
}
299312

300313
self.update_metrics_for_chains();
301314
self.update_metrics_bars();
302315
}
303316

317+
/// Reconsiders a previously invalidated block and its descendants into the non-finalized state
318+
/// based on a block_hash. Reconsidered blocks are inserted into the previous chain and re-inserted
319+
/// into the chain_set.
320+
pub fn reconsider_block(
321+
&mut self,
322+
block_hash: block::Hash,
323+
finalized_state: &ZebraDb,
324+
) -> Result<(), ReconsiderError> {
325+
// Get the invalidated blocks that were invalidated by the given block_hash
326+
let height = self
327+
.invalidated_blocks
328+
.iter()
329+
.find_map(|(height, blocks)| {
330+
if blocks.first()?.hash == block_hash {
331+
Some(height)
332+
} else {
333+
None
334+
}
335+
})
336+
.ok_or(ReconsiderError::MissingInvalidatedBlock(block_hash))?;
337+
338+
let mut invalidated_blocks = self
339+
.invalidated_blocks
340+
.clone()
341+
.shift_remove(height)
342+
.ok_or(ReconsiderError::MissingInvalidatedBlock(block_hash))?;
343+
let mut_blocks = Arc::make_mut(&mut invalidated_blocks);
344+
345+
// Find and fork the parent chain of the invalidated_root. Update the parent chain
346+
// with the invalidated_descendants
347+
let invalidated_root = mut_blocks
348+
.first()
349+
.ok_or(ReconsiderError::InvalidatedBlocksEmpty)?;
350+
351+
let root_parent_hash = invalidated_root.block.header.previous_block_hash;
352+
353+
// If the parent is the tip of the finalized_state we create a new chain and insert it
354+
// into the non finalized state
355+
let chain_result = if root_parent_hash == finalized_state.finalized_tip_hash() {
356+
let chain = Chain::new(
357+
&self.network,
358+
finalized_state
359+
.finalized_tip_height()
360+
.ok_or(ReconsiderError::ParentChainNotFound(block_hash))?,
361+
finalized_state.sprout_tree_for_tip(),
362+
finalized_state.sapling_tree_for_tip(),
363+
finalized_state.orchard_tree_for_tip(),
364+
finalized_state.history_tree(),
365+
finalized_state.finalized_value_pool(),
366+
);
367+
Arc::new(chain)
368+
} else {
369+
// The parent is not the finalized_tip and still exist in the NonFinalizedState
370+
// or else we return an error due to the parent not existing in the NonFinalizedState
371+
self.parent_chain(root_parent_hash)
372+
.map_err(|_| ReconsiderError::ParentChainNotFound(block_hash))?
373+
};
374+
375+
let mut modified_chain = Arc::unwrap_or_clone(chain_result);
376+
for block in Arc::unwrap_or_clone(invalidated_blocks) {
377+
modified_chain = modified_chain.push(block)?;
378+
}
379+
380+
let (height, hash) = modified_chain.non_finalized_tip();
381+
382+
// Only track invalidated_blocks that are not yet finalized. Once blocks are finalized (below the best_chain_root_height)
383+
// we can discard the block.
384+
if let Some(best_chain_root_height) = finalized_state.finalized_tip_height() {
385+
self.invalidated_blocks
386+
.retain(|height, _blocks| *height >= best_chain_root_height);
387+
}
388+
389+
self.insert_with(Arc::new(modified_chain), |chain_set| {
390+
chain_set.retain(|chain| chain.non_finalized_tip_hash() != root_parent_hash)
391+
});
392+
393+
self.update_metrics_for_committed_block(height, hash);
394+
395+
Ok(())
396+
}
397+
304398
/// Commit block to the non-finalized state as a new chain where its parent
305399
/// is the finalized tip.
306400
#[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))]
@@ -352,6 +446,12 @@ impl NonFinalizedState {
352446
prepared: SemanticallyVerifiedBlock,
353447
finalized_state: &ZebraDb,
354448
) -> Result<Arc<Chain>, ValidateContextError> {
449+
if self.invalidated_blocks.contains_key(&prepared.height) {
450+
return Err(ValidateContextError::BlockPreviouslyInvalidated {
451+
block_hash: prepared.hash,
452+
});
453+
}
454+
355455
// Reads from disk
356456
//
357457
// TODO: if these disk reads show up in profiles, run them in parallel, using std::thread::spawn()
@@ -624,7 +724,7 @@ impl NonFinalizedState {
624724
}
625725

626726
/// Return the invalidated blocks.
627-
pub fn invalidated_blocks(&self) -> HashMap<block::Hash, Arc<Vec<ContextuallyVerifiedBlock>>> {
727+
pub fn invalidated_blocks(&self) -> IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>> {
628728
self.invalidated_blocks.clone()
629729
}
630730

zebra-state/src/service/non_finalized_state/chain.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -359,15 +359,15 @@ impl Chain {
359359
(block, treestate)
360360
}
361361

362-
// Returns the block at the provided height and all of its descendant blocks.
362+
/// Returns the block at the provided height and all of its descendant blocks.
363363
pub fn child_blocks(&self, block_height: &block::Height) -> Vec<ContextuallyVerifiedBlock> {
364364
self.blocks
365365
.range(block_height..)
366366
.map(|(_h, b)| b.clone())
367367
.collect()
368368
}
369369

370-
// Returns a new chain without the invalidated block or its descendants.
370+
/// Returns a new chain without the invalidated block or its descendants.
371371
pub fn invalidate_block(
372372
&self,
373373
block_hash: block::Hash,

zebra-state/src/service/non_finalized_state/tests/vectors.rs

+103-10
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,17 @@ fn finalize_pops_from_best_chain_for_network(network: Network) -> Result<()> {
216216
Ok(())
217217
}
218218

219+
#[test]
220+
fn invalidate_block_removes_block_and_descendants_from_chain() -> Result<()> {
221+
let _init_guard = zebra_test::init();
222+
223+
for network in Network::iter() {
224+
invalidate_block_removes_block_and_descendants_from_chain_for_network(network)?;
225+
}
226+
227+
Ok(())
228+
}
229+
219230
fn invalidate_block_removes_block_and_descendants_from_chain_for_network(
220231
network: Network,
221232
) -> Result<()> {
@@ -267,43 +278,125 @@ fn invalidate_block_removes_block_and_descendants_from_chain_for_network(
267278
);
268279

269280
let invalidated_blocks_state = &state.invalidated_blocks;
270-
assert!(
271-
invalidated_blocks_state.contains_key(&block2.hash()),
272-
"invalidated blocks map should reference the hash of block2"
273-
);
274281

275-
let invalidated_blocks_state_descendants =
276-
invalidated_blocks_state.get(&block2.hash()).unwrap();
282+
// Find an entry in the IndexMap that contains block2 hash
283+
let (_, invalidated_blocks_state_descendants) = invalidated_blocks_state
284+
.iter()
285+
.find_map(|(height, blocks)| {
286+
assert!(
287+
blocks.iter().any(|block| block.hash == block2.hash()),
288+
"invalidated_blocks should reference the hash of block2"
289+
);
290+
291+
if blocks.iter().any(|block| block.hash == block2.hash()) {
292+
Some((height, blocks))
293+
} else {
294+
None
295+
}
296+
})
297+
.unwrap();
277298

278299
match network {
279300
Network::Mainnet => assert!(
280301
invalidated_blocks_state_descendants
281302
.iter()
282303
.any(|block| block.height == block::Height(653601)),
283-
"invalidated descendants vec should contain block3"
304+
"invalidated descendants should contain block3"
284305
),
285306
Network::Testnet(_parameters) => assert!(
286307
invalidated_blocks_state_descendants
287308
.iter()
288309
.any(|block| block.height == block::Height(584001)),
289-
"invalidated descendants vec should contain block3"
310+
"invalidated descendants should contain block3"
290311
),
291312
}
292313

293314
Ok(())
294315
}
295316

296317
#[test]
297-
fn invalidate_block_removes_block_and_descendants_from_chain() -> Result<()> {
318+
fn reconsider_block_and_reconsider_chain_correctly_reconsiders_blocks_and_descendants() -> Result<()>
319+
{
298320
let _init_guard = zebra_test::init();
299321

300322
for network in Network::iter() {
301-
invalidate_block_removes_block_and_descendants_from_chain_for_network(network)?;
323+
reconsider_block_inserts_block_and_descendants_into_chain_for_network(network.clone())?;
302324
}
303325

304326
Ok(())
305327
}
306328

329+
fn reconsider_block_inserts_block_and_descendants_into_chain_for_network(
330+
network: Network,
331+
) -> Result<()> {
332+
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
333+
let block2 = block1.make_fake_child().set_work(10);
334+
let block3 = block2.make_fake_child().set_work(1);
335+
336+
let mut state = NonFinalizedState::new(&network);
337+
let finalized_state = FinalizedState::new(
338+
&Config::ephemeral(),
339+
&network,
340+
#[cfg(feature = "elasticsearch")]
341+
false,
342+
);
343+
344+
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
345+
finalized_state.set_finalized_value_pool(fake_value_pool);
346+
347+
state.commit_new_chain(block1.clone().prepare(), &finalized_state)?;
348+
state.commit_block(block2.clone().prepare(), &finalized_state)?;
349+
state.commit_block(block3.clone().prepare(), &finalized_state)?;
350+
351+
assert_eq!(
352+
state
353+
.best_chain()
354+
.unwrap_or(&Arc::new(Chain::default()))
355+
.blocks
356+
.len(),
357+
3
358+
);
359+
360+
// Invalidate block2 to update the invalidated_blocks NonFinalizedState
361+
state.invalidate_block(block2.hash());
362+
363+
// Perform checks to ensure the invalidated_block and descendants were added to the invalidated_block
364+
// state
365+
let post_invalidated_chain = state.best_chain().unwrap();
366+
367+
assert_eq!(post_invalidated_chain.blocks.len(), 1);
368+
assert!(
369+
post_invalidated_chain.contains_block_hash(block1.hash()),
370+
"the new modified chain should contain block1"
371+
);
372+
373+
assert!(
374+
!post_invalidated_chain.contains_block_hash(block2.hash()),
375+
"the new modified chain should not contain block2"
376+
);
377+
assert!(
378+
!post_invalidated_chain.contains_block_hash(block3.hash()),
379+
"the new modified chain should not contain block3"
380+
);
381+
382+
// Reconsider block2 and check that both block2 and block3 were `reconsidered` into the
383+
// best chain
384+
state.reconsider_block(block2.hash(), &finalized_state.db)?;
385+
386+
let best_chain = state.best_chain().unwrap();
387+
388+
assert!(
389+
best_chain.contains_block_hash(block2.hash()),
390+
"the best chain should again contain block2"
391+
);
392+
assert!(
393+
best_chain.contains_block_hash(block3.hash()),
394+
"the best chain should again contain block3"
395+
);
396+
397+
Ok(())
398+
}
399+
307400
#[test]
308401
// This test gives full coverage for `take_chain_if`
309402
fn commit_block_extending_best_chain_doesnt_drop_worst_chains() -> Result<()> {

0 commit comments

Comments
 (0)