Prioritise important parts of block processing (#3696)

## Issue Addressed

Closes https://github.com/sigp/lighthouse/issues/2327

## Proposed Changes

This is an extension of some ideas I implemented while working on `tree-states`:

- Cache the indexed attestations from blocks in the `ConsensusContext`. Previously we were re-computing them 3-4 times over.
- Clean up `import_block` by splitting each part into `import_block_XXX`.
- Move some stuff off hot paths, specifically:
  - Relocate non-essential tasks that were running between receiving the payload verification status and priming the early attester cache. These tasks are moved after the cache priming:
    - Attestation observation
    - Validator monitor updates
    - Slasher updates
    - Updating the shuffling cache
  - Fork choice attestation observation now happens at the end of block verification in parallel with payload verification (this seems to save 5-10ms).
  - Payload verification now happens _before_ advancing the pre-state and writing it to disk! States were previously being written eagerly and adding ~20-30ms in front of verifying the execution payload. State catchup also sometimes takes ~500ms if we get a cache miss and need to rebuild the tree hash cache.

The remaining task that's taking substantial time (~20ms) is importing the block to fork choice. I _think_ this is because of pull-tips, and we should be able to optimise it out with a clever total active balance cache in the state (which would be computed in parallel with payload verification). I've decided to leave that for future work though. For now it can be observed via the new `beacon_block_processing_post_exec_pre_attestable_seconds` metric.


Co-authored-by: Michael Sproul <micsproul@gmail.com>
This commit is contained in:
Michael Sproul
2022-11-30 05:22:58 +00:00
parent b4f4c0d253
commit 22115049ee
14 changed files with 784 additions and 497 deletions

View File

@@ -1,8 +1,11 @@
use crate::common::get_indexed_attestation;
use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError};
use std::collections::{hash_map::Entry, HashMap};
use std::marker::PhantomData;
use tree_hash::TreeHash;
use types::{
BeaconState, BeaconStateError, ChainSpec, EthSpec, ExecPayload, Hash256, SignedBeaconBlock,
Slot,
Attestation, AttestationData, BeaconState, BeaconStateError, BitList, ChainSpec, Epoch,
EthSpec, ExecPayload, Hash256, IndexedAttestation, SignedBeaconBlock, Slot,
};
#[derive(Debug)]
@@ -13,6 +16,9 @@ pub struct ConsensusContext<T: EthSpec> {
proposer_index: Option<u64>,
/// Block root of the block at `slot`.
current_block_root: Option<Hash256>,
/// Cache of indexed attestations constructed during block processing.
indexed_attestations:
HashMap<(AttestationData, BitList<T::MaxValidatorsPerCommittee>), IndexedAttestation<T>>,
_phantom: PhantomData<T>,
}
@@ -20,6 +26,7 @@ pub struct ConsensusContext<T: EthSpec> {
pub enum ContextError {
BeaconState(BeaconStateError),
SlotMismatch { slot: Slot, expected: Slot },
EpochMismatch { epoch: Epoch, expected: Epoch },
}
impl From<BeaconStateError> for ContextError {
@@ -34,6 +41,7 @@ impl<T: EthSpec> ConsensusContext<T> {
slot,
proposer_index: None,
current_block_root: None,
indexed_attestations: HashMap::new(),
_phantom: PhantomData,
}
}
@@ -43,13 +51,39 @@ impl<T: EthSpec> ConsensusContext<T> {
self
}
/// Strict method for fetching the proposer index.
///
/// Gets the proposer index for `self.slot` while ensuring that it matches `state.slot()`. This
/// method should be used in block processing and almost everywhere the proposer index is
/// required. If the slot check is too restrictive, see `get_proposer_index_from_epoch_state`.
pub fn get_proposer_index(
&mut self,
state: &BeaconState<T>,
spec: &ChainSpec,
) -> Result<u64, ContextError> {
self.check_slot(state.slot())?;
self.get_proposer_index_no_checks(state, spec)
}
/// More liberal method for fetching the proposer index.
///
/// Fetches the proposer index for `self.slot` but does not require the state to be from an
/// exactly matching slot (merely a matching epoch). This is useful in batch verification where
/// we want to extract the proposer index from a single state for every slot in the epoch.
pub fn get_proposer_index_from_epoch_state(
&mut self,
state: &BeaconState<T>,
spec: &ChainSpec,
) -> Result<u64, ContextError> {
self.check_epoch(state.current_epoch())?;
self.get_proposer_index_no_checks(state, spec)
}
fn get_proposer_index_no_checks(
&mut self,
state: &BeaconState<T>,
spec: &ChainSpec,
) -> Result<u64, ContextError> {
if let Some(proposer_index) = self.proposer_index {
return Ok(proposer_index);
}
@@ -89,4 +123,39 @@ impl<T: EthSpec> ConsensusContext<T> {
})
}
}
fn check_epoch(&self, epoch: Epoch) -> Result<(), ContextError> {
let expected = self.slot.epoch(T::slots_per_epoch());
if epoch == expected {
Ok(())
} else {
Err(ContextError::EpochMismatch { epoch, expected })
}
}
pub fn get_indexed_attestation(
&mut self,
state: &BeaconState<T>,
attestation: &Attestation<T>,
) -> Result<&IndexedAttestation<T>, BlockOperationError<AttestationInvalid>> {
let key = (
attestation.data.clone(),
attestation.aggregation_bits.clone(),
);
match self.indexed_attestations.entry(key) {
Entry::Occupied(occupied) => Ok(occupied.into_mut()),
Entry::Vacant(vacant) => {
let committee =
state.get_beacon_committee(attestation.data.slot, attestation.data.index)?;
let indexed_attestation =
get_indexed_attestation(committee.committee, attestation)?;
Ok(vacant.insert(indexed_attestation))
}
}
}
pub fn num_cached_indexed_attestations(&self) -> usize {
self.indexed_attestations.len()
}
}

View File

@@ -111,16 +111,13 @@ pub fn per_block_processing<T: EthSpec, Payload: ExecPayload<T>>(
let verify_signatures = match block_signature_strategy {
BlockSignatureStrategy::VerifyBulk => {
// Verify all signatures in the block at once.
let block_root = Some(ctxt.get_current_block_root(signed_block)?);
let proposer_index = Some(ctxt.get_proposer_index(state, spec)?);
block_verify!(
BlockSignatureVerifier::verify_entire_block(
state,
|i| get_pubkey_from_state(state, i),
|pk_bytes| pk_bytes.decompress().ok().map(Cow::Owned),
signed_block,
block_root,
proposer_index,
ctxt,
spec
)
.is_ok(),
@@ -339,6 +336,7 @@ pub fn get_new_eth1_data<T: EthSpec>(
/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/beacon-chain.md#process_execution_payload
pub fn partially_verify_execution_payload<T: EthSpec, Payload: ExecPayload<T>>(
state: &BeaconState<T>,
block_slot: Slot,
payload: &Payload,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
@@ -359,7 +357,7 @@ pub fn partially_verify_execution_payload<T: EthSpec, Payload: ExecPayload<T>>(
}
);
let timestamp = compute_timestamp_at_slot(state, spec)?;
let timestamp = compute_timestamp_at_slot(state, block_slot, spec)?;
block_verify!(
payload.timestamp() == timestamp,
BlockProcessingError::ExecutionInvalidTimestamp {
@@ -383,7 +381,7 @@ pub fn process_execution_payload<T: EthSpec, Payload: ExecPayload<T>>(
payload: &Payload,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
partially_verify_execution_payload(state, payload, spec)?;
partially_verify_execution_payload(state, state.slot(), payload, spec)?;
*state.latest_execution_payload_header_mut()? = payload.to_execution_payload_header();
@@ -420,9 +418,10 @@ pub fn is_execution_enabled<T: EthSpec, Payload: ExecPayload<T>>(
/// https://github.com/ethereum/consensus-specs/blob/dev/specs/merge/beacon-chain.md#compute_timestamp_at_slot
pub fn compute_timestamp_at_slot<T: EthSpec>(
state: &BeaconState<T>,
block_slot: Slot,
spec: &ChainSpec,
) -> Result<u64, ArithError> {
let slots_since_genesis = state.slot().as_u64().safe_sub(spec.genesis_slot.as_u64())?;
let slots_since_genesis = block_slot.as_u64().safe_sub(spec.genesis_slot.as_u64())?;
slots_since_genesis
.safe_mul(spec.seconds_per_slot)
.and_then(|since_genesis| state.genesis_time().safe_add(since_genesis))

View File

@@ -1,14 +1,13 @@
#![allow(clippy::integer_arithmetic)]
use super::signature_sets::{Error as SignatureSetError, *};
use crate::common::get_indexed_attestation;
use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError};
use crate::{ConsensusContext, ContextError};
use bls::{verify_signature_sets, PublicKey, PublicKeyBytes, SignatureSet};
use rayon::prelude::*;
use std::borrow::Cow;
use types::{
BeaconState, BeaconStateError, ChainSpec, EthSpec, ExecPayload, Hash256, IndexedAttestation,
SignedBeaconBlock,
BeaconState, BeaconStateError, ChainSpec, EthSpec, ExecPayload, Hash256, SignedBeaconBlock,
};
pub type Result<T> = std::result::Result<T, Error>;
@@ -28,6 +27,8 @@ pub enum Error {
IncorrectBlockProposer { block: u64, local_shuffling: u64 },
/// Failed to load a signature set. The block may be invalid or we failed to process it.
SignatureSetError(SignatureSetError),
/// Error related to the consensus context, likely the proposer index or block root calc.
ContextError(ContextError),
}
impl From<BeaconStateError> for Error {
@@ -36,6 +37,12 @@ impl From<BeaconStateError> for Error {
}
}
impl From<ContextError> for Error {
fn from(e: ContextError) -> Error {
Error::ContextError(e)
}
}
impl From<SignatureSetError> for Error {
fn from(e: SignatureSetError) -> Error {
match e {
@@ -122,12 +129,11 @@ where
get_pubkey: F,
decompressor: D,
block: &'a SignedBeaconBlock<T, Payload>,
block_root: Option<Hash256>,
verified_proposer_index: Option<u64>,
ctxt: &mut ConsensusContext<T>,
spec: &'a ChainSpec,
) -> Result<()> {
let mut verifier = Self::new(state, get_pubkey, decompressor, spec);
verifier.include_all_signatures(block, block_root, verified_proposer_index)?;
verifier.include_all_signatures(block, ctxt)?;
verifier.verify()
}
@@ -135,11 +141,14 @@ where
pub fn include_all_signatures<Payload: ExecPayload<T>>(
&mut self,
block: &'a SignedBeaconBlock<T, Payload>,
block_root: Option<Hash256>,
verified_proposer_index: Option<u64>,
ctxt: &mut ConsensusContext<T>,
) -> Result<()> {
let block_root = Some(ctxt.get_current_block_root(block)?);
let verified_proposer_index =
Some(ctxt.get_proposer_index_from_epoch_state(self.state, self.spec)?);
self.include_block_proposal(block, block_root, verified_proposer_index)?;
self.include_all_signatures_except_proposal(block, verified_proposer_index)?;
self.include_all_signatures_except_proposal(block, ctxt)?;
Ok(())
}
@@ -149,12 +158,14 @@ where
pub fn include_all_signatures_except_proposal<Payload: ExecPayload<T>>(
&mut self,
block: &'a SignedBeaconBlock<T, Payload>,
verified_proposer_index: Option<u64>,
ctxt: &mut ConsensusContext<T>,
) -> Result<()> {
let verified_proposer_index =
Some(ctxt.get_proposer_index_from_epoch_state(self.state, self.spec)?);
self.include_randao_reveal(block, verified_proposer_index)?;
self.include_proposer_slashings(block)?;
self.include_attester_slashings(block)?;
self.include_attestations(block)?;
self.include_attestations(block, ctxt)?;
// Deposits are not included because they can legally have invalid signatures.
self.include_exits(block)?;
self.include_sync_aggregate(block)?;
@@ -260,7 +271,8 @@ where
pub fn include_attestations<Payload: ExecPayload<T>>(
&mut self,
block: &'a SignedBeaconBlock<T, Payload>,
) -> Result<Vec<IndexedAttestation<T>>> {
ctxt: &mut ConsensusContext<T>,
) -> Result<()> {
self.sets
.sets
.reserve(block.message().body().attestations().len());
@@ -270,28 +282,18 @@ where
.body()
.attestations()
.iter()
.try_fold(
Vec::with_capacity(block.message().body().attestations().len()),
|mut vec, attestation| {
let committee = self
.state
.get_beacon_committee(attestation.data.slot, attestation.data.index)?;
let indexed_attestation =
get_indexed_attestation(committee.committee, attestation)?;
.try_for_each(|attestation| {
let indexed_attestation = ctxt.get_indexed_attestation(self.state, attestation)?;
self.sets.push(indexed_attestation_signature_set(
self.state,
self.get_pubkey.clone(),
&attestation.signature,
&indexed_attestation,
self.spec,
)?);
vec.push(indexed_attestation);
Ok(vec)
},
)
self.sets.push(indexed_attestation_signature_set(
self.state,
self.get_pubkey.clone(),
&attestation.signature,
indexed_attestation,
self.spec,
)?);
Ok(())
})
.map_err(Error::into)
}

View File

@@ -57,8 +57,14 @@ pub mod base {
// Verify and apply each attestation.
for (i, attestation) in attestations.iter().enumerate() {
verify_attestation_for_block_inclusion(state, attestation, verify_signatures, spec)
.map_err(|e| e.into_with_index(i))?;
verify_attestation_for_block_inclusion(
state,
attestation,
ctxt,
verify_signatures,
spec,
)
.map_err(|e| e.into_with_index(i))?;
let pending_attestation = PendingAttestation {
aggregation_bits: attestation.aggregation_bits.clone(),
@@ -94,19 +100,11 @@ pub mod altair {
ctxt: &mut ConsensusContext<T>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
let proposer_index = ctxt.get_proposer_index(state, spec)?;
attestations
.iter()
.enumerate()
.try_for_each(|(i, attestation)| {
process_attestation(
state,
attestation,
i,
proposer_index,
verify_signatures,
spec,
)
process_attestation(state, attestation, i, ctxt, verify_signatures, spec)
})
}
@@ -114,16 +112,24 @@ pub mod altair {
state: &mut BeaconState<T>,
attestation: &Attestation<T>,
att_index: usize,
proposer_index: u64,
ctxt: &mut ConsensusContext<T>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
state.build_committee_cache(RelativeEpoch::Previous, spec)?;
state.build_committee_cache(RelativeEpoch::Current, spec)?;
let indexed_attestation =
verify_attestation_for_block_inclusion(state, attestation, verify_signatures, spec)
.map_err(|e| e.into_with_index(att_index))?;
let proposer_index = ctxt.get_proposer_index(state, spec)?;
let attesting_indices = &verify_attestation_for_block_inclusion(
state,
attestation,
ctxt,
verify_signatures,
spec,
)
.map_err(|e| e.into_with_index(att_index))?
.attesting_indices;
// Matching roots, participation flag indices
let data = &attestation.data;
@@ -135,7 +141,7 @@ pub mod altair {
let total_active_balance = state.get_total_active_balance()?;
let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?;
let mut proposer_reward_numerator = 0;
for index in &indexed_attestation.attesting_indices {
for index in attesting_indices {
let index = *index as usize;
for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() {

View File

@@ -1,7 +1,7 @@
use super::errors::{AttestationInvalid as Invalid, BlockOperationError};
use super::VerifySignatures;
use crate::common::get_indexed_attestation;
use crate::per_block_processing::is_valid_indexed_attestation;
use crate::ConsensusContext;
use safe_arith::SafeArith;
use types::*;
@@ -15,12 +15,13 @@ fn error(reason: Invalid) -> BlockOperationError<Invalid> {
/// to `state`. Otherwise, returns a descriptive `Err`.
///
/// Optionally verifies the aggregate signature, depending on `verify_signatures`.
pub fn verify_attestation_for_block_inclusion<T: EthSpec>(
pub fn verify_attestation_for_block_inclusion<'ctxt, T: EthSpec>(
state: &BeaconState<T>,
attestation: &Attestation<T>,
ctxt: &'ctxt mut ConsensusContext<T>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<IndexedAttestation<T>> {
) -> Result<&'ctxt IndexedAttestation<T>> {
let data = &attestation.data;
verify!(
@@ -39,7 +40,7 @@ pub fn verify_attestation_for_block_inclusion<T: EthSpec>(
}
);
verify_attestation_for_state(state, attestation, verify_signatures, spec)
verify_attestation_for_state(state, attestation, ctxt, verify_signatures, spec)
}
/// Returns `Ok(())` if `attestation` is a valid attestation to the chain that precedes the given
@@ -49,12 +50,13 @@ pub fn verify_attestation_for_block_inclusion<T: EthSpec>(
/// prior blocks in `state`.
///
/// Spec v0.12.1
pub fn verify_attestation_for_state<T: EthSpec>(
pub fn verify_attestation_for_state<'ctxt, T: EthSpec>(
state: &BeaconState<T>,
attestation: &Attestation<T>,
ctxt: &'ctxt mut ConsensusContext<T>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<IndexedAttestation<T>> {
) -> Result<&'ctxt IndexedAttestation<T>> {
let data = &attestation.data;
verify!(
@@ -66,9 +68,8 @@ pub fn verify_attestation_for_state<T: EthSpec>(
verify_casper_ffg_vote(attestation, state)?;
// Check signature and bitfields
let committee = state.get_beacon_committee(attestation.data.slot, attestation.data.index)?;
let indexed_attestation = get_indexed_attestation(committee.committee, attestation)?;
is_valid_indexed_attestation(state, &indexed_attestation, verify_signatures, spec)?;
let indexed_attestation = ctxt.get_indexed_attestation(state, attestation)?;
is_valid_indexed_attestation(state, indexed_attestation, verify_signatures, spec)?;
Ok(indexed_attestation)
}