use anyhow::{anyhow, Context, Result};
use freenet_stdlib::{
client_api::{ClientRequest, ContractRequest, ContractResponse, HostResponse, WebApi},
prelude::*,
};
use freenet_test_network::{Backend, BuildProfile, DockerNatConfig, FreenetBinary, TestNetwork};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio_tungstenite::connect_async;
use tracing::{debug, error, info, warn};
const PING_CONTRACT_PATH: &str =
"../freenet-core/main/apps/freenet-ping/contracts/ping/build/freenet/freenet_ping_contract";
#[derive(Debug, Serialize, Deserialize)]
struct PingContractOptions {
#[serde(with = "humantime_serde")]
ttl: Duration,
#[serde(with = "humantime_serde")]
frequency: Duration,
tag: String,
code_key: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
struct Ping {
from: HashMap<String, Vec<chrono::DateTime<chrono::Utc>>>,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,freenet_test_network=debug".into()),
)
.init();
info!("=== Minimal NAT Reproduction Test ===");
info!("Configuration: 1 gateway (public) + 1 peer (behind NAT)");
info!("Starting Docker NAT test network...");
let start_time = std::time::Instant::now();
let freenet_core_path = std::env::var("FREENET_CORE_PATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("../freenet-core/main"));
let binary = if freenet_core_path.exists() {
info!(
"Using freenet-core workspace at: {}",
freenet_core_path.display()
);
FreenetBinary::Workspace {
path: freenet_core_path,
profile: BuildProfile::Release,
}
} else {
info!("freenet-core workspace not found, using installed binary");
FreenetBinary::Installed
};
let network = Arc::new(
TestNetwork::builder()
.gateways(1)
.peers(1)
.binary(binary)
.backend(Backend::DockerNat(DockerNatConfig::default()))
.require_connectivity(1.0)
.connectivity_timeout(Duration::from_secs(120))
.preserve_temp_dirs_on_failure(true)
.build()
.await
.context("Failed to start test network")?,
);
let startup_duration = start_time.elapsed();
info!("Network started in {:.1}s", startup_duration.as_secs_f64());
info!("Network topology:");
info!(
" Gateway: {} (WS: {})",
network.gateway(0).network_address(),
network.gateway(0).ws_url()
);
info!(
" Peer: {} (behind NAT) (WS: {})",
network.peer(0).network_address(),
network.peer(0).ws_url()
);
info!("Connecting WebSocket clients...");
let mut gateway_client = connect_ws_client(&network.gateway(0).ws_url())
.await
.context("Failed to connect to gateway")?;
let mut peer_client = connect_ws_client(&network.peer(0).ws_url())
.await
.context("Failed to connect to peer")?;
info!("WebSocket clients connected");
info!("Loading test contract...");
let contract_code = load_ping_contract().context("Failed to load ping contract")?;
info!("Contract code loaded: {} bytes", contract_code.len());
let initial_state = Ping::default();
let state_bytes = serde_json::to_vec(&initial_state)?;
let options = PingContractOptions {
ttl: Duration::from_secs(300),
frequency: Duration::from_secs(1),
tag: "nat-test".to_string(),
code_key: "test".to_string(),
};
let params_bytes = serde_json::to_vec(&options)?;
let params = Parameters::from(params_bytes);
let contract = ContractContainer::try_from((contract_code, ¶ms))
.context("Failed to create contract container")?;
let contract_key = contract.key();
info!("Contract key: {}", contract_key);
info!("Step 1: PUT contract from peer (behind NAT)...");
let put_start = std::time::Instant::now();
peer_client
.send(ClientRequest::ContractOp(ContractRequest::Put {
contract,
state: WrappedState::new(state_bytes.clone()),
related_contracts: RelatedContracts::new(),
subscribe: false,
}))
.await
.context("Failed to send PUT request")?;
info!("Waiting for PUT response (timeout: 60s)...");
let put_result = wait_for_response(&mut peer_client, Duration::from_secs(60)).await;
match put_result {
Ok(HostResponse::ContractResponse(ContractResponse::PutResponse { key })) => {
info!(
"PUT successful in {:.1}s, key: {}",
put_start.elapsed().as_secs_f64(),
key
);
}
Ok(other) => {
error!("Unexpected PUT response: {:?}", other);
dump_container_logs(&network, "put_unexpected").await;
return Err(anyhow!("PUT failed with unexpected response"));
}
Err(e) => {
error!(
"PUT failed after {:.1}s: {:?}",
put_start.elapsed().as_secs_f64(),
e
);
dump_container_logs(&network, "put_timeout").await;
return Err(e.context("PUT from peer behind NAT failed"));
}
}
info!("Step 2: GET contract from gateway...");
let get_start = std::time::Instant::now();
gateway_client
.send(ClientRequest::ContractOp(ContractRequest::Get {
key: contract_key.clone(),
return_contract_code: false,
subscribe: false,
}))
.await
.context("Failed to send GET request")?;
let get_response = wait_for_response(&mut gateway_client, Duration::from_secs(30))
.await
.context("Waiting for GET response")?;
match get_response {
HostResponse::ContractResponse(ContractResponse::GetResponse { state, .. }) => {
let get_duration = get_start.elapsed();
info!("GET successful in {:.1}s", get_duration.as_secs_f64());
let retrieved_state: Ping =
serde_json::from_slice(&state).context("Failed to deserialize retrieved state")?;
if retrieved_state == initial_state {
info!("State verification PASSED");
info!(" Expected: {:?}", initial_state);
info!(" Got: {:?}", retrieved_state);
} else {
warn!("State differs (may be expected for timestamp-based contracts)");
info!(" Initial: {:?}", initial_state);
info!(" Got: {:?}", retrieved_state);
}
}
other => {
error!("Unexpected GET response: {:?}", other);
return Err(anyhow!("GET failed with unexpected response"));
}
}
info!("Step 3: Verify GET from peer (self-retrieval)...");
peer_client
.send(ClientRequest::ContractOp(ContractRequest::Get {
key: contract_key.clone(),
return_contract_code: false,
subscribe: false,
}))
.await
.context("Failed to send peer GET request")?;
let peer_get_response = wait_for_response(&mut peer_client, Duration::from_secs(30))
.await
.context("Waiting for peer GET response")?;
match peer_get_response {
HostResponse::ContractResponse(ContractResponse::GetResponse { state, .. }) => {
let peer_state: Ping =
serde_json::from_slice(&state).context("Failed to deserialize peer state")?;
info!("Peer self-GET successful: {:?}", peer_state);
}
other => {
warn!("Peer GET returned unexpected response: {:?}", other);
}
}
info!("=== Test PASSED ===");
info!(
"Total test duration: {:.1}s",
start_time.elapsed().as_secs_f64()
);
info!("Cleaning up...");
Ok(())
}
async fn connect_ws_client(ws_url: &str) -> Result<WebApi> {
let uri = format!("{}?encodingProtocol=native", ws_url);
debug!("Connecting to WebSocket: {}", uri);
let (stream, _) = connect_async(&uri)
.await
.context("WebSocket connection failed")?;
Ok(WebApi::start(stream))
}
fn load_ping_contract() -> Result<Vec<u8>> {
let contract_path =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(PING_CONTRACT_PATH);
if contract_path.exists() {
debug!("Loading contract from: {:?}", contract_path);
std::fs::read(&contract_path).context("Failed to read contract file")
} else {
let alt_paths = [
"/home/ian/code/freenet/freenet-core/main/apps/freenet-ping/contracts/ping/build/freenet/freenet_ping_contract",
"./freenet_ping_contract",
];
for alt in &alt_paths {
let path = std::path::PathBuf::from(alt);
if path.exists() {
debug!("Loading contract from alternative path: {:?}", path);
return std::fs::read(&path).context("Failed to read contract file");
}
}
Err(anyhow!(
"Could not find ping contract. Tried:\n - {:?}\n - {:?}",
contract_path,
alt_paths.join("\n - ")
))
}
}
async fn wait_for_response(client: &mut WebApi, timeout: Duration) -> Result<HostResponse> {
tokio::time::timeout(timeout, client.recv())
.await
.context("Timeout waiting for response")?
.map_err(|e| anyhow!("Client error: {:?}", e))
}
async fn dump_container_logs(network: &TestNetwork, context: &str) {
info!("=== Dumping container logs ({}) ===", context);
info!("--- Gateway logs (last 50 lines) ---");
match network.gateway(0).read_logs() {
Ok(logs) => {
for entry in logs.iter().rev().take(50).rev() {
info!(" [gw] {}", entry.message);
}
}
Err(e) => warn!("Failed to read gateway logs: {:?}", e),
}
info!("--- Peer logs (last 50 lines) ---");
match network.peer(0).read_logs() {
Ok(logs) => {
for entry in logs.iter().rev().take(50).rev() {
info!(" [peer] {}", entry.message);
}
}
Err(e) => warn!("Failed to read peer logs: {:?}", e),
}
info!("=== End of logs ===");
}