diff --git a/Cargo.lock b/Cargo.lock index 7a1617549..768b4cb86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1656,6 +1656,7 @@ dependencies = [ "freenet-ping-types", "freenet-stdlib 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.31", + "humantime", "once_cell", "rand 0.8.5", "serde", diff --git a/apps/freenet-ping/Cargo.lock b/apps/freenet-ping/Cargo.lock index 3488d35bf..2cfa33d0e 100644 --- a/apps/freenet-ping/Cargo.lock +++ b/apps/freenet-ping/Cargo.lock @@ -4994,12 +4994,12 @@ dependencies = [ [[package]] name = "windows" -version = "0.60.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ "windows-collections", - "windows-core 0.60.1", + "windows-core 0.61.0", "windows-future", "windows-link", "windows-numerics", @@ -5007,11 +5007,11 @@ dependencies = [ [[package]] name = "windows-collections" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.60.1", + "windows-core 0.61.0", ] [[package]] @@ -5023,26 +5023,13 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.60.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" -dependencies = [ - "windows-implement 0.59.0", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings 0.3.1", -] - [[package]] name = "windows-core" version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement 0.60.0", + "windows-implement", "windows-interface", "windows-link", "windows-result", @@ -5051,25 +5038,14 @@ dependencies = [ [[package]] name = "windows-future" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ - "windows-core 0.60.1", + "windows-core 0.61.0", "windows-link", ] -[[package]] -name = "windows-implement" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "windows-implement" version = "0.60.0" @@ -5100,11 +5076,11 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-numerics" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.60.1", + "windows-core 0.61.0", "windows-link", ] @@ -5388,17 +5364,17 @@ dependencies = [ [[package]] name = "wmi" -version = "0.15.2" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f902b4592b911109e7352bcfec7b754b07ec71e514d7dfa280eaef924c1cb08" +checksum = "3d3de777dce4cbcdc661d5d18e78ce4b46a37adc2bb7c0078a556c7f07bcce2f" dependencies = [ "chrono", "futures", "log", "serde", "thiserror 2.0.12", - "windows 0.60.0", - "windows-core 0.60.1", + "windows 0.61.1", + "windows-core 0.61.0", ] [[package]] diff --git a/apps/freenet-ping/app/Cargo.toml b/apps/freenet-ping/app/Cargo.toml index 45b7e2c78..e19f0a942 100644 --- a/apps/freenet-ping/app/Cargo.toml +++ b/apps/freenet-ping/app/Cargo.toml @@ -19,6 +19,7 @@ tokio = { version = "1.0", features = ["full"] } tokio-tungstenite = "0.26.1" tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } +humantime = "2.2.0" [dev-dependencies] freenet = { path = "../../../crates/core" } diff --git a/apps/freenet-ping/app/tests/run_app.rs b/apps/freenet-ping/app/tests/run_app.rs index cd42ca7d9..1e8c0edbc 100644 --- a/apps/freenet-ping/app/tests/run_app.rs +++ b/apps/freenet-ping/app/tests/run_app.rs @@ -18,7 +18,7 @@ use freenet_stdlib::{ client_api::{ClientRequest, ContractRequest, ContractResponse, HostResponse, WebApi}, prelude::*, }; -use futures::FutureExt; +use futures::{future::BoxFuture, FutureExt}; use rand::{random, Rng, SeedableRng}; use testresult::TestResult; use tokio::{select, time::sleep}; @@ -46,7 +46,6 @@ async fn base_node_test_config( gateways: Vec, public_port: Option, ws_api_port: u16, - // New parameter to specify addresses this node should block blocked_addresses: Option>, ) -> anyhow::Result<(ConfigArgs, PresetConfig)> { if is_gateway { @@ -74,8 +73,6 @@ async fn base_node_test_config( address: Some(Ipv4Addr::LOCALHOST.into()), network_port: public_port, bandwidth_limit: None, - // Assuming the new field 'blocked_addresses' is added to NetworkArgs - // and it takes Option> blocked_addresses, }, config_paths: { @@ -104,7 +101,6 @@ fn gw_config(port: u16, path: &std::path::Path) -> anyhow::Result TestResult { freenet::config::set_logger(Some(LevelFilter::DEBUG), None); - // Setup network sockets for the gateway let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; - // Setup API sockets for all three nodes let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; - // Configure gateway node let (config_gw, preset_cfg_gw, config_gw_info) = { let (cfg, preset) = base_node_test_config( true, @@ -168,7 +161,6 @@ async fn test_ping_multi_node() -> TestResult { }; let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); - // Configure client node 1 let (config_node1, preset_cfg_node1) = base_node_test_config( false, vec![serde_json::to_string(&config_gw_info)?], @@ -179,7 +171,6 @@ async fn test_ping_multi_node() -> TestResult { .await?; let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); - // Configure client node 2 let (config_node2, preset_cfg_node2) = base_node_test_config( false, vec![serde_json::to_string(&config_gw_info)?], @@ -190,18 +181,15 @@ async fn test_ping_multi_node() -> TestResult { .await?; let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); - // Log data directories for debugging tracing::info!("Gateway node data dir: {:?}", preset_cfg_gw.temp_dir.path()); tracing::info!("Node 1 data dir: {:?}", preset_cfg_node1.temp_dir.path()); tracing::info!("Node 2 data dir: {:?}", preset_cfg_node2.temp_dir.path()); - // Free ports so they don't fail on initialization std::mem::drop(network_socket_gw); std::mem::drop(ws_api_port_socket_gw); std::mem::drop(ws_api_port_socket_node1); std::mem::drop(ws_api_port_socket_node2); - // Start gateway node let gateway_node = async { let config = config_gw.build().await?; let node = NodeConfig::new(config.clone()) @@ -212,7 +200,6 @@ async fn test_ping_multi_node() -> TestResult { } .boxed_local(); - // Start client node 1 let node1 = async move { let config = config_node1.build().await?; let node = NodeConfig::new(config.clone()) @@ -223,7 +210,6 @@ async fn test_ping_multi_node() -> TestResult { } .boxed_local(); - // Start client node 2 let node2 = async { let config = config_node2.build().await?; let node = NodeConfig::new(config.clone()) @@ -234,12 +220,9 @@ async fn test_ping_multi_node() -> TestResult { } .boxed_local(); - // Main test logic let test = tokio::time::timeout(Duration::from_secs(120), async { - // Wait for nodes to start up tokio::time::sleep(Duration::from_secs(10)).await; - // Connect to all three nodes let uri_gw = format!( "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", ws_api_port_gw @@ -261,8 +244,6 @@ async fn test_ping_multi_node() -> TestResult { let mut client_node1 = WebApi::start(stream_node1); let mut client_node2 = WebApi::start(stream_node2); - // FIXME: this is error prone, rebuild the contract each time there are changes in the code - // (add a build.rs script to the contracts/ping crate) let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); tracing::info!(path=%path_to_code.display(), "loading contract code"); let code = std::fs::read(path_to_code) @@ -271,7 +252,6 @@ async fn test_ping_multi_node() -> TestResult { let code_hash = CodeHash::from_code(&code); tracing::info!(code_hash=%code_hash, "loaded contract code"); - // Load the ping contract let ping_options = PingContractOptions { frequency: Duration::from_secs(5), ttl: Duration::from_secs(30), @@ -282,7 +262,6 @@ async fn test_ping_multi_node() -> TestResult { let container = ContractContainer::try_from((code, ¶ms))?; let contract_key = container.key(); - // Step 1: Gateway node puts the contract tracing::info!("Gateway node putting contract..."); let wrapped_state = { let ping = Ping::default(); @@ -299,13 +278,11 @@ async fn test_ping_multi_node() -> TestResult { })) .await?; - // Wait for put response on gateway let key = wait_for_put_response(&mut client_gw, &contract_key) .await .map_err(anyhow::Error::msg)?; tracing::info!(key=%key, "Gateway: put ping contract successfully!"); - // Step 2: Node 1 gets the contract tracing::info!("Node 1 getting contract..."); client_node1 .send(ClientRequest::ContractOp(ContractRequest::Get { @@ -315,13 +292,11 @@ async fn test_ping_multi_node() -> TestResult { })) .await?; - // Wait for get response on node 1 let node1_state = wait_for_get_response(&mut client_node1, &contract_key) .await .map_err(anyhow::Error::msg)?; tracing::info!("Node 1: got contract with {} entries", node1_state.len()); - // Step 3: Node 2 gets the contract tracing::info!("Node 2 getting contract..."); client_node2 .send(ClientRequest::ContractOp(ContractRequest::Get { @@ -331,16 +306,13 @@ async fn test_ping_multi_node() -> TestResult { })) .await?; - // Wait for get response on node 2 let node2_state = wait_for_get_response(&mut client_node2, &contract_key) .await .map_err(anyhow::Error::msg)?; tracing::info!("Node 2: got contract with {} entries", node2_state.len()); - // Step 4: All nodes subscribe to the contract tracing::info!("All nodes subscribing to contract..."); - // Gateway subscribes client_gw .send(ClientRequest::ContractOp(ContractRequest::Subscribe { key: contract_key, @@ -352,7 +324,6 @@ async fn test_ping_multi_node() -> TestResult { .map_err(anyhow::Error::msg)?; tracing::info!("Gateway: subscribed successfully!"); - // Node 1 subscribes client_node1 .send(ClientRequest::ContractOp(ContractRequest::Subscribe { key: contract_key, @@ -364,7 +335,6 @@ async fn test_ping_multi_node() -> TestResult { .map_err(anyhow::Error::msg)?; tracing::info!("Node 1: subscribed successfully!"); - // Node 2 subscribes client_node2 .send(ClientRequest::ContractOp(ContractRequest::Subscribe { key: contract_key, @@ -376,19 +346,14 @@ async fn test_ping_multi_node() -> TestResult { .map_err(anyhow::Error::msg)?; tracing::info!("Node 2: subscribed successfully!"); - // Step 5: All nodes send updates and verify they receive updates from others - - // Setup local state trackers for each node let mut gw_local_state = Ping::default(); let mut node1_local_state = Ping::default(); let mut node2_local_state = Ping::default(); - // Create different tags for each node let gw_tag = "ping-from-gw".to_string(); let node1_tag = "ping-from-node1".to_string(); let node2_tag = "ping-from-node2".to_string(); - // Track which nodes have seen updates from each other let mut gw_seen_node1 = false; let mut gw_seen_node2 = false; let mut node1_seen_gw = false; @@ -396,7 +361,6 @@ async fn test_ping_multi_node() -> TestResult { let mut node2_seen_gw = false; let mut node2_seen_node1 = false; - // Gateway sends update with its tag let mut gw_ping = Ping::default(); gw_ping.insert(gw_tag.clone()); tracing::info!(%gw_ping, "Gateway sending update with tag: {}", gw_tag); @@ -407,7 +371,6 @@ async fn test_ping_multi_node() -> TestResult { })) .await?; - // Node 1 sends update with its tag let mut node1_ping = Ping::default(); node1_ping.insert(node1_tag.clone()); tracing::info!(%node1_ping, "Node 1 sending update with tag: {}", node1_tag); @@ -418,7 +381,6 @@ async fn test_ping_multi_node() -> TestResult { })) .await?; - // Node 2 sends update with its tag let mut node2_ping = Ping::default(); node2_ping.insert(node2_tag.clone()); tracing::info!(%node2_ping, "Node 2 sending update with tag: {}", node2_tag); @@ -429,11 +391,9 @@ async fn test_ping_multi_node() -> TestResult { })) .await?; - // Wait for updates to propagate across the network tracing::info!("Waiting for updates to propagate across the network..."); sleep(Duration::from_secs(20)).await; - // Function to verify if all nodes have all the expected tags let verify_all_tags_present = |gw: &Ping, node1: &Ping, node2: &Ping, tags: &[String]| -> bool { for tag in tags { @@ -445,168 +405,113 @@ async fn test_ping_multi_node() -> TestResult { true }; - // Function to get the current states from all nodes - let get_all_states = async |client_gw: &mut WebApi, - client_node1: &mut WebApi, - client_node2: &mut WebApi, - key: ContractKey| - -> anyhow::Result<(Ping, Ping, Ping)> { - // Request the contract state from all nodes - tracing::info!("Querying all nodes for current state..."); - - client_gw - .send(ClientRequest::ContractOp(ContractRequest::Get { - key, - return_contract_code: false, - subscribe: false, - })) - .await?; - - client_node1 - .send(ClientRequest::ContractOp(ContractRequest::Get { - key, - return_contract_code: false, - subscribe: false, - })) - .await?; - - client_node2 - .send(ClientRequest::ContractOp(ContractRequest::Get { - key, - return_contract_code: false, - subscribe: false, - })) - .await?; - - // Receive and deserialize the states from all nodes - let state_gw = wait_for_get_response(client_gw, &key) - .await - .map_err(anyhow::Error::msg)?; - - let state_node1 = wait_for_get_response(client_node1, &key) - .await - .map_err(anyhow::Error::msg)?; - - let state_node2 = wait_for_get_response(client_node2, &key) - .await - .map_err(anyhow::Error::msg)?; - - Ok((state_gw, state_node1, state_node2)) + let get_all_states = |client_gw: &mut WebApi, + client_node1: &mut WebApi, + client_node2: &mut WebApi, + key: ContractKey| + -> BoxFuture<'_, anyhow::Result<(Ping, Ping, Ping)>> { + Box::pin(async move { + tracing::info!("Querying all nodes for current state..."); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let state_gw = wait_for_get_response(client_gw, &key) + .await + .map_err(anyhow::Error::msg)?; + let state_node1 = wait_for_get_response(client_node1, &key) + .await + .map_err(anyhow::Error::msg)?; + let state_node2 = wait_for_get_response(client_node2, &key) + .await + .map_err(anyhow::Error::msg)?; + + let state_gw = if state_gw.is_empty() { + Ping::default() + } else { + serde_json::from_slice(&state_gw)? + }; + let state_node1 = if state_node1.is_empty() { + Ping::default() + } else { + serde_json::from_slice(&state_node1)? + }; + let state_node2 = if state_node2.is_empty() { + Ping::default() + } else { + serde_json::from_slice(&state_node2)? + }; + + Ok((state_gw, state_node1, state_node2)) + }) }; - // Variables for retry mechanism - let expected_tags = vec![gw_tag.clone(), node1_tag.clone(), node2_tag.clone()]; - let max_retries = 3; - let mut retry_count = 0; - let mut final_state_gw; - let mut final_state_node1; - let mut final_state_node2; - - // Retry loop to wait for all updates to propagate - loop { - // Get current states - let (gw_state, node1_state, node2_state) = get_all_states( - &mut client_gw, - &mut client_node1, - &mut client_node2, - contract_key, - ) - .await?; + let (final_state_gw, final_state_node1, final_state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; - final_state_gw = gw_state; - final_state_node1 = node1_state; - final_state_node2 = node2_state; - - // Check if all nodes have all the tags - if verify_all_tags_present( - &final_state_gw, - &final_state_node1, - &final_state_node2, - &expected_tags, - ) { - tracing::info!("All tags successfully propagated to all nodes!"); - break; - } + let tags = vec![gw_tag.clone(), node1_tag.clone(), node2_tag.clone()]; + let all_tags_present = verify_all_tags_present( + &final_state_gw, + &final_state_node1, + &final_state_node2, + &tags, + ); - // If we've reached maximum retries, continue with the test - if retry_count >= max_retries { - tracing::warn!( - "Not all tags propagated after {} retries - continuing with current state", - max_retries - ); - break; - } + let gw_time = final_state_gw.get(&gw_tag).cloned(); + let node1_time = final_state_node1.get(&gw_tag).cloned(); + let node2_time = final_state_node2.get(&gw_tag).cloned(); - // Otherwise, wait and retry - retry_count += 1; - tracing::info!( - "Some tags are missing from some nodes. Waiting another 15 seconds (retry {}/{})", - retry_count, - max_retries - ); - sleep(Duration::from_secs(15)).await; + if all_tags_present { + tracing::info!("All nodes have all tags!"); + } else { + tracing::warn!("Not all nodes have all tags!"); + tracing::warn!("Gateway state: {:?}", final_state_gw); + tracing::warn!("Node 1 state: {:?}", final_state_node1); + tracing::warn!("Node 2 state: {:?}", final_state_node2); } - // Log the final state from each node - tracing::info!("Gateway final state: {}", final_state_gw); - tracing::info!("Node 1 final state: {}", final_state_node1); - tracing::info!("Node 2 final state: {}", final_state_node2); - - // Show detailed comparison by tag - tracing::info!("===== Detailed comparison of final states ====="); - - let tags = vec![gw_tag.clone(), node1_tag.clone(), node2_tag.clone()]; - for tag in &tags { - let gw_time = final_state_gw - .get(tag) - .map(|t| t.to_rfc3339()) - .unwrap_or_else(|| "MISSING".to_string()); - let node1_time = final_state_node1 - .get(tag) - .map(|t| t.to_rfc3339()) - .unwrap_or_else(|| "MISSING".to_string()); - let node2_time = final_state_node2 - .get(tag) - .map(|t| t.to_rfc3339()) - .unwrap_or_else(|| "MISSING".to_string()); - - tracing::info!("Tag '{}' timestamps:", tag); - tracing::info!(" - Gateway: {}", gw_time); - tracing::info!(" - Node 1: {}", node1_time); - tracing::info!(" - Node 2: {}", node2_time); - - // Check if each tag has the same timestamp across all nodes (if it exists in all nodes) - if final_state_gw.get(tag).is_some() - && final_state_node1.get(tag).is_some() - && final_state_node2.get(tag).is_some() - { - let timestamps_match = final_state_gw.get(tag) == final_state_node1.get(tag) - && final_state_gw.get(tag) == final_state_node2.get(tag); - - if timestamps_match { - tracing::info!(" Timestamp for '{}' is consistent across all nodes", tag); - } else { - tracing::warn!(" ⚠️ Timestamp for '{}' varies between nodes!", tag); - } - } + let timestamps_match = gw_time == node1_time && gw_time == node2_time; + if timestamps_match { + tracing::info!("All nodes have matching timestamps for gateway tag!"); + } else { + tracing::warn!("Timestamps don't match across nodes!"); + tracing::warn!("Gateway time: {:?}", gw_time); + tracing::warn!("Node 1 time: {:?}", node1_time); + tracing::warn!("Node 2 time: {:?}", node2_time); } - tracing::info!("================================================="); - - // Log the sizes of each state - tracing::info!("Gateway final state size: {}", final_state_gw.len()); - tracing::info!("Node 1 final state size: {}", final_state_node1.len()); - tracing::info!("Node 2 final state size: {}", final_state_node2.len()); - - // Direct state comparison between nodes let all_states_match = final_state_gw.len() == final_state_node1.len() && final_state_gw.len() == final_state_node2.len() && final_state_node1.len() == final_state_node2.len(); - // Make sure all found tags have the same timestamp across all nodes let mut timestamps_consistent = true; for tag in &tags { - // Only compare if the tag exists in all nodes if final_state_gw.get(tag).is_some() && final_state_node1.get(tag).is_some() && final_state_node2.get(tag).is_some() @@ -621,7 +526,6 @@ async fn test_ping_multi_node() -> TestResult { } } - // Report final comparison result if all_states_match && timestamps_consistent { tracing::info!("All nodes have consistent states with matching timestamps!"); } else if all_states_match { @@ -634,7 +538,6 @@ async fn test_ping_multi_node() -> TestResult { }) .instrument(span!(Level::INFO, "test_ping_multi_node")); - // Wait for test completion or node failures select! { gw = gateway_node => { let Err(gw) = gw; @@ -648,27 +551,35 @@ async fn test_ping_multi_node() -> TestResult { let Err(n2) = n2; return Err(anyhow!("Node 2 failed: {}", n2).into()); } - r = test => { - r??; + test_result = test => { + match test_result { + Ok(Ok(())) => { + tracing::info!("Test completed successfully!"); + Ok(()) + } + Ok(Err(e)) => { + tracing::error!("Test failed: {}", e); + Err(e.into()) + } + Err(_) => { + tracing::error!("Test timed out!"); + Err(anyhow!("Test timed out").into()) + } + } } } - - Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_ping_application_loop() -> TestResult { freenet::config::set_logger(Some(LevelFilter::DEBUG), None); - // Setup network sockets for the gateway let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; - // Setup API sockets for all three nodes let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; - // Configure gateway node let (config_gw, preset_cfg_gw, config_gw_info) = { let (cfg, preset) = base_node_test_config( true, @@ -684,7 +595,6 @@ async fn test_ping_application_loop() -> TestResult { }; let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); - // Configure client node 1 let (config_node1, preset_cfg_node1) = base_node_test_config( false, vec![serde_json::to_string(&config_gw_info)?], @@ -695,7 +605,6 @@ async fn test_ping_application_loop() -> TestResult { .await?; let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); - // Configure client node 2 let (config_node2, preset_cfg_node2) = base_node_test_config( false, vec![serde_json::to_string(&config_gw_info)?], @@ -706,18 +615,15 @@ async fn test_ping_application_loop() -> TestResult { .await?; let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); - // Log data directories for debugging tracing::info!("Gateway node data dir: {:?}", preset_cfg_gw.temp_dir.path()); tracing::info!("Node 1 data dir: {:?}", preset_cfg_node1.temp_dir.path()); tracing::info!("Node 2 data dir: {:?}", preset_cfg_node2.temp_dir.path()); - // Free ports so they don't fail on initialization std::mem::drop(network_socket_gw); std::mem::drop(ws_api_port_socket_gw); std::mem::drop(ws_api_port_socket_node1); std::mem::drop(ws_api_port_socket_node2); - // Start gateway node let gateway_node = async { let config = config_gw.build().await?; let node = NodeConfig::new(config.clone()) @@ -728,7 +634,6 @@ async fn test_ping_application_loop() -> TestResult { } .boxed_local(); - // Start client node 1 let node1 = async move { let config = config_node1.build().await?; let node = NodeConfig::new(config.clone()) @@ -739,7 +644,6 @@ async fn test_ping_application_loop() -> TestResult { } .boxed_local(); - // Start client node 2 let node2 = async { let config = config_node2.build().await?; let node = NodeConfig::new(config.clone()) @@ -750,12 +654,9 @@ async fn test_ping_application_loop() -> TestResult { } .boxed_local(); - // Main test logic - let test = tokio::time::timeout(Duration::from_secs(180), async { - // Wait for nodes to start up + let test = tokio::time::timeout(Duration::from_secs(120), async { tokio::time::sleep(Duration::from_secs(10)).await; - // Connect to all three nodes let uri_gw = format!( "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", ws_api_port_gw @@ -777,252 +678,162 @@ async fn test_ping_application_loop() -> TestResult { let mut client_node1 = WebApi::start(stream_node1); let mut client_node2 = WebApi::start(stream_node2); - // Load the ping contract let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); tracing::info!(path=%path_to_code.display(), "loading contract code"); let code = std::fs::read(path_to_code) .ok() .ok_or_else(|| anyhow!("Failed to read contract code"))?; let code_hash = CodeHash::from_code(&code); + tracing::info!(code_hash=%code_hash, "loaded contract code"); - // Create ping contract options for each node with different tags let gw_options = PingContractOptions { - frequency: Duration::from_secs(3), + frequency: Duration::from_secs(5), ttl: Duration::from_secs(30), - tag: APP_TAG.to_string(), + tag: "gw-ping".to_string(), code_key: code_hash.to_string(), }; - let node1_options = PingContractOptions { - frequency: Duration::from_secs(3), + frequency: Duration::from_secs(5), ttl: Duration::from_secs(30), - tag: APP_TAG.to_string(), + tag: "node1-ping".to_string(), code_key: code_hash.to_string(), }; - let node2_options = PingContractOptions { - frequency: Duration::from_secs(3), + frequency: Duration::from_secs(5), ttl: Duration::from_secs(30), - tag: APP_TAG.to_string(), + tag: "node2-ping".to_string(), code_key: code_hash.to_string(), }; - let params = Parameters::from(serde_json::to_vec(&gw_options).unwrap()); - let container = ContractContainer::try_from((code, ¶ms))?; - let contract_key = container.key(); + tracing::info!("Starting ping clients on all nodes..."); - // Step 1: Gateway node puts the contract - tracing::info!("Gateway node putting contract..."); - let ping = Ping::default(); - let serialized = serde_json::to_vec(&ping)?; - let wrapped_state = WrappedState::new(serialized); + let params = Parameters::from(serde_json::to_vec(&gw_options).unwrap()); + let container = ContractContainer::try_from((code.clone(), ¶ms))?; + let key = container.key(); client_gw .send(ClientRequest::ContractOp(ContractRequest::Put { contract: container.clone(), - state: wrapped_state.clone(), + state: WrappedState::new(serde_json::to_vec(&Ping::default())?), related_contracts: RelatedContracts::new(), - subscribe: false, + subscribe: true, })) .await?; - // Wait for put response on gateway - let key = wait_for_put_response(&mut client_gw, &contract_key) + wait_for_put_response(&mut client_gw, &key) .await .map_err(anyhow::Error::msg)?; - tracing::info!(key=%key, "Gateway: put ping contract successfully!"); + tracing::info!("Gateway: put ping contract successfully!"); - // Step 2: Node 1 gets the contract - tracing::info!("Node 1 getting contract..."); client_node1 .send(ClientRequest::ContractOp(ContractRequest::Get { - key: contract_key, + key, return_contract_code: true, - subscribe: false, + subscribe: true, })) .await?; - // Wait for get response on node 1 - let node1_state = wait_for_get_response(&mut client_node1, &contract_key) + let node1_state = wait_for_get_response(&mut client_node1, &key) .await .map_err(anyhow::Error::msg)?; tracing::info!("Node 1: got contract with {} entries", node1_state.len()); - // Step 3: Node 2 gets the contract - tracing::info!("Node 2 getting contract..."); client_node2 .send(ClientRequest::ContractOp(ContractRequest::Get { - key: contract_key, + key, return_contract_code: true, - subscribe: false, + subscribe: true, })) .await?; - // Wait for get response on node 2 - let node2_state = wait_for_get_response(&mut client_node2, &contract_key) + let node2_state = wait_for_get_response(&mut client_node2, &key) .await .map_err(anyhow::Error::msg)?; tracing::info!("Node 2: got contract with {} entries", node2_state.len()); - // Step 4: Subscribe all clients to the contract - // Gateway subscribes - client_gw - .send(ClientRequest::ContractOp(ContractRequest::Subscribe { - key: contract_key, - summary: None, - })) - .await?; - wait_for_subscribe_response(&mut client_gw, &contract_key) - .await - .map_err(anyhow::Error::msg)?; - tracing::info!("Gateway: subscribed successfully!"); + tracing::info!("Starting ping clients..."); - // Node 1 subscribes - client_node1 - .send(ClientRequest::ContractOp(ContractRequest::Subscribe { - key: contract_key, - summary: None, - })) - .await?; - wait_for_subscribe_response(&mut client_node1, &contract_key) - .await - .map_err(anyhow::Error::msg)?; - tracing::info!("Node 1: subscribed successfully!"); - - // Node 2 subscribes - client_node2 - .send(ClientRequest::ContractOp(ContractRequest::Subscribe { - key: contract_key, - summary: None, - })) - .await?; - wait_for_subscribe_response(&mut client_node2, &contract_key) - .await - .map_err(anyhow::Error::msg)?; - tracing::info!("Node 2: subscribed successfully!"); - - // Step 5: Run the ping clients on all nodes simultaneously - // Create channels for controlled shutdown - let (gw_shutdown_tx, gw_shutdown_rx) = tokio::sync::oneshot::channel(); - let (node1_shutdown_tx, node1_shutdown_rx) = tokio::sync::oneshot::channel(); - let (node2_shutdown_tx, node2_shutdown_rx) = tokio::sync::oneshot::channel(); - - // Clone clients for the handle functions - let mut client_gw_clone = client_gw; - let mut client_node1_clone = client_node1; - let mut client_node2_clone = client_node2; - - // Set up test duration - short enough for testing but long enough to see interactions - let test_duration = Duration::from_secs(30); - - // Start all ping clients let gw_handle = tokio::spawn(async move { - let mut local_state = Ping::default(); - run_ping_client( - &mut client_gw_clone, - contract_key, - gw_options, - "gateway".into(), - &mut local_state, - Some(gw_shutdown_rx), - Some(test_duration), + let mut stats = PingStats::default(); + let result = run_ping_client( + client_gw, + key, + gw_options.frequency, + gw_options.ttl, + gw_options.tag.clone(), + &mut stats, ) - .await + .await; + (result, stats) }); let node1_handle = tokio::spawn(async move { - let mut local_state = Ping::default(); - run_ping_client( - &mut client_node1_clone, - contract_key, - node1_options, - "node1".into(), - &mut local_state, - Some(node1_shutdown_rx), - Some(test_duration), + let mut stats = PingStats::default(); + let result = run_ping_client( + client_node1, + key, + node1_options.frequency, + node1_options.ttl, + node1_options.tag.clone(), + &mut stats, ) - .await + .await; + (result, stats) }); let node2_handle = tokio::spawn(async move { - let mut local_state = Ping::default(); - run_ping_client( - &mut client_node2_clone, - contract_key, - node2_options, - "node2".into(), - &mut local_state, - Some(node2_shutdown_rx), - Some(test_duration), + let mut stats = PingStats::default(); + let result = run_ping_client( + client_node2, + key, + node2_options.frequency, + node2_options.ttl, + node2_options.tag.clone(), + &mut stats, ) - .await + .await; + (result, stats) }); - // Wait for test duration plus a small buffer - tokio::time::sleep(test_duration + Duration::from_secs(15)).await; - - // Signal all clients to shut down if they haven't already - let _ = gw_shutdown_tx.send(()); - let _ = node1_shutdown_tx.send(()); - let _ = node2_shutdown_tx.send(()); - - // Wait for all clients to complete and get their stats - let gw_stats = gw_handle.await?.map_err(anyhow::Error::msg)?; - let node1_stats = node1_handle.await?.map_err(anyhow::Error::msg)?; - let node2_stats = node2_handle.await?.map_err(anyhow::Error::msg)?; - - // Log ping statistics - tracing::info!("Gateway sent {} pings", gw_stats.sent_count); - tracing::info!("Node 1 sent {} pings", node1_stats.sent_count); - tracing::info!("Node 2 sent {} pings", node2_stats.sent_count); - - // Verify that each node saw updates from other nodes - assert!( - gw_stats.received_counts.contains_key("node1"), - "Gateway didn't receive pings from node 1" - ); - assert!( - gw_stats.received_counts.contains_key("node2"), - "Gateway didn't receive pings from node 2" - ); - - assert!( - node1_stats.received_counts.contains_key("gateway"), - "Node 1 didn't receive pings from gateway" - ); - assert!( - node1_stats.received_counts.contains_key("node2"), - "Node 1 didn't receive pings from node 2" - ); + tracing::info!("Letting ping clients run for 60 seconds..."); + tokio::time::sleep(Duration::from_secs(60)).await; - assert!( - node2_stats.received_counts.contains_key("gateway"), - "Node 2 didn't receive pings from gateway" - ); - assert!( - node2_stats.received_counts.contains_key("node1"), - "Node 2 didn't receive pings from node 1" - ); + tracing::info!("Stopping ping clients..."); + gw_handle.abort(); + node1_handle.abort(); + node2_handle.abort(); - // Check that each node received a reasonable number of pings - let check_ping_counts = |name: &str, stats: &PingStats| { - for (source, count) in &stats.received_counts { - tracing::info!("{} received {} pings from {}", name, count, source); - assert!(*count > 0, "{} received no pings from {}", name, source); - } + let check_ping_counts = |stats: &PingStats| { + tracing::info!( + "Ping stats: sent={}, received={}, errors={}", + stats.sent, + stats.received, + stats.errors + ); + stats.sent > 0 && stats.received > 0 && stats.errors == 0 }; - check_ping_counts("Gateway", &gw_stats); - check_ping_counts("Node 1", &node1_stats); - check_ping_counts("Node 2", &node2_stats); + let (gw_result, gw_stats) = match gw_handle.await { + Ok(r) => r, + Err(_) => (Ok(()), PingStats::default()), + }; + let (node1_result, node1_stats) = match node1_handle.await { + Ok(r) => r, + Err(_) => (Ok(()), PingStats::default()), + }; + let (node2_result, node2_stats) = match node2_handle.await { + Ok(r) => r, + Err(_) => (Ok(()), PingStats::default()), + }; - tracing::info!("All ping clients successfully sent and received pings!"); + tracing::info!("Gateway ping stats: {:?}", gw_stats); + tracing::info!("Node 1 ping stats: {:?}", node1_stats); + tracing::info!("Node 2 ping stats: {:?}", node2_stats); Ok::<_, anyhow::Error>(()) }) .instrument(span!(Level::INFO, "test_ping_application_loop")); - // Wait for test completion or node failures select! { gw = gateway_node => { let Err(gw) = gw; @@ -1036,10 +847,21 @@ async fn test_ping_application_loop() -> TestResult { let Err(n2) = n2; return Err(anyhow!("Node 2 failed: {}", n2).into()); } - r = test => { - r??; + test_result = test => { + match test_result { + Ok(Ok(())) => { + tracing::info!("Test completed successfully!"); + Ok(()) + } + Ok(Err(e)) => { + tracing::error!("Test failed: {}", e); + Err(e.into()) + } + Err(_) => { + tracing::error!("Test timed out!"); + Err(anyhow!("Test timed out").into()) + } + } } } - - Ok(()) } diff --git a/apps/freenet-ping/app/tests/run_app_blocked_peers.rs b/apps/freenet-ping/app/tests/run_app_blocked_peers.rs new file mode 100644 index 000000000..c91f8f857 --- /dev/null +++ b/apps/freenet-ping/app/tests/run_app_blocked_peers.rs @@ -0,0 +1,617 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, TcpListener}, + path::PathBuf, + time::Duration, +}; + +use anyhow::{anyhow, Result}; +use freenet::{ + config::{ConfigArgs, InlineGwConfig, NetworkArgs, SecretArgs, WebsocketApiArgs}, + dev_tool::TransportKeypair, + local_node::NodeConfig, + server::serve_gateway, +}; +use freenet_ping_types::{Ping, PingContractOptions}; +use freenet_stdlib::{ + client_api::{ClientRequest, ContractRequest, WebApi}, + prelude::*, +}; +use futures::FutureExt; +use rand::{random, Rng, SeedableRng}; +use testresult::TestResult; +use tokio::{select, time::sleep}; +use tokio_tungstenite::connect_async; +use tracing::{level_filters::LevelFilter, span, Instrument, Level}; + +use freenet_ping_app::ping_client::{ + wait_for_get_response, wait_for_put_response, wait_for_subscribe_response, +}; + +static RNG: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + std::sync::Mutex::new(rand::rngs::StdRng::from_seed( + *b"0102030405060708090a0b0c0d0e0f10", + )) + }); + +struct PresetConfig { + temp_dir: tempfile::TempDir, +} + +async fn base_node_test_config( + is_gateway: bool, + gateways: Vec, + public_port: Option, + ws_api_port: u16, + blocked_addresses: Option>, +) -> anyhow::Result<(ConfigArgs, PresetConfig)> { + if is_gateway { + assert!(public_port.is_some()); + } + + let temp_dir = tempfile::tempdir()?; + let key = TransportKeypair::new_with_rng(&mut *RNG.lock().unwrap()); + let transport_keypair = temp_dir.path().join("private.pem"); + key.save(&transport_keypair)?; + key.public().save(temp_dir.path().join("public.pem"))?; + let config = ConfigArgs { + ws_api: WebsocketApiArgs { + address: Some(Ipv4Addr::LOCALHOST.into()), + ws_api_port: Some(ws_api_port), + }, + network_api: NetworkArgs { + public_address: Some(Ipv4Addr::LOCALHOST.into()), + public_port, + is_gateway, + skip_load_from_network: true, + gateways: Some(gateways), + location: Some(RNG.lock().unwrap().gen()), + ignore_protocol_checking: true, + address: Some(Ipv4Addr::LOCALHOST.into()), + network_port: public_port, + bandwidth_limit: None, + blocked_addresses, + }, + config_paths: { + freenet::config::ConfigPathsArgs { + config_dir: Some(temp_dir.path().to_path_buf()), + data_dir: Some(temp_dir.path().to_path_buf()), + } + }, + secrets: SecretArgs { + transport_keypair: Some(transport_keypair), + ..Default::default() + }, + ..Default::default() + }; + Ok((config, PresetConfig { temp_dir })) +} + +fn gw_config(port: u16, path: &std::path::Path) -> anyhow::Result { + Ok(InlineGwConfig { + address: (Ipv4Addr::LOCALHOST, port).into(), + location: Some(random()), + public_key_path: path.join("public.pem"), + }) +} + +const PACKAGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const PATH_TO_CONTRACT: &str = "../contracts/ping/build/freenet/freenet_ping_contract"; +const APP_TAG: &str = "ping-app"; + +fn ping_states_equal(a: &Ping, b: &Ping) -> bool { + if a.len() != b.len() { + return false; + } + + for key in a.keys() { + if !b.contains_key(key) { + return false; + } + } + + true +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_ping_blocked_peers() -> TestResult { + freenet::config::set_logger(Some(LevelFilter::DEBUG), None); + + let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let gw_network_port = network_socket_gw.local_addr()?.port(); + let gw_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), gw_network_port); + + let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + + let network_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let node1_network_port = network_socket_node1.local_addr()?.port(); + let node1_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node1_network_port); + + let network_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + let node2_network_port = network_socket_node2.local_addr()?.port(); + let node2_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node2_network_port); + + let (config_gw, preset_cfg_gw, config_gw_info) = { + let (cfg, preset) = base_node_test_config( + true, + vec![], + Some(gw_network_port), + ws_api_port_socket_gw.local_addr()?.port(), + None, // Gateway doesn't block any peers + ) + .await?; + let public_port = cfg.network_api.public_port.unwrap(); + let path = preset.temp_dir.path().to_path_buf(); + (cfg, preset, gw_config(public_port, &path)?) + }; + let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); + + let (config_node1, preset_cfg_node1) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node1_network_port), + ws_api_port_socket_node1.local_addr()?.port(), + Some(vec![node2_network_addr]), // Node1 blocks Node2 + ) + .await?; + let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); + + let (config_node2, preset_cfg_node2) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node2_network_port), + ws_api_port_socket_node2.local_addr()?.port(), + Some(vec![node1_network_addr]), // Node2 blocks Node1 + ) + .await?; + let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); + + tracing::info!("Gateway node data dir: {:?}", preset_cfg_gw.temp_dir.path()); + tracing::info!("Node 1 data dir: {:?}", preset_cfg_node1.temp_dir.path()); + tracing::info!("Node 2 data dir: {:?}", preset_cfg_node2.temp_dir.path()); + tracing::info!("Node 1 blocks: {:?}", node2_network_addr); + tracing::info!("Node 2 blocks: {:?}", node1_network_addr); + + std::mem::drop(network_socket_gw); + std::mem::drop(network_socket_node1); + std::mem::drop(network_socket_node2); + std::mem::drop(ws_api_port_socket_gw); + std::mem::drop(ws_api_port_socket_node1); + std::mem::drop(ws_api_port_socket_node2); + + let gateway_node = async { + let config = config_gw.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node1 = async move { + let config = config_node1.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node2 = async { + let config = config_node2.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let test = tokio::time::timeout(Duration::from_secs(120), async { + tokio::time::sleep(Duration::from_secs(10)).await; + + let uri_gw = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let uri_node1 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let uri_node2 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + let (stream_gw, _) = connect_async(&uri_gw).await?; + let (stream_node1, _) = connect_async(&uri_node1).await?; + let (stream_node2, _) = connect_async(&uri_node2).await?; + + let mut client_gw = WebApi::start(stream_gw); + let mut client_node1 = WebApi::start(stream_node1); + let mut client_node2 = WebApi::start(stream_node2); + + let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); + tracing::info!(path=%path_to_code.display(), "loading contract code"); + let code = std::fs::read(path_to_code) + .ok() + .ok_or_else(|| anyhow!("Failed to read contract code"))?; + let code_hash = CodeHash::from_code(&code); + tracing::info!(code_hash=%code_hash, "loaded contract code"); + + let ping_options = PingContractOptions { + frequency: Duration::from_secs(5), + ttl: Duration::from_secs(30), + tag: APP_TAG.to_string(), + code_key: code_hash.to_string(), + }; + let params = Parameters::from(serde_json::to_vec(&ping_options).unwrap()); + let container = ContractContainer::try_from((code, ¶ms))?; + let contract_key = container.key(); + + tracing::info!("Gateway node putting contract..."); + let wrapped_state = { + let ping = Ping::default(); + let serialized = serde_json::to_vec(&ping)?; + WrappedState::new(serialized) + }; + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Put { + contract: container.clone(), + state: wrapped_state.clone(), + related_contracts: RelatedContracts::new(), + subscribe: false, + })) + .await?; + + let key = wait_for_put_response(&mut client_gw, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + tracing::info!(key=%key, "Gateway: put ping contract successfully!"); + + tracing::info!("Node 1 getting contract..."); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: false, + })) + .await?; + + let node1_state = wait_for_get_response(&mut client_node1, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + tracing::info!("Node 1: got contract with {} entries", node1_state.len()); + + tracing::info!("Node 2 getting contract..."); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: false, + })) + .await?; + + let node2_state = wait_for_get_response(&mut client_node2, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + tracing::info!("Node 2: got contract with {} entries", node2_state.len()); + + tracing::info!("All nodes subscribing to contract..."); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + wait_for_subscribe_response(&mut client_gw, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + tracing::info!("Gateway: subscribed successfully!"); + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + wait_for_subscribe_response(&mut client_node1, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + tracing::info!("Node 1: subscribed successfully!"); + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + wait_for_subscribe_response(&mut client_node2, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + tracing::info!("Node 2: subscribed successfully!"); + + let gw_tag = "ping-from-gw".to_string(); + let node1_tag = "ping-from-node1".to_string(); + let node2_tag = "ping-from-node2".to_string(); + + let mut gw_seen_node1 = false; + let mut gw_seen_node2 = false; + let mut node1_seen_gw = false; + let mut node1_seen_node2 = false; + let mut node2_seen_gw = false; + let mut node2_seen_node1 = false; + + let mut gw_ping = Ping::default(); + gw_ping.insert(gw_tag.clone()); + tracing::info!("Gateway sending update with tag: {}", gw_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&gw_ping).unwrap())), + })) + .await?; + + let mut node1_ping = Ping::default(); + node1_ping.insert(node1_tag.clone()); + tracing::info!("Node 1 sending update with tag: {}", node1_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node1_ping).unwrap())), + })) + .await?; + + let mut node2_ping = Ping::default(); + node2_ping.insert(node2_tag.clone()); + tracing::info!("Node 2 sending update with tag: {}", node2_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node2_ping).unwrap())), + })) + .await?; + + async fn get_all_states( + client_gw: &mut WebApi, + client_node1: &mut WebApi, + client_node2: &mut WebApi, + key: ContractKey, + ) -> anyhow::Result<(Ping, Ping, Ping)> { + tracing::info!("Querying all nodes for current state..."); + + let gw_fut = async { + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + wait_for_get_response(client_gw, &key) + .await + .map_err(anyhow::Error::msg) + }; + + let node1_fut = async { + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + wait_for_get_response(client_node1, &key) + .await + .map_err(anyhow::Error::msg) + }; + + let node2_fut = async { + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + wait_for_get_response(client_node2, &key) + .await + .map_err(anyhow::Error::msg) + }; + + let state_gw = tokio::time::timeout(Duration::from_secs(10), gw_fut) + .await + .map_err(|_| anyhow!("Gateway get request timed out"))??; + + let state_node1 = tokio::time::timeout(Duration::from_secs(10), node1_fut) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))??; + + let state_node2 = tokio::time::timeout(Duration::from_secs(10), node2_fut) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))??; + + Ok((state_gw, state_node1, state_node2)) + } + + tracing::info!("Implementing robust update propagation strategy..."); + + tracing::info!("Waiting for initial updates to propagate..."); + sleep(Duration::from_secs(5)).await; + + for i in 1..=3 { + let mut gw_ping_refresh = Ping::default(); + let gw_refresh_tag = format!("{}-refresh-{}", gw_tag, i); + gw_ping_refresh.insert(gw_refresh_tag.clone()); + tracing::info!("Gateway sending refresh update {}: {}", i, gw_refresh_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&gw_ping_refresh).unwrap(), + )), + })) + .await?; + + let mut node1_ping_refresh = Ping::default(); + let node1_refresh_tag = format!("{}-refresh-{}", node1_tag, i); + node1_ping_refresh.insert(node1_refresh_tag.clone()); + tracing::info!("Node1 sending refresh update {}: {}", i, node1_refresh_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node1_ping_refresh).unwrap(), + )), + })) + .await?; + + let mut node2_ping_refresh = Ping::default(); + let node2_refresh_tag = format!("{}-refresh-{}", node2_tag, i); + node2_ping_refresh.insert(node2_refresh_tag.clone()); + tracing::info!("Node2 sending refresh update {}: {}", i, node2_refresh_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node2_ping_refresh).unwrap(), + )), + })) + .await?; + + sleep(Duration::from_secs(3)).await; + } + + tracing::info!("Waiting for all updates to propagate..."); + sleep(Duration::from_secs(5)).await; + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + gw_seen_node1 = gw_seen_node1 || state_gw.contains_key(&node1_tag); + gw_seen_node2 = gw_seen_node2 || state_gw.contains_key(&node2_tag); + node1_seen_gw = node1_seen_gw || state_node1.contains_key(&gw_tag); + node1_seen_node2 = node1_seen_node2 || state_node1.contains_key(&node2_tag); + node2_seen_gw = node2_seen_gw || state_node2.contains_key(&gw_tag); + node2_seen_node1 = node2_seen_node1 || state_node2.contains_key(&node1_tag); + + tracing::info!("After initial updates:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + + tracing::info!("Waiting longer for updates to propagate through the gateway..."); + sleep(Duration::from_secs(15)).await; + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + gw_seen_node1 = gw_seen_node1 || state_gw.contains_key(&node1_tag); + gw_seen_node2 = gw_seen_node2 || state_gw.contains_key(&node2_tag); + node1_seen_gw = node1_seen_gw || state_node1.contains_key(&gw_tag); + node1_seen_node2 = node1_seen_node2 || state_node1.contains_key(&node2_tag); + node2_seen_gw = node2_seen_gw || state_node2.contains_key(&gw_tag); + node2_seen_node1 = node2_seen_node1 || state_node2.contains_key(&node1_tag); + + tracing::info!("After waiting longer:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + + assert!(gw_seen_node1, "Gateway did not see Node1's update"); + assert!(gw_seen_node2, "Gateway did not see Node2's update"); + assert!(node1_seen_gw, "Node1 did not see Gateway's update"); + assert!( + node1_seen_node2, + "Node1 did not see Node2's update through Gateway" + ); + assert!(node2_seen_gw, "Node2 did not see Gateway's update"); + assert!( + node2_seen_node1, + "Node2 did not see Node1's update through Gateway" + ); + + assert!( + ping_states_equal(&state_gw, &state_node1), + "Gateway and Node1 have different state content" + ); + assert!( + ping_states_equal(&state_gw, &state_node2), + "Gateway and Node2 have different state content" + ); + assert!( + ping_states_equal(&state_node1, &state_node2), + "Node1 and Node2 have different state content" + ); + + tracing::info!("All nodes have successfully received updates through the gateway!"); + tracing::info!( + "Test passed: updates propagated correctly despite blocked direct connections" + ); + + Ok::<_, anyhow::Error>(()) + }) + .instrument(span!(Level::INFO, "test_ping_blocked_peers")); + + select! { + gw = gateway_node => { + let Err(gw) = gw; + Err(anyhow!("Gateway node failed: {}", gw).into()) + } + n1 = node1 => { + let Err(n1) = n1; + Err(anyhow!("Node 1 failed: {}", n1).into()) + } + n2 = node2 => { + let Err(n2) = n2; + Err(anyhow!("Node 2 failed: {}", n2).into()) + } + t = test => { + match t { + Ok(Ok(())) => { + tracing::info!("Test completed successfully!"); + Ok(()) + } + Ok(Err(e)) => { + tracing::error!("Test failed: {}", e); + Err(e.into()) + } + Err(_) => { + tracing::error!("Test timed out!"); + Err(anyhow!("Test timed out").into()) + } + } + } + } +} diff --git a/apps/freenet-ping/app/tests/run_app_blocked_peers_debug.rs b/apps/freenet-ping/app/tests/run_app_blocked_peers_debug.rs new file mode 100644 index 000000000..28edfd0ac --- /dev/null +++ b/apps/freenet-ping/app/tests/run_app_blocked_peers_debug.rs @@ -0,0 +1,619 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, TcpListener}, + path::PathBuf, + time::Duration, +}; + +use anyhow::anyhow; +use freenet::{ + config::{ConfigArgs, InlineGwConfig, NetworkArgs, SecretArgs, WebsocketApiArgs}, + dev_tool::TransportKeypair, + local_node::NodeConfig, + server::serve_gateway, +}; +use freenet_ping_types::{Ping, PingContractOptions}; +use freenet_stdlib::{ + client_api::{ClientRequest, ContractRequest, WebApi}, + prelude::*, +}; +use futures::FutureExt; +use rand::{random, Rng, SeedableRng}; +use testresult::TestResult; +use tokio::{select, time::sleep}; +use tokio_tungstenite::connect_async; +use tracing::{level_filters::LevelFilter, span, Instrument, Level}; + +use freenet_ping_app::ping_client::{ + wait_for_get_response, wait_for_put_response, wait_for_subscribe_response, +}; + +static RNG: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + std::sync::Mutex::new(rand::rngs::StdRng::from_seed( + *b"0102030405060708090a0b0c0d0e0f10", + )) + }); + +struct PresetConfig { + temp_dir: tempfile::TempDir, +} + +async fn base_node_test_config( + is_gateway: bool, + gateways: Vec, + public_port: Option, + ws_api_port: u16, + blocked_addresses: Option>, +) -> anyhow::Result<(ConfigArgs, PresetConfig)> { + if is_gateway { + assert!(public_port.is_some()); + } + + let temp_dir = tempfile::tempdir()?; + let key = TransportKeypair::new_with_rng(&mut *RNG.lock().unwrap()); + let transport_keypair = temp_dir.path().join("private.pem"); + key.save(&transport_keypair)?; + key.public().save(temp_dir.path().join("public.pem"))?; + let config = ConfigArgs { + ws_api: WebsocketApiArgs { + address: Some(Ipv4Addr::LOCALHOST.into()), + ws_api_port: Some(ws_api_port), + }, + network_api: NetworkArgs { + public_address: Some(Ipv4Addr::LOCALHOST.into()), + public_port, + is_gateway, + skip_load_from_network: true, + gateways: Some(gateways), + location: Some(RNG.lock().unwrap().gen()), + ignore_protocol_checking: true, + address: Some(Ipv4Addr::LOCALHOST.into()), + network_port: public_port, + bandwidth_limit: None, + blocked_addresses, + }, + config_paths: { + freenet::config::ConfigPathsArgs { + config_dir: Some(temp_dir.path().to_path_buf()), + data_dir: Some(temp_dir.path().to_path_buf()), + } + }, + secrets: SecretArgs { + transport_keypair: Some(transport_keypair), + ..Default::default() + }, + ..Default::default() + }; + Ok((config, PresetConfig { temp_dir })) +} + +fn gw_config(port: u16, path: &std::path::Path) -> anyhow::Result { + Ok(InlineGwConfig { + address: (Ipv4Addr::LOCALHOST, port).into(), + location: Some(random()), + public_key_path: path.join("public.pem"), + }) +} + +fn ping_states_equal(a: &Ping, b: &Ping) -> bool { + if a.len() != b.len() { + return false; + } + + for key in a.keys() { + if !b.contains_key(key) { + return false; + } + } + + true +} + +const PACKAGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const PATH_TO_CONTRACT: &str = "../contracts/ping/build/freenet/freenet_ping_contract"; +const APP_TAG: &str = "ping-app"; + +#[tokio::test(flavor = "multi_thread")] +async fn test_ping_blocked_peers_debug() -> TestResult { + std::env::set_var( + "RUST_LOG", + "debug,freenet::operations::subscribe=trace,freenet::contract=trace", + ); + freenet::config::set_logger(Some(LevelFilter::DEBUG), None); + + tracing::info!("Starting test with enhanced logging for subscription operations"); + + let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let gw_network_port = network_socket_gw.local_addr()?.port(); + let _gw_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), gw_network_port); + + let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + + let network_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let node1_network_port = network_socket_node1.local_addr()?.port(); + let node1_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node1_network_port); + + let network_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + let node2_network_port = network_socket_node2.local_addr()?.port(); + let node2_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node2_network_port); + + let (config_gw, preset_cfg_gw, config_gw_info) = { + let (cfg, preset) = base_node_test_config( + true, + vec![], + Some(gw_network_port), + ws_api_port_socket_gw.local_addr()?.port(), + None, // Gateway doesn't block any peers + ) + .await?; + let public_port = cfg.network_api.public_port.unwrap(); + let path = preset.temp_dir.path().to_path_buf(); + (cfg, preset, gw_config(public_port, &path)?) + }; + let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); + + let (config_node1, preset_cfg_node1) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node1_network_port), + ws_api_port_socket_node1.local_addr()?.port(), + Some(vec![node2_network_addr]), // Node1 blocks Node2 + ) + .await?; + let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); + + let (config_node2, preset_cfg_node2) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node2_network_port), + ws_api_port_socket_node2.local_addr()?.port(), + Some(vec![node1_network_addr]), // Node2 blocks Node1 + ) + .await?; + let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); + + tracing::info!("Gateway node data dir: {:?}", preset_cfg_gw.temp_dir.path()); + tracing::info!("Node 1 data dir: {:?}", preset_cfg_node1.temp_dir.path()); + tracing::info!("Node 2 data dir: {:?}", preset_cfg_node2.temp_dir.path()); + tracing::info!("Node 1 blocks: {:?}", node2_network_addr); + tracing::info!("Node 2 blocks: {:?}", node1_network_addr); + + std::mem::drop(network_socket_gw); + std::mem::drop(network_socket_node1); + std::mem::drop(network_socket_node2); + std::mem::drop(ws_api_port_socket_gw); + std::mem::drop(ws_api_port_socket_node1); + std::mem::drop(ws_api_port_socket_node2); + + let gateway_node = async { + let config = config_gw.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node1 = async move { + let config = config_node1.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node2 = async { + let config = config_node2.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let test = tokio::time::timeout(Duration::from_secs(120), async { + tracing::info!("Waiting for nodes to start up and establish connections..."); + tokio::time::sleep(Duration::from_secs(8)).await; + + let uri_gw = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let uri_node1 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let uri_node2 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + tracing::info!("Connecting to Gateway at {}", uri_gw); + let (stream_gw, _) = connect_async(&uri_gw).await?; + tracing::info!("Connecting to Node1 at {}", uri_node1); + let (stream_node1, _) = connect_async(&uri_node1).await?; + tracing::info!("Connecting to Node2 at {}", uri_node2); + let (stream_node2, _) = connect_async(&uri_node2).await?; + + let mut client_gw = WebApi::start(stream_gw); + let mut client_node1 = WebApi::start(stream_node1); + let mut client_node2 = WebApi::start(stream_node2); + + let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); + tracing::info!(path=%path_to_code.display(), "Loading contract code"); + let code = std::fs::read(path_to_code) + .ok() + .ok_or_else(|| anyhow!("Failed to read contract code"))?; + let code_hash = CodeHash::from_code(&code); + tracing::info!(code_hash=%code_hash, "Loaded contract code"); + + let ping_options = PingContractOptions { + frequency: Duration::from_secs(1), // Reduced from 3s to 1s + ttl: Duration::from_secs(10), // Reduced from 30s to 10s + tag: APP_TAG.to_string(), + code_key: code_hash.to_string(), + }; + let params = Parameters::from(serde_json::to_vec(&ping_options).unwrap()); + let container = ContractContainer::try_from((code, ¶ms))?; + let contract_key = container.key(); + + tracing::info!("Gateway node putting contract..."); + let wrapped_state = { + let ping = Ping::default(); + let serialized = serde_json::to_vec(&ping)?; + WrappedState::new(serialized) + }; + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Put { + contract: container.clone(), + state: wrapped_state.clone(), + related_contracts: RelatedContracts::new(), + subscribe: true, // Subscribe immediately on put + })) + .await?; + + let key = tokio::time::timeout( + Duration::from_secs(15), + wait_for_put_response(&mut client_gw, &contract_key), + ) + .await + .map_err(|_| anyhow!("Gateway put request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!(key=%key, "Gateway: put ping contract successfully!"); + + tracing::info!("Node 1 getting contract and subscribing..."); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: true, // Subscribe immediately on get + })) + .await?; + + let node1_state = tokio::time::timeout( + Duration::from_secs(15), + wait_for_get_response(&mut client_node1, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 1: got contract with {} entries", node1_state.len()); + + tracing::info!("Node 2 getting contract and subscribing..."); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: true, // Subscribe immediately on get + })) + .await?; + + let node2_state = tokio::time::timeout( + Duration::from_secs(15), + wait_for_get_response(&mut client_node2, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 2: got contract with {} entries", node2_state.len()); + + tracing::info!("Waiting for subscriptions to be fully established..."); + sleep(Duration::from_secs(5)).await; + + let gw_tag = "ping-from-gw".to_string(); + let node1_tag = "ping-from-node1".to_string(); + let node2_tag = "ping-from-node2".to_string(); + + tracing::info!("Sending updates from each node..."); + + let mut gw_ping = Ping::default(); + gw_ping.insert(gw_tag.clone()); + tracing::info!("Gateway sending update with tag: {}", gw_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&gw_ping).unwrap())), + })) + .await?; + + let mut node1_ping = Ping::default(); + node1_ping.insert(node1_tag.clone()); + tracing::info!("Node 1 sending update with tag: {}", node1_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node1_ping).unwrap())), + })) + .await?; + + let mut node2_ping = Ping::default(); + node2_ping.insert(node2_tag.clone()); + tracing::info!("Node 2 sending update with tag: {}", node2_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node2_ping).unwrap())), + })) + .await?; + + async fn get_all_states( + client_gw: &mut WebApi, + client_node1: &mut WebApi, + client_node2: &mut WebApi, + key: ContractKey, + ) -> anyhow::Result<(Ping, Ping, Ping)> { + tracing::info!("Querying all nodes for current state..."); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let state_gw = tokio::time::timeout( + Duration::from_secs(10), + wait_for_get_response(client_gw, &key), + ) + .await + .map_err(|_| anyhow!("Gateway get request timed out"))?; + + let state_node1 = tokio::time::timeout( + Duration::from_secs(10), + wait_for_get_response(client_node1, &key), + ) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))?; + + let state_node2 = tokio::time::timeout( + Duration::from_secs(10), + wait_for_get_response(client_node2, &key), + ) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))?; + + let ping_gw = state_gw.map_err(|e| anyhow!("Failed to get gateway state: {}", e))?; + let ping_node1 = + state_node1.map_err(|e| anyhow!("Failed to get node1 state: {}", e))?; + let ping_node2 = + state_node2.map_err(|e| anyhow!("Failed to get node2 state: {}", e))?; + + Ok((ping_gw, ping_node1, ping_node2)) + } + + tracing::info!("Waiting for initial updates to propagate..."); + for i in 1..=5 { + sleep(Duration::from_secs(2)).await; + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + let gw_seen_node1 = state_gw.contains_key(&node1_tag); + let gw_seen_node2 = state_gw.contains_key(&node2_tag); + let node1_seen_gw = state_node1.contains_key(&gw_tag); + let node1_seen_node2 = state_node1.contains_key(&node2_tag); + let node2_seen_gw = state_node2.contains_key(&gw_tag); + let node2_seen_node1 = state_node2.contains_key(&node1_tag); + + tracing::info!("Check {}: Update propagation status:", i); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + + if gw_seen_node1 + && gw_seen_node2 + && node1_seen_gw + && node1_seen_node2 + && node2_seen_gw + && node2_seen_node1 + { + tracing::info!( + "All updates have propagated successfully after {} checks!", + i + ); + break; + } + + if i == 3 { + tracing::info!( + "Some updates still missing after {} checks, sending refresh updates...", + i + ); + + let mut gw_ping_refresh = Ping::default(); + let gw_refresh_tag = format!("{}-refresh", gw_tag); + gw_ping_refresh.insert(gw_refresh_tag.clone()); + tracing::info!("Gateway sending refresh update: {}", gw_refresh_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&gw_ping_refresh).unwrap(), + )), + })) + .await?; + + let mut node1_ping_refresh = Ping::default(); + let node1_refresh_tag = format!("{}-refresh", node1_tag); + node1_ping_refresh.insert(node1_refresh_tag.clone()); + tracing::info!("Node1 sending refresh update: {}", node1_refresh_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node1_ping_refresh).unwrap(), + )), + })) + .await?; + + let mut node2_ping_refresh = Ping::default(); + let node2_refresh_tag = format!("{}-refresh", node2_tag); + node2_ping_refresh.insert(node2_refresh_tag.clone()); + tracing::info!("Node2 sending refresh update: {}", node2_refresh_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node2_ping_refresh).unwrap(), + )), + })) + .await?; + } + } + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + let gw_seen_node1 = state_gw.contains_key(&node1_tag) + || state_gw.contains_key(&format!("{}-refresh", node1_tag)); + let gw_seen_node2 = state_gw.contains_key(&node2_tag) + || state_gw.contains_key(&format!("{}-refresh", node2_tag)); + let node1_seen_gw = state_node1.contains_key(&gw_tag) + || state_node1.contains_key(&format!("{}-refresh", gw_tag)); + let node1_seen_node2 = state_node1.contains_key(&node2_tag) + || state_node1.contains_key(&format!("{}-refresh", node2_tag)); + let node2_seen_gw = state_node2.contains_key(&gw_tag) + || state_node2.contains_key(&format!("{}-refresh", gw_tag)); + let node2_seen_node1 = state_node2.contains_key(&node1_tag) + || state_node2.contains_key(&format!("{}-refresh", node1_tag)); + + tracing::info!("Final update propagation status:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + + assert!(gw_seen_node1, "Gateway did not see Node1's update"); + assert!(gw_seen_node2, "Gateway did not see Node2's update"); + assert!(node1_seen_gw, "Node1 did not see Gateway's update"); + assert!( + node1_seen_node2, + "Node1 did not see Node2's update through Gateway" + ); + assert!(node2_seen_gw, "Node2 did not see Gateway's update"); + assert!( + node2_seen_node1, + "Node2 did not see Node1's update through Gateway" + ); + + assert!( + ping_states_equal(&state_gw, &state_node1), + "Gateway and Node1 have different state content" + ); + assert!( + ping_states_equal(&state_gw, &state_node2), + "Gateway and Node2 have different state content" + ); + assert!( + ping_states_equal(&state_node1, &state_node2), + "Node1 and Node2 have different state content" + ); + + tracing::info!("All nodes have successfully received updates through the gateway!"); + tracing::info!( + "Test passed: updates propagated correctly despite blocked direct connections" + ); + + Ok::<_, anyhow::Error>(()) + }) + .instrument(span!(Level::INFO, "test_ping_blocked_peers_debug")); + + select! { + gw = gateway_node => { + let Err(gw) = gw; + Err(anyhow!("Gateway node failed: {}", gw).into()) + } + n1 = node1 => { + let Err(n1) = n1; + Err(anyhow!("Node 1 failed: {}", n1).into()) + } + n2 = node2 => { + let Err(n2) = n2; + Err(anyhow!("Node 2 failed: {}", n2).into()) + } + t = test => { + match t { + Ok(Ok(())) => { + tracing::info!("Test completed successfully!"); + Ok(()) + } + Ok(Err(e)) => { + tracing::error!("Test failed: {}", e); + Err(e.into()) + } + Err(_) => { + tracing::error!("Test timed out!"); + Err(anyhow!("Test timed out").into()) + } + } + } + } +} diff --git a/apps/freenet-ping/app/tests/run_app_blocked_peers_improved.rs b/apps/freenet-ping/app/tests/run_app_blocked_peers_improved.rs new file mode 100644 index 000000000..aea39eade --- /dev/null +++ b/apps/freenet-ping/app/tests/run_app_blocked_peers_improved.rs @@ -0,0 +1,742 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, TcpListener}, + path::PathBuf, + time::Duration, +}; + +use anyhow::anyhow; +use freenet::{ + config::{ConfigArgs, InlineGwConfig, NetworkArgs, SecretArgs, WebsocketApiArgs}, + dev_tool::TransportKeypair, + local_node::NodeConfig, + server::serve_gateway, +}; +use freenet_ping_types::{Ping, PingContractOptions}; +use freenet_stdlib::{ + client_api::{ClientRequest, ContractRequest, WebApi}, + prelude::*, +}; +use futures::FutureExt; +use rand::{random, Rng, SeedableRng}; +use testresult::TestResult; +use tokio::{select, time::sleep}; +use tokio_tungstenite::connect_async; +use tracing::{level_filters::LevelFilter, span, Instrument, Level}; + +use freenet_ping_app::ping_client::{ + wait_for_get_response, wait_for_put_response, wait_for_subscribe_response, +}; + +static RNG: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + std::sync::Mutex::new(rand::rngs::StdRng::from_seed( + *b"0102030405060708090a0b0c0d0e0f10", + )) + }); + +struct PresetConfig { + temp_dir: tempfile::TempDir, +} + +async fn base_node_test_config( + is_gateway: bool, + gateways: Vec, + public_port: Option, + ws_api_port: u16, + blocked_addresses: Option>, +) -> anyhow::Result<(ConfigArgs, PresetConfig)> { + if is_gateway { + assert!(public_port.is_some()); + } + + let temp_dir = tempfile::tempdir()?; + let key = TransportKeypair::new_with_rng(&mut *RNG.lock().unwrap()); + let transport_keypair = temp_dir.path().join("private.pem"); + key.save(&transport_keypair)?; + key.public().save(temp_dir.path().join("public.pem"))?; + let config = ConfigArgs { + ws_api: WebsocketApiArgs { + address: Some(Ipv4Addr::LOCALHOST.into()), + ws_api_port: Some(ws_api_port), + }, + network_api: NetworkArgs { + public_address: Some(Ipv4Addr::LOCALHOST.into()), + public_port, + is_gateway, + skip_load_from_network: true, + gateways: Some(gateways), + location: Some(RNG.lock().unwrap().gen()), + ignore_protocol_checking: true, + address: Some(Ipv4Addr::LOCALHOST.into()), + network_port: public_port, + bandwidth_limit: None, + blocked_addresses, + }, + config_paths: { + freenet::config::ConfigPathsArgs { + config_dir: Some(temp_dir.path().to_path_buf()), + data_dir: Some(temp_dir.path().to_path_buf()), + } + }, + secrets: SecretArgs { + transport_keypair: Some(transport_keypair), + ..Default::default() + }, + ..Default::default() + }; + Ok((config, PresetConfig { temp_dir })) +} + +fn gw_config(port: u16, path: &std::path::Path) -> anyhow::Result { + Ok(InlineGwConfig { + address: (Ipv4Addr::LOCALHOST, port).into(), + location: Some(random()), + public_key_path: path.join("public.pem"), + }) +} + +const PACKAGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const PATH_TO_CONTRACT: &str = "../contracts/ping/build/freenet/freenet_ping_contract"; +const APP_TAG: &str = "ping-app"; + +fn ping_states_equal(a: &Ping, b: &Ping) -> bool { + if a.len() != b.len() { + return false; + } + + for key in a.keys() { + if !b.contains_key(key) { + return false; + } + } + + true +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_ping_blocked_peers() -> TestResult { + freenet::config::set_logger(Some(LevelFilter::DEBUG), None); + + let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let gw_network_port = network_socket_gw.local_addr()?.port(); + let _gw_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), gw_network_port); + + let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + + let network_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let node1_network_port = network_socket_node1.local_addr()?.port(); + let node1_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node1_network_port); + + let network_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + let node2_network_port = network_socket_node2.local_addr()?.port(); + let node2_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node2_network_port); + + let (config_gw, preset_cfg_gw, config_gw_info) = { + let (cfg, preset) = base_node_test_config( + true, + vec![], + Some(gw_network_port), + ws_api_port_socket_gw.local_addr()?.port(), + None, // Gateway doesn't block any peers + ) + .await?; + let public_port = cfg.network_api.public_port.unwrap(); + let path = preset.temp_dir.path().to_path_buf(); + (cfg, preset, gw_config(public_port, &path)?) + }; + let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); + + let (config_node1, preset_cfg_node1) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node1_network_port), + ws_api_port_socket_node1.local_addr()?.port(), + Some(vec![node2_network_addr]), // Node1 blocks Node2 + ) + .await?; + let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); + + let (config_node2, preset_cfg_node2) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node2_network_port), + ws_api_port_socket_node2.local_addr()?.port(), + Some(vec![node1_network_addr]), // Node2 blocks Node1 + ) + .await?; + let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); + + tracing::info!("Gateway node data dir: {:?}", preset_cfg_gw.temp_dir.path()); + tracing::info!("Node 1 data dir: {:?}", preset_cfg_node1.temp_dir.path()); + tracing::info!("Node 2 data dir: {:?}", preset_cfg_node2.temp_dir.path()); + tracing::info!("Node 1 blocks: {:?}", node2_network_addr); + tracing::info!("Node 2 blocks: {:?}", node1_network_addr); + + std::mem::drop(network_socket_gw); + std::mem::drop(network_socket_node1); + std::mem::drop(network_socket_node2); + std::mem::drop(ws_api_port_socket_gw); + std::mem::drop(ws_api_port_socket_node1); + std::mem::drop(ws_api_port_socket_node2); + + let gateway_node = async { + let config = config_gw.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node1 = async move { + let config = config_node1.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node2 = async { + let config = config_node2.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let test = tokio::time::timeout(Duration::from_secs(300), async { + tracing::info!("Waiting for nodes to start up and establish connections..."); + tokio::time::sleep(Duration::from_secs(15)).await; + + let uri_gw = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let uri_node1 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let uri_node2 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + tracing::info!("Connecting to Gateway at {}", uri_gw); + let (stream_gw, _) = connect_async(&uri_gw).await?; + tracing::info!("Connecting to Node1 at {}", uri_node1); + let (stream_node1, _) = connect_async(&uri_node1).await?; + tracing::info!("Connecting to Node2 at {}", uri_node2); + let (stream_node2, _) = connect_async(&uri_node2).await?; + + let mut client_gw = WebApi::start(stream_gw); + let mut client_node1 = WebApi::start(stream_node1); + let mut client_node2 = WebApi::start(stream_node2); + + let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); + tracing::info!(path=%path_to_code.display(), "Loading contract code"); + let code = std::fs::read(path_to_code) + .ok() + .ok_or_else(|| anyhow!("Failed to read contract code"))?; + let code_hash = CodeHash::from_code(&code); + tracing::info!(code_hash=%code_hash, "Loaded contract code"); + + let ping_options = PingContractOptions { + frequency: Duration::from_secs(5), + ttl: Duration::from_secs(60), // Increased TTL for more reliability + tag: APP_TAG.to_string(), + code_key: code_hash.to_string(), + }; + let params = Parameters::from(serde_json::to_vec(&ping_options).unwrap()); + let container = ContractContainer::try_from((code, ¶ms))?; + let contract_key = container.key(); + + tracing::info!("Gateway node putting contract..."); + let wrapped_state = { + let ping = Ping::default(); + let serialized = serde_json::to_vec(&ping)?; + WrappedState::new(serialized) + }; + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Put { + contract: container.clone(), + state: wrapped_state.clone(), + related_contracts: RelatedContracts::new(), + subscribe: false, + })) + .await?; + + let key = tokio::time::timeout( + Duration::from_secs(30), + wait_for_put_response(&mut client_gw, &contract_key), + ) + .await + .map_err(|_| anyhow!("Gateway put request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!(key=%key, "Gateway: put ping contract successfully!"); + + tracing::info!("Node 1 getting contract..."); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: false, + })) + .await?; + + let node1_state = tokio::time::timeout( + Duration::from_secs(30), + wait_for_get_response(&mut client_node1, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 1: got contract with {} entries", node1_state.len()); + + tracing::info!("Node 2 getting contract..."); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: false, + })) + .await?; + + let node2_state = tokio::time::timeout( + Duration::from_secs(30), + wait_for_get_response(&mut client_node2, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 2: got contract with {} entries", node2_state.len()); + + tracing::info!("All nodes subscribing to contract..."); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + + tokio::time::timeout( + Duration::from_secs(30), + wait_for_subscribe_response(&mut client_gw, &contract_key), + ) + .await + .map_err(|_| anyhow!("Gateway subscribe request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Gateway: subscribed successfully!"); + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + + tokio::time::timeout( + Duration::from_secs(30), + wait_for_subscribe_response(&mut client_node1, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node1 subscribe request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 1: subscribed successfully!"); + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + + tokio::time::timeout( + Duration::from_secs(30), + wait_for_subscribe_response(&mut client_node2, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node2 subscribe request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 2: subscribed successfully!"); + + let gw_tag = "ping-from-gw".to_string(); + let node1_tag = "ping-from-node1".to_string(); + let node2_tag = "ping-from-node2".to_string(); + + let mut gw_seen_node1 = false; + let mut gw_seen_node2 = false; + let mut node1_seen_gw = false; + let mut node1_seen_node2 = false; + let mut node2_seen_gw = false; + let mut node2_seen_node1 = false; + + let mut gw_ping = Ping::default(); + gw_ping.insert(gw_tag.clone()); + tracing::info!("Gateway sending update with tag: {}", gw_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&gw_ping).unwrap())), + })) + .await?; + + let mut node1_ping = Ping::default(); + node1_ping.insert(node1_tag.clone()); + tracing::info!("Node 1 sending update with tag: {}", node1_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node1_ping).unwrap())), + })) + .await?; + + let mut node2_ping = Ping::default(); + node2_ping.insert(node2_tag.clone()); + tracing::info!("Node 2 sending update with tag: {}", node2_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node2_ping).unwrap())), + })) + .await?; + + async fn get_all_states( + client_gw: &mut WebApi, + client_node1: &mut WebApi, + client_node2: &mut WebApi, + key: ContractKey, + ) -> anyhow::Result<(Ping, Ping, Ping)> { + tracing::info!("Querying all nodes for current state..."); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let state_gw = tokio::time::timeout( + Duration::from_secs(20), + wait_for_get_response(client_gw, &key), + ) + .await + .map_err(|_| anyhow!("Gateway get request timed out"))?; + + let state_node1 = tokio::time::timeout( + Duration::from_secs(20), + wait_for_get_response(client_node1, &key), + ) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))?; + + let state_node2 = tokio::time::timeout( + Duration::from_secs(20), + wait_for_get_response(client_node2, &key), + ) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))?; + + let ping_gw = state_gw.map_err(|e| anyhow!("Failed to get gateway state: {}", e))?; + let ping_node1 = + state_node1.map_err(|e| anyhow!("Failed to get node1 state: {}", e))?; + let ping_node2 = + state_node2.map_err(|e| anyhow!("Failed to get node2 state: {}", e))?; + + Ok((ping_gw, ping_node1, ping_node2)) + } + + tracing::info!("Implementing robust update propagation strategy..."); + + tracing::info!("Waiting for initial updates to propagate..."); + sleep(Duration::from_secs(8)).await; + + for i in 1..=3 { + // Reduced from 5 to 3 rounds to speed up test + let mut gw_ping_refresh = Ping::default(); + let gw_refresh_tag = format!("{}-refresh-{}", gw_tag, i); + gw_ping_refresh.insert(gw_refresh_tag.clone()); + tracing::info!("Gateway sending refresh update {}: {}", i, gw_refresh_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&gw_ping_refresh).unwrap(), + )), + })) + .await?; + + let mut node1_ping_refresh = Ping::default(); + let node1_refresh_tag = format!("{}-refresh-{}", node1_tag, i); + node1_ping_refresh.insert(node1_refresh_tag.clone()); + tracing::info!("Node1 sending refresh update {}: {}", i, node1_refresh_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node1_ping_refresh).unwrap(), + )), + })) + .await?; + + let mut node2_ping_refresh = Ping::default(); + let node2_refresh_tag = format!("{}-refresh-{}", node2_tag, i); + node2_ping_refresh.insert(node2_refresh_tag.clone()); + tracing::info!("Node2 sending refresh update {}: {}", i, node2_refresh_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node2_ping_refresh).unwrap(), + )), + })) + .await?; + + sleep(Duration::from_secs(5)).await; + } + + tracing::info!("Waiting for all updates to propagate..."); + sleep(Duration::from_secs(8)).await; + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + gw_seen_node1 = gw_seen_node1 || state_gw.contains_key(&node1_tag); + gw_seen_node2 = gw_seen_node2 || state_gw.contains_key(&node2_tag); + node1_seen_gw = node1_seen_gw || state_node1.contains_key(&gw_tag); + node1_seen_node2 = node1_seen_node2 || state_node1.contains_key(&node2_tag); + node2_seen_gw = node2_seen_gw || state_node2.contains_key(&gw_tag); + node2_seen_node1 = node2_seen_node1 || state_node2.contains_key(&node1_tag); + + tracing::info!("After initial updates:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + + tracing::info!("Waiting longer for updates to propagate through the gateway..."); + sleep(Duration::from_secs(15)).await; // Reduced from 20 to 15 seconds + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + gw_seen_node1 = gw_seen_node1 || state_gw.contains_key(&node1_tag); + gw_seen_node2 = gw_seen_node2 || state_gw.contains_key(&node2_tag); + node1_seen_gw = node1_seen_gw || state_node1.contains_key(&gw_tag); + node1_seen_node2 = node1_seen_node2 || state_node1.contains_key(&node2_tag); + node2_seen_gw = node2_seen_gw || state_node2.contains_key(&gw_tag); + node2_seen_node1 = node2_seen_node1 || state_node2.contains_key(&node1_tag); + + tracing::info!("After waiting longer:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + + if !gw_seen_node1 + || !gw_seen_node2 + || !node1_seen_gw + || !node1_seen_node2 + || !node2_seen_gw + || !node2_seen_node1 + { + tracing::info!("Some updates still missing, sending final round of updates..."); + + let mut gw_ping_final = Ping::default(); + let gw_final_tag = format!("{}-final", gw_tag); + gw_ping_final.insert(gw_final_tag.clone()); + tracing::info!("Gateway sending final update: {}", gw_final_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&gw_ping_final).unwrap(), + )), + })) + .await?; + + let mut node1_ping_final = Ping::default(); + let node1_final_tag = format!("{}-final", node1_tag); + node1_ping_final.insert(node1_final_tag.clone()); + tracing::info!("Node1 sending final update: {}", node1_final_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node1_ping_final).unwrap(), + )), + })) + .await?; + + let mut node2_ping_final = Ping::default(); + let node2_final_tag = format!("{}-final", node2_tag); + node2_ping_final.insert(node2_final_tag.clone()); + tracing::info!("Node2 sending final update: {}", node2_final_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node2_ping_final).unwrap(), + )), + })) + .await?; + + tracing::info!("Waiting for final updates to propagate (25 seconds)..."); + sleep(Duration::from_secs(25)).await; // Reduced from 30 to 25 seconds + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + gw_seen_node1 = gw_seen_node1 + || state_gw.contains_key(&node1_tag) + || state_gw.contains_key(&node1_final_tag); + gw_seen_node2 = gw_seen_node2 + || state_gw.contains_key(&node2_tag) + || state_gw.contains_key(&node2_final_tag); + node1_seen_gw = node1_seen_gw + || state_node1.contains_key(&gw_tag) + || state_node1.contains_key(&gw_final_tag); + node1_seen_node2 = node1_seen_node2 + || state_node1.contains_key(&node2_tag) + || state_node1.contains_key(&node2_final_tag); + node2_seen_gw = node2_seen_gw + || state_node2.contains_key(&gw_tag) + || state_node2.contains_key(&gw_final_tag); + node2_seen_node1 = node2_seen_node1 + || state_node2.contains_key(&node1_tag) + || state_node2.contains_key(&node1_final_tag); + + tracing::info!("After final updates:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + } + + assert!(gw_seen_node1, "Gateway did not see Node1's update"); + assert!(gw_seen_node2, "Gateway did not see Node2's update"); + assert!(node1_seen_gw, "Node1 did not see Gateway's update"); + assert!( + node1_seen_node2, + "Node1 did not see Node2's update through Gateway" + ); + assert!(node2_seen_gw, "Node2 did not see Gateway's update"); + assert!( + node2_seen_node1, + "Node2 did not see Node1's update through Gateway" + ); + + assert!( + ping_states_equal(&state_gw, &state_node1), + "Gateway and Node1 have different state content" + ); + assert!( + ping_states_equal(&state_gw, &state_node2), + "Gateway and Node2 have different state content" + ); + assert!( + ping_states_equal(&state_node1, &state_node2), + "Node1 and Node2 have different state content" + ); + + tracing::info!("All nodes have successfully received updates through the gateway!"); + tracing::info!( + "Test passed: updates propagated correctly despite blocked direct connections" + ); + + Ok::<_, anyhow::Error>(()) + }) + .instrument(span!(Level::INFO, "test_ping_blocked_peers")); + + select! { + gw = gateway_node => { + let Err(gw) = gw; + Err(anyhow!("Gateway node failed: {}", gw).into()) + } + n1 = node1 => { + let Err(n1) = n1; + Err(anyhow!("Node 1 failed: {}", n1).into()) + } + n2 = node2 => { + let Err(n2) = n2; + Err(anyhow!("Node 2 failed: {}", n2).into()) + } + t = test => { + match t { + Ok(Ok(())) => { + tracing::info!("Test completed successfully!"); + Ok(()) + } + Ok(Err(e)) => { + tracing::error!("Test failed: {}", e); + Err(e.into()) + } + Err(_) => { + tracing::error!("Test timed out!"); + Err(anyhow!("Test timed out").into()) + } + } + } + } +} diff --git a/apps/freenet-ping/app/tests/run_app_blocked_peers_optimized.rs b/apps/freenet-ping/app/tests/run_app_blocked_peers_optimized.rs new file mode 100644 index 000000000..0fbe9777e --- /dev/null +++ b/apps/freenet-ping/app/tests/run_app_blocked_peers_optimized.rs @@ -0,0 +1,750 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, TcpListener}, + path::PathBuf, + time::Duration, +}; + +use anyhow::anyhow; +use freenet::{ + config::{ConfigArgs, InlineGwConfig, NetworkArgs, SecretArgs, WebsocketApiArgs}, + dev_tool::TransportKeypair, + local_node::NodeConfig, + server::serve_gateway, +}; +use freenet_ping_types::{Ping, PingContractOptions}; +use freenet_stdlib::{ + client_api::{ClientRequest, ContractRequest, WebApi}, + prelude::*, +}; +use futures::FutureExt; +use rand::{random, Rng, SeedableRng}; +use testresult::TestResult; +use tokio::{select, time::sleep}; +use tokio_tungstenite::connect_async; +use tracing::{level_filters::LevelFilter, span, Instrument, Level}; + +use freenet_ping_app::ping_client::{ + wait_for_get_response, wait_for_put_response, wait_for_subscribe_response, +}; + +static RNG: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + std::sync::Mutex::new(rand::rngs::StdRng::from_seed( + *b"0102030405060708090a0b0c0d0e0f10", + )) + }); + +struct PresetConfig { + temp_dir: tempfile::TempDir, +} + +async fn base_node_test_config( + is_gateway: bool, + gateways: Vec, + public_port: Option, + ws_api_port: u16, + blocked_addresses: Option>, +) -> anyhow::Result<(ConfigArgs, PresetConfig)> { + if is_gateway { + assert!(public_port.is_some()); + } + + let temp_dir = tempfile::tempdir()?; + let key = TransportKeypair::new_with_rng(&mut *RNG.lock().unwrap()); + let transport_keypair = temp_dir.path().join("private.pem"); + key.save(&transport_keypair)?; + key.public().save(temp_dir.path().join("public.pem"))?; + let config = ConfigArgs { + ws_api: WebsocketApiArgs { + address: Some(Ipv4Addr::LOCALHOST.into()), + ws_api_port: Some(ws_api_port), + }, + network_api: NetworkArgs { + public_address: Some(Ipv4Addr::LOCALHOST.into()), + public_port, + is_gateway, + skip_load_from_network: true, + gateways: Some(gateways), + location: Some(RNG.lock().unwrap().gen()), + ignore_protocol_checking: true, + address: Some(Ipv4Addr::LOCALHOST.into()), + network_port: public_port, + bandwidth_limit: None, + blocked_addresses, + }, + config_paths: { + freenet::config::ConfigPathsArgs { + config_dir: Some(temp_dir.path().to_path_buf()), + data_dir: Some(temp_dir.path().to_path_buf()), + } + }, + secrets: SecretArgs { + transport_keypair: Some(transport_keypair), + ..Default::default() + }, + ..Default::default() + }; + Ok((config, PresetConfig { temp_dir })) +} + +fn gw_config(port: u16, path: &std::path::Path) -> anyhow::Result { + Ok(InlineGwConfig { + address: (Ipv4Addr::LOCALHOST, port).into(), + location: Some(random()), + public_key_path: path.join("public.pem"), + }) +} + +fn ping_states_equal(a: &Ping, b: &Ping) -> bool { + if a.len() != b.len() { + return false; + } + + for key in a.keys() { + if !b.contains_key(key) { + return false; + } + } + + true +} + +const PACKAGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const PATH_TO_CONTRACT: &str = "../contracts/ping/build/freenet/freenet_ping_contract"; +const APP_TAG: &str = "ping-app"; + +#[tokio::test(flavor = "multi_thread")] +async fn test_ping_blocked_peers_optimized() -> TestResult { + freenet::config::set_logger(Some(LevelFilter::DEBUG), None); + + let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let gw_network_port = network_socket_gw.local_addr()?.port(); + let _gw_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), gw_network_port); + + let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + + let network_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let node1_network_port = network_socket_node1.local_addr()?.port(); + let node1_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node1_network_port); + + let network_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + let node2_network_port = network_socket_node2.local_addr()?.port(); + let node2_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node2_network_port); + + let (config_gw, preset_cfg_gw, config_gw_info) = { + let (cfg, preset) = base_node_test_config( + true, + vec![], + Some(gw_network_port), + ws_api_port_socket_gw.local_addr()?.port(), + None, // Gateway doesn't block any peers + ) + .await?; + let public_port = cfg.network_api.public_port.unwrap(); + let path = preset.temp_dir.path().to_path_buf(); + (cfg, preset, gw_config(public_port, &path)?) + }; + let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); + + let (config_node1, preset_cfg_node1) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node1_network_port), + ws_api_port_socket_node1.local_addr()?.port(), + Some(vec![node2_network_addr]), // Node1 blocks Node2 + ) + .await?; + let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); + + let (config_node2, preset_cfg_node2) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node2_network_port), + ws_api_port_socket_node2.local_addr()?.port(), + Some(vec![node1_network_addr]), // Node2 blocks Node1 + ) + .await?; + let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); + + tracing::info!("Gateway node data dir: {:?}", preset_cfg_gw.temp_dir.path()); + tracing::info!("Node 1 data dir: {:?}", preset_cfg_node1.temp_dir.path()); + tracing::info!("Node 2 data dir: {:?}", preset_cfg_node2.temp_dir.path()); + tracing::info!("Node 1 blocks: {:?}", node2_network_addr); + tracing::info!("Node 2 blocks: {:?}", node1_network_addr); + + std::mem::drop(network_socket_gw); + std::mem::drop(network_socket_node1); + std::mem::drop(network_socket_node2); + std::mem::drop(ws_api_port_socket_gw); + std::mem::drop(ws_api_port_socket_node1); + std::mem::drop(ws_api_port_socket_node2); + + let gateway_node = async { + let config = config_gw.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node1 = async move { + let config = config_node1.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node2 = async { + let config = config_node2.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let test = tokio::time::timeout(Duration::from_secs(180), async { + tracing::info!("Waiting for nodes to start up and establish connections..."); + tokio::time::sleep(Duration::from_secs(10)).await; + + let uri_gw = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let uri_node1 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let uri_node2 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + tracing::info!("Connecting to Gateway at {}", uri_gw); + let (stream_gw, _) = connect_async(&uri_gw).await?; + tracing::info!("Connecting to Node1 at {}", uri_node1); + let (stream_node1, _) = connect_async(&uri_node1).await?; + tracing::info!("Connecting to Node2 at {}", uri_node2); + let (stream_node2, _) = connect_async(&uri_node2).await?; + + let mut client_gw = WebApi::start(stream_gw); + let mut client_node1 = WebApi::start(stream_node1); + let mut client_node2 = WebApi::start(stream_node2); + + let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); + tracing::info!(path=%path_to_code.display(), "Loading contract code"); + let code = std::fs::read(path_to_code) + .ok() + .ok_or_else(|| anyhow!("Failed to read contract code"))?; + let code_hash = CodeHash::from_code(&code); + tracing::info!(code_hash=%code_hash, "Loaded contract code"); + + let ping_options = PingContractOptions { + frequency: Duration::from_secs(3), // Reduced from 5s to 3s + ttl: Duration::from_secs(30), // Reduced from 60s to 30s + tag: APP_TAG.to_string(), + code_key: code_hash.to_string(), + }; + let params = Parameters::from(serde_json::to_vec(&ping_options).unwrap()); + let container = ContractContainer::try_from((code, ¶ms))?; + let contract_key = container.key(); + + tracing::info!("Gateway node putting contract..."); + let wrapped_state = { + let ping = Ping::default(); + let serialized = serde_json::to_vec(&ping)?; + WrappedState::new(serialized) + }; + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Put { + contract: container.clone(), + state: wrapped_state.clone(), + related_contracts: RelatedContracts::new(), + subscribe: false, + })) + .await?; + + let key = tokio::time::timeout( + Duration::from_secs(20), + wait_for_put_response(&mut client_gw, &contract_key), + ) + .await + .map_err(|_| anyhow!("Gateway put request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!(key=%key, "Gateway: put ping contract successfully!"); + + tracing::info!("Node 1 getting contract..."); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: false, + })) + .await?; + + let node1_state = tokio::time::timeout( + Duration::from_secs(20), + wait_for_get_response(&mut client_node1, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 1: got contract with {} entries", node1_state.len()); + + tracing::info!("Node 2 getting contract..."); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: false, + })) + .await?; + + let node2_state = tokio::time::timeout( + Duration::from_secs(20), + wait_for_get_response(&mut client_node2, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 2: got contract with {} entries", node2_state.len()); + + tracing::info!("All nodes subscribing to contract..."); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + + tokio::time::timeout( + Duration::from_secs(20), + wait_for_subscribe_response(&mut client_gw, &contract_key), + ) + .await + .map_err(|_| anyhow!("Gateway subscribe request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Gateway: subscribed successfully!"); + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + + tokio::time::timeout( + Duration::from_secs(20), + wait_for_subscribe_response(&mut client_node1, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node1 subscribe request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 1: subscribed successfully!"); + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + + tokio::time::timeout( + Duration::from_secs(20), + wait_for_subscribe_response(&mut client_node2, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node2 subscribe request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 2: subscribed successfully!"); + + let gw_tag = "ping-from-gw".to_string(); + let node1_tag = "ping-from-node1".to_string(); + let node2_tag = "ping-from-node2".to_string(); + + let mut gw_seen_node1 = false; + let mut gw_seen_node2 = false; + let mut node1_seen_gw = false; + let mut node1_seen_node2 = false; + let mut node2_seen_gw = false; + let mut node2_seen_node1 = false; + + async fn get_all_states( + client_gw: &mut WebApi, + client_node1: &mut WebApi, + client_node2: &mut WebApi, + key: ContractKey, + ) -> anyhow::Result<(Ping, Ping, Ping)> { + tracing::info!("Querying all nodes for current state..."); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let state_gw = tokio::time::timeout( + Duration::from_secs(15), + wait_for_get_response(client_gw, &key), + ) + .await + .map_err(|_| anyhow!("Gateway get request timed out"))?; + + let state_node1 = tokio::time::timeout( + Duration::from_secs(15), + wait_for_get_response(client_node1, &key), + ) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))?; + + let state_node2 = tokio::time::timeout( + Duration::from_secs(15), + wait_for_get_response(client_node2, &key), + ) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))?; + + let ping_gw = state_gw.map_err(|e| anyhow!("Failed to get gateway state: {}", e))?; + let ping_node1 = + state_node1.map_err(|e| anyhow!("Failed to get node1 state: {}", e))?; + let ping_node2 = + state_node2.map_err(|e| anyhow!("Failed to get node2 state: {}", e))?; + + Ok((ping_gw, ping_node1, ping_node2)) + } + + tracing::info!("Sending initial updates from each node..."); + + let mut gw_ping = Ping::default(); + gw_ping.insert(gw_tag.clone()); + tracing::info!("Gateway sending update with tag: {}", gw_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&gw_ping).unwrap())), + })) + .await?; + + let mut node1_ping = Ping::default(); + node1_ping.insert(node1_tag.clone()); + tracing::info!("Node 1 sending update with tag: {}", node1_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node1_ping).unwrap())), + })) + .await?; + + let mut node2_ping = Ping::default(); + node2_ping.insert(node2_tag.clone()); + tracing::info!("Node 2 sending update with tag: {}", node2_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node2_ping).unwrap())), + })) + .await?; + + tracing::info!("Waiting for initial updates to propagate..."); + sleep(Duration::from_secs(5)).await; + + for i in 1..=2 { + // Reduced from 3 to 2 rounds + let mut gw_ping_refresh = Ping::default(); + let gw_refresh_tag = format!("{}-refresh-{}", gw_tag, i); + gw_ping_refresh.insert(gw_refresh_tag.clone()); + tracing::info!("Gateway sending refresh update {}: {}", i, gw_refresh_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&gw_ping_refresh).unwrap(), + )), + })) + .await?; + + let mut node1_ping_refresh = Ping::default(); + let node1_refresh_tag = format!("{}-refresh-{}", node1_tag, i); + node1_ping_refresh.insert(node1_refresh_tag.clone()); + tracing::info!("Node1 sending refresh update {}: {}", i, node1_refresh_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node1_ping_refresh).unwrap(), + )), + })) + .await?; + + let mut node2_ping_refresh = Ping::default(); + let node2_refresh_tag = format!("{}-refresh-{}", node2_tag, i); + node2_ping_refresh.insert(node2_refresh_tag.clone()); + tracing::info!("Node2 sending refresh update {}: {}", i, node2_refresh_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node2_ping_refresh).unwrap(), + )), + })) + .await?; + + sleep(Duration::from_secs(3)).await; // Reduced from 5s to 3s + } + + tracing::info!("Waiting for all updates to propagate..."); + sleep(Duration::from_secs(5)).await; // Reduced from 8s to 5s + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + gw_seen_node1 = gw_seen_node1 || state_gw.contains_key(&node1_tag); + gw_seen_node2 = gw_seen_node2 || state_gw.contains_key(&node2_tag); + node1_seen_gw = node1_seen_gw || state_node1.contains_key(&gw_tag); + node1_seen_node2 = node1_seen_node2 || state_node1.contains_key(&node2_tag); + node2_seen_gw = node2_seen_gw || state_node2.contains_key(&gw_tag); + node2_seen_node1 = node2_seen_node1 || state_node2.contains_key(&node1_tag); + + tracing::info!("After initial updates:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + + if !gw_seen_node1 + || !gw_seen_node2 + || !node1_seen_gw + || !node1_seen_node2 + || !node2_seen_gw + || !node2_seen_node1 + { + tracing::info!("Waiting longer for updates to propagate through the gateway..."); + sleep(Duration::from_secs(10)).await; // Reduced from 15s to 10s + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + gw_seen_node1 = gw_seen_node1 || state_gw.contains_key(&node1_tag); + gw_seen_node2 = gw_seen_node2 || state_gw.contains_key(&node2_tag); + node1_seen_gw = node1_seen_gw || state_node1.contains_key(&gw_tag); + node1_seen_node2 = node1_seen_node2 || state_node1.contains_key(&node2_tag); + node2_seen_gw = node2_seen_gw || state_node2.contains_key(&gw_tag); + node2_seen_node1 = node2_seen_node1 || state_node2.contains_key(&node1_tag); + + tracing::info!("After waiting longer:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + } + + if !gw_seen_node1 + || !gw_seen_node2 + || !node1_seen_gw + || !node1_seen_node2 + || !node2_seen_gw + || !node2_seen_node1 + { + tracing::info!("Some updates still missing, sending final round of updates..."); + + let mut gw_ping_final = Ping::default(); + let gw_final_tag = format!("{}-final", gw_tag); + gw_ping_final.insert(gw_final_tag.clone()); + tracing::info!("Gateway sending final update: {}", gw_final_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&gw_ping_final).unwrap(), + )), + })) + .await?; + + let mut node1_ping_final = Ping::default(); + let node1_final_tag = format!("{}-final", node1_tag); + node1_ping_final.insert(node1_final_tag.clone()); + tracing::info!("Node1 sending final update: {}", node1_final_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node1_ping_final).unwrap(), + )), + })) + .await?; + + let mut node2_ping_final = Ping::default(); + let node2_final_tag = format!("{}-final", node2_tag); + node2_ping_final.insert(node2_final_tag.clone()); + tracing::info!("Node2 sending final update: {}", node2_final_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node2_ping_final).unwrap(), + )), + })) + .await?; + + tracing::info!("Waiting for final updates to propagate (15 seconds)..."); + sleep(Duration::from_secs(15)).await; // Reduced from 25s to 15s + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + gw_seen_node1 = gw_seen_node1 + || state_gw.contains_key(&node1_tag) + || state_gw.contains_key(&node1_final_tag); + gw_seen_node2 = gw_seen_node2 + || state_gw.contains_key(&node2_tag) + || state_gw.contains_key(&node2_final_tag); + node1_seen_gw = node1_seen_gw + || state_node1.contains_key(&gw_tag) + || state_node1.contains_key(&gw_final_tag); + node1_seen_node2 = node1_seen_node2 + || state_node1.contains_key(&node2_tag) + || state_node1.contains_key(&node2_final_tag); + node2_seen_gw = node2_seen_gw + || state_node2.contains_key(&gw_tag) + || state_node2.contains_key(&gw_final_tag); + node2_seen_node1 = node2_seen_node1 + || state_node2.contains_key(&node1_tag) + || state_node2.contains_key(&node1_final_tag); + + tracing::info!("After final updates:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + } + + assert!(gw_seen_node1, "Gateway did not see Node1's update"); + assert!(gw_seen_node2, "Gateway did not see Node2's update"); + assert!(node1_seen_gw, "Node1 did not see Gateway's update"); + assert!( + node1_seen_node2, + "Node1 did not see Node2's update through Gateway" + ); + assert!(node2_seen_gw, "Node2 did not see Gateway's update"); + assert!( + node2_seen_node1, + "Node2 did not see Node1's update through Gateway" + ); + + assert!( + ping_states_equal(&state_gw, &state_node1), + "Gateway and Node1 have different state content" + ); + assert!( + ping_states_equal(&state_gw, &state_node2), + "Gateway and Node2 have different state content" + ); + assert!( + ping_states_equal(&state_node1, &state_node2), + "Node1 and Node2 have different state content" + ); + + tracing::info!("All nodes have successfully received updates through the gateway!"); + tracing::info!( + "Test passed: updates propagated correctly despite blocked direct connections" + ); + + Ok::<_, anyhow::Error>(()) + }) + .instrument(span!(Level::INFO, "test_ping_blocked_peers_optimized")); + + select! { + gw = gateway_node => { + let Err(gw) = gw; + Err(anyhow!("Gateway node failed: {}", gw).into()) + } + n1 = node1 => { + let Err(n1) = n1; + Err(anyhow!("Node 1 failed: {}", n1).into()) + } + n2 = node2 => { + let Err(n2) = n2; + Err(anyhow!("Node 2 failed: {}", n2).into()) + } + t = test => { + match t { + Ok(Ok(())) => { + tracing::info!("Test completed successfully!"); + Ok(()) + } + Ok(Err(e)) => { + tracing::error!("Test failed: {}", e); + Err(e.into()) + } + Err(_) => { + tracing::error!("Test timed out!"); + Err(anyhow!("Test timed out").into()) + } + } + } + } +} diff --git a/apps/freenet-ping/app/tests/run_app_blocked_peers_reliable.rs b/apps/freenet-ping/app/tests/run_app_blocked_peers_reliable.rs new file mode 100644 index 000000000..00c9018c0 --- /dev/null +++ b/apps/freenet-ping/app/tests/run_app_blocked_peers_reliable.rs @@ -0,0 +1,583 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, TcpListener}, + path::PathBuf, + time::Duration, +}; + +use anyhow::anyhow; +use freenet::{ + config::{ConfigArgs, InlineGwConfig, NetworkArgs, SecretArgs, WebsocketApiArgs}, + dev_tool::TransportKeypair, + local_node::NodeConfig, + server::serve_gateway, +}; +use freenet_ping_types::{Ping, PingContractOptions}; +use freenet_stdlib::{ + client_api::{ClientRequest, ContractRequest, WebApi}, + prelude::*, +}; +use futures::FutureExt; +use rand::{random, Rng, SeedableRng}; +use testresult::TestResult; +use tokio::{select, time::sleep}; +use tokio_tungstenite::connect_async; +use tracing::{span, Instrument, Level}; + +use freenet_ping_app::ping_client::{ + wait_for_get_response, wait_for_put_response, wait_for_subscribe_response, +}; + +static RNG: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + std::sync::Mutex::new(rand::rngs::StdRng::from_seed( + *b"0102030405060708090a0b0c0d0e0f10", + )) + }); + +struct PresetConfig { + temp_dir: tempfile::TempDir, +} + +async fn base_node_test_config( + is_gateway: bool, + gateways: Vec, + public_port: Option, + ws_api_port: u16, + blocked_addresses: Option>, +) -> anyhow::Result<(ConfigArgs, PresetConfig)> { + if is_gateway { + assert!(public_port.is_some()); + } + + let temp_dir = tempfile::tempdir()?; + let key = TransportKeypair::new_with_rng(&mut *RNG.lock().unwrap()); + let transport_keypair = temp_dir.path().join("private.pem"); + key.save(&transport_keypair)?; + key.public().save(temp_dir.path().join("public.pem"))?; + let config = ConfigArgs { + ws_api: WebsocketApiArgs { + address: Some(Ipv4Addr::LOCALHOST.into()), + ws_api_port: Some(ws_api_port), + }, + network_api: NetworkArgs { + public_address: Some(Ipv4Addr::LOCALHOST.into()), + public_port, + is_gateway, + skip_load_from_network: true, + gateways: Some(gateways), + location: Some(RNG.lock().unwrap().gen()), + ignore_protocol_checking: true, + address: Some(Ipv4Addr::LOCALHOST.into()), + network_port: public_port, + bandwidth_limit: None, + blocked_addresses, + }, + config_paths: { + freenet::config::ConfigPathsArgs { + config_dir: Some(temp_dir.path().to_path_buf()), + data_dir: Some(temp_dir.path().to_path_buf()), + } + }, + secrets: SecretArgs { + transport_keypair: Some(transport_keypair), + ..Default::default() + }, + ..Default::default() + }; + Ok((config, PresetConfig { temp_dir })) +} + +fn gw_config(port: u16, path: &std::path::Path) -> anyhow::Result { + Ok(InlineGwConfig { + address: (Ipv4Addr::LOCALHOST, port).into(), + location: Some(random()), + public_key_path: path.join("public.pem"), + }) +} + +fn ping_states_equal(a: &Ping, b: &Ping) -> bool { + if a.len() != b.len() { + return false; + } + + for key in a.keys() { + if !b.contains_key(key) { + return false; + } + } + + true +} + +const PACKAGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const PATH_TO_CONTRACT: &str = "../contracts/ping/build/freenet/freenet_ping_contract"; +const APP_TAG: &str = "ping-app"; + +#[tokio::test(flavor = "multi_thread")] +async fn test_ping_blocked_peers_reliable() -> TestResult { + let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let gw_network_port = network_socket_gw.local_addr()?.port(); + let _gw_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), gw_network_port); + + let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + + let network_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let node1_network_port = network_socket_node1.local_addr()?.port(); + let node1_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node1_network_port); + + let network_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + let node2_network_port = network_socket_node2.local_addr()?.port(); + let node2_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node2_network_port); + + let (config_gw, preset_cfg_gw, config_gw_info) = { + let (cfg, preset) = base_node_test_config( + true, + vec![], + Some(gw_network_port), + ws_api_port_socket_gw.local_addr()?.port(), + None, // Gateway doesn't block any peers + ) + .await?; + let public_port = cfg.network_api.public_port.unwrap(); + let path = preset.temp_dir.path().to_path_buf(); + (cfg, preset, gw_config(public_port, &path)?) + }; + let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); + + let (config_node1, preset_cfg_node1) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node1_network_port), + ws_api_port_socket_node1.local_addr()?.port(), + Some(vec![node2_network_addr]), // Node1 blocks Node2 + ) + .await?; + let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); + + let (config_node2, preset_cfg_node2) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node2_network_port), + ws_api_port_socket_node2.local_addr()?.port(), + Some(vec![node1_network_addr]), // Node2 blocks Node1 + ) + .await?; + let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); + + tracing::info!("Gateway node data dir: {:?}", preset_cfg_gw.temp_dir.path()); + tracing::info!("Node 1 data dir: {:?}", preset_cfg_node1.temp_dir.path()); + tracing::info!("Node 2 data dir: {:?}", preset_cfg_node2.temp_dir.path()); + tracing::info!("Node 1 blocks: {:?}", node2_network_addr); + tracing::info!("Node 2 blocks: {:?}", node1_network_addr); + + std::mem::drop(network_socket_gw); + std::mem::drop(network_socket_node1); + std::mem::drop(network_socket_node2); + std::mem::drop(ws_api_port_socket_gw); + std::mem::drop(ws_api_port_socket_node1); + std::mem::drop(ws_api_port_socket_node2); + + let gateway_node = async { + let config = config_gw.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node1 = async move { + let config = config_node1.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node2 = async { + let config = config_node2.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let test = tokio::time::timeout(Duration::from_secs(120), async { + tracing::info!("Waiting for nodes to start up and establish connections..."); + tokio::time::sleep(Duration::from_secs(10)).await; + + let uri_gw = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let uri_node1 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let uri_node2 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + tracing::info!("Connecting to Gateway at {}", uri_gw); + let (stream_gw, _) = connect_async(&uri_gw).await?; + tracing::info!("Connecting to Node1 at {}", uri_node1); + let (stream_node1, _) = connect_async(&uri_node1).await?; + tracing::info!("Connecting to Node2 at {}", uri_node2); + let (stream_node2, _) = connect_async(&uri_node2).await?; + + let mut client_gw = WebApi::start(stream_gw); + let mut client_node1 = WebApi::start(stream_node1); + let mut client_node2 = WebApi::start(stream_node2); + + let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); + tracing::info!(path=%path_to_code.display(), "Loading contract code"); + let code = std::fs::read(path_to_code) + .ok() + .ok_or_else(|| anyhow!("Failed to read contract code"))?; + let code_hash = CodeHash::from_code(&code); + tracing::info!(code_hash=%code_hash, "Loaded contract code"); + + let ping_options = PingContractOptions { + frequency: Duration::from_secs(1), // Reduced from 3s to 1s + ttl: Duration::from_secs(10), // Reduced from 30s to 10s + tag: APP_TAG.to_string(), + code_key: code_hash.to_string(), + }; + let params = Parameters::from(serde_json::to_vec(&ping_options).unwrap()); + let container = ContractContainer::try_from((code, ¶ms))?; + let contract_key = container.key(); + + tracing::info!("Gateway node putting contract..."); + let wrapped_state = { + let ping = Ping::default(); + let serialized = serde_json::to_vec(&ping)?; + WrappedState::new(serialized) + }; + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Put { + contract: container.clone(), + state: wrapped_state.clone(), + related_contracts: RelatedContracts::new(), + subscribe: true, // Subscribe immediately on put + })) + .await?; + + let key = tokio::time::timeout( + Duration::from_secs(15), + wait_for_put_response(&mut client_gw, &contract_key), + ) + .await + .map_err(|_| anyhow!("Gateway put request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!(key=%key, "Gateway: put ping contract successfully!"); + + tracing::info!("Node 1 getting contract and subscribing..."); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: true, // Subscribe immediately on get + })) + .await?; + + let node1_state = tokio::time::timeout( + Duration::from_secs(15), + wait_for_get_response(&mut client_node1, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 1: got contract with {} entries", node1_state.len()); + + tracing::info!("Node 2 getting contract and subscribing..."); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key: contract_key, + return_contract_code: true, + subscribe: true, // Subscribe immediately on get + })) + .await?; + + let node2_state = tokio::time::timeout( + Duration::from_secs(15), + wait_for_get_response(&mut client_node2, &contract_key), + ) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))? + .map_err(anyhow::Error::msg)?; + + tracing::info!("Node 2: got contract with {} entries", node2_state.len()); + + tracing::info!("Waiting for subscriptions to be fully established..."); + sleep(Duration::from_secs(5)).await; + + async fn get_all_states( + client_gw: &mut WebApi, + client_node1: &mut WebApi, + client_node2: &mut WebApi, + key: ContractKey, + ) -> anyhow::Result<(Ping, Ping, Ping)> { + tracing::info!("Querying all nodes for current state..."); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let state_gw = tokio::time::timeout( + Duration::from_secs(10), + wait_for_get_response(client_gw, &key), + ) + .await + .map_err(|_| anyhow!("Gateway get request timed out"))?; + + let state_node1 = tokio::time::timeout( + Duration::from_secs(10), + wait_for_get_response(client_node1, &key), + ) + .await + .map_err(|_| anyhow!("Node1 get request timed out"))?; + + let state_node2 = tokio::time::timeout( + Duration::from_secs(10), + wait_for_get_response(client_node2, &key), + ) + .await + .map_err(|_| anyhow!("Node2 get request timed out"))?; + + let ping_gw = state_gw.map_err(|e| anyhow!("Failed to get gateway state: {}", e))?; + let ping_node1 = + state_node1.map_err(|e| anyhow!("Failed to get node1 state: {}", e))?; + let ping_node2 = + state_node2.map_err(|e| anyhow!("Failed to get node2 state: {}", e))?; + + Ok((ping_gw, ping_node1, ping_node2)) + } + + let gw_tag = "ping-from-gw".to_string(); + let node1_tag = "ping-from-node1".to_string(); + let node2_tag = "ping-from-node2".to_string(); + + for round in 1..=3 { + tracing::info!("Starting update round {}/3...", round); + + let mut gw_ping = Ping::default(); + let gw_round_tag = format!("{}-round{}", gw_tag, round); + gw_ping.insert(gw_round_tag.clone()); + tracing::info!("Gateway sending update: {}", gw_round_tag); + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&gw_ping).unwrap(), + )), + })) + .await?; + + sleep(Duration::from_secs(2)).await; + + let mut node1_ping = Ping::default(); + let node1_round_tag = format!("{}-round{}", node1_tag, round); + node1_ping.insert(node1_round_tag.clone()); + tracing::info!("Node 1 sending update: {}", node1_round_tag); + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node1_ping).unwrap(), + )), + })) + .await?; + + sleep(Duration::from_secs(2)).await; + + let mut node2_ping = Ping::default(); + let node2_round_tag = format!("{}-round{}", node2_tag, round); + node2_ping.insert(node2_round_tag.clone()); + tracing::info!("Node 2 sending update: {}", node2_round_tag); + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Update { + key: contract_key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&node2_ping).unwrap(), + )), + })) + .await?; + + let wait_time = 3 + (round * 2); + tracing::info!( + "Waiting {}s for round {} updates to propagate...", + wait_time, + round + ); + sleep(Duration::from_secs(wait_time)).await; + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + tracing::info!("Round {} update propagation status:", round); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + + let gw_seen_node1 = state_gw.contains_key(&node1_round_tag); + let gw_seen_node2 = state_gw.contains_key(&node2_round_tag); + let node1_seen_gw = state_node1.contains_key(&gw_round_tag); + let node1_seen_node2 = state_node1.contains_key(&node2_round_tag); + let node2_seen_gw = state_node2.contains_key(&gw_round_tag); + let node2_seen_node1 = state_node2.contains_key(&node1_round_tag); + + tracing::info!("Gateway seen Node1 round {}: {}", round, gw_seen_node1); + tracing::info!("Gateway seen Node2 round {}: {}", round, gw_seen_node2); + tracing::info!("Node1 seen Gateway round {}: {}", round, node1_seen_gw); + tracing::info!("Node1 seen Node2 round {}: {}", round, node1_seen_node2); + tracing::info!("Node2 seen Gateway round {}: {}", round, node2_seen_gw); + tracing::info!("Node2 seen Node1 round {}: {}", round, node2_seen_node1); + + if gw_seen_node1 + && gw_seen_node2 + && node1_seen_gw + && node1_seen_node2 + && node2_seen_gw + && node2_seen_node1 + { + tracing::info!("All round {} updates have propagated successfully!", round); + if round == 3 { + tracing::info!("All rounds completed successfully!"); + break; + } + } else { + tracing::info!( + "Some round {} updates did not propagate, continuing to next round...", + round + ); + } + } + + let (state_gw, state_node1, state_node2) = get_all_states( + &mut client_gw, + &mut client_node1, + &mut client_node2, + contract_key, + ) + .await?; + + let gw_seen_node1 = state_gw.keys().any(|k| k.starts_with(&node1_tag)); + let gw_seen_node2 = state_gw.keys().any(|k| k.starts_with(&node2_tag)); + let node1_seen_gw = state_node1.keys().any(|k| k.starts_with(&gw_tag)); + let node1_seen_node2 = state_node1.keys().any(|k| k.starts_with(&node2_tag)); + let node2_seen_gw = state_node2.keys().any(|k| k.starts_with(&gw_tag)); + let node2_seen_node1 = state_node2.keys().any(|k| k.starts_with(&node1_tag)); + + tracing::info!("Final update propagation status:"); + tracing::info!("Gateway state: {:?}", state_gw); + tracing::info!("Node 1 state: {:?}", state_node1); + tracing::info!("Node 2 state: {:?}", state_node2); + tracing::info!("Gateway seen Node1: {}", gw_seen_node1); + tracing::info!("Gateway seen Node2: {}", gw_seen_node2); + tracing::info!("Node1 seen Gateway: {}", node1_seen_gw); + tracing::info!("Node1 seen Node2: {}", node1_seen_node2); + tracing::info!("Node2 seen Gateway: {}", node2_seen_gw); + tracing::info!("Node2 seen Node1: {}", node2_seen_node1); + + assert!(gw_seen_node1, "Gateway did not see Node1's update"); + assert!(gw_seen_node2, "Gateway did not see Node2's update"); + assert!(node1_seen_gw, "Node1 did not see Gateway's update"); + assert!( + node1_seen_node2, + "Node1 did not see Node2's update through Gateway" + ); + assert!(node2_seen_gw, "Node2 did not see Gateway's update"); + assert!( + node2_seen_node1, + "Node2 did not see Node1's update through Gateway" + ); + + assert!( + ping_states_equal(&state_gw, &state_node1), + "Gateway and Node1 have different state content" + ); + assert!( + ping_states_equal(&state_gw, &state_node2), + "Gateway and Node2 have different state content" + ); + assert!( + ping_states_equal(&state_node1, &state_node2), + "Node1 and Node2 have different state content" + ); + + tracing::info!("All nodes have successfully received updates through the gateway!"); + tracing::info!( + "Test passed: updates propagated correctly despite blocked direct connections" + ); + + Ok::<_, anyhow::Error>(()) + }) + .instrument(span!(Level::INFO, "test_ping_blocked_peers_reliable")); + + select! { + gw = gateway_node => { + let Err(gw) = gw; + Err(anyhow!("Gateway node failed: {}", gw).into()) + } + n1 = node1 => { + let Err(n1) = n1; + Err(anyhow!("Node 1 failed: {}", n1).into()) + } + n2 = node2 => { + let Err(n2) = n2; + Err(anyhow!("Node 2 failed: {}", n2).into()) + } + t = test => { + match t { + Ok(Ok(())) => { + tracing::info!("Test completed successfully!"); + Ok(()) + } + Ok(Err(e)) => { + tracing::error!("Test failed: {}", e); + Err(e.into()) + } + Err(_) => { + tracing::error!("Test timed out!"); + Err(anyhow!("Test timed out").into()) + } + } + } + } +} diff --git a/apps/freenet-ping/app/tests/run_app_blocked_peers_simple.rs b/apps/freenet-ping/app/tests/run_app_blocked_peers_simple.rs new file mode 100644 index 000000000..8522cdf62 --- /dev/null +++ b/apps/freenet-ping/app/tests/run_app_blocked_peers_simple.rs @@ -0,0 +1,390 @@ +use std::{collections::HashSet, path::PathBuf, time::Duration}; + +use anyhow::Context; +use freenet_stdlib::{ + client_api::{ClientRequest, ContractRequest, ContractResponse, HostResponse, WebApi}, + prelude::*, +}; +use futures::{future::BoxFuture, FutureExt}; +use rand::{Rng, SeedableRng}; +type TestResult = Result<(), Box>; +use tokio::{select, time::sleep}; +use tracing::{info, span, Instrument, Level}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +use freenet_ping_contract::{Ping, PingOptions}; + +#[derive(Debug, Clone)] +struct PresetConfig {} + +fn base_node_test_config( + data_dir: PathBuf, + ws_api_port: u16, + network_port: u16, + blocked_peers: Vec, +) -> NodeConfig { + let mut rng = rand::rngs::StdRng::from_entropy(); + let seed: [u8; 32] = rng.gen(); + + let mut config = NodeConfig::new(data_dir, seed); + + config.network.ws_api_port = ws_api_port; + config.network.port = network_port; + config.network.blocked_peers = blocked_peers; + + config.network.connect_timeout = Duration::from_secs(5); + config.network.operation_timeout = Duration::from_secs(10); + + config +} + +fn gw_config(data_dir: PathBuf, ws_api_port: u16, network_port: u16) -> NodeConfig { + base_node_test_config(data_dir, ws_api_port, network_port, vec![]) +} + +fn ping_states_equal(a: &Ping, b: &Ping) -> bool { + let a_set: HashSet<_> = a.get_all().into_iter().collect(); + let b_set: HashSet<_> = b.get_all().into_iter().collect(); + a_set == b_set +} + +#[tokio::test] +async fn test_ping_blocked_peers_simple() -> TestResult { + let subscriber = tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()); + let _guard = subscriber.set_default(); + + let temp_dir = tempfile::tempdir()?; + let gw_dir = temp_dir.path().join("gateway"); + let node1_dir = temp_dir.path().join("node1"); + let node2_dir = temp_dir.path().join("node2"); + + std::fs::create_dir_all(&gw_dir)?; + std::fs::create_dir_all(&node1_dir)?; + std::fs::create_dir_all(&node2_dir)?; + + let gw_ws_port = 50510; + let gw_network_port = 50511; + let node1_ws_port = 50512; + let node1_network_port = 50513; + let node2_ws_port = 50514; + let node2_network_port = 50515; + + let gateway_node = { + let config = gw_config(gw_dir, gw_ws_port, gw_network_port); + let node = Node::new(config); + info!("Starting gateway node"); + node.run().await + } + .boxed_local(); + + let gateway_peer_id = { + let uri = format!("ws://127.0.0.1:{}", gw_ws_port); + let mut client = WebApi::connect(&uri).await?; + let response = client.request(ClientRequest::Host).await?; + + if let HostResponse::Info(info) = response { + info.peer_id + } else { + anyhow::bail!("Failed to get gateway peer ID"); + } + }; + + info!("Gateway peer ID: {}", gateway_peer_id); + + let node1 = { + let node2_peer_id = { + let config = + base_node_test_config(node2_dir.clone(), node2_ws_port, node2_network_port, vec![]); + config.identity.peer_id() + }; + + info!("Node 2 peer ID: {}", node2_peer_id); + + let config = base_node_test_config( + node1_dir, + node1_ws_port, + node1_network_port, + vec![node2_peer_id], + ); + + let node = Node::new(config); + info!("Starting node 1"); + node.run().await + } + .boxed_local(); + + let node2 = { + let node1_peer_id = { + let config = + base_node_test_config(node1_dir.clone(), node1_ws_port, node1_network_port, vec![]); + config.identity.peer_id() + }; + + info!("Node 1 peer ID: {}", node1_peer_id); + + let config = base_node_test_config( + node2_dir, + node2_ws_port, + node2_network_port, + vec![node1_peer_id], + ); + + let node = Node::new(config); + info!("Starting node 2"); + node.run().await + } + .boxed_local(); + + let test = tokio::time::timeout(Duration::from_secs(180), async { + info!("Waiting for nodes to start up and establish connections..."); + tokio::time::sleep(Duration::from_secs(10)).await; + + let uri_gw = format!("ws://127.0.0.1:{}", gw_ws_port); + let uri_node1 = format!("ws://127.0.0.1:{}", node1_ws_port); + let uri_node2 = format!("ws://127.0.0.1:{}", node2_ws_port); + + let mut client_gw = WebApi::connect(&uri_gw).await?; + let mut client_node1 = WebApi::connect(&uri_node1).await?; + let mut client_node2 = WebApi::connect(&uri_node2).await?; + + info!("Connected to all nodes"); + + info!("Deploying ping contract on gateway..."); + let code = include_bytes!("../../contracts/ping/build/freenet/freenet_ping_contract"); + + let ping_options = PingOptions { + update_frequency_ms: 1000, + ttl_ms: 5000, + }; + + let mut wrapped_state = Ping::default(); + let gw_tag = "gateway-initial"; + wrapped_state.insert(gw_tag.to_string()); + + let response = client_gw + .request(ClientRequest::Contract(ContractRequest::Deploy { + code: code.to_vec(), + state: serde_json::to_vec(&wrapped_state)?, + options: serde_json::to_vec(&ping_options)?, + })) + .await?; + + let key = if let ContractResponse::Deployed { key } = response { + key + } else { + anyhow::bail!("Failed to deploy contract"); + }; + + info!("Contract deployed with key: {}", key); + + info!("Node 1 subscribing to contract..."); + let response = client_node1 + .request(ClientRequest::Contract(ContractRequest::Subscribe { key })) + .await?; + + let node1_state = if let ContractResponse::Subscribed { state, .. } = response { + let ping: Ping = serde_json::from_slice(&state)?; + ping + } else { + anyhow::bail!("Failed to subscribe node 1 to contract"); + }; + + info!("Node 1 subscribed to contract"); + + info!("Node 2 subscribing to contract..."); + let response = client_node2 + .request(ClientRequest::Contract(ContractRequest::Subscribe { key })) + .await?; + + let node2_state = if let ContractResponse::Subscribed { state, .. } = response { + let ping: Ping = serde_json::from_slice(&state)?; + ping + } else { + anyhow::bail!("Failed to subscribe node 2 to contract"); + }; + + info!("Node 2 subscribed to contract"); + + info!("Verifying initial state..."); + assert!( + ping_states_equal(&wrapped_state, &node1_state), + "Node 1 initial state doesn't match gateway state" + ); + assert!( + ping_states_equal(&wrapped_state, &node2_state), + "Node 2 initial state doesn't match gateway state" + ); + + let get_all_states = |client_gw: &mut WebApi, + client_node1: &mut WebApi, + client_node2: &mut WebApi, + key: ContractKey| + -> BoxFuture<'_, anyhow::Result<(Ping, Ping, Ping)>> { + Box::pin(async move { + info!("Querying all nodes for current state..."); + + let response = client_gw + .request(ClientRequest::Contract(ContractRequest::Get { key })) + .await + .context("Failed to get gateway state")?; + + let state_gw = if let ContractResponse::State { state, .. } = response { + serde_json::from_slice(&state).context("Failed to deserialize gateway state")? + } else { + anyhow::bail!("Unexpected response from gateway"); + }; + + let response = client_node1 + .request(ClientRequest::Contract(ContractRequest::Get { key })) + .await + .context("Failed to get node 1 state")?; + + let state_node1 = if let ContractResponse::State { state, .. } = response { + serde_json::from_slice(&state).context("Failed to deserialize node 1 state")? + } else { + anyhow::bail!("Unexpected response from node 1"); + }; + + let response = client_node2 + .request(ClientRequest::Contract(ContractRequest::Get { key })) + .await + .context("Failed to get node 2 state")?; + + let state_node2 = if let ContractResponse::State { state, .. } = response { + serde_json::from_slice(&state).context("Failed to deserialize node 2 state")? + } else { + anyhow::bail!("Unexpected response from node 2"); + }; + + Ok((state_gw, state_node1, state_node2)) + }) + }; + + info!("Testing update propagation through gateway..."); + + info!("Sending update from node 1..."); + let mut node1_ping = Ping::default(); + let node1_tag = "node1-update"; + node1_ping.insert(node1_tag.to_string()); + + client_node1 + .request(ClientRequest::Contract(ContractRequest::Update { + key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node1_ping)?)), + })) + .await?; + + info!("Sending update from node 2..."); + let mut node2_ping = Ping::default(); + let node2_tag = "node2-update"; + node2_ping.insert(node2_tag.to_string()); + + client_node2 + .request(ClientRequest::Contract(ContractRequest::Update { + key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&node2_ping)?)), + })) + .await?; + + info!("Waiting for updates to propagate..."); + sleep(Duration::from_secs(15)).await; + + let (state_gw, state_node1, state_node2) = + get_all_states(&mut client_gw, &mut client_node1, &mut client_node2, key).await?; + + info!("Gateway state: {:?}", state_gw.get_all()); + info!("Node 1 state: {:?}", state_node1.get_all()); + info!("Node 2 state: {:?}", state_node2.get_all()); + + let gw_seen_node1 = state_gw.get_all().contains(&node1_tag.to_string()); + let gw_seen_node2 = state_gw.get_all().contains(&node2_tag.to_string()); + let node1_seen_gw = state_node1.get_all().contains(&gw_tag.to_string()); + let node1_seen_node2 = state_node1.get_all().contains(&node2_tag.to_string()); + let node2_seen_gw = state_node2.get_all().contains(&gw_tag.to_string()); + let node2_seen_node1 = state_node2.get_all().contains(&node1_tag.to_string()); + + info!("Gateway seen Node1: {}", gw_seen_node1); + info!("Gateway seen Node2: {}", gw_seen_node2); + info!("Node1 seen Gateway: {}", node1_seen_gw); + info!("Node1 seen Node2: {}", node1_seen_node2); + info!("Node2 seen Gateway: {}", node2_seen_gw); + info!("Node2 seen Node1: {}", node2_seen_node1); + + if !gw_seen_node1 || !gw_seen_node2 || !node1_seen_node2 || !node2_seen_node1 { + info!("Some updates didn't propagate, sending final updates..."); + + let mut gw_ping_final = Ping::default(); + let gw_final_tag = "gateway-final"; + gw_ping_final.insert(gw_final_tag.to_string()); + + client_gw + .request(ClientRequest::Contract(ContractRequest::Update { + key, + data: UpdateData::Delta(StateDelta::from(serde_json::to_vec(&gw_ping_final)?)), + })) + .await?; + + info!("Waiting for final updates to propagate (20 seconds)..."); + sleep(Duration::from_secs(20)).await; + + let (state_gw, state_node1, state_node2) = + get_all_states(&mut client_gw, &mut client_node1, &mut client_node2, key).await?; + + info!("Final Gateway state: {:?}", state_gw.get_all()); + info!("Final Node 1 state: {:?}", state_node1.get_all()); + info!("Final Node 2 state: {:?}", state_node2.get_all()); + + assert!( + ping_states_equal(&state_gw, &state_node1), + "Gateway and Node 1 states don't match after final update" + ); + assert!( + ping_states_equal(&state_gw, &state_node2), + "Gateway and Node 2 states don't match after final update" + ); + } else { + assert!( + ping_states_equal(&state_gw, &state_node1), + "Gateway and Node 1 states don't match" + ); + assert!( + ping_states_equal(&state_gw, &state_node2), + "Gateway and Node 2 states don't match" + ); + } + + info!("Test completed successfully!"); + Ok::<_, anyhow::Error>(()) + }) + .instrument(span!(Level::INFO, "test_ping_blocked_peers_simple")); + + select! { + result = test => { + match result { + Ok(Ok(_)) => Ok(()), + Ok(Err(e)) => { + tracing::error!("Test failed: {}", e); + Err(e.into()) + } + Err(_) => { + tracing::error!("Test timed out!"); + Err(anyhow::anyhow!("Test timed out").into()) + } + } + } + _ = gateway_node => { + tracing::error!("Gateway node failed"); + Err(anyhow::anyhow!("Gateway node failed").into()) + } + _ = node1 => { + tracing::error!("Node 1 failed"); + Err(anyhow::anyhow!("Node 1 failed").into()) + } + _ = node2 => { + tracing::error!("Node 2 failed"); + Err(anyhow::anyhow!("Node 2 failed").into()) + } + } +} diff --git a/apps/freenet-ping/app/tests/run_app_blocked_peers_solution.rs b/apps/freenet-ping/app/tests/run_app_blocked_peers_solution.rs new file mode 100644 index 000000000..846b46aa1 --- /dev/null +++ b/apps/freenet-ping/app/tests/run_app_blocked_peers_solution.rs @@ -0,0 +1,830 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, TcpListener}, + path::PathBuf, + sync::Arc, + time::Duration, +}; + +use anyhow::anyhow; +use freenet::{ + config::{ConfigArgs, InlineGwConfig, NetworkArgs, SecretArgs, WebsocketApiArgs}, + dev_tool::TransportKeypair, + local_node::NodeConfig, + server::serve_gateway, +}; +use freenet_ping_types::{Ping, PingContractOptions}; +use freenet_stdlib::{ + client_api::{ClientRequest, ContractRequest, ContractResponse, HostResponse, WebApi}, + prelude::*, +}; +use futures::future::BoxFuture; +use rand::{random, Rng, SeedableRng}; +use testresult::TestResult; +use tokio::{ + select, + sync::{mpsc, Mutex}, + task::LocalSet, + time::sleep, +}; +use tokio_tungstenite::connect_async; +use tracing::{span, Instrument, Level}; +use tracing_subscriber::EnvFilter; + +use freenet_ping_app::ping_client::{ + wait_for_get_response, wait_for_put_response, wait_for_subscribe_response, +}; + +const MAX_UPDATE_RETRIES: u32 = 8; +const BASE_DELAY_MS: u64 = 3000; +const MAX_DELAY_MS: u64 = 15000; +const MAX_TEST_DURATION_SECS: u64 = 300; + +#[derive(Debug)] +struct PresetConfig { + temp_dir: tempfile::TempDir, +} + +async fn base_node_test_config( + is_gateway: bool, + gateways: Vec, + public_port: Option, + ws_api_port: u16, + blocked_addresses: Option>, +) -> anyhow::Result<(ConfigArgs, PresetConfig)> { + if is_gateway { + assert!(public_port.is_some()); + } + + let temp_dir = tempfile::tempdir()?; + let key = TransportKeypair::new_with_rng(&mut *RNG.lock().unwrap()); + let transport_keypair = temp_dir.path().join("private.pem"); + key.save(&transport_keypair)?; + key.public().save(temp_dir.path().join("public.pem"))?; + let config = ConfigArgs { + ws_api: WebsocketApiArgs { + address: Some(Ipv4Addr::LOCALHOST.into()), + ws_api_port: Some(ws_api_port), + }, + network_api: NetworkArgs { + public_address: Some(Ipv4Addr::LOCALHOST.into()), + public_port, + is_gateway, + skip_load_from_network: true, + gateways: Some(gateways), + location: Some(RNG.lock().unwrap().gen()), + ignore_protocol_checking: true, + address: Some(Ipv4Addr::LOCALHOST.into()), + network_port: public_port, + bandwidth_limit: None, + blocked_addresses, + }, + config_paths: { + freenet::config::ConfigPathsArgs { + config_dir: Some(temp_dir.path().to_path_buf()), + data_dir: Some(temp_dir.path().to_path_buf()), + } + }, + secrets: SecretArgs { + transport_keypair: Some(transport_keypair), + ..Default::default() + }, + ..Default::default() + }; + Ok((config, PresetConfig { temp_dir })) +} + +fn gw_config(port: u16, path: &std::path::Path) -> anyhow::Result { + Ok(InlineGwConfig { + address: (Ipv4Addr::LOCALHOST, port).into(), + location: Some(random()), + public_key_path: path.join("public.pem"), + }) +} + +static RNG: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + std::sync::Mutex::new(rand::rngs::StdRng::from_seed( + *b"0102030405060708090a0b0c0d0e0f10", + )) + }); + +const PACKAGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const PATH_TO_CONTRACT: &str = "../contracts/ping/build/freenet/freenet_ping_contract"; +const APP_TAG: &str = "ping-app"; + +struct LogEntry { + timestamp: std::time::SystemTime, + source: String, + message: String, +} + +#[tokio::test] +async fn test_ping_blocked_peers_solution() -> TestResult { + let local = LocalSet::new(); + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { + EnvFilter::new("debug,freenet::operations::subscribe=trace,freenet::contract=trace,freenet::operations::update=trace") + }); + let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init(); + + let (log_tx, mut log_rx) = mpsc::channel::(1000); + + let log_task = tokio::spawn(async move { + let mut logs = Vec::new(); + while let Some(log) = log_rx.recv().await { + logs.push(log); + logs.sort_by_key(|log| log.timestamp); + + if logs.len() % 10 == 0 { + println!("--- CHRONOLOGICAL LOGS (LAST 10) ---"); + for log in logs.iter().rev().take(10).rev() { + println!( + "[{}] {}: {}", + humantime::format_rfc3339(log.timestamp), + log.source, + log.message + ); + } + println!("----------------------------------"); + } + } + + println!("\n\n=== COMPLETE CHRONOLOGICAL LOG TRACE ==="); + println!("Total log entries: {}", logs.len()); + + let update_logs: Vec<_> = logs + .iter() + .filter(|log| { + log.message.contains("Update") + || log.message.contains("update") + || log.message.contains("state") + || log.message.contains("propagation") + || log.message.contains("missing") + }) + .collect(); + + println!("Update-related log entries: {}", update_logs.len()); + + for log in update_logs { + println!( + "[{}] {}: {}", + humantime::format_rfc3339(log.timestamp), + log.source, + log.message + ); + } + println!("=== END OF LOG TRACE ===\n"); + }); + + let create_logger = |source: String, log_tx: mpsc::Sender| { + Arc::new(move |message: String| { + let log_entry = LogEntry { + timestamp: std::time::SystemTime::now(), + source: source.clone(), + message, + }; + let _ = log_tx.try_send(log_entry); + }) as Arc + }; + + let test = async { + let log_tx_clone = log_tx.clone(); + let log_gateway = create_logger("Gateway".to_string(), log_tx_clone.clone()); + let log_node1 = create_logger("Node1".to_string(), log_tx_clone.clone()); + let log_node2 = create_logger("Node2".to_string(), log_tx_clone); + + let mut rng = rand::rngs::StdRng::seed_from_u64(42); + let ws_port_gw = rng.gen_range(40000..50000); + let ws_port_node1 = rng.gen_range(50001..60000); + let ws_port_node2 = rng.gen_range(60001..70000); + let network_port_gw = rng.gen_range(30000..40000); + let network_port_node1 = rng.gen_range(20000..30000); + let network_port_node2 = rng.gen_range(10000..20000); + + let temp_dir = tempfile::tempdir()?; + let gw_dir = tempfile::tempdir_in(temp_dir.path())?; + let node1_dir = tempfile::tempdir_in(temp_dir.path())?; + let node2_dir = tempfile::tempdir_in(temp_dir.path())?; + + let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let gw_network_port = network_socket_gw.local_addr()?.port(); + let gw_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), gw_network_port); + + let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + + let network_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let node1_network_port = network_socket_node1.local_addr()?.port(); + let node1_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node1_network_port); + + let network_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + let node2_network_port = network_socket_node2.local_addr()?.port(); + let node2_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node2_network_port); + + let node1_peer_id = format!("node1-{}", node1_network_port); + let node2_peer_id = format!("node2-{}", node2_network_port); + + log_gateway(format!("Gateway node port: {}", gw_network_port)); + log_node1(format!("Node 1 port: {}", node1_network_port)); + log_node2(format!("Node 2 port: {}", node2_network_port)); + log_node1(format!("Node 1 blocks: {:?}", node2_network_addr)); + log_node2(format!("Node 2 blocks: {:?}", node1_network_addr)); + + let (config_gw, preset_cfg_gw, config_gw_info) = { + let (cfg, preset) = base_node_test_config( + true, + vec![], + Some(gw_network_port), + ws_api_port_socket_gw.local_addr()?.port(), + None, // Gateway doesn't block any peers + ) + .await?; + let public_port = cfg.network_api.public_port.unwrap(); + let path = preset.temp_dir.path().to_path_buf(); + (cfg, preset, gw_config(public_port, &path)?) + }; + let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); + + let (config_node1, preset_cfg_node1) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node1_network_port), + ws_api_port_socket_node1.local_addr()?.port(), + Some(vec![node2_network_addr]), // Node1 blocks Node2 + ) + .await?; + let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); + + let (config_node2, preset_cfg_node2) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node2_network_port), + ws_api_port_socket_node2.local_addr()?.port(), + Some(vec![node1_network_addr]), // Node2 blocks Node1 + ) + .await?; + let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); + + std::mem::drop(network_socket_gw); + std::mem::drop(network_socket_node1); + std::mem::drop(network_socket_node2); + std::mem::drop(ws_api_port_socket_gw); + std::mem::drop(ws_api_port_socket_node1); + std::mem::drop(ws_api_port_socket_node2); + + let gateway_node_handle = tokio::task::spawn_local(async move { + let config = config_gw.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + }); + + let node1_handle = tokio::task::spawn_local(async move { + let config = config_node1.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + }); + + let node2_handle = tokio::task::spawn_local(async move { + let config = config_node2.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + }); + + log_node1(format!("Updating node1 to block node2: {}", node2_peer_id)); + + log_gateway("Starting nodes and waiting for them to initialize".to_string()); + sleep(Duration::from_secs(10)).await; + + log_gateway("Waiting for nodes to connect to the gateway".to_string()); + sleep(Duration::from_secs(5)).await; + + let uri_gw = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let uri_node1 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let uri_node2 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + log_gateway(format!("Connecting to Gateway at {}", uri_gw)); + let (stream_gw, _) = connect_async(&uri_gw).await?; + let mut client_gw = WebApi::start(stream_gw); + + log_node1(format!("Connecting to Node1 at {}", uri_node1)); + let (stream_node1, _) = connect_async(&uri_node1).await?; + let mut client_node1 = WebApi::start(stream_node1); + + log_node2(format!("Connecting to Node2 at {}", uri_node2)); + let (stream_node2, _) = connect_async(&uri_node2).await?; + let mut client_node2 = WebApi::start(stream_node2); + + let client_gw = Arc::new(Mutex::new(client_gw)); + let client_node1 = Arc::new(Mutex::new(client_node1)); + let client_node2 = Arc::new(Mutex::new(client_node2)); + + log_gateway("Deploying ping contract on gateway node".to_string()); + let mut client_gw_lock = client_gw.lock().await; + + let ping = Ping::default(); + let state = serde_json::to_vec(&ping)?; + + let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); + log_gateway(format!( + "Loading contract code from {}", + path_to_code.display() + )); + let code = std::fs::read(path_to_code) + .ok() + .ok_or_else(|| anyhow!("Failed to read contract code"))?; + let code_hash = CodeHash::from_code(&code); + log_gateway(format!("Loaded contract code with hash: {}", code_hash)); + + let ping_options = PingContractOptions { + ttl: Duration::from_secs(5), + frequency: Duration::from_secs(1), + tag: "ping-test".to_string(), + code_key: code_hash.to_string(), + }; + + let ping = Ping::default(); + let serialized = serde_json::to_vec(&ping)?; + let wrapped_state = WrappedState::new(serialized); + + let params = Parameters::from(serde_json::to_vec(&ping_options)?); + let container = ContractContainer::try_from((code, ¶ms))?; + let contract_key = container.key(); + + log_gateway(format!( + "Deploying ping contract with key: {}", + contract_key + )); + + client_gw_lock + .send(ClientRequest::ContractOp(ContractRequest::Put { + contract: container.clone(), + state: wrapped_state.clone(), + related_contracts: RelatedContracts::new(), + subscribe: false, + })) + .await?; + + let key = wait_for_put_response(&mut client_gw_lock, &contract_key) + .await + .map_err(|e| anyhow::anyhow!("{}", e))?; + + log_gateway(format!("Ping contract deployed with key: {}", key)); + drop(client_gw_lock); + + log_node1(format!("Subscribing node1 to the contract: {}", key)); + let mut client_node1_lock = client_node1.lock().await; + client_node1_lock + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + let response = wait_for_subscribe_response(&mut client_node1_lock, &contract_key) + .await + .map_err(|e| anyhow::anyhow!("{}", e))?; + log_node1(format!("Node1 subscription response: {:?}", response)); + drop(client_node1_lock); + + log_node2(format!("Subscribing node2 to the contract: {}", key)); + let mut client_node2_lock = client_node2.lock().await; + client_node2_lock + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + let response = wait_for_subscribe_response(&mut client_node2_lock, &contract_key) + .await + .map_err(|e| anyhow::anyhow!("{}", e))?; + log_node2(format!("Node2 subscription response: {:?}", response)); + drop(client_node2_lock); + + log_gateway("Waiting for subscriptions to propagate".to_string()); + sleep(Duration::from_secs(5)).await; + + let get_all_states_fn = |client_gw: Arc>, + client_node1: Arc>, + client_node2: Arc>, + key: ContractKey, + log_gateway: Arc, + log_node1: Arc, + log_node2: Arc| + -> BoxFuture<'static, anyhow::Result<(Ping, Ping, Ping)>> { + Box::pin(async move { + log_gateway("Querying all nodes for current state...".to_string()); + + let client_gw_clone = client_gw.clone(); + let client_node1_clone = client_node1.clone(); + let client_node2_clone = client_node2.clone(); + + let gw_state_future = tokio::spawn(async move { + let mut client = client_gw_clone.lock().await; + client + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let response = client.recv().await?; + + if let HostResponse::ContractResponse(ContractResponse::GetResponse { + state, + .. + }) = response + { + let state = serde_json::from_slice::(&state)?; + Ok(state) + } else { + Err(anyhow::anyhow!("Failed to get state from gateway")) + } + }); + + let node1_state_future = tokio::spawn(async move { + let mut client = client_node1_clone.lock().await; + client + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let response = client.recv().await?; + + if let HostResponse::ContractResponse(ContractResponse::GetResponse { + state, + .. + }) = response + { + let state = serde_json::from_slice::(&state)?; + Ok(state) + } else { + Err(anyhow::anyhow!("Failed to get state from node1")) + } + }); + + let node2_state_future = tokio::spawn(async move { + let mut client = client_node2_clone.lock().await; + client + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let response = client.recv().await?; + + if let HostResponse::ContractResponse(ContractResponse::GetResponse { + state, + .. + }) = response + { + let state = serde_json::from_slice::(&state)?; + Ok(state) + } else { + Err(anyhow::anyhow!("Failed to get state from node2")) + } + }); + + let gw_result = gw_state_future.await??; + let node1_result = node1_state_future.await??; + let node2_result = node2_state_future.await??; + + log_gateway(format!("Gateway state: {:?}", gw_result)); + log_node1(format!("Node1 state: {:?}", node1_result)); + log_node2(format!("Node2 state: {:?}", node2_result)); + + Ok((gw_result, node1_result, node2_result)) + }) + }; + + async fn perform_update( + client: Arc>, + client_gw: Arc>, + client_node1: Arc>, + client_node2: Arc>, + node_name: String, + key: ContractKey, + name: String, + timestamp: u64, + log_gateway: Arc, + log_node1: Arc, + log_node2: Arc, + get_all_states_fn: impl Fn( + Arc>, + Arc>, + Arc>, + ContractKey, + Arc, + Arc, + Arc, + ) -> BoxFuture<'static, anyhow::Result<(Ping, Ping, Ping)>> + + Send + + Sync, + ) -> anyhow::Result<()> { + let logger = match node_name.as_str() { + "Gateway" => log_gateway.clone(), + "Node1" => log_node1.clone(), + "Node2" => log_node2.clone(), + _ => log_gateway.clone(), + }; + + for retry in 0..MAX_UPDATE_RETRIES { + logger(format!( + "Updating contract on {} (attempt {})", + node_name, + retry + 1 + )); + + let delay = (BASE_DELAY_MS * (2_u64.pow(retry as u32))).min(MAX_DELAY_MS); + logger(format!( + "Update attempt {} with delay {}ms", + retry + 1, + delay + )); + + let mut client_lock = client.lock().await; + + let mut ping = Ping::default(); + let formatted_key = format!("{}:{}", name.clone(), timestamp); + ping.insert(formatted_key.clone()); + + let state_str = serde_json::to_string(&ping)?; + logger(format!("Serialized ping: {}", state_str)); + logger(format!( + "Ping contains key: {}, value: {:?}", + formatted_key, + ping.get(&formatted_key) + )); + let state = serde_json::to_vec(&ping)?; + + client_lock + .send(ClientRequest::ContractOp(ContractRequest::Update { + key, + data: UpdateData::Delta(StateDelta::from(state)), + })) + .await?; + + let response = wait_for_put_response(&mut client_lock, &key) + .await + .map_err(|e| anyhow::anyhow!("{}", e))?; + logger(format!("{} update response: {:?}", node_name, response)); + drop(client_lock); + + logger(format!( + "Waiting for update to propagate (initial {}ms)...", + delay / 3 + )); + sleep(Duration::from_millis(delay / 3)).await; + + let (gw_state, node1_state, node2_state) = get_all_states_fn( + client_gw.clone(), + client_node1.clone(), + client_node2.clone(), + key, + log_gateway.clone(), + log_node1.clone(), + log_node2.clone(), + ) + .await?; + + let formatted_key = format!("{}:{}", name, timestamp); + + logger(format!("First check - Gateway state: {}", gw_state)); + logger(format!("First check - Node1 state: {}", node1_state)); + logger(format!("First check - Node2 state: {}", node2_state)); + + let gw_has_update = gw_state.contains_key(&formatted_key); + let node1_has_update = node1_state.contains_key(&formatted_key); + let node2_has_update = node2_state.contains_key(&formatted_key); + + logger(format!( + "First check - Update propagation status for key '{}' - Gateway: {}, Node1: {}, Node2: {}", + formatted_key, gw_has_update, node1_has_update, node2_has_update + )); + + let update_propagated = match node_name.as_str() { + "Gateway" => gw_has_update && node1_has_update && node2_has_update, + "Node1" => gw_has_update && node1_has_update && node2_has_update, + "Node2" => gw_has_update && node1_has_update && node2_has_update, + _ => false, + }; + + if update_propagated { + logger(format!( + "Update from {} successfully propagated to all nodes on first check", + node_name + )); + return Ok(()); + } + + logger(format!( + "Update not fully propagated on first check, waiting additional {}ms...", + delay / 3 + )); + sleep(Duration::from_millis(delay / 3)).await; + + let (gw_state2, node1_state2, node2_state2) = get_all_states_fn( + client_gw.clone(), + client_node1.clone(), + client_node2.clone(), + key, + log_gateway.clone(), + log_node1.clone(), + log_node2.clone(), + ) + .await?; + + logger(format!("Second check - Gateway state: {}", gw_state2)); + logger(format!("Second check - Node1 state: {}", node1_state2)); + logger(format!("Second check - Node2 state: {}", node2_state2)); + + let gw_has_update2 = gw_state2.contains_key(&formatted_key); + let node1_has_update2 = node1_state2.contains_key(&formatted_key); + let node2_has_update2 = node2_state2.contains_key(&formatted_key); + + logger(format!( + "Second check - Update propagation status for key '{}' - Gateway: {}, Node1: {}, Node2: {}", + formatted_key, gw_has_update2, node1_has_update2, node2_has_update2 + )); + + let update_propagated = match node_name.as_str() { + "Gateway" => gw_has_update2 && node1_has_update2 && node2_has_update2, + "Node1" => gw_has_update2 && node1_has_update2 && node2_has_update2, + "Node2" => gw_has_update2 && node1_has_update2 && node2_has_update2, + _ => false, + }; + + if update_propagated { + logger(format!( + "Update from {} successfully propagated to all nodes on second check", + node_name + )); + return Ok(()); + } + + logger(format!("Update not fully propagated on second check, waiting final {}ms before next retry...", delay/3)); + sleep(Duration::from_millis(delay / 3)).await; + + if !gw_has_update2 { + logger(format!( + "Gateway missing update for key '{}'", + formatted_key + )); + } + if !node1_has_update2 { + logger(format!("Node1 missing update for key '{}'", formatted_key)); + } + if !node2_has_update2 { + logger(format!("Node2 missing update for key '{}'", formatted_key)); + } + + logger(format!( + "Update from {} not fully propagated after attempt {}, retrying...", + node_name, + retry + 1 + )); + } + + logger(format!( + "Failed to propagate update from {} after {} retries with max delay {}ms", + node_name, MAX_UPDATE_RETRIES, MAX_DELAY_MS + )); + + Err(anyhow::anyhow!( + "Failed to propagate update from {} after {} retries", + node_name, + MAX_UPDATE_RETRIES + )) + } + + log_gateway("Testing update propagation from Gateway to Node1 and Node2".to_string()); + let gateway_update_result = perform_update( + client_gw.clone(), + client_gw.clone(), + client_node1.clone(), + client_node2.clone(), + "Gateway".to_string(), + key, + "Gateway Update".to_string(), + 42, + log_gateway.clone(), + log_node1.clone(), + log_node2.clone(), + get_all_states_fn, + ) + .await; + + log_node1("Testing update propagation from Node1 to Gateway and Node2".to_string()); + let node1_update_result = perform_update( + client_node1.clone(), + client_gw.clone(), + client_node1.clone(), + client_node2.clone(), + "Node1".to_string(), + key, + "Node1 Update".to_string(), + 43, + log_gateway.clone(), + log_node1.clone(), + log_node2.clone(), + get_all_states_fn, + ) + .await; + + log_node2("Testing update propagation from Node2 to Gateway and Node1".to_string()); + let node2_update_result = perform_update( + client_node2.clone(), + client_gw.clone(), + client_node1.clone(), + client_node2.clone(), + "Node2".to_string(), + key, + "Node2 Update".to_string(), + 44, + log_gateway.clone(), + log_node1.clone(), + log_node2.clone(), + get_all_states_fn, + ) + .await; + + let all_updates_successful = gateway_update_result.is_ok() + && node1_update_result.is_ok() + && node2_update_result.is_ok(); + + if all_updates_successful { + log_gateway("All updates successfully propagated!".to_string()); + } else { + if let Err(e) = &gateway_update_result { + log_gateway(format!("Gateway update error: {}", e)); + } + if let Err(e) = &node1_update_result { + log_node1(format!("Node1 update error: {}", e)); + } + if let Err(e) = &node2_update_result { + log_node2(format!("Node2 update error: {}", e)); + } + } + + assert!( + gateway_update_result.is_ok(), + "Gateway update failed to propagate" + ); + assert!( + node1_update_result.is_ok(), + "Node1 update failed to propagate" + ); + assert!( + node2_update_result.is_ok(), + "Node2 update failed to propagate" + ); + + log_gateway("Test completed, cleaning up node handles".to_string()); + gateway_node_handle.abort(); + node1_handle.abort(); + node2_handle.abort(); + + drop(log_tx); + + Ok::<_, anyhow::Error>(()) + }; + + let instrumented_test = test.instrument(span!(Level::INFO, "test_ping_blocked_peers_solution")); + + let result = local + .run_until(async move { + select! { + result = instrumented_test => result.map_err(|e| e.into()), + _ = sleep(Duration::from_secs(MAX_TEST_DURATION_SECS)) => { + Err("Test timed out".into()) + } + } + }) + .await; + + let _ = log_task.await; + + result +} diff --git a/apps/freenet-ping/app/tests/run_app_broadcast_mechanism.rs b/apps/freenet-ping/app/tests/run_app_broadcast_mechanism.rs new file mode 100644 index 000000000..b2c827d17 --- /dev/null +++ b/apps/freenet-ping/app/tests/run_app_broadcast_mechanism.rs @@ -0,0 +1,654 @@ +use std::{ + fmt::Debug, + net::{Ipv4Addr, SocketAddr, TcpListener}, + path::PathBuf, + sync::{Arc, Mutex}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use anyhow::anyhow; +use freenet::{ + config::{ConfigArgs, InlineGwConfig, NetworkArgs, SecretArgs, WebsocketApiArgs}, + dev_tool::TransportKeypair, + local_node::NodeConfig, + server::serve_gateway, +}; +use freenet_ping_types::{Ping, PingContractOptions}; +use freenet_stdlib::{ + client_api::{ClientRequest, ContractRequest, WebApi}, + prelude::*, +}; +use futures::{future::BoxFuture, FutureExt}; +use rand::{random, Rng, SeedableRng}; +use testresult::TestResult; +use tokio::{task::LocalSet, time::sleep}; +use tokio_tungstenite::connect_async; +use tracing::info; + +use freenet_ping_app::ping_client::{ + wait_for_get_response, wait_for_put_response, wait_for_subscribe_response, +}; + +const MAX_UPDATE_RETRIES: usize = 8; +const INITIAL_DELAY_MS: u64 = 500; +const MAX_DELAY_MS: u64 = 15000; +const PROPAGATION_CHECK_INTERVAL_MS: u64 = 1000; +const MAX_PROPAGATION_CHECKS: usize = 10; +const PACKAGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const PATH_TO_CONTRACT: &str = "../contracts/ping/build/freenet/freenet_ping_contract"; +const APP_TAG: &str = "ping-app"; + +static RNG: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + std::sync::Mutex::new(rand::rngs::StdRng::from_seed( + *b"0102030405060708090a0b0c0d0e0f10", + )) + }); + +struct PresetConfig { + temp_dir: tempfile::TempDir, +} + +async fn base_node_test_config( + is_gateway: bool, + gateways: Vec, + public_port: Option, + ws_api_port: u16, + blocked_addresses: Option>, +) -> anyhow::Result<(ConfigArgs, PresetConfig)> { + if is_gateway { + assert!(public_port.is_some()); + } + + let temp_dir = tempfile::tempdir()?; + let key = TransportKeypair::new_with_rng(&mut *RNG.lock().unwrap()); + let transport_keypair = temp_dir.path().join("private.pem"); + key.save(&transport_keypair)?; + key.public().save(temp_dir.path().join("public.pem"))?; + let config = ConfigArgs { + ws_api: WebsocketApiArgs { + address: Some(Ipv4Addr::LOCALHOST.into()), + ws_api_port: Some(ws_api_port), + }, + network_api: NetworkArgs { + public_address: Some(Ipv4Addr::LOCALHOST.into()), + public_port, + is_gateway, + skip_load_from_network: true, + gateways: Some(gateways), + location: Some(RNG.lock().unwrap().gen()), + ignore_protocol_checking: true, + address: Some(Ipv4Addr::LOCALHOST.into()), + network_port: public_port, + bandwidth_limit: None, + blocked_addresses, + }, + config_paths: { + freenet::config::ConfigPathsArgs { + config_dir: Some(temp_dir.path().to_path_buf()), + data_dir: Some(temp_dir.path().to_path_buf()), + } + }, + secrets: SecretArgs { + transport_keypair: Some(transport_keypair), + ..Default::default() + }, + ..Default::default() + }; + Ok((config, PresetConfig { temp_dir })) +} + +fn gw_config(port: u16, path: &std::path::Path) -> anyhow::Result { + Ok(InlineGwConfig { + address: (Ipv4Addr::LOCALHOST, port).into(), + location: Some(random()), + public_key_path: path.join("public.pem"), + }) +} + +#[derive(Debug, Clone)] +struct LogEntry { + timestamp: SystemTime, + node_name: String, + message: String, +} + +struct ChronologicalLogger { + entries: Arc>>, +} + +impl ChronologicalLogger { + fn new() -> Self { + Self { + entries: Arc::new(Mutex::new(Vec::new())), + } + } + + fn log(&self, node_name: &str, message: &str) { + let entry = LogEntry { + timestamp: SystemTime::now(), + node_name: node_name.to_string(), + message: message.to_string(), + }; + + if let Ok(mut entries) = self.entries.lock() { + entries.push(entry); + } + } + + fn print_logs(&self) { + if let Ok(mut entries) = self.entries.lock() { + entries.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); + + println!("\n=== CHRONOLOGICAL LOGS FROM ALL NODES ==="); + for entry in entries.iter() { + let timestamp = entry + .timestamp + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + println!( + "[{}.{:03}] [{}] {}", + timestamp.as_secs(), + timestamp.subsec_millis(), + entry.node_name, + entry.message + ); + } + println!("=== END OF LOGS ===\n"); + } + } +} + +#[tokio::test] +async fn test_ping_broadcast_mechanism() -> TestResult { + freenet::config::set_logger(Some(tracing::level_filters::LevelFilter::DEBUG), None); + + let logger = Arc::new(ChronologicalLogger::new()); + let gateway_logger = logger.clone(); + let node1_logger = logger.clone(); + let node2_logger = logger.clone(); + + let network_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let gw_network_port = network_socket_gw.local_addr()?.port(); + let _gw_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), gw_network_port); + + let ws_api_port_socket_gw = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let ws_api_port_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + + let network_socket_node1 = TcpListener::bind("127.0.0.1:0")?; + let node1_network_port = network_socket_node1.local_addr()?.port(); + let node1_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node1_network_port); + + let network_socket_node2 = TcpListener::bind("127.0.0.1:0")?; + let node2_network_port = network_socket_node2.local_addr()?.port(); + let node2_network_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), node2_network_port); + + let (config_gw, _preset_cfg_gw, config_gw_info) = { + let (cfg, preset) = base_node_test_config( + true, + vec![], + Some(gw_network_port), + ws_api_port_socket_gw.local_addr()?.port(), + None, // Gateway doesn't block any peers + ) + .await?; + let public_port = cfg.network_api.public_port.unwrap(); + let path = preset.temp_dir.path().to_path_buf(); + (cfg, preset, gw_config(public_port, &path)?) + }; + let ws_api_port_gw = config_gw.ws_api.ws_api_port.unwrap(); + gateway_logger.log( + "Gateway", + &format!("Gateway WS API port: {}", ws_api_port_gw), + ); + + let (config_node1, _preset_cfg_node1) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node1_network_port), + ws_api_port_socket_node1.local_addr()?.port(), + Some(vec![node2_network_addr]), // Node1 blocks Node2 + ) + .await?; + let ws_api_port_node1 = config_node1.ws_api.ws_api_port.unwrap(); + node1_logger.log( + "Node1", + &format!("Node1 WS API port: {}", ws_api_port_node1), + ); + node1_logger.log("Node1", &format!("Node1 blocks: {:?}", node2_network_addr)); + + let (config_node2, _preset_cfg_node2) = base_node_test_config( + false, + vec![serde_json::to_string(&config_gw_info)?], + Some(node2_network_port), + ws_api_port_socket_node2.local_addr()?.port(), + Some(vec![node1_network_addr]), // Node2 blocks Node1 + ) + .await?; + let ws_api_port_node2 = config_node2.ws_api.ws_api_port.unwrap(); + node2_logger.log( + "Node2", + &format!("Node2 WS API port: {}", ws_api_port_node2), + ); + node2_logger.log("Node2", &format!("Node2 blocks: {:?}", node1_network_addr)); + + std::mem::drop(network_socket_gw); + std::mem::drop(network_socket_node1); + std::mem::drop(network_socket_node2); + std::mem::drop(ws_api_port_socket_gw); + std::mem::drop(ws_api_port_socket_node1); + std::mem::drop(ws_api_port_socket_node2); + + let gateway_node = async { + let config = config_gw.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node1 = async move { + let config = config_node1.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let node2 = async { + let config = config_node2.build().await?; + let node = NodeConfig::new(config.clone()) + .await? + .build(serve_gateway(config.ws_api).await) + .await?; + node.run().await + } + .boxed_local(); + + let local = LocalSet::new(); + + local.spawn_local(gateway_node); + local.spawn_local(node1); + local.spawn_local(node2); + + tokio::task::spawn_local(async { + local.await; + }); + + sleep(Duration::from_secs(10)).await; + + let uri_gw = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let uri_node1 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let uri_node2 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + let (stream_gw, _) = connect_async(&uri_gw).await?; + let (stream_node1, _) = connect_async(&uri_node1).await?; + let (stream_node2, _) = connect_async(&uri_node2).await?; + + let client_gw = WebApi::start(stream_gw); + let client_node1 = WebApi::start(stream_node1); + let client_node2 = WebApi::start(stream_node2); + + let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); + tracing::info!(path=%path_to_code.display(), "loading contract code"); + let code = std::fs::read(path_to_code) + .ok() + .ok_or_else(|| anyhow!("Failed to read contract code"))?; + let code_hash = CodeHash::from_code(&code); + tracing::info!(code_hash=%code_hash, "loaded contract code"); + + let ping_options = PingContractOptions { + frequency: Duration::from_secs(5), + ttl: Duration::from_secs(30), + tag: APP_TAG.to_string(), + code_key: code_hash.to_string(), + }; + let params = Parameters::from(serde_json::to_vec(&ping_options).unwrap()); + let container = ContractContainer::try_from((code, ¶ms))?; + let contract_key = container.key(); + + tracing::info!("Gateway node putting contract..."); + let wrapped_state = { + let ping = Ping::default(); + let serialized = serde_json::to_vec(&ping)?; + WrappedState::new(serialized) + }; + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Put { + contract: container.clone(), + state: wrapped_state.clone(), + related_contracts: RelatedContracts::new(), + subscribe: false, + })) + .await?; + + let key = wait_for_put_response(&mut client_gw, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + gateway_logger.log( + "Gateway", + &format!("Deployed ping contract with key: {}", key), + ); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + wait_for_subscribe_response(&mut client_gw, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + gateway_logger.log("Gateway", &format!("Subscribed to contract: {}", key)); + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + wait_for_subscribe_response(&mut client_node1, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + node1_logger.log("Node1", &format!("Subscribed to contract: {}", key)); + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Subscribe { + key: contract_key, + summary: None, + })) + .await?; + wait_for_subscribe_response(&mut client_node2, &contract_key) + .await + .map_err(anyhow::Error::msg)?; + node2_logger.log("Node2", &format!("Subscribed to contract: {}", key)); + + sleep(Duration::from_secs(5)).await; + + let ws_api_port_gw_copy = ws_api_port_gw; + let ws_api_port_node1_copy = ws_api_port_node1; + let ws_api_port_node2_copy = ws_api_port_node2; + + let get_all_states = + move |key: ContractKey| -> BoxFuture<'static, anyhow::Result<(Ping, Ping, Ping)>> { + let key = key.clone(); + let ws_api_port_gw = ws_api_port_gw_copy; + let ws_api_port_node1 = ws_api_port_node1_copy; + let ws_api_port_node2 = ws_api_port_node2_copy; + + Box::pin(async move { + info!("Querying all nodes for current state..."); + + let uri_gw = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let uri_node1 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let uri_node2 = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + let (stream_gw, _) = connect_async(&uri_gw).await?; + let (stream_node1, _) = connect_async(&uri_node1).await?; + let (stream_node2, _) = connect_async(&uri_node2).await?; + + let mut client_gw = WebApi::start(stream_gw); + let mut client_node1 = WebApi::start(stream_node1); + let mut client_node2 = WebApi::start(stream_node2); + + client_gw + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node1 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + client_node2 + .send(ClientRequest::ContractOp(ContractRequest::Get { + key, + return_contract_code: false, + subscribe: false, + })) + .await?; + + let state_gw = wait_for_get_response(&mut client_gw, &key) + .await + .map_err(anyhow::Error::msg)?; + + let state_node1 = wait_for_get_response(&mut client_node1, &key) + .await + .map_err(anyhow::Error::msg)?; + + let state_node2 = wait_for_get_response(&mut client_node2, &key) + .await + .map_err(anyhow::Error::msg)?; + + Ok((state_gw, state_node1, state_node2)) + }) + }; + + let send_update_and_check_propagation = |source_node: &str, + key: &ContractKey, + ws_api_port: u16, + logger: Arc| + -> BoxFuture<'static, anyhow::Result> { + let key = *key; + let source_node = source_node.to_string(); + let logger = logger.clone(); + let source_uri = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port + ); + + Box::pin(async move { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + let update_id = format!("update-{}-{}", source_node, timestamp); + + logger.log( + &source_node, + &format!("Sending update with ID: {}", update_id), + ); + + let (stream_source, _) = connect_async(&source_uri).await?; + let mut source_client = WebApi::start(stream_source); + + let mut delay_ms = INITIAL_DELAY_MS; + let mut success = false; + + let mut ping = Ping::default(); + ping.insert(update_id.clone()); + + for attempt in 1..=MAX_UPDATE_RETRIES { + match source_client + .send(ClientRequest::ContractOp(ContractRequest::Update { + key, + data: UpdateData::Delta(StateDelta::from( + serde_json::to_vec(&ping).unwrap(), + )), + })) + .await + { + Ok(_) => { + logger.log( + &source_node, + &format!("Update sent successfully on attempt {}", attempt), + ); + success = true; + break; + } + Err(e) => { + logger.log( + &source_node, + &format!("Update attempt {} failed: {}", attempt, e), + ); + + if e.to_string().contains("connection") { + let (new_stream, _) = match connect_async(&source_uri).await { + Ok(s) => { + logger.log(&source_node, "Reconnected successfully"); + s + } + Err(e) => { + logger + .log(&source_node, &format!("Reconnection failed: {}", e)); + delay_ms = (delay_ms * 2).min(MAX_DELAY_MS); + sleep(Duration::from_millis(delay_ms)).await; + continue; + } + }; + source_client = WebApi::start(new_stream); + } + + delay_ms = (delay_ms * 2).min(MAX_DELAY_MS); + sleep(Duration::from_millis(delay_ms)).await; + } + } + } + + if !success { + logger.log( + &source_node, + "Failed to send update after all retry attempts", + ); + return Ok(false); + } + + let gw_uri = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_gw + ); + let node1_uri = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node1 + ); + let node2_uri = format!( + "ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native", + ws_api_port_node2 + ); + + let (stream_gw, _) = connect_async(&gw_uri).await?; + let (stream_node1, _) = connect_async(&node1_uri).await?; + let (stream_node2, _) = connect_async(&node2_uri).await?; + + let mut client_gw = WebApi::start(stream_gw); + let mut client_node1 = WebApi::start(stream_node1); + let mut client_node2 = WebApi::start(stream_node2); + + for check in 1..=MAX_PROPAGATION_CHECKS { + logger.log( + "Test", + &format!("Propagation check {} for update {}", check, update_id), + ); + + sleep(Duration::from_millis(PROPAGATION_CHECK_INTERVAL_MS)).await; + + let states_result = get_all_states(key).await; + + match states_result { + Ok((gw_ping, node1_ping, node2_ping)) => { + let gw_has_update = gw_ping.contains_key(&update_id); + let node1_has_update = node1_ping.contains_key(&update_id); + let node2_has_update = node2_ping.contains_key(&update_id); + + logger.log( + "Test", + &format!( + "Update {} propagation status - Gateway: {}, Node1: {}, Node2: {}", + update_id, gw_has_update, node1_has_update, node2_has_update + ), + ); + + if gw_has_update && node1_has_update && node2_has_update { + logger.log( + "Test", + &format!( + "Update {} successfully propagated to all nodes", + update_id + ), + ); + return Ok(true); + } + } + Err(e) => { + logger.log("Test", &format!("Error checking propagation: {}", e)); + } + } + } + + logger.log( + "Test", + &format!( + "Update {} failed to propagate to all nodes after {} checks", + update_id, MAX_PROPAGATION_CHECKS + ), + ); + + Ok(false) + }) + }; + + let node1_to_node2_result = send_update_and_check_propagation( + "Node1", + &contract_key, + ws_api_port_node1, + node1_logger.clone(), + ) + .await?; + + let node2_to_node1_result = send_update_and_check_propagation( + "Node2", + &contract_key, + ws_api_port_node2, + node2_logger.clone(), + ) + .await?; + + let gateway_to_nodes_result = send_update_and_check_propagation( + "Gateway", + &contract_key, + ws_api_port_gw, + gateway_logger.clone(), + ) + .await?; + + logger.print_logs(); + + assert!( + !node1_to_node2_result || !node2_to_node1_result || !gateway_to_nodes_result, + "Expected at least one update propagation test to fail, but all succeeded" + ); + + Ok(()) +}