diff --git a/README.md b/README.md index da927d8..2d9e854 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ Note: this is a community-maintained list and thus no security guarantees are pr This is not recommended as malicious checkpoints can be returned from the listed apis, even if they are considered _healthy_. This can be run like so: `helios --load-external-fallback` (or `helios -l` with the shorthand). +`--strict-checkpoint-age` or `-s` enables strict checkpoint age checking. If the checkpoint is over two weeks old and this flag is enabled, Helios will error. Without this flag, Helios will instead surface a warning to the user and continue. If the checkpoint is greater than two weeks old, there are theoretical attacks that can cause Helios and over light clients to sync incorrectly. These attacks are complex and expensive, so Helios disables this by default. + `--help` or `-h` prints the help message. ### Configuration Files diff --git a/cli/src/main.rs b/cli/src/main.rs index d62f03e..cee757d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -89,6 +89,8 @@ struct Cli { fallback: Option, #[clap(short = 'l', long, env)] load_external_fallback: bool, + #[clap(short = 's', long, env)] + strict_checkpoint_age: bool, } impl Cli { @@ -106,6 +108,7 @@ impl Cli { rpc_port: self.rpc_port, fallback: self.fallback.clone(), load_external_fallback: self.load_external_fallback, + strict_checkpoint_age: self.strict_checkpoint_age, } } diff --git a/client/src/client.rs b/client/src/client.rs index 0f87a82..f80e6ad 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use std::sync::Arc; use config::networks::Network; +use consensus::errors::ConsensusError; use ethers::prelude::{Address, U256}; use ethers::types::{Filter, Log, Transaction, TransactionReceipt, H256}; use eyre::{eyre, Result}; @@ -10,12 +11,13 @@ use common::types::BlockTag; use config::{CheckpointFallback, Config}; use consensus::{types::Header, ConsensusClient}; use execution::types::{CallOpts, ExecutionBlock}; -use log::{info, warn}; +use log::{error, info, warn}; use tokio::spawn; use tokio::sync::RwLock; use tokio::time::sleep; use crate::database::{Database, FileDB}; +use crate::errors::NodeError; use crate::node::Node; use crate::rpc::Rpc; @@ -30,6 +32,7 @@ pub struct ClientBuilder { config: Option, fallback: Option, load_external_fallback: bool, + strict_checkpoint_age: bool, } impl ClientBuilder { @@ -84,6 +87,11 @@ impl ClientBuilder { self } + pub fn strict_checkpoint_age(mut self) -> Self { + self.strict_checkpoint_age = true; + self + } + pub fn build(self) -> Result> { let base_config = if let Some(network) = self.network { network.to_base_config() @@ -149,6 +157,12 @@ impl ClientBuilder { self.load_external_fallback }; + let strict_checkpoint_age = if let Some(config) = &self.config { + self.strict_checkpoint_age || config.strict_checkpoint_age + } else { + self.strict_checkpoint_age + }; + let config = Config { consensus_rpc, execution_rpc, @@ -160,6 +174,7 @@ impl ClientBuilder { max_checkpoint_age: base_config.max_checkpoint_age, fallback, load_external_fallback, + strict_checkpoint_age, }; Client::new(config) @@ -201,16 +216,28 @@ impl Client { rpc.start().await?; } - if self.node.write().await.sync().await.is_err() { - warn!( - "failed to sync consensus node with checkpoint: 0x{}", - hex::encode(&self.node.read().await.config.checkpoint), - ); - let fallback = self.boot_from_fallback().await; - if fallback.is_err() && self.load_external_fallback { - self.boot_from_external_fallbacks().await? - } else if fallback.is_err() { - return Err(eyre::eyre!("Checkpoint is too old. Please update your checkpoint. Alternatively, set an explicit checkpoint fallback service url with the `-f` flag or use the configured external fallback services with `-l` (NOT RECOMMENDED). See https://github.com/a16z/helios#additional-options for more information.")); + let sync_res = self.node.write().await.sync().await; + + if let Err(err) = sync_res { + match err { + NodeError::ConsensusSyncError(err) => match err.downcast_ref().unwrap() { + ConsensusError::CheckpointTooOld => { + warn!( + "failed to sync consensus node with checkpoint: 0x{}", + hex::encode(&self.node.read().await.config.checkpoint), + ); + + let fallback = self.boot_from_fallback().await; + if fallback.is_err() && self.load_external_fallback { + self.boot_from_external_fallbacks().await? + } else if fallback.is_err() { + error!("Invalid checkpoint. Please update your checkpoint too a more recent block. Alternatively, set an explicit checkpoint fallback service url with the `-f` flag or use the configured external fallback services with `-l` (NOT RECOMMENDED). See https://github.com/a16z/helios#additional-options for more information."); + return Err(err); + } + } + _ => return Err(err), + }, + _ => return Err(err.into()), } } diff --git a/config/src/cli.rs b/config/src/cli.rs index 6c88f06..cd032ba 100644 --- a/config/src/cli.rs +++ b/config/src/cli.rs @@ -13,6 +13,7 @@ pub struct CliConfig { pub data_dir: PathBuf, pub fallback: Option, pub load_external_fallback: bool, + pub strict_checkpoint_age: bool, } impl CliConfig { @@ -46,6 +47,11 @@ impl CliConfig { Value::from(self.load_external_fallback), ); + user_dict.insert( + "strict_checkpoint_age", + Value::from(self.strict_checkpoint_age), + ); + Serialized::from(user_dict, network) } } diff --git a/config/src/config.rs b/config/src/config.rs index c2692a0..c19aee6 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -27,6 +27,7 @@ pub struct Config { pub max_checkpoint_age: u64, pub fallback: Option, pub load_external_fallback: bool, + pub strict_checkpoint_age: bool, } impl Config { diff --git a/consensus/src/consensus.rs b/consensus/src/consensus.rs index 8eb6258..dc1ea65 100644 --- a/consensus/src/consensus.rs +++ b/consensus/src/consensus.rs @@ -6,6 +6,7 @@ use blst::min_pk::PublicKey; use chrono::Duration; use eyre::eyre; use eyre::Result; +use log::warn; use log::{debug, info}; use ssz_rs::prelude::*; @@ -94,10 +95,6 @@ impl ConsensusClient { } pub async fn sync(&mut self) -> Result<()> { - info!( - "Consensus client in sync with checkpoint: 0x{}", - hex::encode(&self.initial_checkpoint) - ); self.bootstrap().await?; let current_period = calc_sync_period(self.store.finalized_header.slot); @@ -119,6 +116,11 @@ impl ConsensusClient { self.verify_optimistic_update(&optimistic_update)?; self.apply_optimistic_update(&optimistic_update); + info!( + "consensus client in sync with checkpoint: 0x{}", + hex::encode(&self.initial_checkpoint) + ); + Ok(()) } @@ -158,8 +160,13 @@ impl ConsensusClient { .map_err(|_| eyre!("could not fetch bootstrap"))?; let is_valid = self.is_valid_checkpoint(bootstrap.header.slot); + if !is_valid { - return Err(ConsensusError::CheckpointTooOld.into()); + if self.config.strict_checkpoint_age { + return Err(ConsensusError::CheckpointTooOld.into()); + } else { + warn!("checkpoint too old, consider using a more recent block"); + } } let committee_valid = is_current_committee_proof_valid( @@ -594,14 +601,14 @@ mod tests { }; use config::{networks, Config}; - async fn get_client(large_checkpoint_age: bool) -> ConsensusClient { + async fn get_client(strict_checkpoint_age: bool) -> ConsensusClient { let base_config = networks::goerli(); let config = Config { consensus_rpc: String::new(), execution_rpc: String::new(), chain: base_config.chain, forks: base_config.forks, - max_checkpoint_age: if large_checkpoint_age { 123123123 } else { 123 }, + strict_checkpoint_age, ..Default::default() }; @@ -616,7 +623,7 @@ mod tests { #[tokio::test] async fn test_verify_update() { - let client = get_client(true).await; + let client = get_client(false).await; let period = calc_sync_period(client.store.finalized_header.slot); let updates = client .rpc @@ -630,7 +637,7 @@ mod tests { #[tokio::test] async fn test_verify_update_invalid_committee() { - let client = get_client(true).await; + let client = get_client(false).await; let period = calc_sync_period(client.store.finalized_header.slot); let updates = client .rpc @@ -650,7 +657,7 @@ mod tests { #[tokio::test] async fn test_verify_update_invalid_finality() { - let client = get_client(true).await; + let client = get_client(false).await; let period = calc_sync_period(client.store.finalized_header.slot); let updates = client .rpc @@ -670,7 +677,7 @@ mod tests { #[tokio::test] async fn test_verify_update_invalid_sig() { - let client = get_client(true).await; + let client = get_client(false).await; let period = calc_sync_period(client.store.finalized_header.slot); let updates = client .rpc @@ -690,7 +697,7 @@ mod tests { #[tokio::test] async fn test_verify_finality() { - let mut client = get_client(true).await; + let mut client = get_client(false).await; client.sync().await.unwrap(); let update = client.rpc.get_finality_update().await.unwrap(); @@ -700,7 +707,7 @@ mod tests { #[tokio::test] async fn test_verify_finality_invalid_finality() { - let mut client = get_client(true).await; + let mut client = get_client(false).await; client.sync().await.unwrap(); let mut update = client.rpc.get_finality_update().await.unwrap(); @@ -715,7 +722,7 @@ mod tests { #[tokio::test] async fn test_verify_finality_invalid_sig() { - let mut client = get_client(true).await; + let mut client = get_client(false).await; client.sync().await.unwrap(); let mut update = client.rpc.get_finality_update().await.unwrap(); @@ -730,7 +737,7 @@ mod tests { #[tokio::test] async fn test_verify_optimistic() { - let mut client = get_client(true).await; + let mut client = get_client(false).await; client.sync().await.unwrap(); let update = client.rpc.get_optimistic_update().await.unwrap(); @@ -739,7 +746,7 @@ mod tests { #[tokio::test] async fn test_verify_optimistic_invalid_sig() { - let mut client = get_client(true).await; + let mut client = get_client(false).await; client.sync().await.unwrap(); let mut update = client.rpc.get_optimistic_update().await.unwrap(); @@ -755,6 +762,6 @@ mod tests { #[tokio::test] #[should_panic] async fn test_verify_checkpoint_age_invalid() { - get_client(false).await; + get_client(true).await; } }