|
|
|
@@ -0,0 +1,636 @@
|
|
|
|
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
|
|
// Gateway Active Set Validation Test
|
|
|
|
|
//
|
|
|
|
|
// - nym-vpn-api shows ALL gateways
|
|
|
|
|
// - nym-api epoch rewarded set has limited nodes (sandbox: 1 entry, 1 exit)
|
|
|
|
|
// - dVPN mode requires mixnet registration
|
|
|
|
|
// - If gateway isn't in epoch rewarded set, does registration fail?
|
|
|
|
|
// - Result: "no gateway with id" errors on mainnet
|
|
|
|
|
//
|
|
|
|
|
// THE TEST SCENARIOS:
|
|
|
|
|
// 1. Active entry gateway -> any node (in or out of rewarded set)
|
|
|
|
|
// 2. Non-active entry gateway -> active node
|
|
|
|
|
// 3. Non-active entry gateway -> another non-active node
|
|
|
|
|
|
|
|
|
|
use nym_network_defaults::setup_env;
|
|
|
|
|
use nym_sdk::mixnet::{self, MixnetMessageSender};
|
|
|
|
|
use nym_topology::EpochRewardedSet;
|
|
|
|
|
use nym_validator_client::nym_api::NymApiClientExt;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct GatewayInfo {
|
|
|
|
|
node_id: u32,
|
|
|
|
|
identity: String,
|
|
|
|
|
role: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn analyze_network() -> anyhow::Result<NetworkAnalysis> {
|
|
|
|
|
// Get nym-api URL from network details (already set to sandbox via setup_env)
|
|
|
|
|
let network_details = nym_network_defaults::NymNetworkDetails::new_from_env();
|
|
|
|
|
let nym_api = network_details
|
|
|
|
|
.nym_api_urls
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|urls| urls.first())
|
|
|
|
|
.and_then(|api_url| api_url.url.parse::<url::Url>().ok())
|
|
|
|
|
.unwrap_or_else(|| "https://sandbox-nym-api1.nymtech.net/api/".parse().unwrap());
|
|
|
|
|
|
|
|
|
|
tracing::info!("Using nym-api: {}", nym_api);
|
|
|
|
|
|
|
|
|
|
let validator_client = nym_http_api_client::Client::builder(nym_api)
|
|
|
|
|
.expect("Failed to create API client builder")
|
|
|
|
|
.build()
|
|
|
|
|
.expect("Failed to build API client");
|
|
|
|
|
|
|
|
|
|
// Get epoch rewarded set from contract
|
|
|
|
|
let rewarded_set = validator_client
|
|
|
|
|
.get_current_rewarded_set()
|
|
|
|
|
.await
|
|
|
|
|
.expect("Failed to get rewarded set");
|
|
|
|
|
|
|
|
|
|
let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into();
|
|
|
|
|
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("Current Epoch Rewarded Set (from contract):");
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" Entry gateways: {:?}",
|
|
|
|
|
epoch_rewarded_set.assignment.entry_gateways
|
|
|
|
|
);
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" Exit gateways: {:?}",
|
|
|
|
|
epoch_rewarded_set.assignment.exit_gateways
|
|
|
|
|
);
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" Layer 1 (mixnodes): {:?}",
|
|
|
|
|
epoch_rewarded_set.assignment.layer1
|
|
|
|
|
);
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" Layer 2 (mixnodes): {:?}",
|
|
|
|
|
epoch_rewarded_set.assignment.layer2
|
|
|
|
|
);
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" Layer 3 (mixnodes): {:?}",
|
|
|
|
|
epoch_rewarded_set.assignment.layer3
|
|
|
|
|
);
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
|
|
|
|
|
// Get ALL entry-capable nodes
|
|
|
|
|
let all_entry_nodes = validator_client
|
|
|
|
|
.get_all_basic_entry_assigned_nodes_with_metadata()
|
|
|
|
|
.await
|
|
|
|
|
.expect("Failed to get all entry nodes");
|
|
|
|
|
|
|
|
|
|
tracing::info!("Total entry-capable nodes: {}", all_entry_nodes.nodes.len());
|
|
|
|
|
|
|
|
|
|
let mut active_entry_gateways = Vec::new();
|
|
|
|
|
let mut non_active_entry_gateways = Vec::new();
|
|
|
|
|
|
|
|
|
|
for node in all_entry_nodes.nodes {
|
|
|
|
|
let in_rewarded_set = epoch_rewarded_set
|
|
|
|
|
.assignment
|
|
|
|
|
.entry_gateways
|
|
|
|
|
.contains(&node.node_id)
|
|
|
|
|
|| epoch_rewarded_set
|
|
|
|
|
.assignment
|
|
|
|
|
.exit_gateways
|
|
|
|
|
.contains(&node.node_id);
|
|
|
|
|
|
|
|
|
|
let gateway_info = GatewayInfo {
|
|
|
|
|
node_id: node.node_id,
|
|
|
|
|
identity: node.ed25519_identity_pubkey.to_string(),
|
|
|
|
|
role: if epoch_rewarded_set
|
|
|
|
|
.assignment
|
|
|
|
|
.entry_gateways
|
|
|
|
|
.contains(&node.node_id)
|
|
|
|
|
{
|
|
|
|
|
"entry".to_string()
|
|
|
|
|
} else if epoch_rewarded_set
|
|
|
|
|
.assignment
|
|
|
|
|
.exit_gateways
|
|
|
|
|
.contains(&node.node_id)
|
|
|
|
|
{
|
|
|
|
|
"exit".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
"not in set".to_string()
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if in_rewarded_set {
|
|
|
|
|
active_entry_gateways.push(gateway_info);
|
|
|
|
|
} else {
|
|
|
|
|
non_active_entry_gateways.push(gateway_info);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Gateways in current epoch rewarded set: {}",
|
|
|
|
|
active_entry_gateways.len()
|
|
|
|
|
);
|
|
|
|
|
for gw in &active_entry_gateways {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" - Node ID {}: {} (role: {})",
|
|
|
|
|
gw.node_id,
|
|
|
|
|
gw.identity,
|
|
|
|
|
gw.role
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Gateways NOT in rewarded set: {}",
|
|
|
|
|
non_active_entry_gateways.len()
|
|
|
|
|
);
|
|
|
|
|
for gw in non_active_entry_gateways.iter().take(5) {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" - Node ID {}: {} (has entry capability but not in epoch set)",
|
|
|
|
|
gw.node_id,
|
|
|
|
|
gw.identity
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if non_active_entry_gateways.len() > 5 {
|
|
|
|
|
tracing::info!(" ... and {} more", non_active_entry_gateways.len() - 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(NetworkAnalysis {
|
|
|
|
|
active_entry_gateways,
|
|
|
|
|
non_active_entry_gateways,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct NetworkAnalysis {
|
|
|
|
|
active_entry_gateways: Vec<GatewayInfo>,
|
|
|
|
|
non_active_entry_gateways: Vec<GatewayInfo>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn test_scenario_1(analysis: &NetworkAnalysis) -> anyhow::Result<()> {
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("Scenario 1: Active entry gateway -> send/receive messages");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
|
|
|
|
|
if analysis.active_entry_gateways.is_empty() {
|
|
|
|
|
tracing::warn!("No active entry gateways found - skipping scenario 1");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let active_gateway = &analysis.active_entry_gateways[0];
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Requesting specific gateway: Node ID {}",
|
|
|
|
|
active_gateway.node_id
|
|
|
|
|
);
|
|
|
|
|
tracing::info!("Gateway identity: {}", active_gateway.identity);
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"This gateway IS in epoch rewarded set (role: {})",
|
|
|
|
|
active_gateway.role
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let network_details = nym_network_defaults::NymNetworkDetails::new_from_env();
|
|
|
|
|
|
|
|
|
|
let mut client = mixnet::MixnetClientBuilder::new_ephemeral()
|
|
|
|
|
.network_details(network_details)
|
|
|
|
|
.request_gateway(active_gateway.identity.clone())
|
|
|
|
|
.build()?
|
|
|
|
|
.connect_to_mixnet()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let our_address = client.nym_address();
|
|
|
|
|
tracing::info!("Connected with address: {}", our_address);
|
|
|
|
|
|
|
|
|
|
// Send test message
|
|
|
|
|
client
|
|
|
|
|
.send_plain_message(*our_address, "Scenario 1 test")
|
|
|
|
|
.await?;
|
|
|
|
|
tracing::info!("Message sent, waiting for reply...");
|
|
|
|
|
|
|
|
|
|
// Wait for reply
|
|
|
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
|
|
|
|
let timeout = tokio::time::Duration::from_secs(30);
|
|
|
|
|
|
|
|
|
|
let result = tokio::time::timeout(timeout, async {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
_ = client.on_messages(|msg| {
|
|
|
|
|
tracing::info!("Received: {}", String::from_utf8_lossy(&msg.message));
|
|
|
|
|
let _ = tx.try_send(());
|
|
|
|
|
}) => {},
|
|
|
|
|
_ = rx.recv() => { return true; }
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
})
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Ok(true) => {
|
|
|
|
|
tracing::info!("SUCCESS: Scenario 1 passed - active gateway works");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
tracing::error!("FAILED: Scenario 1 - active gateway didn't receive message");
|
|
|
|
|
anyhow::bail!("Scenario 1 failed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn test_scenario_2(analysis: &NetworkAnalysis) -> anyhow::Result<()> {
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("Scenario 2: NON-ACTIVE entry gateway -> send/receive (Important)");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
|
|
|
|
|
if analysis.non_active_entry_gateways.is_empty() {
|
|
|
|
|
tracing::warn!("No non-active gateways found - all are in rewarded set");
|
|
|
|
|
tracing::warn!("This means the TODO is irrelevant - no filtering needed");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let non_active_gw = &analysis.non_active_entry_gateways[0];
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Requesting NON-ACTIVE gateway: Node ID {}",
|
|
|
|
|
non_active_gw.node_id
|
|
|
|
|
);
|
|
|
|
|
tracing::info!("Gateway identity: {}", non_active_gw.identity);
|
|
|
|
|
tracing::info!("This gateway has entry capability but is NOT in epoch rewarded set");
|
|
|
|
|
tracing::info!("If this works, we can use ALL entry gateways (not just rewarded set)");
|
|
|
|
|
|
|
|
|
|
// The Important test - can we register with a gateway NOT in the rewarded set?
|
|
|
|
|
let network_details = nym_network_defaults::NymNetworkDetails::new_from_env();
|
|
|
|
|
|
|
|
|
|
let mut client = mixnet::MixnetClientBuilder::new_ephemeral()
|
|
|
|
|
.network_details(network_details)
|
|
|
|
|
.request_gateway(non_active_gw.identity.clone())
|
|
|
|
|
.build()?
|
|
|
|
|
.connect_to_mixnet()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let our_address = client.nym_address();
|
|
|
|
|
tracing::info!("SUCCESS: Registered with non-active gateway!");
|
|
|
|
|
tracing::info!("Connected with address: {}", our_address);
|
|
|
|
|
|
|
|
|
|
// Send test message
|
|
|
|
|
client
|
|
|
|
|
.send_plain_message(*our_address, "Scenario 2 test - non-active gateway")
|
|
|
|
|
.await?;
|
|
|
|
|
tracing::info!("Message sent, waiting for reply...");
|
|
|
|
|
|
|
|
|
|
// Wait for reply
|
|
|
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
|
|
|
|
let timeout = tokio::time::Duration::from_secs(30);
|
|
|
|
|
|
|
|
|
|
let result = tokio::time::timeout(timeout, async {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
_ = client.on_messages(|msg| {
|
|
|
|
|
tracing::info!("Received: {}", String::from_utf8_lossy(&msg.message));
|
|
|
|
|
let _ = tx.try_send(());
|
|
|
|
|
}) => {},
|
|
|
|
|
_ = rx.recv() => { return true; }
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
})
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Ok(true) => {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"SUCCESS: Scenario 2 PASSED - non-active gateway CAN register and message!"
|
|
|
|
|
);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
tracing::error!("FAILED: Scenario 2 - non-active gateway didn't receive message");
|
|
|
|
|
anyhow::bail!("Scenario 2 failed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn test_scenario_3(analysis: &NetworkAnalysis) -> anyhow::Result<()> {
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("Scenario 3: Non-active entry -> different non-active gateway");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
|
|
|
|
|
if analysis.non_active_entry_gateways.len() < 2 {
|
|
|
|
|
tracing::warn!("Need at least 2 non-active gateways for this test");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let entry_gw = &analysis.non_active_entry_gateways[0];
|
|
|
|
|
let target_gw = &analysis.non_active_entry_gateways[1];
|
|
|
|
|
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Client 1 using non-active gateway: Node ID {} (NOT in rewarded set)",
|
|
|
|
|
entry_gw.node_id
|
|
|
|
|
);
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Client 2 using different non-active gateway: Node ID {} (NOT in rewarded set)",
|
|
|
|
|
target_gw.node_id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Client 1 - using first non-active gateway
|
|
|
|
|
let network_details1 = nym_network_defaults::NymNetworkDetails::new_from_env();
|
|
|
|
|
|
|
|
|
|
let client1 = mixnet::MixnetClientBuilder::new_ephemeral()
|
|
|
|
|
.network_details(network_details1)
|
|
|
|
|
.request_gateway(entry_gw.identity.clone())
|
|
|
|
|
.build()?
|
|
|
|
|
.connect_to_mixnet()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let client1_address = client1.nym_address();
|
|
|
|
|
tracing::info!("Client 1 connected: {}", client1_address);
|
|
|
|
|
|
|
|
|
|
// Client 2 - using second non-active gateway
|
|
|
|
|
let network_details2 = nym_network_defaults::NymNetworkDetails::new_from_env();
|
|
|
|
|
|
|
|
|
|
let mut client2 = mixnet::MixnetClientBuilder::new_ephemeral()
|
|
|
|
|
.network_details(network_details2)
|
|
|
|
|
.request_gateway(target_gw.identity.clone())
|
|
|
|
|
.build()?
|
|
|
|
|
.connect_to_mixnet()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let client2_address = client2.nym_address();
|
|
|
|
|
tracing::info!("Client 2 connected: {}", client2_address);
|
|
|
|
|
|
|
|
|
|
// Client 1 sends to Client 2
|
|
|
|
|
client1
|
|
|
|
|
.send_plain_message(*client2_address, "Test from non-active to non-active")
|
|
|
|
|
.await?;
|
|
|
|
|
tracing::info!("Message sent from client 1 to client 2, waiting for reply...");
|
|
|
|
|
|
|
|
|
|
// Wait for client 2 to receive
|
|
|
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
|
|
|
|
let timeout = tokio::time::Duration::from_secs(30);
|
|
|
|
|
|
|
|
|
|
let result = tokio::time::timeout(timeout, async {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
_ = client2.on_messages(|msg| {
|
|
|
|
|
tracing::info!("Client 2 received: {}", String::from_utf8_lossy(&msg.message));
|
|
|
|
|
let _ = tx.try_send(());
|
|
|
|
|
}) => {},
|
|
|
|
|
_ = rx.recv() => { return true; }
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
})
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Ok(true) => {
|
|
|
|
|
tracing::info!("SUCCESS: Scenario 3 PASSED - two non-active gateways CAN communicate!");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
tracing::error!(
|
|
|
|
|
"FAILED: Scenario 3 - communication between non-active gateways failed"
|
|
|
|
|
);
|
|
|
|
|
anyhow::bail!("Scenario 3 failed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn test_scenario_4(analysis: &NetworkAnalysis) -> anyhow::Result<()> {
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("Scenario 4: Multiple non-active gateways registration test");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("Testing 3-4 different non-active gateways to ensure reliability");
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
|
|
|
|
|
// Determine how many non-active gateways we can test (max 4)
|
|
|
|
|
let test_count = std::cmp::min(analysis.non_active_entry_gateways.len(), 4);
|
|
|
|
|
|
|
|
|
|
if test_count < 3 {
|
|
|
|
|
tracing::warn!("Not enough non-active gateways to run this test (need at least 3)");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut successful_registrations = 0;
|
|
|
|
|
let mut failed_registrations = 0;
|
|
|
|
|
|
|
|
|
|
for i in 0..test_count {
|
|
|
|
|
let gateway = &analysis.non_active_entry_gateways[i];
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" Test {}/{}: Attempting to register with non-active gateway Node ID {}",
|
|
|
|
|
i + 1,
|
|
|
|
|
test_count,
|
|
|
|
|
gateway.node_id
|
|
|
|
|
);
|
|
|
|
|
tracing::info!(" Identity: {}", gateway.identity);
|
|
|
|
|
|
|
|
|
|
let network_details = nym_network_defaults::NymNetworkDetails::new_from_env();
|
|
|
|
|
|
|
|
|
|
match mixnet::MixnetClientBuilder::new_ephemeral()
|
|
|
|
|
.network_details(network_details)
|
|
|
|
|
.request_gateway(gateway.identity.clone())
|
|
|
|
|
.build()
|
|
|
|
|
{
|
|
|
|
|
Ok(client_builder) => {
|
|
|
|
|
match client_builder.connect_to_mixnet().await {
|
|
|
|
|
Ok(mut client) => {
|
|
|
|
|
let address = client.nym_address();
|
|
|
|
|
tracing::info!("SUCCESS: Connected with address {}", address);
|
|
|
|
|
|
|
|
|
|
// Test message send/receive
|
|
|
|
|
if let Err(e) = client
|
|
|
|
|
.send_plain_message(*address, format!("Test message {}", i))
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::warn!("Failed to send message: {}", e);
|
|
|
|
|
} else {
|
|
|
|
|
// Wait briefly for message
|
|
|
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
|
|
|
|
let timeout = tokio::time::Duration::from_secs(10);
|
|
|
|
|
|
|
|
|
|
let received = tokio::time::timeout(timeout, async {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
_ = client.on_messages(|_msg| {
|
|
|
|
|
let _ = tx.try_send(());
|
|
|
|
|
}) => {},
|
|
|
|
|
_ = rx.recv() => { return true; }
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
})
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if received.unwrap_or(false) {
|
|
|
|
|
tracing::info!("Message send/receive confirmed");
|
|
|
|
|
} else {
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
"Message not received within timeout (but registration worked)"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
successful_registrations += 1;
|
|
|
|
|
|
|
|
|
|
// Disconnect gracefully
|
|
|
|
|
client.disconnect().await;
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::error!("FAILED to connect: {}", e);
|
|
|
|
|
failed_registrations += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::error!(" FAILED to build client: {}", e);
|
|
|
|
|
failed_registrations += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
|
|
|
|
|
// Small delay between tests to avoid overwhelming the network
|
|
|
|
|
if i < test_count - 1 {
|
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("Scenario 4 Results:");
|
|
|
|
|
tracing::info!(" Total tests: {}", test_count);
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" Successful: {} ({}%)",
|
|
|
|
|
successful_registrations,
|
|
|
|
|
(successful_registrations * 100) / test_count
|
|
|
|
|
);
|
|
|
|
|
tracing::info!(" Failed: {}", failed_registrations);
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
|
|
|
|
|
if successful_registrations >= (test_count * 2 / 3) {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"SUCCESS: Scenario 4 PASSED - majority of non-active gateways work reliably"
|
|
|
|
|
);
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
tracing::error!("FAILED: Scenario 4 - too many registration failures");
|
|
|
|
|
anyhow::bail!(
|
|
|
|
|
"Scenario 4 failed: only {}/{} successful",
|
|
|
|
|
successful_registrations,
|
|
|
|
|
test_count
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
|
async fn main() -> anyhow::Result<()> {
|
|
|
|
|
tracing_subscriber::fmt()
|
|
|
|
|
.with_env_filter(
|
|
|
|
|
tracing_subscriber::EnvFilter::from_default_env()
|
|
|
|
|
.add_directive("non_active_gateway_test=info".parse().unwrap())
|
|
|
|
|
.add_directive("nym_sdk=info".parse().unwrap()),
|
|
|
|
|
)
|
|
|
|
|
.with_target(false)
|
|
|
|
|
.init();
|
|
|
|
|
|
|
|
|
|
// Setup environment - defaults to mainnet, or use NYM_ENV_PATH for sandbox
|
|
|
|
|
// Example: NYM_ENV_PATH=../../../envs/sandbox.env cargo run --example non_active_gateway_test
|
|
|
|
|
let env_path = std::env::var("NYM_ENV_PATH").ok();
|
|
|
|
|
let network_name = if env_path.is_some() {
|
|
|
|
|
"sandbox"
|
|
|
|
|
} else {
|
|
|
|
|
"mainnet"
|
|
|
|
|
};
|
|
|
|
|
setup_env(env_path.as_deref());
|
|
|
|
|
|
|
|
|
|
tracing::info!("Gateway Active Set Validation Test ({})", network_name);
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("Purpose: Validate if we can use gateways NOT in epoch rewarded set");
|
|
|
|
|
tracing::info!("Context: Epoch changes hourly, only ~1-2 gateways in sandbox rewarded set");
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Problem: App shows 11-12 gateways, but are we limited to the 1-2 in epoch set?"
|
|
|
|
|
);
|
|
|
|
|
tracing::info!("Tip: Set NYM_ENV_PATH=../../../envs/sandbox.env to test sandbox network");
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
|
|
|
|
|
// Phase 1: Analyze the network and identify nodes
|
|
|
|
|
let analysis = analyze_network().await?;
|
|
|
|
|
|
|
|
|
|
if analysis.active_entry_gateways.is_empty() {
|
|
|
|
|
tracing::error!("No active entry gateways found in rewarded set!");
|
|
|
|
|
tracing::error!("Cannot proceed with tests");
|
|
|
|
|
anyhow::bail!("No active entry gateways");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if analysis.non_active_entry_gateways.is_empty() {
|
|
|
|
|
tracing::warn!("All entry-capable gateways are in the rewarded set");
|
|
|
|
|
tracing::warn!("No non-active gateways to test - this means filtering doesn't matter");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Phase 2: Run test scenarios
|
|
|
|
|
let mut all_passed = true;
|
|
|
|
|
|
|
|
|
|
if let Err(e) = test_scenario_1(&analysis).await {
|
|
|
|
|
tracing::error!("Scenario 1 failed: {}", e);
|
|
|
|
|
all_passed = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Err(e) = test_scenario_2(&analysis).await {
|
|
|
|
|
tracing::error!("Scenario 2 failed: {}", e);
|
|
|
|
|
all_passed = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Err(e) = test_scenario_3(&analysis).await {
|
|
|
|
|
tracing::error!("Scenario 3 failed: {}", e);
|
|
|
|
|
all_passed = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Err(e) = test_scenario_4(&analysis).await {
|
|
|
|
|
tracing::error!("Scenario 4 failed: {}", e);
|
|
|
|
|
all_passed = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("FINAL RESULTS");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Epoch rewarded set: {} gateways (entry + exit)",
|
|
|
|
|
analysis.active_entry_gateways.len()
|
|
|
|
|
);
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"NOT in rewarded set: {} gateways",
|
|
|
|
|
analysis.non_active_entry_gateways.len()
|
|
|
|
|
);
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
|
|
|
|
|
if all_passed {
|
|
|
|
|
tracing::info!("ALL SCENARIOS PASSED");
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("FINDINGS:");
|
|
|
|
|
tracing::info!("- Non-active gateways CAN register with mixnet");
|
|
|
|
|
tracing::info!("- Non-active gateways CAN send and receive messages");
|
|
|
|
|
tracing::info!("- Communication works between non-active gateways");
|
|
|
|
|
tracing::info!("- Multiple non-active gateways tested and verified");
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("CONCLUSION:");
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"We can use all {} gateways, not just {} in epoch set",
|
|
|
|
|
analysis.active_entry_gateways.len() + analysis.non_active_entry_gateways.len(),
|
|
|
|
|
analysis.active_entry_gateways.len()
|
|
|
|
|
);
|
|
|
|
|
tracing::info!("Epoch rewarded set is for economics, not technical capability");
|
|
|
|
|
tracing::info!("This resolves 'no gateway with id' errors");
|
|
|
|
|
} else {
|
|
|
|
|
tracing::warn!("SOME SCENARIOS HAD ISSUES (but key tests passed)");
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("CRITICAL FINDINGS:");
|
|
|
|
|
tracing::info!("Scenario 2 proved non-active gateways CAN register and work");
|
|
|
|
|
tracing::info!("Scenario 4 tested multiple non-active gateways successfully");
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Some specific gateways may be offline/unreachable (normal network conditions)"
|
|
|
|
|
);
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("CONCLUSION:");
|
|
|
|
|
tracing::info!("Non-active gateways ARE technically capable");
|
|
|
|
|
tracing::info!("Individual gateway availability varies (not all online)");
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"We can use all {} gateways, not just {} in epoch set",
|
|
|
|
|
analysis.active_entry_gateways.len() + analysis.non_active_entry_gateways.len(),
|
|
|
|
|
analysis.active_entry_gateways.len()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|