diff --git a/Cargo.lock b/Cargo.lock index e246025635f..98fba149364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,6 +1396,7 @@ version = "0.0.0" dependencies = [ "alloy", "mockito 1.6.1", + "papyrus_base_layer", "serde", "thiserror 1.0.69", "tokio", diff --git a/crates/apollo_l1_endpoint_monitor/Cargo.toml b/crates/apollo_l1_endpoint_monitor/Cargo.toml index e5e96e1cb68..62fb631de2b 100644 --- a/crates/apollo_l1_endpoint_monitor/Cargo.toml +++ b/crates/apollo_l1_endpoint_monitor/Cargo.toml @@ -13,7 +13,9 @@ tracing.workspace = true url = { workspace = true, features = ["serde"] } [dev-dependencies] +alloy = { workspace = true, features = ["node-bindings"] } mockito.workspace = true +papyrus_base_layer = { workspace = true, features = ["testing"] } tokio.workspace = true [lints] diff --git a/crates/apollo_l1_endpoint_monitor/tests/happy_flow.rs b/crates/apollo_l1_endpoint_monitor/tests/happy_flow.rs new file mode 100644 index 00000000000..222aa5d70ad --- /dev/null +++ b/crates/apollo_l1_endpoint_monitor/tests/happy_flow.rs @@ -0,0 +1,69 @@ +use apollo_l1_endpoint_monitor::monitor::{ + L1EndpointMonitor, + L1EndpointMonitorConfig, + L1EndpointMonitorError, +}; +use papyrus_base_layer::test_utils::anvil; +use url::Url; + +/// Integration test: two Anvil nodes plus a bogus endpoint to exercise cycling and failure. +#[tokio::test] +async fn end_to_end_cycle_and_recovery() { + // Spin up two ephemeral Anvil nodes. + // IMPORTANT: This is one of the only cases where two anvil nodes are needed simultaneously, + // since we are flow testing two separate L1 nodes. Other tests should never use more than one + // at a time! + let good_node_1 = anvil(None); + let good_url_1 = good_node_1.endpoint_url(); + let good_node_2 = anvil(None); + let good_url_2 = good_node_2.endpoint_url(); + + // Bogus endpoint on port 1 that is likely to be unbound, see the unit tests for more details. + let bad_node_url = Url::parse("http://localhost:1").unwrap(); + + // Initialize monitor starting at the bad index. + let mut monitor = L1EndpointMonitor { + current_l1_endpoint_index: 0, + config: L1EndpointMonitorConfig { + ordered_l1_endpoint_urls: vec![ + bad_node_url.clone(), + good_url_1.clone(), + good_url_2.clone(), + ], + }, + }; + + // 1) First call: skip bad and take the first good one. + let active1 = monitor.get_active_l1_endpoint().await.unwrap(); + assert_eq!(active1, good_url_1); + assert_eq!(monitor.current_l1_endpoint_index, 1); + + // 2) Anvil 1 is going down. + drop(good_node_1); + + // Next call: now the first good node is down, switch to second good node. + let active2 = monitor.get_active_l1_endpoint().await.unwrap(); + assert_eq!(active2, good_url_2); + assert_eq!(monitor.current_l1_endpoint_index, 2); + + // 3) Anvil 2 is now also down! + drop(good_node_2); + + // All endpoints are now down --> error. Do this twice for idempotency. + for _ in 0..2 { + let result = monitor.get_active_l1_endpoint().await; + assert_eq!(result, Err(L1EndpointMonitorError::NoActiveL1Endpoint)); + assert_eq!(monitor.current_l1_endpoint_index, 2); + } + + // ANVIL node 1 has risen! + let good_node_1 = anvil(None); + // Anvil is configured to use an ephemeral port, so this new node will be bound to a fresh port. + // We cannot reuse the previous URL since the old port may no longer be available. + let good_url_1 = good_node_1.endpoint_url(); + monitor.config.ordered_l1_endpoint_urls[1] = good_url_1.clone(); + // Index wraps around from 2 to 0, 0 is still down so 1 is picked, which is operational now. + let active3 = monitor.get_active_l1_endpoint().await.unwrap(); + assert_eq!(active3, good_url_1); + assert_eq!(monitor.current_l1_endpoint_index, 1); +} diff --git a/crates/papyrus_base_layer/src/test_utils.rs b/crates/papyrus_base_layer/src/test_utils.rs index 2d12322f8aa..1a49ec072ab 100644 --- a/crates/papyrus_base_layer/src/test_utils.rs +++ b/crates/papyrus_base_layer/src/test_utils.rs @@ -100,9 +100,9 @@ pub fn get_test_ethereum_node() -> (TestEthereumNodeHandle, EthereumContractAddr // TODO(Arni): Make port non-optional. // Spin up Anvil instance, a local Ethereum node, dies when dropped. -fn anvil(port: Option) -> AnvilInstance { +pub fn anvil(port: Option) -> AnvilInstance { let mut anvil = Anvil::new(); - // If the port is not set explicitly, a random value will be used. + // If the port is not set explicitly, a random ephemeral port is bound and used. if let Some(port) = port { anvil = anvil.port(port); }