Use Local Payload if More Profitable than Builder (#3934)

* Use Local Payload if More Profitable than Builder

* Rename clone -> clone_from_ref

* Minimize Clones of GetPayloadResponse

* Cleanup & Fix Tests

* Added Tests for Payload Choice by Profit

* Fix Outdated Comments
This commit is contained in:
ethDreamer 2023-02-01 19:37:46 -06:00 committed by GitHub
parent 4d0955cd0b
commit 90b6ae62e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 344 additions and 69 deletions

View File

@ -426,6 +426,7 @@ where
DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_BLOCK,
shanghai_time, shanghai_time,
eip4844_time, eip4844_time,
None,
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
spec, spec,
None, None,
@ -435,7 +436,11 @@ where
self self
} }
pub fn mock_execution_layer_with_builder(mut self, beacon_url: SensitiveUrl) -> Self { pub fn mock_execution_layer_with_builder(
mut self,
beacon_url: SensitiveUrl,
builder_threshold: Option<u128>,
) -> Self {
// Get a random unused port // Get a random unused port
let port = unused_port::unused_tcp_port().unwrap(); let port = unused_port::unused_tcp_port().unwrap();
let builder_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(); let builder_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
@ -452,6 +457,7 @@ where
DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_BLOCK,
shanghai_time, shanghai_time,
eip4844_time, eip4844_time,
builder_threshold,
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
spec.clone(), spec.clone(),
Some(builder_url.clone()), Some(builder_url.clone()),

View File

@ -14,8 +14,8 @@ use std::convert::TryFrom;
use strum::IntoStaticStr; use strum::IntoStaticStr;
use superstruct::superstruct; use superstruct::superstruct;
pub use types::{ pub use types::{
Address, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader, FixedVector, Address, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader,
ForkName, Hash256, Uint256, VariableList, Withdrawal, ExecutionPayloadRef, FixedVector, ForkName, Hash256, Uint256, VariableList, Withdrawal,
}; };
use types::{ExecutionPayloadCapella, ExecutionPayloadEip4844, ExecutionPayloadMerge}; use types::{ExecutionPayloadCapella, ExecutionPayloadEip4844, ExecutionPayloadMerge};
@ -322,6 +322,8 @@ pub struct ProposeBlindedBlockResponse {
#[superstruct( #[superstruct(
variants(Merge, Capella, Eip4844), variants(Merge, Capella, Eip4844),
variant_attributes(derive(Clone, Debug, PartialEq),), variant_attributes(derive(Clone, Debug, PartialEq),),
map_into(ExecutionPayload),
map_ref_into(ExecutionPayloadRef),
cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"),
partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant")
)] )]
@ -336,22 +338,47 @@ pub struct GetPayloadResponse<T: EthSpec> {
pub block_value: Uint256, pub block_value: Uint256,
} }
impl<T: EthSpec> GetPayloadResponse<T> { impl<'a, T: EthSpec> From<GetPayloadResponseRef<'a, T>> for ExecutionPayloadRef<'a, T> {
pub fn execution_payload(self) -> ExecutionPayload<T> { fn from(response: GetPayloadResponseRef<'a, T>) -> Self {
match self { map_get_payload_response_ref_into_execution_payload_ref!(&'a _, response, |inner, cons| {
GetPayloadResponse::Merge(response) => { cons(&inner.execution_payload)
ExecutionPayload::Merge(response.execution_payload) })
} }
GetPayloadResponse::Capella(response) => { }
ExecutionPayload::Capella(response.execution_payload)
} impl<T: EthSpec> From<GetPayloadResponse<T>> for ExecutionPayload<T> {
GetPayloadResponse::Eip4844(response) => { fn from(response: GetPayloadResponse<T>) -> Self {
ExecutionPayload::Eip4844(response.execution_payload) map_get_payload_response_into_execution_payload!(response, |inner, cons| {
} cons(inner.execution_payload)
})
}
}
impl<T: EthSpec> From<GetPayloadResponse<T>> for (ExecutionPayload<T>, Uint256) {
fn from(response: GetPayloadResponse<T>) -> Self {
match response {
GetPayloadResponse::Merge(inner) => (
ExecutionPayload::Merge(inner.execution_payload),
inner.block_value,
),
GetPayloadResponse::Capella(inner) => (
ExecutionPayload::Capella(inner.execution_payload),
inner.block_value,
),
GetPayloadResponse::Eip4844(inner) => (
ExecutionPayload::Eip4844(inner.execution_payload),
inner.block_value,
),
} }
} }
} }
impl<T: EthSpec> GetPayloadResponse<T> {
pub fn execution_payload_ref(&self) -> ExecutionPayloadRef<T> {
self.to_ref().into()
}
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct EngineCapabilities { pub struct EngineCapabilities {
pub new_payload_v1: bool, pub new_payload_v1: bool,

View File

@ -804,7 +804,7 @@ impl HttpJsonRpc {
pub async fn get_payload_v1<T: EthSpec>( pub async fn get_payload_v1<T: EthSpec>(
&self, &self,
payload_id: PayloadId, payload_id: PayloadId,
) -> Result<ExecutionPayload<T>, Error> { ) -> Result<GetPayloadResponse<T>, Error> {
let params = json!([JsonPayloadIdRequest::from(payload_id)]); let params = json!([JsonPayloadIdRequest::from(payload_id)]);
let payload_v1: JsonExecutionPayloadV1<T> = self let payload_v1: JsonExecutionPayloadV1<T> = self
@ -815,7 +815,11 @@ impl HttpJsonRpc {
) )
.await?; .await?;
Ok(JsonExecutionPayload::V1(payload_v1).into()) Ok(GetPayloadResponse::Merge(GetPayloadResponseMerge {
execution_payload: payload_v1.into(),
// Have to guess zero here as we don't know the value
block_value: Uint256::zero(),
}))
} }
pub async fn get_payload_v2<T: EthSpec>( pub async fn get_payload_v2<T: EthSpec>(
@ -1015,16 +1019,10 @@ impl HttpJsonRpc {
&self, &self,
fork_name: ForkName, fork_name: ForkName,
payload_id: PayloadId, payload_id: PayloadId,
) -> Result<ExecutionPayload<T>, Error> { ) -> Result<GetPayloadResponse<T>, Error> {
let engine_capabilities = self.get_engine_capabilities(None).await?; let engine_capabilities = self.get_engine_capabilities(None).await?;
if engine_capabilities.get_payload_v2 { if engine_capabilities.get_payload_v2 {
// TODO: modify this method to return GetPayloadResponse instead self.get_payload_v2(fork_name, payload_id).await
// of throwing away the `block_value` and returning only the
// ExecutionPayload
Ok(self
.get_payload_v2(fork_name, payload_id)
.await?
.execution_payload())
} else if engine_capabilities.new_payload_v1 { } else if engine_capabilities.new_payload_v1 {
self.get_payload_v1(payload_id).await self.get_payload_v1(payload_id).await
} else { } else {
@ -1675,10 +1673,11 @@ mod test {
} }
})], })],
|client| async move { |client| async move {
let payload = client let payload: ExecutionPayload<_> = client
.get_payload_v1::<MainnetEthSpec>(str_to_payload_id("0xa247243752eb10b4")) .get_payload_v1::<MainnetEthSpec>(str_to_payload_id("0xa247243752eb10b4"))
.await .await
.unwrap(); .unwrap()
.into();
let expected = ExecutionPayload::Merge(ExecutionPayloadMerge { let expected = ExecutionPayload::Merge(ExecutionPayloadMerge {
parent_hash: ExecutionBlockHash::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a").unwrap(), parent_hash: ExecutionBlockHash::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a").unwrap(),

View File

@ -119,9 +119,13 @@ impl From<ApiError> for Error {
} }
pub enum BlockProposalContents<T: EthSpec, Payload: AbstractExecPayload<T>> { pub enum BlockProposalContents<T: EthSpec, Payload: AbstractExecPayload<T>> {
Payload(Payload), Payload {
payload: Payload,
block_value: Uint256,
},
PayloadAndBlobs { PayloadAndBlobs {
payload: Payload, payload: Payload,
block_value: Uint256,
kzg_commitments: Vec<KzgCommitment>, kzg_commitments: Vec<KzgCommitment>,
blobs: Vec<Blob<T>>, blobs: Vec<Blob<T>>,
}, },
@ -130,9 +134,13 @@ pub enum BlockProposalContents<T: EthSpec, Payload: AbstractExecPayload<T>> {
impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Payload> { impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Payload> {
pub fn payload(&self) -> &Payload { pub fn payload(&self) -> &Payload {
match self { match self {
Self::Payload(payload) => payload, Self::Payload {
payload,
block_value: _,
} => payload,
Self::PayloadAndBlobs { Self::PayloadAndBlobs {
payload, payload,
block_value: _,
kzg_commitments: _, kzg_commitments: _,
blobs: _, blobs: _,
} => payload, } => payload,
@ -140,9 +148,13 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
} }
pub fn to_payload(self) -> Payload { pub fn to_payload(self) -> Payload {
match self { match self {
Self::Payload(payload) => payload, Self::Payload {
payload,
block_value: _,
} => payload,
Self::PayloadAndBlobs { Self::PayloadAndBlobs {
payload, payload,
block_value: _,
kzg_commitments: _, kzg_commitments: _,
blobs: _, blobs: _,
} => payload, } => payload,
@ -150,9 +162,13 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
} }
pub fn kzg_commitments(&self) -> Option<&[KzgCommitment]> { pub fn kzg_commitments(&self) -> Option<&[KzgCommitment]> {
match self { match self {
Self::Payload(_) => None, Self::Payload {
payload: _,
block_value: _,
} => None,
Self::PayloadAndBlobs { Self::PayloadAndBlobs {
payload: _, payload: _,
block_value: _,
kzg_commitments, kzg_commitments,
blobs: _, blobs: _,
} => Some(kzg_commitments), } => Some(kzg_commitments),
@ -160,21 +176,43 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
} }
pub fn blobs(&self) -> Option<&[Blob<T>]> { pub fn blobs(&self) -> Option<&[Blob<T>]> {
match self { match self {
Self::Payload(_) => None, Self::Payload {
payload: _,
block_value: _,
} => None,
Self::PayloadAndBlobs { Self::PayloadAndBlobs {
payload: _, payload: _,
block_value: _,
kzg_commitments: _, kzg_commitments: _,
blobs, blobs,
} => Some(blobs), } => Some(blobs),
} }
} }
pub fn block_value(&self) -> &Uint256 {
match self {
Self::Payload {
payload: _,
block_value,
} => block_value,
Self::PayloadAndBlobs {
payload: _,
block_value,
kzg_commitments: _,
blobs: _,
} => block_value,
}
}
pub fn default_at_fork(fork_name: ForkName) -> Result<Self, BeaconStateError> { pub fn default_at_fork(fork_name: ForkName) -> Result<Self, BeaconStateError> {
Ok(match fork_name { Ok(match fork_name {
ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => {
BlockProposalContents::Payload(Payload::default_at_fork(fork_name)?) BlockProposalContents::Payload {
payload: Payload::default_at_fork(fork_name)?,
block_value: Uint256::zero(),
}
} }
ForkName::Eip4844 => BlockProposalContents::PayloadAndBlobs { ForkName::Eip4844 => BlockProposalContents::PayloadAndBlobs {
payload: Payload::default_at_fork(fork_name)?, payload: Payload::default_at_fork(fork_name)?,
block_value: Uint256::zero(),
blobs: vec![], blobs: vec![],
kzg_commitments: vec![], kzg_commitments: vec![],
}, },
@ -366,12 +404,12 @@ impl<T: EthSpec> ExecutionLayer<T> {
&self.inner.builder &self.inner.builder
} }
/// Cache a full payload, keyed on the `tree_hash_root` of its `transactions` field. /// Cache a full payload, keyed on the `tree_hash_root` of the payload
fn cache_payload(&self, payload: &ExecutionPayload<T>) -> Option<ExecutionPayload<T>> { fn cache_payload(&self, payload: ExecutionPayloadRef<T>) -> Option<ExecutionPayload<T>> {
self.inner.payload_cache.put(payload.clone()) self.inner.payload_cache.put(payload.clone_from_ref())
} }
/// Attempt to retrieve a full payload from the payload cache by the `transactions_root`. /// Attempt to retrieve a full payload from the payload cache by the payload root
pub fn get_payload_by_root(&self, root: &Hash256) -> Option<ExecutionPayload<T>> { pub fn get_payload_by_root(&self, root: &Hash256) -> Option<ExecutionPayload<T>> {
self.inner.payload_cache.pop(root) self.inner.payload_cache.pop(root)
} }
@ -808,6 +846,18 @@ impl<T: EthSpec> ExecutionLayer<T> {
"parent_hash" => ?parent_hash, "parent_hash" => ?parent_hash,
); );
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(local));
}
match verify_builder_bid( match verify_builder_bid(
&relay, &relay,
parent_hash, parent_hash,
@ -818,7 +868,10 @@ impl<T: EthSpec> ExecutionLayer<T> {
spec, spec,
) { ) {
Ok(()) => Ok(ProvenancedPayload::Builder( Ok(()) => Ok(ProvenancedPayload::Builder(
BlockProposalContents::Payload(relay.data.message.header), BlockProposalContents::Payload {
payload: relay.data.message.header,
block_value: relay.data.message.value,
},
)), )),
Err(reason) if !reason.payload_invalid() => { Err(reason) if !reason.payload_invalid() => {
info!( info!(
@ -869,12 +922,18 @@ impl<T: EthSpec> ExecutionLayer<T> {
spec, spec,
) { ) {
Ok(()) => Ok(ProvenancedPayload::Builder( Ok(()) => Ok(ProvenancedPayload::Builder(
BlockProposalContents::Payload(relay.data.message.header), BlockProposalContents::Payload {
payload: relay.data.message.header,
block_value: relay.data.message.value,
},
)), )),
// If the payload is valid then use it. The local EE failed // If the payload is valid then use it. The local EE failed
// to produce a payload so we have no alternative. // to produce a payload so we have no alternative.
Err(e) if !e.payload_invalid() => Ok(ProvenancedPayload::Builder( Err(e) if !e.payload_invalid() => Ok(ProvenancedPayload::Builder(
BlockProposalContents::Payload(relay.data.message.header), BlockProposalContents::Payload {
payload: relay.data.message.header,
block_value: relay.data.message.value,
},
)), )),
Err(reason) => { Err(reason) => {
metrics::inc_counter_vec( metrics::inc_counter_vec(
@ -988,7 +1047,7 @@ impl<T: EthSpec> ExecutionLayer<T> {
payload_attributes: &PayloadAttributes, payload_attributes: &PayloadAttributes,
forkchoice_update_params: ForkchoiceUpdateParameters, forkchoice_update_params: ForkchoiceUpdateParameters,
current_fork: ForkName, current_fork: ForkName,
f: fn(&ExecutionLayer<T>, &ExecutionPayload<T>) -> Option<ExecutionPayload<T>>, f: fn(&ExecutionLayer<T>, ExecutionPayloadRef<T>) -> Option<ExecutionPayload<T>>,
) -> Result<BlockProposalContents<T, Payload>, Error> { ) -> Result<BlockProposalContents<T, Payload>, Error> {
self.engine() self.engine()
.request(move |engine| async move { .request(move |engine| async move {
@ -1071,9 +1130,9 @@ impl<T: EthSpec> ExecutionLayer<T> {
); );
engine.api.get_payload::<T>(current_fork, payload_id).await engine.api.get_payload::<T>(current_fork, payload_id).await
}; };
let (blob, payload) = tokio::join!(blob_fut, payload_fut); let (blob, payload_response) = tokio::join!(blob_fut, payload_fut);
let payload = payload.map(|full_payload| { let (execution_payload, block_value) = payload_response.map(|payload_response| {
if full_payload.fee_recipient() != payload_attributes.suggested_fee_recipient() { if payload_response.execution_payload_ref().fee_recipient() != payload_attributes.suggested_fee_recipient() {
error!( error!(
self.log(), self.log(),
"Inconsistent fee recipient"; "Inconsistent fee recipient";
@ -1082,28 +1141,32 @@ impl<T: EthSpec> ExecutionLayer<T> {
indicate that fees are being diverted to another address. Please \ indicate that fees are being diverted to another address. Please \
ensure that the value of suggested_fee_recipient is set correctly and \ ensure that the value of suggested_fee_recipient is set correctly and \
that the Execution Engine is trusted.", that the Execution Engine is trusted.",
"fee_recipient" => ?full_payload.fee_recipient(), "fee_recipient" => ?payload_response.execution_payload_ref().fee_recipient(),
"suggested_fee_recipient" => ?payload_attributes.suggested_fee_recipient(), "suggested_fee_recipient" => ?payload_attributes.suggested_fee_recipient(),
); );
} }
if f(self, &full_payload).is_some() { if f(self, payload_response.execution_payload_ref()).is_some() {
warn!( warn!(
self.log(), self.log(),
"Duplicate payload cached, this might indicate redundant proposal \ "Duplicate payload cached, this might indicate redundant proposal \
attempts." attempts."
); );
} }
full_payload.into() payload_response.into()
})?; })?;
if let Some(blob) = blob.transpose()? { if let Some(blob) = blob.transpose()? {
// FIXME(sean) cache blobs // FIXME(sean) cache blobs
Ok(BlockProposalContents::PayloadAndBlobs { Ok(BlockProposalContents::PayloadAndBlobs {
payload, payload: execution_payload.into(),
block_value,
blobs: blob.blobs, blobs: blob.blobs,
kzg_commitments: blob.kzgs, kzg_commitments: blob.kzgs,
}) })
} else { } else {
Ok(BlockProposalContents::Payload(payload)) Ok(BlockProposalContents::Payload {
payload: execution_payload.into(),
block_value,
})
} }
}) })
.await .await
@ -2089,7 +2152,10 @@ mod test {
} }
} }
fn noop<T: EthSpec>(_: &ExecutionLayer<T>, _: &ExecutionPayload<T>) -> Option<ExecutionPayload<T>> { fn noop<T: EthSpec>(
_: &ExecutionLayer<T>,
_: ExecutionPayloadRef<T>,
) -> Option<ExecutionPayload<T>> {
None None
} }

View File

@ -1,6 +1,7 @@
use super::Context; use super::Context;
use crate::engine_api::{http::*, *}; use crate::engine_api::{http::*, *};
use crate::json_structures::*; use crate::json_structures::*;
use crate::test_utils::DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use std::sync::Arc; use std::sync::Arc;
@ -211,14 +212,14 @@ pub async fn handle_rpc<T: EthSpec>(
JsonExecutionPayload::V1(execution_payload) => { JsonExecutionPayload::V1(execution_payload) => {
serde_json::to_value(JsonGetPayloadResponseV1 { serde_json::to_value(JsonGetPayloadResponseV1 {
execution_payload, execution_payload,
block_value: 0.into(), block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into(),
}) })
.unwrap() .unwrap()
} }
JsonExecutionPayload::V2(execution_payload) => { JsonExecutionPayload::V2(execution_payload) => {
serde_json::to_value(JsonGetPayloadResponseV2 { serde_json::to_value(JsonGetPayloadResponseV2 {
execution_payload, execution_payload,
block_value: 0.into(), block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into(),
}) })
.unwrap() .unwrap()
} }

View File

@ -1,4 +1,4 @@
use crate::test_utils::DEFAULT_JWT_SECRET; use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET};
use crate::{Config, ExecutionLayer, PayloadAttributes}; use crate::{Config, ExecutionLayer, PayloadAttributes};
use async_trait::async_trait; use async_trait::async_trait;
use eth2::types::{BlockId, StateId, ValidatorId}; use eth2::types::{BlockId, StateId, ValidatorId};
@ -328,7 +328,7 @@ impl<E: EthSpec> mev_build_rs::BlindedBlockProvider for MockBuilder<E> {
let mut message = BuilderBid { let mut message = BuilderBid {
header, header,
value: ssz_rs::U256::default(), value: to_ssz_rs(&Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI))?,
public_key: self.builder_sk.public_key(), public_key: self.builder_sk.public_key(),
}; };

View File

@ -29,6 +29,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_BLOCK,
None, None,
None, None,
None,
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
spec, spec,
None, None,
@ -41,6 +42,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
terminal_block: u64, terminal_block: u64,
shanghai_time: Option<u64>, shanghai_time: Option<u64>,
eip4844_time: Option<u64>, eip4844_time: Option<u64>,
builder_threshold: Option<u128>,
jwt_key: Option<JwtKey>, jwt_key: Option<JwtKey>,
spec: ChainSpec, spec: ChainSpec,
builder_url: Option<SensitiveUrl>, builder_url: Option<SensitiveUrl>,
@ -69,7 +71,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
builder_url, builder_url,
secret_files: vec![path], secret_files: vec![path],
suggested_fee_recipient: Some(Address::repeat_byte(42)), suggested_fee_recipient: Some(Address::repeat_byte(42)),
builder_profit_threshold: DEFAULT_BUILDER_THRESHOLD_WEI, builder_profit_threshold: builder_threshold.unwrap_or(DEFAULT_BUILDER_THRESHOLD_WEI),
..Default::default() ..Default::default()
}; };
let el = let el =

View File

@ -32,6 +32,8 @@ pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400;
pub const DEFAULT_TERMINAL_BLOCK: u64 = 64; pub const DEFAULT_TERMINAL_BLOCK: u64 = 64;
pub const DEFAULT_JWT_SECRET: [u8; 32] = [42; 32]; pub const DEFAULT_JWT_SECRET: [u8; 32] = [42; 32];
pub const DEFAULT_BUILDER_THRESHOLD_WEI: u128 = 1_000_000_000_000_000_000; pub const DEFAULT_BUILDER_THRESHOLD_WEI: u128 = 1_000_000_000_000_000_000;
pub const DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI: u128 = 10_000_000_000_000_000;
pub const DEFAULT_BUILDER_PAYLOAD_VALUE_WEI: u128 = 20_000_000_000_000_000;
pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities {
new_payload_v1: true, new_payload_v1: true,
new_payload_v2: true, new_payload_v2: true,

View File

@ -11,9 +11,11 @@ use eth2::{
types::{BlockId as CoreBlockId, StateId as CoreStateId, *}, types::{BlockId as CoreBlockId, StateId as CoreStateId, *},
BeaconNodeHttpClient, Error, StatusCode, Timeouts, BeaconNodeHttpClient, Error, StatusCode, Timeouts,
}; };
use execution_layer::test_utils::Operation;
use execution_layer::test_utils::TestingBuilder; use execution_layer::test_utils::TestingBuilder;
use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI; use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI;
use execution_layer::test_utils::{
Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI,
};
use futures::stream::{Stream, StreamExt}; use futures::stream::{Stream, StreamExt};
use futures::FutureExt; use futures::FutureExt;
use http_api::{BlockId, StateId}; use http_api::{BlockId, StateId};
@ -72,38 +74,53 @@ struct ApiTester {
mock_builder: Option<Arc<TestingBuilder<E>>>, mock_builder: Option<Arc<TestingBuilder<E>>>,
} }
struct ApiTesterConfig {
spec: ChainSpec,
builder_threshold: Option<u128>,
}
impl Default for ApiTesterConfig {
fn default() -> Self {
let mut spec = E::default_spec();
spec.shard_committee_period = 2;
Self {
spec,
builder_threshold: None,
}
}
}
impl ApiTester { impl ApiTester {
pub async fn new() -> Self { pub async fn new() -> Self {
// This allows for testing voluntary exits without building out a massive chain. // This allows for testing voluntary exits without building out a massive chain.
let mut spec = E::default_spec(); Self::new_from_config(ApiTesterConfig::default()).await
spec.shard_committee_period = 2;
Self::new_from_spec(spec).await
} }
pub async fn new_with_hard_forks(altair: bool, bellatrix: bool) -> Self { pub async fn new_with_hard_forks(altair: bool, bellatrix: bool) -> Self {
let mut spec = E::default_spec(); let mut config = ApiTesterConfig::default();
spec.shard_committee_period = 2;
// Set whether the chain has undergone each hard fork. // Set whether the chain has undergone each hard fork.
if altair { if altair {
spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.altair_fork_epoch = Some(Epoch::new(0));
} }
if bellatrix { if bellatrix {
spec.bellatrix_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
} }
Self::new_from_spec(spec).await Self::new_from_config(config).await
} }
pub async fn new_from_spec(spec: ChainSpec) -> Self { pub async fn new_from_config(config: ApiTesterConfig) -> Self {
// Get a random unused port // Get a random unused port
let spec = config.spec;
let port = unused_port::unused_tcp_port().unwrap(); let port = unused_port::unused_tcp_port().unwrap();
let beacon_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(); let beacon_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
let harness = Arc::new( let harness = Arc::new(
BeaconChainHarness::builder(MainnetEthSpec) BeaconChainHarness::builder(MainnetEthSpec)
.spec(spec.clone()) .spec(spec.clone())
.logger(logging::test_logger())
.deterministic_keypairs(VALIDATOR_COUNT) .deterministic_keypairs(VALIDATOR_COUNT)
.fresh_ephemeral_store() .fresh_ephemeral_store()
.mock_execution_layer_with_builder(beacon_url.clone()) .mock_execution_layer_with_builder(beacon_url.clone(), config.builder_threshold)
.build(), .build(),
); );
@ -358,6 +375,28 @@ impl ApiTester {
tester tester
} }
pub async fn new_mev_tester_no_builder_threshold() -> Self {
let mut config = ApiTesterConfig {
builder_threshold: Some(0),
spec: E::default_spec(),
};
config.spec.altair_fork_epoch = Some(Epoch::new(0));
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
let tester = Self::new_from_config(config)
.await
.test_post_validator_register_validator()
.await;
tester
.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_BUILDER_PAYLOAD_VALUE_WEI,
)));
tester
}
fn skip_slots(self, count: u64) -> Self { fn skip_slots(self, count: u64) -> Self {
for _ in 0..count { for _ in 0..count {
self.chain self.chain
@ -3278,6 +3317,117 @@ impl ApiTester {
self self
} }
pub async fn test_builder_payload_chosen_when_more_profitable(self) -> Self {
// Mutate value.
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1,
)));
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.data
.body()
.execution_payload()
.unwrap()
.into();
// The builder's payload should've been chosen, so this cache should not be populated
assert!(self
.chain
.execution_layer
.as_ref()
.unwrap()
.get_payload_by_root(&payload.tree_hash_root())
.is_none());
self
}
pub async fn test_local_payload_chosen_when_equally_profitable(self) -> Self {
// Mutate value.
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI,
)));
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.data
.body()
.execution_payload()
.unwrap()
.into();
// The local payload should've been chosen, so this cache should be populated
assert!(self
.chain
.execution_layer
.as_ref()
.unwrap()
.get_payload_by_root(&payload.tree_hash_root())
.is_some());
self
}
pub async fn test_local_payload_chosen_when_more_profitable(self) -> Self {
// Mutate value.
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI - 1,
)));
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.data
.body()
.execution_payload()
.unwrap()
.into();
// The local payload should've been chosen, so this cache should be populated
assert!(self
.chain
.execution_layer
.as_ref()
.unwrap()
.get_payload_by_root(&payload.tree_hash_root())
.is_some());
self
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn test_get_lighthouse_health(self) -> Self { pub async fn test_get_lighthouse_health(self) -> Self {
self.client.get_lighthouse_health().await.unwrap(); self.client.get_lighthouse_health().await.unwrap();
@ -3747,9 +3897,9 @@ async fn get_events() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_events_altair() { async fn get_events_altair() {
let mut spec = E::default_spec(); let mut config = ApiTesterConfig::default();
spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.altair_fork_epoch = Some(Epoch::new(0));
ApiTester::new_from_spec(spec) ApiTester::new_from_config(config)
.await .await
.test_get_events_altair() .test_get_events_altair()
.await; .await;
@ -4262,6 +4412,18 @@ async fn builder_inadequate_builder_threshold() {
.await; .await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn builder_payload_chosen_by_profit() {
ApiTester::new_mev_tester_no_builder_threshold()
.await
.test_builder_payload_chosen_when_more_profitable()
.await
.test_local_payload_chosen_when_equally_profitable()
.await
.test_local_payload_chosen_when_more_profitable()
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lighthouse_endpoints() { async fn lighthouse_endpoints() {
ApiTester::new() ApiTester::new()

View File

@ -87,6 +87,16 @@ pub struct ExecutionPayload<T: EthSpec> {
pub withdrawals: Withdrawals<T>, pub withdrawals: Withdrawals<T>,
} }
impl<'a, T: EthSpec> ExecutionPayloadRef<'a, T> {
// this emulates clone on a normal reference type
pub fn clone_from_ref(&self) -> ExecutionPayload<T> {
map_execution_payload_ref!(&'a _, self, move |payload, cons| {
cons(payload);
payload.clone().into()
})
}
}
impl<T: EthSpec> ExecutionPayload<T> { impl<T: EthSpec> ExecutionPayload<T> {
pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result<Self, ssz::DecodeError> { pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result<Self, ssz::DecodeError> {
match fork_name { match fork_name {