mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-14 18:32:42 +00:00
Builder Specs v0.2.0 (#3134)
## Issue Addressed https://github.com/sigp/lighthouse/issues/3091 Extends https://github.com/sigp/lighthouse/pull/3062, adding pre-bellatrix block support on blinded endpoints and allowing the normal proposal flow (local payload construction) on blinded endpoints. This resulted in better fallback logic because the VC will not have to switch endpoints on failure in the BN <> Builder API, the BN can just fallback immediately and without repeating block processing that it shouldn't need to. We can also keep VC fallback from the VC<>BN API's blinded endpoint to full endpoint. ## Proposed Changes - Pre-bellatrix blocks on blinded endpoints - Add a new `PayloadCache` to the execution layer - Better fallback-from-builder logic ## Todos - [x] Remove VC transition logic - [x] Add logic to only enable builder flow after Merge transition finalization - [x] Tests - [x] Fix metrics - [x] Rustdocs Co-authored-by: Mac L <mjladson@pm.me> Co-authored-by: realbigsean <sean@sigmaprime.io>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
//! This crate only provides useful functionality for "The Merge", it does not provide any of the
|
||||
//! deposit-contract functionality that the `beacon_node/eth1` crate already provides.
|
||||
|
||||
use crate::payload_cache::PayloadCache;
|
||||
use auth::{strip_prefix, Auth, JwtKey};
|
||||
use builder_client::BuilderHttpClient;
|
||||
use engine_api::Error as ApiError;
|
||||
@@ -31,13 +32,14 @@ use tokio::{
|
||||
time::sleep,
|
||||
};
|
||||
use types::{
|
||||
BlindedPayload, BlockType, ChainSpec, Epoch, ExecPayload, ExecutionBlockHash,
|
||||
BlindedPayload, BlockType, ChainSpec, Epoch, ExecPayload, ExecutionBlockHash, ForkName,
|
||||
ProposerPreparationData, PublicKeyBytes, SignedBeaconBlock, Slot,
|
||||
};
|
||||
|
||||
mod engine_api;
|
||||
mod engines;
|
||||
mod metrics;
|
||||
pub mod payload_cache;
|
||||
mod payload_status;
|
||||
pub mod test_utils;
|
||||
|
||||
@@ -69,6 +71,7 @@ pub enum Error {
|
||||
NoPayloadBuilder,
|
||||
ApiError(ApiError),
|
||||
Builder(builder_client::Error),
|
||||
NoHeaderFromBuilder,
|
||||
EngineError(Box<EngineError>),
|
||||
NotSynced,
|
||||
ShuttingDown,
|
||||
@@ -101,6 +104,26 @@ pub struct Proposer {
|
||||
payload_attributes: PayloadAttributes,
|
||||
}
|
||||
|
||||
/// Information from the beacon chain that is necessary for querying the builder API.
|
||||
pub struct BuilderParams {
|
||||
pub pubkey: PublicKeyBytes,
|
||||
pub slot: Slot,
|
||||
pub chain_health: ChainHealth,
|
||||
}
|
||||
|
||||
pub enum ChainHealth {
|
||||
Healthy,
|
||||
Unhealthy(FailedCondition),
|
||||
PreMerge,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FailedCondition {
|
||||
Skips,
|
||||
SkipsPerEpoch,
|
||||
EpochsSinceFinalization,
|
||||
}
|
||||
|
||||
struct Inner<E: EthSpec> {
|
||||
engine: Arc<Engine>,
|
||||
builder: Option<BuilderHttpClient>,
|
||||
@@ -110,7 +133,7 @@ struct Inner<E: EthSpec> {
|
||||
execution_blocks: Mutex<LruCache<ExecutionBlockHash, ExecutionBlock>>,
|
||||
proposers: RwLock<HashMap<ProposerKey, Proposer>>,
|
||||
executor: TaskExecutor,
|
||||
phantom: std::marker::PhantomData<E>,
|
||||
payload_cache: PayloadCache<E>,
|
||||
log: Logger,
|
||||
}
|
||||
|
||||
@@ -212,7 +235,7 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
||||
proposers: RwLock::new(HashMap::new()),
|
||||
execution_blocks: Mutex::new(LruCache::new(EXECUTION_BLOCKS_LRU_CACHE_SIZE)),
|
||||
executor,
|
||||
phantom: std::marker::PhantomData,
|
||||
payload_cache: PayloadCache::default(),
|
||||
log,
|
||||
};
|
||||
|
||||
@@ -231,6 +254,16 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
||||
&self.inner.builder
|
||||
}
|
||||
|
||||
/// Cache a full payload, keyed on the `tree_hash_root` of its `transactions` field.
|
||||
fn cache_payload(&self, payload: &ExecutionPayload<T>) -> Option<ExecutionPayload<T>> {
|
||||
self.inner.payload_cache.put(payload.clone())
|
||||
}
|
||||
|
||||
/// Attempt to retrieve a full payload from the payload cache by the `transactions_root`.
|
||||
pub fn get_payload_by_root(&self, root: &Hash256) -> Option<ExecutionPayload<T>> {
|
||||
self.inner.payload_cache.pop(root)
|
||||
}
|
||||
|
||||
pub fn executor(&self) -> &TaskExecutor {
|
||||
&self.inner.executor
|
||||
}
|
||||
@@ -487,9 +520,9 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
||||
timestamp: u64,
|
||||
prev_randao: Hash256,
|
||||
proposer_index: u64,
|
||||
pubkey: Option<PublicKeyBytes>,
|
||||
slot: Slot,
|
||||
forkchoice_update_params: ForkchoiceUpdateParameters,
|
||||
builder_params: BuilderParams,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Payload, Error> {
|
||||
let suggested_fee_recipient = self.get_suggested_fee_recipient(proposer_index).await;
|
||||
|
||||
@@ -504,9 +537,9 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
||||
timestamp,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
pubkey,
|
||||
slot,
|
||||
forkchoice_update_params,
|
||||
builder_params,
|
||||
spec,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -534,36 +567,137 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
||||
timestamp: u64,
|
||||
prev_randao: Hash256,
|
||||
suggested_fee_recipient: Address,
|
||||
pubkey_opt: Option<PublicKeyBytes>,
|
||||
slot: Slot,
|
||||
forkchoice_update_params: ForkchoiceUpdateParameters,
|
||||
builder_params: BuilderParams,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Payload, Error> {
|
||||
//FIXME(sean) fallback logic included in PR #3134
|
||||
if let Some(builder) = self.builder() {
|
||||
let slot = builder_params.slot;
|
||||
let pubkey = builder_params.pubkey;
|
||||
|
||||
// Don't attempt to outsource payload construction until after the merge transition has been
|
||||
// finalized. We want to be conservative with payload construction until then.
|
||||
if let (Some(builder), Some(pubkey)) = (self.builder(), pubkey_opt) {
|
||||
if forkchoice_update_params
|
||||
.finalized_hash
|
||||
.map_or(false, |finalized_block_hash| {
|
||||
finalized_block_hash != ExecutionBlockHash::zero()
|
||||
})
|
||||
{
|
||||
info!(
|
||||
self.log(),
|
||||
"Requesting blinded header from connected builder";
|
||||
"slot" => ?slot,
|
||||
"pubkey" => ?pubkey,
|
||||
"parent_hash" => ?parent_hash,
|
||||
);
|
||||
return builder
|
||||
.get_builder_header::<T, Payload>(slot, parent_hash, &pubkey)
|
||||
.await
|
||||
.map(|d| d.data.message.header)
|
||||
.map_err(Error::Builder);
|
||||
match builder_params.chain_health {
|
||||
ChainHealth::Healthy => {
|
||||
info!(
|
||||
self.log(),
|
||||
"Requesting blinded header from connected builder";
|
||||
"slot" => ?slot,
|
||||
"pubkey" => ?pubkey,
|
||||
"parent_hash" => ?parent_hash,
|
||||
);
|
||||
let (relay_result, local_result) = tokio::join!(
|
||||
builder.get_builder_header::<T, Payload>(slot, parent_hash, &pubkey),
|
||||
self.get_full_payload_caching(
|
||||
parent_hash,
|
||||
timestamp,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
forkchoice_update_params,
|
||||
)
|
||||
);
|
||||
|
||||
return match (relay_result, local_result) {
|
||||
(Err(e), Ok(local)) => {
|
||||
warn!(
|
||||
self.log(),
|
||||
"Unable to retrieve a payload from a connected \
|
||||
builder, falling back to the local execution client: {e:?}"
|
||||
);
|
||||
Ok(local)
|
||||
}
|
||||
(Ok(None), Ok(local)) => {
|
||||
warn!(
|
||||
self.log(),
|
||||
"No payload provided by connected builder. \
|
||||
Attempting to propose through local execution engine"
|
||||
);
|
||||
Ok(local)
|
||||
}
|
||||
(Ok(Some(relay)), Ok(local)) => {
|
||||
let is_signature_valid = relay.data.verify_signature(spec);
|
||||
let header = relay.data.message.header;
|
||||
|
||||
info!(
|
||||
self.log(),
|
||||
"Received a payload header from the connected builder";
|
||||
"block_hash" => ?header.block_hash(),
|
||||
);
|
||||
|
||||
if header.parent_hash() != parent_hash {
|
||||
warn!(
|
||||
self.log(),
|
||||
"Invalid parent hash from connected builder, \
|
||||
falling back to local execution engine."
|
||||
);
|
||||
Ok(local)
|
||||
} else if header.prev_randao() != prev_randao {
|
||||
warn!(
|
||||
self.log(),
|
||||
"Invalid prev randao from connected builder, \
|
||||
falling back to local execution engine."
|
||||
);
|
||||
Ok(local)
|
||||
} else if header.timestamp() != local.timestamp() {
|
||||
warn!(
|
||||
self.log(),
|
||||
"Invalid timestamp from connected builder, \
|
||||
falling back to local execution engine."
|
||||
);
|
||||
Ok(local)
|
||||
} else if header.block_number() != local.block_number() {
|
||||
warn!(
|
||||
self.log(),
|
||||
"Invalid block number from connected builder, \
|
||||
falling back to local execution engine."
|
||||
);
|
||||
Ok(local)
|
||||
} else if !matches!(relay.version, Some(ForkName::Merge)) {
|
||||
// Once fork information is added to the payload, we will need to
|
||||
// check that the local and relay payloads match. At this point, if
|
||||
// we are requesting a payload at all, we have to assume this is
|
||||
// the Bellatrix fork.
|
||||
warn!(
|
||||
self.log(),
|
||||
"Invalid fork from connected builder, falling \
|
||||
back to local execution engine."
|
||||
);
|
||||
Ok(local)
|
||||
} else if !is_signature_valid {
|
||||
let pubkey_bytes = relay.data.message.pubkey;
|
||||
warn!(self.log(), "Invalid signature for pubkey {pubkey_bytes} on \
|
||||
bid from connected builder, falling back to local execution engine.");
|
||||
Ok(local)
|
||||
} else {
|
||||
if header.fee_recipient() != suggested_fee_recipient {
|
||||
info!(
|
||||
self.log(),
|
||||
"Fee recipient from connected builder does \
|
||||
not match, using it anyways."
|
||||
);
|
||||
}
|
||||
Ok(header)
|
||||
}
|
||||
}
|
||||
(relay_result, Err(local_error)) => {
|
||||
warn!(self.log(), "Failure from local execution engine. Attempting to \
|
||||
propose through connected builder"; "error" => ?local_error);
|
||||
relay_result
|
||||
.map_err(Error::Builder)?
|
||||
.ok_or(Error::NoHeaderFromBuilder)
|
||||
.map(|d| d.data.message.header)
|
||||
}
|
||||
};
|
||||
}
|
||||
ChainHealth::Unhealthy(condition) => {
|
||||
info!(self.log(), "Due to poor chain health the local execution engine will be used \
|
||||
for payload construction. To adjust chain health conditions \
|
||||
Use `builder-fallback` prefixed flags";
|
||||
"failed_condition" => ?condition)
|
||||
}
|
||||
// Intentional no-op, so we never attempt builder API proposals pre-merge.
|
||||
ChainHealth::PreMerge => (),
|
||||
}
|
||||
}
|
||||
self.get_full_payload::<Payload>(
|
||||
self.get_full_payload_caching(
|
||||
parent_hash,
|
||||
timestamp,
|
||||
prev_randao,
|
||||
@@ -593,6 +727,26 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get a full payload and cache its result in the execution layer's payload cache.
|
||||
async fn get_full_payload_caching<Payload: ExecPayload<T>>(
|
||||
&self,
|
||||
parent_hash: ExecutionBlockHash,
|
||||
timestamp: u64,
|
||||
prev_randao: Hash256,
|
||||
suggested_fee_recipient: Address,
|
||||
forkchoice_update_params: ForkchoiceUpdateParameters,
|
||||
) -> Result<Payload, Error> {
|
||||
self.get_full_payload_with(
|
||||
parent_hash,
|
||||
timestamp,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
forkchoice_update_params,
|
||||
Self::cache_payload,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_full_payload_with<Payload: ExecPayload<T>>(
|
||||
&self,
|
||||
parent_hash: ExecutionBlockHash,
|
||||
|
||||
Reference in New Issue
Block a user