Merge branch 'capella' of https://github.com/sigp/lighthouse into eip4844

# Conflicts:
#	beacon_node/beacon_chain/src/beacon_chain.rs
#	beacon_node/beacon_chain/src/block_verification.rs
#	beacon_node/beacon_chain/src/test_utils.rs
#	beacon_node/execution_layer/src/engine_api.rs
#	beacon_node/execution_layer/src/engine_api/http.rs
#	beacon_node/execution_layer/src/lib.rs
#	beacon_node/execution_layer/src/test_utils/handle_rpc.rs
#	beacon_node/http_api/src/lib.rs
#	beacon_node/http_api/tests/fork_tests.rs
#	beacon_node/network/src/beacon_processor/mod.rs
#	beacon_node/network/src/beacon_processor/work_reprocessing_queue.rs
#	beacon_node/network/src/beacon_processor/worker/sync_methods.rs
#	beacon_node/operation_pool/src/bls_to_execution_changes.rs
#	beacon_node/operation_pool/src/lib.rs
#	beacon_node/operation_pool/src/persistence.rs
#	consensus/serde_utils/src/u256_hex_be_opt.rs
#	testing/antithesis/Dockerfile.libvoidstar
This commit is contained in:
realbigsean
2023-02-07 12:12:56 -05:00
53 changed files with 1680 additions and 560 deletions

View File

@@ -1,4 +1,9 @@
use crate::engines::ForkchoiceState;
use crate::http::{
ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1, ENGINE_FORKCHOICE_UPDATED_V1,
ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2,
ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2,
};
use crate::BlobTxConversionError;
pub use ethers_core::types::Transaction;
use ethers_core::utils::rlp::{self, Decodable, Rlp};
@@ -10,8 +15,8 @@ use std::convert::TryFrom;
use strum::IntoStaticStr;
use superstruct::superstruct;
pub use types::{
Address, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader, FixedVector,
ForkName, Hash256, Uint256, VariableList, Withdrawal,
Address, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader,
ExecutionPayloadRef, FixedVector, ForkName, Hash256, Uint256, VariableList, Withdrawal,
};
use types::{ExecutionPayloadCapella, ExecutionPayloadEip4844, ExecutionPayloadMerge};
@@ -330,6 +335,8 @@ pub struct ProposeBlindedBlockResponse {
#[superstruct(
variants(Merge, Capella, Eip4844),
variant_attributes(derive(Clone, Debug, PartialEq),),
map_into(ExecutionPayload),
map_ref_into(ExecutionPayloadRef),
cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"),
partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant")
)]
@@ -344,27 +351,49 @@ pub struct GetPayloadResponse<T: EthSpec> {
pub block_value: Uint256,
}
impl<T: EthSpec> GetPayloadResponse<T> {
pub fn execution_payload(self) -> ExecutionPayload<T> {
match self {
GetPayloadResponse::Merge(response) => {
ExecutionPayload::Merge(response.execution_payload)
}
GetPayloadResponse::Capella(response) => {
ExecutionPayload::Capella(response.execution_payload)
}
GetPayloadResponse::Eip4844(response) => {
ExecutionPayload::Eip4844(response.execution_payload)
}
impl<'a, T: EthSpec> From<GetPayloadResponseRef<'a, T>> for ExecutionPayloadRef<'a, T> {
fn from(response: GetPayloadResponseRef<'a, T>) -> Self {
map_get_payload_response_ref_into_execution_payload_ref!(&'a _, response, |inner, cons| {
cons(&inner.execution_payload)
})
}
}
impl<T: EthSpec> From<GetPayloadResponse<T>> for ExecutionPayload<T> {
fn from(response: GetPayloadResponse<T>) -> Self {
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,
),
}
}
}
// This name is work in progress, it could
// change when this method is actually proposed
// but I'm writing this as it has been described
impl<T: EthSpec> GetPayloadResponse<T> {
pub fn execution_payload_ref(&self) -> ExecutionPayloadRef<T> {
self.to_ref().into()
}
}
#[derive(Clone, Copy, Debug)]
pub struct SupportedApis {
pub struct EngineCapabilities {
pub new_payload_v1: bool,
pub new_payload_v2: bool,
pub new_payload_v3: bool,
@@ -375,3 +404,32 @@ pub struct SupportedApis {
pub get_payload_v3: bool,
pub exchange_transition_configuration_v1: bool,
}
impl EngineCapabilities {
pub fn to_response(&self) -> Vec<&str> {
let mut response = Vec::new();
if self.new_payload_v1 {
response.push(ENGINE_NEW_PAYLOAD_V1);
}
if self.new_payload_v2 {
response.push(ENGINE_NEW_PAYLOAD_V2);
}
if self.forkchoice_updated_v1 {
response.push(ENGINE_FORKCHOICE_UPDATED_V1);
}
if self.forkchoice_updated_v2 {
response.push(ENGINE_FORKCHOICE_UPDATED_V2);
}
if self.get_payload_v1 {
response.push(ENGINE_GET_PAYLOAD_V1);
}
if self.get_payload_v2 {
response.push(ENGINE_GET_PAYLOAD_V2);
}
if self.exchange_transition_configuration_v1 {
response.push(ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1);
}
response
}
}

View File

@@ -7,10 +7,11 @@ use reqwest::header::CONTENT_TYPE;
use sensitive_url::SensitiveUrl;
use serde::de::DeserializeOwned;
use serde_json::json;
use tokio::sync::RwLock;
use std::collections::HashSet;
use tokio::sync::Mutex;
use std::time::Duration;
use types::{ChainSpec, EthSpec};
use std::time::{Duration, SystemTime};
use types::EthSpec;
pub use deposit_log::{DepositLog, Log};
pub use reqwest::Client;
@@ -50,8 +51,37 @@ pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1: &str =
"engine_exchangeTransitionConfigurationV1";
pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT: Duration = Duration::from_secs(1);
pub const ENGINE_EXCHANGE_CAPABILITIES: &str = "engine_exchangeCapabilities";
pub const ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT: Duration = Duration::from_secs(1);
/// This error is returned during a `chainId` call by Geth.
pub const EIP155_ERROR_STR: &str = "chain not synced beyond EIP-155 replay-protection fork block";
/// This code is returned by all clients when a method is not supported
/// (verified geth, nethermind, erigon, besu)
pub const METHOD_NOT_FOUND_CODE: i64 = -32601;
pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[
ENGINE_NEW_PAYLOAD_V1,
ENGINE_NEW_PAYLOAD_V2,
ENGINE_GET_PAYLOAD_V1,
ENGINE_GET_PAYLOAD_V2,
ENGINE_FORKCHOICE_UPDATED_V1,
ENGINE_FORKCHOICE_UPDATED_V2,
ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1,
];
/// This is necessary because a user might run a capella-enabled version of
/// lighthouse before they update to a capella-enabled execution engine.
// TODO (mark): rip this out once we are post-capella on mainnet
pub static PRE_CAPELLA_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities {
new_payload_v1: true,
new_payload_v2: false,
forkchoice_updated_v1: true,
forkchoice_updated_v2: false,
get_payload_v1: true,
get_payload_v2: false,
exchange_transition_configuration_v1: true,
};
/// Contains methods to convert arbitrary bytes to an ETH2 deposit contract object.
pub mod deposit_log {
@@ -528,11 +558,47 @@ pub mod deposit_methods {
}
}
#[derive(Clone, Debug)]
pub struct CapabilitiesCacheEntry {
engine_capabilities: EngineCapabilities,
fetch_time: SystemTime,
}
impl CapabilitiesCacheEntry {
pub fn new(engine_capabilities: EngineCapabilities) -> Self {
Self {
engine_capabilities,
fetch_time: SystemTime::now(),
}
}
pub fn engine_capabilities(&self) -> &EngineCapabilities {
&self.engine_capabilities
}
pub fn age(&self) -> Duration {
// duration_since() may fail because measurements taken earlier
// are not guaranteed to always be before later measurements
// due to anomalies such as the system clock being adjusted
// either forwards or backwards
//
// In such cases, we'll just say the age is zero
SystemTime::now()
.duration_since(self.fetch_time)
.unwrap_or(Duration::ZERO)
}
/// returns `true` if the entry's age is >= age_limit
pub fn older_than(&self, age_limit: Option<Duration>) -> bool {
age_limit.map_or(false, |limit| self.age() >= limit)
}
}
pub struct HttpJsonRpc {
pub client: Client,
pub url: SensitiveUrl,
pub execution_timeout_multiplier: u32,
pub cached_supported_apis: RwLock<Option<SupportedApis>>,
pub engine_capabilities_cache: Mutex<Option<CapabilitiesCacheEntry>>,
auth: Option<Auth>,
}
@@ -540,29 +606,12 @@ impl HttpJsonRpc {
pub fn new(
url: SensitiveUrl,
execution_timeout_multiplier: Option<u32>,
spec: &ChainSpec,
) -> Result<Self, Error> {
// FIXME: remove this `cached_supported_apis` spec hack once the `engine_getCapabilities`
// method is implemented in all execution clients:
// https://github.com/ethereum/execution-apis/issues/321
let cached_supported_apis = RwLock::new(Some(SupportedApis {
new_payload_v1: true,
new_payload_v2: spec.capella_fork_epoch.is_some() || spec.eip4844_fork_epoch.is_some(),
new_payload_v3: spec.eip4844_fork_epoch.is_some(),
forkchoice_updated_v1: true,
forkchoice_updated_v2: spec.capella_fork_epoch.is_some()
|| spec.eip4844_fork_epoch.is_some(),
get_payload_v1: true,
get_payload_v2: spec.capella_fork_epoch.is_some() || spec.eip4844_fork_epoch.is_some(),
get_payload_v3: spec.eip4844_fork_epoch.is_some(),
exchange_transition_configuration_v1: true,
}));
Ok(Self {
client: Client::builder().build()?,
url,
execution_timeout_multiplier: execution_timeout_multiplier.unwrap_or(1),
cached_supported_apis,
engine_capabilities_cache: Mutex::new(None),
auth: None,
})
}
@@ -571,29 +620,12 @@ impl HttpJsonRpc {
url: SensitiveUrl,
auth: Auth,
execution_timeout_multiplier: Option<u32>,
spec: &ChainSpec,
) -> Result<Self, Error> {
// FIXME: remove this `cached_supported_apis` spec hack once the `engine_getCapabilities`
// method is implemented in all execution clients:
// https://github.com/ethereum/execution-apis/issues/321
let cached_supported_apis = RwLock::new(Some(SupportedApis {
new_payload_v1: true,
new_payload_v2: spec.capella_fork_epoch.is_some() || spec.eip4844_fork_epoch.is_some(),
new_payload_v3: spec.eip4844_fork_epoch.is_some(),
forkchoice_updated_v1: true,
forkchoice_updated_v2: spec.capella_fork_epoch.is_some()
|| spec.eip4844_fork_epoch.is_some(),
get_payload_v1: true,
get_payload_v2: spec.capella_fork_epoch.is_some() || spec.eip4844_fork_epoch.is_some(),
get_payload_v3: spec.eip4844_fork_epoch.is_some(),
exchange_transition_configuration_v1: true,
}));
Ok(Self {
client: Client::builder().build()?,
url,
execution_timeout_multiplier: execution_timeout_multiplier.unwrap_or(1),
cached_supported_apis,
engine_capabilities_cache: Mutex::new(None),
auth: Some(auth),
})
}
@@ -791,7 +823,7 @@ impl HttpJsonRpc {
pub async fn get_payload_v1<T: EthSpec>(
&self,
payload_id: PayloadId,
) -> Result<ExecutionPayload<T>, Error> {
) -> Result<GetPayloadResponse<T>, Error> {
let params = json!([JsonPayloadIdRequest::from(payload_id)]);
let payload_v1: JsonExecutionPayloadV1<T> = self
@@ -802,7 +834,11 @@ impl HttpJsonRpc {
)
.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>(
@@ -961,37 +997,67 @@ impl HttpJsonRpc {
Ok(response)
}
// TODO: This is currently a stub for the `engine_getCapabilities`
// method. This stub is unused because we set cached_supported_apis
// in the constructor based on the `spec`
// Implement this once the execution clients support it
// https://github.com/ethereum/execution-apis/issues/321
pub async fn get_capabilities(&self) -> Result<SupportedApis, Error> {
Ok(SupportedApis {
new_payload_v1: true,
new_payload_v2: true,
new_payload_v3: true,
forkchoice_updated_v1: true,
forkchoice_updated_v2: true,
get_payload_v1: true,
get_payload_v2: true,
get_payload_v3: true,
exchange_transition_configuration_v1: true,
})
pub async fn exchange_capabilities(&self) -> Result<EngineCapabilities, Error> {
let params = json!([LIGHTHOUSE_CAPABILITIES]);
let response: Result<HashSet<String>, _> = self
.rpc_request(
ENGINE_EXCHANGE_CAPABILITIES,
params,
ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT * self.execution_timeout_multiplier,
)
.await;
match response {
// TODO (mark): rip this out once we are post capella on mainnet
Err(error) => match error {
Error::ServerMessage { code, message: _ } if code == METHOD_NOT_FOUND_CODE => {
Ok(PRE_CAPELLA_ENGINE_CAPABILITIES)
}
_ => Err(error),
},
Ok(capabilities) => Ok(EngineCapabilities {
new_payload_v1: capabilities.contains(ENGINE_NEW_PAYLOAD_V1),
new_payload_v2: capabilities.contains(ENGINE_NEW_PAYLOAD_V2),
forkchoice_updated_v1: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V1),
forkchoice_updated_v2: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V2),
get_payload_v1: capabilities.contains(ENGINE_GET_PAYLOAD_V1),
get_payload_v2: capabilities.contains(ENGINE_GET_PAYLOAD_V2),
exchange_transition_configuration_v1: capabilities
.contains(ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1),
}),
}
}
pub async fn set_cached_supported_apis(&self, supported_apis: Option<SupportedApis>) {
*self.cached_supported_apis.write().await = supported_apis;
pub async fn clear_exchange_capabilties_cache(&self) {
*self.engine_capabilities_cache.lock().await = None;
}
pub async fn get_cached_supported_apis(&self) -> Result<SupportedApis, Error> {
let cached_opt = *self.cached_supported_apis.read().await;
if let Some(supported_apis) = cached_opt {
Ok(supported_apis)
/// Returns the execution engine capabilities resulting from a call to
/// engine_exchangeCapabilities. If the capabilities cache is not populated,
/// or if it is populated with a cached result of age >= `age_limit`, this
/// method will fetch the result from the execution engine and populate the
/// cache before returning it. Otherwise it will return a cached result from
/// a previous call.
///
/// Set `age_limit` to `None` to always return the cached result
/// Set `age_limit` to `Some(Duration::ZERO)` to force fetching from EE
pub async fn get_engine_capabilities(
&self,
age_limit: Option<Duration>,
) -> Result<EngineCapabilities, Error> {
let mut lock = self.engine_capabilities_cache.lock().await;
if lock
.as_ref()
.map_or(true, |entry| entry.older_than(age_limit))
{
let engine_capabilities = self.exchange_capabilities().await?;
*lock = Some(CapabilitiesCacheEntry::new(engine_capabilities));
Ok(engine_capabilities)
} else {
let supported_apis = self.get_capabilities().await?;
self.set_cached_supported_apis(Some(supported_apis)).await;
Ok(supported_apis)
// here entry is guaranteed to exist so unwrap() is safe
Ok(*lock.as_ref().unwrap().engine_capabilities())
}
}
@@ -1001,12 +1067,12 @@ impl HttpJsonRpc {
&self,
execution_payload: ExecutionPayload<T>,
) -> Result<PayloadStatusV1, Error> {
let supported_apis = self.get_cached_supported_apis().await?;
if supported_apis.new_payload_v3 {
let engine_capabilities = self.get_engine_capabilities(None).await?;
if engine_capabilities.new_payload_v3 {
self.new_payload_v3(execution_payload).await
} else if supported_apis.new_payload_v2 {
}else if engine_capabilities.new_payload_v2 {
self.new_payload_v2(execution_payload).await
} else if supported_apis.new_payload_v1 {
} else if engine_capabilities.new_payload_v1 {
self.new_payload_v1(execution_payload).await
} else {
Err(Error::RequiredMethodUnsupported("engine_newPayload"))
@@ -1019,22 +1085,13 @@ impl HttpJsonRpc {
&self,
fork_name: ForkName,
payload_id: PayloadId,
) -> Result<ExecutionPayload<T>, Error> {
let supported_apis = self.get_cached_supported_apis().await?;
if supported_apis.get_payload_v3 {
Ok(self
.get_payload_v3(fork_name, payload_id)
.await?
.execution_payload())
} else if supported_apis.get_payload_v2 {
// TODO: modify this method to return GetPayloadResponse instead
// 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 supported_apis.get_payload_v1 {
) -> Result<GetPayloadResponse<T>, Error> {
let engine_capabilities = self.get_engine_capabilities(None).await?;
if engine_capabilities.get_payload_v3 {
self.get_payload_v3(fork_name, payload_id).await
} else if engine_capabilities.get_payload_v2 {
self.get_payload_v2(fork_name, payload_id).await
} else if engine_capabilities.new_payload_v1 {
self.get_payload_v1(payload_id).await
} else {
Err(Error::RequiredMethodUnsupported("engine_getPayload"))
@@ -1048,11 +1105,11 @@ impl HttpJsonRpc {
forkchoice_state: ForkchoiceState,
payload_attributes: Option<PayloadAttributes>,
) -> Result<ForkchoiceUpdatedResponse, Error> {
let supported_apis = self.get_cached_supported_apis().await?;
if supported_apis.forkchoice_updated_v2 {
let engine_capabilities = self.get_engine_capabilities(None).await?;
if engine_capabilities.forkchoice_updated_v2 {
self.forkchoice_updated_v2(forkchoice_state, payload_attributes)
.await
} else if supported_apis.forkchoice_updated_v1 {
} else if engine_capabilities.forkchoice_updated_v1 {
self.forkchoice_updated_v1(forkchoice_state, payload_attributes)
.await
} else {
@@ -1080,7 +1137,6 @@ mod test {
impl Tester {
pub fn new(with_auth: bool) -> Self {
let server = MockServer::unit_testing();
let spec = MainnetEthSpec::default_spec();
let rpc_url = SensitiveUrl::parse(&server.url()).unwrap();
let echo_url = SensitiveUrl::parse(&format!("{}/echo", server.url())).unwrap();
@@ -1091,13 +1147,13 @@ mod test {
let echo_auth =
Auth::new(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(), None, None);
(
Arc::new(HttpJsonRpc::new_with_auth(rpc_url, rpc_auth, None, &spec).unwrap()),
Arc::new(HttpJsonRpc::new_with_auth(echo_url, echo_auth, None, &spec).unwrap()),
Arc::new(HttpJsonRpc::new_with_auth(rpc_url, rpc_auth, None).unwrap()),
Arc::new(HttpJsonRpc::new_with_auth(echo_url, echo_auth, None).unwrap()),
)
} else {
(
Arc::new(HttpJsonRpc::new(rpc_url, None, &spec).unwrap()),
Arc::new(HttpJsonRpc::new(echo_url, None, &spec).unwrap()),
Arc::new(HttpJsonRpc::new(rpc_url, None).unwrap()),
Arc::new(HttpJsonRpc::new(echo_url, None).unwrap()),
)
};
@@ -1685,10 +1741,11 @@ mod test {
}
})],
|client| async move {
let payload = client
let payload: ExecutionPayload<_> = client
.get_payload_v1::<MainnetEthSpec>(str_to_payload_id("0xa247243752eb10b4"))
.await
.unwrap();
.unwrap()
.into();
let expected = ExecutionPayload::Merge(ExecutionPayloadMerge {
parent_hash: ExecutionBlockHash::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a").unwrap(),

View File

@@ -1,13 +1,15 @@
//! Provides generic behaviour for multiple execution engines, specifically fallback behaviour.
use crate::engine_api::{
Error as EngineApiError, ForkchoiceUpdatedResponse, PayloadAttributes, PayloadId,
EngineCapabilities, Error as EngineApiError, ForkchoiceUpdatedResponse, PayloadAttributes,
PayloadId,
};
use crate::HttpJsonRpc;
use lru::LruCache;
use slog::{debug, error, info, Logger};
use slog::{debug, error, info, warn, Logger};
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use task_executor::TaskExecutor;
use tokio::sync::{watch, Mutex, RwLock};
use tokio_stream::wrappers::WatchStream;
@@ -18,6 +20,7 @@ use types::ExecutionBlockHash;
/// Since the size of each value is small (~100 bytes) a large number is used for safety.
/// FIXME: check this assumption now that the key includes entire payload attributes which now includes withdrawals
const PAYLOAD_ID_LRU_CACHE_SIZE: usize = 512;
const CACHED_ENGINE_CAPABILITIES_AGE_LIMIT: Duration = Duration::from_secs(900); // 15 minutes
/// Stores the remembered state of a engine.
#[derive(Copy, Clone, PartialEq, Debug, Eq, Default)]
@@ -29,6 +32,14 @@ enum EngineStateInternal {
AuthFailed,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
enum CapabilitiesCacheAction {
#[default]
None,
Update,
Clear,
}
/// A subset of the engine state to inform other services if the engine is online or offline.
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum EngineState {
@@ -231,7 +242,7 @@ impl Engine {
/// Run the `EngineApi::upcheck` function if the node's last known state is not synced. This
/// might be used to recover the node if offline.
pub async fn upcheck(&self) {
let state: EngineStateInternal = match self.api.upcheck().await {
let (state, cache_action) = match self.api.upcheck().await {
Ok(()) => {
let mut state = self.state.write().await;
if **state != EngineStateInternal::Synced {
@@ -249,12 +260,12 @@ impl Engine {
);
}
state.update(EngineStateInternal::Synced);
**state
(**state, CapabilitiesCacheAction::Update)
}
Err(EngineApiError::IsSyncing) => {
let mut state = self.state.write().await;
state.update(EngineStateInternal::Syncing);
**state
(**state, CapabilitiesCacheAction::Update)
}
Err(EngineApiError::Auth(err)) => {
error!(
@@ -265,7 +276,7 @@ impl Engine {
let mut state = self.state.write().await;
state.update(EngineStateInternal::AuthFailed);
**state
(**state, CapabilitiesCacheAction::None)
}
Err(e) => {
error!(
@@ -276,10 +287,30 @@ impl Engine {
let mut state = self.state.write().await;
state.update(EngineStateInternal::Offline);
**state
// need to clear the engine capabilities cache if we detect the
// execution engine is offline as it is likely the engine is being
// updated to a newer version with new capabilities
(**state, CapabilitiesCacheAction::Clear)
}
};
// do this after dropping state lock guard to avoid holding two locks at once
match cache_action {
CapabilitiesCacheAction::None => {}
CapabilitiesCacheAction::Update => {
if let Err(e) = self
.get_engine_capabilities(Some(CACHED_ENGINE_CAPABILITIES_AGE_LIMIT))
.await
{
warn!(self.log,
"Error during exchange capabilities";
"error" => ?e,
)
}
}
CapabilitiesCacheAction::Clear => self.api.clear_exchange_capabilties_cache().await,
}
debug!(
self.log,
"Execution engine upcheck complete";
@@ -287,6 +318,22 @@ impl Engine {
);
}
/// Returns the execution engine capabilities resulting from a call to
/// engine_exchangeCapabilities. If the capabilities cache is not populated,
/// or if it is populated with a cached result of age >= `age_limit`, this
/// method will fetch the result from the execution engine and populate the
/// cache before returning it. Otherwise it will return a cached result from
/// a previous call.
///
/// Set `age_limit` to `None` to always return the cached result
/// Set `age_limit` to `Some(Duration::ZERO)` to force fetching from EE
pub async fn get_engine_capabilities(
&self,
age_limit: Option<Duration>,
) -> Result<EngineCapabilities, EngineApiError> {
self.api.get_engine_capabilities(age_limit).await
}
/// Run `func` on the node regardless of the node's current state.
///
/// ## Note

View File

@@ -7,6 +7,7 @@
use crate::payload_cache::PayloadCache;
use auth::{strip_prefix, Auth, JwtKey};
use builder_client::BuilderHttpClient;
pub use engine_api::EngineCapabilities;
use engine_api::Error as ApiError;
pub use engine_api::*;
pub use engine_api::{http, http::deposit_methods, http::HttpJsonRpc};
@@ -123,9 +124,13 @@ impl From<ApiError> for Error {
}
pub enum BlockProposalContents<T: EthSpec, Payload: AbstractExecPayload<T>> {
Payload(Payload),
Payload {
payload: Payload,
block_value: Uint256,
},
PayloadAndBlobs {
payload: Payload,
block_value: Uint256,
kzg_commitments: VariableList<KzgCommitment, T::MaxBlobsPerBlock>,
blobs: VariableList<Blob<T>, T::MaxBlobsPerBlock>,
},
@@ -151,9 +156,13 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
pub fn payload(&self) -> &Payload {
match self {
Self::Payload(payload) => payload,
Self::Payload {
payload,
block_value: _,
} => payload,
Self::PayloadAndBlobs {
payload,
block_value: _,
kzg_commitments: _,
blobs: _,
} => payload,
@@ -161,21 +170,43 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
}
pub fn to_payload(self) -> Payload {
match self {
Self::Payload(payload) => payload,
Self::Payload {
payload,
block_value: _,
} => payload,
Self::PayloadAndBlobs {
payload,
block_value: _,
kzg_commitments: _,
blobs: _,
} => payload,
}
}
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> {
Ok(match fork_name {
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 {
payload: Payload::default_at_fork(fork_name)?,
block_value: Uint256::zero(),
blobs: VariableList::default(),
kzg_commitments: VariableList::default(),
},
@@ -267,12 +298,7 @@ pub struct ExecutionLayer<T: EthSpec> {
impl<T: EthSpec> ExecutionLayer<T> {
/// Instantiate `Self` with an Execution engine specified in `Config`, using JSON-RPC via HTTP.
pub fn from_config(
config: Config,
executor: TaskExecutor,
log: Logger,
spec: &ChainSpec,
) -> Result<Self, Error> {
pub fn from_config(config: Config, executor: TaskExecutor, log: Logger) -> Result<Self, Error> {
let Config {
execution_endpoints: urls,
builder_url,
@@ -327,13 +353,8 @@ impl<T: EthSpec> ExecutionLayer<T> {
let engine: Engine = {
let auth = Auth::new(jwt_key, jwt_id, jwt_version);
debug!(log, "Loaded execution endpoint"; "endpoint" => %execution_url, "jwt_path" => ?secret_file.as_path());
let api = HttpJsonRpc::new_with_auth(
execution_url,
auth,
execution_timeout_multiplier,
&spec,
)
.map_err(Error::ApiError)?;
let api = HttpJsonRpc::new_with_auth(execution_url, auth, execution_timeout_multiplier)
.map_err(Error::ApiError)?;
Engine::new(api, executor.clone(), &log)
};
@@ -377,12 +398,12 @@ 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())
/// Cache a full payload, keyed on the `tree_hash_root` of the payload
fn cache_payload(&self, payload: ExecutionPayloadRef<T>) -> Option<ExecutionPayload<T>> {
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>> {
self.inner.payload_cache.pop(root)
}
@@ -819,17 +840,32 @@ impl<T: EthSpec> ExecutionLayer<T> {
"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(
&relay,
parent_hash,
payload_attributes.prev_randao(),
payload_attributes.timestamp(),
payload_attributes,
Some(local.payload().block_number()),
self.inner.builder_profit_threshold,
current_fork,
spec,
) {
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() => {
info!(
@@ -873,19 +909,25 @@ impl<T: EthSpec> ExecutionLayer<T> {
match verify_builder_bid(
&relay,
parent_hash,
payload_attributes.prev_randao(),
payload_attributes.timestamp(),
payload_attributes,
None,
self.inner.builder_profit_threshold,
current_fork,
spec,
) {
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
// to produce a payload so we have no alternative.
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) => {
metrics::inc_counter_vec(
@@ -999,7 +1041,7 @@ impl<T: EthSpec> ExecutionLayer<T> {
payload_attributes: &PayloadAttributes,
forkchoice_update_params: ForkchoiceUpdateParameters,
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> {
self.engine()
.request(move |engine| async move {
@@ -1082,9 +1124,9 @@ impl<T: EthSpec> ExecutionLayer<T> {
);
engine.api.get_payload::<T>(current_fork, payload_id).await
};
let (blob, payload) = tokio::join!(blob_fut, payload_fut);
let payload = payload.map(|full_payload| {
if full_payload.fee_recipient() != payload_attributes.suggested_fee_recipient() {
let (blob, payload_response) = tokio::join!(blob_fut, payload_fut);
let (execution_payload, block_value) = payload_response.map(|payload_response| {
if payload_response.execution_payload_ref().fee_recipient() != payload_attributes.suggested_fee_recipient() {
error!(
self.log(),
"Inconsistent fee recipient";
@@ -1093,28 +1135,32 @@ impl<T: EthSpec> ExecutionLayer<T> {
indicate that fees are being diverted to another address. Please \
ensure that the value of suggested_fee_recipient is set correctly and \
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(),
);
}
if f(self, &full_payload).is_some() {
if f(self, payload_response.execution_payload_ref()).is_some() {
warn!(
self.log(),
"Duplicate payload cached, this might indicate redundant proposal \
attempts."
);
}
full_payload.into()
payload_response.into()
})?;
if let Some(blob) = blob.transpose()? {
// FIXME(sean) cache blobs
Ok(BlockProposalContents::PayloadAndBlobs {
payload,
payload: execution_payload.into(),
block_value,
blobs: blob.blobs,
kzg_commitments: blob.kzgs,
})
} else {
Ok(BlockProposalContents::Payload(payload))
Ok(BlockProposalContents::Payload {
payload: execution_payload.into(),
block_value,
})
}
})
.await
@@ -1373,6 +1419,26 @@ impl<T: EthSpec> ExecutionLayer<T> {
}
}
/// Returns the execution engine capabilities resulting from a call to
/// engine_exchangeCapabilities. If the capabilities cache is not populated,
/// or if it is populated with a cached result of age >= `age_limit`, this
/// method will fetch the result from the execution engine and populate the
/// cache before returning it. Otherwise it will return a cached result from
/// a previous call.
///
/// Set `age_limit` to `None` to always return the cached result
/// Set `age_limit` to `Some(Duration::ZERO)` to force fetching from EE
pub async fn get_engine_capabilities(
&self,
age_limit: Option<Duration>,
) -> Result<EngineCapabilities, Error> {
self.engine()
.request(|engine| engine.get_engine_capabilities(age_limit))
.await
.map_err(Box::new)
.map_err(Error::EngineError)
}
/// Used during block production to determine if the merge has been triggered.
///
/// ## Specification
@@ -1801,6 +1867,11 @@ enum InvalidBuilderPayload {
signature: Signature,
pubkey: PublicKeyBytes,
},
#[allow(dead_code)]
WithdrawalsRoot {
payload: Hash256,
expected: Hash256,
},
}
impl InvalidBuilderPayload {
@@ -1815,6 +1886,7 @@ impl InvalidBuilderPayload {
InvalidBuilderPayload::BlockNumber { .. } => true,
InvalidBuilderPayload::Fork { .. } => true,
InvalidBuilderPayload::Signature { .. } => true,
InvalidBuilderPayload::WithdrawalsRoot { .. } => true,
}
}
}
@@ -1850,6 +1922,13 @@ impl fmt::Display for InvalidBuilderPayload {
"invalid payload signature {} for pubkey {}",
signature, pubkey
),
InvalidBuilderPayload::WithdrawalsRoot { payload, expected } => {
write!(
f,
"payload withdrawals root was {} not {}",
payload, expected
)
}
}
}
}
@@ -1858,10 +1937,10 @@ impl fmt::Display for InvalidBuilderPayload {
fn verify_builder_bid<T: EthSpec, Payload: AbstractExecPayload<T>>(
bid: &ForkVersionedResponse<SignedBuilderBid<T, Payload>>,
parent_hash: ExecutionBlockHash,
prev_randao: Hash256,
timestamp: u64,
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);
@@ -1888,29 +1967,25 @@ fn verify_builder_bid<T: EthSpec, Payload: AbstractExecPayload<T>>(
payload: header.parent_hash(),
expected: parent_hash,
}))
} else if header.prev_randao() != prev_randao {
} else if header.prev_randao() != payload_attributes.prev_randao() {
Err(Box::new(InvalidBuilderPayload::PrevRandao {
payload: header.prev_randao(),
expected: prev_randao,
expected: payload_attributes.prev_randao(),
}))
} else if header.timestamp() != timestamp {
} else if header.timestamp() != payload_attributes.timestamp() {
Err(Box::new(InvalidBuilderPayload::Timestamp {
payload: header.timestamp(),
expected: timestamp,
expected: payload_attributes.timestamp(),
}))
} else if block_number.map_or(false, |n| n != header.block_number()) {
Err(Box::new(InvalidBuilderPayload::BlockNumber {
payload: header.block_number(),
expected: block_number,
}))
} else if !matches!(bid.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.
} else if bid.version != Some(current_fork) {
Err(Box::new(InvalidBuilderPayload::Fork {
payload: bid.version,
expected: ForkName::Merge,
expected: current_fork,
}))
} else if !is_signature_valid {
Err(Box::new(InvalidBuilderPayload::Signature {

View File

@@ -1,25 +1,33 @@
use super::Context;
use crate::engine_api::{http::*, *};
use crate::json_structures::*;
use crate::test_utils::DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI;
use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue;
use std::sync::Arc;
use types::{EthSpec, ForkName};
pub const GENERIC_ERROR_CODE: i64 = -1234;
pub const BAD_PARAMS_ERROR_CODE: i64 = -32602;
pub const UNKNOWN_PAYLOAD_ERROR_CODE: i64 = -38001;
pub const FORK_REQUEST_MISMATCH_ERROR_CODE: i64 = -32000;
pub async fn handle_rpc<T: EthSpec>(
body: JsonValue,
ctx: Arc<Context<T>>,
) -> Result<JsonValue, String> {
) -> Result<JsonValue, (String, i64)> {
*ctx.previous_request.lock() = Some(body.clone());
let method = body
.get("method")
.and_then(JsonValue::as_str)
.ok_or_else(|| "missing/invalid method field".to_string())?;
.ok_or_else(|| "missing/invalid method field".to_string())
.map_err(|s| (s, GENERIC_ERROR_CODE))?;
let params = body
.get("params")
.ok_or_else(|| "missing/invalid params field".to_string())?;
.ok_or_else(|| "missing/invalid params field".to_string())
.map_err(|s| (s, GENERIC_ERROR_CODE))?;
match method {
ETH_SYNCING => Ok(JsonValue::Bool(false)),
@@ -27,7 +35,8 @@ pub async fn handle_rpc<T: EthSpec>(
let tag = params
.get(0)
.and_then(JsonValue::as_str)
.ok_or_else(|| "missing/invalid params[0] value".to_string())?;
.ok_or_else(|| "missing/invalid params[0] value".to_string())
.map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?;
match tag {
"latest" => Ok(serde_json::to_value(
@@ -36,7 +45,10 @@ pub async fn handle_rpc<T: EthSpec>(
.latest_execution_block(),
)
.unwrap()),
other => Err(format!("The tag {} is not supported", other)),
other => Err((
format!("The tag {} is not supported", other),
BAD_PARAMS_ERROR_CODE,
)),
}
}
ETH_GET_BLOCK_BY_HASH => {
@@ -47,7 +59,8 @@ pub async fn handle_rpc<T: EthSpec>(
.and_then(|s| {
s.parse()
.map_err(|e| format!("unable to parse hash: {:?}", e))
})?;
})
.map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?;
// If we have a static response set, just return that.
if let Some(response) = *ctx.static_get_block_by_hash_response.lock() {
@@ -57,7 +70,8 @@ pub async fn handle_rpc<T: EthSpec>(
let full_tx = params
.get(1)
.and_then(JsonValue::as_bool)
.ok_or_else(|| "missing/invalid params[1] value".to_string())?;
.ok_or_else(|| "missing/invalid params[1] value".to_string())
.map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?;
if full_tx {
Ok(serde_json::to_value(
ctx.execution_block_generator
@@ -76,15 +90,17 @@ pub async fn handle_rpc<T: EthSpec>(
}
ENGINE_NEW_PAYLOAD_V1 | ENGINE_NEW_PAYLOAD_V2 | ENGINE_NEW_PAYLOAD_V3 => {
let request = match method {
ENGINE_NEW_PAYLOAD_V1 => {
JsonExecutionPayload::V1(get_param::<JsonExecutionPayloadV1<T>>(params, 0)?)
}
ENGINE_NEW_PAYLOAD_V1 => JsonExecutionPayload::V1(
get_param::<JsonExecutionPayloadV1<T>>(params, 0)
.map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?,
),
ENGINE_NEW_PAYLOAD_V2 => get_param::<JsonExecutionPayloadV2<T>>(params, 0)
.map(|jep| JsonExecutionPayload::V2(jep))
.or_else(|_| {
get_param::<JsonExecutionPayloadV1<T>>(params, 0)
.map(|jep| JsonExecutionPayload::V1(jep))
})?,
})
.map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?,
ENGINE_NEW_PAYLOAD_V3 => get_param::<JsonExecutionPayloadV3<T>>(params, 0)
.map(|jep| JsonExecutionPayload::V3(jep))
.or_else(|_| {
@@ -106,37 +122,46 @@ pub async fn handle_rpc<T: EthSpec>(
match fork {
ForkName::Merge => {
if matches!(request, JsonExecutionPayload::V2(_)) {
return Err(format!(
"{} called with `ExecutionPayloadV2` before capella fork!",
method
return Err((
format!(
"{} called with `ExecutionPayloadV2` before Capella fork!",
method
),
GENERIC_ERROR_CODE,
));
}
}
ForkName::Capella => {
if method == ENGINE_NEW_PAYLOAD_V1 {
return Err(format!("{} called after capella fork!", method));
return Err((
format!("{} called after Capella fork!", method),
GENERIC_ERROR_CODE,
));
}
if matches!(request, JsonExecutionPayload::V1(_)) {
return Err(format!(
"{} called with `ExecutionPayloadV1` after capella fork!",
method
return Err((
format!(
"{} called with `ExecutionPayloadV1` after Capella fork!",
method
),
GENERIC_ERROR_CODE,
));
}
}
ForkName::Eip4844 => {
if method == ENGINE_NEW_PAYLOAD_V1 || method == ENGINE_NEW_PAYLOAD_V2 {
return Err(format!("{} called after capella fork!", method));
return Err((format!("{} called after capella fork!", method), GENERIC_ERROR_CODE));
}
if matches!(request, JsonExecutionPayload::V1(_)) {
return Err(format!(
return Err((format!(
"{} called with `ExecutionPayloadV1` after eip4844 fork!",
method
));
), GENERIC_ERROR_CODE));
}
if matches!(request, JsonExecutionPayload::V2(_)) {
return Err(format!(
return Err((format!(
"{} called with `ExecutionPayloadV2` after eip4844 fork!",
method
method), GENERIC_ERROR_CODE
));
}
}
@@ -174,14 +199,20 @@ pub async fn handle_rpc<T: EthSpec>(
Ok(serde_json::to_value(JsonPayloadStatusV1::from(response)).unwrap())
}
ENGINE_GET_PAYLOAD_V1 | ENGINE_GET_PAYLOAD_V2 | ENGINE_GET_PAYLOAD_V3 => {
let request: JsonPayloadIdRequest = get_param(params, 0)?;
let request: JsonPayloadIdRequest =
get_param(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?;
let id = request.into();
let response = ctx
.execution_block_generator
.write()
.get_payload(&id)
.ok_or_else(|| format!("no payload for id {:?}", id))?;
.ok_or_else(|| {
(
format!("no payload for id {:?}", id),
UNKNOWN_PAYLOAD_ERROR_CODE,
)
})?;
// validate method called correctly according to shanghai fork time
if ctx
@@ -191,7 +222,10 @@ pub async fn handle_rpc<T: EthSpec>(
== ForkName::Capella
&& method == ENGINE_GET_PAYLOAD_V1
{
return Err(format!("{} called after capella fork!", method));
return Err((
format!("{} called after Capella fork!", method),
FORK_REQUEST_MISMATCH_ERROR_CODE,
));
}
// validate method called correctly according to eip4844 fork time
if ctx
@@ -201,7 +235,7 @@ pub async fn handle_rpc<T: EthSpec>(
== ForkName::Eip4844
&& (method == ENGINE_GET_PAYLOAD_V1 || method == ENGINE_GET_PAYLOAD_V2)
{
return Err(format!("{} called after eip4844 fork!", method));
return Err((format!("{} called after eip4844 fork!", method), FORK_REQUEST_MISMATCH_ERROR_CODE));
}
match method {
@@ -212,14 +246,14 @@ pub async fn handle_rpc<T: EthSpec>(
JsonExecutionPayload::V1(execution_payload) => {
serde_json::to_value(JsonGetPayloadResponseV1 {
execution_payload,
block_value: 0.into(),
block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into(),
})
.unwrap()
}
JsonExecutionPayload::V2(execution_payload) => {
serde_json::to_value(JsonGetPayloadResponseV2 {
execution_payload,
block_value: 0.into(),
block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into(),
})
.unwrap()
}
@@ -229,61 +263,65 @@ pub async fn handle_rpc<T: EthSpec>(
JsonExecutionPayload::V1(execution_payload) => {
serde_json::to_value(JsonGetPayloadResponseV1 {
execution_payload,
block_value: 0.into(),
block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into()
})
.unwrap()
.unwrap()
}
JsonExecutionPayload::V2(execution_payload) => {
serde_json::to_value(JsonGetPayloadResponseV2 {
execution_payload,
block_value: 0.into(),
block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into()
})
.unwrap()
.unwrap()
}
JsonExecutionPayload::V3(execution_payload) => {
serde_json::to_value(JsonGetPayloadResponseV3 {
execution_payload,
block_value: 0.into(),
block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into()
})
.unwrap()
.unwrap()
}
}),
_ => unreachable!(),
}
}
ENGINE_FORKCHOICE_UPDATED_V1 | ENGINE_FORKCHOICE_UPDATED_V2 => {
let forkchoice_state: JsonForkchoiceStateV1 = get_param(params, 0)?;
let forkchoice_state: JsonForkchoiceStateV1 =
get_param(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?;
let payload_attributes = match method {
ENGINE_FORKCHOICE_UPDATED_V1 => {
let jpa1: Option<JsonPayloadAttributesV1> = get_param(params, 1)?;
let jpa1: Option<JsonPayloadAttributesV1> =
get_param(params, 1).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?;
jpa1.map(JsonPayloadAttributes::V1)
}
ENGINE_FORKCHOICE_UPDATED_V2 => {
// we can't use `deny_unknown_fields` without breaking compatibility with some
// clients that haven't updated to the latest engine_api spec. So instead we'll
// need to deserialize based on timestamp
get_param::<Option<JsonPayloadAttributes>>(params, 1).and_then(|pa| {
pa.and_then(|pa| {
match ctx
.execution_block_generator
.read()
.get_fork_at_timestamp(*pa.timestamp())
{
ForkName::Merge => {
get_param::<Option<JsonPayloadAttributesV1>>(params, 1)
.map(|opt| opt.map(JsonPayloadAttributes::V1))
.transpose()
get_param::<Option<JsonPayloadAttributes>>(params, 1)
.and_then(|pa| {
pa.and_then(|pa| {
match ctx
.execution_block_generator
.read()
.get_fork_at_timestamp(*pa.timestamp())
{
ForkName::Merge => {
get_param::<Option<JsonPayloadAttributesV1>>(params, 1)
.map(|opt| opt.map(JsonPayloadAttributes::V1))
.transpose()
}
ForkName::Capella => {
get_param::<Option<JsonPayloadAttributesV2>>(params, 1)
.map(|opt| opt.map(JsonPayloadAttributes::V2))
.transpose()
}
_ => unreachable!(),
}
ForkName::Capella | ForkName::Eip4844 => {
get_param::<Option<JsonPayloadAttributesV2>>(params, 1)
.map(|opt| opt.map(JsonPayloadAttributes::V2))
.transpose()
}
_ => unreachable!(),
}
})
.transpose()
})
.transpose()
})?
.map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?
}
_ => unreachable!(),
};
@@ -297,20 +335,29 @@ pub async fn handle_rpc<T: EthSpec>(
{
ForkName::Merge => {
if matches!(pa, JsonPayloadAttributes::V2(_)) {
return Err(format!(
"{} called with `JsonPayloadAttributesV2` before capella fork!",
method
return Err((
format!(
"{} called with `JsonPayloadAttributesV2` before Capella fork!",
method
),
GENERIC_ERROR_CODE,
));
}
}
ForkName::Capella | ForkName::Eip4844 => {
if method == ENGINE_FORKCHOICE_UPDATED_V1 {
return Err(format!("{} called after capella fork!", method));
return Err((
format!("{} called after Capella fork!", method),
FORK_REQUEST_MISMATCH_ERROR_CODE,
));
}
if matches!(pa, JsonPayloadAttributes::V1(_)) {
return Err(format!(
"{} called with `JsonPayloadAttributesV1` after capella fork!",
method
return Err((
format!(
"{} called with `JsonPayloadAttributesV1` after Capella fork!",
method
),
FORK_REQUEST_MISMATCH_ERROR_CODE,
));
}
}
@@ -337,10 +384,14 @@ pub async fn handle_rpc<T: EthSpec>(
return Ok(serde_json::to_value(response).unwrap());
}
let mut response = ctx.execution_block_generator.write().forkchoice_updated(
forkchoice_state.into(),
payload_attributes.map(|json| json.into()),
)?;
let mut response = ctx
.execution_block_generator
.write()
.forkchoice_updated(
forkchoice_state.into(),
payload_attributes.map(|json| json.into()),
)
.map_err(|s| (s, GENERIC_ERROR_CODE))?;
if let Some(mut status) = ctx.static_forkchoice_updated_response.lock().clone() {
if status.status == PayloadStatusV1Status::Valid {
@@ -361,9 +412,13 @@ pub async fn handle_rpc<T: EthSpec>(
};
Ok(serde_json::to_value(transition_config).unwrap())
}
other => Err(format!(
"The method {} does not exist/is not available",
other
ENGINE_EXCHANGE_CAPABILITIES => {
let engine_capabilities = ctx.engine_capabilities.read();
Ok(serde_json::to_value(engine_capabilities.to_response()).unwrap())
}
other => Err((
format!("The method {} does not exist/is not available", other),
METHOD_NOT_FOUND_CODE,
)),
}
}

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 async_trait::async_trait;
use eth2::types::{BlockId, StateId, ValidatorId};
@@ -84,8 +84,7 @@ impl<E: EthSpec> TestingBuilder<E> {
};
let el =
ExecutionLayer::from_config(config, executor.clone(), executor.log().clone(), &spec)
.unwrap();
ExecutionLayer::from_config(config, executor.clone(), executor.log().clone()).unwrap();
// This should probably be done for all fields, we only update ones we are testing with so far.
let mut context = Context::for_mainnet();
@@ -329,7 +328,7 @@ impl<E: EthSpec> mev_build_rs::BlindedBlockProvider for MockBuilder<E> {
let mut message = BuilderBid {
header,
value: ssz_rs::U256::default(),
value: to_ssz_rs(&Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI))?,
public_key: self.builder_sk.public_key(),
};

View File

@@ -29,6 +29,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
DEFAULT_TERMINAL_BLOCK,
None,
None,
None,
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
spec,
None,
@@ -41,6 +42,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
terminal_block: u64,
shanghai_time: Option<u64>,
eip4844_time: Option<u64>,
builder_threshold: Option<u128>,
jwt_key: Option<JwtKey>,
spec: ChainSpec,
builder_url: Option<SensitiveUrl>,
@@ -69,12 +71,11 @@ impl<T: EthSpec> MockExecutionLayer<T> {
builder_url,
secret_files: vec![path],
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()
};
let el =
ExecutionLayer::from_config(config, executor.clone(), executor.log().clone(), &spec)
.unwrap();
ExecutionLayer::from_config(config, executor.clone(), executor.log().clone()).unwrap();
Self {
server,
@@ -106,7 +107,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
prev_randao,
Address::repeat_byte(42),
// FIXME: think about how to handle different forks / withdrawals here..
Some(vec![]),
None,
);
// Insert a proposer to ensure the fork choice updated command works.

View File

@@ -22,6 +22,7 @@ use tokio::{runtime, sync::oneshot};
use types::{EthSpec, ExecutionBlockHash, Uint256};
use warp::{http::StatusCode, Filter, Rejection};
use crate::EngineCapabilities;
pub use execution_block_generator::{generate_pow_block, Block, ExecutionBlockGenerator};
pub use hook::Hook;
pub use mock_builder::{Context as MockBuilderContext, MockBuilder, Operation, TestingBuilder};
@@ -31,6 +32,17 @@ pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400;
pub const DEFAULT_TERMINAL_BLOCK: u64 = 64;
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_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 {
new_payload_v1: true,
new_payload_v2: true,
forkchoice_updated_v1: true,
forkchoice_updated_v2: true,
get_payload_v1: true,
get_payload_v2: true,
exchange_transition_configuration_v1: true,
};
mod execution_block_generator;
mod handle_rpc;
@@ -117,6 +129,7 @@ impl<T: EthSpec> MockServer<T> {
hook: <_>::default(),
new_payload_statuses: <_>::default(),
fcu_payload_statuses: <_>::default(),
engine_capabilities: Arc::new(RwLock::new(DEFAULT_ENGINE_CAPABILITIES)),
_phantom: PhantomData,
});
@@ -147,6 +160,10 @@ impl<T: EthSpec> MockServer<T> {
}
}
pub fn set_engine_capabilities(&self, engine_capabilities: EngineCapabilities) {
*self.ctx.engine_capabilities.write() = engine_capabilities;
}
pub fn new(
handle: &runtime::Handle,
jwt_key: JwtKey,
@@ -469,6 +486,7 @@ pub struct Context<T: EthSpec> {
pub new_payload_statuses: Arc<Mutex<HashMap<ExecutionBlockHash, PayloadStatusV1>>>,
pub fcu_payload_statuses: Arc<Mutex<HashMap<ExecutionBlockHash, PayloadStatusV1>>>,
pub engine_capabilities: Arc<RwLock<EngineCapabilities>>,
pub _phantom: PhantomData<T>,
}
@@ -620,11 +638,11 @@ pub fn serve<T: EthSpec>(
"jsonrpc": JSONRPC_VERSION,
"result": result
}),
Err(message) => json!({
Err((message, code)) => json!({
"id": id,
"jsonrpc": JSONRPC_VERSION,
"error": {
"code": -1234, // Junk error code.
"code": code,
"message": message
}
}),