lighthouse/beacon_node/beacon_chain/src/pre_finalization_cache.rs
Michael Sproul 99d2c33387 Avoid looking up pre-finalization blocks (#2909)
## Issue Addressed

This PR fixes the unnecessary `WARN Single block lookup failed` messages described here:

https://github.com/sigp/lighthouse/pull/2866#issuecomment-1008442640

## Proposed Changes

Add a new cache to the `BeaconChain` that tracks the block roots of blocks from before finalization. These could be blocks from the canonical chain (which might need to be read from disk), or old pre-finalization blocks that have been forked out.

The cache also stores a set of block roots for in-progress single block lookups, which duplicates some of the information from sync's `single_block_lookups` hashmap:

a836e180f9/beacon_node/network/src/sync/manager.rs (L192-L196)

On a live node you can confirm that the cache is working by grepping logs for the message: `Rejected attestation to finalized block`.
2022-01-27 22:58:32 +00:00

120 lines
4.7 KiB
Rust

use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
use itertools::process_results;
use lru::LruCache;
use parking_lot::Mutex;
use slog::debug;
use std::time::Duration;
use types::Hash256;
const BLOCK_ROOT_CACHE_LIMIT: usize = 512;
const LOOKUP_LIMIT: usize = 8;
const METRICS_TIMEOUT: Duration = Duration::from_millis(100);
/// Cache for rejecting attestations to blocks from before finalization.
///
/// It stores a collection of block roots that are pre-finalization and therefore not known to fork
/// choice in `verify_head_block_is_known` during attestation processing.
#[derive(Default)]
pub struct PreFinalizationBlockCache {
cache: Mutex<Cache>,
}
struct Cache {
/// Set of block roots that are known to be pre-finalization.
block_roots: LruCache<Hash256, ()>,
/// Set of block roots that are the subject of single block lookups.
in_progress_lookups: LruCache<Hash256, ()>,
}
impl Default for Cache {
fn default() -> Self {
Cache {
block_roots: LruCache::new(BLOCK_ROOT_CACHE_LIMIT),
in_progress_lookups: LruCache::new(LOOKUP_LIMIT),
}
}
}
impl<T: BeaconChainTypes> BeaconChain<T> {
/// Check whether the block with `block_root` is known to be pre-finalization.
///
/// The provided `block_root` is assumed to be unknown to fork choice. I.e., it
/// is not known to be a descendant of the finalized block.
///
/// Return `true` if the attestation to this block should be rejected outright,
/// return `false` if more information is needed from a single-block-lookup.
pub fn is_pre_finalization_block(&self, block_root: Hash256) -> Result<bool, BeaconChainError> {
let mut cache = self.pre_finalization_block_cache.cache.lock();
// Check the cache to see if we already know this pre-finalization block root.
if cache.block_roots.contains(&block_root) {
return Ok(true);
}
// Avoid repeating the disk lookup for blocks that are already subject to a network lookup.
// Sync will take care of de-duplicating the single block lookups.
if cache.in_progress_lookups.contains(&block_root) {
return Ok(false);
}
// 1. Check memory for a recent pre-finalization block.
let is_recent_finalized_block = self.with_head(|head| {
process_results(
head.beacon_state.rev_iter_block_roots(&self.spec),
|mut iter| iter.any(|(_, root)| root == block_root),
)
.map_err(BeaconChainError::BeaconStateError)
})?;
if is_recent_finalized_block {
cache.block_roots.put(block_root, ());
return Ok(true);
}
// 2. Check on disk.
if self.store.get_block(&block_root)?.is_some() {
cache.block_roots.put(block_root, ());
return Ok(true);
}
// 3. Check the network with a single block lookup.
cache.in_progress_lookups.put(block_root, ());
if cache.in_progress_lookups.len() == LOOKUP_LIMIT {
// NOTE: we expect this to occur sometimes if a lot of blocks that we look up fail to be
// imported for reasons other than being pre-finalization. The cache will eventually
// self-repair in this case by replacing old entries with new ones until all the failed
// blocks have been flushed out. Solving this issue isn't as simple as hooking the
// beacon processor's functions that handle failed blocks because we need the block root
// and it has been erased from the `BlockError` by that point.
debug!(
self.log,
"Pre-finalization lookup cache is full";
);
}
Ok(false)
}
pub fn pre_finalization_block_rejected(&self, block_root: Hash256) {
// Future requests can know that this block is invalid without having to look it up again.
let mut cache = self.pre_finalization_block_cache.cache.lock();
cache.in_progress_lookups.pop(&block_root);
cache.block_roots.put(block_root, ());
}
}
impl PreFinalizationBlockCache {
pub fn block_processed(&self, block_root: Hash256) {
// Future requests will find this block in fork choice, so no need to cache it in the
// ongoing lookup cache any longer.
self.cache.lock().in_progress_lookups.pop(&block_root);
}
pub fn contains(&self, block_root: Hash256) -> bool {
self.cache.lock().block_roots.contains(&block_root)
}
pub fn metrics(&self) -> Option<(usize, usize)> {
let cache = self.cache.try_lock_for(METRICS_TIMEOUT)?;
Some((cache.block_roots.len(), cache.in_progress_lookups.len()))
}
}