mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-23 14:54:45 +00:00
Unify execution layer endpoints (#3214)
## Issue Addressed Resolves #3069 ## Proposed Changes Unify the `eth1-endpoints` and `execution-endpoints` flags in a backwards compatible way as described in https://github.com/sigp/lighthouse/issues/3069#issuecomment-1134219221 Users have 2 options: 1. Use multiple non auth execution endpoints for deposit processing pre-merge 2. Use a single jwt authenticated execution endpoint for both execution layer and deposit processing post merge Related https://github.com/sigp/lighthouse/issues/3118 To enable jwt authenticated deposit processing, this PR removes the calls to `net_version` as the `net` namespace is not exposed in the auth server in execution clients. Moving away from using `networkId` is a good step in my opinion as it doesn't provide us with any added guarantees over `chainId`. See https://github.com/ethereum/consensus-specs/issues/2163 and https://github.com/sigp/lighthouse/issues/2115 Co-authored-by: Paul Hauner <paul@paulhauner.com>
This commit is contained in:
@@ -2,12 +2,13 @@ use crate::metrics;
|
||||
use crate::{
|
||||
block_cache::{BlockCache, Error as BlockCacheError, Eth1Block},
|
||||
deposit_cache::{DepositCacheInsertOutcome, Error as DepositCacheError},
|
||||
http::{
|
||||
get_block, get_block_number, get_chain_id, get_deposit_logs_in_range, get_network_id,
|
||||
BlockQuery, Eth1Id,
|
||||
},
|
||||
inner::{DepositUpdater, Inner},
|
||||
};
|
||||
use execution_layer::auth::Auth;
|
||||
use execution_layer::http::{
|
||||
deposit_methods::{BlockQuery, Eth1Id},
|
||||
HttpJsonRpc,
|
||||
};
|
||||
use fallback::{Fallback, FallbackError};
|
||||
use futures::future::TryFutureExt;
|
||||
use parking_lot::{RwLock, RwLockReadGuard};
|
||||
@@ -17,14 +18,13 @@ use slog::{crit, debug, error, info, trace, warn, Logger};
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::ops::{Range, RangeInclusive};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::RwLock as TRwLock;
|
||||
use tokio::time::{interval_at, Duration, Instant};
|
||||
use types::{ChainSpec, EthSpec, Unsigned};
|
||||
|
||||
/// Indicates the default eth1 network id we use for the deposit contract.
|
||||
pub const DEFAULT_NETWORK_ID: Eth1Id = Eth1Id::Goerli;
|
||||
/// Indicates the default eth1 chain id we use for the deposit contract.
|
||||
pub const DEFAULT_CHAIN_ID: Eth1Id = Eth1Id::Goerli;
|
||||
/// Indicates the default eth1 endpoint.
|
||||
@@ -63,14 +63,14 @@ pub enum EndpointError {
|
||||
type EndpointState = Result<(), EndpointError>;
|
||||
|
||||
pub struct EndpointWithState {
|
||||
endpoint: SensitiveUrl,
|
||||
client: HttpJsonRpc,
|
||||
state: TRwLock<Option<EndpointState>>,
|
||||
}
|
||||
|
||||
impl EndpointWithState {
|
||||
pub fn new(endpoint: SensitiveUrl) -> Self {
|
||||
pub fn new(client: HttpJsonRpc) -> Self {
|
||||
Self {
|
||||
endpoint,
|
||||
client,
|
||||
state: TRwLock::new(None),
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,6 @@ async fn get_state(endpoint: &EndpointWithState) -> Option<EndpointState> {
|
||||
/// is not usable.
|
||||
pub struct EndpointsCache {
|
||||
pub fallback: Fallback<EndpointWithState>,
|
||||
pub config_network_id: Eth1Id,
|
||||
pub config_chain_id: Eth1Id,
|
||||
pub log: Logger,
|
||||
}
|
||||
@@ -107,20 +106,14 @@ impl EndpointsCache {
|
||||
}
|
||||
crate::metrics::inc_counter_vec(
|
||||
&crate::metrics::ENDPOINT_REQUESTS,
|
||||
&[&endpoint.endpoint.to_string()],
|
||||
&[&endpoint.client.to_string()],
|
||||
);
|
||||
let state = endpoint_state(
|
||||
&endpoint.endpoint,
|
||||
&self.config_network_id,
|
||||
&self.config_chain_id,
|
||||
&self.log,
|
||||
)
|
||||
.await;
|
||||
let state = endpoint_state(&endpoint.client, &self.config_chain_id, &self.log).await;
|
||||
*value = Some(state.clone());
|
||||
if state.is_err() {
|
||||
crate::metrics::inc_counter_vec(
|
||||
&crate::metrics::ENDPOINT_ERRORS,
|
||||
&[&endpoint.endpoint.to_string()],
|
||||
&[&endpoint.client.to_string()],
|
||||
);
|
||||
crate::metrics::set_gauge(&metrics::ETH1_CONNECTED, 0);
|
||||
} else {
|
||||
@@ -136,7 +129,7 @@ impl EndpointsCache {
|
||||
func: F,
|
||||
) -> Result<(O, usize), FallbackError<SingleEndpointError>>
|
||||
where
|
||||
F: Fn(&'a SensitiveUrl) -> R,
|
||||
F: Fn(&'a HttpJsonRpc) -> R,
|
||||
R: Future<Output = Result<O, SingleEndpointError>>,
|
||||
{
|
||||
let func = &func;
|
||||
@@ -144,12 +137,12 @@ impl EndpointsCache {
|
||||
.first_success(|endpoint| async move {
|
||||
match self.state(endpoint).await {
|
||||
Ok(()) => {
|
||||
let endpoint_str = &endpoint.endpoint.to_string();
|
||||
let endpoint_str = &endpoint.client.to_string();
|
||||
crate::metrics::inc_counter_vec(
|
||||
&crate::metrics::ENDPOINT_REQUESTS,
|
||||
&[endpoint_str],
|
||||
);
|
||||
match func(&endpoint.endpoint).await {
|
||||
match func(&endpoint.client).await {
|
||||
Ok(t) => Ok(t),
|
||||
Err(t) => {
|
||||
crate::metrics::inc_counter_vec(
|
||||
@@ -186,8 +179,7 @@ impl EndpointsCache {
|
||||
/// Returns `Ok` if the endpoint is usable, i.e. is reachable and has a correct network id and
|
||||
/// chain id. Otherwise it returns `Err`.
|
||||
async fn endpoint_state(
|
||||
endpoint: &SensitiveUrl,
|
||||
config_network_id: &Eth1Id,
|
||||
endpoint: &HttpJsonRpc,
|
||||
config_chain_id: &Eth1Id,
|
||||
log: &Logger,
|
||||
) -> EndpointState {
|
||||
@@ -200,21 +192,9 @@ async fn endpoint_state(
|
||||
);
|
||||
EndpointError::RequestFailed(e)
|
||||
};
|
||||
let network_id = get_network_id(endpoint, Duration::from_millis(STANDARD_TIMEOUT_MILLIS))
|
||||
.await
|
||||
.map_err(error_connecting)?;
|
||||
if &network_id != config_network_id {
|
||||
warn!(
|
||||
log,
|
||||
"Invalid eth1 network id on endpoint. Please switch to correct network id";
|
||||
"endpoint" => %endpoint,
|
||||
"action" => "trying fallbacks",
|
||||
"expected" => format!("{:?}",config_network_id),
|
||||
"received" => format!("{:?}",network_id),
|
||||
);
|
||||
return Err(EndpointError::WrongNetworkId);
|
||||
}
|
||||
let chain_id = get_chain_id(endpoint, Duration::from_millis(STANDARD_TIMEOUT_MILLIS))
|
||||
|
||||
let chain_id = endpoint
|
||||
.get_chain_id(Duration::from_millis(STANDARD_TIMEOUT_MILLIS))
|
||||
.await
|
||||
.map_err(error_connecting)?;
|
||||
// Eth1 nodes return chain_id = 0 if the node is not synced
|
||||
@@ -253,7 +233,7 @@ pub enum HeadType {
|
||||
/// Returns the head block and the new block ranges relevant for deposits and the block cache
|
||||
/// from the given endpoint.
|
||||
async fn get_remote_head_and_new_block_ranges(
|
||||
endpoint: &SensitiveUrl,
|
||||
endpoint: &HttpJsonRpc,
|
||||
service: &Service,
|
||||
node_far_behind_seconds: u64,
|
||||
) -> Result<
|
||||
@@ -315,14 +295,14 @@ async fn get_remote_head_and_new_block_ranges(
|
||||
/// Returns the range of new block numbers to be considered for the given head type from the given
|
||||
/// endpoint.
|
||||
async fn relevant_new_block_numbers_from_endpoint(
|
||||
endpoint: &SensitiveUrl,
|
||||
endpoint: &HttpJsonRpc,
|
||||
service: &Service,
|
||||
head_type: HeadType,
|
||||
) -> Result<Option<RangeInclusive<u64>>, SingleEndpointError> {
|
||||
let remote_highest_block =
|
||||
get_block_number(endpoint, Duration::from_millis(BLOCK_NUMBER_TIMEOUT_MILLIS))
|
||||
.map_err(SingleEndpointError::GetBlockNumberFailed)
|
||||
.await?;
|
||||
let remote_highest_block = endpoint
|
||||
.get_block_number(Duration::from_millis(BLOCK_NUMBER_TIMEOUT_MILLIS))
|
||||
.map_err(SingleEndpointError::GetBlockNumberFailed)
|
||||
.await?;
|
||||
service.relevant_new_block_numbers(remote_highest_block, None, head_type)
|
||||
}
|
||||
|
||||
@@ -379,14 +359,41 @@ pub struct DepositCacheUpdateOutcome {
|
||||
pub logs_imported: usize,
|
||||
}
|
||||
|
||||
/// Supports either one authenticated jwt JSON-RPC endpoint **or**
|
||||
/// multiple non-authenticated endpoints with fallback.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Eth1Endpoint {
|
||||
Auth {
|
||||
endpoint: SensitiveUrl,
|
||||
jwt_path: PathBuf,
|
||||
jwt_id: Option<String>,
|
||||
jwt_version: Option<String>,
|
||||
},
|
||||
NoAuth(Vec<SensitiveUrl>),
|
||||
}
|
||||
|
||||
impl Eth1Endpoint {
|
||||
fn len(&self) -> usize {
|
||||
match &self {
|
||||
Self::Auth { .. } => 1,
|
||||
Self::NoAuth(urls) => urls.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_endpoints(&self) -> Vec<SensitiveUrl> {
|
||||
match &self {
|
||||
Self::Auth { endpoint, .. } => vec![endpoint.clone()],
|
||||
Self::NoAuth(endpoints) => endpoints.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// An Eth1 node (e.g., Geth) running a HTTP JSON-RPC endpoint.
|
||||
pub endpoints: Vec<SensitiveUrl>,
|
||||
pub endpoints: Eth1Endpoint,
|
||||
/// The address the `BlockCache` and `DepositCache` should assume is the canonical deposit contract.
|
||||
pub deposit_contract_address: String,
|
||||
/// The eth1 network id where the deposit contract is deployed (Goerli/Mainnet).
|
||||
pub network_id: Eth1Id,
|
||||
/// The eth1 chain id where the deposit contract is deployed (Goerli/Mainnet).
|
||||
pub chain_id: Eth1Id,
|
||||
/// Defines the first block that the `DepositCache` will start searching for deposit logs.
|
||||
@@ -461,10 +468,9 @@ impl Config {
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
endpoints: vec![SensitiveUrl::parse(DEFAULT_ETH1_ENDPOINT)
|
||||
.expect("The default Eth1 endpoint must always be a valid URL.")],
|
||||
endpoints: Eth1Endpoint::NoAuth(vec![SensitiveUrl::parse(DEFAULT_ETH1_ENDPOINT)
|
||||
.expect("The default Eth1 endpoint must always be a valid URL.")]),
|
||||
deposit_contract_address: "0x0000000000000000000000000000000000000000".into(),
|
||||
network_id: DEFAULT_NETWORK_ID,
|
||||
chain_id: DEFAULT_CHAIN_ID,
|
||||
deposit_contract_deploy_block: 1,
|
||||
lowest_cached_block_number: 1,
|
||||
@@ -673,27 +679,45 @@ impl Service {
|
||||
}
|
||||
|
||||
/// Builds a new `EndpointsCache` with empty states.
|
||||
pub fn init_endpoints(&self) -> Arc<EndpointsCache> {
|
||||
pub fn init_endpoints(&self) -> Result<Arc<EndpointsCache>, String> {
|
||||
let endpoints = self.config().endpoints.clone();
|
||||
let config_network_id = self.config().network_id.clone();
|
||||
let config_chain_id = self.config().chain_id.clone();
|
||||
|
||||
let servers = match endpoints {
|
||||
Eth1Endpoint::Auth {
|
||||
jwt_path,
|
||||
endpoint,
|
||||
jwt_id,
|
||||
jwt_version,
|
||||
} => {
|
||||
let auth = Auth::new_with_path(jwt_path, jwt_id, jwt_version)
|
||||
.map_err(|e| format!("Failed to initialize jwt auth: {:?}", e))?;
|
||||
vec![HttpJsonRpc::new_with_auth(endpoint, auth)
|
||||
.map_err(|e| format!("Failed to build auth enabled json rpc {:?}", e))?]
|
||||
}
|
||||
Eth1Endpoint::NoAuth(urls) => urls
|
||||
.into_iter()
|
||||
.map(|url| {
|
||||
HttpJsonRpc::new(url).map_err(|e| format!("Failed to build json rpc {:?}", e))
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
};
|
||||
let new_cache = Arc::new(EndpointsCache {
|
||||
fallback: Fallback::new(endpoints.into_iter().map(EndpointWithState::new).collect()),
|
||||
config_network_id,
|
||||
fallback: Fallback::new(servers.into_iter().map(EndpointWithState::new).collect()),
|
||||
config_chain_id,
|
||||
log: self.log.clone(),
|
||||
});
|
||||
|
||||
let mut endpoints_cache = self.inner.endpoints_cache.write();
|
||||
*endpoints_cache = Some(new_cache.clone());
|
||||
new_cache
|
||||
Ok(new_cache)
|
||||
}
|
||||
|
||||
/// Returns the cached `EndpointsCache` if it exists or builds a new one.
|
||||
pub fn get_endpoints(&self) -> Arc<EndpointsCache> {
|
||||
pub fn get_endpoints(&self) -> Result<Arc<EndpointsCache>, String> {
|
||||
let endpoints_cache = self.inner.endpoints_cache.read();
|
||||
if let Some(cache) = endpoints_cache.clone() {
|
||||
cache
|
||||
Ok(cache)
|
||||
} else {
|
||||
drop(endpoints_cache);
|
||||
self.init_endpoints()
|
||||
@@ -711,7 +735,7 @@ impl Service {
|
||||
pub async fn update(
|
||||
&self,
|
||||
) -> Result<(DepositCacheUpdateOutcome, BlockCacheUpdateOutcome), String> {
|
||||
let endpoints = self.get_endpoints();
|
||||
let endpoints = self.get_endpoints()?;
|
||||
|
||||
// Reset the state of any endpoints which have errored so their state can be redetermined.
|
||||
endpoints.reset_errorred_endpoints().await;
|
||||
@@ -738,7 +762,7 @@ impl Service {
|
||||
}
|
||||
}
|
||||
}
|
||||
endpoints.fallback.map_format_error(|s| &s.endpoint, e)
|
||||
endpoints.fallback.map_format_error(|s| &s.client, e)
|
||||
};
|
||||
|
||||
let process_err = |e: Error| match &e {
|
||||
@@ -988,15 +1012,15 @@ impl Service {
|
||||
*/
|
||||
let block_range_ref = &block_range;
|
||||
let logs = endpoints
|
||||
.first_success(|e| async move {
|
||||
get_deposit_logs_in_range(
|
||||
e,
|
||||
deposit_contract_address_ref,
|
||||
block_range_ref.clone(),
|
||||
Duration::from_millis(GET_DEPOSIT_LOG_TIMEOUT_MILLIS),
|
||||
)
|
||||
.await
|
||||
.map_err(SingleEndpointError::GetDepositLogsFailed)
|
||||
.first_success(|endpoint| async move {
|
||||
endpoint
|
||||
.get_deposit_logs_in_range(
|
||||
deposit_contract_address_ref,
|
||||
block_range_ref.clone(),
|
||||
Duration::from_millis(GET_DEPOSIT_LOG_TIMEOUT_MILLIS),
|
||||
)
|
||||
.await
|
||||
.map_err(SingleEndpointError::GetDepositLogsFailed)
|
||||
})
|
||||
.await
|
||||
.map(|(res, _)| res)
|
||||
@@ -1305,7 +1329,7 @@ fn relevant_block_range(
|
||||
///
|
||||
/// Performs three async calls to an Eth1 HTTP JSON RPC endpoint.
|
||||
async fn download_eth1_block(
|
||||
endpoint: &SensitiveUrl,
|
||||
endpoint: &HttpJsonRpc,
|
||||
cache: Arc<Inner>,
|
||||
block_number_opt: Option<u64>,
|
||||
) -> Result<Eth1Block, SingleEndpointError> {
|
||||
@@ -1326,15 +1350,15 @@ async fn download_eth1_block(
|
||||
});
|
||||
|
||||
// Performs a `get_blockByNumber` call to an eth1 node.
|
||||
let http_block = get_block(
|
||||
endpoint,
|
||||
block_number_opt
|
||||
.map(BlockQuery::Number)
|
||||
.unwrap_or_else(|| BlockQuery::Latest),
|
||||
Duration::from_millis(GET_BLOCK_TIMEOUT_MILLIS),
|
||||
)
|
||||
.map_err(SingleEndpointError::BlockDownloadFailed)
|
||||
.await?;
|
||||
let http_block = endpoint
|
||||
.get_block(
|
||||
block_number_opt
|
||||
.map(BlockQuery::Number)
|
||||
.unwrap_or_else(|| BlockQuery::Latest),
|
||||
Duration::from_millis(GET_BLOCK_TIMEOUT_MILLIS),
|
||||
)
|
||||
.map_err(SingleEndpointError::BlockDownloadFailed)
|
||||
.await?;
|
||||
|
||||
Ok(Eth1Block {
|
||||
hash: http_block.hash,
|
||||
@@ -1359,8 +1383,8 @@ mod tests {
|
||||
#[test]
|
||||
fn serde_serialize() {
|
||||
let serialized =
|
||||
toml::to_string(&Config::default()).expect("Should serde encode default config");
|
||||
toml::from_str::<Config>(&serialized).expect("Should serde decode default config");
|
||||
serde_yaml::to_string(&Config::default()).expect("Should serde encode default config");
|
||||
serde_yaml::from_str::<Config>(&serialized).expect("Should serde decode default config");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user