mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-15 10:52:43 +00:00
## Proposed Changes
This PR has two aims: to speed up attestation packing in the op pool, and to fix bugs in the verification of attester slashings, proposer slashings and voluntary exits. The changes are bundled into a single database schema upgrade (v12).
Attestation packing is sped up by removing several inefficiencies:
- No more recalculation of `attesting_indices` during packing.
- No (unnecessary) examination of the `ParticipationFlags`: a bitfield suffices. See `RewardCache`.
- No re-checking of attestation validity during packing: the `AttestationMap` provides attestations which are "correct by construction" (I have checked this using Hydra).
- No SSZ re-serialization for the clunky `AttestationId` type (it can be removed in a future release).
So far the speed-up seems to be roughly 2-10x, from 500ms down to 50-100ms.
Verification of attester slashings, proposer slashings and voluntary exits is fixed by:
- Tracking the `ForkVersion`s that were used to verify each message inside the `SigVerifiedOp`. This allows us to quickly re-verify that they match the head state's opinion of what the `ForkVersion` should be at the epoch(s) relevant to the message.
- Storing the `SigVerifiedOp` on disk rather than the raw operation. This allows us to continue track the fork versions after a reboot.
This is mostly contained in this commit 52bb1840ae.
## Additional Info
The schema upgrade uses the justified state to re-verify attestations and compute `attesting_indices` for them. It will drop any attestations that fail to verify, by the logic that attestations are most valuable in the few slots after they're observed, and are probably stale and useless by the time a node restarts. Exits and proposer slashings and similarly re-verified to obtain `SigVerifiedOp`s.
This PR contains a runtime killswitch `--paranoid-block-proposal` which opts out of all the optimisations in favour of closely verifying every included message. Although I'm quite sure that the optimisations are correct this flag could be useful in the event of an unforeseen emergency.
Finally, you might notice that the `RewardCache` appears quite useless in its current form because it is only updated on the hot-path immediately before proposal. My hope is that in future we can shift calls to `RewardCache::update` into the background, e.g. while performing the state advance. It is also forward-looking to `tree-states` compatibility, where iterating and indexing `state.{previous,current}_epoch_participation` is expensive and needs to be minimised.
189 lines
6.3 KiB
Rust
189 lines
6.3 KiB
Rust
use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped};
|
|
use eth2::lighthouse::{BlockReward, BlockRewardsQuery};
|
|
use lru::LruCache;
|
|
use slog::{debug, warn, Logger};
|
|
use state_processing::BlockReplayer;
|
|
use std::sync::Arc;
|
|
use types::BeaconBlock;
|
|
use warp_utils::reject::{
|
|
beacon_chain_error, beacon_state_error, custom_bad_request, custom_server_error,
|
|
};
|
|
|
|
const STATE_CACHE_SIZE: usize = 2;
|
|
|
|
/// Fetch block rewards for blocks from the canonical chain.
|
|
pub fn get_block_rewards<T: BeaconChainTypes>(
|
|
query: BlockRewardsQuery,
|
|
chain: Arc<BeaconChain<T>>,
|
|
log: Logger,
|
|
) -> Result<Vec<BlockReward>, warp::Rejection> {
|
|
let start_slot = query.start_slot;
|
|
let end_slot = query.end_slot;
|
|
let prior_slot = start_slot - 1;
|
|
|
|
if start_slot > end_slot || start_slot == 0 {
|
|
return Err(custom_bad_request(format!(
|
|
"invalid start and end: {}, {}",
|
|
start_slot, end_slot
|
|
)));
|
|
}
|
|
|
|
let end_block_root = chain
|
|
.block_root_at_slot(end_slot, WhenSlotSkipped::Prev)
|
|
.map_err(beacon_chain_error)?
|
|
.ok_or_else(|| custom_bad_request(format!("block at end slot {} unknown", end_slot)))?;
|
|
|
|
let blocks = chain
|
|
.store
|
|
.load_blocks_to_replay(start_slot, end_slot, end_block_root)
|
|
.map_err(|e| beacon_chain_error(e.into()))?;
|
|
|
|
let state_root = chain
|
|
.state_root_at_slot(prior_slot)
|
|
.map_err(beacon_chain_error)?
|
|
.ok_or_else(|| custom_bad_request(format!("prior state at slot {} unknown", prior_slot)))?;
|
|
|
|
let mut state = chain
|
|
.get_state(&state_root, Some(prior_slot))
|
|
.and_then(|maybe_state| maybe_state.ok_or(BeaconChainError::MissingBeaconState(state_root)))
|
|
.map_err(beacon_chain_error)?;
|
|
|
|
state
|
|
.build_all_caches(&chain.spec)
|
|
.map_err(beacon_state_error)?;
|
|
|
|
let mut reward_cache = Default::default();
|
|
let mut block_rewards = Vec::with_capacity(blocks.len());
|
|
|
|
let block_replayer = BlockReplayer::new(state, &chain.spec)
|
|
.pre_block_hook(Box::new(|state, block| {
|
|
state.build_all_committee_caches(&chain.spec)?;
|
|
|
|
// Compute block reward.
|
|
let block_reward = chain.compute_block_reward(
|
|
block.message(),
|
|
block.canonical_root(),
|
|
state,
|
|
&mut reward_cache,
|
|
query.include_attestations,
|
|
)?;
|
|
block_rewards.push(block_reward);
|
|
Ok(())
|
|
}))
|
|
.state_root_iter(
|
|
chain
|
|
.forwards_iter_state_roots_until(prior_slot, end_slot)
|
|
.map_err(beacon_chain_error)?,
|
|
)
|
|
.no_signature_verification()
|
|
.minimal_block_root_verification()
|
|
.apply_blocks(blocks, None)
|
|
.map_err(beacon_chain_error)?;
|
|
|
|
if block_replayer.state_root_miss() {
|
|
warn!(
|
|
log,
|
|
"Block reward state root miss";
|
|
"start_slot" => start_slot,
|
|
"end_slot" => end_slot,
|
|
);
|
|
}
|
|
|
|
drop(block_replayer);
|
|
|
|
Ok(block_rewards)
|
|
}
|
|
|
|
/// Compute block rewards for blocks passed in as input.
|
|
pub fn compute_block_rewards<T: BeaconChainTypes>(
|
|
blocks: Vec<BeaconBlock<T::EthSpec>>,
|
|
chain: Arc<BeaconChain<T>>,
|
|
log: Logger,
|
|
) -> Result<Vec<BlockReward>, warp::Rejection> {
|
|
let mut block_rewards = Vec::with_capacity(blocks.len());
|
|
let mut state_cache = LruCache::new(STATE_CACHE_SIZE);
|
|
let mut reward_cache = Default::default();
|
|
|
|
for block in blocks {
|
|
let parent_root = block.parent_root();
|
|
|
|
// Check LRU cache for a constructed state from a previous iteration.
|
|
let state = if let Some(state) = state_cache.get(&(parent_root, block.slot())) {
|
|
debug!(
|
|
log,
|
|
"Re-using cached state for block rewards";
|
|
"parent_root" => ?parent_root,
|
|
"slot" => block.slot(),
|
|
);
|
|
state
|
|
} else {
|
|
debug!(
|
|
log,
|
|
"Fetching state for block rewards";
|
|
"parent_root" => ?parent_root,
|
|
"slot" => block.slot()
|
|
);
|
|
let parent_block = chain
|
|
.get_blinded_block(&parent_root)
|
|
.map_err(beacon_chain_error)?
|
|
.ok_or_else(|| {
|
|
custom_bad_request(format!(
|
|
"parent block not known or not canonical: {:?}",
|
|
parent_root
|
|
))
|
|
})?;
|
|
|
|
let parent_state = chain
|
|
.get_state(&parent_block.state_root(), Some(parent_block.slot()))
|
|
.map_err(beacon_chain_error)?
|
|
.ok_or_else(|| {
|
|
custom_bad_request(format!(
|
|
"no state known for parent block: {:?}",
|
|
parent_root
|
|
))
|
|
})?;
|
|
|
|
let block_replayer = BlockReplayer::new(parent_state, &chain.spec)
|
|
.no_signature_verification()
|
|
.state_root_iter([Ok((parent_block.state_root(), parent_block.slot()))].into_iter())
|
|
.minimal_block_root_verification()
|
|
.apply_blocks(vec![], Some(block.slot()))
|
|
.map_err(beacon_chain_error)?;
|
|
|
|
if block_replayer.state_root_miss() {
|
|
warn!(
|
|
log,
|
|
"Block reward state root miss";
|
|
"parent_slot" => parent_block.slot(),
|
|
"slot" => block.slot(),
|
|
);
|
|
}
|
|
|
|
let mut state = block_replayer.into_state();
|
|
state
|
|
.build_all_committee_caches(&chain.spec)
|
|
.map_err(beacon_state_error)?;
|
|
|
|
state_cache
|
|
.get_or_insert((parent_root, block.slot()), || state)
|
|
.ok_or_else(|| {
|
|
custom_server_error("LRU cache insert should always succeed".into())
|
|
})?
|
|
};
|
|
|
|
// Compute block reward.
|
|
let block_reward = chain
|
|
.compute_block_reward(
|
|
block.to_ref(),
|
|
block.canonical_root(),
|
|
state,
|
|
&mut reward_cache,
|
|
true,
|
|
)
|
|
.map_err(beacon_chain_error)?;
|
|
block_rewards.push(block_reward);
|
|
}
|
|
|
|
Ok(block_rewards)
|
|
}
|