mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-15 19:02:42 +00:00
## Issue Addressed Closes #4332 ## Proposed Changes Remove the `CountUnrealized` type, defaulting unrealized justification to _on_. This fixes the #4332 issue by ensuring that importing the same block to fork choice always results in the same outcome. Finalized sync speed may be slightly impacted by this change, but that is deemed an acceptable trade-off until the optimisation from #4118 is implemented. TODO: - [x] Also check that the block isn't a duplicate before importing
207 lines
7.8 KiB
Rust
207 lines
7.8 KiB
Rust
use crate::{BeaconForkChoiceStore, BeaconSnapshot};
|
|
use fork_choice::{ForkChoice, PayloadVerificationStatus};
|
|
use itertools::process_results;
|
|
use slog::{info, warn, Logger};
|
|
use state_processing::state_advance::complete_state_advance;
|
|
use state_processing::{
|
|
per_block_processing, per_block_processing::BlockSignatureStrategy, ConsensusContext,
|
|
StateProcessingStrategy, VerifyBlockRoot,
|
|
};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use store::{iter::ParentRootBlockIterator, HotColdDB, ItemStore};
|
|
use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot};
|
|
|
|
const CORRUPT_DB_MESSAGE: &str = "The database could be corrupt. Check its file permissions or \
|
|
consider deleting it by running with the --purge-db flag.";
|
|
|
|
/// Revert the head to the last block before the most recent hard fork.
|
|
///
|
|
/// This function is destructive and should only be used if there is no viable alternative. It will
|
|
/// cause the reverted blocks and states to be completely forgotten, lying dormant in the database
|
|
/// forever.
|
|
///
|
|
/// Return the `(head_block_root, head_block)` that should be used post-reversion.
|
|
pub fn revert_to_fork_boundary<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>(
|
|
current_slot: Slot,
|
|
head_block_root: Hash256,
|
|
store: Arc<HotColdDB<E, Hot, Cold>>,
|
|
spec: &ChainSpec,
|
|
log: &Logger,
|
|
) -> Result<(Hash256, SignedBeaconBlock<E>), String> {
|
|
let current_fork = spec.fork_name_at_slot::<E>(current_slot);
|
|
let fork_epoch = spec
|
|
.fork_epoch(current_fork)
|
|
.ok_or_else(|| format!("Current fork '{}' never activates", current_fork))?;
|
|
|
|
if current_fork == ForkName::Base {
|
|
return Err(format!(
|
|
"Cannot revert to before phase0 hard fork. {}",
|
|
CORRUPT_DB_MESSAGE
|
|
));
|
|
}
|
|
|
|
warn!(
|
|
log,
|
|
"Reverting invalid head block";
|
|
"target_fork" => %current_fork,
|
|
"fork_epoch" => fork_epoch,
|
|
);
|
|
let block_iter = ParentRootBlockIterator::fork_tolerant(&store, head_block_root);
|
|
|
|
let (block_root, blinded_block) = process_results(block_iter, |mut iter| {
|
|
iter.find_map(|(block_root, block)| {
|
|
if block.slot() < fork_epoch.start_slot(E::slots_per_epoch()) {
|
|
Some((block_root, block))
|
|
} else {
|
|
info!(
|
|
log,
|
|
"Reverting block";
|
|
"block_root" => ?block_root,
|
|
"slot" => block.slot(),
|
|
);
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.map_err(|e| {
|
|
format!(
|
|
"Error fetching blocks to revert: {:?}. {}",
|
|
e, CORRUPT_DB_MESSAGE
|
|
)
|
|
})?
|
|
.ok_or_else(|| format!("No pre-fork blocks found. {}", CORRUPT_DB_MESSAGE))?;
|
|
|
|
let block = store
|
|
.make_full_block(&block_root, blinded_block)
|
|
.map_err(|e| format!("Unable to add payload to new head block: {:?}", e))?;
|
|
|
|
Ok((block_root, block))
|
|
}
|
|
|
|
/// Reset fork choice to the finalized checkpoint of the supplied head state.
|
|
///
|
|
/// The supplied `head_block_root` should correspond to the most recently applied block on
|
|
/// `head_state`.
|
|
///
|
|
/// This function avoids quirks of fork choice initialization by replaying all of the blocks from
|
|
/// the checkpoint to the head.
|
|
///
|
|
/// See this issue for details: https://github.com/ethereum/consensus-specs/issues/2566
|
|
///
|
|
/// It will fail if the finalized state or any of the blocks to replay are unavailable.
|
|
///
|
|
/// WARNING: this function is destructive and causes fork choice to permanently forget all
|
|
/// chains other than the chain leading to `head_block_root`. It should only be used in extreme
|
|
/// circumstances when there is no better alternative.
|
|
pub fn reset_fork_choice_to_finalization<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>(
|
|
head_block_root: Hash256,
|
|
head_state: &BeaconState<E>,
|
|
store: Arc<HotColdDB<E, Hot, Cold>>,
|
|
current_slot: Option<Slot>,
|
|
spec: &ChainSpec,
|
|
) -> Result<ForkChoice<BeaconForkChoiceStore<E, Hot, Cold>, E>, String> {
|
|
// Fetch finalized block.
|
|
let finalized_checkpoint = head_state.finalized_checkpoint();
|
|
let finalized_block_root = finalized_checkpoint.root;
|
|
let finalized_block = store
|
|
.get_full_block(&finalized_block_root)
|
|
.map_err(|e| format!("Error loading finalized block: {:?}", e))?
|
|
.ok_or_else(|| {
|
|
format!(
|
|
"Finalized block missing for revert: {:?}",
|
|
finalized_block_root
|
|
)
|
|
})?;
|
|
|
|
// Advance finalized state to finalized epoch (to handle skipped slots).
|
|
let finalized_state_root = finalized_block.state_root();
|
|
let mut finalized_state = store
|
|
.get_state(&finalized_state_root, Some(finalized_block.slot()))
|
|
.map_err(|e| format!("Error loading finalized state: {:?}", e))?
|
|
.ok_or_else(|| {
|
|
format!(
|
|
"Finalized block state missing from database: {:?}",
|
|
finalized_state_root
|
|
)
|
|
})?;
|
|
let finalized_slot = finalized_checkpoint.epoch.start_slot(E::slots_per_epoch());
|
|
complete_state_advance(
|
|
&mut finalized_state,
|
|
Some(finalized_state_root),
|
|
finalized_slot,
|
|
spec,
|
|
)
|
|
.map_err(|e| {
|
|
format!(
|
|
"Error advancing finalized state to finalized epoch: {:?}",
|
|
e
|
|
)
|
|
})?;
|
|
let finalized_snapshot = BeaconSnapshot {
|
|
beacon_block_root: finalized_block_root,
|
|
beacon_block: Arc::new(finalized_block),
|
|
beacon_state: finalized_state,
|
|
};
|
|
|
|
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), &finalized_snapshot)
|
|
.map_err(|e| format!("Unable to reset fork choice store for revert: {e:?}"))?;
|
|
|
|
let mut fork_choice = ForkChoice::from_anchor(
|
|
fc_store,
|
|
finalized_block_root,
|
|
&finalized_snapshot.beacon_block,
|
|
&finalized_snapshot.beacon_state,
|
|
current_slot,
|
|
spec,
|
|
)
|
|
.map_err(|e| format!("Unable to reset fork choice for revert: {:?}", e))?;
|
|
|
|
// Replay blocks from finalized checkpoint back to head.
|
|
// We do not replay attestations presently, relying on the absence of other blocks
|
|
// to guarantee `head_block_root` as the head.
|
|
let blocks = store
|
|
.load_blocks_to_replay(finalized_slot + 1, head_state.slot(), head_block_root)
|
|
.map_err(|e| format!("Error loading blocks to replay for fork choice: {:?}", e))?;
|
|
|
|
let mut state = finalized_snapshot.beacon_state;
|
|
for block in blocks {
|
|
complete_state_advance(&mut state, None, block.slot(), spec)
|
|
.map_err(|e| format!("State advance failed: {:?}", e))?;
|
|
|
|
let mut ctxt = ConsensusContext::new(block.slot())
|
|
.set_proposer_index(block.message().proposer_index());
|
|
per_block_processing(
|
|
&mut state,
|
|
&block,
|
|
BlockSignatureStrategy::NoVerification,
|
|
StateProcessingStrategy::Accurate,
|
|
VerifyBlockRoot::True,
|
|
&mut ctxt,
|
|
spec,
|
|
)
|
|
.map_err(|e| format!("Error replaying block: {:?}", e))?;
|
|
|
|
// Setting this to unverified is the safest solution, since we don't have a way to
|
|
// retro-actively determine if they were valid or not.
|
|
//
|
|
// This scenario is so rare that it seems OK to double-verify some blocks.
|
|
let payload_verification_status = PayloadVerificationStatus::Optimistic;
|
|
|
|
fork_choice
|
|
.on_block(
|
|
block.slot(),
|
|
block.message(),
|
|
block.canonical_root(),
|
|
// Reward proposer boost. We are reinforcing the canonical chain.
|
|
Duration::from_secs(0),
|
|
&state,
|
|
payload_verification_status,
|
|
spec,
|
|
)
|
|
.map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?;
|
|
}
|
|
|
|
Ok(fork_choice)
|
|
}
|