mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-20 13:24:44 +00:00
Verify execution block hashes during finalized sync (#3794)
## Issue Addressed Recent discussions with other client devs about optimistic sync have revealed a conceptual issue with the optimisation implemented in #3738. In designing that feature I failed to consider that the execution node checks the `blockHash` of the execution payload before responding with `SYNCING`, and that omitting this check entirely results in a degradation of the full node's validation. A node omitting the `blockHash` checks could be tricked by a supermajority of validators into following an invalid chain, something which is ordinarily impossible. ## Proposed Changes I've added verification of the `payload.block_hash` in Lighthouse. In case of failure we log a warning and fall back to verifying the payload with the execution client. I've used our existing dependency on `ethers_core` for RLP support, and a new dependency on Parity's `triehash` crate for the Merkle patricia trie. Although the `triehash` crate is currently unmaintained it seems like our best option at the moment (it is also used by Reth, and requires vastly less boilerplate than Parity's generic `trie-root` library). Block hash verification is pretty quick, about 500us per block on my machine (mainnet). The optimistic finalized sync feature can be disabled using `--disable-optimistic-finalized-sync` which forces full verification with the EL. ## Additional Info This PR also introduces a new dependency on our [`metastruct`](https://github.com/sigp/metastruct) library, which was perfectly suited to the RLP serialization method. There will likely be changes as `metastruct` grows, but I think this is a good way to start dogfooding it. I took inspiration from some Parity and Reth code while writing this, and have preserved the relevant license headers on the files containing code that was copied and modified.
This commit is contained in:
@@ -15,7 +15,7 @@ use crate::{
|
||||
use execution_layer::{BuilderParams, PayloadStatus};
|
||||
use fork_choice::{InvalidationOperation, PayloadVerificationStatus};
|
||||
use proto_array::{Block as ProtoBlock, ExecutionStatus};
|
||||
use slog::debug;
|
||||
use slog::{debug, warn};
|
||||
use slot_clock::SlotClock;
|
||||
use state_processing::per_block_processing::{
|
||||
compute_timestamp_at_slot, is_execution_enabled, is_merge_transition_complete,
|
||||
@@ -59,26 +59,46 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
notify_execution_layer: NotifyExecutionLayer,
|
||||
) -> Result<Self, BlockError<T::EthSpec>> {
|
||||
let payload_verification_status = match notify_execution_layer {
|
||||
NotifyExecutionLayer::No => Some(PayloadVerificationStatus::Optimistic),
|
||||
NotifyExecutionLayer::Yes => {
|
||||
if is_execution_enabled(state, block.message().body()) {
|
||||
// Perform the initial stages of payload verification.
|
||||
//
|
||||
// We will duplicate these checks again during `per_block_processing`, however these checks
|
||||
// are cheap and doing them here ensures we protect the execution engine from junk.
|
||||
partially_verify_execution_payload(
|
||||
state,
|
||||
block.slot(),
|
||||
block.message().execution_payload()?,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(BlockError::PerBlockProcessingError)?;
|
||||
None
|
||||
} else {
|
||||
Some(PayloadVerificationStatus::Irrelevant)
|
||||
let payload_verification_status = if is_execution_enabled(state, block.message().body()) {
|
||||
// Perform the initial stages of payload verification.
|
||||
//
|
||||
// We will duplicate these checks again during `per_block_processing`, however these
|
||||
// checks are cheap and doing them here ensures we have verified them before marking
|
||||
// the block as optimistically imported. This is particularly relevant in the case
|
||||
// where we do not send the block to the EL at all.
|
||||
let block_message = block.message();
|
||||
let payload = block_message.execution_payload()?;
|
||||
partially_verify_execution_payload(state, block.slot(), payload, &chain.spec)
|
||||
.map_err(BlockError::PerBlockProcessingError)?;
|
||||
|
||||
match notify_execution_layer {
|
||||
NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => {
|
||||
// Verify the block hash here in Lighthouse and immediately mark the block as
|
||||
// optimistically imported. This saves a lot of roundtrips to the EL.
|
||||
let execution_layer = chain
|
||||
.execution_layer
|
||||
.as_ref()
|
||||
.ok_or(ExecutionPayloadError::NoExecutionConnection)?;
|
||||
|
||||
if let Err(e) =
|
||||
execution_layer.verify_payload_block_hash(&payload.execution_payload)
|
||||
{
|
||||
warn!(
|
||||
chain.log,
|
||||
"Falling back to slow block hash verification";
|
||||
"block_number" => payload.block_number(),
|
||||
"info" => "you can silence this warning with --disable-optimistic-finalized-sync",
|
||||
"error" => ?e,
|
||||
);
|
||||
None
|
||||
} else {
|
||||
Some(PayloadVerificationStatus::Optimistic)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
Some(PayloadVerificationStatus::Irrelevant)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
|
||||
Reference in New Issue
Block a user