mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-20 05:14:35 +00:00
Add early attester cache (#2872)
## Issue Addressed NA ## Proposed Changes Introduces a cache to attestation to produce atop blocks which will become the head, but are not fully imported (e.g., not inserted into the database). Whilst attesting to a block before it's imported is rather easy, if we're going to produce that attestation then we also need to be able to: 1. Verify that attestation. 1. Respond to RPC requests for the `beacon_block_root`. Attestation verification (1) is *partially* covered. Since we prime the shuffling cache before we insert the block into the early attester cache, we should be fine for all typical use-cases. However, it is possible that the cache is washed out before we've managed to insert the state into the database and then attestation verification will fail with a "missing beacon state"-type error. Providing the block via RPC (2) is also partially covered, since we'll check the database *and* the early attester cache when responding a blocks-by-root request. However, we'll still omit the block from blocks-by-range requests (until the block lands in the DB). I *think* this is fine, since there's no guarantee that we return all blocks for those responses. Another important consideration is whether or not the *parent* of the early attester block is available in the databse. If it were not, we might fail to respond to blocks-by-root request that are iterating backwards to collect a chain of blocks. I argue that *we will always have the parent of the early attester block in the database.* This is because we are holding the fork-choice write-lock when inserting the block into the early attester cache and we do not drop that until the block is in the database.
This commit is contained in:
@@ -12,6 +12,7 @@ use crate::block_verification::{
|
||||
IntoFullyVerifiedBlock,
|
||||
};
|
||||
use crate::chain_config::ChainConfig;
|
||||
use crate::early_attester_cache::EarlyAttesterCache;
|
||||
use crate::errors::{BeaconChainError as Error, BlockProductionError};
|
||||
use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend};
|
||||
use crate::events::ServerSentEventHandler;
|
||||
@@ -107,6 +108,9 @@ pub const OP_POOL_DB_KEY: Hash256 = Hash256::zero();
|
||||
pub const ETH1_CACHE_DB_KEY: Hash256 = Hash256::zero();
|
||||
pub const FORK_CHOICE_DB_KEY: Hash256 = Hash256::zero();
|
||||
|
||||
/// Defines how old a block can be before it's no longer a candidate for the early attester cache.
|
||||
const EARLY_ATTESTER_CACHE_HISTORIC_SLOTS: u64 = 4;
|
||||
|
||||
/// Defines the behaviour when a block/block-root for a skipped slot is requested.
|
||||
pub enum WhenSlotSkipped {
|
||||
/// If the slot is a skip slot, return `None`.
|
||||
@@ -328,6 +332,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
pub(crate) validator_pubkey_cache: TimeoutRwLock<ValidatorPubkeyCache<T>>,
|
||||
/// A cache used when producing attestations.
|
||||
pub(crate) attester_cache: Arc<AttesterCache>,
|
||||
/// A cache used when producing attestations whilst the head block is still being imported.
|
||||
pub early_attester_cache: EarlyAttesterCache<T::EthSpec>,
|
||||
/// A cache used to keep track of various block timings.
|
||||
pub block_times_cache: Arc<RwLock<BlockTimesCache>>,
|
||||
/// A list of any hard-coded forks that have been disabled.
|
||||
@@ -926,6 +932,28 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
)?
|
||||
}
|
||||
|
||||
/// Returns the block at the given root, if any.
|
||||
///
|
||||
/// Will also check the early attester cache for the block. Because of this, there's no
|
||||
/// guarantee that a block returned from this function has a `BeaconState` available in
|
||||
/// `self.store`. The expected use for this function is *only* for returning blocks requested
|
||||
/// from P2P peers.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// May return a database error.
|
||||
pub fn get_block_checking_early_attester_cache(
|
||||
&self,
|
||||
block_root: &Hash256,
|
||||
) -> Result<Option<SignedBeaconBlock<T::EthSpec>>, Error> {
|
||||
let block_opt = self
|
||||
.store
|
||||
.get_block(block_root)?
|
||||
.or_else(|| self.early_attester_cache.get_block(*block_root));
|
||||
|
||||
Ok(block_opt)
|
||||
}
|
||||
|
||||
/// Returns the block at the given root, if any.
|
||||
///
|
||||
/// ## Errors
|
||||
@@ -1422,6 +1450,29 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
) -> Result<Attestation<T::EthSpec>, Error> {
|
||||
let _total_timer = metrics::start_timer(&metrics::ATTESTATION_PRODUCTION_SECONDS);
|
||||
|
||||
// The early attester cache will return `Some(attestation)` in the scenario where there is a
|
||||
// block being imported that will become the head block, but that block has not yet been
|
||||
// inserted into the database and set as `self.canonical_head`.
|
||||
//
|
||||
// In effect, the early attester cache prevents slow database IO from causing missed
|
||||
// head/target votes.
|
||||
match self
|
||||
.early_attester_cache
|
||||
.try_attest(request_slot, request_index, &self.spec)
|
||||
{
|
||||
// The cache matched this request, return the value.
|
||||
Ok(Some(attestation)) => return Ok(attestation),
|
||||
// The cache did not match this request, proceed with the rest of this function.
|
||||
Ok(None) => (),
|
||||
// The cache returned an error. Log the error and proceed with the rest of this
|
||||
// function.
|
||||
Err(e) => warn!(
|
||||
self.log,
|
||||
"Early attester cache failed";
|
||||
"error" => ?e
|
||||
),
|
||||
}
|
||||
|
||||
let slots_per_epoch = T::EthSpec::slots_per_epoch();
|
||||
let request_epoch = request_slot.epoch(slots_per_epoch);
|
||||
|
||||
@@ -2602,6 +2653,42 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// If the block is recent enough, check to see if it becomes the head block. If so, apply it
|
||||
// to the early attester cache. This will allow attestations to the block without waiting
|
||||
// for the block and state to be inserted to the database.
|
||||
//
|
||||
// Only performing this check on recent blocks avoids slowing down sync with lots of calls
|
||||
// to fork choice `get_head`.
|
||||
if block.slot() + EARLY_ATTESTER_CACHE_HISTORIC_SLOTS >= current_slot {
|
||||
let new_head_root = fork_choice
|
||||
.get_head(current_slot, &self.spec)
|
||||
.map_err(BeaconChainError::from)?;
|
||||
|
||||
if new_head_root == block_root {
|
||||
if let Some(proto_block) = fork_choice.get_block(&block_root) {
|
||||
if let Err(e) = self.early_attester_cache.add_head_block(
|
||||
block_root,
|
||||
signed_block.clone(),
|
||||
proto_block,
|
||||
&state,
|
||||
&self.spec,
|
||||
) {
|
||||
warn!(
|
||||
self.log,
|
||||
"Early attester cache insert failed";
|
||||
"error" => ?e
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
self.log,
|
||||
"Early attester block missing";
|
||||
"block_root" => ?block_root
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register sync aggregate with validator monitor
|
||||
if let Ok(sync_aggregate) = block.body().sync_aggregate() {
|
||||
// `SyncCommittee` for the sync_aggregate should correspond to the duty slot
|
||||
@@ -3248,6 +3335,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
drop(lag_timer);
|
||||
|
||||
// Clear the early attester cache in case it conflicts with `self.canonical_head`.
|
||||
self.early_attester_cache.clear();
|
||||
|
||||
// Update the snapshot that stores the head of the chain at the time it received the
|
||||
// block.
|
||||
*self
|
||||
|
||||
Reference in New Issue
Block a user