diff --git a/Cargo.lock b/Cargo.lock index 83f908bb..130a18e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1626,6 +1626,7 @@ dependencies = [ "futures", "jsonwebtoken", "lazy_static", + "notify", "pbkdf2 0.12.2", "rand 0.9.2", "rayon", @@ -1675,6 +1676,7 @@ dependencies = [ "eyre", "futures", "lazy_static", + "notify", "parking_lot", "prometheus", "reqwest 0.12.23", @@ -1731,6 +1733,7 @@ dependencies = [ "serde_json", "tempfile", "tokio", + "toml", "tracing", "tracing-subscriber", "tracing-test", @@ -3061,6 +3064,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "funty" version = "2.0.0" @@ -3809,6 +3821,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.4", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.4" @@ -3964,6 +3996,26 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "kzg" version = "0.1.0" @@ -4247,6 +4299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -4325,6 +4378,30 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.9.4", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -5570,6 +5647,15 @@ dependencies = [ "cipher 0.3.0", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -7144,6 +7230,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -7348,6 +7444,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 23679360..c468bca1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ lazy_static = "1.5.0" lh_eth2 = { package = "eth2", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0" } lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0" } lh_types = { package = "types", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0" } +notify = "8.2.0" parking_lot = "0.12.3" pbkdf2 = "0.12.2" prometheus = "0.14.0" diff --git a/bin/pbs.rs b/bin/pbs.rs index 69945fe8..640c515a 100644 --- a/bin/pbs.rs +++ b/bin/pbs.rs @@ -15,10 +15,10 @@ async fn main() -> Result<()> { let _args = cb_cli::PbsArgs::parse(); - let pbs_config = load_pbs_config().await?; + let (pbs_config, config_path) = load_pbs_config(None).await?; PbsService::init_metrics(pbs_config.chain)?; - let state = PbsState::new(pbs_config); + let state = PbsState::new(pbs_config, config_path); let server = PbsService::run::<_, DefaultBuilderApi>(state); tokio::select! { diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 35367a12..5faaf031 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -30,6 +30,7 @@ lazy_static.workspace = true lh_eth2.workspace = true lh_eth2_keystore.workspace = true lh_types.workspace = true +notify.workspace = true pbkdf2.workspace = true rand.workspace = true rayon.workspace = true diff --git a/crates/common/src/config/log.rs b/crates/common/src/config/log.rs index 595a81a1..a792ebc8 100644 --- a/crates/common/src/config/log.rs +++ b/crates/common/src/config/log.rs @@ -16,7 +16,7 @@ pub struct LogsSettings { impl LogsSettings { pub fn from_env_config() -> Result { - let mut config = CommitBoostConfig::from_env_path()?; + let (mut config, _) = CommitBoostConfig::from_env_path()?; // Override log dir path if env var is set if let Some(log_dir) = load_optional_env_var(LOGS_DIR_ENV) { diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 67a13cb0..c833d8e3 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -56,14 +56,14 @@ impl CommitBoostConfig { } pub fn from_file(path: &PathBuf) -> Result { - let config: Self = load_from_file(path)?; + let (config, _): (Self, _) = load_from_file(path)?; Ok(config) } // When loading the config from the environment, it's important that every path // is replaced with the correct value if the config is loaded inside a container - pub fn from_env_path() -> Result { - let helper_config: HelperConfig = load_file_from_env(CONFIG_ENV)?; + pub fn from_env_path() -> Result<(Self, PathBuf)> { + let (helper_config, config_path): (HelperConfig, PathBuf) = load_file_from_env(CONFIG_ENV)?; let chain = match helper_config.chain { ChainLoader::Path { path, genesis_time_secs } => { @@ -109,13 +109,13 @@ impl CommitBoostConfig { logs: helper_config.logs, }; - Ok(config) + Ok((config, config_path)) } /// Returns the path to the chain spec file if any pub fn chain_spec_file(path: &PathBuf) -> Option { match load_from_file::<_, ChainConfig>(path) { - Ok(config) => { + Ok((config, _)) => { if let ChainLoader::Path { path, genesis_time_secs: _ } = config.chain { Some(path) } else { diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index 02fa90da..6f5fdd3e 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -82,7 +82,7 @@ pub fn load_commit_module_config() -> Result = load_file_from_env(CONFIG_ENV)?; + let (cb_config, _): (StubConfig, _) = load_file_from_env(CONFIG_ENV)?; // find all matching modules config let matches: Vec> = cb_config @@ -148,7 +148,7 @@ pub fn load_builder_module_config() -> eyre::Result = load_file_from_env(CONFIG_ENV)?; + let (cb_config, _): (StubConfig, _) = load_file_from_env(CONFIG_ENV)?; // find all matching modules config let matches: Vec> = cb_config diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index 7bcf91e3..f24e75c4 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -3,6 +3,7 @@ use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddr}, + path::PathBuf, sync::Arc, }; @@ -242,8 +243,11 @@ fn default_pbs() -> String { } /// Loads the default pbs config, i.e. with no signer client or custom data -pub async fn load_pbs_config() -> Result { - let config = CommitBoostConfig::from_env_path()?; +pub async fn load_pbs_config(config_path: Option) -> Result<(PbsModuleConfig, PathBuf)> { + let (config, config_path) = match config_path { + Some(path) => (CommitBoostConfig::from_file(&path)?, path), + None => CommitBoostConfig::from_env_path()?, + }; config.validate().await?; // Make sure relays isn't empty - since the config is still technically valid if @@ -295,16 +299,19 @@ pub async fn load_pbs_config() -> Result { let all_relays = all_relays.into_values().collect(); - Ok(PbsModuleConfig { - chain: config.chain, - endpoint, - pbs_config: Arc::new(config.pbs.pbs_config), - relays: relay_clients, - all_relays, - signer_client: None, - registry_muxes, - mux_lookup, - }) + Ok(( + PbsModuleConfig { + chain: config.chain, + endpoint, + pbs_config: Arc::new(config.pbs.pbs_config), + relays: relay_clients, + all_relays, + signer_client: None, + registry_muxes, + mux_lookup, + }, + config_path, + )) } /// Loads a custom pbs config, i.e. with signer client and/or custom data @@ -326,7 +333,7 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC } // load module config including the extra data (if any) - let cb_config: StubConfig = load_file_from_env(CONFIG_ENV)?; + let (cb_config, _): (StubConfig, _) = load_file_from_env(CONFIG_ENV)?; cb_config.pbs.static_config.pbs_config.validate(cb_config.chain).await?; // use endpoint from env if set, otherwise use default host and port diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index bc1d2c45..7c27d516 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -138,7 +138,7 @@ pub struct StartSignerConfig { impl StartSignerConfig { pub fn load_from_env() -> Result { - let config = CommitBoostConfig::from_env_path()?; + let (config, _) = CommitBoostConfig::from_env_path()?; let jwts = load_jwt_secrets()?; diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index b956df59..a8fcbacd 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, path::Path}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use eyre::{Context, Result, bail}; use serde::de::DeserializeOwned; @@ -17,13 +20,18 @@ pub fn load_optional_env_var(env: &str) -> Option { std::env::var(env).ok() } -pub fn load_from_file + std::fmt::Debug, T: DeserializeOwned>(path: P) -> Result { +pub fn load_from_file + std::fmt::Debug, T: DeserializeOwned>( + path: P, +) -> Result<(T, PathBuf)> { let config_file = std::fs::read_to_string(path.as_ref()) .wrap_err(format!("Unable to find config file: {path:?}"))?; - toml::from_str(&config_file).wrap_err("could not deserialize toml from string") + match toml::from_str(&config_file).wrap_err("could not deserialize toml from string") { + Ok(config) => Ok((config, path.as_ref().to_path_buf())), + Err(e) => Err(e), + } } -pub fn load_file_from_env(env: &str) -> Result { +pub fn load_file_from_env(env: &str) -> Result<(T, PathBuf)> { let path = std::env::var(env).wrap_err(format!("{env} is not set"))?; load_from_file(&path) } diff --git a/crates/pbs/Cargo.toml b/crates/pbs/Cargo.toml index e8cb0b31..a9124c06 100644 --- a/crates/pbs/Cargo.toml +++ b/crates/pbs/Cargo.toml @@ -15,6 +15,7 @@ cb-metrics.workspace = true eyre.workspace = true futures.workspace = true lazy_static.workspace = true +notify.workspace = true parking_lot.workspace = true prometheus.workspace = true reqwest.workspace = true diff --git a/crates/pbs/src/mev_boost/reload.rs b/crates/pbs/src/mev_boost/reload.rs index 0a0555b6..adfab89f 100644 --- a/crates/pbs/src/mev_boost/reload.rs +++ b/crates/pbs/src/mev_boost/reload.rs @@ -6,8 +6,8 @@ use crate::{BuilderApiState, PbsState}; /// Reload the PBS state with the latest configuration in the config file /// Returns 200 if successful or 500 if failed pub async fn reload(state: PbsState) -> eyre::Result> { - let pbs_config = load_pbs_config().await?; - let new_state = PbsState::new(pbs_config).with_data(state.data); + let (pbs_config, config_path) = load_pbs_config(None).await?; + let new_state = PbsState::new(pbs_config, config_path).with_data(state.data); if state.config.pbs_config.host != new_state.config.pbs_config.host { warn!( diff --git a/crates/pbs/src/service.rs b/crates/pbs/src/service.rs index 6659ae85..8c606074 100644 --- a/crates/pbs/src/service.rs +++ b/crates/pbs/src/service.rs @@ -5,13 +5,14 @@ use std::{ }; use cb_common::{ - config::{MuxKeysLoader, PbsModuleConfig}, + config::{MuxKeysLoader, PbsModuleConfig, load_pbs_config}, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, pbs::{BUILDER_V1_API_PATH, GET_STATUS_PATH}, types::Chain, }; use cb_metrics::provider::MetricsProvider; use eyre::{Context, Result, bail}; +use notify::{Error, Event, RecommendedWatcher, RecursiveMode, Watcher}; use parking_lot::RwLock; use prometheus::core::Collector; use tokio::net::TcpListener; @@ -40,6 +41,7 @@ impl PbsService { }) }); + let config_path = state.config_path.clone(); let state: Arc>> = RwLock::new(state).into(); let app = create_app_router::(state.clone()); let listener = TcpListener::bind(addr).await?; @@ -59,6 +61,38 @@ impl PbsService { bail!("PBS server failed to start. Are the relays properly configured?"); } + // Set up the filesystem watcher for the config file + let mut watcher: RecommendedWatcher; + if config_path.to_str() != Some("") { + let state_for_watcher = state.clone(); + let config_path_for_watcher = config_path.clone(); + watcher = RecommendedWatcher::new( + move |result: Result| { + let event = result.unwrap(); + if !event.kind.is_modify() { + return; + } + + // Reload the configuration when the file is modified + let result = futures::executor::block_on(load_pbs_config(Some( + config_path_for_watcher.to_path_buf(), + ))); + match result { + Ok((new_config, _)) => { + let mut state = state_for_watcher.write(); + state.config = Arc::new(new_config); + info!("configuration reloaded from file after update"); + } + Err(err) => { + warn!(%err, "failed to reload configuration from file after update"); + } + } + }, + notify::Config::default(), + )?; + watcher.watch(config_path.as_path(), RecursiveMode::Recursive)?; + } + // Run the registry refresher task if is_refreshing_required { let mut interval = tokio::time::interval(Duration::from_secs(registry_refresh_time)); diff --git a/crates/pbs/src/state.rs b/crates/pbs/src/state.rs index dd0e118e..bd683e5f 100644 --- a/crates/pbs/src/state.rs +++ b/crates/pbs/src/state.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use cb_common::{ config::{PbsConfig, PbsModuleConfig}, @@ -19,17 +19,19 @@ pub type PbsStateGuard = Arc>>; pub struct PbsState { /// Config data for the Pbs service pub config: Arc, + /// Path of the config file, for watching changes + pub config_path: Arc, /// Opaque extra data for library use pub data: S, } impl PbsState<()> { - pub fn new(config: PbsModuleConfig) -> Self { - Self { config: Arc::new(config), data: () } + pub fn new(config: PbsModuleConfig, config_path: PathBuf) -> Self { + Self { config: Arc::new(config), config_path: Arc::new(config_path), data: () } } pub fn with_data(self, data: S) -> PbsState { - PbsState { data, config: self.config } + PbsState { data, config: self.config, config_path: self.config_path } } } diff --git a/examples/status_api/src/main.rs b/examples/status_api/src/main.rs index 7ad9b533..3530c800 100644 --- a/examples/status_api/src/main.rs +++ b/examples/status_api/src/main.rs @@ -1,6 +1,9 @@ -use std::sync::{ - Arc, - atomic::{AtomicU64, Ordering}, +use std::{ + path::PathBuf, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, }; use async_trait::async_trait; @@ -70,7 +73,8 @@ impl BuilderApi for MyBuilderApi { let mut data = state.data.clone(); data.inc_amount = extra_config.inc_amount; - Ok(PbsState::new(pbs_config).with_data(data)) + let empty_config_path = PathBuf::new(); + Ok(PbsState::new(pbs_config, empty_config_path).with_data(data)) } fn extra_routes() -> Option>> { @@ -94,7 +98,8 @@ async fn main() -> Result<()> { let _guard = initialize_tracing_log(PBS_MODULE_NAME, LogsSettings::from_env_config()?)?; let custom_state = MyBuilderState::from_config(extra); - let state = PbsState::new(pbs_config).with_data(custom_state); + let empty_config_path = PathBuf::new(); + let state = PbsState::new(pbs_config, empty_config_path).with_data(custom_state); PbsService::register_metric(Box::new(CHECK_RECEIVED_COUNTER.clone())); PbsService::init_metrics(chain)?; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 5e8e1596..270fb9f6 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -16,6 +16,7 @@ reqwest.workspace = true serde_json.workspace = true tempfile.workspace = true tokio.workspace = true +toml.workspace = true tracing.workspace = true tracing-subscriber.workspace = true tracing-test.workspace = true diff --git a/tests/tests/pbs_cfg_file_update.rs b/tests/tests/pbs_cfg_file_update.rs new file mode 100644 index 00000000..0c4e8e47 --- /dev/null +++ b/tests/tests/pbs_cfg_file_update.rs @@ -0,0 +1,158 @@ +use std::{net::Ipv4Addr, sync::Arc, time::Duration}; + +use alloy::primitives::U256; +use cb_common::{ + config::{CommitBoostConfig, LogsSettings, PbsConfig, RelayConfig, StaticPbsConfig}, + pbs::RelayEntry, + signer::random_secret, + types::Chain, +}; +use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; +use cb_tests::{ + mock_relay::{MockRelayState, start_mock_relay_service}, + mock_validator::MockValidator, + utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, +}; +use eyre::Result; +use reqwest::StatusCode; +use tracing::info; +use url::Url; + +/// Updates the config file that was used to load the PBS config, and ensures +/// the filesystem watcher triggers a reload of the configuration. +#[tokio::test] +async fn test_cfg_file_update() -> Result<()> { + // Random keys needed for the relays to start + setup_test_env(); + let signer = random_secret(); + let pubkey = signer.public_key(); + + let chain = Chain::Hoodi; + let pbs_port = 3720; + + // Start relay 1 + let relay1_port = pbs_port + 1; + let relay1 = generate_mock_relay(relay1_port, pubkey.clone())?; + let relay1_state = Arc::new(MockRelayState::new(chain, signer.clone())); + tokio::spawn(start_mock_relay_service(relay1_state.clone(), relay1_port)); + + // Start relay 2 + let relay2_port = relay1_port + 1; + let relay2 = generate_mock_relay(relay2_port, pubkey.clone())?; + let relay2_id = relay2.id.clone().to_string(); + let relay2_state = Arc::new(MockRelayState::new(chain, signer)); + tokio::spawn(start_mock_relay_service(relay2_state.clone(), relay2_port)); + + // Make a config with relay 1 only + let pbs_config = PbsConfig { + // get_pbs_static_config(pbs_port); + host: Ipv4Addr::LOCALHOST, + port: pbs_port, + relay_check: false, + wait_all_registrations: false, + timeout_get_header_ms: 950, + timeout_get_payload_ms: 4000, + timeout_register_validator_ms: 3000, + skip_sigverify: true, + min_bid_wei: U256::ZERO, + late_in_slot_time_ms: u64::MAX / 2, /* serde gets very upset about serializing u64::MAX + * or anything close to it */ + extra_validation_enabled: false, + rpc_url: None, + ssv_api_url: Url::parse("http://example.com").unwrap(), + http_timeout_seconds: 10, + register_validator_retry_limit: 3, + validator_registration_batch_size: None, + mux_registry_refresh_interval_seconds: 384, + }; + let cb_config = CommitBoostConfig { + chain, + pbs: StaticPbsConfig { + docker_image: String::new(), + pbs_config: pbs_config.clone(), + with_signer: false, + }, + muxes: None, + modules: None, + signer: None, + logs: LogsSettings::default(), + metrics: None, + relays: vec![RelayConfig { + id: Some(relay1.id.to_string()), + enable_timing_games: false, + frequency_get_header_ms: None, + get_params: None, + headers: None, + target_first_request_ms: None, + validator_registration_batch_size: None, + entry: RelayEntry { + id: relay1.id.to_string(), + url: Url::parse(&format!("http://localhost:{relay1_port}"))?, + pubkey: pubkey.clone(), + }, + }], + }; + + // Save to a file + let temp_file = tempfile::NamedTempFile::new()?; + let config_path = temp_file.path().to_path_buf(); + let config_toml = toml::to_string_pretty(&cb_config)?; + info!("Writing initial config to {:?}", config_path); + std::fs::write(config_path.clone(), config_toml.as_bytes())?; + + // Run the PBS service + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![relay1.clone()]); + let state = PbsState::new(config, config_path.clone()); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers - extra time for the file watcher + tokio::time::sleep(Duration::from_millis(1000)).await; + + // Send a get header request - should go to relay 1 only + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending get header"); + let res = mock_validator.do_get_header(None).await?; + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(relay1_state.received_get_header(), 1); + assert_eq!(relay2_state.received_get_header(), 0); + + // Update the config to only have relay 2 + let cb_config = CommitBoostConfig { + chain, + pbs: StaticPbsConfig { docker_image: String::new(), pbs_config, with_signer: false }, + muxes: None, + modules: None, + signer: None, + logs: LogsSettings::default(), + metrics: None, + relays: vec![RelayConfig { + id: Some(relay2_id.clone()), + enable_timing_games: false, + frequency_get_header_ms: None, + get_params: None, + headers: None, + target_first_request_ms: None, + validator_registration_batch_size: None, + entry: RelayEntry { + id: relay2_id, + url: Url::parse(&format!("http://{pubkey}@localhost:{relay2_port}"))?, + pubkey, + }, + }], + }; + let config_toml = toml::to_string_pretty(&cb_config)?; + info!("Writing updated config to {:?}", config_path); + std::fs::write(config_path, config_toml.as_bytes())?; + + // leave some time for the watcher to pick up the change and reload + tokio::time::sleep(Duration::from_millis(1000)).await; + + // Send another get header request - should go to relay 2 only + info!("Sending get header after config update"); + let res = mock_validator.do_get_header(None).await?; + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(relay1_state.received_get_header(), 1); // no change + assert_eq!(relay2_state.received_get_header(), 1); // incremented + + Ok(()) +} diff --git a/tests/tests/pbs_get_header.rs b/tests/tests/pbs_get_header.rs index d44d70ce..5ae4b656 100644 --- a/tests/tests/pbs_get_header.rs +++ b/tests/tests/pbs_get_header.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use alloy::primitives::{B256, U256}; use cb_common::{ @@ -37,7 +37,7 @@ async fn test_get_header() -> Result<()> { // Run the PBS service let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers @@ -83,7 +83,7 @@ async fn test_get_header_returns_204_if_relay_down() -> Result<()> { // Run the PBS service let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers @@ -115,7 +115,7 @@ async fn test_get_header_returns_400_if_request_is_invalid() -> Result<()> { // Run the PBS service let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers diff --git a/tests/tests/pbs_get_status.rs b/tests/tests/pbs_get_status.rs index 0ca09bf5..9dc8615f 100644 --- a/tests/tests/pbs_get_status.rs +++ b/tests/tests/pbs_get_status.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use cb_common::{signer::random_secret, types::Chain}; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; @@ -31,7 +31,7 @@ async fn test_get_status() -> Result<()> { tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_1_port)); let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays.clone()); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers @@ -64,7 +64,7 @@ async fn test_get_status_returns_502_if_relay_down() -> Result<()> { // tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays.clone()); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers diff --git a/tests/tests/pbs_mux.rs b/tests/tests/pbs_mux.rs index 3a15b49b..b5fd14dd 100644 --- a/tests/tests/pbs_mux.rs +++ b/tests/tests/pbs_mux.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration}; use cb_common::{ config::{HTTP_TIMEOUT_SECONDS_DEFAULT, MUXER_HTTP_MAX_LENGTH, RuntimeMuxConfig}, @@ -186,7 +186,7 @@ async fn test_mux() -> Result<()> { config.mux_lookup = Some(HashMap::from([(validator_pubkey.clone(), mux)])); // Run PBS service - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers diff --git a/tests/tests/pbs_mux_refresh.rs b/tests/tests/pbs_mux_refresh.rs index da582ec7..e1a5a8fe 100644 --- a/tests/tests/pbs_mux_refresh.rs +++ b/tests/tests/pbs_mux_refresh.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use cb_common::{ config::{MuxConfig, MuxKeysLoader, PbsMuxes}, @@ -98,7 +98,7 @@ async fn test_auto_refresh() -> Result<()> { config.registry_muxes = Some(registry_muxes); // Run PBS service - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); let pbs_server = tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); info!("Started PBS server with pubkey {default_pubkey}"); diff --git a/tests/tests/pbs_post_blinded_blocks.rs b/tests/tests/pbs_post_blinded_blocks.rs index 37e9612c..3a0f619f 100644 --- a/tests/tests/pbs_post_blinded_blocks.rs +++ b/tests/tests/pbs_post_blinded_blocks.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use cb_common::{ pbs::{BuilderApiVersion, GetPayloadInfo, SubmitBlindedBlockResponse}, @@ -52,7 +52,7 @@ async fn test_submit_block_too_large() -> Result<()> { tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers @@ -82,7 +82,7 @@ async fn submit_block_impl(pbs_port: u16, api_version: &BuilderApiVersion) -> Re // Run the PBS service let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers diff --git a/tests/tests/pbs_post_validators.rs b/tests/tests/pbs_post_validators.rs index 35e6c5be..ef2ac40b 100644 --- a/tests/tests/pbs_post_validators.rs +++ b/tests/tests/pbs_post_validators.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use alloy::rpc::types::beacon::relay::ValidatorRegistration; use cb_common::{ @@ -31,7 +31,7 @@ async fn test_register_validators() -> Result<()> { // Run the PBS service let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); // leave some time to start servers @@ -80,7 +80,7 @@ async fn test_register_validators_does_not_retry_on_429() -> Result<()> { // Run the PBS service let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state.clone())); // Leave some time to start servers @@ -135,7 +135,7 @@ async fn test_register_validators_retries_on_500() -> Result<()> { pbs_config.register_validator_retry_limit = 3; let config = to_pbs_config(chain, pbs_config, relays); - let state = PbsState::new(config); + let state = PbsState::new(config, PathBuf::new()); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state.clone())); tokio::time::sleep(Duration::from_millis(100)).await;