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:
Pawan Dhananjay
2022-06-29 09:07:09 +00:00
parent 53b2b500db
commit 5de00b7ee8
31 changed files with 1113 additions and 992 deletions

View File

@@ -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]