mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-16 20:39:10 +00:00
[Merge] Implement execution_layer (#2635)
* Checkout serde_utils from rayonism
* Make eth1::http functions pub
* Add bones of execution_layer
* Modify decoding
* Expose Transaction, cargo fmt
* Add executePayload
* Add all minimal spec endpoints
* Start adding json rpc wrapper
* Finish custom JSON response handler
* Switch to new rpc sending method
* Add first test
* Fix camelCase
* Finish adding tests
* Begin threading execution layer into BeaconChain
* Fix clippy lints
* Fix clippy lints
* Thread execution layer into ClientBuilder
* Add CLI flags
* Add block processing methods to ExecutionLayer
* Add block_on to execution_layer
* Integrate execute_payload
* Add extra_data field
* Begin implementing payload handle
* Send consensus valid/invalid messages
* Fix minor type in task_executor
* Call forkchoiceUpdated
* Add search for TTD block
* Thread TTD into execution layer
* Allow producing block with execution payload
* Add LRU cache for execution blocks
* Remove duplicate 0x on ssz_types serialization
* Add tests for block getter methods
* Add basic block generator impl
* Add is_valid_terminal_block to EL
* Verify merge block in block_verification
* Partially implement --terminal-block-hash-override
* Add terminal_block_hash to ChainSpec
* Remove Option from terminal_block_hash in EL
* Revert merge changes to consensus/fork_choice
* Remove commented-out code
* Add bones for handling RPC methods on test server
* Add first ExecutionLayer tests
* Add testing for finding terminal block
* Prevent infinite loops
* Add insert_merge_block to block gen
* Add block gen test for pos blocks
* Start adding payloads to block gen
* Fix clippy lints
* Add execution payload to block gen
* Add execute_payload to block_gen
* Refactor block gen
* Add all routes to mock server
* Use Uint256 for base_fee_per_gas
* Add working execution chain build
* Remove unused var
* Revert "Use Uint256 for base_fee_per_gas"
This reverts commit 6c88f19ac4.
* Fix base_fee_for_gas Uint256
* Update execute payload handle
* Improve testing, fix bugs
* Fix default fee-recipient
* Fix fee-recipient address (again)
* Add check for terminal block, add comments, tidy
* Apply suggestions from code review
Co-authored-by: realbigsean <seananderson33@GMAIL.com>
* Fix is_none on handle Drop
* Remove commented-out tests
Co-authored-by: realbigsean <seananderson33@GMAIL.com>
This commit is contained in:
@@ -49,6 +49,7 @@ use crate::{metrics, BeaconChainError};
|
||||
use eth2::types::{
|
||||
EventKind, SseBlock, SseChainReorg, SseFinalizedCheckpoint, SseHead, SseLateHead, SyncDuty,
|
||||
};
|
||||
use execution_layer::ExecutionLayer;
|
||||
use fork_choice::ForkChoice;
|
||||
use futures::channel::mpsc::Sender;
|
||||
use itertools::process_results;
|
||||
@@ -62,7 +63,9 @@ use slot_clock::SlotClock;
|
||||
use state_processing::{
|
||||
common::get_indexed_attestation,
|
||||
per_block_processing,
|
||||
per_block_processing::errors::AttestationValidationError,
|
||||
per_block_processing::{
|
||||
compute_timestamp_at_slot, errors::AttestationValidationError, is_merge_complete,
|
||||
},
|
||||
per_slot_processing,
|
||||
state_advance::{complete_state_advance, partial_state_advance},
|
||||
BlockSignatureStrategy, SigVerifiedOp,
|
||||
@@ -275,6 +278,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
Mutex<ObservedOperations<AttesterSlashing<T::EthSpec>, T::EthSpec>>,
|
||||
/// Provides information from the Ethereum 1 (PoW) chain.
|
||||
pub eth1_chain: Option<Eth1Chain<T::Eth1Chain, T::EthSpec>>,
|
||||
/// Interfaces with the execution client.
|
||||
pub execution_layer: Option<ExecutionLayer>,
|
||||
/// Stores a "snapshot" of the chain at the time the head-of-the-chain block was received.
|
||||
pub(crate) canonical_head: TimeoutRwLock<BeaconSnapshot<T::EthSpec>>,
|
||||
/// The root of the genesis block.
|
||||
@@ -2407,7 +2412,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let _fork_choice_block_timer =
|
||||
metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_BLOCK_TIMES);
|
||||
fork_choice
|
||||
.on_block(current_slot, &block, block_root, &state, &self.spec)
|
||||
.on_block(current_slot, &block, block_root, &state)
|
||||
.map_err(|e| BlockError::BeaconChainError(e.into()))?;
|
||||
}
|
||||
|
||||
@@ -2839,12 +2844,42 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}))
|
||||
};
|
||||
// Closure to fetch a sync aggregate in cases where it is required.
|
||||
let get_execution_payload = || -> Result<ExecutionPayload<_>, BlockProductionError> {
|
||||
// TODO: actually get the payload from eth1 node..
|
||||
Ok(ExecutionPayload::default())
|
||||
let get_execution_payload = |latest_execution_payload_header: &ExecutionPayloadHeader<
|
||||
T::EthSpec,
|
||||
>|
|
||||
-> Result<ExecutionPayload<_>, BlockProductionError> {
|
||||
let execution_layer = self
|
||||
.execution_layer
|
||||
.as_ref()
|
||||
.ok_or(BlockProductionError::ExecutionLayerMissing)?;
|
||||
|
||||
let parent_hash;
|
||||
if !is_merge_complete(&state) {
|
||||
let terminal_pow_block_hash = execution_layer
|
||||
.block_on(|execution_layer| execution_layer.get_terminal_pow_block_hash())
|
||||
.map_err(BlockProductionError::TerminalPoWBlockLookupFailed)?;
|
||||
|
||||
if let Some(terminal_pow_block_hash) = terminal_pow_block_hash {
|
||||
parent_hash = terminal_pow_block_hash;
|
||||
} else {
|
||||
return Ok(<_>::default());
|
||||
}
|
||||
} else {
|
||||
parent_hash = latest_execution_payload_header.block_hash;
|
||||
}
|
||||
|
||||
let timestamp =
|
||||
compute_timestamp_at_slot(&state, &self.spec).map_err(BeaconStateError::from)?;
|
||||
let random = *state.get_randao_mix(state.current_epoch())?;
|
||||
|
||||
execution_layer
|
||||
.block_on(|execution_layer| {
|
||||
execution_layer.get_payload(parent_hash, timestamp, random)
|
||||
})
|
||||
.map_err(BlockProductionError::GetPayloadFailed)
|
||||
};
|
||||
|
||||
let inner_block = match state {
|
||||
let inner_block = match &state {
|
||||
BeaconState::Base(_) => BeaconBlock::Base(BeaconBlockBase {
|
||||
slot,
|
||||
proposer_index,
|
||||
@@ -2881,9 +2916,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
},
|
||||
})
|
||||
}
|
||||
BeaconState::Merge(_) => {
|
||||
BeaconState::Merge(state) => {
|
||||
let sync_aggregate = get_sync_aggregate()?;
|
||||
let execution_payload = get_execution_payload()?;
|
||||
let execution_payload =
|
||||
get_execution_payload(&state.latest_execution_payload_header)?;
|
||||
BeaconBlock::Merge(BeaconBlockMerge {
|
||||
slot,
|
||||
proposer_index,
|
||||
@@ -3094,6 +3130,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.beacon_state
|
||||
.attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current);
|
||||
|
||||
// Used later for the execution engine.
|
||||
let new_head_execution_block_hash = new_head
|
||||
.beacon_block
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.map(|ep| ep.block_hash);
|
||||
|
||||
drop(lag_timer);
|
||||
|
||||
// Update the snapshot that stores the head of the chain at the time it received the
|
||||
@@ -3297,9 +3341,67 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a post-merge block, update the execution layer.
|
||||
if let Some(new_head_execution_block_hash) = new_head_execution_block_hash {
|
||||
let execution_layer = self
|
||||
.execution_layer
|
||||
.clone()
|
||||
.ok_or(Error::ExecutionLayerMissing)?;
|
||||
let store = self.store.clone();
|
||||
let log = self.log.clone();
|
||||
|
||||
// Spawn the update task, without waiting for it to complete.
|
||||
execution_layer.spawn(
|
||||
move |execution_layer| async move {
|
||||
if let Err(e) = Self::update_execution_engine_forkchoice(
|
||||
execution_layer,
|
||||
store,
|
||||
new_finalized_checkpoint.root,
|
||||
new_head_execution_block_hash,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
log,
|
||||
"Failed to update execution head";
|
||||
"error" => ?e
|
||||
);
|
||||
}
|
||||
},
|
||||
"update_execution_engine_forkchoice",
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_execution_engine_forkchoice(
|
||||
execution_layer: ExecutionLayer,
|
||||
store: BeaconStore<T>,
|
||||
finalized_beacon_block_root: Hash256,
|
||||
head_execution_block_hash: Hash256,
|
||||
) -> Result<(), Error> {
|
||||
// Loading the finalized block from the store is not ideal. Perhaps it would be better to
|
||||
// store it on fork-choice so we can do a lookup without hitting the database.
|
||||
//
|
||||
// See: https://github.com/sigp/lighthouse/pull/2627#issuecomment-927537245
|
||||
let finalized_block = store
|
||||
.get_block(&finalized_beacon_block_root)?
|
||||
.ok_or(Error::MissingBeaconBlock(finalized_beacon_block_root))?;
|
||||
|
||||
let finalized_execution_block_hash = finalized_block
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.map(|ep| ep.block_hash)
|
||||
.unwrap_or_else(Hash256::zero);
|
||||
|
||||
execution_layer
|
||||
.forkchoice_updated(head_execution_block_hash, finalized_execution_block_hash)
|
||||
.await
|
||||
.map_err(Error::ExecutionForkChoiceUpdateFailed)
|
||||
}
|
||||
|
||||
/// This function takes a configured weak subjectivity `Checkpoint` and the latest finalized `Checkpoint`.
|
||||
/// If the weak subjectivity checkpoint and finalized checkpoint share the same epoch, we compare
|
||||
/// roots. If we the weak subjectivity checkpoint is from an older epoch, we iterate back through
|
||||
|
||||
@@ -48,8 +48,9 @@ use crate::{
|
||||
BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT, MAXIMUM_GOSSIP_CLOCK_DISPARITY,
|
||||
VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT,
|
||||
},
|
||||
eth1_chain, metrics, BeaconChain, BeaconChainError, BeaconChainTypes,
|
||||
metrics, BeaconChain, BeaconChainError, BeaconChainTypes,
|
||||
};
|
||||
use execution_layer::ExecutePayloadResponse;
|
||||
use fork_choice::{ForkChoice, ForkChoiceStore};
|
||||
use parking_lot::RwLockReadGuard;
|
||||
use proto_array::Block as ProtoBlock;
|
||||
@@ -57,7 +58,7 @@ use safe_arith::ArithError;
|
||||
use slog::{debug, error, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use ssz::Encode;
|
||||
use state_processing::per_block_processing::is_execution_enabled;
|
||||
use state_processing::per_block_processing::{is_execution_enabled, is_merge_block};
|
||||
use state_processing::{
|
||||
block_signature_verifier::{BlockSignatureVerifier, Error as BlockSignatureVerifierError},
|
||||
per_block_processing, per_slot_processing,
|
||||
@@ -242,19 +243,25 @@ pub enum ExecutionPayloadError {
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// As this is our fault, do not penalize the peer
|
||||
NoEth1Connection,
|
||||
NoExecutionConnection,
|
||||
/// Error occurred during engine_executePayload
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// Some issue with our configuration, do not penalize peer
|
||||
Eth1VerificationError(eth1_chain::Error),
|
||||
RequestFailed(execution_layer::Error),
|
||||
/// The execution engine returned INVALID for the payload
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The block is invalid and the peer is faulty
|
||||
RejectedByExecutionEngine,
|
||||
/// The execution engine returned SYNCING for the payload
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// It is not known if the block is valid or invalid.
|
||||
ExecutionEngineIsSyncing,
|
||||
/// The execution payload timestamp does not match the slot
|
||||
///
|
||||
/// ## Peer scoring
|
||||
@@ -279,6 +286,38 @@ pub enum ExecutionPayloadError {
|
||||
///
|
||||
/// The block is invalid and the peer is faulty
|
||||
TransactionDataExceedsSizeLimit,
|
||||
/// The execution payload references an execution block that cannot trigger the merge.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The block is invalid and the peer sent us a block that passes gossip propagation conditions,
|
||||
/// but is invalid upon further verification.
|
||||
InvalidTerminalPoWBlock,
|
||||
/// The execution payload references execution blocks that are unavailable on our execution
|
||||
/// nodes.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// It's not clear if the peer is invalid or if it's on a different execution fork to us.
|
||||
TerminalPoWBlockNotFound,
|
||||
}
|
||||
|
||||
impl From<execution_layer::Error> for ExecutionPayloadError {
|
||||
fn from(e: execution_layer::Error) -> Self {
|
||||
ExecutionPayloadError::RequestFailed(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EthSpec> From<ExecutionPayloadError> for BlockError<T> {
|
||||
fn from(e: ExecutionPayloadError) -> Self {
|
||||
BlockError::ExecutionPayloadError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EthSpec> From<InconsistentFork> for BlockError<T> {
|
||||
fn from(e: InconsistentFork) -> Self {
|
||||
BlockError::InconsistentFork(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EthSpec> std::fmt::Display for BlockError<T> {
|
||||
@@ -1054,35 +1093,79 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
// This is the soonest we can run these checks as they must be called AFTER per_slot_processing
|
||||
if is_execution_enabled(&state, block.message().body()) {
|
||||
let eth1_chain = chain
|
||||
.eth1_chain
|
||||
// If this block triggers the merge, check to ensure that it references valid execution
|
||||
// blocks.
|
||||
//
|
||||
// The specification defines this check inside `on_block` in the fork-choice specification,
|
||||
// however we perform the check here for two reasons:
|
||||
//
|
||||
// - There's no point in importing a block that will fail fork choice, so it's best to fail
|
||||
// early.
|
||||
// - Doing the check here means we can keep our fork-choice implementation "pure". I.e., no
|
||||
// calls to remote servers.
|
||||
if is_merge_block(&state, block.message().body()) {
|
||||
let execution_layer = chain
|
||||
.execution_layer
|
||||
.as_ref()
|
||||
.ok_or(BlockError::ExecutionPayloadError(
|
||||
ExecutionPayloadError::NoEth1Connection,
|
||||
))?;
|
||||
|
||||
let payload_valid = eth1_chain
|
||||
.on_payload(block.message().body().execution_payload().ok_or_else(|| {
|
||||
BlockError::InconsistentFork(InconsistentFork {
|
||||
.ok_or(ExecutionPayloadError::NoExecutionConnection)?;
|
||||
let execution_payload =
|
||||
block
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.ok_or_else(|| InconsistentFork {
|
||||
fork_at_slot: eth2::types::ForkName::Merge,
|
||||
object_fork: block.message().body().fork_name(),
|
||||
})
|
||||
})?)
|
||||
.map_err(|e| {
|
||||
BlockError::ExecutionPayloadError(ExecutionPayloadError::Eth1VerificationError(
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
})?;
|
||||
|
||||
if !payload_valid {
|
||||
return Err(BlockError::ExecutionPayloadError(
|
||||
ExecutionPayloadError::RejectedByExecutionEngine,
|
||||
));
|
||||
}
|
||||
let is_valid_terminal_pow_block = execution_layer
|
||||
.block_on(|execution_layer| {
|
||||
execution_layer.is_valid_terminal_pow_block_hash(execution_payload.parent_hash)
|
||||
})
|
||||
.map_err(ExecutionPayloadError::from)?;
|
||||
|
||||
match is_valid_terminal_pow_block {
|
||||
Some(true) => Ok(()),
|
||||
Some(false) => Err(ExecutionPayloadError::InvalidTerminalPoWBlock),
|
||||
None => Err(ExecutionPayloadError::TerminalPoWBlockNotFound),
|
||||
}?;
|
||||
}
|
||||
|
||||
// This is the soonest we can run these checks as they must be called AFTER per_slot_processing
|
||||
let execute_payload_handle = if is_execution_enabled(&state, block.message().body()) {
|
||||
let execution_layer = chain
|
||||
.execution_layer
|
||||
.as_ref()
|
||||
.ok_or(ExecutionPayloadError::NoExecutionConnection)?;
|
||||
let execution_payload =
|
||||
block
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.ok_or_else(|| InconsistentFork {
|
||||
fork_at_slot: eth2::types::ForkName::Merge,
|
||||
object_fork: block.message().body().fork_name(),
|
||||
})?;
|
||||
|
||||
let (execute_payload_status, execute_payload_handle) = execution_layer
|
||||
.block_on(|execution_layer| execution_layer.execute_payload(execution_payload))
|
||||
.map_err(ExecutionPayloadError::from)?;
|
||||
|
||||
match execute_payload_status {
|
||||
ExecutePayloadResponse::Valid => Ok(()),
|
||||
ExecutePayloadResponse::Invalid => {
|
||||
Err(ExecutionPayloadError::RejectedByExecutionEngine)
|
||||
}
|
||||
ExecutePayloadResponse::Syncing => {
|
||||
Err(ExecutionPayloadError::ExecutionEngineIsSyncing)
|
||||
}
|
||||
}?;
|
||||
|
||||
Some(execute_payload_handle)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// If the block is sufficiently recent, notify the validator monitor.
|
||||
if let Some(slot) = chain.slot_clock.now() {
|
||||
let epoch = slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
@@ -1181,6 +1264,15 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
|
||||
});
|
||||
}
|
||||
|
||||
// If this block required an `executePayload` call to the execution node, inform it that the
|
||||
// block is indeed valid.
|
||||
//
|
||||
// If the handle is dropped without explicitly declaring validity, an invalid message will
|
||||
// be sent to the execution engine.
|
||||
if let Some(execute_payload_handle) = execute_payload_handle {
|
||||
execute_payload_handle.publish_consensus_valid();
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
block,
|
||||
block_root,
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::{
|
||||
Eth1ChainBackend, ServerSentEventHandler,
|
||||
};
|
||||
use eth1::Config as Eth1Config;
|
||||
use execution_layer::ExecutionLayer;
|
||||
use fork_choice::ForkChoice;
|
||||
use futures::channel::mpsc::Sender;
|
||||
use operation_pool::{OperationPool, PersistedOperationPool};
|
||||
@@ -75,6 +76,7 @@ pub struct BeaconChainBuilder<T: BeaconChainTypes> {
|
||||
>,
|
||||
op_pool: Option<OperationPool<T::EthSpec>>,
|
||||
eth1_chain: Option<Eth1Chain<T::Eth1Chain, T::EthSpec>>,
|
||||
execution_layer: Option<ExecutionLayer>,
|
||||
event_handler: Option<ServerSentEventHandler<T::EthSpec>>,
|
||||
slot_clock: Option<T::SlotClock>,
|
||||
shutdown_sender: Option<Sender<ShutdownReason>>,
|
||||
@@ -115,6 +117,7 @@ where
|
||||
fork_choice: None,
|
||||
op_pool: None,
|
||||
eth1_chain: None,
|
||||
execution_layer: None,
|
||||
event_handler: None,
|
||||
slot_clock: None,
|
||||
shutdown_sender: None,
|
||||
@@ -476,6 +479,12 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `BeaconChain` execution layer.
|
||||
pub fn execution_layer(mut self, execution_layer: Option<ExecutionLayer>) -> Self {
|
||||
self.execution_layer = execution_layer;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `BeaconChain` event handler backend.
|
||||
///
|
||||
/// For example, provide `ServerSentEventHandler` as a `handler`.
|
||||
@@ -737,6 +746,7 @@ where
|
||||
observed_proposer_slashings: <_>::default(),
|
||||
observed_attester_slashings: <_>::default(),
|
||||
eth1_chain: self.eth1_chain,
|
||||
execution_layer: self.execution_layer,
|
||||
genesis_validators_root: canonical_head.beacon_state.genesis_validators_root(),
|
||||
canonical_head: TimeoutRwLock::new(canonical_head.clone()),
|
||||
genesis_block_root,
|
||||
|
||||
@@ -134,6 +134,8 @@ pub enum BeaconChainError {
|
||||
new_slot: Slot,
|
||||
},
|
||||
AltairForkDisabled,
|
||||
ExecutionLayerMissing,
|
||||
ExecutionForkChoiceUpdateFailed(execution_layer::Error),
|
||||
}
|
||||
|
||||
easy_from_to!(SlotProcessingError, BeaconChainError);
|
||||
@@ -175,6 +177,9 @@ pub enum BlockProductionError {
|
||||
produce_at_slot: Slot,
|
||||
state_slot: Slot,
|
||||
},
|
||||
ExecutionLayerMissing,
|
||||
TerminalPoWBlockLookupFailed(execution_layer::Error),
|
||||
GetPayloadFailed(execution_layer::Error),
|
||||
}
|
||||
|
||||
easy_from_to!(BlockProcessingError, BlockProductionError);
|
||||
|
||||
@@ -166,7 +166,7 @@ pub fn reset_fork_choice_to_finalization<E: EthSpec, Hot: ItemStore<E>, Cold: It
|
||||
|
||||
let (block, _) = block.deconstruct();
|
||||
fork_choice
|
||||
.on_block(block.slot(), &block, block.canonical_root(), &state, spec)
|
||||
.on_block(block.slot(), &block, block.canonical_root(), &state)
|
||||
.map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user