diff --git a/Cargo.lock b/Cargo.lock index ce22ac9bcf..836ce887e6 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-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..e1947f2277 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,99 @@ 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 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()); + + 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 + && 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..fc36d93367 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,126 @@ 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. + // 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 + 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) + 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..ee403d6b07 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 { @@ -79,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 @@ -134,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(), @@ -144,15 +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(), - &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/types.rs b/nym-gateway-probe/src/common/types.rs index 8541a57421..72953cfcb9 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,43 @@ 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 { + 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() + } +} + #[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..ad20d96693 100644 --- a/nym-gateway-probe/src/common/wireguard.rs +++ b/nym-gateway-probe/src/common/wireguard.rs @@ -63,23 +63,27 @@ 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. +/// **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) -/// * `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. +/// * `port_check_only` - If true, skip pings/download and only run TCP port checks +// 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, - wg_outcome: &mut WgProbeResults, -) { + port_check_only: bool, +) -> WgProbeResults { + let mut wg_outcome = WgProbeResults::default(); // Build the netstack request let netstack_request = NetstackRequest::new( &config.private_ipv4, @@ -91,9 +95,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 +127,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 +141,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 wg_outcome; + } + // Perform IPv6 ping test info!("Testing IPv6 tunnel connectivity..."); let ipv6_request = NetstackRequestGo::from_rust_v6(&netstack_request); @@ -167,4 +183,6 @@ pub fn run_tunnel_tests( error!("Internal error (IPv6): {error}") } } + + wg_outcome } 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..a316666f37 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,295 @@ 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, +} + +/// 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, + 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, + target: PortScanTarget, + bandwidth: PortScanBandwidth<'_>, + 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(bandwidth.ticket_type, bandwidth.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, + target.authenticator, + target.authenticator_version, + Arc::new(x25519::KeyPair::new(&mut rng)), + target.ip_address, + ); + + match wg_probe( + auth_client, + target.ip_address, + target.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[0] { + 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, + 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, + ) + .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 +383,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 +561,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 +829,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 +858,7 @@ impl Probe { exit_node.authenticator_version, self.config.amnezia_args.clone(), self.config.netstack_args.clone(), + false, credential, ) .await @@ -404,6 +866,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..aa961b784b 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(Box), + 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(|r| ProbeOutput::Standard(Box::new(r))) } 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(|r| ProbeOutput::Standard(Box::new(r))) + } + 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(|r| ProbeOutput::Standard(Box::new(r))) } } } 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..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 @@ -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()); @@ -32,8 +34,10 @@ 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, @@ -53,6 +57,7 @@ pub(crate) struct Args { #[derive(Subcommand, Debug)] pub(crate) enum Command { RunProbe(RunProbeArgs), + RunPortsCheck(RunPortsCheckArgs), GenerateKeypair { #[arg(long)] @@ -73,6 +78,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 +124,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..aa370ceb15 --- /dev/null +++ b/nym-node-status-api/nym-node-status-agent/src/cli/run_ports_check.rs @@ -0,0 +1,149 @@ +use crate::cli::ServerConfig; +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], + 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 probe_future = nym_gateway_probe::Probe::run_ports_for_agent( + gateway_identity_pubkey, + network, + &run_ports_config, + credentials_args, + ); + 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 = 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, + 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..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 @@ -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,17 +44,13 @@ 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(); - 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() { @@ -110,15 +89,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-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-ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64.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-ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-0c635c539930c10a9be35a12ba3f2a66aae5be1c37af2eca521bf75261cecf28.json index 59962eb2ed..53756614d0 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-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 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 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": "ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64" + "hash": "0c635c539930c10a9be35a12ba3f2a66aae5be1c37af2eca521bf75261cecf28" } 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-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-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-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-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-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-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/.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-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-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-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-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-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" } 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-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-f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8.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-f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-b32d29ca3cedae93fa2fcd44cc301614edc4e967f348c57f485f736b8f2f03e8.json index b6dd6ac8a9..f543ada553 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-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 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 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": "f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8" + "hash": "b32d29ca3cedae93fa2fcd44cc301614edc4e967f348c57f485f736b8f2f03e8" } 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-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/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..f506b093f5 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20260415000000_gateways_ports_check.sql @@ -0,0 +1,16 @@ +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 ~ '^[\[{]' + 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'; \ 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'; 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..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,6 +82,15 @@ pub(crate) struct Cli { #[arg(value_parser = parse_duration_std)] pub(crate) testruns_refresh_interval: Duration, + /// Safety net for stale in-progress testruns (whole seconds, es. 7200). + #[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..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 @@ -87,6 +87,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,14 +104,22 @@ 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?"))?; - tracing::trace!("Fetched {} gateways from DB", items.len()); - Ok(items) + 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"); + } + tracing::trace!("Fetched {} gateways from DB", gateways.len()); + Ok(gateways) } } 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..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 @@ -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( @@ -22,6 +24,20 @@ 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, @@ -32,6 +48,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 +107,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 @@ -101,6 +131,7 @@ pub(crate) async fn assign_oldest_testrun( 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 @@ -120,28 +151,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 +216,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 +358,7 @@ pub(crate) async fn get_testrun_by_id( id, gateway_id, status, + kind, created_utc, ip_address, log, @@ -250,14 +385,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( + "/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,93 @@ 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_ports_check_testruns = db::queries::testruns::count_testruns_in_progress_by_kind( + &mut conn, + TestRunKind::PortsCheck, + ) + .await + .map_err(HttpError::internal_with_logging)?; + let max_count = state.agent_max_count(); + 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()); + } + + // 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 { + 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 +470,152 @@ 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, + ); + + // 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, + &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..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 @@ -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,6 +165,7 @@ 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(), 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..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 @@ -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, @@ -355,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 { @@ -432,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, }) } } @@ -676,6 +679,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 +743,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..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 @@ -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 = storage.pool().clone(); let state = AppState::new( storage, @@ -46,6 +48,35 @@ 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 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 { + 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..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 @@ -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}; @@ -65,15 +68,69 @@ impl NsApiClient { } } - serde_json::from_str(&response_text) - .map(|testrun| { - tracing::info!("Received testrun assignment: {:?}", testrun); - testrun - }) - .map_err(|err| { - tracing::error!("err"); - err.into() - }) + let testrun: TestrunAssignmentWithTickets = + serde_json::from_str(&response_text).map_err(|err| { + tracing::error!("{err}"); + 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)] + 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); + } + } + + let testrun: TestrunAssignmentWithTickets = + serde_json::from_str(&response_text).map_err(|err| { + tracing::error!("{err}"); + 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)] @@ -114,7 +171,7 @@ impl NsApiClient { response.error_for_status() })?; - tracing::debug!("Submitted results: {})", res.status()); + tracing::debug!("Submitted results: {}", res.status()); Ok(()) } @@ -147,7 +204,41 @@ 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(()) + } + + #[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(()) } 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 + } + } +}