diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index f8c21b862..80310402b 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -723,6 +723,9 @@ where execution_layer.spawn_clean_proposer_preparation_routine::( beacon_chain.slot_clock.clone(), ); + + // Spawns a routine that polls the `exchange_transition_configuration` endpoint. + execution_layer.spawn_transition_configuration_poll(beacon_chain.spec.clone()); } } diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index c178c0d5c..659423282 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; pub const LATEST_TAG: &str = "latest"; use crate::engines::ForkChoiceState; +pub use json_structures::TransitionConfigurationV1; pub use types::{Address, EthSpec, ExecutionBlockHash, ExecutionPayload, Hash256, Uint256}; pub mod http; @@ -27,6 +28,7 @@ pub enum Error { ExecutionHeadBlockNotFound, ParentHashEqualsBlockHash(ExecutionBlockHash), PayloadIdUnavailable, + TransitionConfigurationMismatch, } impl From for Error { @@ -71,6 +73,11 @@ pub trait EngineApi { forkchoice_state: ForkChoiceState, payload_attributes: Option, ) -> Result; + + async fn exchange_transition_configuration_v1( + &self, + transition_configuration: TransitionConfigurationV1, + ) -> Result; } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 4fa5e80a7..bb382b328 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -36,6 +36,11 @@ pub const ENGINE_GET_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(2); pub const ENGINE_FORKCHOICE_UPDATED_V1: &str = "engine_forkchoiceUpdatedV1"; pub const ENGINE_FORKCHOICE_UPDATED_TIMEOUT: Duration = Duration::from_millis(500); +pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1: &str = + "engine_exchangeTransitionConfigurationV1"; +pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT: Duration = + Duration::from_millis(500); + pub struct HttpJsonRpc { pub client: Client, pub url: SensitiveUrl, @@ -179,6 +184,23 @@ impl EngineApi for HttpJsonRpc { Ok(response.into()) } + + async fn exchange_transition_configuration_v1( + &self, + transition_configuration: TransitionConfigurationV1, + ) -> Result { + let params = json!([transition_configuration]); + + let response = self + .rpc_request( + ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1, + params, + ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT, + ) + .await?; + + Ok(response) + } } #[cfg(test)] diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 8febe451d..77dc6ff47 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -363,6 +363,15 @@ impl From for JsonForkchoiceUpdatedV1Response { } } +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransitionConfigurationV1 { + pub terminal_total_difficulty: Uint256, + pub terminal_block_hash: ExecutionBlockHash, + #[serde(with = "eth2_serde_utils::u64_hex_be")] + pub terminal_block_number: u64, +} + /// Serializes the `logs_bloom` field of an `ExecutionPayload`. pub mod serde_logs_bloom { use super::*; diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index ef2ce065c..b1f71ce34 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -44,6 +44,8 @@ const EXECUTION_BLOCKS_LRU_CACHE_SIZE: usize = 128; const DEFAULT_SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; +const CONFIG_POLL_INTERVAL: Duration = Duration::from_secs(60); + #[derive(Debug)] pub enum Error { NoEngines, @@ -303,6 +305,24 @@ impl ExecutionLayer { self.spawn(preparation_cleaner, "exec_preparation_cleanup"); } + /// Spawns a routine that polls the `exchange_transition_configuration` endpoint. + pub fn spawn_transition_configuration_poll(&self, spec: ChainSpec) { + let routine = |el: ExecutionLayer| async move { + loop { + if let Err(e) = el.exchange_transition_configuration(&spec).await { + error!( + el.log(), + "Failed to check transition config"; + "error" => ?e + ); + } + sleep(CONFIG_POLL_INTERVAL).await; + } + }; + + self.spawn(routine, "exec_config_poll"); + } + /// Returns `true` if there is at least one synced and reachable engine. pub async fn is_synced(&self) -> bool { self.engines().any_synced().await @@ -551,6 +571,65 @@ impl ExecutionLayer { ) } + pub async fn exchange_transition_configuration(&self, spec: &ChainSpec) -> Result<(), Error> { + let local = TransitionConfigurationV1 { + terminal_total_difficulty: spec.terminal_total_difficulty, + terminal_block_hash: spec.terminal_block_hash, + terminal_block_number: 0, + }; + + let broadcast_results = self + .engines() + .broadcast(|engine| engine.api.exchange_transition_configuration_v1(local)) + .await; + + let mut errors = vec![]; + for (i, result) in broadcast_results.into_iter().enumerate() { + match result { + Ok(remote) => { + if local.terminal_total_difficulty != remote.terminal_total_difficulty + || local.terminal_block_hash != remote.terminal_block_hash + { + error!( + self.log(), + "Execution client config mismatch"; + "msg" => "ensure lighthouse and the execution client are up-to-date and \ + configured consistently", + "execution_endpoint" => i, + "remote" => ?remote, + "local" => ?local, + ); + errors.push(EngineError::Api { + id: i.to_string(), + error: ApiError::TransitionConfigurationMismatch, + }); + } else { + debug!( + self.log(), + "Execution client config is OK"; + "execution_endpoint" => i + ); + } + } + Err(e) => { + error!( + self.log(), + "Unable to get transition config"; + "error" => ?e, + "execution_endpoint" => i, + ); + errors.push(e); + } + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(Error::EngineErrors(errors)) + } + } + /// Used during block production to determine if the merge has been triggered. /// /// ## Specification diff --git a/testing/execution_engine_integration/src/execution_engine.rs b/testing/execution_engine_integration/src/execution_engine.rs index 84d721008..475c111ca 100644 --- a/testing/execution_engine_integration/src/execution_engine.rs +++ b/testing/execution_engine_integration/src/execution_engine.rs @@ -109,7 +109,7 @@ impl GenericExecutionEngine for Geth { .arg("engine,eth") .arg("--http.port") .arg(http_port.to_string()) - .arg("--http.authport") + .arg("--authrpc.port") .arg(http_auth_port.to_string()) .arg("--port") .arg(network_port.to_string()) diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 3f0e95442..9ab010d1d 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -108,6 +108,16 @@ impl TestRig { pub async fn perform_tests(&self) { self.wait_until_synced().await; + /* + * Check the transition config endpoint. + */ + for ee in [&self.ee_a, &self.ee_b] { + ee.execution_layer + .exchange_transition_configuration(&self.spec) + .await + .unwrap(); + } + /* * Read the terminal block hash from both pairs, check it's equal. */