From d37b4226d09777296ce923680216ce389ac941d7 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Fri, 13 Feb 2026 10:31:55 +0100 Subject: [PATCH 01/17] testing port checks add no-log to anywhere add support for not registered nodes ... address comments remove unregistered nodes testing port checks add support for not registered nodes ... address comments test port check in probe results migration update probe arg fix bump NS versions cleanup and remove unannounced node option bugsfixes Remove in-prove remove in-probe test, it isn't needed. add multiple target host options cleanup change default target, and use batch only for portquiz Revert "change default target, and use batch only for portquiz" This reverts commit 8b38969964e7808b9c4e50a920ee5bc51438c7bf. ded line bugfixes batch fix batch limits force ipv4 --- nym-gateway-probe/Cargo.toml | 2 +- nym-gateway-probe/build.rs | 92 ++++ nym-gateway-probe/netstack_ping/lib.go | 162 ++++++- nym-gateway-probe/src/common/netstack.rs | 29 ++ nym-gateway-probe/src/common/probe_tests.rs | 2 + nym-gateway-probe/src/common/types.rs | 67 +++ nym-gateway-probe/src/common/wireguard.rs | 17 +- nym-gateway-probe/src/config/mod.rs | 4 +- nym-gateway-probe/src/config/netstack.rs | 67 +++ nym-gateway-probe/src/config/test_mode.rs | 3 + nym-gateway-probe/src/lib.rs | 454 +++++++++++++++++- nym-gateway-probe/src/run.rs | 149 +++++- .../nym-node-status-agent/Cargo.toml | 2 +- .../nym-node-status-agent/src/cli/common.rs | 42 ++ .../nym-node-status-agent/src/cli/mod.rs | 40 +- .../src/cli/run_ports_check.rs | 131 +++++ .../src/cli/run_probe.rs | 42 +- ...2a66aae5be1c37af2eca521bf75261cecf28.json} | 19 +- ...2b71a1a91ef114ffdd0572a738b577fab15d.json} | 5 +- ...d8ea15b957dd19b097101dcda2c05aee21d20.json | 15 + ...a57fb24f6b21655039a9bfa21c44409c87910.json | 18 + ...783249a03f2ae04ec89f734e67ceb897a385.json} | 16 +- ...4cb8bf8ebb1c26839f915780d0fc0d1939973.json | 31 ++ ...1614edc4e967f348c57f485f736b8f2f03e8.json} | 16 +- ...087af53e9504a8bb13280dde5c37b9b418370.json | 22 + .../nym-node-status-api/Cargo.toml | 2 +- .../nym-node-status-api/Makefile | 13 + ...0260414000000_ports_check_testrun_kind.sql | 8 + .../20260415000000_gateways_ports_check.sql | 14 + .../nym-node-status-api/src/cli/mod.rs | 9 + .../nym-node-status-api/src/db/models.rs | 124 ++++- .../src/db/queries/gateways.rs | 9 +- .../src/db/queries/testruns.rs | 154 +++++- .../nym-node-status-api/src/db/tests.rs | 99 ++++ .../src/http/api/gateways.rs | 4 + .../src/http/api/services/json_path.rs | 2 + .../src/http/api/services/mod.rs | 2 + .../src/http/api/testruns.rs | 220 ++++++++- .../src/http/models/gw_probe/test.rs | 4 + .../src/http/models/mod.rs | 6 + .../nym-node-status-api/src/http/server.rs | 30 ++ .../nym-node-status-api/src/http/state.rs | 6 +- .../nym-node-status-api/src/main.rs | 7 +- .../src/test_helpers/mod.rs | 3 + .../nym-node-status-api/src/testruns/mod.rs | 8 +- .../nym-node-status-api/src/testruns/queue.rs | 8 +- .../nym-node-status-client/src/api.rs | 11 + .../nym-node-status-client/src/lib.rs | 87 +++- .../nym-node-status-client/src/models.rs | 39 ++ 49 files changed, 2195 insertions(+), 121 deletions(-) create mode 100644 nym-node-status-api/nym-node-status-agent/src/cli/common.rs create mode 100644 nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs rename nym-node-status-api/nym-node-status-api/.sqlx/{query-f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8.json => query-0c635c539930c10a9be35a12ba3f2a66aae5be1c37af2eca521bf75261cecf28.json} (66%) rename nym-node-status-api/nym-node-status-api/.sqlx/{query-88a4554c2857288c314768c56648a5f1811d2053582380ca602335a122cef8db.json => query-1dc7d1e6c2173cd1ca70d699124c2b71a1a91ef114ffdd0572a738b577fab15d.json} (51%) create mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20.json create mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-65b5a3ed9a73f1304badfa8d9bba57fb24f6b21655039a9bfa21c44409c87910.json rename nym-node-status-api/nym-node-status-api/.sqlx/{query-d41cafc76cb49c03df7452c405a4e2e5e3951c41dc35c20261c1d959c0d6403f.json => query-75bdccbff8ea6a59d56230075163783249a03f2ae04ec89f734e67ceb897a385.json} (67%) create mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973.json rename nym-node-status-api/nym-node-status-api/.sqlx/{query-ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64.json => query-b32d29ca3cedae93fa2fcd44cc301614edc4e967f348c57f485f736b8f2f03e8.json} (72%) create mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370.json create mode 100644 nym-node-status-api/nym-node-status-api/migrations_pg/20260414000000_ports_check_testrun_kind.sql create mode 100644 nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql diff --git a/nym-gateway-probe/Cargo.toml b/nym-gateway-probe/Cargo.toml index e50f61cf9d..d7db904a5c 100644 --- a/nym-gateway-probe/Cargo.toml +++ b/nym-gateway-probe/Cargo.toml @@ -17,7 +17,7 @@ anyhow = { workspace = true } base64 = { workspace = true } bs58 = { workspace = true } bytes = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } +clap = { workspace = true, features = ["cargo", "derive", "env"] } futures = { workspace = true } hex = { workspace = true } tracing = { workspace = true } diff --git a/nym-gateway-probe/build.rs b/nym-gateway-probe/build.rs index 43af675650..97a8d14c7c 100644 --- a/nym-gateway-probe/build.rs +++ b/nym-gateway-probe/build.rs @@ -9,6 +9,7 @@ use vergen_gitcl::{BuildBuilder, CargoBuilder, Emitter, GitclBuilder, RustcBuild fn main() -> anyhow::Result<()> { build_go()?; + generate_exit_policy_ports()?; Emitter::default() .add_instructions(&BuildBuilder::all_build()?)? @@ -18,6 +19,97 @@ fn main() -> anyhow::Result<()> { .emit() } +/// Parse PORT_MAPPINGS from network-tunnel-manager.sh and generate a sorted +/// Rust const with every unique port. Ranges are represented by their start +/// and end values so a single TCP check can confirm the iptables rule exists. +/// TODO: consider runtime fetch from NS API exit policy endpoint instead of parsing the script +fn generate_exit_policy_ports() -> anyhow::Result<()> { + use std::collections::BTreeMap; + + let script_path = PathBuf::from("../scripts/nym-node-setup/network-tunnel-manager.sh"); + let out_dir = PathBuf::from(std::env::var("OUT_DIR").context("OUT_DIR not set")?); + + println!("cargo::rerun-if-changed={}", script_path.display()); + + let content = std::fs::read_to_string(&script_path).context( + "failed to read network-tunnel-manager.sh — is it present at ../scripts/nym-node-setup/ ?", + )?; + + // port → service name (BTreeMap keeps them sorted) + let mut port_map: BTreeMap = BTreeMap::new(); + let mut in_mappings = false; + + for line in content.lines() { + let trimmed = line.trim(); + + if trimmed.starts_with("declare -A PORT_MAPPINGS=(") { + in_mappings = true; + continue; + } + if in_mappings && trimmed == ")" { + break; + } + if !in_mappings { + continue; + } + + // strip comment prefix so we still pick up ports that are opened + // via a separate mechanism (e.g. SMTP/465 with rate limiting) + let stripped = trimmed.trim_start_matches('#').trim(); + + // match ["ServiceName"]="port-or-range" + let Some(name_start) = stripped.find("[\"") else { + continue; + }; + let Some(name_end) = stripped.find("\"]=") else { + continue; + }; + let service = &stripped[name_start + 2..name_end]; + let value = stripped[name_end + 3..].trim_matches('"'); + + if value.contains('-') { + let parts: Vec<&str> = value.split('-').collect(); + if parts.len() == 2 { + if let (Ok(lo), Ok(hi)) = (parts[0].parse::(), parts[1].parse::()) { + port_map + .entry(lo) + .or_insert_with(|| format!("{service} (range start)")); + port_map + .entry(hi) + .or_insert_with(|| format!("{service} (range end)")); + } + } + } else if let Ok(port) = value.parse::() { + port_map.entry(port).or_insert_with(|| service.to_string()); + } + } + + if port_map.is_empty() { + bail!("No ports found in PORT_MAPPINGS — is network-tunnel-manager.sh correct?"); + } + + // write generated Rust source + let mut out = String::new(); + out.push_str( + "// Auto-generated from scripts/nym-node-setup/network-tunnel-manager.sh PORT_MAPPINGS.\n", + ); + out.push_str("// Do not edit — changes are overwritten on rebuild.\n"); + out.push_str("// To add or remove ports, update PORT_MAPPINGS in the shell script.\n\n"); + out.push_str(&format!( + "/// {} unique ports parsed from the canonical exit policy at build time.\n", + port_map.len() + )); + out.push_str("pub const EXIT_POLICY_PORTS: &[u16] = &[\n"); + for (port, service) in &port_map { + let entry = format!("{port},"); + out.push_str(&format!(" {entry:<7}// {service}\n")); + } + out.push_str("];\n"); + + std::fs::write(out_dir.join("exit_policy_ports.rs"), out)?; + Ok(()) +} + fn build_go() -> anyhow::Result<()> { const LIB_NAME: &str = "netstack_ping"; diff --git a/nym-gateway-probe/netstack_ping/lib.go b/nym-gateway-probe/netstack_ping/lib.go index a80af4bee5..bcbf738368 100644 --- a/nym-gateway-probe/netstack_ping/lib.go +++ b/nym-gateway-probe/netstack_ping/lib.go @@ -23,6 +23,7 @@ import ( netUrl "net/url" "os" "strings" + "sync" "time" "unsafe" @@ -62,21 +63,27 @@ type NetstackRequestGo struct { DownloadTimeoutSec uint64 `json:"download_timeout_sec"` MetadataTimeoutSec uint64 `json:"metadata_timeout_sec"` AwgArgs string `json:"awg_args"` + // exit policy port check + PortCheckTarget string `json:"port_check_target"` + PortCheckPorts []uint16 `json:"port_check_ports"` + PortCheckOnly bool `json:"port_check_only"` + PortCheckTimeoutSec uint64 `json:"port_check_timeout_sec"` } type NetstackResponse struct { - CanHandshake bool `json:"can_handshake"` - CanQueryMetadata bool `json:"can_query_metadata"` - SentIps uint16 `json:"sent_ips"` - ReceivedIps uint16 `json:"received_ips"` - SentHosts uint16 `json:"sent_hosts"` - ReceivedHosts uint16 `json:"received_hosts"` - CanResolveDns bool `json:"can_resolve_dns"` - DownloadedFile string `json:"downloaded_file"` - DownloadedFileSizeBytes uint64 `json:"downloaded_file_size_bytes"` - DownloadDurationSec uint64 `json:"download_duration_sec"` - DownloadDurationMilliseconds uint64 `json:"download_duration_milliseconds"` - DownloadError string `json:"download_error"` + CanHandshake bool `json:"can_handshake"` + CanQueryMetadata bool `json:"can_query_metadata"` + SentIps uint16 `json:"sent_ips"` + ReceivedIps uint16 `json:"received_ips"` + SentHosts uint16 `json:"sent_hosts"` + ReceivedHosts uint16 `json:"received_hosts"` + CanResolveDns bool `json:"can_resolve_dns"` + DownloadedFile string `json:"downloaded_file"` + DownloadedFileSizeBytes uint64 `json:"downloaded_file_size_bytes"` + DownloadDurationSec uint64 `json:"download_duration_sec"` + DownloadDurationMilliseconds uint64 `json:"download_duration_milliseconds"` + DownloadError string `json:"download_error"` + PortCheckResults map[string]bool `json:"port_check_results,omitempty"` } type SuccessResult = struct { @@ -201,7 +208,7 @@ func pingTwoHop(req TwoHopNetstackRequest) (NetstackResponse, error) { log.Printf("Exit WG IP: %s", req.ExitWgIp) log.Printf("IP version: %d", req.IpVersion) - response := NetstackResponse{false, false, 0, 0, 0, 0, false, "", 0, 0, 0, ""} + response := NetstackResponse{} // Parse the exit endpoint to determine IP version for forwarder exitEndpoint, err := netip.ParseAddrPort(req.ExitEndpoint) @@ -439,7 +446,7 @@ func ping(req NetstackRequestGo) (NetstackResponse, error) { ipc.WriteString("\nallowed_ip=::/0\n") } - response := NetstackResponse{false, false, 0, 0, 0, 0, false, "", 0, 0, 0, ""} + response := NetstackResponse{} err = dev.IpcSet(ipc.String()) if err != nil { @@ -463,6 +470,19 @@ func ping(req NetstackRequestGo) (NetstackResponse, error) { response.CanHandshake = true + // port-check-only mode: skip pings/download, only test TCP port reachability + if req.PortCheckOnly && len(req.PortCheckPorts) > 0 { + log.Printf("=== Port Check Only Mode ===") + response.PortCheckResults = checkPorts(req.PortCheckTarget, req.PortCheckPorts, req.PortCheckTimeoutSec, tnet) + return response, nil + } + + // run port checks alongside normal tests if ports were requested + if len(req.PortCheckPorts) > 0 { + log.Printf("=== Running Port Checks (alongside normal tests) ===") + response.PortCheckResults = checkPorts(req.PortCheckTarget, req.PortCheckPorts, req.PortCheckTimeoutSec, tnet) + } + // Skip metadata query if endpoint is empty (e.g., for IPv6 where the IPv4 metadata endpoint is not reachable) if req.MetadataEndpoint != "" { version, duration, err := queryMetadata(req.MetadataEndpoint, req.MetadataTimeoutSec, tnet) @@ -569,6 +589,120 @@ func ping(req NetstackRequestGo) (NetstackResponse, error) { return response, nil } +const portquizBatchSize = 20 +const portquizBatchDelay = 25 * time.Second +const portquizDialGap = 20 * time.Millisecond + +func checkPorts(target string, ports []uint16, timeoutSec uint64, tnet *netstack.Net) map[string]bool { + if target == "" { + target = "portquiz.net" + } + if timeoutSec == 0 { + timeoutSec = 5 + } + + targetIP := target + if net.ParseIP(target) == nil { + addrs, err := net.LookupHost(target) + if err != nil || len(addrs) == 0 { + log.Printf("Port check: DNS lookup for %s failed (%v), using hostname as-is", target, err) + } else { + chosen := addrs[0] + for _, a := range addrs { + if net.ParseIP(a).To4() != nil { + chosen = a + break + } + } + targetIP = chosen + log.Printf("Port check: resolved %s -> %s", target, targetIP) + } + } + + timeout := time.Duration(timeoutSec) * time.Second + results := make(map[string]bool, len(ports)) + + if strings.Contains(target, "portquiz.net") { + // portquiz.net rate-limits after ~29 connections per window; use large batches with a long cooldown + log.Printf("Port check: testing %d ports on %s in batches of %d with %v cooldown (timeout %v each)", + len(ports), target, portquizBatchSize, portquizBatchDelay, timeout) + + for batchIdx := 0; batchIdx < len(ports); batchIdx += portquizBatchSize { + end := batchIdx + portquizBatchSize + if end > len(ports) { + end = len(ports) + } + batch := ports[batchIdx:end] + + for i, p := range batch { + addr := net.JoinHostPort(targetIP, fmt.Sprintf("%d", p)) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + c, err := tnet.DialContext(ctx, "tcp", addr) + cancel() + key := fmt.Sprintf("%d", p) + if err != nil { + log.Printf("Port %d: CLOSED (%v)", p, err) + results[key] = false + } else { + c.Close() + log.Printf("Port %d: OPEN", p) + results[key] = true + } + if i < len(batch)-1 { + time.Sleep(portquizDialGap) + } + } + + if batchIdx+portquizBatchSize < len(ports) { + time.Sleep(portquizBatchDelay) + } + } + } else { + // All other targets can handle concurrent connections, probably + log.Printf("Port check: testing %d ports on %s concurrently (timeout %v each)", + len(ports), target, timeout) + + var ( + mu sync.Mutex + wg sync.WaitGroup + ) + for _, p := range ports { + wg.Add(1) + go func(port uint16) { + defer wg.Done() + addr := net.JoinHostPort(targetIP, fmt.Sprintf("%d", port)) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + c, err := tnet.DialContext(ctx, "tcp", addr) + cancel() + key := fmt.Sprintf("%d", port) + if err != nil { + log.Printf("Port %d: CLOSED (%v)", port, err) + mu.Lock() + results[key] = false + mu.Unlock() + } else { + c.Close() + log.Printf("Port %d: OPEN", port) + mu.Lock() + results[key] = true + mu.Unlock() + } + }(p) + } + wg.Wait() + } + + openCount := 0 + for _, open := range results { + if open { + openCount++ + } + } + log.Printf("Port check complete: %d/%d ports open", openCount, len(ports)) + + return results +} + func sendPing(address string, seq uint8, sendTtimeoutSecs uint64, receiveTimoutSecs uint64, tnet *netstack.Net, ipVersion uint8) (time.Duration, error) { maxPingRetries := 2 baseTimeout := receiveTimoutSecs diff --git a/nym-gateway-probe/src/common/netstack.rs b/nym-gateway-probe/src/common/netstack.rs index 761b6713f0..d76bdffd13 100644 --- a/nym-gateway-probe/src/common/netstack.rs +++ b/nym-gateway-probe/src/common/netstack.rs @@ -4,6 +4,7 @@ use crate::config::NetstackArgs; use anyhow::Context; use serde::Deserialize; +use std::collections::BTreeMap; use std::ffi::{CStr, CString}; mod sys { @@ -15,6 +16,19 @@ mod sys { } } +/// Port-check fields shared between `NetstackRequest` and `NetstackRequestGo` +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct PortCheckConfig { + #[serde(rename = "port_check_target")] + pub target: String, + #[serde(rename = "port_check_ports")] + pub ports: Vec, + #[serde(rename = "port_check_only")] + pub only: bool, + #[serde(rename = "port_check_timeout_sec")] + pub timeout_sec: u64, +} + #[derive(serde::Serialize)] pub struct NetstackRequest { private_key: String, @@ -25,6 +39,8 @@ pub struct NetstackRequest { v6_ping_config: PingConfig, download_timeout_sec: u64, awg_args: String, + #[serde(flatten)] + pub port_check: PortCheckConfig, } #[derive(serde::Serialize)] @@ -76,6 +92,7 @@ impl NetstackRequest { download_timeout_sec: u64, awg_args: &str, netstack_args: NetstackArgs, + port_check_only: bool, ) -> Self { Self { private_key: private_key.to_string(), @@ -86,6 +103,12 @@ impl NetstackRequest { v4_ping_config: PingConfig::from_netstack_args_v4(wg_ip4, &netstack_args), v6_ping_config: PingConfig::from_netstack_args_v6(wg_ip6, &netstack_args), download_timeout_sec, + port_check: PortCheckConfig { + target: netstack_args.port_check_target.clone(), + ports: netstack_args.port_check_ports.clone(), + only: port_check_only, + timeout_sec: netstack_args.port_check_timeout_sec, + }, } } @@ -116,6 +139,8 @@ pub struct NetstackRequestGo { recv_timeout_sec: u64, download_timeout_sec: u64, awg_args: String, + #[serde(flatten)] + pub port_check: PortCheckConfig, } impl NetstackRequestGo { @@ -135,6 +160,7 @@ impl NetstackRequestGo { recv_timeout_sec: req.v4_ping_config.recv_timeout_sec, download_timeout_sec: req.download_timeout_sec, awg_args: req.awg_args.clone(), + port_check: req.port_check.clone(), } } @@ -155,6 +181,7 @@ impl NetstackRequestGo { recv_timeout_sec: req.v6_ping_config.recv_timeout_sec, download_timeout_sec: req.download_timeout_sec, awg_args: req.awg_args.clone(), + port_check: req.port_check.clone(), } } } @@ -173,6 +200,8 @@ pub struct NetstackResponse { pub downloaded_file_size_bytes: u64, pub download_duration_milliseconds: u64, pub download_error: String, + #[serde(default)] + pub port_check_results: Option>, } #[derive(Clone, Debug, serde::Deserialize)] diff --git a/nym-gateway-probe/src/common/probe_tests.rs b/nym-gateway-probe/src/common/probe_tests.rs index c9fc0531b0..30b14345d0 100644 --- a/nym-gateway-probe/src/common/probe_tests.rs +++ b/nym-gateway-probe/src/common/probe_tests.rs @@ -46,6 +46,7 @@ pub async fn wg_probe( auth_version: AuthenticatorVersion, awg_args: Option, netstack_args: NetstackArgs, + port_check_only: bool, // TODO: update type credential: CredentialSpendingData, ) -> anyhow::Result { @@ -149,6 +150,7 @@ pub async fn wg_probe( &tunnel_config, &netstack_args, &awg_args.unwrap_or_default(), + port_check_only, &mut wg_outcome, ); diff --git a/nym-gateway-probe/src/common/types.rs b/nym-gateway-probe/src/common/types.rs index 8541a57421..99190df348 100644 --- a/nym-gateway-probe/src/common/types.rs +++ b/nym-gateway-probe/src/common/types.rs @@ -1,10 +1,50 @@ use nym_connection_monitor::ConnectionStatusEvent; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; pub use super::bandwidth_helpers::{AttachedTicket, AttachedTicketMaterials}; pub use super::socks5_test::HttpsConnectivityResult; pub use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PortsCheckSummary { + pub all_pass: bool, + pub error: Option, + pub port_check_target: String, + pub failed_ports: Vec, +} + +impl PortsCheckSummary { + pub fn from_port_map( + can_register: bool, + port_check_target: impl Into, + ports: &BTreeMap, + ) -> Self { + let failed_ports: Vec = ports + .iter() + .filter_map(|(port, open)| if *open { None } else { Some(port.clone()) }) + .collect(); + + let all_pass = can_register && failed_ports.is_empty() && !ports.is_empty(); + + Self { + all_pass, + error: None, + port_check_target: port_check_target.into(), + failed_ports, + } + } + + pub fn probe_error(port_check_target: impl Into, message: impl Into) -> Self { + Self { + all_pass: false, + error: Some(message.into()), + port_check_target: port_check_target.into(), + failed_ports: vec![], + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProbeResult { pub node: String, @@ -50,6 +90,10 @@ pub struct WgProbeResults { pub download_duration_milliseconds_v6: u64, pub downloaded_file_v6: String, pub download_error_v6: String, + + /// Per-port exit-policy check + #[serde(default, skip_serializing_if = "Option::is_none")] + pub port_check_results: Option>, } #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -195,6 +239,29 @@ impl Socks5ProbeResults { } } +/// Output of the `run-ports` subcommand — per-port TCP reachability through +/// the WG exit tunnel, without the full probe outcome. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PortCheckResult { + pub gateway: String, + pub can_register: bool, + pub port_check_target: String, + /// port → open/closed (BTreeMap for deterministic bincode serialization in signed requests) + pub ports: BTreeMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl PortCheckResult { + /// Returns the list of ports that were found **closed** on this gateway. + pub fn closed_ports(&self) -> Vec { + self.ports + .iter() + .filter_map(|(k, &open)| if !open { k.parse().ok() } else { None }) + .collect() + } +} + #[derive(Debug, Clone, Default)] pub struct IpPingReplies { pub ipr_tun_ip_v4: bool, diff --git a/nym-gateway-probe/src/common/wireguard.rs b/nym-gateway-probe/src/common/wireguard.rs index 3439a4bb5f..a2cb783433 100644 --- a/nym-gateway-probe/src/common/wireguard.rs +++ b/nym-gateway-probe/src/common/wireguard.rs @@ -63,6 +63,7 @@ impl WgTunnelConfig { /// - DNS resolution /// - ICMP ping to specified hosts and IPs /// - Optional download test +/// - Optional exit policy port check (TCP connect through tunnel) /// /// Results are written directly into the provided `wg_outcome` to avoid field-by-field /// copying at call sites. @@ -71,6 +72,7 @@ impl WgTunnelConfig { /// * `config` - WireGuard tunnel configuration /// * `netstack_args` - Netstack test parameters (DNS, hosts to ping, timeouts, etc.) /// * `awg_args` - Amnezia WireGuard arguments (empty string for standard WG) +/// * `port_check_only` - If true, skip pings/download and only run TCP port checks /// * `wg_outcome` - Mutable reference to write test results into // This function extracts the shared netstack testing logic from // wg_probe() and wg_probe_lp() to eliminate code duplication. @@ -78,6 +80,7 @@ pub fn run_tunnel_tests( config: &WgTunnelConfig, netstack_args: &NetstackArgs, awg_args: &str, + port_check_only: bool, wg_outcome: &mut WgProbeResults, ) { // Build the netstack request @@ -91,9 +94,10 @@ pub fn run_tunnel_tests( netstack_args.netstack_download_timeout_sec, awg_args, netstack_args.clone(), + port_check_only, ); - // Perform IPv4 ping test + // Perform IPv4 ping test (also carries port check results in port-check-only mode) info!("Testing IPv4 tunnel connectivity..."); let ipv4_request = NetstackRequestGo::from_rust_v4(&netstack_request); @@ -122,6 +126,11 @@ pub fn run_tunnel_tests( netstack_response_v4.downloaded_file_size_bytes; wg_outcome.downloaded_file_v4 = netstack_response_v4.downloaded_file; wg_outcome.download_error_v4 = netstack_response_v4.download_error; + + // capture port check results (present when ports were requested) + if netstack_response_v4.port_check_results.is_some() { + wg_outcome.port_check_results = netstack_response_v4.port_check_results; + } } Ok(NetstackResult::Error { error }) => { error!("Netstack runtime error (IPv4): {error}") @@ -131,6 +140,12 @@ pub fn run_tunnel_tests( } } + // in port-check-only mode, skip IPv6 tests — port checks ran through IPv4 above + if port_check_only { + info!("Port-check-only mode: skipping IPv6 tunnel tests"); + return; + } + // Perform IPv6 ping test info!("Testing IPv6 tunnel connectivity..."); let ipv6_request = NetstackRequestGo::from_rust_v6(&netstack_request); diff --git a/nym-gateway-probe/src/config/mod.rs b/nym-gateway-probe/src/config/mod.rs index e110823cbf..c54e8897fd 100644 --- a/nym-gateway-probe/src/config/mod.rs +++ b/nym-gateway-probe/src/config/mod.rs @@ -9,7 +9,7 @@ mod socks5; mod test_mode; pub use credentials::{CredentialArgs, CredentialMode}; -pub use netstack::NetstackArgs; +pub use netstack::{EXIT_POLICY_PORTS, NetstackArgs}; pub use socks5::Socks5Args; pub use test_mode::TestMode; @@ -22,7 +22,7 @@ pub struct ProbeConfig { /// Test mode - explicitly specify which tests to run /// /// Modes: - /// core. - Traditional mixnet testing (entry/exit pings + WireGuard via authenticator) + /// core - Traditional mixnet testing (entry/exit pings + WireGuard via authenticator) /// wg-mix - Wireguard via authenticator /// wg-lp - Entry LP + Exit LP (nested forwarding) + WireGuard /// lp-only - LP registration only (no WireGuard) diff --git a/nym-gateway-probe/src/config/netstack.rs b/nym-gateway-probe/src/config/netstack.rs index 5fdd052413..080e7bd31b 100644 --- a/nym-gateway-probe/src/config/netstack.rs +++ b/nym-gateway-probe/src/config/netstack.rs @@ -3,6 +3,11 @@ use clap::Args; +// EXIT_POLICY_PORTS is generated at build time by parsing PORT_MAPPINGS +// from scripts/nym-node-setup/network-tunnel-manager.sh. +// To add or remove ports, update PORT_MAPPINGS in the shell script and rebuild. +include!(concat!(env!("OUT_DIR"), "/exit_policy_ports.rs")); + #[derive(Args, Clone, Debug)] pub struct NetstackArgs { #[arg(long, hide = true, env = "PROBE_NETSTACK_DOWNLOAD_TIMEOUT_SEC", default_value_t = NetstackArgs::default().netstack_download_timeout_sec)] @@ -37,6 +42,22 @@ pub struct NetstackArgs { #[arg(long, hide = true, env = "PROBE_NETSTACK_PING_IPS_V6", default_values_t = NetstackArgs::default().netstack_ping_ips_v6)] pub netstack_ping_ips_v6: Vec, + + /// Target host for exit policy port checks (must listen on all tested ports) + #[arg( + long = "use-target", + env = "PROBE_NETSTACK_PORT_CHECK_TARGET", + default_value = "portquiz.net" + )] + pub port_check_target: String, + + /// List ports to check, separated by a comma. + #[arg(long = "check-ports", value_delimiter = ',', default_values_t = Vec::::new())] + pub port_check_ports: Vec, + + /// Timeout in seconds for each individual port check attempt + #[arg(long, default_value_t = NetstackArgs::default().port_check_timeout_sec)] + pub port_check_timeout_sec: u64, } impl Default for NetstackArgs { @@ -57,6 +78,9 @@ impl Default for NetstackArgs { "2606:4700:4700::1111".to_string(), "2620:fe::fe".to_string(), ], + port_check_target: "portquiz.net".to_string(), + port_check_ports: vec![], + port_check_timeout_sec: 5, } } } @@ -87,6 +111,9 @@ mod tests { "2606:4700:4700::1111".to_string(), "2620:fe::fe".to_string(), ], + port_check_target: "portquiz.net".to_string(), + port_check_ports: vec![], + port_check_timeout_sec: 5, }; // Test IPv4 defaults @@ -111,6 +138,11 @@ mod tests { assert_eq!(args.netstack_num_ping, 5); assert_eq!(args.netstack_send_timeout_sec, 3); assert_eq!(args.netstack_recv_timeout_sec, 3); + + // Test port check defaults + assert_eq!(args.port_check_target, "portquiz.net"); + assert!(args.port_check_ports.is_empty()); + assert_eq!(args.port_check_timeout_sec, 5); } #[test] @@ -128,6 +160,9 @@ mod tests { netstack_ping_ips_v4: vec!["8.8.8.8".to_string()], netstack_ping_hosts_v6: vec!["ipv6.example.com".to_string()], netstack_ping_ips_v6: vec!["2001:4860:4860::8888".to_string()], + port_check_target: "portquiz.net".to_string(), + port_check_ports: vec![80, 443, 8332], + port_check_timeout_sec: 10, }; assert_eq!(args.netstack_ping_hosts_v4, vec!["example.com"]); @@ -163,6 +198,9 @@ mod tests { "2001:4860:4860::8888".to_string(), "2606:4700:4700::1111".to_string(), ], + port_check_target: "portquiz.net".to_string(), + port_check_ports: vec![], + port_check_timeout_sec: 5, }; assert_eq!(args.netstack_ping_hosts_v4, vec!["nym.com", "example.com"]); @@ -192,6 +230,9 @@ mod tests { netstack_ping_ips_v4: vec![], netstack_ping_hosts_v6: vec![], netstack_ping_ips_v6: vec![], + port_check_target: "portquiz.net".to_string(), + port_check_ports: vec![], + port_check_timeout_sec: 0, }; assert_eq!(args.netstack_num_ping, 0); @@ -219,6 +260,9 @@ mod tests { netstack_ping_ips_v4: vec!["1.1.1.1".to_string()], netstack_ping_hosts_v6: vec!["cloudflare.com".to_string()], netstack_ping_ips_v6: vec!["2001:4860:4860::8888".to_string()], + port_check_target: "portquiz.net".to_string(), + port_check_ports: vec![], + port_check_timeout_sec: 5, }; assert!(args.netstack_ping_hosts_v4[0].contains("nym")); @@ -228,4 +272,27 @@ mod tests { assert_eq!(args.netstack_v4_dns, "1.1.1.1"); assert_eq!(args.netstack_v6_dns, "2606:4700:4700::1111"); } + + #[test] + fn test_exit_policy_ports_no_duplicates_and_sorted() { + let ports = EXIT_POLICY_PORTS; + assert!(!ports.is_empty(), "EXIT_POLICY_PORTS should not be empty"); + + // verify sorted + for window in ports.windows(2) { + assert!( + window[0] < window[1], + "EXIT_POLICY_PORTS out of order or duplicate: {} >= {}", + window[0], + window[1] + ); + } + + // spot-check a few well-known ports + assert!(ports.contains(&22), "should contain SSH (22)"); + assert!(ports.contains(&443), "should contain HTTPS (443)"); + assert!(ports.contains(&22021), "should contain Session (22021)"); + assert!(ports.contains(&8332), "should contain Bitcoin (8332)"); + assert!(ports.contains(&9735), "should contain Lightning (9735)"); + } } diff --git a/nym-gateway-probe/src/config/test_mode.rs b/nym-gateway-probe/src/config/test_mode.rs index eec509f0e9..a1a200404a 100644 --- a/nym-gateway-probe/src/config/test_mode.rs +++ b/nym-gateway-probe/src/config/test_mode.rs @@ -10,6 +10,9 @@ //! - LpOnly: LP registration only, no WireGuard //! - Socks5Only: Socks5 test //! - All: Mixnet, wireguard over authenticator and LP registration +//! +//! Note: Exit policy port checking is handled by the `run-ports` subcommand, +//! not via a test mode. /// Test mode for the gateway probe. /// diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index fe4dbffc47..bc9af69858 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -9,7 +9,9 @@ use crate::common::probe_tests::{ }; use crate::common::types::{Entry, LpProbeResults}; use crate::config::{CredentialArgs, CredentialMode, NetstackArgs, ProbeConfig}; -use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient}; +use nym_authenticator_client::{ + AuthClientMixnetListener, AuthClientMixnetListenerHandle, AuthenticatorClient, +}; use nym_bandwidth_controller::BandwidthTicketProvider; use nym_client_core::config::ForgetMe; use nym_config::defaults::NymNetworkDetails; @@ -20,13 +22,16 @@ use nym_sdk::mixnet::{ }; use nym_topology::{HardcodedTopologyProvider, NymTopology}; use rand::rngs::OsRng; +use std::collections::BTreeMap; +use std::net::IpAddr; use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; use tokio_util::sync::CancellationToken; use tracing::*; pub use crate::common::nodes::{NymApiDirectory, query_gateway_by_ip}; -pub use crate::common::types::{ProbeOutcome, ProbeResult}; +pub use crate::common::types::{PortCheckResult, PortsCheckSummary, ProbeOutcome, ProbeResult}; mod common; pub use common::types; @@ -45,7 +50,280 @@ pub struct Probe { topology: Option, } +#[derive(Debug, Clone)] +pub struct RunPortsConfig { + pub min_gateway_mixnet_performance: Option, + pub ignore_egress_epoch_role: bool, + pub netstack_args: NetstackArgs, +} + +// Port checks always target a bonded gateway. There are two entry points: +// - `run_ports` : local CLI, on-disk storage + mnemonic. +// - `run_ports_for_agent`: NS agent, ephemeral storage + ticket materials. + +struct PortScanRun { + can_register: bool, + port_results: BTreeMap, + last_error: Option, +} + +/// Validated info needed to run a WG port-check via the mixnet. +struct PortCheckSetup { + exit_node: TestedNodeDetails, + exit_identity: String, + authenticator: nym_sdk::mixnet::Recipient, + ip_address: IpAddr, + port_check_target: String, + ports_count: usize, +} + +impl PortCheckSetup { + fn new(exit_node: TestedNodeDetails, config: &RunPortsConfig) -> anyhow::Result { + let exit_identity = exit_node.identity.to_string(); + + let (authenticator, ip_address) = + match (exit_node.authenticator_address, exit_node.ip_address) { + (Some(auth), Some(ip)) => (auth, ip), + _ => anyhow::bail!( + "Gateway {} missing authenticator address or IP — not a functional exit", + exit_identity + ), + }; + + let ports_count = config.netstack_args.port_check_ports.len(); + if ports_count == 0 { + anyhow::bail!( + "No ports specified. Use --check-ports 80,443,22021 or --check-all-ports" + ); + } + + Ok(Self { + exit_node, + exit_identity, + authenticator, + ip_address, + port_check_target: config.netstack_args.port_check_target.clone(), + ports_count, + }) + } + + fn failed_to_connect(&self, err: impl std::fmt::Display) -> PortCheckResult { + PortCheckResult { + gateway: self.exit_identity.clone(), + can_register: false, + port_check_target: self.port_check_target.clone(), + ports: BTreeMap::new(), + error: Some(format!("Failed to connect to mixnet: {err}")), + } + } +} + impl Probe { + fn parse_port_check_targets(raw: &str) -> Vec { + raw.split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned) + .collect() + } + + async fn is_target_reachable(host: &str) -> bool { + let timeout = Duration::from_secs(2); + + for port in [80u16, 443u16] { + if tokio::time::timeout(timeout, tokio::net::TcpStream::connect((host, port))) + .await + .is_ok_and(|res| res.is_ok()) + { + return true; + } + } + + false + } + + async fn run_port_scan_with_retries( + mixnet_listener_task: &AuthClientMixnetListenerHandle, + nym_address: nym_sdk::mixnet::Recipient, + authenticator: nym_sdk::mixnet::Recipient, + authenticator_version: nym_authenticator_requests::AuthenticatorVersion, + ip_address: IpAddr, + bandwidth_provider: &dyn BandwidthTicketProvider, + wg_ticket_type: TicketType, + credential_provider: nym_sdk::mixnet::NodeIdentity, + netstack_args: NetstackArgs, + awg_args: Option, + ) -> PortScanRun { + let mut port_results: BTreeMap = BTreeMap::new(); + let mut can_register = false; + let mut last_error = None; + let max_attempts = 3; + + for attempt in 1..=max_attempts { + if attempt > 1 { + info!("Retrying authenticator registration (attempt {attempt}/{max_attempts})..."); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + + let credential = match bandwidth_provider + .get_ecash_ticket(wg_ticket_type, credential_provider, 1) + .await + { + Ok(ticket) => ticket.data, + Err(e) => { + error!("Failed to get ecash ticket: {e}"); + last_error = Some(format!("Failed to get ecash ticket: {e}")); + break; + } + }; + + let mut rng = rand::thread_rng(); + let auth_client = AuthenticatorClient::new( + mixnet_listener_task.subscribe(), + mixnet_listener_task.mixnet_sender(), + nym_address, + authenticator, + authenticator_version, + Arc::new(x25519::KeyPair::new(&mut rng)), + ip_address, + ); + + match wg_probe( + auth_client, + ip_address, + authenticator_version, + awg_args.clone(), + netstack_args.clone(), + true, // port_check_only + credential, + ) + .await + { + Ok(outcome) => { + if outcome.can_register { + can_register = true; + port_results = outcome + .port_check_results + .unwrap_or_default() + .into_iter() + .collect(); + let open = port_results.values().filter(|&&v| v).count(); + info!( + "Port check complete: {}/{} ports open", + open, + port_results.len() + ); + break; + } + warn!( + "Auth registration returned but can_register=false (attempt {attempt}/{max_attempts})" + ); + last_error = Some("Auth registration did not complete".into()); + } + Err(e) => { + warn!("WG probe error: {e} (attempt {attempt}/{max_attempts})"); + last_error = Some(format!("WG probe error: {e}")); + } + } + } + + PortScanRun { + can_register, + port_results, + last_error, + } + } + + /// Warm up routes, register with the authenticator, run the port scan and tear down. + async fn port_check_after_connect( + mixnet_client: MixnetClient, + setup: PortCheckSetup, + bandwidth_provider: &dyn BandwidthTicketProvider, + netstack_args: NetstackArgs, + ) -> PortCheckResult { + let targets = Self::parse_port_check_targets(&setup.port_check_target); + let mut selected_target = targets + .first() + .cloned() + .unwrap_or_else(|| setup.port_check_target.clone()); + + if targets.len() > 1 { + let mut found = false; + for candidate in &targets { + if Self::is_target_reachable(candidate).await { + selected_target = candidate.clone(); + found = true; + break; + } + warn!( + "Port-check target '{}' is unreachable, trying next", + candidate + ); + } + if !found { + warn!( + "All port-check targets unreachable; falling back to first: '{}'", + selected_target + ); + } else if selected_target != *targets.first().unwrap() { + info!( + "Port check: selected target '{}' (first reachable from list)", + selected_target + ); + } + } + + let mut netstack_args = netstack_args; + netstack_args.port_check_target = selected_target.clone(); + + info!("Warming up mixnet routes..."); + let nym_address = *mixnet_client.nym_address(); + let (warmup_result, mixnet_client) = do_ping( + mixnet_client, + nym_address, + setup.exit_node.exit_router_address, + false, + ) + .await; + + match warmup_result { + Ok(_) => info!("Mixnet warmup done"), + Err(e) => warn!("Warmup had issues ({e}), auth may be less reliable"), + } + + let nym_address = *mixnet_client.nym_address(); + let mixnet_listener_task = + AuthClientMixnetListener::new(mixnet_client, CancellationToken::new()).start(); + + let scan = Self::run_port_scan_with_retries( + &mixnet_listener_task, + nym_address, + setup.authenticator, + setup.exit_node.authenticator_version, + setup.ip_address, + bandwidth_provider, + TicketType::V1WireguardExit, + setup.exit_node.identity, + netstack_args, + None, + ) + .await; + + mixnet_listener_task.stop().await; + + PortCheckResult { + gateway: setup.exit_identity, + can_register: scan.can_register, + port_check_target: selected_target, + ports: scan.port_results, + error: if scan.can_register { + None + } else { + scan.last_error + }, + } + } + /// Create a probe with pre-queried gateway nodes pub fn new( entry_node: TestedNodeDetails, @@ -90,7 +368,6 @@ impl Probe { }) } - /// Run a probe as an NS agent (orchestrator for multiple probe runs for NS API) pub async fn probe_run_agent( mut self, credential_args: CredentialArgs, @@ -269,6 +546,174 @@ impl Probe { self.do_probe_test(mixnet_client, bandwidth_provider).await } + pub async fn run_ports( + entry_node: TestedNodeDetails, + exit_node: Option, + network: NymNetworkDetails, + config: &RunPortsConfig, + config_dir: &PathBuf, + credential: CredentialMode, + ) -> anyhow::Result { + let exit_node = exit_node.unwrap_or(entry_node.clone()); + let setup = PortCheckSetup::new(exit_node, config)?; + + info!( + "Port check: testing {} ports on gateway {} via {}", + setup.ports_count, setup.exit_identity, setup.port_check_target + ); + + let storage_paths = StoragePaths::new_from_dir(config_dir)?; + let storage = storage_paths + .initialise_default_persistent_storage() + .await?; + + let mixnet_debug_config = helpers::mixnet_debug_config( + config.min_gateway_mixnet_performance, + config.ignore_egress_epoch_role, + ); + + let topology = helpers::fetch_topology(&network, &mixnet_debug_config) + .await + .inspect_err(|e| warn!("Failed to fetch topology: {e}")) + .ok(); + + let mut mixnet_client_builder = MixnetClientBuilder::new_with_storage(storage.clone()) + .request_gateway(entry_node.identity.to_string()) + .network_details(network.clone()) + .debug_config(mixnet_debug_config) + .with_forget_me(ForgetMe::new_stats()) + .credentials_mode(!credential.use_mock_ecash); + + if let Some(topology) = &topology { + mixnet_client_builder = mixnet_client_builder.custom_topology_provider(Box::new( + HardcodedTopologyProvider::new(topology.clone()), + )); + } + + let disconnected_mixnet_client = mixnet_client_builder.build()?; + + // make sure identity keys exist before credential acquisition + // (acquire_bandwidth → create_bandwidth_client needs them on disk) + let key_store = storage.key_store(); + if key_store.load_keys().await.is_err() { + debug!("Generating new client keys"); + let mut rng = OsRng; + nym_client_core::init::generate_new_client_keys(&mut rng, key_store).await?; + } + + credential + .acquire(&disconnected_mixnet_client, &storage) + .await?; + + let bandwidth_provider = build_bandwidth_controller( + &network, + storage.credential_store().clone(), + credential.use_mock_ecash, + )?; + + let mixnet_client = match disconnected_mixnet_client.connect_to_mixnet().await { + Ok(client) => { + info!( + "Connected to mixnet via entry gateway: {}", + entry_node.identity + ); + info!("Our nym address: {}", *client.nym_address()); + client + } + Err(e) => return Ok(setup.failed_to_connect(e)), + }; + + Ok(Self::port_check_after_connect( + mixnet_client, + setup, + bandwidth_provider.as_ref(), + config.netstack_args.clone(), + ) + .await) + } + + /// Bonded gateway port-check, run by the NS agent. Uses ephemeral storage and ticket + /// materials provided by the NS API instead of mnemonic-based acquisition. + pub async fn run_ports_for_agent( + entry_gateway: nym_sdk::mixnet::ed25519::PublicKey, + network: NymNetworkDetails, + config: &RunPortsConfig, + credential_args: CredentialArgs, + ) -> anyhow::Result { + let api_url = network + .endpoints + .first() + .and_then(|ep| ep.api_url()) + .ok_or(anyhow::anyhow!("missing api url"))?; + + let directory = NymApiDirectory::new(api_url).await?; + let entry_node = directory + .entry_gateway(&entry_gateway)? + .to_testable_node()?; + + // agent always uses the entry gateway as the exit + let setup = PortCheckSetup::new(entry_node.clone(), config)?; + + info!( + "Port check (agent): testing {} ports on gateway {} via {}", + setup.ports_count, setup.exit_identity, setup.port_check_target + ); + + let storage = Ephemeral::default(); + + let mixnet_debug_config = helpers::mixnet_debug_config( + config.min_gateway_mixnet_performance, + config.ignore_egress_epoch_role, + ); + + let topology = helpers::fetch_topology(&network, &mixnet_debug_config) + .await + .inspect_err(|e| warn!("Failed to fetch topology: {e}")) + .ok(); + + let mut mixnet_client_builder = MixnetClientBuilder::new_with_storage(storage.clone()) + .request_gateway(entry_node.identity.to_string()) + .network_details(network.clone()) + .debug_config(mixnet_debug_config) + .with_forget_me(ForgetMe::new_stats()) + .credentials_mode(true); + + if let Some(topology) = &topology { + mixnet_client_builder = mixnet_client_builder.custom_topology_provider(Box::new( + HardcodedTopologyProvider::new(topology.clone()), + )); + } + + let disconnected_mixnet_client = mixnet_client_builder.build()?; + + credential_args + .import_credential(&disconnected_mixnet_client) + .await?; + + let bandwidth_provider = + build_bandwidth_controller(&network, storage.credential_store().clone(), false)?; + + let mixnet_client = match disconnected_mixnet_client.connect_to_mixnet().await { + Ok(client) => { + info!( + "Connected to mixnet via entry gateway: {}", + entry_node.identity + ); + info!("Our nym address: {}", *client.nym_address()); + client + } + Err(e) => return Ok(setup.failed_to_connect(e)), + }; + + Ok(Self::port_check_after_connect( + mixnet_client, + setup, + bandwidth_provider.as_ref(), + config.netstack_args.clone(), + ) + .await) + } + #[allow(clippy::too_many_arguments)] pub async fn do_probe_test( self, @@ -369,6 +814,7 @@ impl Probe { let mixnet_listener_task = AuthClientMixnetListener::new(mixnet_client, CancellationToken::new()) .start(); + let mut rng = rand::thread_rng(); let auth_client = AuthenticatorClient::new( mixnet_listener_task.subscribe(), @@ -397,6 +843,7 @@ impl Probe { exit_node.authenticator_version, self.config.amnezia_args.clone(), self.config.netstack_args.clone(), + false, credential, ) .await @@ -404,6 +851,7 @@ impl Probe { // Add wg results to probe result probe_result.outcome.wg = Some(outcome); + mixnet_listener_task.stop().await; } else { warn!("Not enough information to run WireGuard via mixnet registration tests"); diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index d1c15e22b2..06fcbbb1d8 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -1,12 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; use nym_bin_common::bin_info; use nym_config::defaults::setup_env; -use nym_gateway_probe::config::{CredentialArgs, CredentialMode, ProbeConfig}; -use nym_gateway_probe::{NymApiDirectory, ProbeResult, query_gateway_by_ip}; +use nym_gateway_probe::config::{CredentialArgs, CredentialMode, NetstackArgs, ProbeConfig}; +use nym_gateway_probe::{ + NymApiDirectory, PortCheckResult, ProbeResult, RunPortsConfig, query_gateway_by_ip, +}; use nym_sdk::mixnet::NodeIdentity; +use serde::Serialize; use std::path::Path; use std::{path::PathBuf, sync::OnceLock}; use tracing::*; @@ -28,7 +31,7 @@ struct CliArgs { config_env_file: Option, /// Disable logging during probe - #[arg(long)] + #[arg(long, global = true)] no_log: bool, } @@ -81,6 +84,35 @@ enum Commands { probe_config: ProbeConfig, }, + /// Check WG exit policy ports on a bonded gateway. + /// Tests TCP connectivity through the WG tunnel for each port. + /// Use --check-ports to pick specific ports, or --check-all-ports for the full exit policy list. + RunPorts { + /// Directory for credential and mixnet storage + #[arg(long)] + config_dir: Option, + + /// Bonded gateway identity. + #[arg(long, short = 'g', alias = "gateway")] + entry_gateway: NodeIdentity, + + /// Separate exit gateway to test (entry_gateway is used for mixnet entry). + #[arg(long)] + exit_gateway: Option, + + /// Test every port in the canonical exit policy (network-tunnel-manager.sh PORT_MAPPINGS). + /// Overrides --check-ports. + #[arg(long)] + check_all_ports: bool, + + /// Arguments to manage credentials + #[command(flatten)] + credential_mode: CredentialMode, + + #[command(flatten)] + probe_config: RunPortsProbeConfig, + }, + /// Run the probe by NS agents RunAgent { /// The specific gateway specified by ID. @@ -96,6 +128,29 @@ enum Commands { }, } +#[derive(Debug, Args, Clone)] +struct RunPortsProbeConfig { + /// Only choose gateway with that minimum performance + #[arg(long)] + min_gateway_mixnet_performance: Option, + + /// Ignore egress epoch role constraints + #[arg(long, global = true)] + ignore_egress_epoch_role: bool, + + /// Arguments to manage netstack downloads and port checks + #[command(flatten)] + netstack_args: NetstackArgs, +} + +/// CLI output wrapper — either a standard probe result or a port-check result +#[derive(Serialize)] +#[serde(untagged)] +pub(crate) enum ProbeOutput { + Standard(ProbeResult), + PortCheck(PortCheckResult), +} + fn setup_logging() { // SAFETY: those are valid directives #[allow(clippy::unwrap_used)] @@ -112,7 +167,7 @@ fn setup_logging() { .init(); } -pub(crate) async fn run() -> anyhow::Result { +pub(crate) async fn run() -> anyhow::Result { let args = CliArgs::parse(); if !args.no_log { setup_logging(); @@ -122,7 +177,7 @@ pub(crate) async fn run() -> anyhow::Result { setup_env(args.config_env_file.as_ref()); let network = nym_sdk::NymNetworkDetails::new_from_env(); - info!("{:#?}", network); + debug!("{:#?}", network); match args.command { Commands::RunLocal { @@ -165,7 +220,9 @@ pub(crate) async fn run() -> anyhow::Result { let trial = nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config); - Box::pin(trial.probe_run_locally(&config_dir, credential_mode)).await + Box::pin(trial.probe_run_locally(&config_dir, credential_mode)) + .await + .map(ProbeOutput::Standard) } Commands::Run { entry_gateway, @@ -209,7 +266,79 @@ pub(crate) async fn run() -> anyhow::Result { let trial = nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config); - Box::pin(trial.probe_run(&config_dir, credential_mode)).await + Box::pin(trial.probe_run(&config_dir, credential_mode)) + .await + .map(ProbeOutput::Standard) + } + Commands::RunPorts { + entry_gateway, + exit_gateway, + config_dir, + check_all_ports, + credential_mode, + probe_config, + } => { + let mut run_ports_config = RunPortsConfig { + min_gateway_mixnet_performance: probe_config.min_gateway_mixnet_performance, + ignore_egress_epoch_role: probe_config.ignore_egress_epoch_role, + netstack_args: probe_config.netstack_args, + }; + + // --check-all-ports overrides --check-ports with the full exit policy list + if check_all_ports { + use nym_gateway_probe::config::EXIT_POLICY_PORTS; + run_ports_config.netstack_args.port_check_ports = EXIT_POLICY_PORTS.to_vec(); + info!( + "Using full exit policy port list ({} ports)", + EXIT_POLICY_PORTS.len() + ); + } + + let api_url = network + .endpoints + .first() + .and_then(|ep| ep.api_url()) + .ok_or(anyhow::anyhow!("missing api url"))?; + + let directory = NymApiDirectory::new(api_url).await?; + + let entry_details = directory + .entry_gateway(&entry_gateway)? + .to_testable_node()?; + + let exit_details = exit_gateway + .map(|id_key| directory.exit_gateway(&id_key)) + .transpose()? + .map(|node| node.to_testable_node()) + .transpose()?; + + let config_dir = config_dir + .clone() + .unwrap_or_else(|| Path::new(DEFAULT_CONFIG_DIR).join(&network.network_name)); + + if config_dir.is_file() { + anyhow::bail!("provided configuration directory is a file"); + } + + if !config_dir.exists() { + std::fs::create_dir_all(config_dir.clone())?; + } + + info!( + "using the following directory for the probe config: {}", + config_dir.display() + ); + + Box::pin(nym_gateway_probe::Probe::run_ports( + entry_details, + exit_details, + network, + &run_ports_config, + &config_dir, + credential_mode, + )) + .await + .map(ProbeOutput::PortCheck) } Commands::RunAgent { entry_gateway, @@ -219,7 +348,9 @@ pub(crate) async fn run() -> anyhow::Result { let trial = nym_gateway_probe::Probe::new_for_agent(entry_gateway, network, probe_config) .await?; - Box::pin(trial.probe_run_agent(credential_args)).await + Box::pin(trial.probe_run_agent(credential_args)) + .await + .map(ProbeOutput::Standard) } } } diff --git a/nym-node-status-api/nym-node-status-agent/Cargo.toml b/nym-node-status-api/nym-node-status-agent/Cargo.toml index 46d25e8e28..5750036c26 100644 --- a/nym-node-status-api/nym-node-status-agent/Cargo.toml +++ b/nym-node-status-api/nym-node-status-agent/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-agent" -version = "2.0.1-rc3" +version = "2.0.1" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/common.rs b/nym-node-status-api/nym-node-status-agent/src/cli/common.rs new file mode 100644 index 0000000000..95d3277bc0 --- /dev/null +++ b/nym-node-status-api/nym-node-status-agent/src/cli/common.rs @@ -0,0 +1,42 @@ +use crate::cli::{ServerConfig, parse_server_config}; +use anyhow::anyhow; +use nym_gateway_probe::config::CredentialArgs; +use nym_gateway_probe::types::{AttachedTicketMaterials, VersionedSerialise}; +use nym_node_status_client::NsApiClient; +use nym_sdk::mixnet::ed25519::PublicKey; + +pub(crate) fn parse_servers(raw: &[String]) -> anyhow::Result> { + raw.iter() + .map(|s| { + parse_server_config(s).map_err(|e| { + tracing::error!("Invalid server config '{}': {}", s, e); + anyhow!("Invalid server config '{}': {}", s, e) + }) + }) + .collect() +} + +pub(crate) fn primary(servers: &[ServerConfig]) -> anyhow::Result<&ServerConfig> { + servers + .first() + .ok_or_else(|| anyhow!("No servers configured")) +} + +pub(crate) fn build_client(server: &ServerConfig) -> NsApiClient { + let auth_key = + nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes(&server.auth_key.to_bytes()) + .expect("Failed to clone auth key"); + NsApiClient::new(&server.address, server.port, auth_key) +} + +pub(crate) fn parse_gateway_pubkey(key: &str) -> anyhow::Result { + PublicKey::from_base58_string(key).map_err(|e| anyhow!("Failed to parse GW identity key: {e}")) +} + +pub(crate) fn credential_args_from(materials: AttachedTicketMaterials) -> CredentialArgs { + CredentialArgs { + ticket_materials: materials.to_serialised_string(), + ticket_materials_revision: + ::CURRENT_SERIALISATION_REVISION, + } +} diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs b/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs index 8acd095fb9..5fbe2ae62d 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs @@ -6,7 +6,9 @@ use std::{env, sync::OnceLock}; use tokio::time::Instant; use tracing::info; +pub(crate) mod common; pub(crate) mod generate_keypair; +pub(crate) mod run_ports_check; pub(crate) mod run_probe; #[derive(Debug)] @@ -22,7 +24,7 @@ fn pretty_build_info_static() -> &'static str { PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) } -fn parse_server_config(s: &str) -> Result { +pub(super) fn parse_server_config(s: &str) -> Result { let parts: Vec<&str> = s.split('|').collect(); if parts.len() != 2 { return Err("Server config must be in format 'address|port'".to_string()); @@ -53,6 +55,7 @@ pub(crate) struct Args { #[derive(Subcommand, Debug)] pub(crate) enum Command { RunProbe(RunProbeArgs), + RunPortsCheck(RunPortsCheckArgs), GenerateKeypair { #[arg(long)] @@ -73,6 +76,26 @@ pub(crate) struct RunProbeArgs { pub probe_config: nym_gateway_probe::config::ProbeConfig, } +#[derive(clap::Args, Debug)] +pub(crate) struct RunPortsCheckArgs { + /// Server configurations in format "address|port" + /// Can be specified multiple times for multiple servers + #[arg(short, long, required = true)] + pub server: Vec, + + /// Only choose gateway with that minimum performance + #[arg(long)] + pub min_gateway_mixnet_performance: Option, + + /// Ignore egress epoch role constraints + #[arg(long)] + pub ignore_egress_epoch_role: bool, + + /// Arguments to manage netstack downloads and port checks + #[command(flatten)] + pub netstack_args: nym_gateway_probe::config::NetstackArgs, +} + impl Args { pub(crate) async fn execute(self, log_capture: LogCapture) -> anyhow::Result<()> { match self.command { @@ -99,6 +122,21 @@ impl Args { res?; } + Command::RunPortsCheck(args) => { + let servers = common::parse_servers(&args.server)?; + + run_ports_check::run_ports_check( + &servers, + args.min_gateway_mixnet_performance, + args.ignore_egress_epoch_role, + args.netstack_args, + log_capture, + ) + .await + .inspect_err(|err| { + tracing::error!("{err}"); + })? + } Command::GenerateKeypair { path } => { let path = path .to_owned() diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs b/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs new file mode 100644 index 0000000000..3fa6b2f2bf --- /dev/null +++ b/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs @@ -0,0 +1,131 @@ +use crate::cli::ServerConfig; +use crate::cli::common; +use crate::log_capture::LogCapture; +use nym_gateway_probe::RunPortsConfig; +use tracing::instrument; + +pub(crate) async fn run_ports_check( + servers: &[ServerConfig], + min_gateway_mixnet_performance: Option, + ignore_egress_epoch_role: bool, + mut netstack_args: nym_gateway_probe::config::NetstackArgs, + log_capture: LogCapture, +) -> anyhow::Result<()> { + let primary_server = common::primary(servers)?; + tracing::info!( + "Requesting ports-check testrun from primary server: {}:{}", + primary_server.address, + primary_server.port + ); + + let ns_api_client = common::build_client(primary_server); + + let testrun = match ns_api_client.request_ports_check_testrun().await { + Ok(Some(testrun)) => testrun, + Ok(None) => { + tracing::info!("No ports-check testruns available from primary server"); + return Ok(()); + } + Err(err) => { + tracing::error!("Failed to contact primary server: {err}"); + return Err(err); + } + }; + + let testrun_id = testrun.assignment.testrun_id; + let testrun_assigned_at = testrun.assignment.assigned_at_utc; + let gateway_identity_key = testrun.assignment.gateway_identity_key.clone(); + let gateway_identity_pubkey = common::parse_gateway_pubkey(&gateway_identity_key)?; + + tracing::info!( + "Received ports-check testrun {testrun_id} for gateway {gateway_identity_key} from primary", + ); + + let network = nym_sdk::NymNetworkDetails::new_from_env(); + + // Force full exit policy list for this job kind + netstack_args.port_check_ports = nym_gateway_probe::config::EXIT_POLICY_PORTS.to_vec(); + + let run_ports_config = RunPortsConfig { + min_gateway_mixnet_performance, + ignore_egress_epoch_role, + netstack_args, + }; + + let credentials_args = common::credential_args_from(testrun.ticket_materials); + + log_capture.start(); + let port_check_result = nym_gateway_probe::Probe::run_ports_for_agent( + gateway_identity_pubkey, + network, + &run_ports_config, + credentials_args, + ) + .await?; + let probe_log = log_capture.stop_and_drain(); + + submit_ports_check_results_to_servers( + servers, + testrun_id, + testrun_assigned_at, + &gateway_identity_key, + port_check_result, + probe_log, + ) + .await; + + Ok(()) +} + +#[instrument(level = "info", skip_all, fields(gateway_id = %gateway_identity_key, testrun = testrun_id))] +async fn submit_ports_check_results_to_servers( + servers: &[ServerConfig], + testrun_id: i32, + testrun_assigned_at: i64, + gateway_identity_key: &str, + port_check_result: nym_gateway_probe::PortCheckResult, + probe_log: String, +) { + let handles = servers + .iter() + .enumerate() + .map(|(idx, server)| { + let port_check_result = port_check_result.clone(); + let probe_log = probe_log.clone(); + let gateway_identity_key = gateway_identity_key.to_string(); + + async move { + let client = common::build_client(server); + + let result = client + .submit_ports_check_results_with_context( + testrun_id, + port_check_result, + probe_log, + testrun_assigned_at, + gateway_identity_key, + ) + .await; + + (idx, server.address.clone(), server.port, result) + } + }) + .collect::>(); + + let results = futures::future::join_all(handles).await; + + for (index, server_address, server_port, result) in results { + match result { + Ok(()) => { + tracing::info!( + "✅ Successfully submitted ports-check to server[{index}] {server_address}:{server_port}", + ); + } + Err(e) => { + tracing::warn!( + "❌ Failed to submit ports-check to server[{index}] {server_address}:{server_port} - {e}" + ); + } + } + } +} diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs b/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs index 216329fa78..6e4da33cab 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs @@ -1,9 +1,6 @@ use crate::cli::ServerConfig; +use crate::cli::common; use crate::log_capture::LogCapture; -use anyhow::anyhow; -use nym_gateway_probe::config::CredentialArgs; -use nym_gateway_probe::types::{AttachedTicketMaterials, VersionedSerialise}; -use nym_sdk::mixnet::ed25519::PublicKey; use tracing::instrument; pub(crate) async fn run_probe( @@ -11,27 +8,14 @@ pub(crate) async fn run_probe( probe_config: nym_gateway_probe::config::ProbeConfig, log_capture: LogCapture, ) -> anyhow::Result<()> { - if servers.is_empty() { - anyhow::bail!("No servers configured"); - } - - // Always use first server as primary - let primary_server = &servers[0]; + let primary_server = common::primary(servers)?; tracing::info!( "Requesting testrun from primary server: {}:{}", primary_server.address, primary_server.port ); - let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes( - &primary_server.auth_key.to_bytes(), - ) - .expect("Failed to clone auth key"); - let ns_api_client = nym_node_status_client::NsApiClient::new( - &primary_server.address, - primary_server.port, - auth_key, - ); + let ns_api_client = common::build_client(primary_server); let testrun = match ns_api_client.request_testrun().await { Ok(Some(testrun)) => testrun, @@ -48,8 +32,7 @@ pub(crate) async fn run_probe( let testrun_id = testrun.assignment.testrun_id; let testrun_assigned_at = testrun.assignment.assigned_at_utc; let gateway_identity_key = testrun.assignment.gateway_identity_key.clone(); - let gateway_identity_pubkey = PublicKey::from_base58_string(gateway_identity_key.clone()) - .map_err(|e| anyhow!("Failed to parse GW identity key: {e}"))?; + let gateway_identity_pubkey = common::parse_gateway_pubkey(&gateway_identity_key)?; tracing::info!("Received testrun {testrun_id} for gateway {gateway_identity_key} from primary",); @@ -61,12 +44,7 @@ pub(crate) async fn run_probe( // probe constructor might modify config to suit the testing mode, so log afterwards tracing::info!("Using probe config:\n{:#?}", &probe.config()); - let serialized_ticket_materials = testrun.ticket_materials.to_serialised_string(); - let credentials_args = CredentialArgs { - ticket_materials: serialized_ticket_materials, - ticket_materials_revision: - ::CURRENT_SERIALISATION_REVISION, - }; + let credentials_args = common::credential_args_from(testrun.ticket_materials); // Run the probe, capturing all tracing output log_capture.start(); @@ -110,15 +88,7 @@ async fn submit_results_to_servers( let gateway_identity_key = gateway_identity_key.to_string(); async move { - let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes( - &server.auth_key.to_bytes(), - ) - .expect("Failed to clone auth key"); - let client = nym_node_status_client::NsApiClient::new( - &server.address, - server.port, - auth_key, - ); + let client = common::build_client(server); let result = if idx == 0 { // Primary server: submit regular results without context diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-0c635c539930c10a9be35a12ba3f2a66aae5be1c37af2eca521bf75261cecf28.json similarity index 66% rename from nym-node-status-api/nym-node-status-api/.sqlx/query-f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-0c635c539930c10a9be35a12ba3f2a66aae5be1c37af2eca521bf75261cecf28.json index b6dd6ac8a9..53756614d0 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-0c635c539930c10a9be35a12ba3f2a66aae5be1c37af2eca521bf75261cecf28.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n id,\n gateway_id,\n status,\n created_utc,\n ip_address,\n log,\n last_assigned_utc\n FROM testruns\n WHERE gateway_id = $1 AND status != 2\n ORDER BY id DESC\n LIMIT 1", + "query": "SELECT\n id,\n gateway_id,\n status,\n kind,\n created_utc,\n ip_address,\n log,\n last_assigned_utc\n FROM testruns\n WHERE gateway_id = $1 AND status != 2 AND kind = $2\n ORDER BY id DESC\n LIMIT 1", "describe": { "columns": [ { @@ -20,28 +20,34 @@ }, { "ordinal": 3, + "name": "kind", + "type_info": "Int2" + }, + { + "ordinal": 4, "name": "created_utc", "type_info": "Int8" }, { - "ordinal": 4, + "ordinal": 5, "name": "ip_address", "type_info": "Varchar" }, { - "ordinal": 5, + "ordinal": 6, "name": "log", "type_info": "Varchar" }, { - "ordinal": 6, + "ordinal": 7, "name": "last_assigned_utc", "type_info": "Int8" } ], "parameters": { "Left": [ - "Int4" + "Int4", + "Int2" ] }, "nullable": [ @@ -51,8 +57,9 @@ false, false, false, + false, true ] }, - "hash": "f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8" + "hash": "0c635c539930c10a9be35a12ba3f2a66aae5be1c37af2eca521bf75261cecf28" } diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-88a4554c2857288c314768c56648a5f1811d2053582380ca602335a122cef8db.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-1dc7d1e6c2173cd1ca70d699124c2b71a1a91ef114ffdd0572a738b577fab15d.json similarity index 51% rename from nym-node-status-api/nym-node-status-api/.sqlx/query-88a4554c2857288c314768c56648a5f1811d2053582380ca602335a122cef8db.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-1dc7d1e6c2173cd1ca70d699124c2b71a1a91ef114ffdd0572a738b577fab15d.json index 174d528667..656a892089 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-88a4554c2857288c314768c56648a5f1811d2053582380ca602335a122cef8db.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-1dc7d1e6c2173cd1ca70d699124c2b71a1a91ef114ffdd0572a738b577fab15d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO testruns (\n id,\n gateway_id,\n status,\n created_utc,\n last_assigned_utc,\n ip_address,\n log\n ) VALUES ($1, $2, $3, $4, $5, $6, $7)", + "query": "INSERT INTO testruns (\n id,\n gateway_id,\n status,\n kind,\n created_utc,\n last_assigned_utc,\n ip_address,\n log\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", "describe": { "columns": [], "parameters": { @@ -8,6 +8,7 @@ "Int4", "Int4", "Int4", + "Int2", "Int8", "Int8", "Varchar", @@ -16,5 +17,5 @@ }, "nullable": [] }, - "hash": "88a4554c2857288c314768c56648a5f1811d2053582380ca602335a122cef8db" + "hash": "1dc7d1e6c2173cd1ca70d699124c2b71a1a91ef114ffdd0572a738b577fab15d" } diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20.json new file mode 100644 index 0000000000..bd975d3146 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE gateways SET last_ports_check_utc = $1, last_updated_utc = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20" +} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-65b5a3ed9a73f1304badfa8d9bba57fb24f6b21655039a9bfa21c44409c87910.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-65b5a3ed9a73f1304badfa8d9bba57fb24f6b21655039a9bfa21c44409c87910.json new file mode 100644 index 0000000000..fa226820d2 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-65b5a3ed9a73f1304badfa8d9bba57fb24f6b21655039a9bfa21c44409c87910.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO testruns (gateway_id, status, kind, created_utc, last_assigned_utc, ip_address, log)\n SELECT\n gw.id,\n $1,\n $2,\n $3,\n NULL,\n 'ports_check_scheduler',\n ''\n FROM gateways gw\n WHERE gw.bonded = true\n AND (gw.last_ports_check_utc IS NULL OR gw.last_ports_check_utc < $4)\n AND NOT EXISTS (\n SELECT 1\n FROM testruns t\n WHERE t.gateway_id = gw.id\n AND t.kind = $2\n AND t.status IN ($1, $5)\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int2", + "Int8", + "Int8", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "65b5a3ed9a73f1304badfa8d9bba57fb24f6b21655039a9bfa21c44409c87910" +} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-d41cafc76cb49c03df7452c405a4e2e5e3951c41dc35c20261c1d959c0d6403f.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-75bdccbff8ea6a59d56230075163783249a03f2ae04ec89f734e67ceb897a385.json similarity index 67% rename from nym-node-status-api/nym-node-status-api/.sqlx/query-d41cafc76cb49c03df7452c405a4e2e5e3951c41dc35c20261c1d959c0d6403f.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-75bdccbff8ea6a59d56230075163783249a03f2ae04ec89f734e67ceb897a385.json index 7eafc8e67d..a99e7bebdc 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-d41cafc76cb49c03df7452c405a4e2e5e3951c41dc35c20261c1d959c0d6403f.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-75bdccbff8ea6a59d56230075163783249a03f2ae04ec89f734e67ceb897a385.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n created_utc as \"created_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\",\n last_assigned_utc\n FROM testruns\n WHERE\n id = $1\n AND\n status = $2\n ORDER BY created_utc\n LIMIT 1", + "query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n kind as \"kind!\",\n created_utc as \"created_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\",\n last_assigned_utc\n FROM testruns\n WHERE\n id = $1\n AND\n status = $2\n ORDER BY created_utc\n LIMIT 1", "describe": { "columns": [ { @@ -20,21 +20,26 @@ }, { "ordinal": 3, + "name": "kind!", + "type_info": "Int2" + }, + { + "ordinal": 4, "name": "created_utc!", "type_info": "Int8" }, { - "ordinal": 4, + "ordinal": 5, "name": "ip_address!", "type_info": "Varchar" }, { - "ordinal": 5, + "ordinal": 6, "name": "log!", "type_info": "Varchar" }, { - "ordinal": 6, + "ordinal": 7, "name": "last_assigned_utc", "type_info": "Int8" } @@ -52,8 +57,9 @@ false, false, false, + false, true ] }, - "hash": "d41cafc76cb49c03df7452c405a4e2e5e3951c41dc35c20261c1d959c0d6403f" + "hash": "75bdccbff8ea6a59d56230075163783249a03f2ae04ec89f734e67ceb897a385" } diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973.json new file mode 100644 index 0000000000..9d0d9105ef --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973.json @@ -0,0 +1,31 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH oldest_queued AS (\n SELECT id\n FROM testruns\n WHERE status = $1 AND kind = $4\n ORDER BY created_utc asc\n LIMIT 1\n FOR UPDATE SKIP LOCKED\n )\n UPDATE testruns\n SET\n status = $3,\n last_assigned_utc = $2\n FROM oldest_queued\n WHERE testruns.id = oldest_queued.id\n RETURNING\n testruns.id,\n testruns.gateway_id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "gateway_id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4", + "Int8", + "Int4", + "Int2" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973" +} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-b32d29ca3cedae93fa2fcd44cc301614edc4e967f348c57f485f736b8f2f03e8.json similarity index 72% rename from nym-node-status-api/nym-node-status-api/.sqlx/query-ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-b32d29ca3cedae93fa2fcd44cc301614edc4e967f348c57f485f736b8f2f03e8.json index 59962eb2ed..f543ada553 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-b32d29ca3cedae93fa2fcd44cc301614edc4e967f348c57f485f736b8f2f03e8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n id,\n gateway_id,\n status,\n created_utc,\n ip_address,\n log,\n last_assigned_utc\n FROM testruns\n WHERE id = $1", + "query": "SELECT\n id,\n gateway_id,\n status,\n kind,\n created_utc,\n ip_address,\n log,\n last_assigned_utc\n FROM testruns\n WHERE id = $1", "describe": { "columns": [ { @@ -20,21 +20,26 @@ }, { "ordinal": 3, + "name": "kind", + "type_info": "Int2" + }, + { + "ordinal": 4, "name": "created_utc", "type_info": "Int8" }, { - "ordinal": 4, + "ordinal": 5, "name": "ip_address", "type_info": "Varchar" }, { - "ordinal": 5, + "ordinal": 6, "name": "log", "type_info": "Varchar" }, { - "ordinal": 6, + "ordinal": 7, "name": "last_assigned_utc", "type_info": "Int8" } @@ -51,8 +56,9 @@ false, false, false, + false, true ] }, - "hash": "ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64" + "hash": "b32d29ca3cedae93fa2fcd44cc301614edc4e967f348c57f485f736b8f2f03e8" } diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370.json new file mode 100644 index 0000000000..5e76c4c3cc --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT last_probe_result FROM gateways WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "last_probe_result", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + true + ] + }, + "hash": "cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370" +} diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 2306bc0708..aaf21e044b 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "4.6.2-rc10" +version = "4.6.2" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/nym-node-status-api/nym-node-status-api/Makefile b/nym-node-status-api/nym-node-status-api/Makefile index f0ae2fe951..5b410e2468 100644 --- a/nym-node-status-api/nym-node-status-api/Makefile +++ b/nym-node-status-api/nym-node-status-api/Makefile @@ -39,6 +39,19 @@ test-db-wait: ## Wait for the PostgreSQL database to be healthy sleep 1; \ done; \ echo " Database is healthy!" + @echo "Waiting for host port to be ready..." + @for i in $$(seq 1 30); do \ + if pg_isready -h localhost -p 5433 -U testuser -d nym_node_status_api_test >/dev/null 2>&1; then \ + echo " Host port is ready!"; \ + break; \ + fi; \ + if [ $$i -eq 30 ]; then \ + echo " Timed out waiting for host port"; \ + exit 1; \ + fi; \ + echo -n "."; \ + sleep 1; \ + done .PHONY: test-db-down test-db-down: ## Stop and remove the test database diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20260414000000_ports_check_testrun_kind.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20260414000000_ports_check_testrun_kind.sql new file mode 100644 index 0000000000..ea013b2c52 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20260414000000_ports_check_testrun_kind.sql @@ -0,0 +1,8 @@ +ALTER TABLE gateways + ADD COLUMN IF NOT EXISTS last_ports_check_utc BIGINT; + +ALTER TABLE testruns + ADD COLUMN IF NOT EXISTS kind SMALLINT NOT NULL DEFAULT 0; + +CREATE INDEX IF NOT EXISTS idx_testruns_kind_status_created + ON testruns (kind, status, created_utc); diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql new file mode 100644 index 0000000000..e521cb2aa5 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql @@ -0,0 +1,14 @@ +ALTER TABLE gateways ADD COLUMN IF NOT EXISTS ports_check JSONB; + +UPDATE gateways +SET ports_check = (last_probe_result::jsonb -> 'ports_check') +WHERE last_probe_result IS NOT NULL + AND btrim(last_probe_result) <> '' + AND last_probe_result::jsonb ? 'ports_check' + AND ports_check IS NULL; + +UPDATE gateways +SET last_probe_result = (last_probe_result::jsonb - 'ports_check')::text +WHERE last_probe_result IS NOT NULL + AND btrim(last_probe_result) <> '' + AND last_probe_result::jsonb ? 'ports_check'; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs index e52a97ca32..29b5b30657 100644 --- a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs @@ -82,6 +82,15 @@ pub(crate) struct Cli { #[arg(value_parser = parse_duration_std)] pub(crate) testruns_refresh_interval: Duration, + /// 2 hour safety net for test runs which include a port check + #[clap( + long, + default_value = "7200", + env = "NODE_STATUS_API_TESTRUN_STALE_IN_PROGRESS" + )] + #[arg(value_parser = parse_duration_std)] + pub(crate) testruns_stale_in_progress: Duration, + #[clap(long, default_value = "86400", env = "NODE_STATUS_API_GEODATA_TTL")] #[arg(value_parser = parse_duration_std)] pub(crate) geodata_ttl: Duration, diff --git a/nym-node-status-api/nym-node-status-api/src/db/models.rs b/nym-node-status-api/nym-node-status-api/src/db/models.rs index d9291462af..567fc34a18 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/models.rs @@ -18,6 +18,103 @@ use strum_macros::{EnumString, FromRepr}; use time::{Date, OffsetDateTime, UtcDateTime}; use utoipa::ToSchema; +fn build_ports_check_summary_json( + can_register: bool, + port_check_target: Option, + ports: Option<&serde_json::Map>, + error: Option, +) -> serde_json::Value { + let failed_ports = ports + .map(|ports| { + ports + .iter() + .filter_map(|(port, open)| open.as_bool().filter(|is_open| !is_open).map(|_| port)) + .cloned() + .collect::>() + }) + .unwrap_or_default(); + + let has_ports = ports.is_some_and(|p| !p.is_empty()); + let all_pass = can_register && failed_ports.is_empty() && has_ports; + + serde_json::json!({ + "all_pass": all_pass, + "error": error, + "port_check_target": port_check_target, + "failed_ports": failed_ports, + }) +} + +pub(crate) fn ports_check_summary_json_from_result( + result: &nym_gateway_probe::PortCheckResult, +) -> serde_json::Value { + let ports = result + .ports + .iter() + .map(|(k, v)| (k.clone(), serde_json::Value::Bool(*v))) + .collect::>(); + + build_ports_check_summary_json( + result.can_register, + Some(result.port_check_target.clone()), + Some(&ports), + result.error.clone(), + ) +} + +pub(crate) fn normalize_ports_check_payload(value: serde_json::Value) -> Option { + let serde_json::Value::Object(map) = value else { + return None; + }; + + // New shape is already in place; pass through untouched. + if map.contains_key("all_pass") + && map.contains_key("error") + && map.contains_key("port_check_target") + && map.contains_key("failed_ports") + { + return Some(serde_json::Value::Object(map)); + } + + // Legacy dedicated shape: { gateway, can_register, port_check_target, ports, error } + if map.contains_key("can_register") && map.contains_key("ports") { + let can_register = map + .get("can_register") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + let port_check_target = map + .get("port_check_target") + .and_then(serde_json::Value::as_str) + .map(ToOwned::to_owned); + let ports = map.get("ports").and_then(serde_json::Value::as_object); + let error = map + .get("error") + .and_then(serde_json::Value::as_str) + .map(ToOwned::to_owned); + + return Some(build_ports_check_summary_json( + can_register, + port_check_target, + ports, + error, + )); + } + + if map.contains_key("all_pass") && map.contains_key("failed_ports") { + let mut normalized = map; + normalized.remove("ports_tested"); + normalized + .entry("port_check_target".to_string()) + .or_insert(serde_json::Value::Null); + normalized + .entry("error".to_string()) + .or_insert(serde_json::Value::Null); + return Some(serde_json::Value::Object(normalized)); + } + + Some(serde_json::Value::Object(map)) +} + macro_rules! serialize_opt_to_value { ($var:expr) => {{ match $var { @@ -48,6 +145,8 @@ pub(crate) struct GatewayDto { pub(crate) explorer_pretty_bond: Option, pub(crate) last_probe_result: Option, pub(crate) last_probe_log: Option, + pub(crate) ports_check: Option, + pub(crate) last_ports_check_utc: Option, pub(crate) last_testrun_utc: Option, pub(crate) last_updated_utc: i64, pub(crate) moniker: String, @@ -73,7 +172,7 @@ impl TryFrom for http::models::Gateway { .explorer_pretty_bond .clone() .unwrap_or("null".to_string()); - let last_probe_result = value + let last_probe_result_raw = value .last_probe_result .clone() .unwrap_or("null".to_string()); @@ -81,7 +180,18 @@ impl TryFrom for http::models::Gateway { let self_described = serde_json::from_str(&self_described).unwrap_or(None); let explorer_pretty_bond = serde_json::from_str(&explorer_pretty_bond).unwrap_or(None); - let last_probe_result = serde_json::from_str(&last_probe_result).unwrap_or(None); + let last_probe_result = serde_json::from_str::(&last_probe_result_raw) + .ok() + .and_then(|v| (!v.is_null()).then_some(v)); + + let ports_check = value + .ports_check + .clone() + .filter(|v| !v.is_null()) + .and_then(normalize_ports_check_payload); + let last_ports_check_utc = value + .last_ports_check_utc + .map(unix_timestamp_to_utc_rfc3339); let bonded = value.bonded; let performance = value.performance as u8; @@ -104,6 +214,8 @@ impl TryFrom for http::models::Gateway { description, last_probe_result, last_probe_log, + ports_check, + last_ports_check_utc, routing_score, config_score, last_testrun_utc, @@ -292,6 +404,7 @@ pub struct TestRunDto { pub id: i32, pub gateway_id: i32, pub status: i32, + pub kind: i16, pub created_utc: i64, pub ip_address: String, pub log: String, @@ -306,6 +419,13 @@ pub(crate) enum TestRunStatus { Queued = 0, } +#[derive(Debug, Clone, Copy, strum_macros::Display, EnumString, FromRepr, PartialEq, Eq)] +#[repr(i16)] +pub(crate) enum TestRunKind { + Probe = 0, + PortsCheck = 1, +} + #[derive(Debug, Clone)] pub struct GatewayIdentityDto { pub gateway_identity_key: String, diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs index 99e178086a..2b7c33da9b 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs @@ -76,6 +76,12 @@ impl Storage { Ok(()) } + gw.last_probe_log as "last_probe_log?", + gw.ports_check as "ports_check?", + + gw.last_testrun_utc as "last_testrun_utc?", + .await?; + .await?; pub(crate) async fn get_all_gateways(&self) -> anyhow::Result> { let items = sqlx::query_as!( GatewayDto, @@ -87,6 +93,8 @@ impl Storage { gw.explorer_pretty_bond as "explorer_pretty_bond?", gw.last_probe_result as "last_probe_result?", gw.last_probe_log as "last_probe_log?", + gw.ports_check as "ports_check?", + gw.last_ports_check_utc as "last_ports_check_utc?", gw.last_testrun_utc as "last_testrun_utc?", gw.last_updated_utc as "last_updated_utc!", gw.bridges as "bridges?: serde_json::Value", @@ -102,7 +110,6 @@ impl Storage { .fetch(&self.pool) .try_collect::>() .await?; - let items: Vec = items .into_iter() .map(|item| item.try_into()) diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs index 71d5d88e60..bc2719cea6 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs @@ -1,8 +1,10 @@ use crate::db::DbConnection; use crate::db::DbPool; -use crate::db::models::{TestRunDto, TestRunStatus}; +use crate::db::models::{TestRunDto, TestRunKind, TestRunStatus}; use crate::http::models::TestrunAssignment; use crate::utils::now_utc; +use sqlx::Row; +use sqlx::types::Json; use time::Duration; pub(crate) async fn count_testruns_in_progress( @@ -32,6 +34,7 @@ pub(crate) async fn get_in_progress_testrun_by_id( id as "id!", gateway_id as "gateway_id!", status as "status!", + kind as "kind!", created_utc as "created_utc!", ip_address as "ip_address!", log as "log!", @@ -90,6 +93,19 @@ pub(crate) async fn update_testruns_assigned_before( pub(crate) async fn assign_oldest_testrun( conn: &mut DbConnection, +) -> anyhow::Result> { + assign_oldest_testrun_by_kind(conn, TestRunKind::Probe).await +} + +pub(crate) async fn assign_oldest_ports_check_testrun( + conn: &mut DbConnection, +) -> anyhow::Result> { + assign_oldest_testrun_by_kind(conn, TestRunKind::PortsCheck).await +} + +async fn assign_oldest_testrun_by_kind( + conn: &mut DbConnection, + kind: TestRunKind, ) -> anyhow::Result> { let now = now_utc().unix_timestamp(); // find & mark as "In progress" in the same transaction to avoid race conditions @@ -120,28 +136,27 @@ pub(crate) async fn assign_oldest_testrun( TestRunStatus::Queued as i32, now, TestRunStatus::InProgress as i32, + kind as i16, ) .fetch_optional(conn.as_mut()) .await?; if let Some(testrun) = returning { - let gw_identity = sqlx::query!( - r#" - SELECT - id, - gateway_identity_key - FROM gateways - WHERE id = $1 - LIMIT 1"#, - testrun.gateway_id + let row = sqlx::query( + r#"SELECT gateway_identity_key, last_ports_check_utc FROM gateways WHERE id = $1 LIMIT 1"#, ) + .bind(testrun.gateway_id) .fetch_one(conn.as_mut()) .await?; + let gateway_identity_key: String = row.try_get("gateway_identity_key")?; + let last_ports_check_utc: Option = row.try_get("last_ports_check_utc")?; + Ok(Some(TestrunAssignment { testrun_id: testrun.id, - gateway_identity_key: gw_identity.gateway_identity_key, + gateway_identity_key, assigned_at_utc: now, + last_ports_check_utc, })) } else { Ok(None) @@ -186,17 +201,121 @@ pub(crate) async fn update_gateway_last_probe_result( gateway_pk: i32, result: &str, ) -> anyhow::Result<()> { - sqlx::query!( - "UPDATE gateways SET last_probe_result = $1 WHERE id = $2", - result, - gateway_pk, + sqlx::query("UPDATE gateways SET last_probe_result = $1 WHERE id = $2") + .bind(result) + .bind(gateway_pk) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +// NOTE: port-check submissions must not re-embed `ports_check` into `last_probe_result`. + +pub(crate) async fn update_gateway_ports_check_only( + conn: &mut DbConnection, + gateway_pk: i32, + port_check_result: &nym_gateway_probe::PortCheckResult, +) -> anyhow::Result<()> { + use crate::db::models::ports_check_summary_json_from_result; + + let now_ts = now_utc().unix_timestamp(); + let value = ports_check_summary_json_from_result(port_check_result); + + sqlx::query( + r#"UPDATE gateways SET + ports_check = $1, + last_ports_check_utc = $2 + WHERE id = $3"#, ) + .bind(Json(value)) + .bind(now_ts) + .bind(gateway_pk) .execute(conn.as_mut()) .await .map(drop) .map_err(From::from) } +pub(crate) async fn insert_external_ports_check_testrun( + conn: &mut DbConnection, + testrun_id: i32, + gateway_id: i32, + assigned_at_utc: i64, +) -> anyhow::Result<()> { + let now = now_utc().unix_timestamp(); + + sqlx::query!( + r#"INSERT INTO testruns ( + id, + gateway_id, + status, + kind, + created_utc, + last_assigned_utc, + ip_address, + log + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"#, + testrun_id, + gateway_id, + TestRunStatus::InProgress as i32, + TestRunKind::PortsCheck as i16, + now, + assigned_at_utc, + "external", + "" + ) + .execute(conn.as_mut()) + .await?; + + tracing::debug!( + "Created external ports-check testrun {} for gateway {}", + testrun_id, + gateway_id + ); + Ok(()) +} + +pub(crate) async fn enqueue_due_ports_check_testruns(db: &DbPool) -> anyhow::Result { + let mut conn = db.acquire().await?; + let now = now_utc().unix_timestamp(); + // 14 days soft TTL for dedicated ports-check queueing. + let cutoff = now - time::Duration::days(14).whole_seconds(); + + let res = sqlx::query!( + r#" + INSERT INTO testruns (gateway_id, status, kind, created_utc, last_assigned_utc, ip_address, log) + SELECT + gw.id, + $1, + $2, + $3, + NULL, + 'ports_check_scheduler', + '' + FROM gateways gw + WHERE gw.bonded = true + AND (gw.last_ports_check_utc IS NULL OR gw.last_ports_check_utc < $4) + AND NOT EXISTS ( + SELECT 1 + FROM testruns t + WHERE t.gateway_id = gw.id + AND t.kind = $2 + AND t.status IN ($1, $5) + ) + "#, + TestRunStatus::Queued as i32, + TestRunKind::PortsCheck as i16, + now, + cutoff, + TestRunStatus::InProgress as i32, + ) + .execute(conn.as_mut()) + .await?; + + Ok(res.rows_affected()) +} + pub(crate) async fn update_gateway_score( conn: &mut DbConnection, gateway_pk: i32, @@ -224,6 +343,7 @@ pub(crate) async fn get_testrun_by_id( id, gateway_id, status, + kind, created_utc, ip_address, log, @@ -250,14 +370,16 @@ pub(crate) async fn insert_external_testrun( id, gateway_id, status, + kind, created_utc, last_assigned_utc, ip_address, log - ) VALUES ($1, $2, $3, $4, $5, $6, $7)"#, + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"#, testrun_id, gateway_id, TestRunStatus::InProgress as i32, + TestRunKind::Probe as i16, now, assigned_at_utc, "external", // Marker for external origin diff --git a/nym-node-status-api/nym-node-status-api/src/db/tests.rs b/nym-node-status-api/nym-node-status-api/src/db/tests.rs index 5fbb6ac712..2734612da2 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/tests.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/tests.rs @@ -11,6 +11,8 @@ mod db_tests { explorer_pretty_bond: Some("{\"key\":\"value\"}".to_string()), last_probe_result: Some("{\"key\":\"value\"}".to_string()), last_probe_log: Some("log".to_string()), + ports_check: None, + last_ports_check_utc: None, last_testrun_utc: Some(1672531200), last_updated_utc: 1672531200, moniker: "moniker".to_string(), @@ -37,6 +39,101 @@ mod db_tests { assert_eq!(http_gateway.description.details, "details"); } + #[test] + fn test_gateway_dto_normalizes_legacy_dedicated_ports_check_shape() { + let legacy_ports = serde_json::json!({ + "gateway": "gw1", + "can_register": true, + "port_check_target": "portquiz.net", + "ports": { + "20": false, + "21": true, + "43": false + }, + "error": null + }); + let gateway_dto = crate::db::models::GatewayDto { + gateway_identity_key: "id1".to_string(), + bonded: true, + performance: 50, + self_described: Some("{}".to_string()), + explorer_pretty_bond: Some("{}".to_string()), + last_probe_result: None, + last_probe_log: None, + ports_check: Some(legacy_ports), + last_ports_check_utc: Some(1672531200), + last_testrun_utc: None, + last_updated_utc: 1672531200, + moniker: "m".to_string(), + security_contact: "c".to_string(), + details: "d".to_string(), + website: "w".to_string(), + bridges: None, + }; + + let http_gateway: crate::http::models::Gateway = gateway_dto.try_into().unwrap(); + let normalized = http_gateway.ports_check.unwrap(); + + assert_eq!( + normalized.get("all_pass"), + Some(&serde_json::Value::Bool(false)) + ); + assert_eq!( + normalized.get("port_check_target"), + Some(&serde_json::Value::String("portquiz.net".to_string())) + ); + assert_eq!(normalized.get("error"), Some(&serde_json::Value::Null)); + assert_eq!( + normalized.get("failed_ports"), + Some(&serde_json::json!(["20", "43"])) + ); + assert!(normalized.get("gateway").is_none()); + assert!(normalized.get("ports").is_none()); + } + + #[test] + fn test_gateway_dto_normalizes_legacy_in_probe_summary_shape() { + let legacy_summary = serde_json::json!({ + "all_pass": true, + "failed_ports": [], + "error": null, + "ports_tested": 32 + }); + let gateway_dto = crate::db::models::GatewayDto { + gateway_identity_key: "id2".to_string(), + bonded: true, + performance: 50, + self_described: Some("{}".to_string()), + explorer_pretty_bond: Some("{}".to_string()), + last_probe_result: None, + last_probe_log: None, + ports_check: Some(legacy_summary), + last_ports_check_utc: Some(1672531200), + last_testrun_utc: None, + last_updated_utc: 1672531200, + moniker: "m".to_string(), + security_contact: "c".to_string(), + details: "d".to_string(), + website: "w".to_string(), + bridges: None, + }; + + let http_gateway: crate::http::models::Gateway = gateway_dto.try_into().unwrap(); + let normalized = http_gateway.ports_check.unwrap(); + + assert_eq!( + normalized.get("all_pass"), + Some(&serde_json::Value::Bool(true)) + ); + assert_eq!(normalized.get("failed_ports"), Some(&serde_json::json!([]))); + assert_eq!(normalized.get("error"), Some(&serde_json::Value::Null)); + assert_eq!( + normalized.get("port_check_target"), + Some(&serde_json::Value::Null) + ); + assert!(normalized.get("ports_tested").is_none()); + } + #[test] fn test_mixnode_dto_try_from() { let mixnode_dto = crate::db::models::MixnodeDto { @@ -267,6 +364,8 @@ fn test_gateway_dto_with_null_values() { explorer_pretty_bond: None, last_probe_result: None, last_probe_log: None, + ports_check: None, + last_ports_check_utc: None, last_testrun_utc: None, last_updated_utc: 0, moniker: "".to_string(), diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/gateways.rs b/nym-node-status-api/nym-node-status-api/src/http/api/gateways.rs index 0270de9abf..f52df6bbad 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/gateways.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/gateways.rs @@ -106,6 +106,8 @@ fn filter_bonded_gateways_to_skinny(gateways: Vec) -> Vec Router { Router::new() .route("/", axum::routing::get(request_testrun)) - .route("/{testrun_id}", axum::routing::post(submit_testrun)) - .route("/{testrun_id}/v2", axum::routing::post(submit_testrun_v2)) + .route( + "/ports-check", + axum::routing::get(request_ports_check_testrun), + ) + .route("/:testrun_id", axum::routing::post(submit_testrun)) + .route("/:testrun_id/v2", axum::routing::post(submit_testrun_v2)) + .route( + "/:testrun_id/ports-check/v2", + axum::routing::post(submit_ports_check_testrun_v2), + ) .layer(DefaultBodyLimit::max(1024 * 1024 * 5)) } @@ -117,6 +126,64 @@ async fn request_testrun( } } +#[tracing::instrument(level = "debug", skip_all)] +async fn request_ports_check_testrun( + State(state): State, + Json(request): Json, +) -> HttpResult> { + state.authenticate_agent_submission(&request)?; + state.is_fresh(&request.payload.timestamp)?; + + tracing::debug!("Agent requested ports-check testrun"); + + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + let active_testruns = db::queries::testruns::count_testruns_in_progress(&mut conn) + .await + .map_err(HttpError::internal_with_logging)? + .unwrap_or_default(); + let max_count = state.agent_max_count(); + if active_testruns >= max_count { + tracing::warn!("{active_testruns}/{max_count} testruns in progress, rejecting",); + return Err(HttpError::no_testruns_available()); + } + + match db::queries::testruns::assign_oldest_ports_check_testrun(&mut conn).await { + Ok(res) => { + let Some(assignment) = res else { + tracing::debug!("No ports-check testruns available"); + return Err(HttpError::no_testruns_available()); + }; + + tracing::info!( + "🏃‍ Assigned ports-check testrun row_id {} gateway {} to agent", + &assignment.testrun_id, + assignment.gateway_identity_key, + ); + + let materials = state + .ticketbook_manager_state() + .attempt_assign_ticket_materials(assignment.testrun_id) + .await + .map_err(|err| { + error!( + "failed to get ticket materials for runner {}: {err}", + assignment.testrun_id + ); + HttpError::internal_with_logging(format!( + "could not retrieve needed tickets: {err}" + )) + })?; + Ok(Json(assignment.with_ticket_materials(materials))) + } + Err(err) => Err(HttpError::internal_with_logging(err)), + } +} + #[tracing::instrument(level = "debug", skip_all)] async fn submit_testrun( Path(submitted_testrun_id): Path, @@ -374,3 +441,148 @@ async fn process_testrun_submission_by_gateway( Ok(StatusCode::CREATED) } + +#[tracing::instrument(level = "debug", skip_all)] +async fn submit_ports_check_testrun_v2( + Path(submitted_testrun_id): Path, + State(state): State, + Json(submission): Json, +) -> HttpResult { + state.authenticate_agent_submission(&submission)?; + + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + match queries::testruns::get_testrun_by_id(&mut conn, submitted_testrun_id).await { + Ok(testrun) => { + if testrun.kind != TestRunKind::PortsCheck as i16 { + tracing::warn!( + "Testrun {} has wrong kind for ports-check submit: {}", + submitted_testrun_id, + testrun.kind + ); + return Err(HttpError::invalid_input(format!( + "Testrun {} is not a ports-check run", + submitted_testrun_id + ))); + } + + let gw_identity = queries::select_gateway_identity(&mut conn, testrun.gateway_id) + .await + .map_err(HttpError::internal_with_logging)?; + + if gw_identity != submission.payload.gateway_identity_key { + tracing::warn!( + "Gateway mismatch for ports-check testrun {}: expected {}, got {}", + submitted_testrun_id, + gw_identity, + submission.payload.gateway_identity_key + ); + return Err(HttpError::invalid_input("Gateway identity mismatch")); + } + + if Some(submission.payload.assigned_at_utc) != testrun.last_assigned_utc { + tracing::warn!( + "Submitted ports-check testrun timestamp mismatch: {} != {:?}, rejecting", + submission.payload.assigned_at_utc, + testrun.last_assigned_utc + ); + return Err(HttpError::invalid_input(format!( + "Ports-check testrun {} timestamp mismatch: expected {:?}, got {}", + submitted_testrun_id, + testrun.last_assigned_utc, + submission.payload.assigned_at_utc + ))); + } + + process_ports_check_submission( + submitted_testrun_id, + testrun.gateway_id, + submission.payload, + &mut conn, + ) + .await + } + Err(_) => { + tracing::info!( + "Creating external ports-check testrun {} for gateway {}", + submitted_testrun_id, + submission.payload.gateway_identity_key + ); + + let gateway_id = + queries::get_or_create_gateway(&mut conn, &submission.payload.gateway_identity_key) + .await + .map_err(HttpError::internal_with_logging)?; + + queries::testruns::insert_external_ports_check_testrun( + &mut conn, + submitted_testrun_id, + gateway_id, + submission.payload.assigned_at_utc, + ) + .await + .map_err(HttpError::internal_with_logging)?; + + process_ports_check_submission( + submitted_testrun_id, + gateway_id, + submission.payload, + &mut conn, + ) + .await + } + } +} + +async fn process_ports_check_submission( + testrun_id: i32, + gateway_id: i32, + payload: submit_ports_check_results_v2::Payload, + conn: &mut DbConnection, +) -> HttpResult { + let gw_identity = &payload.gateway_identity_key; + let open_ports = payload + .port_check_result + .ports + .values() + .filter(|&&v| v) + .count(); + let total_ports = payload.port_check_result.ports.len(); + + tracing::debug!( + "Processing ports-check submission for gateway {} ({} bytes, {}/{} ports open, can_register={})", + gw_identity, + payload.probe_log.len(), + open_ports, + total_ports, + payload.port_check_result.can_register, + ); + + queries::testruns::update_gateway_ports_check_only( + conn, + gateway_id, + &payload.port_check_result, + ) + .await + .map_err(HttpError::internal_with_logging)?; + + queries::testruns::update_testrun_status(conn, testrun_id, TestRunStatus::Complete) + .await + .map_err(HttpError::internal_with_logging)?; + + let assigned_at = unix_timestamp_to_utc_rfc3339(payload.assigned_at_utc); + let now = now_utc(); + tracing::info!( + "✅ Ports-check testrun {} for gateway {} complete (assigned at {}, current time {})", + testrun_id, + gw_identity, + assigned_at, + now + ); + + Ok(StatusCode::CREATED) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/models/gw_probe/test.rs b/nym-node-status-api/nym-node-status-api/src/http/models/gw_probe/test.rs index e88512b5d3..dd06ce0450 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models/gw_probe/test.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models/gw_probe/test.rs @@ -20,6 +20,8 @@ fn test_weighted_score_calculation() { }, last_probe_result: None, last_probe_log: None, + ports_check: None, + last_ports_check_utc: None, last_testrun_utc: None, last_updated_utc: "2025-10-10T00:00:00Z".to_string(), routing_score: 0.0, @@ -163,10 +165,12 @@ fn conversion_from_gw_probe_latest() { downloaded_file_size_bytes_v6: 20971520, downloaded_file_v6: "test-file-v6.bin".to_string(), download_error_v6: "none-v6".to_string(), + port_check_results: None, }; let probe_latest = ProbeResultLatest { node: "test-node-identity-key".to_string(), used_entry: "test-entry-node".to_string(), + ports_check: None, outcome: ProbeOutcomeLatest { as_entry: EntryLatest::Tested(EntryTestResultLatest { can_connect: true, diff --git a/nym-node-status-api/nym-node-status-api/src/http/models/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/models/mod.rs index dfb93f9c52..fb3f354fa2 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models/mod.rs @@ -44,6 +44,8 @@ pub struct Gateway { pub description: NodeDescription, pub last_probe_result: Option, pub last_probe_log: Option, + pub ports_check: Option, + pub last_ports_check_utc: Option, pub last_testrun_utc: Option, pub last_updated_utc: String, pub routing_score: f32, @@ -676,6 +678,8 @@ mod test { self_described: Some(serde_json::json!({"test": "value"})), explorer_pretty_bond: None, last_probe_result: Some(serde_json::json!({"status": "ok"})), + ports_check: None, + last_ports_check_utc: None, last_testrun_utc: Some("2024-01-20T10:00:00Z".to_string()), last_updated_utc: "2024-01-20T11:00:00Z".to_string(), routing_score: 0.95, @@ -738,6 +742,8 @@ pub struct GatewaySkinny { pub self_described: Option, pub explorer_pretty_bond: Option, pub last_probe_result: Option, + pub ports_check: Option, + pub last_ports_check_utc: Option, pub last_testrun_utc: Option, pub last_updated_utc: String, pub routing_score: f32, diff --git a/nym-node-status-api/nym-node-status-api/src/http/server.rs b/nym-node-status-api/nym-node-status-api/src/http/server.rs index 0127cc0fc1..13011a3472 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/server.rs @@ -9,6 +9,7 @@ use core::net::SocketAddr; use nym_crypto::asymmetric::ed25519::PublicKey; use nym_task::ShutdownTracker; use std::sync::Arc; +use tokio::time::MissedTickBehavior; use tokio::{net::TcpListener, sync::RwLock}; /// Return handles that allow for graceful shutdown of server + awaiting its @@ -27,6 +28,7 @@ pub(crate) async fn start_http_api( shutdown_tracker: &ShutdownTracker, ) -> anyhow::Result<()> { let router_builder = RouterBuilder::with_default_routes(); + let db_pool_for_scheduler = db_pool.clone(); let state = AppState::new( storage, @@ -46,6 +48,34 @@ pub(crate) async fn start_http_api( let server = router.build_server(bind_addr).await?; let shutdown = shutdown_tracker.clone_shutdown_token().cancelled_owned(); + let ports_check_scheduler_enabled = std::env::var("PORTS_CHECK_SCHEDULER_ENABLED") + .ok() + .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes" | "YES")) + .unwrap_or(true); + + if ports_check_scheduler_enabled { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(60 * 10)); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + let scheduler_shutdown = shutdown_tracker.clone_shutdown_token().cancelled_owned(); + shutdown_tracker.spawn(async move { + tokio::select! { + _ = async { + loop { + interval.tick().await; + match crate::db::queries::testruns::enqueue_due_ports_check_testruns(&db_pool_for_scheduler).await { + Ok(enqueued) if enqueued > 0 => tracing::info!("Enqueued {enqueued} due ports-check testruns"), + Ok(_) => {} + Err(e) => tracing::warn!("Failed to enqueue due ports-check testruns: {e}"), + } + } + } => {}, + _ = scheduler_shutdown => {} + } + }); + } else { + tracing::info!("Ports-check scheduler disabled (PORTS_CHECK_SCHEDULER_ENABLED=false)"); + } + shutdown_tracker.spawn(async move { axum::serve( server.listener, diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index 1776ea23e3..1d8affed94 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -134,10 +134,10 @@ impl AppState { return Err(HttpError::unauthorized()); }; - request.verify_signature().map_err(|_| { + if request.verify_signature().is_err() { tracing::warn!("Signature verification failed, rejecting"); - HttpError::unauthorized() - })?; + return Err(HttpError::unauthorized()); + } Ok(()) } diff --git a/nym-node-status-api/nym-node-status-api/src/main.rs b/nym-node-status-api/nym-node-status-api/src/main.rs index c392edb880..e8ae6b6954 100644 --- a/nym-node-status-api/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/nym-node-status-api/src/main.rs @@ -130,7 +130,12 @@ async fn main() -> anyhow::Result<()> { let pool = storage.pool_owned(); shutdown_manager.spawn_with_shutdown(async move { - testruns::start(pool, args.testruns_refresh_interval).await + testruns::start( + pool, + args.testruns_refresh_interval, + args.testruns_stale_in_progress, + ) + .await }); let db_pool_scraper = storage.pool_owned(); diff --git a/nym-node-status-api/nym-node-status-api/src/test_helpers/mod.rs b/nym-node-status-api/nym-node-status-api/src/test_helpers/mod.rs index ee201d555d..8173a6647c 100644 --- a/nym-node-status-api/nym-node-status-api/src/test_helpers/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/test_helpers/mod.rs @@ -29,10 +29,13 @@ pub mod builders { }, last_probe_result: None, last_probe_log: None, + ports_check: None, + last_ports_check_utc: None, last_testrun_utc: None, last_updated_utc: "2024-01-01T00:00:00Z".to_string(), routing_score: 0.95, config_score: 100, + bridges: None, }, } } diff --git a/nym-node-status-api/nym-node-status-api/src/testruns/mod.rs b/nym-node-status-api/nym-node-status-api/src/testruns/mod.rs index eb212a2b11..e17f44c2a8 100644 --- a/nym-node-status-api/nym-node-status-api/src/testruns/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/testruns/mod.rs @@ -8,9 +8,13 @@ use tracing::instrument; pub(crate) mod models; mod queue; -pub(crate) async fn start(pool: DbPool, refresh_interval: Duration) { +pub(crate) async fn start( + pool: DbPool, + refresh_interval: Duration, + stale_in_progress_after: Duration, +) { loop { - if let Err(e) = refresh_stale_testruns(&pool, refresh_interval).await { + if let Err(e) = refresh_stale_testruns(&pool, stale_in_progress_after).await { tracing::error!("{e}"); } diff --git a/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs b/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs index 8a283f3464..f64164972d 100644 --- a/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs +++ b/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs @@ -1,5 +1,5 @@ use crate::db::DbConnection; -use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus}; +use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunKind, TestRunStatus}; use crate::testruns::models::TestRun; use crate::utils::now_utc; use anyhow::anyhow; @@ -53,15 +53,17 @@ pub(crate) async fn try_queue_testrun( id, gateway_id, status, + kind, created_utc, ip_address, log, last_assigned_utc FROM testruns - WHERE gateway_id = $1 AND status != 2 + WHERE gateway_id = $1 AND status != 2 AND kind = $2 ORDER BY id DESC LIMIT 1"#, - gateway_id + gateway_id, + TestRunKind::Probe as i16 ) .fetch(conn.as_mut()) .try_collect::>() diff --git a/nym-node-status-api/nym-node-status-client/src/api.rs b/nym-node-status-api/nym-node-status-client/src/api.rs index c41b25bf89..4f1c1029ea 100644 --- a/nym-node-status-api/nym-node-status-client/src/api.rs +++ b/nym-node-status-api/nym-node-status-client/src/api.rs @@ -12,6 +12,10 @@ impl ApiPaths { format!("{}/internal/testruns", self.server_address) } + pub(super) fn request_ports_check_testrun(&self) -> String { + format!("{}/internal/testruns/ports-check", self.server_address) + } + pub(super) fn submit_results(&self, testrun_id: impl Display) -> String { format!("{}/internal/testruns/{}", self.server_address, testrun_id) } @@ -22,4 +26,11 @@ impl ApiPaths { self.server_address, testrun_id ) } + + pub(super) fn submit_ports_check_results_v2(&self, testrun_id: impl Display) -> String { + format!( + "{}/internal/testruns/{}/ports-check/v2", + self.server_address, testrun_id + ) + } } diff --git a/nym-node-status-api/nym-node-status-client/src/lib.rs b/nym-node-status-api/nym-node-status-client/src/lib.rs index 6d118062fc..73de44b361 100644 --- a/nym-node-status-api/nym-node-status-client/src/lib.rs +++ b/nym-node-status-api/nym-node-status-client/src/lib.rs @@ -1,4 +1,7 @@ -use crate::models::{TestrunAssignmentWithTickets, get_testrun, submit_results, submit_results_v2}; +use crate::models::{ + TestrunAssignmentWithTickets, get_testrun, submit_ports_check_results_v2, submit_results, + submit_results_v2, +}; use anyhow::bail; use api::ApiPaths; use nym_crypto::asymmetric::ed25519::{PrivateKey, Signature}; @@ -71,7 +74,53 @@ impl NsApiClient { testrun }) .map_err(|err| { - tracing::error!("err"); + tracing::error!("{err}"); + err.into() + }) + } + + #[instrument(level = "info", skip_all)] + pub async fn request_ports_check_testrun( + &self, + ) -> anyhow::Result> { + let target_url = self.api.request_ports_check_testrun(); + + let payload = get_testrun::Payload { + agent_public_key: self.auth_key.public_key(), + timestamp: time::UtcDateTime::now().unix_timestamp(), + }; + let signature = self.sign_message(&payload)?; + let request = get_testrun::GetTestrunRequest { payload, signature }; + + let res = self.client.get(target_url).json(&request).send().await?; + let status = res.status(); + let response_text = res.text().await?; + + if status.is_client_error() { + if matches!(status, reqwest::StatusCode::NOT_FOUND) { + tracing::warn!( + "Ports-check request endpoint not available on server (404), skipping iteration" + ); + return Ok(None); + } + bail!("{}: {}", status, response_text); + } else if status.is_server_error() { + if matches!(status, reqwest::StatusCode::SERVICE_UNAVAILABLE) + && response_text.contains("No testruns available") + { + return Ok(None); + } else { + bail!("{}: {}", status, response_text); + } + } + + serde_json::from_str(&response_text) + .map(|testrun| { + tracing::info!("Received ports-check testrun assignment: {:?}", testrun); + testrun + }) + .map_err(|err| { + tracing::error!("{err}"); err.into() }) } @@ -151,6 +200,40 @@ impl NsApiClient { Ok(()) } + #[instrument(level = "info", skip(self, port_check_result))] + pub async fn submit_ports_check_results_with_context( + &self, + testrun_id: i32, + port_check_result: nym_gateway_probe::PortCheckResult, + probe_log: String, + assigned_at_utc: i64, + gateway_identity_key: String, + ) -> anyhow::Result<()> { + let target_url = self.api.submit_ports_check_results_v2(testrun_id); + + let payload = submit_ports_check_results_v2::Payload { + port_check_result, + probe_log, + agent_public_key: self.auth_key.public_key(), + assigned_at_utc, + gateway_identity_key, + }; + let signature = self.sign_message(&payload)?; + let submit = + submit_ports_check_results_v2::SubmitPortsCheckResultsV2 { payload, signature }; + + let res = self + .client + .post(target_url) + .json(&submit) + .send() + .await + .and_then(|response| response.error_for_status())?; + + tracing::debug!("Submitted ports-check results: {})", res.status()); + Ok(()) + } + fn sign_message(&self, message: &T) -> anyhow::Result where T: serde::Serialize, diff --git a/nym-node-status-api/nym-node-status-client/src/models.rs b/nym-node-status-api/nym-node-status-client/src/models.rs index 4b362f24fc..00935c5a17 100644 --- a/nym-node-status-api/nym-node-status-client/src/models.rs +++ b/nym-node-status-api/nym-node-status-client/src/models.rs @@ -41,6 +41,9 @@ pub struct TestrunAssignment { pub testrun_id: i32, pub assigned_at_utc: i64, pub gateway_identity_key: String, + /// Unix timestamp (seconds) of the last persisted exit-policy port check for this gateway. + #[serde(default)] + pub last_ports_check_utc: Option, } impl TestrunAssignment { @@ -143,3 +146,39 @@ pub mod submit_results_v2 { } } } + +pub mod submit_ports_check_results_v2 { + use crate::auth::SignedRequest; + + use super::*; + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct Payload { + pub port_check_result: nym_gateway_probe::PortCheckResult, + pub probe_log: String, + pub agent_public_key: PublicKey, + pub assigned_at_utc: i64, + pub gateway_identity_key: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct SubmitPortsCheckResultsV2 { + pub payload: Payload, + pub signature: Signature, + } + + impl SignedRequest for SubmitPortsCheckResultsV2 { + type Payload = Payload; + + fn public_key(&self) -> &PublicKey { + &self.payload.agent_public_key + } + + fn signature(&self) -> &Signature { + &self.signature + } + + fn payload(&self) -> &Self::Payload { + &self.payload + } + } +} From 1532c0c16e3345402c7ff67562ec25d3227a73df Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 11 May 2026 09:34:44 +0200 Subject: [PATCH 02/17] addressing ai comments --- .../src/cli/run_ports_check.rs | 5 +++-- .../nym-node-status-agent/src/cli/run_probe.rs | 3 ++- .../20260415000000_gateways_ports_check.sql | 2 ++ .../nym-node-status-api/src/cli/mod.rs | 2 +- .../nym-node-status-client/src/lib.rs | 16 ++++++++++++---- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs b/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs index 3fa6b2f2bf..c4bc641b6f 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs @@ -55,14 +55,15 @@ pub(crate) async fn run_ports_check( let credentials_args = common::credential_args_from(testrun.ticket_materials); log_capture.start(); - let port_check_result = nym_gateway_probe::Probe::run_ports_for_agent( + let port_check_result_res = nym_gateway_probe::Probe::run_ports_for_agent( gateway_identity_pubkey, network, &run_ports_config, credentials_args, ) - .await?; + .await; let probe_log = log_capture.stop_and_drain(); + let port_check_result = port_check_result_res?; submit_ports_check_results_to_servers( servers, diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs b/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs index 6e4da33cab..2e5aca47cd 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs @@ -48,8 +48,9 @@ pub(crate) async fn run_probe( // Run the probe, capturing all tracing output log_capture.start(); - let probe_result = Box::pin(probe.probe_run_agent(credentials_args)).await?; + let probe_result_res = Box::pin(probe.probe_run_agent(credentials_args)).await; let probe_log = log_capture.stop_and_drain(); + let probe_result = probe_result_res?; // Inspect the probe output for socks5 field match probe_result.outcome.socks5.as_ref() { diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql index e521cb2aa5..f506b093f5 100644 --- a/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql @@ -4,6 +4,7 @@ UPDATE gateways SET ports_check = (last_probe_result::jsonb -> 'ports_check') WHERE last_probe_result IS NOT NULL AND btrim(last_probe_result) <> '' + AND last_probe_result ~ '^[\[{]' AND last_probe_result::jsonb ? 'ports_check' AND ports_check IS NULL; @@ -11,4 +12,5 @@ UPDATE gateways SET last_probe_result = (last_probe_result::jsonb - 'ports_check')::text WHERE last_probe_result IS NOT NULL AND btrim(last_probe_result) <> '' + AND last_probe_result ~ '^[\[{]' AND last_probe_result::jsonb ? 'ports_check'; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs index 29b5b30657..18bc84f48b 100644 --- a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs @@ -82,7 +82,7 @@ pub(crate) struct Cli { #[arg(value_parser = parse_duration_std)] pub(crate) testruns_refresh_interval: Duration, - /// 2 hour safety net for test runs which include a port check + /// Safety net for stale in-progress testruns (whole seconds, es. 7200). #[clap( long, default_value = "7200", diff --git a/nym-node-status-api/nym-node-status-client/src/lib.rs b/nym-node-status-api/nym-node-status-client/src/lib.rs index 73de44b361..27aac8b538 100644 --- a/nym-node-status-api/nym-node-status-client/src/lib.rs +++ b/nym-node-status-api/nym-node-status-client/src/lib.rs @@ -69,8 +69,12 @@ impl NsApiClient { } serde_json::from_str(&response_text) - .map(|testrun| { - tracing::info!("Received testrun assignment: {:?}", testrun); + .map(|testrun: TestrunAssignmentWithTickets| { + tracing::info!( + testrun_id = testrun.assignment.testrun_id, + gateway = %testrun.assignment.gateway_identity_key, + "Received testrun assignment", + ); testrun }) .map_err(|err| { @@ -115,8 +119,12 @@ impl NsApiClient { } serde_json::from_str(&response_text) - .map(|testrun| { - tracing::info!("Received ports-check testrun assignment: {:?}", testrun); + .map(|testrun: TestrunAssignmentWithTickets| { + tracing::info!( + testrun_id = testrun.assignment.testrun_id, + gateway = %testrun.assignment.gateway_identity_key, + "Received ports-check testrun assignment", + ); testrun }) .map_err(|err| { From d791e08fac56dc34858ec8753afdba9985c36a6e Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 18 May 2026 11:37:37 +0200 Subject: [PATCH 03/17] add port check to dvpn endpoint --- .../nym-node-status-api/src/http/models/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/http/models/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/models/mod.rs index fb3f354fa2..750f564df4 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models/mod.rs @@ -357,21 +357,20 @@ pub struct DVpnGateway { pub role: NodeRole, pub entry: Option, pub bridges: Option, - // The performance data here originates from the nym-api, and is effectively mixnet performance // at the time of writing this pub performance: String, - // Node performance information needed by the NymVPN UI / Explorer to show more information // about the node in a user-friendly way pub performance_v2: Option, - pub lewes_protocol_details: Option, pub family_data: Option, pub staking_data: Option, pub build_information: BinaryBuildInformationOwned, + pub ports_check: Option, + pub last_ports_check_utc: Option, } impl DVpnGateway { @@ -434,6 +433,8 @@ impl DVpnGateway { family_data: family_details, staking_data: staking_details, build_information: self_described.build_information, + ports_check: gateway.ports_check, + last_ports_check_utc: gateway.last_ports_check_utc, }) } } From ff0ecc95fb114abd8c61ff605b11be63f6814962 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Tue, 19 May 2026 15:21:00 +0200 Subject: [PATCH 04/17] fix compile error --- .../nym-node-status-client/src/lib.rs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/nym-node-status-api/nym-node-status-client/src/lib.rs b/nym-node-status-api/nym-node-status-client/src/lib.rs index 27aac8b538..20370a5d63 100644 --- a/nym-node-status-api/nym-node-status-client/src/lib.rs +++ b/nym-node-status-api/nym-node-status-client/src/lib.rs @@ -68,19 +68,19 @@ impl NsApiClient { } } - serde_json::from_str(&response_text) - .map(|testrun: TestrunAssignmentWithTickets| { - tracing::info!( - testrun_id = testrun.assignment.testrun_id, - gateway = %testrun.assignment.gateway_identity_key, - "Received testrun assignment", - ); - testrun - }) - .map_err(|err| { + let testrun: TestrunAssignmentWithTickets = + serde_json::from_str(&response_text).map_err(|err| { tracing::error!("{err}"); - err.into() - }) + anyhow::Error::from(err) + })?; + + tracing::info!( + testrun_id = testrun.assignment.testrun_id, + gateway = %testrun.assignment.gateway_identity_key, + "Received testrun assignment", + ); + + Ok(Some(testrun)) } #[instrument(level = "info", skip_all)] @@ -118,19 +118,19 @@ impl NsApiClient { } } - serde_json::from_str(&response_text) - .map(|testrun: TestrunAssignmentWithTickets| { - tracing::info!( - testrun_id = testrun.assignment.testrun_id, - gateway = %testrun.assignment.gateway_identity_key, - "Received ports-check testrun assignment", - ); - testrun - }) - .map_err(|err| { + let testrun: TestrunAssignmentWithTickets = + serde_json::from_str(&response_text).map_err(|err| { tracing::error!("{err}"); - err.into() - }) + anyhow::Error::from(err) + })?; + + tracing::info!( + testrun_id = testrun.assignment.testrun_id, + gateway = %testrun.assignment.gateway_identity_key, + "Received ports-check testrun assignment", + ); + + Ok(Some(testrun)) } #[instrument(level = "info", fields(testrun_id, assigned_at_utc), skip_all)] @@ -171,7 +171,7 @@ impl NsApiClient { response.error_for_status() })?; - tracing::debug!("Submitted results: {})", res.status()); + tracing::debug!("Submitted results: {}", res.status()); Ok(()) } @@ -204,7 +204,7 @@ impl NsApiClient { .await .and_then(|response| response.error_for_status())?; - tracing::debug!("Submitted results with context: {})", res.status()); + tracing::debug!("Submitted results with context: {}", res.status()); Ok(()) } @@ -238,7 +238,7 @@ impl NsApiClient { .await .and_then(|response| response.error_for_status())?; - tracing::debug!("Submitted ports-check results: {})", res.status()); + tracing::debug!("Submitted ports-check results: {}", res.status()); Ok(()) } From c9a9940cb9210e29af683898b147dd3c656de4cf Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Tue, 19 May 2026 15:34:58 +0200 Subject: [PATCH 05/17] migration fix --- .../20260415000000_gateways_ports_check.sql | 2 -- ...60519133000_gateways_ports_check_json_guard.sql | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 nym-node-status-api/nym-node-status-api/migrations_pg/20260519133000_gateways_ports_check_json_guard.sql diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql index f506b093f5..e521cb2aa5 100644 --- a/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql @@ -4,7 +4,6 @@ UPDATE gateways SET ports_check = (last_probe_result::jsonb -> 'ports_check') WHERE last_probe_result IS NOT NULL AND btrim(last_probe_result) <> '' - AND last_probe_result ~ '^[\[{]' AND last_probe_result::jsonb ? 'ports_check' AND ports_check IS NULL; @@ -12,5 +11,4 @@ UPDATE gateways SET last_probe_result = (last_probe_result::jsonb - 'ports_check')::text WHERE last_probe_result IS NOT NULL AND btrim(last_probe_result) <> '' - AND last_probe_result ~ '^[\[{]' AND last_probe_result::jsonb ? 'ports_check'; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20260519133000_gateways_ports_check_json_guard.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20260519133000_gateways_ports_check_json_guard.sql new file mode 100644 index 0000000000..bcdb3b4b7d --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20260519133000_gateways_ports_check_json_guard.sql @@ -0,0 +1,14 @@ +UPDATE gateways +SET ports_check = (last_probe_result::jsonb -> 'ports_check') +WHERE last_probe_result IS NOT NULL + AND btrim(last_probe_result) <> '' + AND last_probe_result ~ '^[\[{]' + AND last_probe_result::jsonb ? 'ports_check' + AND ports_check IS NULL; + +UPDATE gateways +SET last_probe_result = (last_probe_result::jsonb - 'ports_check')::text +WHERE last_probe_result IS NOT NULL + AND btrim(last_probe_result) <> '' + AND last_probe_result ~ '^[\[{]' + AND last_probe_result::jsonb ? 'ports_check'; From 17d3791b8e90a032f7f91a69c71c4f1bea6e43f3 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Wed, 20 May 2026 09:36:47 +0200 Subject: [PATCH 06/17] typo --- .../nym-node-status-api/src/db/queries/gateways.rs | 6 ------ .../nym-node-status-api/src/http/models/gw_probe/test.rs | 1 - nym-node-status-api/nym-node-status-api/src/http/server.rs | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs index 2b7c33da9b..055498fa1c 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs @@ -76,12 +76,6 @@ impl Storage { Ok(()) } - gw.last_probe_log as "last_probe_log?", - gw.ports_check as "ports_check?", - - gw.last_testrun_utc as "last_testrun_utc?", - .await?; - .await?; pub(crate) async fn get_all_gateways(&self) -> anyhow::Result> { let items = sqlx::query_as!( GatewayDto, diff --git a/nym-node-status-api/nym-node-status-api/src/http/models/gw_probe/test.rs b/nym-node-status-api/nym-node-status-api/src/http/models/gw_probe/test.rs index dd06ce0450..d244fbfaa3 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models/gw_probe/test.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models/gw_probe/test.rs @@ -170,7 +170,6 @@ fn conversion_from_gw_probe_latest() { let probe_latest = ProbeResultLatest { node: "test-node-identity-key".to_string(), used_entry: "test-entry-node".to_string(), - ports_check: None, outcome: ProbeOutcomeLatest { as_entry: EntryLatest::Tested(EntryTestResultLatest { can_connect: true, diff --git a/nym-node-status-api/nym-node-status-api/src/http/server.rs b/nym-node-status-api/nym-node-status-api/src/http/server.rs index 13011a3472..356fb7cdc9 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/server.rs @@ -28,7 +28,7 @@ pub(crate) async fn start_http_api( shutdown_tracker: &ShutdownTracker, ) -> anyhow::Result<()> { let router_builder = RouterBuilder::with_default_routes(); - let db_pool_for_scheduler = db_pool.clone(); + let db_pool_for_scheduler = storage.pool().clone(); let state = AppState::new( storage, From 23e6169c020b735311b5bf11c64c88051e417999 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Wed, 20 May 2026 10:16:55 +0200 Subject: [PATCH 07/17] update sqlx --- ...671cd0c8c28b3f1ed049f008d14b711ef788.json} | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) rename nym-node-status-api/nym-node-status-api/.sqlx/{query-1668f5a5a0abc8a73454953c3f5b61d2afb1b37720f5756b9c6fb3aef55a3027.json => query-9c077a2322ad141cfae4c81d1046671cd0c8c28b3f1ed049f008d14b711ef788.json} (67%) diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-1668f5a5a0abc8a73454953c3f5b61d2afb1b37720f5756b9c6fb3aef55a3027.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-9c077a2322ad141cfae4c81d1046671cd0c8c28b3f1ed049f008d14b711ef788.json similarity index 67% rename from nym-node-status-api/nym-node-status-api/.sqlx/query-1668f5a5a0abc8a73454953c3f5b61d2afb1b37720f5756b9c6fb3aef55a3027.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-9c077a2322ad141cfae4c81d1046671cd0c8c28b3f1ed049f008d14b711ef788.json index 1fee9eb103..77dfd2f4bc 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-1668f5a5a0abc8a73454953c3f5b61d2afb1b37720f5756b9c6fb3aef55a3027.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-9c077a2322ad141cfae4c81d1046671cd0c8c28b3f1ed049f008d14b711ef788.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n gw.bridges as \"bridges?: serde_json::Value\",\n COALESCE(gd.moniker, 'NA') as \"moniker!\",\n COALESCE(gd.website, 'NA') as \"website!\",\n COALESCE(gd.security_contact, 'NA') as \"security_contact!\",\n COALESCE(gd.details, 'NA') as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key", + "query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.ports_check as \"ports_check?\",\n gw.last_ports_check_utc as \"last_ports_check_utc?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n gw.bridges as \"bridges?: serde_json::Value\",\n COALESCE(gd.moniker, 'NA') as \"moniker!\",\n COALESCE(gd.website, 'NA') as \"website!\",\n COALESCE(gd.security_contact, 'NA') as \"security_contact!\",\n COALESCE(gd.details, 'NA') as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key", "describe": { "columns": [ { @@ -40,36 +40,46 @@ }, { "ordinal": 7, - "name": "last_testrun_utc?", - "type_info": "Int8" + "name": "ports_check?", + "type_info": "Jsonb" }, { "ordinal": 8, - "name": "last_updated_utc!", + "name": "last_ports_check_utc?", "type_info": "Int8" }, { "ordinal": 9, + "name": "last_testrun_utc?", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "last_updated_utc!", + "type_info": "Int8" + }, + { + "ordinal": 11, "name": "bridges?: serde_json::Value", "type_info": "Jsonb" }, { - "ordinal": 10, + "ordinal": 12, "name": "moniker!", "type_info": "Varchar" }, { - "ordinal": 11, + "ordinal": 13, "name": "website!", "type_info": "Varchar" }, { - "ordinal": 12, + "ordinal": 14, "name": "security_contact!", "type_info": "Varchar" }, { - "ordinal": 13, + "ordinal": 15, "name": "details!", "type_info": "Varchar" } @@ -86,6 +96,8 @@ true, true, true, + true, + true, false, true, null, @@ -94,5 +106,5 @@ null ] }, - "hash": "1668f5a5a0abc8a73454953c3f5b61d2afb1b37720f5756b9c6fb3aef55a3027" + "hash": "9c077a2322ad141cfae4c81d1046671cd0c8c28b3f1ed049f008d14b711ef788" } From a52a8c3e8112f1943d9f969fd8e15ea63ae57f46 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Wed, 27 May 2026 09:08:25 +0200 Subject: [PATCH 08/17] fix --- Cargo.lock | 4 +-- ...528594a84f560130a6956507ef8c4500e4bb9.json | 31 +++++++++++++++++++ ...6b5ac23f7f7eabc6b2ba13ccfb8ef14fcfd30.json | 30 ------------------ .../src/db/queries/testruns.rs | 1 + .../src/http/api/testruns.rs | 6 ++-- 5 files changed, 37 insertions(+), 35 deletions(-) create mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-2e825cb61d34ed7b41d622743b4528594a84f560130a6956507ef8c4500e4bb9.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-62be5c3a94e6b2e72823e0fe6d26b5ac23f7f7eabc6b2ba13ccfb8ef14fcfd30.json diff --git a/Cargo.lock b/Cargo.lock index c606bed85c..0a1fe54aab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7870,7 +7870,7 @@ dependencies = [ [[package]] name = "nym-node-status-agent" -version = "2.0.1-rc3" +version = "2.0.1" dependencies = [ "anyhow", "clap", @@ -7891,7 +7891,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "4.6.2-rc10" +version = "4.6.2" dependencies = [ "ammonia", "anyhow", diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-2e825cb61d34ed7b41d622743b4528594a84f560130a6956507ef8c4500e4bb9.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-2e825cb61d34ed7b41d622743b4528594a84f560130a6956507ef8c4500e4bb9.json new file mode 100644 index 0000000000..2a8239cd82 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-2e825cb61d34ed7b41d622743b4528594a84f560130a6956507ef8c4500e4bb9.json @@ -0,0 +1,31 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH oldest_queued AS (\n SELECT testruns.id\n FROM testruns\n JOIN gateways ON gateways.id = testruns.gateway_id\n WHERE testruns.status = $1\n AND testruns.kind = $4\n AND gateways.bonded = true\n AND gateways.performance > 0\n ORDER BY testruns.created_utc asc\n LIMIT 1\n FOR UPDATE OF testruns SKIP LOCKED\n )\n UPDATE testruns\n SET\n status = $3,\n last_assigned_utc = $2\n FROM oldest_queued\n WHERE testruns.id = oldest_queued.id\n RETURNING\n testruns.id,\n testruns.gateway_id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "gateway_id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4", + "Int8", + "Int4", + "Int2" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "2e825cb61d34ed7b41d622743b4528594a84f560130a6956507ef8c4500e4bb9" +} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-62be5c3a94e6b2e72823e0fe6d26b5ac23f7f7eabc6b2ba13ccfb8ef14fcfd30.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-62be5c3a94e6b2e72823e0fe6d26b5ac23f7f7eabc6b2ba13ccfb8ef14fcfd30.json deleted file mode 100644 index b1bc6785eb..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-62be5c3a94e6b2e72823e0fe6d26b5ac23f7f7eabc6b2ba13ccfb8ef14fcfd30.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH oldest_queued AS (\n SELECT testruns.id\n FROM testruns\n JOIN gateways ON gateways.id = testruns.gateway_id\n WHERE testruns.status = $1\n AND gateways.bonded = true\n AND gateways.performance > 0\n ORDER BY testruns.created_utc asc\n LIMIT 1\n FOR UPDATE OF testruns SKIP LOCKED\n )\n UPDATE testruns\n SET\n status = $3,\n last_assigned_utc = $2\n FROM oldest_queued\n WHERE testruns.id = oldest_queued.id\n RETURNING\n testruns.id,\n testruns.gateway_id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "gateway_id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int4", - "Int8", - "Int4" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "62be5c3a94e6b2e72823e0fe6d26b5ac23f7f7eabc6b2ba13ccfb8ef14fcfd30" -} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs index bc2719cea6..fa7d726389 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs @@ -117,6 +117,7 @@ async fn assign_oldest_testrun_by_kind( FROM testruns JOIN gateways ON gateways.id = testruns.gateway_id WHERE testruns.status = $1 + AND testruns.kind = $4 AND gateways.bonded = true AND gateways.performance > 0 ORDER BY testruns.created_utc asc diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs index aa706d161c..add6ab2f1d 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs @@ -32,10 +32,10 @@ pub(crate) fn routes() -> Router { "/ports-check", axum::routing::get(request_ports_check_testrun), ) - .route("/:testrun_id", axum::routing::post(submit_testrun)) - .route("/:testrun_id/v2", axum::routing::post(submit_testrun_v2)) + .route("/{testrun_id}", axum::routing::post(submit_testrun)) + .route("/{testrun_id}/v2", axum::routing::post(submit_testrun_v2)) .route( - "/:testrun_id/ports-check/v2", + "/{testrun_id}/ports-check/v2", axum::routing::post(submit_ports_check_testrun_v2), ) .layer(DefaultBodyLimit::max(1024 * 1024 * 5)) From a98a65c16d2d8893f3e0d4e055838d91327d713b Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 1 Jun 2026 11:59:43 +0200 Subject: [PATCH 09/17] addressing coderabbit comments --- nym-gateway-probe/netstack_ping/lib.go | 16 +++++++++----- nym-gateway-probe/src/common/types.rs | 16 +++++++++++++- .../nym-node-status-agent/src/cli/mod.rs | 6 ++++-- .../src/db/queries/gateways.rs | 20 +++++++++++++----- .../src/db/queries/testruns.rs | 21 +++++++++++++++++++ .../src/http/api/testruns.rs | 21 +++++++++++++------ .../nym-node-status-api/src/http/server.rs | 3 ++- .../nym-node-status-api/src/http/state.rs | 3 ++- 8 files changed, 85 insertions(+), 21 deletions(-) diff --git a/nym-gateway-probe/netstack_ping/lib.go b/nym-gateway-probe/netstack_ping/lib.go index bcbf738368..fc36d93367 100644 --- a/nym-gateway-probe/netstack_ping/lib.go +++ b/nym-gateway-probe/netstack_ping/lib.go @@ -658,18 +658,24 @@ func checkPorts(target string, ports []uint16, timeoutSec uint64, tnet *netstack } } } else { - // All other targets can handle concurrent connections, probably - log.Printf("Port check: testing %d ports on %s concurrently (timeout %v each)", - len(ports), target, timeout) + // All other targets can handle concurrent connections, probably. + // A semaphore caps concurrent tnet.DialContext calls to avoid + // overwhelming the single userspace netstack instance. + const maxConcurrentDials = 64 + log.Printf("Port check: testing %d ports on %s concurrently (max %d at a time, timeout %v each)", + len(ports), target, maxConcurrentDials, timeout) var ( - mu sync.Mutex - wg sync.WaitGroup + mu sync.Mutex + wg sync.WaitGroup + sem = make(chan struct{}, maxConcurrentDials) ) for _, p := range ports { wg.Add(1) go func(port uint16) { defer wg.Done() + sem <- struct{}{} + defer func() { <-sem }() addr := net.JoinHostPort(targetIP, fmt.Sprintf("%d", port)) ctx, cancel := context.WithTimeout(context.Background(), timeout) c, err := tnet.DialContext(ctx, "tcp", addr) diff --git a/nym-gateway-probe/src/common/types.rs b/nym-gateway-probe/src/common/types.rs index 99190df348..72953cfcb9 100644 --- a/nym-gateway-probe/src/common/types.rs +++ b/nym-gateway-probe/src/common/types.rs @@ -257,7 +257,21 @@ impl PortCheckResult { pub fn closed_ports(&self) -> Vec { self.ports .iter() - .filter_map(|(k, &open)| if !open { k.parse().ok() } else { None }) + .filter_map(|(k, &open)| { + if open { + return None; + } + match k.parse::() { + Ok(port) => Some(port), + Err(e) => { + tracing::warn!( + "Skipping port key {:?} that could not be parsed as u16: {e}", + k + ); + None + } + } + }) .collect() } } diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs b/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs index 5fbe2ae62d..d0a3625840 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs @@ -34,8 +34,10 @@ pub(super) fn parse_server_config(s: &str) -> Result { let port = parts[1] .parse::() .map_err(|_| "Invalid port number".to_string())?; - let auth_key = - PrivateKey::from_base58_string(env::var("NODE_STATUS_AGENT_AUTH_KEY").unwrap()).unwrap(); + let raw_key = env::var("NODE_STATUS_AGENT_AUTH_KEY") + .map_err(|_| "NODE_STATUS_AGENT_AUTH_KEY environment variable is not set".to_string())?; + let auth_key = PrivateKey::from_base58_string(raw_key) + .map_err(|e| format!("Failed to decode NODE_STATUS_AGENT_AUTH_KEY as base58: {e}"))?; Ok(ServerConfig { address, diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs index 055498fa1c..5049163317 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs @@ -104,11 +104,21 @@ impl Storage { .fetch(&self.pool) .try_collect::>() .await?; - let items: Vec = items - .into_iter() - .map(|item| item.try_into()) - .collect::>>() - .inspect_err(|e| error!("Conversion from DTO failed: {e}. Invalidly stored data?"))?; + let mut gateways: Vec = Vec::with_capacity(items.len()); + let mut failed = 0usize; + for item in items { + match item.try_into() { + Ok(gw) => gateways.push(gw), + Err(e) => { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + failed += 1; + } + } + } + if failed > 0 { + tracing::warn!("{failed} gateway DTO(s) failed conversion and were skipped"); + } + let items = gateways; tracing::trace!("Fetched {} gateways from DB", items.len()); Ok(items) } diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs index fa7d726389..238d51be31 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs @@ -24,6 +24,27 @@ pub(crate) async fn count_testruns_in_progress( .map_err(anyhow::Error::from) } +pub(crate) async fn count_testruns_in_progress_by_kind( + conn: &mut DbConnection, + kind: TestRunKind, +) -> anyhow::Result> { + sqlx::query_scalar!( + r#"SELECT + COUNT(id) as "count: i64" + FROM testruns + WHERE + status = $1 + AND + kind = $2 + "#, + TestRunStatus::InProgress as i64, + kind as i16, + ) + .fetch_one(conn.as_mut()) + .await + .map_err(anyhow::Error::from) +} + pub(crate) async fn get_in_progress_testrun_by_id( conn: &mut DbConnection, testrun_id: i32, diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs index add6ab2f1d..a9c421d395 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs @@ -142,13 +142,18 @@ async fn request_ports_check_testrun( .await .map_err(HttpError::internal_with_logging)?; - let active_testruns = db::queries::testruns::count_testruns_in_progress(&mut conn) - .await - .map_err(HttpError::internal_with_logging)? - .unwrap_or_default(); + let active_ports_check_testruns = db::queries::testruns::count_testruns_in_progress_by_kind( + &mut conn, + TestRunKind::PortsCheck, + ) + .await + .map_err(HttpError::internal_with_logging)? + .unwrap_or_default(); let max_count = state.agent_max_count(); - if active_testruns >= max_count { - tracing::warn!("{active_testruns}/{max_count} testruns in progress, rejecting",); + if active_ports_check_testruns >= max_count { + tracing::warn!( + "{active_ports_check_testruns}/{max_count} ports-check testruns in progress, rejecting", + ); return Err(HttpError::no_testruns_available()); } @@ -562,6 +567,10 @@ async fn process_ports_check_submission( payload.port_check_result.can_register, ); + // probe_log is intentionally not persisted for ports-check submissions: + // the ports-check result is a lightweight JSONB record and does not warrant + // the storage overhead of a full probe log. Full logs are retained only for + // regular probe testruns via update_gateway_last_probe_log. queries::testruns::update_gateway_ports_check_only( conn, gateway_id, diff --git a/nym-node-status-api/nym-node-status-api/src/http/server.rs b/nym-node-status-api/nym-node-status-api/src/http/server.rs index 356fb7cdc9..01048d192c 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/server.rs @@ -54,7 +54,8 @@ pub(crate) async fn start_http_api( .unwrap_or(true); if ports_check_scheduler_enabled { - let mut interval = tokio::time::interval(std::time::Duration::from_secs(60 * 10)); + let period = std::time::Duration::from_secs(60 * 10); + let mut interval = tokio::time::interval_at(tokio::time::Instant::now() + period, period); interval.set_missed_tick_behavior(MissedTickBehavior::Skip); let scheduler_shutdown = shutdown_tracker.clone_shutdown_token().cancelled_owned(); shutdown_tracker.spawn(async move { diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index 1d8affed94..a05980a69b 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -134,7 +134,8 @@ impl AppState { return Err(HttpError::unauthorized()); }; - if request.verify_signature().is_err() { + if let Err(err) = request.verify_signature() { + tracing::debug!("Signature verification error: {:?}", err); tracing::warn!("Signature verification failed, rejecting"); return Err(HttpError::unauthorized()); } From 0796e9e0a68992151e189148cc47c79cc4661db0 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 1 Jun 2026 12:40:26 +0200 Subject: [PATCH 10/17] build fix --- .../src/db/queries/testruns.rs | 23 +++++++------------ .../nym-node-status-api/src/http/state.rs | 3 +-- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs index 238d51be31..2535838e58 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs @@ -28,21 +28,14 @@ pub(crate) async fn count_testruns_in_progress_by_kind( conn: &mut DbConnection, kind: TestRunKind, ) -> anyhow::Result> { - sqlx::query_scalar!( - r#"SELECT - COUNT(id) as "count: i64" - FROM testruns - WHERE - status = $1 - AND - kind = $2 - "#, - TestRunStatus::InProgress as i64, - kind as i16, - ) - .fetch_one(conn.as_mut()) - .await - .map_err(anyhow::Error::from) + let count: i64 = + sqlx::query_scalar(r#"SELECT COUNT(id) FROM testruns WHERE status = $1 AND kind = $2"#) + .bind(TestRunStatus::InProgress as i64) + .bind(kind as i16) + .fetch_one(conn.as_mut()) + .await + .map_err(anyhow::Error::from)?; + Ok(Some(count)) } pub(crate) async fn get_in_progress_testrun_by_id( diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index a05980a69b..1d8affed94 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -134,8 +134,7 @@ impl AppState { return Err(HttpError::unauthorized()); }; - if let Err(err) = request.verify_signature() { - tracing::debug!("Signature verification error: {:?}", err); + if request.verify_signature().is_err() { tracing::warn!("Signature verification failed, rejecting"); return Err(HttpError::unauthorized()); } From 4ce136ccf06eda7d340ed0476e6f9edf0248881a Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 1 Jun 2026 13:12:15 +0200 Subject: [PATCH 11/17] fix clippy --- nym-gateway-probe/build.rs | 18 ++++++------ nym-gateway-probe/src/lib.rs | 55 +++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/nym-gateway-probe/build.rs b/nym-gateway-probe/build.rs index 97a8d14c7c..bd3b21ab13 100644 --- a/nym-gateway-probe/build.rs +++ b/nym-gateway-probe/build.rs @@ -69,15 +69,15 @@ fn generate_exit_policy_ports() -> anyhow::Result<()> { if value.contains('-') { let parts: Vec<&str> = value.split('-').collect(); - if parts.len() == 2 { - if let (Ok(lo), Ok(hi)) = (parts[0].parse::(), parts[1].parse::()) { - port_map - .entry(lo) - .or_insert_with(|| format!("{service} (range start)")); - port_map - .entry(hi) - .or_insert_with(|| format!("{service} (range end)")); - } + if parts.len() == 2 + && let (Ok(lo), Ok(hi)) = (parts[0].parse::(), parts[1].parse::()) + { + port_map + .entry(lo) + .or_insert_with(|| format!("{service} (range start)")); + port_map + .entry(hi) + .or_insert_with(|| format!("{service} (range end)")); } } else if let Ok(port) = value.parse::() { port_map.entry(port).or_insert_with(|| service.to_string()); diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index bc9af69858..a316666f37 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -67,6 +67,20 @@ struct PortScanRun { last_error: Option, } +/// Auth endpoint to probe during a port scan. +struct PortScanTarget { + authenticator: nym_sdk::mixnet::Recipient, + authenticator_version: nym_authenticator_requests::AuthenticatorVersion, + ip_address: IpAddr, +} + +/// Ticket-acquisition parameters for a single port scan session. +struct PortScanBandwidth<'a> { + provider: &'a dyn BandwidthTicketProvider, + ticket_type: TicketType, + credential_provider: nym_sdk::mixnet::NodeIdentity, +} + /// Validated info needed to run a WG port-check via the mixnet. struct PortCheckSetup { exit_node: TestedNodeDetails, @@ -145,12 +159,8 @@ impl Probe { async fn run_port_scan_with_retries( mixnet_listener_task: &AuthClientMixnetListenerHandle, nym_address: nym_sdk::mixnet::Recipient, - authenticator: nym_sdk::mixnet::Recipient, - authenticator_version: nym_authenticator_requests::AuthenticatorVersion, - ip_address: IpAddr, - bandwidth_provider: &dyn BandwidthTicketProvider, - wg_ticket_type: TicketType, - credential_provider: nym_sdk::mixnet::NodeIdentity, + target: PortScanTarget, + bandwidth: PortScanBandwidth<'_>, netstack_args: NetstackArgs, awg_args: Option, ) -> PortScanRun { @@ -165,8 +175,9 @@ impl Probe { tokio::time::sleep(std::time::Duration::from_secs(5)).await; } - let credential = match bandwidth_provider - .get_ecash_ticket(wg_ticket_type, credential_provider, 1) + let credential = match bandwidth + .provider + .get_ecash_ticket(bandwidth.ticket_type, bandwidth.credential_provider, 1) .await { Ok(ticket) => ticket.data, @@ -182,16 +193,16 @@ impl Probe { mixnet_listener_task.subscribe(), mixnet_listener_task.mixnet_sender(), nym_address, - authenticator, - authenticator_version, + target.authenticator, + target.authenticator_version, Arc::new(x25519::KeyPair::new(&mut rng)), - ip_address, + target.ip_address, ); match wg_probe( auth_client, - ip_address, - authenticator_version, + target.ip_address, + target.authenticator_version, awg_args.clone(), netstack_args.clone(), true, // port_check_only @@ -265,7 +276,7 @@ impl Probe { "All port-check targets unreachable; falling back to first: '{}'", selected_target ); - } else if selected_target != *targets.first().unwrap() { + } else if selected_target != targets[0] { info!( "Port check: selected target '{}' (first reachable from list)", selected_target @@ -298,12 +309,16 @@ impl Probe { let scan = Self::run_port_scan_with_retries( &mixnet_listener_task, nym_address, - setup.authenticator, - setup.exit_node.authenticator_version, - setup.ip_address, - bandwidth_provider, - TicketType::V1WireguardExit, - setup.exit_node.identity, + PortScanTarget { + authenticator: setup.authenticator, + authenticator_version: setup.exit_node.authenticator_version, + ip_address: setup.ip_address, + }, + PortScanBandwidth { + provider: bandwidth_provider, + ticket_type: TicketType::V1WireguardExit, + credential_provider: setup.exit_node.identity, + }, netstack_args, None, ) From 639c7f83a42518e0d4a1ac4cd8a27df81b1c9992 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 1 Jun 2026 14:02:25 +0200 Subject: [PATCH 12/17] clippy --- nym-gateway-probe/src/run.rs | 8 ++++---- .../nym-node-status-api/src/db/queries/gateways.rs | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index 06fcbbb1d8..aa961b784b 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -147,7 +147,7 @@ struct RunPortsProbeConfig { #[derive(Serialize)] #[serde(untagged)] pub(crate) enum ProbeOutput { - Standard(ProbeResult), + Standard(Box), PortCheck(PortCheckResult), } @@ -222,7 +222,7 @@ pub(crate) async fn run() -> anyhow::Result { Box::pin(trial.probe_run_locally(&config_dir, credential_mode)) .await - .map(ProbeOutput::Standard) + .map(|r| ProbeOutput::Standard(Box::new(r))) } Commands::Run { entry_gateway, @@ -268,7 +268,7 @@ pub(crate) async fn run() -> anyhow::Result { nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config); Box::pin(trial.probe_run(&config_dir, credential_mode)) .await - .map(ProbeOutput::Standard) + .map(|r| ProbeOutput::Standard(Box::new(r))) } Commands::RunPorts { entry_gateway, @@ -350,7 +350,7 @@ pub(crate) async fn run() -> anyhow::Result { .await?; Box::pin(trial.probe_run_agent(credential_args)) .await - .map(ProbeOutput::Standard) + .map(|r| ProbeOutput::Standard(Box::new(r))) } } } diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs index 5049163317..57ca705fa4 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs @@ -118,9 +118,8 @@ impl Storage { if failed > 0 { tracing::warn!("{failed} gateway DTO(s) failed conversion and were skipped"); } - let items = gateways; - tracing::trace!("Fetched {} gateways from DB", items.len()); - Ok(items) + tracing::trace!("Fetched {} gateways from DB", gateways.len()); + Ok(gateways) } } From 2fd26581eb3da76c695a1f420a59336ab41390da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 1 Jun 2026 16:18:24 +0100 Subject: [PATCH 13/17] fix(ns-api): guard ports-check migration against non-JSON last_probe_result The 20260415 migration cast last_probe_result::jsonb guarded only by a btrim non-empty check, so any row with non-JSON text aborted the whole migration. Add the same last_probe_result ~ '^[\[{]' guard the follow-up 20260519133000 migration uses, directly to both UPDATEs here. --- .../migrations_pg/20260415000000_gateways_ports_check.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql index e521cb2aa5..f506b093f5 100644 --- a/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql @@ -4,6 +4,7 @@ UPDATE gateways SET ports_check = (last_probe_result::jsonb -> 'ports_check') WHERE last_probe_result IS NOT NULL AND btrim(last_probe_result) <> '' + AND last_probe_result ~ '^[\[{]' AND last_probe_result::jsonb ? 'ports_check' AND ports_check IS NULL; @@ -11,4 +12,5 @@ UPDATE gateways SET last_probe_result = (last_probe_result::jsonb - 'ports_check')::text WHERE last_probe_result IS NOT NULL AND btrim(last_probe_result) <> '' + AND last_probe_result ~ '^[\[{]' AND last_probe_result::jsonb ? 'ports_check'; \ No newline at end of file From 3dd6b907fe43b7165c2ebbe2bf9549202e69fcf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 1 Jun 2026 16:18:25 +0100 Subject: [PATCH 14/17] fix(nym-gateway-probe): anchor exit-policy script path to CARGO_MANIFEST_DIR The build read network-tunnel-manager.sh via a CWD-relative path, coupling the build to the invocation directory. Anchor it to CARGO_MANIFEST_DIR. --- nym-gateway-probe/build.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nym-gateway-probe/build.rs b/nym-gateway-probe/build.rs index bd3b21ab13..e1947f2277 100644 --- a/nym-gateway-probe/build.rs +++ b/nym-gateway-probe/build.rs @@ -26,7 +26,9 @@ fn main() -> anyhow::Result<()> { fn generate_exit_policy_ports() -> anyhow::Result<()> { use std::collections::BTreeMap; - let script_path = PathBuf::from("../scripts/nym-node-setup/network-tunnel-manager.sh"); + let manifest_dir = + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").context("CARGO_MANIFEST_DIR not set")?); + let script_path = manifest_dir.join("../scripts/nym-node-setup/network-tunnel-manager.sh"); let out_dir = PathBuf::from(std::env::var("OUT_DIR").context("OUT_DIR not set")?); println!("cargo::rerun-if-changed={}", script_path.display()); From cc405cc802608c3db4a0b591cbbd8a1b555a7d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 1 Jun 2026 16:21:14 +0100 Subject: [PATCH 15/17] refactor(ns-api): compile-time-check count_testruns_in_progress_by_kind Switch from runtime sqlx::query_scalar to the checked query_scalar! macro (consistent with the rest of the file), returning i64 instead of an always-Some Option and dropping the unwrap_or_default() at the call site. Regenerates the .sqlx cache, adding the new query and pruning accumulated orphans. --- ...afa2525da6c0b871633aad80ad555db9cf47c.json | 32 ---------------- ...b19f15d22fff72490473587a14cdc046fda32.json | 20 ---------- ...3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1.json | 12 ------ ...4c270fd5dd7f00eaab0fe82e6fe927e604294.json | 12 ------ ...af7974d7795b4cbdba234273345fe161df233.json | 12 ------ ...b5905d4dd6b7ac0e20f1cf100a51a1a40b35d.json | 12 ------ ...82b4dc00ba6a580de1faf83b137365f44b36d.json | 12 ------ ...105614dea7fd301f0ec38bfe85bfe546dad40.json | 12 ------ ...997139172411b10c7444dea74a1438de4f343.json | 12 ------ ...08cb6c3b99c6b808ba50c67b066fc3f36073b.json | 12 ------ ...765854899103fd3c0fa670eb6809492270e02.json | 12 ------ ...a317aabce063a650be362d3a8ed83cc7c3549.json | 12 ------ ...9f166ad7f57681f76a1d0c7723d007c1f2c1e.json | 12 ------ ...38edcdf149acfd38c56a4db3bbf97bdb13210.json | 12 ------ ...15212a6f526f78258e173c96ec177988696ef.json | 12 ------ ...2038a77ec37cca9f9aef18008dcd03030c2c4.json | 12 ------ ...bd97845350f4fb9f806c60b93c7cebd5e410d.json | 20 ---------- ...d8ea15b957dd19b097101dcda2c05aee21d20.json | 15 -------- ...91d2a2af57656b405212af414d765b2263347.json | 26 ------------- ...1bbace1bcf110e048e594e8bd03fa45290732.json | 12 ------ ...7cec1442967e53764b83ba0a66cd9a72494f9.json | 12 ------ ...6b2a5bec1c191993ff58f0c71c03e3ef6d92d.json | 12 ------ ...11e5732ece0bf84ea98f2328b20add8f2b5ef.json | 12 ------ ...5e03504afa58e345ae16a128629d8e76f0a11.json | 26 ------------- ...a8787133ee41814bd3efdd501f9ee0c050f2b.json | 20 ---------- ...e38b7f6ceb8e9191a7b9201922efcf6b07966.json | 12 ------ ...751a5ca340ac9c6dd86da1a5e9a3e65ea589f.json | 26 ------------- ...0fd083e87cc9a18d798b2cf8d6774224aebed.json | 20 ---------- ...6067dbf82d0dc376ad3f0df835b77dfcc3c37.json | 20 ---------- ...0d5ebd37d62612b65b8ac4c9eb5d7b67a8884.json | 26 ------------- ...8d18a0c6792d7a4244a65243d4b22d578a1e5.json | 20 ---------- ...7fca89d80869484f2f3c66cabb898f0298c62.json | 12 ------ ...2b60ccb76d6468f359891428c0bfb96ddd7ef.json | 12 ------ ...de04e5ece789fb0b0cd27ad09858fbf5c5e27.json | 12 ------ ...682169b87f488b6927fa67fade6bf5abf7582.json | 12 ------ ...6156311713d2e9f69d39c868cd16296b56326.json | 12 ------ ...4d63462e9ea0049e2edafea1dc3f8476b33e4.json | 12 ------ ...de3eb76fed954b7336530401db72cd008aff3.json | 12 ------ ...b57e6ea8685e9473a3a8bd635552810493a58.json | 32 ---------------- ...57f91a11f5057273cb70bd0e629712d17dd41.json | 12 ------ ...9db66d1dc1307c0873627b39cb22a245b5a8c.json | 12 ------ ...15dcfcffb502ba8398caefd56c481f44eb84e.json | 12 ------ ...13ab15fed747fb0cee1c9f949fb58461b3f79.json | 32 ---------------- ...4cb8bf8ebb1c26839f915780d0fc0d1939973.json | 31 --------------- ...a8ed4e9a64884ac5619e0abdecb04f2d13a74.json | 28 -------------- ...4adec312abffa604fa805cc362a2c206107b4.json | 12 ------ ...6a811c593aea8febf1f891117e5e84213f147.json | 12 ------ ...eafc9b5f64061b6c9a829e2912945b6cffc82.json | 32 ---------------- ...89dde284b38a3a4960afed758206d03ca1cf4.json | 12 ------ ...b253c071e04eb853d4b0f3b21782ea57c2f68.json | 12 ------ ...5ebe0222a2a0ba96f28e3a3aa58fdc4b689db.json | 12 ------ ...125c24750b523cfcad1e5e9069dbf7050e2a1.json | 20 ---------- ...c9f560017b4da6b0303ea0397d9568229e167.json | 38 ------------------- ...087af53e9504a8bb13280dde5c37b9b418370.json | 22 ----------- ...abd806877ce914b514d4f7cd6be318c4debe6.json | 12 ------ ...1da2809ec4bd66ad49d2e3b8d67b741835475.json | 12 ------ ...8f633a94d18a19b7f0f96935a62560def7d0f.json | 12 ------ ...bcb8900bca7157d3c66c5664386ca2c26e96a.json | 23 +++++++++++ ...9cde3f6f6689a80299b5b6c1d7bd048965e6e.json | 15 -------- ...35677b9d9a1b29e4d89437134e92967fa98d1.json | 12 ------ ...52f2bfed2d918b894ec0f588e38dd5e8ad726.json | 12 ------ ...6a2e314787586a46cffd074abb67f2f4d109e.json | 38 ------------------- ...ac000904bb7fe3934786d652adc80fde78add.json | 20 ---------- ...3d41724666bf92357bf5faf42120a2aab131c.json | 12 ------ ...18a1f62fa67dfe8f130deb876ebee11bf1602.json | 20 ---------- ...bb91cd5046a8c9d4434cf085a4fa2ebf03e93.json | 12 ------ .../src/db/queries/testruns.rs | 18 ++++----- .../src/http/api/testruns.rs | 3 +- 68 files changed, 33 insertions(+), 1102 deletions(-) delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-1b43cc81ce6b6007ccc59172fc64c270fd5dd7f00eaab0fe82e6fe927e604294.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-1f4fde2cafa3b5fae95ab5fb653b5905d4dd6b7ac0e20f1cf100a51a1a40b35d.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-1fa61c23a1504de280d67c7943282b4dc00ba6a580de1faf83b137365f44b36d.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-1fc72f8ba24039548047e1766c9105614dea7fd301f0ec38bfe85bfe546dad40.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-20797a3e0bb951ed77216a906d7997139172411b10c7444dea74a1438de4f343.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-22bc9c7bcf96ec817c83c26104408cb6c3b99c6b808ba50c67b066fc3f36073b.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-28681fcd8e2d4326f628681b8f2a317aabce063a650be362d3a8ed83cc7c3549.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-2930ca6e3875c74acb7abb9ad889f166ad7f57681f76a1d0c7723d007c1f2c1e.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-396f40c33f0f62796eb7449d640bd97845350f4fb9f806c60b93c7cebd5e410d.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-3cc446220668fb3e02f0578104291d2a2af57656b405212af414d765b2263347.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-42dfc51aea793fc60484cff6c481bbace1bcf110e048e594e8bd03fa45290732.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-52b378e282d93db941eff53b5b311e5732ece0bf84ea98f2328b20add8f2b5ef.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-5e51396c534409a4b55c08170e00fd083e87cc9a18d798b2cf8d6774224aebed.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-62f471858c831ac762a0ecf60876067dbf82d0dc376ad3f0df835b77dfcc3c37.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-648fff18c4016fb356c164f813e0d5ebd37d62612b65b8ac4c9eb5d7b67a8884.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-67db5e91bcdc831e0d1659eda358d18a0c6792d7a4244a65243d4b22d578a1e5.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-70d8f240ad6edda6b8c7f2e800e7fca89d80869484f2f3c66cabb898f0298c62.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-74d77a3e15f49bde7b8b17dd638de04e5ece789fb0b0cd27ad09858fbf5c5e27.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-85d542a8cb2e1fbba6142729056b57e6ea8685e9473a3a8bd635552810493a58.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-97d97ebb6bc8f4114fdea9ebc9f57f91a11f5057273cb70bd0e629712d17dd41.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-a5a71e480118639397f7464abb09db66d1dc1307c0873627b39cb22a245b5a8c.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-a8b7ce0fe4755c28b96d1e503e313ab15fed747fb0cee1c9f949fb58461b3f79.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-b0c588db5f7be90c6d4cb1b55c3a8ed4e9a64884ac5619e0abdecb04f2d13a74.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-b6ce77fcffda2ee24ba181213e04adec312abffa604fa805cc362a2c206107b4.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-b8257a0832d0124f0a8aaaf81dc6a811c593aea8febf1f891117e5e84213f147.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-bdaf8e8dd711ff420807659456f5ebe0222a2a0ba96f28e3a3aa58fdc4b689db.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-c2b841762bdb963fff337ef5c8ec9f560017b4da6b0303ea0397d9568229e167.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-d3510846941fa2525926b9bfbcdabd806877ce914b514d4f7cd6be318c4debe6.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-daa9ffcbe13178d773e538e96971da2809ec4bd66ad49d2e3b8d67b741835475.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-db176e98198fe594d88eb860d918f633a94d18a19b7f0f96935a62560def7d0f.json create mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-decdc7c0e6d279a137a80797cbabcb8900bca7157d3c66c5664386ca2c26e96a.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-dfa035c968534926736adf0e5359cde3f6f6689a80299b5b6c1d7bd048965e6e.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-e29e0c82d034626f9b369348b8e35677b9d9a1b29e4d89437134e92967fa98d1.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-e584253e3856355899537eb8fc152f2bfed2d918b894ec0f588e38dd5e8ad726.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-e77ffab19b099b84470fe5611716a2e314787586a46cffd074abb67f2f4d109e.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-e8717ef6523ef9bcf37a0e81570ac000904bb7fe3934786d652adc80fde78add.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-eded014a5fffbd9c359f8557bcd3d41724666bf92357bf5faf42120a2aab131c.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-ef60c2683211cc4ec2d3e46392518a1f62fa67dfe8f130deb876ebee11bf1602.json delete mode 100644 nym-node-status-api/nym-node-status-api/.sqlx/query-f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93.json diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json deleted file mode 100644 index 068bc54b4b..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_key, serialization_revision as \"serialization_revision: u8\"\n FROM master_verification_key WHERE epoch_id = ?\n ", - "describe": { - "columns": [ - { - "name": "epoch_id: u32", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "serialised_key", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "serialization_revision: u8", - "ordinal": 2, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json deleted file mode 100644 index 20c47048d8..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", - "describe": { - "columns": [ - { - "name": "exists", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1.json deleted file mode 100644 index e4f26c5053..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)\n VALUES (?, ?, ?, ?);\n UPDATE expiration_date_signatures\n SET\n serialised_signatures = ?,\n serialization_revision = ?\n WHERE expiration_date = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-1b43cc81ce6b6007ccc59172fc64c270fd5dd7f00eaab0fe82e6fe927e604294.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-1b43cc81ce6b6007ccc59172fc64c270fd5dd7f00eaab0fe82e6fe927e604294.json deleted file mode 100644 index 6d2842a9df..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-1b43cc81ce6b6007ccc59172fc64c270fd5dd7f00eaab0fe82e6fe927e604294.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_listener, fallback_listener, expiration_timestamp)\n VALUES (?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "1b43cc81ce6b6007ccc59172fc64c270fd5dd7f00eaab0fe82e6fe927e604294" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233.json deleted file mode 100644 index 9d4281137b..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM remote_gateway_details WHERE gateway_id_bs58 = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-1f4fde2cafa3b5fae95ab5fb653b5905d4dd6b7ac0e20f1cf100a51a1a40b35d.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-1f4fde2cafa3b5fae95ab5fb653b5905d4dd6b7ac0e20f1cf100a51a1a40b35d.json deleted file mode 100644 index d891d0d97a..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-1f4fde2cafa3b5fae95ab5fb653b5905d4dd6b7ac0e20f1cf100a51a1a40b35d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM reply_key;", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "1f4fde2cafa3b5fae95ab5fb653b5905d4dd6b7ac0e20f1cf100a51a1a40b35d" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-1fa61c23a1504de280d67c7943282b4dc00ba6a580de1faf83b137365f44b36d.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-1fa61c23a1504de280d67c7943282b4dc00ba6a580de1faf83b137365f44b36d.json deleted file mode 100644 index 096aa7a892..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-1fa61c23a1504de280d67c7943282b4dc00ba6a580de1faf83b137365f44b36d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE status SET previous_flush = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "1fa61c23a1504de280d67c7943282b4dc00ba6a580de1faf83b137365f44b36d" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-1fc72f8ba24039548047e1766c9105614dea7fd301f0ec38bfe85bfe546dad40.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-1fc72f8ba24039548047e1766c9105614dea7fd301f0ec38bfe85bfe546dad40.json deleted file mode 100644 index cdc057df57..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-1fc72f8ba24039548047e1766c9105614dea7fd301f0ec38bfe85bfe546dad40.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO ecash_deposit_usage (deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid)\n VALUES (?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "1fc72f8ba24039548047e1766c9105614dea7fd301f0ec38bfe85bfe546dad40" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-20797a3e0bb951ed77216a906d7997139172411b10c7444dea74a1438de4f343.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-20797a3e0bb951ed77216a906d7997139172411b10c7444dea74a1438de4f343.json deleted file mode 100644 index eb6e8b8904..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-20797a3e0bb951ed77216a906d7997139172411b10c7444dea74a1438de4f343.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE remote_gateway_details SET gateway_listener = ?, fallback_listener = ?, expiration_timestamp = ? WHERE gateway_id_bs58 = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "20797a3e0bb951ed77216a906d7997139172411b10c7444dea74a1438de4f343" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-22bc9c7bcf96ec817c83c26104408cb6c3b99c6b808ba50c67b066fc3f36073b.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-22bc9c7bcf96ec817c83c26104408cb6c3b99c6b808ba50c67b066fc3f36073b.json deleted file mode 100644 index 3fbb4e474d..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-22bc9c7bcf96ec817c83c26104408cb6c3b99c6b808ba50c67b066fc3f36073b.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO reply_surb(reply_surb_sender_id, reply_surb, encoded_key_rotation) VALUES (?, ?, ?);\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "22bc9c7bcf96ec817c83c26104408cb6c3b99c6b808ba50c67b066fc3f36073b" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02.json deleted file mode 100644 index 6ee25bbc1a..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO ecash_ticketbook\n (serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-28681fcd8e2d4326f628681b8f2a317aabce063a650be362d3a8ed83cc7c3549.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-28681fcd8e2d4326f628681b8f2a317aabce063a650be362d3a8ed83cc7c3549.json deleted file mode 100644 index 32f3f9958c..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-28681fcd8e2d4326f628681b8f2a317aabce063a650be362d3a8ed83cc7c3549.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM blinded_shares WHERE created < ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "28681fcd8e2d4326f628681b8f2a317aabce063a650be362d3a8ed83cc7c3549" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-2930ca6e3875c74acb7abb9ad889f166ad7f57681f76a1d0c7723d007c1f2c1e.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-2930ca6e3875c74acb7abb9ad889f166ad7f57681f76a1d0c7723d007c1f2c1e.json deleted file mode 100644 index 9ef7d96cca..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-2930ca6e3875c74acb7abb9ad889f166ad7f57681f76a1d0c7723d007c1f2c1e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)\n VALUES (?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "2930ca6e3875c74acb7abb9ad889f166ad7f57681f76a1d0c7723d007c1f2c1e" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210.json deleted file mode 100644 index 4fd84fdf93..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data)\n VALUES (?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef.json deleted file mode 100644 index 405049043d..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM emergency_credential\n WHERE id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4.json deleted file mode 100644 index ea79b11bbd..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-396f40c33f0f62796eb7449d640bd97845350f4fb9f806c60b93c7cebd5e410d.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-396f40c33f0f62796eb7449d640bd97845350f4fb9f806c60b93c7cebd5e410d.json deleted file mode 100644 index 944c86e33e..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-396f40c33f0f62796eb7449d640bd97845350f4fb9f806c60b93c7cebd5e410d.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT error_message\n FROM blinded_shares\n WHERE id = ?;\n ", - "describe": { - "columns": [ - { - "name": "error_message", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "396f40c33f0f62796eb7449d640bd97845350f4fb9f806c60b93c7cebd5e410d" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20.json deleted file mode 100644 index bd975d3146..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE gateways SET last_ports_check_utc = $1, last_updated_utc = $1 WHERE id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3cc446220668fb3e02f0578104291d2a2af57656b405212af414d765b2263347.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3cc446220668fb3e02f0578104291d2a2af57656b405212af414d765b2263347.json deleted file mode 100644 index 9d8cff1326..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-3cc446220668fb3e02f0578104291d2a2af57656b405212af414d765b2263347.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM global_expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ", - "describe": { - "columns": [ - { - "name": "serialised_signatures", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "serialization_revision: u8", - "ordinal": 1, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false - ] - }, - "hash": "3cc446220668fb3e02f0578104291d2a2af57656b405212af414d765b2263347" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-42dfc51aea793fc60484cff6c481bbace1bcf110e048e594e8bd03fa45290732.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-42dfc51aea793fc60484cff6c481bbace1bcf110e048e594e8bd03fa45290732.json deleted file mode 100644 index d1ea957e7f..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-42dfc51aea793fc60484cff6c481bbace1bcf110e048e594e8bd03fa45290732.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE status SET client_in_use = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "42dfc51aea793fc60484cff6c481bbace1bcf110e048e594e8bd03fa45290732" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9.json deleted file mode 100644 index e71f300174..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM emergency_credential\n WHERE type = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d.json deleted file mode 100644 index eb2dc28e12..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM custom_gateway_details WHERE gateway_id_bs58 = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-52b378e282d93db941eff53b5b311e5732ece0bf84ea98f2328b20add8f2b5ef.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-52b378e282d93db941eff53b5b311e5732ece0bf84ea98f2328b20add8f2b5ef.json deleted file mode 100644 index 77a99816d4..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-52b378e282d93db941eff53b5b311e5732ece0bf84ea98f2328b20add8f2b5ef.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM partial_blinded_wallet_failure WHERE created < ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "52b378e282d93db941eff53b5b311e5732ece0bf84ea98f2328b20add8f2b5ef" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11.json deleted file mode 100644 index b1d46fd96b..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT * FROM custom_gateway_details WHERE gateway_id_bs58 = ?", - "describe": { - "columns": [ - { - "name": "gateway_id_bs58", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "data", - "ordinal": 1, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - true - ] - }, - "hash": "54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b.json deleted file mode 100644 index ad91647c9a..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT gateway_id_bs58 FROM registered_gateway", - "describe": { - "columns": [ - { - "name": "gateway_id_bs58", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966.json deleted file mode 100644 index ea667b0bc6..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM pending_issuance WHERE deposit_id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f.json deleted file mode 100644 index eb6a62ba22..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ", - "describe": { - "columns": [ - { - "name": "serialised_signatures", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "serialization_revision: u8", - "ordinal": 1, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false - ] - }, - "hash": "5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-5e51396c534409a4b55c08170e00fd083e87cc9a18d798b2cf8d6774224aebed.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-5e51396c534409a4b55c08170e00fd083e87cc9a18d798b2cf8d6774224aebed.json deleted file mode 100644 index 35fd74e6ee..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-5e51396c534409a4b55c08170e00fd083e87cc9a18d798b2cf8d6774224aebed.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT name FROM sqlite_master WHERE type='table' AND name='status'", - "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - true - ] - }, - "hash": "5e51396c534409a4b55c08170e00fd083e87cc9a18d798b2cf8d6774224aebed" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-62f471858c831ac762a0ecf60876067dbf82d0dc376ad3f0df835b77dfcc3c37.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-62f471858c831ac762a0ecf60876067dbf82d0dc376ad3f0df835b77dfcc3c37.json deleted file mode 100644 index 632b688eeb..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-62f471858c831ac762a0ecf60876067dbf82d0dc376ad3f0df835b77dfcc3c37.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT flush_in_progress FROM status;", - "describe": { - "columns": [ - { - "name": "flush_in_progress", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "62f471858c831ac762a0ecf60876067dbf82d0dc376ad3f0df835b77dfcc3c37" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-648fff18c4016fb356c164f813e0d5ebd37d62612b65b8ac4c9eb5d7b67a8884.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-648fff18c4016fb356c164f813e0d5ebd37d62612b65b8ac4c9eb5d7b67a8884.json deleted file mode 100644 index 974bbd1cd1..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-648fff18c4016fb356c164f813e0d5ebd37d62612b65b8ac4c9eb5d7b67a8884.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT min_reply_surb_threshold as \"min_reply_surb_threshold: u32\", max_reply_surb_threshold as \"max_reply_surb_threshold: u32\" FROM reply_surb_storage_metadata;\n ", - "describe": { - "columns": [ - { - "name": "min_reply_surb_threshold: u32", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "max_reply_surb_threshold: u32", - "ordinal": 1, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false - ] - }, - "hash": "648fff18c4016fb356c164f813e0d5ebd37d62612b65b8ac4c9eb5d7b67a8884" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-67db5e91bcdc831e0d1659eda358d18a0c6792d7a4244a65243d4b22d578a1e5.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-67db5e91bcdc831e0d1659eda358d18a0c6792d7a4244a65243d4b22d578a1e5.json deleted file mode 100644 index 01a4ca077c..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-67db5e91bcdc831e0d1659eda358d18a0c6792d7a4244a65243d4b22d578a1e5.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT client_in_use FROM status;", - "describe": { - "columns": [ - { - "name": "client_in_use", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "67db5e91bcdc831e0d1659eda358d18a0c6792d7a4244a65243d4b22d578a1e5" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-70d8f240ad6edda6b8c7f2e800e7fca89d80869484f2f3c66cabb898f0298c62.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-70d8f240ad6edda6b8c7f2e800e7fca89d80869484f2f3c66cabb898f0298c62.json deleted file mode 100644 index 2f22ed93fa..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-70d8f240ad6edda6b8c7f2e800e7fca89d80869484f2f3c66cabb898f0298c62.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "70d8f240ad6edda6b8c7f2e800e7fca89d80869484f2f3c66cabb898f0298c62" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef.json deleted file mode 100644 index 389d07c6ea..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type)\n VALUES (?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-74d77a3e15f49bde7b8b17dd638de04e5ece789fb0b0cd27ad09858fbf5c5e27.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-74d77a3e15f49bde7b8b17dd638de04e5ece789fb0b0cd27ad09858fbf5c5e27.json deleted file mode 100644 index caf7479a75..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-74d77a3e15f49bde7b8b17dd638de04e5ece789fb0b0cd27ad09858fbf5c5e27.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE status SET flush_in_progress = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "74d77a3e15f49bde7b8b17dd638de04e5ece789fb0b0cd27ad09858fbf5c5e27" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582.json deleted file mode 100644 index 02b9ef7426..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE active_gateway SET active_gateway_id_bs58 = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326.json deleted file mode 100644 index e1eb54bb9c..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO emergency_credential\n (type, content, expiration)\n VALUES (?, ?, ?)\n ON CONFLICT(type, content) DO NOTHING;\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4.json deleted file mode 100644 index ae81f52d0e..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO pending_issuance\n (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date)\n VALUES (?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3.json deleted file mode 100644 index 051c18bba2..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-85d542a8cb2e1fbba6142729056b57e6ea8685e9473a3a8bd635552810493a58.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-85d542a8cb2e1fbba6142729056b57e6ea8685e9473a3a8bd635552810493a58.json deleted file mode 100644 index 12145c00d4..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-85d542a8cb2e1fbba6142729056b57e6ea8685e9473a3a8bd635552810493a58.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as \"encoded_key_rotation: u8\" FROM reply_surb\n WHERE reply_surb_sender_id = ?\n ", - "describe": { - "columns": [ - { - "name": "reply_surb_sender_id", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "reply_surb", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "encoded_key_rotation: u8", - "ordinal": 2, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "85d542a8cb2e1fbba6142729056b57e6ea8685e9473a3a8bd635552810493a58" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-97d97ebb6bc8f4114fdea9ebc9f57f91a11f5057273cb70bd0e629712d17dd41.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-97d97ebb6bc8f4114fdea9ebc9f57f91a11f5057273cb70bd0e629712d17dd41.json deleted file mode 100644 index 84e8ba9f16..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-97d97ebb6bc8f4114fdea9ebc9f57f91a11f5057273cb70bd0e629712d17dd41.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message)\n VALUES (?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "97d97ebb6bc8f4114fdea9ebc9f57f91a11f5057273cb70bd0e629712d17dd41" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-a5a71e480118639397f7464abb09db66d1dc1307c0873627b39cb22a245b5a8c.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-a5a71e480118639397f7464abb09db66d1dc1307c0873627b39cb22a245b5a8c.json deleted file mode 100644 index 6cd1eab066..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-a5a71e480118639397f7464abb09db66d1dc1307c0873627b39cb22a245b5a8c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO status(flush_in_progress, previous_flush, client_in_use) VALUES (0, 0, 1)", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "a5a71e480118639397f7464abb09db66d1dc1307c0873627b39cb22a245b5a8c" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e.json deleted file mode 100644 index 5fd4e4f2e6..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?);\n UPDATE master_verification_key\n SET\n serialised_key = ?,\n serialization_revision = ?\n WHERE epoch_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-a8b7ce0fe4755c28b96d1e503e313ab15fed747fb0cee1c9f949fb58461b3f79.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-a8b7ce0fe4755c28b96d1e503e313ab15fed747fb0cee1c9f949fb58461b3f79.json deleted file mode 100644 index 6cb8138cc5..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-a8b7ce0fe4755c28b96d1e503e313ab15fed747fb0cee1c9f949fb58461b3f79.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM global_coin_index_signatures WHERE epoch_id = ?\n ", - "describe": { - "columns": [ - { - "name": "epoch_id: u32", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "serialised_signatures", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "serialization_revision: u8", - "ordinal": 2, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "a8b7ce0fe4755c28b96d1e503e313ab15fed747fb0cee1c9f949fb58461b3f79" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973.json deleted file mode 100644 index 9d0d9105ef..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH oldest_queued AS (\n SELECT id\n FROM testruns\n WHERE status = $1 AND kind = $4\n ORDER BY created_utc asc\n LIMIT 1\n FOR UPDATE SKIP LOCKED\n )\n UPDATE testruns\n SET\n status = $3,\n last_assigned_utc = $2\n FROM oldest_queued\n WHERE testruns.id = oldest_queued.id\n RETURNING\n testruns.id,\n testruns.gateway_id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "gateway_id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int4", - "Int8", - "Int4", - "Int2" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-b0c588db5f7be90c6d4cb1b55c3a8ed4e9a64884ac5619e0abdecb04f2d13a74.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-b0c588db5f7be90c6d4cb1b55c3a8ed4e9a64884ac5619e0abdecb04f2d13a74.json deleted file mode 100644 index 677fc961c0..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-b0c588db5f7be90c6d4cb1b55c3a8ed4e9a64884ac5619e0abdecb04f2d13a74.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n id,\n gateway_identity_key\n FROM gateways\n WHERE id = $1\n LIMIT 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "gateway_identity_key", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "b0c588db5f7be90c6d4cb1b55c3a8ed4e9a64884ac5619e0abdecb04f2d13a74" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-b6ce77fcffda2ee24ba181213e04adec312abffa604fa805cc362a2c206107b4.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-b6ce77fcffda2ee24ba181213e04adec312abffa604fa805cc362a2c206107b4.json deleted file mode 100644 index 7b8a1a92fc..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-b6ce77fcffda2ee24ba181213e04adec312abffa604fa805cc362a2c206107b4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO reply_surb_storage_metadata(min_reply_surb_threshold, max_reply_surb_threshold)\n VALUES (?, ?);\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "b6ce77fcffda2ee24ba181213e04adec312abffa604fa805cc362a2c206107b4" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-b8257a0832d0124f0a8aaaf81dc6a811c593aea8febf1f891117e5e84213f147.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-b8257a0832d0124f0a8aaaf81dc6a811c593aea8febf1f891117e5e84213f147.json deleted file mode 100644 index 03efdc4734..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-b8257a0832d0124f0a8aaaf81dc6a811c593aea8febf1f891117e5e84213f147.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM partial_blinded_wallet WHERE created < ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "b8257a0832d0124f0a8aaaf81dc6a811c593aea8febf1f891117e5e84213f147" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json deleted file mode 100644 index 7c01c8d1fa..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM coin_indices_signatures WHERE epoch_id = ?\n ", - "describe": { - "columns": [ - { - "name": "epoch_id: u32", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "serialised_signatures", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "serialization_revision: u8", - "ordinal": 2, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4.json deleted file mode 100644 index ec7111bc5a..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE ecash_ticketbook\n SET used_tickets = used_tickets - ?\n WHERE id = ?\n AND used_tickets = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68.json deleted file mode 100644 index a2077f76ae..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?);\n UPDATE coin_indices_signatures\n SET\n serialised_signatures = ?,\n serialization_revision = ?\n WHERE epoch_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-bdaf8e8dd711ff420807659456f5ebe0222a2a0ba96f28e3a3aa58fdc4b689db.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-bdaf8e8dd711ff420807659456f5ebe0222a2a0ba96f28e3a3aa58fdc4b689db.json deleted file mode 100644 index 383ca24d4e..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-bdaf8e8dd711ff420807659456f5ebe0222a2a0ba96f28e3a3aa58fdc4b689db.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO reply_surb_sender(tag, last_sent) VALUES (?, ?);\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "bdaf8e8dd711ff420807659456f5ebe0222a2a0ba96f28e3a3aa58fdc4b689db" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1.json deleted file mode 100644 index a1ae46b595..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT active_gateway_id_bs58 FROM active_gateway", - "describe": { - "columns": [ - { - "name": "active_gateway_id_bs58", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - true - ] - }, - "hash": "bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-c2b841762bdb963fff337ef5c8ec9f560017b4da6b0303ea0397d9568229e167.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-c2b841762bdb963fff337ef5c8ec9f560017b4da6b0303ea0397d9568229e167.json deleted file mode 100644 index 6b70481c75..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-c2b841762bdb963fff337ef5c8ec9f560017b4da6b0303ea0397d9568229e167.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n t1.node_id as \"node_id!\",\n t1.blinded_signature as \"blinded_signature!\",\n t1.epoch_id as \"epoch_id!\",\n t1.expiration_date as \"expiration_date!: Date\"\n FROM partial_blinded_wallet as t1\n JOIN ecash_deposit_usage as t2\n on t1.corresponding_deposit = t2.deposit_id\n JOIN blinded_shares as t3\n ON t2.request_uuid = t3.request_uuid\n WHERE t3.device_id = ? AND t3.credential_id = ?;\n ", - "describe": { - "columns": [ - { - "name": "node_id!", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "blinded_signature!", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "epoch_id!", - "ordinal": 2, - "type_info": "Integer" - }, - { - "name": "expiration_date!: Date", - "ordinal": 3, - "type_info": "Date" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - true, - true, - true, - true - ] - }, - "hash": "c2b841762bdb963fff337ef5c8ec9f560017b4da6b0303ea0397d9568229e167" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370.json deleted file mode 100644 index 5e76c4c3cc..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT last_probe_result FROM gateways WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "last_probe_result", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - true - ] - }, - "hash": "cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-d3510846941fa2525926b9bfbcdabd806877ce914b514d4f7cd6be318c4debe6.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-d3510846941fa2525926b9bfbcdabd806877ce914b514d4f7cd6be318c4debe6.json deleted file mode 100644 index 9de4c7f230..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-d3510846941fa2525926b9bfbcdabd806877ce914b514d4f7cd6be318c4debe6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "d3510846941fa2525926b9bfbcdabd806877ce914b514d4f7cd6be318c4debe6" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-daa9ffcbe13178d773e538e96971da2809ec4bd66ad49d2e3b8d67b741835475.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-daa9ffcbe13178d773e538e96971da2809ec4bd66ad49d2e3b8d67b741835475.json deleted file mode 100644 index 1faa4be465..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-daa9ffcbe13178d773e538e96971da2809ec4bd66ad49d2e3b8d67b741835475.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM reply_surb_sender;", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "daa9ffcbe13178d773e538e96971da2809ec4bd66ad49d2e3b8d67b741835475" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-db176e98198fe594d88eb860d918f633a94d18a19b7f0f96935a62560def7d0f.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-db176e98198fe594d88eb860d918f633a94d18a19b7f0f96935a62560def7d0f.json deleted file mode 100644 index e5dd7c8413..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-db176e98198fe594d88eb860d918f633a94d18a19b7f0f96935a62560def7d0f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature)\n VALUES (?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "db176e98198fe594d88eb860d918f633a94d18a19b7f0f96935a62560def7d0f" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-decdc7c0e6d279a137a80797cbabcb8900bca7157d3c66c5664386ca2c26e96a.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-decdc7c0e6d279a137a80797cbabcb8900bca7157d3c66c5664386ca2c26e96a.json new file mode 100644 index 0000000000..f28a1c1921 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-decdc7c0e6d279a137a80797cbabcb8900bca7157d3c66c5664386ca2c26e96a.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(id) as \"count!: i64\" FROM testruns WHERE status = $1 AND kind = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count!: i64", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4", + "Int2" + ] + }, + "nullable": [ + null + ] + }, + "hash": "decdc7c0e6d279a137a80797cbabcb8900bca7157d3c66c5664386ca2c26e96a" +} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-dfa035c968534926736adf0e5359cde3f6f6689a80299b5b6c1d7bd048965e6e.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-dfa035c968534926736adf0e5359cde3f6f6689a80299b5b6c1d7bd048965e6e.json deleted file mode 100644 index 2887c3bea6..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-dfa035c968534926736adf0e5359cde3f6f6689a80299b5b6c1d7bd048965e6e.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE gateways SET last_probe_result = $1 WHERE id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "dfa035c968534926736adf0e5359cde3f6f6689a80299b5b6c1d7bd048965e6e" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-e29e0c82d034626f9b369348b8e35677b9d9a1b29e4d89437134e92967fa98d1.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-e29e0c82d034626f9b369348b8e35677b9d9a1b29e4d89437134e92967fa98d1.json deleted file mode 100644 index ecab96d023..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-e29e0c82d034626f9b369348b8e35677b9d9a1b29e4d89437134e92967fa98d1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM reply_surb;", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "e29e0c82d034626f9b369348b8e35677b9d9a1b29e4d89437134e92967fa98d1" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-e584253e3856355899537eb8fc152f2bfed2d918b894ec0f588e38dd5e8ad726.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-e584253e3856355899537eb8fc152f2bfed2d918b894ec0f588e38dd5e8ad726.json deleted file mode 100644 index 340f83a280..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-e584253e3856355899537eb8fc152f2bfed2d918b894ec0f588e38dd5e8ad726.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE ecash_deposit_usage\n SET ticketbook_request_error = ?\n WHERE deposit_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "e584253e3856355899537eb8fc152f2bfed2d918b894ec0f588e38dd5e8ad726" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-e77ffab19b099b84470fe5611716a2e314787586a46cffd074abb67f2f4d109e.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-e77ffab19b099b84470fe5611716a2e314787586a46cffd074abb67f2f4d109e.json deleted file mode 100644 index 449bc6869f..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-e77ffab19b099b84470fe5611716a2e314787586a46cffd074abb67f2f4d109e.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as \"expiration_date!: Date\"\n FROM partial_blinded_wallet as t1\n JOIN ecash_deposit_usage as t2\n on t1.corresponding_deposit = t2.deposit_id\n JOIN blinded_shares as t3\n ON t2.request_uuid = t3.request_uuid\n WHERE t3.id = ?;\n ", - "describe": { - "columns": [ - { - "name": "node_id", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "blinded_signature", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "epoch_id", - "ordinal": 2, - "type_info": "Integer" - }, - { - "name": "expiration_date!: Date", - "ordinal": 3, - "type_info": "Date" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "e77ffab19b099b84470fe5611716a2e314787586a46cffd074abb67f2f4d109e" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-e8717ef6523ef9bcf37a0e81570ac000904bb7fe3934786d652adc80fde78add.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-e8717ef6523ef9bcf37a0e81570ac000904bb7fe3934786d652adc80fde78add.json deleted file mode 100644 index a7d1dee6ec..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-e8717ef6523ef9bcf37a0e81570ac000904bb7fe3934786d652adc80fde78add.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT previous_flush AS \"previous_flush: OffsetDateTime\" FROM status", - "describe": { - "columns": [ - { - "name": "previous_flush: OffsetDateTime", - "ordinal": 0, - "type_info": "Null" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "e8717ef6523ef9bcf37a0e81570ac000904bb7fe3934786d652adc80fde78add" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-eded014a5fffbd9c359f8557bcd3d41724666bf92357bf5faf42120a2aab131c.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-eded014a5fffbd9c359f8557bcd3d41724666bf92357bf5faf42120a2aab131c.json deleted file mode 100644 index 7e0d68b0db..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-eded014a5fffbd9c359f8557bcd3d41724666bf92357bf5faf42120a2aab131c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO reply_key(key_digest, reply_key, sent_at) VALUES (?, ?, ?);\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "eded014a5fffbd9c359f8557bcd3d41724666bf92357bf5faf42120a2aab131c" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-ef60c2683211cc4ec2d3e46392518a1f62fa67dfe8f130deb876ebee11bf1602.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-ef60c2683211cc4ec2d3e46392518a1f62fa67dfe8f130deb876ebee11bf1602.json deleted file mode 100644 index c78ff30f50..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-ef60c2683211cc4ec2d3e46392518a1f62fa67dfe8f130deb876ebee11bf1602.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT error_message\n FROM blinded_shares\n WHERE device_id = ? AND credential_id = ?;\n ", - "describe": { - "columns": [ - { - "name": "error_message", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - true - ] - }, - "hash": "ef60c2683211cc4ec2d3e46392518a1f62fa67dfe8f130deb876ebee11bf1602" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93.json deleted file mode 100644 index e2cfa29a82..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM registered_gateway WHERE gateway_id_bs58 = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93" -} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs index 2535838e58..6c6e3e5bda 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs @@ -27,15 +27,15 @@ pub(crate) async fn count_testruns_in_progress( pub(crate) async fn count_testruns_in_progress_by_kind( conn: &mut DbConnection, kind: TestRunKind, -) -> anyhow::Result> { - let count: i64 = - sqlx::query_scalar(r#"SELECT COUNT(id) FROM testruns WHERE status = $1 AND kind = $2"#) - .bind(TestRunStatus::InProgress as i64) - .bind(kind as i16) - .fetch_one(conn.as_mut()) - .await - .map_err(anyhow::Error::from)?; - Ok(Some(count)) +) -> anyhow::Result { + sqlx::query_scalar!( + r#"SELECT COUNT(id) as "count!: i64" FROM testruns WHERE status = $1 AND kind = $2"#, + TestRunStatus::InProgress as i64, + kind as i16, + ) + .fetch_one(conn.as_mut()) + .await + .map_err(anyhow::Error::from) } pub(crate) async fn get_in_progress_testrun_by_id( diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs index a9c421d395..98219a8348 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs @@ -147,8 +147,7 @@ async fn request_ports_check_testrun( TestRunKind::PortsCheck, ) .await - .map_err(HttpError::internal_with_logging)? - .unwrap_or_default(); + .map_err(HttpError::internal_with_logging)?; let max_count = state.agent_max_count(); if active_ports_check_testruns >= max_count { tracing::warn!( From 8392f7da9400cfad1cdd7af1bb509dae527f1e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 1 Jun 2026 16:21:27 +0100 Subject: [PATCH 16/17] fix(ns-api): lock assignment + ticketbook pre-check in ports-check request request_ports_check_testrun skipped the lock_testrun_assignment() guard and has_enough_ticketbooks() pre-check that request_testrun performs, so two concurrent ports-check requests could race on ticket materials and a depleted cache left the run InProgress until stale-refresh. Mirror the probe path. --- .../src/http/api/testruns.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs index 98219a8348..0e7384866c 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs @@ -156,6 +156,31 @@ async fn request_ports_check_testrun( return Err(HttpError::no_testruns_available()); } + // 1. guard against other agents attempting to get testruns so that we would be able to check + // for ticketbook count without cross-interaction + let _guard = state.lock_testrun_assignment().await; + + // 2. check if we have enough ticketbooks of ALL types before assigning the run + match state + .ticketbook_manager_state() + .has_enough_ticketbooks() + .await + { + Err(err) => { + return Err(HttpError::internal_with_logging(format!( + "failed to check ticketbook storage: {err}" + ))); + } + Ok(false) => { + tracing::warn!("not enough ticketbooks available, rejecting testrun assignment"); + return Err(HttpError::internal_with_logging( + "not enough ticketbooks available to assign a testrun", + )); + } + Ok(true) => (), + } + + // 3. attempt to assign the testrun itself match db::queries::testruns::assign_oldest_ports_check_testrun(&mut conn).await { Ok(res) => { let Some(assignment) = res else { From dad2d3077394e94ae92e2ee982ef03915ab9b450 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Tue, 2 Jun 2026 08:03:27 +0200 Subject: [PATCH 17/17] address comment --- nym-gateway-probe/src/common/probe_tests.rs | 26 +++++++++---------- nym-gateway-probe/src/common/wireguard.rs | 19 ++++++++------ .../src/cli/run_ports_check.rs | 25 +++++++++++++++--- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/nym-gateway-probe/src/common/probe_tests.rs b/nym-gateway-probe/src/common/probe_tests.rs index 30b14345d0..ee403d6b07 100644 --- a/nym-gateway-probe/src/common/probe_tests.rs +++ b/nym-gateway-probe/src/common/probe_tests.rs @@ -80,8 +80,6 @@ pub async fn wg_probe( AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => bail!("unknown version number"), }; - let mut wg_outcome = WgProbeResults::default(); - info!( "connecting to authenticator: {}...", auth_client.auth_recipient @@ -135,9 +133,9 @@ pub async fn wg_probe( info!("Successfully registered with the gateway"); - wg_outcome.can_register = true; - - // Run tunnel connectivity tests using shared helper + // Run tunnel connectivity tests using shared helper. + // run_tunnel_tests issues blocking CGo calls into Go, so it must run on + // tokio's dedicated blocking thread pool to avoid stalling the async runtime. let tunnel_config = WgTunnelConfig::new( registered_data.private_ips().ipv4.to_string(), registered_data.private_ips().ipv6.to_string(), @@ -145,16 +143,18 @@ pub async fn wg_probe( public_key_hex, wg_endpoint, ); + let awg_str = awg_args.unwrap_or_default(); - run_tunnel_tests( - &tunnel_config, - &netstack_args, - &awg_args.unwrap_or_default(), - port_check_only, - &mut wg_outcome, - ); + let mut tunnel_result = tokio::task::spawn_blocking(move || { + run_tunnel_tests(&tunnel_config, &netstack_args, &awg_str, port_check_only) + }) + .await + .map_err(|e| anyhow::anyhow!("netstack task panicked: {e}"))?; - Ok(wg_outcome) + // can_register is determined by the auth handshake above, not by netstack + tunnel_result.can_register = true; + + Ok(tunnel_result) } pub async fn lp_registration_probe( diff --git a/nym-gateway-probe/src/common/wireguard.rs b/nym-gateway-probe/src/common/wireguard.rs index a2cb783433..ad20d96693 100644 --- a/nym-gateway-probe/src/common/wireguard.rs +++ b/nym-gateway-probe/src/common/wireguard.rs @@ -65,24 +65,25 @@ impl WgTunnelConfig { /// - Optional download test /// - Optional exit policy port check (TCP connect through tunnel) /// -/// Results are written directly into the provided `wg_outcome` to avoid field-by-field -/// copying at call sites. +/// **Important:** this function issues blocking FFI calls into Go (CGo) and MUST be +/// called via `tokio::task::spawn_blocking` at any async call site. It returns a +/// fresh `WgProbeResults`; the caller sets `can_register` after verifying WG +/// registration succeeded. /// /// # Arguments /// * `config` - WireGuard tunnel configuration /// * `netstack_args` - Netstack test parameters (DNS, hosts to ping, timeouts, etc.) /// * `awg_args` - Amnezia WireGuard arguments (empty string for standard WG) /// * `port_check_only` - If true, skip pings/download and only run TCP port checks -/// * `wg_outcome` - Mutable reference to write test results into -// This function extracts the shared netstack testing logic from -// wg_probe() and wg_probe_lp() to eliminate code duplication. +// This function extracts the shared netstack testing logic from wg_probe() +// to eliminate code duplication across probe modes. pub fn run_tunnel_tests( config: &WgTunnelConfig, netstack_args: &NetstackArgs, awg_args: &str, port_check_only: bool, - wg_outcome: &mut WgProbeResults, -) { +) -> WgProbeResults { + let mut wg_outcome = WgProbeResults::default(); // Build the netstack request let netstack_request = NetstackRequest::new( &config.private_ipv4, @@ -143,7 +144,7 @@ pub fn run_tunnel_tests( // in port-check-only mode, skip IPv6 tests — port checks ran through IPv4 above if port_check_only { info!("Port-check-only mode: skipping IPv6 tunnel tests"); - return; + return wg_outcome; } // Perform IPv6 ping test @@ -182,4 +183,6 @@ pub fn run_tunnel_tests( error!("Internal error (IPv6): {error}") } } + + wg_outcome } diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs b/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs index c4bc641b6f..aa370ceb15 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs @@ -3,6 +3,8 @@ use crate::cli::common; use crate::log_capture::LogCapture; use nym_gateway_probe::RunPortsConfig; use tracing::instrument; +// Hard deadline for a single port-scan job. +const PORT_SCAN_HARD_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5400); pub(crate) async fn run_ports_check( servers: &[ServerConfig], @@ -55,15 +57,30 @@ pub(crate) async fn run_ports_check( let credentials_args = common::credential_args_from(testrun.ticket_materials); log_capture.start(); - let port_check_result_res = nym_gateway_probe::Probe::run_ports_for_agent( + let probe_future = nym_gateway_probe::Probe::run_ports_for_agent( gateway_identity_pubkey, network, &run_ports_config, credentials_args, - ) - .await; + ); + let port_check_result_res = tokio::time::timeout(PORT_SCAN_HARD_TIMEOUT, probe_future).await; let probe_log = log_capture.stop_and_drain(); - let port_check_result = port_check_result_res?; + + let port_check_result = match port_check_result_res { + Ok(inner) => inner?, + Err(_elapsed) => { + tracing::error!( + gateway = %gateway_identity_key, + testrun = testrun_id, + timeout_secs = PORT_SCAN_HARD_TIMEOUT.as_secs(), + "Port scan exceeded hard timeout; aborting to free resources" + ); + return Err(anyhow::anyhow!( + "port scan timed out after {}s", + PORT_SCAN_HARD_TIMEOUT.as_secs() + )); + } + }; submit_ports_check_results_to_servers( servers,