From f605f009a70ad84ed2212f3c3b83eb3ed69c6588 Mon Sep 17 00:00:00 2001 From: Noah Citron Date: Thu, 3 Nov 2022 15:24:17 -0400 Subject: [PATCH] feat: add client builder (#84) * add client builder * use client builder in cli * make build sync * fix data dir override * fix tests --- cli/src/main.rs | 17 +++-- client/src/client.rs | 130 ++++++++++++++++++++++++++++++++++++- client/src/node.rs | 5 +- config/src/lib.rs | 15 ++++- config/src/networks.rs | 16 ++++- consensus/src/consensus.rs | 87 ++++++++++++++----------- consensus/tests/sync.rs | 4 +- 7 files changed, 219 insertions(+), 55 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 34301b1..b8bc870 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,6 +2,7 @@ use std::{ fs, path::PathBuf, process::exit, + str::FromStr, sync::{Arc, Mutex}, }; @@ -11,7 +12,7 @@ use dirs::home_dir; use env_logger::Env; use eyre::Result; -use client::{database::FileDB, Client}; +use client::{database::FileDB, Client, ClientBuilder}; use config::{CliConfig, Config}; use futures::executor::block_on; use log::info; @@ -21,7 +22,7 @@ async fn main() -> Result<()> { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); let config = get_config(); - let mut client = Client::new(config).await?; + let mut client = ClientBuilder::new().config(config).build()?; client.start().await?; @@ -82,6 +83,8 @@ struct Cli { execution_rpc: Option, #[clap(short, long, env)] consensus_rpc: Option, + #[clap(short, long, env)] + data_dir: Option, } impl Cli { @@ -116,8 +119,12 @@ impl Cli { } fn get_data_dir(&self) -> PathBuf { - home_dir() - .unwrap() - .join(format!(".helios/data/{}", self.network)) + if let Some(dir) = &self.data_dir { + PathBuf::from_str(dir).expect("cannot find data dir") + } else { + home_dir() + .unwrap() + .join(format!(".helios/data/{}", self.network)) + } } } diff --git a/client/src/client.rs b/client/src/client.rs index 703b1b1..5ba062c 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -1,5 +1,7 @@ +use std::path::PathBuf; use std::sync::Arc; +use config::networks::Network; use ethers::prelude::{Address, U256}; use ethers::types::{Transaction, TransactionReceipt, H256}; use eyre::{eyre, Result}; @@ -24,9 +26,9 @@ pub struct Client { } impl Client { - pub async fn new(config: Config) -> Result { + fn new(config: Config) -> Result { let config = Arc::new(config); - let node = Node::new(config.clone()).await?; + let node = Node::new(config.clone())?; let node = Arc::new(RwLock::new(node)); let rpc = if let Some(port) = config.rpc_port { @@ -42,6 +44,130 @@ impl Client { } } +pub struct ClientBuilder { + network: Option, + consensus_rpc: Option, + execution_rpc: Option, + checkpoint: Option>, + rpc_port: Option, + data_dir: Option, + config: Option, +} + +impl ClientBuilder { + pub fn new() -> Self { + Self { + network: None, + consensus_rpc: None, + execution_rpc: None, + checkpoint: None, + rpc_port: None, + data_dir: None, + config: None, + } + } + + pub fn network(mut self, network: Network) -> Self { + self.network = Some(network); + self + } + + pub fn consensus_rpc(mut self, consensus_rpc: &str) -> Self { + self.consensus_rpc = Some(consensus_rpc.to_string()); + self + } + + pub fn execution_rpc(mut self, execution_rpc: &str) -> Self { + self.execution_rpc = Some(execution_rpc.to_string()); + self + } + + pub fn checkpoint(mut self, checkpoint: &str) -> Self { + let checkpoint = hex::decode(checkpoint).expect("cannot parse checkpoint"); + self.checkpoint = Some(checkpoint); + self + } + + pub fn rpc_port(mut self, port: u16) -> Self { + self.rpc_port = Some(port); + self + } + + pub fn data_dir(mut self, data_dir: PathBuf) -> Self { + self.data_dir = Some(data_dir); + self + } + + pub fn config(mut self, config: Config) -> Self { + self.config = Some(config); + self + } + + pub fn build(self) -> Result> { + let base_config = if let Some(network) = self.network { + network.to_base_config() + } else { + let config = self + .config + .as_ref() + .ok_or(eyre!("missing network config"))?; + config.to_base_config() + }; + + let consensus_rpc = self.consensus_rpc.unwrap_or( + self.config + .as_ref() + .ok_or(eyre!("missing consensus rpc"))? + .consensus_rpc + .clone(), + ); + + let execution_rpc = self.execution_rpc.unwrap_or( + self.config + .as_ref() + .ok_or(eyre!("missing execution rpc"))? + .execution_rpc + .clone(), + ); + + let checkpoint = self.checkpoint.unwrap_or( + self.config + .as_ref() + .ok_or(eyre!("missing checkpoint"))? + .checkpoint + .clone(), + ); + + let rpc_port = if self.rpc_port.is_some() { + self.rpc_port + } else if let Some(config) = &self.config { + config.rpc_port + } else { + None + }; + + let data_dir = if self.data_dir.is_some() { + self.data_dir + } else if let Some(config) = &self.config { + config.data_dir.clone() + } else { + None + }; + + let config = Config { + consensus_rpc, + execution_rpc, + checkpoint, + rpc_port, + data_dir, + chain: base_config.chain, + forks: base_config.forks, + }; + + Client::new(config) + } +} + impl Client { pub async fn start(&mut self) -> Result<()> { self.rpc.as_mut().unwrap().start().await?; diff --git a/client/src/node.rs b/client/src/node.rs index 1befb44..e2282fe 100644 --- a/client/src/node.rs +++ b/client/src/node.rs @@ -27,13 +27,12 @@ pub struct Node { } impl Node { - pub async fn new(config: Arc) -> Result { + pub fn new(config: Arc) -> Result { let consensus_rpc = &config.consensus_rpc; let checkpoint_hash = &config.checkpoint; let execution_rpc = &config.execution_rpc; - let consensus = - ConsensusClient::new(consensus_rpc, checkpoint_hash, config.clone()).await?; + let consensus = ConsensusClient::new(consensus_rpc, checkpoint_hash, config.clone())?; let execution = Arc::new(ExecutionClient::new(execution_rpc)?); let payloads = BTreeMap::new(); diff --git a/config/src/lib.rs b/config/src/lib.rs index 945eec5..fdad5a8 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -85,6 +85,15 @@ impl Config { self.forks.genesis.fork_version.clone() } } + + pub fn to_base_config(&self) -> BaseConfig { + BaseConfig { + rpc_port: self.rpc_port.unwrap_or(8545), + checkpoint: self.checkpoint.clone(), + chain: self.chain.clone(), + forks: self.forks.clone(), + } + } } #[derive(Serialize)] @@ -122,7 +131,7 @@ impl CliConfig { } } -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct ChainConfig { pub chain_id: u64, pub genesis_time: u64, @@ -133,14 +142,14 @@ pub struct ChainConfig { pub genesis_root: Vec, } -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct Forks { pub genesis: Fork, pub altair: Fork, pub bellatrix: Fork, } -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct Fork { pub epoch: u64, #[serde( diff --git a/config/src/networks.rs b/config/src/networks.rs index 57033c0..ff98716 100644 --- a/config/src/networks.rs +++ b/config/src/networks.rs @@ -3,9 +3,23 @@ use serde::Serialize; use crate::{bytes_serialize, ChainConfig, Fork, Forks}; use common::utils::hex_str_to_bytes; +pub enum Network { + MAINNET, + GOERLI, +} + +impl Network { + pub fn to_base_config(&self) -> BaseConfig { + match self { + Self::MAINNET => mainnet(), + Self::GOERLI => goerli(), + } + } +} + #[derive(Serialize, Default)] pub struct BaseConfig { - rpc_port: u16, + pub rpc_port: u16, #[serde( deserialize_with = "bytes_deserialize", serialize_with = "bytes_serialize" diff --git a/consensus/src/consensus.rs b/consensus/src/consensus.rs index e6ed07c..c7f00c2 100644 --- a/consensus/src/consensus.rs +++ b/consensus/src/consensus.rs @@ -25,11 +25,12 @@ use super::utils::*; pub struct ConsensusClient { rpc: R, store: LightClientStore, + initial_checkpoint: Vec, pub last_checkpoint: Option>, pub config: Arc, } -#[derive(Debug)] +#[derive(Debug, Default)] struct LightClientStore { finalized_header: Header, current_sync_committee: SyncCommittee, @@ -40,50 +41,19 @@ struct LightClientStore { } impl ConsensusClient { - pub async fn new( + pub fn new( rpc: &str, checkpoint_block_root: &Vec, config: Arc, ) -> Result> { let rpc = R::new(rpc); - let mut bootstrap = rpc - .get_bootstrap(checkpoint_block_root) - .await - .map_err(|_| eyre!("could not fetch bootstrap"))?; - - let committee_valid = is_current_committee_proof_valid( - &bootstrap.header, - &mut bootstrap.current_sync_committee, - &bootstrap.current_sync_committee_branch, - ); - - let header_hash = bootstrap.header.hash_tree_root()?.to_string(); - let expected_hash = format!("0x{}", hex::encode(checkpoint_block_root)); - let header_valid = header_hash == expected_hash; - - if !header_valid { - return Err(ConsensusError::InvalidHeaderHash(expected_hash, header_hash).into()); - } - - if !committee_valid { - return Err(ConsensusError::InvalidCurrentSyncCommitteeProof.into()); - } - - let store = LightClientStore { - finalized_header: bootstrap.header.clone(), - current_sync_committee: bootstrap.current_sync_committee, - next_sync_committee: None, - optimistic_header: bootstrap.header.clone(), - previous_max_active_participants: 0, - current_max_active_participants: 0, - }; - Ok(ConsensusClient { rpc, - store, + store: LightClientStore::default(), last_checkpoint: None, config, + initial_checkpoint: checkpoint_block_root.clone(), }) } @@ -123,6 +93,8 @@ impl ConsensusClient { } pub async fn sync(&mut self) -> Result<()> { + self.bootstrap().await?; + let current_period = calc_sync_period(self.store.finalized_header.slot); let updates = self.rpc.get_updates(current_period).await?; @@ -170,6 +142,43 @@ impl ConsensusClient { Ok(()) } + async fn bootstrap(&mut self) -> Result<()> { + let mut bootstrap = self + .rpc + .get_bootstrap(&self.initial_checkpoint) + .await + .map_err(|_| eyre!("could not fetch bootstrap"))?; + + let committee_valid = is_current_committee_proof_valid( + &bootstrap.header, + &mut bootstrap.current_sync_committee, + &bootstrap.current_sync_committee_branch, + ); + + let header_hash = bootstrap.header.hash_tree_root()?.to_string(); + let expected_hash = format!("0x{}", hex::encode(&self.initial_checkpoint)); + let header_valid = header_hash == expected_hash; + + if !header_valid { + return Err(ConsensusError::InvalidHeaderHash(expected_hash, header_hash).into()); + } + + if !committee_valid { + return Err(ConsensusError::InvalidCurrentSyncCommitteeProof.into()); + } + + self.store = LightClientStore { + finalized_header: bootstrap.header.clone(), + current_sync_committee: bootstrap.current_sync_committee, + next_sync_committee: None, + optimistic_header: bootstrap.header.clone(), + previous_max_active_participants: 0, + current_max_active_participants: 0, + }; + + Ok(()) + } + // implements checks from validate_light_client_update and process_light_client_update in the // specification fn verify_generic_update(&self, update: &GenericUpdate) -> Result<()> { @@ -570,9 +579,11 @@ mod tests { ..Default::default() }; - ConsensusClient::new("testdata/", &base_config.checkpoint, Arc::new(config)) - .await - .unwrap() + let mut client = + ConsensusClient::new("testdata/", &base_config.checkpoint, Arc::new(config)).unwrap(); + client.bootstrap().await.unwrap(); + + client } #[tokio::test] diff --git a/consensus/tests/sync.rs b/consensus/tests/sync.rs index 502e75b..02f137d 100644 --- a/consensus/tests/sync.rs +++ b/consensus/tests/sync.rs @@ -13,9 +13,7 @@ async fn setup() -> ConsensusClient { ..Default::default() }; - ConsensusClient::new("testdata/", &base_config.checkpoint, Arc::new(config)) - .await - .unwrap() + ConsensusClient::new("testdata/", &base_config.checkpoint, Arc::new(config)).unwrap() } #[tokio::test]