Refactor & Fix Bugs in Payload Selection Logic (#4950)

* Refactor & Fix Bugs in Payload Selection Logic

* Fix lint

* Update beacon_node/execution_layer/src/lib.rs

* Finish renaming function
This commit is contained in:
ethDreamer 2023-11-28 22:20:12 -06:00 committed by GitHub
parent 86163d94f2
commit 43d98153d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -320,6 +320,7 @@ pub struct BuilderParams {
pub chain_health: ChainHealth,
}
#[derive(PartialEq)]
pub enum ChainHealth {
Healthy,
Unhealthy(FailedCondition),
@ -327,7 +328,7 @@ pub enum ChainHealth {
PreMerge,
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum FailedCondition {
Skips,
SkipsPerEpoch,
@ -928,6 +929,72 @@ impl<T: EthSpec> ExecutionLayer<T> {
}
}
/// Fetches local and builder paylaods concurrently, Logs and returns results.
async fn fetch_builder_and_local_payloads(
&self,
builder: &BuilderHttpClient,
parent_hash: ExecutionBlockHash,
builder_params: &BuilderParams,
payload_attributes: &PayloadAttributes,
forkchoice_update_params: ForkchoiceUpdateParameters,
current_fork: ForkName,
) -> (
Result<Option<ForkVersionedResponse<SignedBuilderBid<T>>>, builder_client::Error>,
Result<GetPayloadResponse<T>, Error>,
) {
let slot = builder_params.slot;
let pubkey = &builder_params.pubkey;
info!(
self.log(),
"Requesting blinded header from connected builder";
"slot" => ?slot,
"pubkey" => ?pubkey,
"parent_hash" => ?parent_hash,
);
// Wait for the builder *and* local EL to produce a payload (or return an error).
let ((relay_result, relay_duration), (local_result, local_duration)) = tokio::join!(
timed_future(metrics::GET_BLINDED_PAYLOAD_BUILDER, async {
builder
.get_builder_header::<T>(slot, parent_hash, pubkey)
.await
}),
timed_future(metrics::GET_BLINDED_PAYLOAD_LOCAL, async {
self.get_full_payload_caching(
parent_hash,
payload_attributes,
forkchoice_update_params,
current_fork,
)
.await
.and_then(|local_result_type| match local_result_type {
GetPayloadResponseType::Full(payload) => Ok(payload),
GetPayloadResponseType::Blinded(_) => Err(Error::PayloadTypeMismatch),
})
})
);
info!(
self.log(),
"Requested blinded execution payload";
"relay_fee_recipient" => match &relay_result {
Ok(Some(r)) => format!("{:?}", r.data.message.header().fee_recipient()),
Ok(None) => "empty response".to_string(),
Err(_) => "request failed".to_string(),
},
"relay_response_ms" => relay_duration.as_millis(),
"local_fee_recipient" => match &local_result {
Ok(get_payload_response) => format!("{:?}", get_payload_response.fee_recipient()),
Err(_) => "request failed".to_string()
},
"local_response_ms" => local_duration.as_millis(),
"parent_hash" => ?parent_hash,
);
(relay_result, local_result)
}
async fn determine_and_fetch_payload(
&self,
parent_hash: ExecutionBlockHash,
@ -937,250 +1004,24 @@ impl<T: EthSpec> ExecutionLayer<T> {
current_fork: ForkName,
spec: &ChainSpec,
) -> Result<ProvenancedPayload<BlockProposalContentsType<T>>, Error> {
if let Some(builder) = self.builder() {
let slot = builder_params.slot;
let pubkey = builder_params.pubkey;
let Some(builder) = self.builder() else {
// no builder.. return local payload
return self
.get_full_payload_caching(
parent_hash,
payload_attributes,
forkchoice_update_params,
current_fork,
)
.await
.and_then(GetPayloadResponseType::try_into)
.map(ProvenancedPayload::Local);
};
// check chain health
if builder_params.chain_health != ChainHealth::Healthy {
// chain is unhealthy, gotta use local payload
match builder_params.chain_health {
ChainHealth::Healthy => {
info!(
self.log(),
"Requesting blinded header from connected builder";
"slot" => ?slot,
"pubkey" => ?pubkey,
"parent_hash" => ?parent_hash,
);
// Wait for the builder *and* local EL to produce a payload (or return an error).
let ((relay_result, relay_duration), (local_result_type, local_duration)) = tokio::join!(
timed_future(metrics::GET_BLINDED_PAYLOAD_BUILDER, async {
builder
.get_builder_header::<T>(slot, parent_hash, &pubkey)
.await
}),
timed_future(metrics::GET_BLINDED_PAYLOAD_LOCAL, async {
self.get_full_payload_caching(
parent_hash,
payload_attributes,
forkchoice_update_params,
current_fork,
)
.await
})
);
let local_result = match local_result_type? {
GetPayloadResponseType::Full(payload) => Ok(payload),
GetPayloadResponseType::Blinded(_) => Err(Error::PayloadTypeMismatch),
};
info!(
self.log(),
"Requested blinded execution payload";
"relay_fee_recipient" => match &relay_result {
Ok(Some(r)) => format!("{:?}", r.data.message.header().fee_recipient()),
Ok(None) => "empty response".to_string(),
Err(_) => "request failed".to_string(),
},
"relay_response_ms" => relay_duration.as_millis(),
"local_fee_recipient" => match &local_result {
Ok(get_payload_response) => format!("{:?}", get_payload_response.fee_recipient()),
Err(_) => "request failed".to_string()
},
"local_response_ms" => local_duration.as_millis(),
"parent_hash" => ?parent_hash,
);
return match (relay_result, local_result) {
(Err(e), Ok(local)) => {
warn!(
self.log(),
"Builder error when requesting payload";
"info" => "falling back to local execution client",
"relay_error" => ?e,
"local_block_hash" => ?local.block_hash(),
"parent_hash" => ?parent_hash,
);
Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)))
}
(Ok(None), Ok(local)) => {
info!(
self.log(),
"Builder did not return a payload";
"info" => "falling back to local execution client",
"local_block_hash" => ?local.block_hash(),
"parent_hash" => ?parent_hash,
);
Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)))
}
(Ok(Some(relay)), Ok(local)) => {
let header = &relay.data.message.header();
info!(
self.log(),
"Received local and builder payloads";
"relay_block_hash" => ?header.block_hash(),
"local_block_hash" => ?local.block_hash(),
"parent_hash" => ?parent_hash,
);
let relay_value = relay.data.message.value();
let local_value = *local.block_value();
if !self.inner.always_prefer_builder_payload {
if local_value >= *relay_value {
info!(
self.log(),
"Local block is more profitable than relay block";
"local_block_value" => %local_value,
"relay_value" => %relay_value
);
return Ok(ProvenancedPayload::Local(
BlockProposalContentsType::Full(local.try_into()?),
));
} else if local.should_override_builder().unwrap_or(false) {
let percentage_difference =
percentage_difference_u256(local_value, *relay_value);
if percentage_difference.map_or(false, |percentage| {
percentage
< self
.inner
.ignore_builder_override_suggestion_threshold
}) {
info!(
self.log(),
"Using local payload because execution engine suggested we ignore builder payload";
"local_block_value" => %local_value,
"relay_value" => %relay_value
);
return Ok(ProvenancedPayload::Local(
BlockProposalContentsType::Full(local.try_into()?),
));
}
} else {
info!(
self.log(),
"Relay block is more profitable than local block";
"local_block_value" => %local_value,
"relay_value" => %relay_value
);
}
}
match verify_builder_bid(
&relay,
parent_hash,
payload_attributes,
Some(local.block_number()),
self.inner.builder_profit_threshold,
current_fork,
spec,
) {
Ok(()) => Ok(ProvenancedPayload::try_from(relay.data.message)?),
Err(reason) if !reason.payload_invalid() => {
info!(
self.log(),
"Builder payload ignored";
"info" => "using local payload",
"reason" => %reason,
"relay_block_hash" => ?header.block_hash(),
"parent_hash" => ?parent_hash,
);
Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)))
}
Err(reason) => {
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS,
&[reason.as_ref().as_ref()],
);
warn!(
self.log(),
"Builder returned invalid payload";
"info" => "using local payload",
"reason" => %reason,
"relay_block_hash" => ?header.block_hash(),
"parent_hash" => ?parent_hash,
);
Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)))
}
}
}
(Ok(Some(relay)), Err(local_error)) => {
let header = &relay.data.message.header();
info!(
self.log(),
"Received builder payload with local error";
"relay_block_hash" => ?header.block_hash(),
"local_error" => ?local_error,
"parent_hash" => ?parent_hash,
);
match verify_builder_bid(
&relay,
parent_hash,
payload_attributes,
None,
self.inner.builder_profit_threshold,
current_fork,
spec,
) {
Ok(()) => Ok(ProvenancedPayload::try_from(relay.data.message)?),
// If the payload is valid then use it. The local EE failed
// to produce a payload so we have no alternative.
Err(e) if !e.payload_invalid() => {
Ok(ProvenancedPayload::try_from(relay.data.message)?)
}
Err(reason) => {
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS,
&[reason.as_ref().as_ref()],
);
crit!(
self.log(),
"Builder returned invalid payload";
"info" => "no local payload either - unable to propose block",
"reason" => %reason,
"relay_block_hash" => ?header.block_hash(),
"parent_hash" => ?parent_hash,
);
Err(Error::CannotProduceHeader)
}
}
}
(Err(relay_error), Err(local_error)) => {
crit!(
self.log(),
"Unable to produce execution payload";
"info" => "the local EL and builder both failed - unable to propose block",
"relay_error" => ?relay_error,
"local_error" => ?local_error,
"parent_hash" => ?parent_hash,
);
Err(Error::CannotProduceHeader)
}
(Ok(None), Err(local_error)) => {
crit!(
self.log(),
"Unable to produce execution payload";
"info" => "the local EL failed and the builder returned nothing - \
the block proposal will be missed",
"local_error" => ?local_error,
"parent_hash" => ?parent_hash,
);
Err(Error::CannotProduceHeader)
}
};
}
ChainHealth::Unhealthy(condition) => info!(
self.log(),
"Chain is unhealthy, using local payload";
@ -1196,17 +1037,220 @@ impl<T: EthSpec> ExecutionLayer<T> {
"info" => "the local execution engine is syncing and the builder network \
cannot safely be used - unable to propose block"
),
ChainHealth::Healthy => crit!(
self.log(),
"got healthy but also not healthy.. this shouldn't happen!"
),
}
return self
.get_full_payload_caching(
parent_hash,
payload_attributes,
forkchoice_update_params,
current_fork,
)
.await
.and_then(GetPayloadResponseType::try_into)
.map(ProvenancedPayload::Local);
}
let (relay_result, local_result) = self
.fetch_builder_and_local_payloads(
builder.as_ref(),
parent_hash,
&builder_params,
payload_attributes,
forkchoice_update_params,
current_fork,
)
.await;
match (relay_result, local_result) {
(Err(e), Ok(local)) => {
warn!(
self.log(),
"Builder error when requesting payload";
"info" => "falling back to local execution client",
"relay_error" => ?e,
"local_block_hash" => ?local.block_hash(),
"parent_hash" => ?parent_hash,
);
Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)))
}
(Ok(None), Ok(local)) => {
info!(
self.log(),
"Builder did not return a payload";
"info" => "falling back to local execution client",
"local_block_hash" => ?local.block_hash(),
"parent_hash" => ?parent_hash,
);
Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)))
}
(Err(relay_error), Err(local_error)) => {
crit!(
self.log(),
"Unable to produce execution payload";
"info" => "the local EL and builder both failed - unable to propose block",
"relay_error" => ?relay_error,
"local_error" => ?local_error,
"parent_hash" => ?parent_hash,
);
Err(Error::CannotProduceHeader)
}
(Ok(None), Err(local_error)) => {
crit!(
self.log(),
"Unable to produce execution payload";
"info" => "the local EL failed and the builder returned nothing - \
the block proposal will be missed",
"local_error" => ?local_error,
"parent_hash" => ?parent_hash,
);
Err(Error::CannotProduceHeader)
}
(Ok(Some(relay)), Ok(local)) => {
let header = &relay.data.message.header();
info!(
self.log(),
"Received local and builder payloads";
"relay_block_hash" => ?header.block_hash(),
"local_block_hash" => ?local.block_hash(),
"parent_hash" => ?parent_hash,
);
// check relay payload validity
if let Err(reason) = verify_builder_bid(
&relay,
parent_hash,
payload_attributes,
Some(local.block_number()),
current_fork,
spec,
) {
// relay payload invalid -> return local
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS,
&[reason.as_ref().as_ref()],
);
warn!(
self.log(),
"Builder returned invalid payload";
"info" => "using local payload",
"reason" => %reason,
"relay_block_hash" => ?header.block_hash(),
"parent_hash" => ?parent_hash,
);
return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)));
}
if self.inner.always_prefer_builder_payload {
return ProvenancedPayload::try_from(relay.data.message);
}
let relay_value = *relay.data.message.value();
let local_value = *local.block_value();
if local_value >= relay_value {
info!(
self.log(),
"Local block is more profitable than relay block";
"local_block_value" => %local_value,
"relay_value" => %relay_value
);
return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)));
}
if relay_value < self.inner.builder_profit_threshold {
info!(
self.log(),
"Builder payload ignored";
"info" => "using local payload",
"reason" => format!("payload value of {} does not meet user-configured profit-threshold of {}", relay_value, self.inner.builder_profit_threshold),
"relay_block_hash" => ?header.block_hash(),
"parent_hash" => ?parent_hash,
);
return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)));
}
if local.should_override_builder().unwrap_or(false) {
let percentage_difference =
percentage_difference_u256(local_value, relay_value);
if percentage_difference.map_or(false, |percentage| {
percentage < self.inner.ignore_builder_override_suggestion_threshold
}) {
info!(
self.log(),
"Using local payload because execution engine suggested we ignore builder payload";
"local_block_value" => %local_value,
"relay_value" => %relay_value
);
return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full(
local.try_into()?,
)));
}
}
info!(
self.log(),
"Relay block is more profitable than local block";
"local_block_value" => %local_value,
"relay_value" => %relay_value
);
Ok(ProvenancedPayload::try_from(relay.data.message)?)
}
(Ok(Some(relay)), Err(local_error)) => {
let header = &relay.data.message.header();
info!(
self.log(),
"Received builder payload with local error";
"relay_block_hash" => ?header.block_hash(),
"local_error" => ?local_error,
"parent_hash" => ?parent_hash,
);
match verify_builder_bid(
&relay,
parent_hash,
payload_attributes,
None,
current_fork,
spec,
) {
Ok(()) => Ok(ProvenancedPayload::try_from(relay.data.message)?),
Err(reason) => {
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS,
&[reason.as_ref().as_ref()],
);
crit!(
self.log(),
"Builder returned invalid payload";
"info" => "no local payload either - unable to propose block",
"reason" => %reason,
"relay_block_hash" => ?header.block_hash(),
"parent_hash" => ?parent_hash,
);
Err(Error::CannotProduceHeader)
}
}
}
}
self.get_full_payload_caching(
parent_hash,
payload_attributes,
forkchoice_update_params,
current_fork,
)
.await
.and_then(GetPayloadResponseType::try_into)
.map(ProvenancedPayload::Local)
}
/// Get a full payload and cache its result in the execution layer's payload cache.
@ -2027,10 +2071,6 @@ impl<T: EthSpec> ExecutionLayer<T> {
#[derive(AsRefStr)]
#[strum(serialize_all = "snake_case")]
enum InvalidBuilderPayload {
LowValue {
profit_threshold: Uint256,
payload_value: Uint256,
},
ParentHash {
payload: ExecutionBlockHash,
expected: ExecutionBlockHash,
@ -2061,34 +2101,9 @@ enum InvalidBuilderPayload {
},
}
impl InvalidBuilderPayload {
/// Returns `true` if a payload is objectively invalid and should never be included on chain.
fn payload_invalid(&self) -> bool {
match self {
// A low-value payload isn't invalid, it should just be avoided if possible.
InvalidBuilderPayload::LowValue { .. } => false,
InvalidBuilderPayload::ParentHash { .. } => true,
InvalidBuilderPayload::PrevRandao { .. } => true,
InvalidBuilderPayload::Timestamp { .. } => true,
InvalidBuilderPayload::BlockNumber { .. } => true,
InvalidBuilderPayload::Fork { .. } => true,
InvalidBuilderPayload::Signature { .. } => true,
InvalidBuilderPayload::WithdrawalsRoot { .. } => true,
}
}
}
impl fmt::Display for InvalidBuilderPayload {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InvalidBuilderPayload::LowValue {
profit_threshold,
payload_value,
} => write!(
f,
"payload value of {} does not meet user-configured profit-threshold of {}",
payload_value, profit_threshold
),
InvalidBuilderPayload::ParentHash { payload, expected } => {
write!(f, "payload block hash was {} not {}", payload, expected)
}
@ -2132,13 +2147,11 @@ fn verify_builder_bid<T: EthSpec>(
parent_hash: ExecutionBlockHash,
payload_attributes: &PayloadAttributes,
block_number: Option<u64>,
profit_threshold: Uint256,
current_fork: ForkName,
spec: &ChainSpec,
) -> Result<(), Box<InvalidBuilderPayload>> {
let is_signature_valid = bid.data.verify_signature(spec);
let header = &bid.data.message.header();
let payload_value = bid.data.message.value();
// Avoid logging values that we can't represent with our Prometheus library.
let payload_value_gwei = bid.data.message.value() / 1_000_000_000;
@ -2157,12 +2170,7 @@ fn verify_builder_bid<T: EthSpec>(
.map(|withdrawals| Withdrawals::<T>::from(withdrawals).tree_hash_root());
let payload_withdrawals_root = header.withdrawals_root().ok().copied();
if *payload_value < profit_threshold {
Err(Box::new(InvalidBuilderPayload::LowValue {
profit_threshold,
payload_value: *payload_value,
}))
} else if header.parent_hash() != parent_hash {
if header.parent_hash() != parent_hash {
Err(Box::new(InvalidBuilderPayload::ParentHash {
payload: header.parent_hash(),
expected: parent_hash,