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:
parent
69b8108dae
commit
cb6cf75d59
|
@ -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 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).
|
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.
|
`--help` or `-h` prints the help message.
|
||||||
|
|
||||||
### Configuration Files
|
### Configuration Files
|
||||||
|
|
|
@ -89,6 +89,8 @@ struct Cli {
|
||||||
fallback: Option<String>,
|
fallback: Option<String>,
|
||||||
#[clap(short = 'l', long, env)]
|
#[clap(short = 'l', long, env)]
|
||||||
load_external_fallback: bool,
|
load_external_fallback: bool,
|
||||||
|
#[clap(short = 's', long, env)]
|
||||||
|
strict_checkpoint_age: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
|
@ -106,6 +108,7 @@ impl Cli {
|
||||||
rpc_port: self.rpc_port,
|
rpc_port: self.rpc_port,
|
||||||
fallback: self.fallback.clone(),
|
fallback: self.fallback.clone(),
|
||||||
load_external_fallback: self.load_external_fallback,
|
load_external_fallback: self.load_external_fallback,
|
||||||
|
strict_checkpoint_age: self.strict_checkpoint_age,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use config::networks::Network;
|
use config::networks::Network;
|
||||||
|
use consensus::errors::ConsensusError;
|
||||||
use ethers::prelude::{Address, U256};
|
use ethers::prelude::{Address, U256};
|
||||||
use ethers::types::{Filter, Log, Transaction, TransactionReceipt, H256};
|
use ethers::types::{Filter, Log, Transaction, TransactionReceipt, H256};
|
||||||
use eyre::{eyre, Result};
|
use eyre::{eyre, Result};
|
||||||
|
@ -10,12 +11,13 @@ use common::types::BlockTag;
|
||||||
use config::{CheckpointFallback, Config};
|
use config::{CheckpointFallback, Config};
|
||||||
use consensus::{types::Header, ConsensusClient};
|
use consensus::{types::Header, ConsensusClient};
|
||||||
use execution::types::{CallOpts, ExecutionBlock};
|
use execution::types::{CallOpts, ExecutionBlock};
|
||||||
use log::{info, warn};
|
use log::{error, info, warn};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::database::{Database, FileDB};
|
use crate::database::{Database, FileDB};
|
||||||
|
use crate::errors::NodeError;
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
use crate::rpc::Rpc;
|
use crate::rpc::Rpc;
|
||||||
|
|
||||||
|
@ -30,6 +32,7 @@ pub struct ClientBuilder {
|
||||||
config: Option<Config>,
|
config: Option<Config>,
|
||||||
fallback: Option<String>,
|
fallback: Option<String>,
|
||||||
load_external_fallback: bool,
|
load_external_fallback: bool,
|
||||||
|
strict_checkpoint_age: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientBuilder {
|
impl ClientBuilder {
|
||||||
|
@ -84,6 +87,11 @@ impl ClientBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn strict_checkpoint_age(mut self) -> Self {
|
||||||
|
self.strict_checkpoint_age = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Result<Client<FileDB>> {
|
pub fn build(self) -> Result<Client<FileDB>> {
|
||||||
let base_config = if let Some(network) = self.network {
|
let base_config = if let Some(network) = self.network {
|
||||||
network.to_base_config()
|
network.to_base_config()
|
||||||
|
@ -149,6 +157,12 @@ impl ClientBuilder {
|
||||||
self.load_external_fallback
|
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 {
|
let config = Config {
|
||||||
consensus_rpc,
|
consensus_rpc,
|
||||||
execution_rpc,
|
execution_rpc,
|
||||||
|
@ -160,6 +174,7 @@ impl ClientBuilder {
|
||||||
max_checkpoint_age: base_config.max_checkpoint_age,
|
max_checkpoint_age: base_config.max_checkpoint_age,
|
||||||
fallback,
|
fallback,
|
||||||
load_external_fallback,
|
load_external_fallback,
|
||||||
|
strict_checkpoint_age,
|
||||||
};
|
};
|
||||||
|
|
||||||
Client::new(config)
|
Client::new(config)
|
||||||
|
@ -201,16 +216,28 @@ impl<DB: Database> Client<DB> {
|
||||||
rpc.start().await?;
|
rpc.start().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.node.write().await.sync().await.is_err() {
|
let sync_res = self.node.write().await.sync().await;
|
||||||
warn!(
|
|
||||||
"failed to sync consensus node with checkpoint: 0x{}",
|
if let Err(err) = sync_res {
|
||||||
hex::encode(&self.node.read().await.config.checkpoint),
|
match err {
|
||||||
);
|
NodeError::ConsensusSyncError(err) => match err.downcast_ref().unwrap() {
|
||||||
let fallback = self.boot_from_fallback().await;
|
ConsensusError::CheckpointTooOld => {
|
||||||
if fallback.is_err() && self.load_external_fallback {
|
warn!(
|
||||||
self.boot_from_external_fallbacks().await?
|
"failed to sync consensus node with checkpoint: 0x{}",
|
||||||
} else if fallback.is_err() {
|
hex::encode(&self.node.read().await.config.checkpoint),
|
||||||
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 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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ pub struct CliConfig {
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
pub fallback: Option<String>,
|
pub fallback: Option<String>,
|
||||||
pub load_external_fallback: bool,
|
pub load_external_fallback: bool,
|
||||||
|
pub strict_checkpoint_age: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CliConfig {
|
impl CliConfig {
|
||||||
|
@ -46,6 +47,11 @@ impl CliConfig {
|
||||||
Value::from(self.load_external_fallback),
|
Value::from(self.load_external_fallback),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
user_dict.insert(
|
||||||
|
"strict_checkpoint_age",
|
||||||
|
Value::from(self.strict_checkpoint_age),
|
||||||
|
);
|
||||||
|
|
||||||
Serialized::from(user_dict, network)
|
Serialized::from(user_dict, network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ pub struct Config {
|
||||||
pub max_checkpoint_age: u64,
|
pub max_checkpoint_age: u64,
|
||||||
pub fallback: Option<String>,
|
pub fallback: Option<String>,
|
||||||
pub load_external_fallback: bool,
|
pub load_external_fallback: bool,
|
||||||
|
pub strict_checkpoint_age: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
|
@ -6,6 +6,7 @@ use blst::min_pk::PublicKey;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use eyre::eyre;
|
use eyre::eyre;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
|
use log::warn;
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use ssz_rs::prelude::*;
|
use ssz_rs::prelude::*;
|
||||||
|
|
||||||
|
@ -94,10 +95,6 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sync(&mut self) -> Result<()> {
|
pub async fn sync(&mut self) -> Result<()> {
|
||||||
info!(
|
|
||||||
"Consensus client in sync with checkpoint: 0x{}",
|
|
||||||
hex::encode(&self.initial_checkpoint)
|
|
||||||
);
|
|
||||||
self.bootstrap().await?;
|
self.bootstrap().await?;
|
||||||
|
|
||||||
let current_period = calc_sync_period(self.store.finalized_header.slot);
|
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.verify_optimistic_update(&optimistic_update)?;
|
||||||
self.apply_optimistic_update(&optimistic_update);
|
self.apply_optimistic_update(&optimistic_update);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"consensus client in sync with checkpoint: 0x{}",
|
||||||
|
hex::encode(&self.initial_checkpoint)
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,8 +160,13 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
|
||||||
.map_err(|_| eyre!("could not fetch bootstrap"))?;
|
.map_err(|_| eyre!("could not fetch bootstrap"))?;
|
||||||
|
|
||||||
let is_valid = self.is_valid_checkpoint(bootstrap.header.slot);
|
let is_valid = self.is_valid_checkpoint(bootstrap.header.slot);
|
||||||
|
|
||||||
if !is_valid {
|
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(
|
let committee_valid = is_current_committee_proof_valid(
|
||||||
|
@ -594,14 +601,14 @@ mod tests {
|
||||||
};
|
};
|
||||||
use config::{networks, Config};
|
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 base_config = networks::goerli();
|
||||||
let config = Config {
|
let config = Config {
|
||||||
consensus_rpc: String::new(),
|
consensus_rpc: String::new(),
|
||||||
execution_rpc: String::new(),
|
execution_rpc: String::new(),
|
||||||
chain: base_config.chain,
|
chain: base_config.chain,
|
||||||
forks: base_config.forks,
|
forks: base_config.forks,
|
||||||
max_checkpoint_age: if large_checkpoint_age { 123123123 } else { 123 },
|
strict_checkpoint_age,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -616,7 +623,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_update() {
|
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 period = calc_sync_period(client.store.finalized_header.slot);
|
||||||
let updates = client
|
let updates = client
|
||||||
.rpc
|
.rpc
|
||||||
|
@ -630,7 +637,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_update_invalid_committee() {
|
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 period = calc_sync_period(client.store.finalized_header.slot);
|
||||||
let updates = client
|
let updates = client
|
||||||
.rpc
|
.rpc
|
||||||
|
@ -650,7 +657,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_update_invalid_finality() {
|
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 period = calc_sync_period(client.store.finalized_header.slot);
|
||||||
let updates = client
|
let updates = client
|
||||||
.rpc
|
.rpc
|
||||||
|
@ -670,7 +677,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_update_invalid_sig() {
|
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 period = calc_sync_period(client.store.finalized_header.slot);
|
||||||
let updates = client
|
let updates = client
|
||||||
.rpc
|
.rpc
|
||||||
|
@ -690,7 +697,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_finality() {
|
async fn test_verify_finality() {
|
||||||
let mut client = get_client(true).await;
|
let mut client = get_client(false).await;
|
||||||
client.sync().await.unwrap();
|
client.sync().await.unwrap();
|
||||||
|
|
||||||
let update = client.rpc.get_finality_update().await.unwrap();
|
let update = client.rpc.get_finality_update().await.unwrap();
|
||||||
|
@ -700,7 +707,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_finality_invalid_finality() {
|
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();
|
client.sync().await.unwrap();
|
||||||
|
|
||||||
let mut update = client.rpc.get_finality_update().await.unwrap();
|
let mut update = client.rpc.get_finality_update().await.unwrap();
|
||||||
|
@ -715,7 +722,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_finality_invalid_sig() {
|
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();
|
client.sync().await.unwrap();
|
||||||
|
|
||||||
let mut update = client.rpc.get_finality_update().await.unwrap();
|
let mut update = client.rpc.get_finality_update().await.unwrap();
|
||||||
|
@ -730,7 +737,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_optimistic() {
|
async fn test_verify_optimistic() {
|
||||||
let mut client = get_client(true).await;
|
let mut client = get_client(false).await;
|
||||||
client.sync().await.unwrap();
|
client.sync().await.unwrap();
|
||||||
|
|
||||||
let update = client.rpc.get_optimistic_update().await.unwrap();
|
let update = client.rpc.get_optimistic_update().await.unwrap();
|
||||||
|
@ -739,7 +746,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_verify_optimistic_invalid_sig() {
|
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();
|
client.sync().await.unwrap();
|
||||||
|
|
||||||
let mut update = client.rpc.get_optimistic_update().await.unwrap();
|
let mut update = client.rpc.get_optimistic_update().await.unwrap();
|
||||||
|
@ -755,6 +762,6 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
async fn test_verify_checkpoint_age_invalid() {
|
async fn test_verify_checkpoint_age_invalid() {
|
||||||
get_client(false).await;
|
get_client(true).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue