Skip to content

Commit

Permalink
Fix bad assumption when checking finalized descendant (#1629)
Browse files Browse the repository at this point in the history
## Issue Addressed

- Resolves #1616

## Proposed Changes

Fixes a bug where we are unable to read the finalized block from fork choice.

## Detail

I had made an assumption that the finalized block always has a parent root of `None`:

https://github.com/sigp/lighthouse/blob/e5fc6bab485fa54d7e518b325f4eb9201ba5c6a1/consensus/fork_choice/src/fork_choice.rs#L749-L752

This was a faulty assumption, we don't set parent *roots* to `None`. Instead we *sometimes* set parent *indices* to `None`, depending if this pruning condition is satisfied: 

https://github.com/sigp/lighthouse/blob/e5fc6bab485fa54d7e518b325f4eb9201ba5c6a1/consensus/proto_array/src/proto_array.rs#L229-L232 

The bug manifested itself like this:

1. We attempt to get the finalized block from fork choice
1. We try to check that the block is descendant of the finalized block (note: they're the same block).
1. We expect the parent root to be `None`, but it's actually the parent root of the finalized root.
1. We therefore end up checking if the parent of the finalized root is a descendant of itself. (note: it's an *ancestor* not a *descendant*).
1. We therefore declare that the finalized block is not a descendant of (or eq to) the finalized block. Bad.

## Additional Info

In reflection, I made a poor assumption in the quest to obtain a probably negligible performance gain. The performance gain wasn't worth the risk and we got burnt.
  • Loading branch information
paulhauner committed Sep 18, 2020
1 parent 49ab414 commit 18508e2
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 12 deletions.
15 changes: 9 additions & 6 deletions beacon_node/beacon_chain/src/block_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -980,14 +980,17 @@ fn load_parent<T: BeaconChainTypes>(
// exist in fork choice but not in the database yet. In such a case we simply
// indicate that we don't yet know the parent.
let root = block.parent_root();
let parent_block = if let Some(block) = chain
let parent_block = chain
.get_block(&block.parent_root())
.map_err(BlockError::BeaconChainError)?
{
block
} else {
return Err(BlockError::ParentUnknown(Box::new(block)));
};
.ok_or_else(|| {
// Return a `MissingBeaconBlock` error instead of a `ParentUnknown` error since
// we've already checked fork choice for this block.
//
// It's an internal error if the block exists in fork choice but not in the
// database.
BlockError::from(BeaconChainError::MissingBeaconBlock(block.parent_root()))
})?;

// Load the parent blocks state from the database, returning an error if it is not found.
// It is an error because if we know the parent block we should also know the parent state.
Expand Down
11 changes: 5 additions & 6 deletions consensus/fork_choice/src/fork_choice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,12 +745,11 @@ where

/// Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
pub fn get_block(&self, block_root: &Hash256) -> Option<ProtoBlock> {
self.proto_array.get_block(block_root).filter(|block| {
// If available, use the parent_root to perform the lookup since it will involve one
// less lookup. This involves making the assumption that the finalized block will
// always have `block.parent_root` of `None`.
self.is_descendant_of_finalized(block.parent_root.unwrap_or(block.root))
})
if self.is_descendant_of_finalized(*block_root) {
self.proto_array.get_block(block_root)
} else {
None
}
}

/// Return `true` if `block_root` is equal to the finalized root, or a known descendant of it.
Expand Down
28 changes: 28 additions & 0 deletions consensus/fork_choice/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,26 @@ impl ForkChoiceTest {

self
}

/// Check to ensure that we can read the finalized block. This is a regression test.
pub fn check_finalized_block_is_accessible(self) -> Self {
self.harness
.chain
.fork_choice
.write()
.get_block(
&self
.harness
.chain
.head_info()
.unwrap()
.finalized_checkpoint
.root,
)
.unwrap();

self
}
}

fn is_safe_to_update(slot: Slot) -> bool {
Expand Down Expand Up @@ -879,3 +899,11 @@ fn valid_attestation_skip_across_epoch() {
|result| result.unwrap(),
);
}

#[test]
fn can_read_finalized_block() {
ForkChoiceTest::new()
.apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0)
.apply_blocks(1)
.check_finalized_block_is_accessible();
}

0 comments on commit 18508e2

Please sign in to comment.