feat: make checkpoint age check optional (#170)

* feat: make checkpoint age check optional

* add new flag to readme

* fix tests
This commit is contained in:
Noah Citron 2023-01-18 21:18:26 -05:00 committed by GitHub
parent 69b8108dae
commit cb6cf75d59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 28 deletions

View File

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

View File

@ -89,6 +89,8 @@ struct Cli {
fallback: Option<String>,
#[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,
}
}

View File

@ -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<Config>,
fallback: Option<String>,
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<Client<FileDB>> {
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<DB: Database> Client<DB> {
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()),
}
}

View File

@ -13,6 +13,7 @@ pub struct CliConfig {
pub data_dir: PathBuf,
pub fallback: Option<String>,
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)
}
}

View File

@ -27,6 +27,7 @@ pub struct Config {
pub max_checkpoint_age: u64,
pub fallback: Option<String>,
pub load_external_fallback: bool,
pub strict_checkpoint_age: bool,
}
impl Config {

View File

@ -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<R: ConsensusRpc> ConsensusClient<R> {
}
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<R: ConsensusRpc> ConsensusClient<R> {
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<R: ConsensusRpc> ConsensusClient<R> {
.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<MockRpc> {
async fn get_client(strict_checkpoint_age: bool) -> ConsensusClient<MockRpc> {
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;
}
}