From 90e07d99804d5746f22162810c65c9e22e01ed14 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Fri, 24 Oct 2025 12:08:16 +0200 Subject: [PATCH 1/4] weighted scoring and unit test --- .../nym-node-status-api/src/http/models.rs | 146 ++++++++++++++++-- 1 file changed, 136 insertions(+), 10 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index 2721115068d..3d4b9fbf68b 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -387,13 +387,14 @@ fn calculate_mixnet_score(gateway: &Gateway) -> ScoreValue { } } -/// calculates a visual score for the gateway +/// calculates a visual score for the gateway using weighted metrics fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreValue { let mixnet_performance = gateway.performance as f64 / 100.0; - let ping_ips_performance = probe_outcome + + let (download_speed_score, ping_ips_performance) = probe_outcome .outcome .wg - .clone() + .as_ref() .map(|p| { let ping_ips_performance = p.ping_ips_performance_v4 as f64; @@ -419,18 +420,19 @@ fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreV 0.1 }; - // combine the scores - file_download_score * ping_ips_performance + (file_download_score, ping_ips_performance) }) - .unwrap_or(0f64); + .unwrap_or((0.0, 0.0)); - let score = mixnet_performance * ping_ips_performance; + // Weighted scoring: mixnet performance (40%), download speed (30%), ping performance (30%) + let weighted_score = + (mixnet_performance * 0.4) + (download_speed_score * 0.3) + (ping_ips_performance * 0.3); - if score > 0.75 { + if weighted_score > 0.75 { ScoreValue::High - } else if score > 0.5 { + } else if weighted_score > 0.5 { ScoreValue::Medium - } else if score > 0.1 { + } else if weighted_score > 0.1 { ScoreValue::Low } else { ScoreValue::Offline @@ -733,6 +735,130 @@ mod test { assert!(service.mixnet_websockets.is_none()); assert!(service.last_successful_ping_utc.is_none()); } + + #[test] + fn test_weighted_score_calculation() { + use crate::http::models::directory_gw_probe_outcome::EntryTestResult; + use crate::http::models::wg_outcome_versions::ProbeOutcomeV1; + + // Helper function to create a test gateway + fn create_test_gateway(performance: u8) -> Gateway { + Gateway { + gateway_identity_key: "test_key".to_string(), + bonded: true, + performance, + self_described: None, + explorer_pretty_bond: None, + description: nym_node_requests::api::v1::node::models::NodeDescription { + moniker: "test".to_string(), + details: "test".to_string(), + security_contact: "test@example.com".to_string(), + website: "https://example.com".to_string(), + }, + last_probe_result: None, + last_probe_log: None, + last_testrun_utc: None, + last_updated_utc: "2025-10-10T00:00:00Z".to_string(), + routing_score: 0.0, + config_score: 0, + bridges: None, + } + } + + // Helper function to create a test probe outcome + fn create_test_probe_outcome( + download_speed_mbps: f64, + ping_ips_performance: f32, + ) -> LastProbeResult { + let duration_sec = 1.0; + let file_size_mb = download_speed_mbps; + + LastProbeResult { + node: "test_node".to_string(), + used_entry: "test_entry".to_string(), + outcome: ProbeOutcome { + as_entry: directory_gw_probe_outcome::Entry::Tested(EntryTestResult { + can_connect: true, + can_route: true, + }), + as_exit: None, + wg: Some(ProbeOutcomeV1 { + can_register: true, + can_handshake: Some(true), + can_resolve_dns: Some(true), + ping_hosts_performance: Some(ping_ips_performance), + ping_ips_performance: Some(ping_ips_performance), + can_query_metadata_v4: Some(true), + can_handshake_v4: true, + can_resolve_dns_v4: true, + ping_hosts_performance_v4: ping_ips_performance, + ping_ips_performance_v4: ping_ips_performance, + can_handshake_v6: true, + can_resolve_dns_v6: true, + ping_hosts_performance_v6: ping_ips_performance, + ping_ips_performance_v6: ping_ips_performance, + download_duration_sec_v4: (duration_sec * 1000.0) as u64, + download_duration_milliseconds_v4: Some((duration_sec * 1000.0) as u64), + downloaded_file_size_bytes_v4: Some( + (file_size_mb * 1024.0 * 1024.0) as u64, + ), + downloaded_file_v4: "test".to_string(), + download_error_v4: "".to_string(), + download_duration_sec_v6: 0, + download_duration_milliseconds_v6: Some(0), + downloaded_file_size_bytes_v6: Some(0), + downloaded_file_v6: "".to_string(), + download_error_v6: "".to_string(), + }), + }, + } + } + + // Test case 1: Excellent node (should be High) + let gateway = create_test_gateway(90); // 90% mixnet performance + let probe = create_test_probe_outcome(6.0, 1.0); // 6 Mbps, 100% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::High, "Excellent node should be High"); + + // Test case 2: Good node (should be High with weighted scoring) + let gateway = create_test_gateway(90); // 90% mixnet performance + let probe = create_test_probe_outcome(3.0, 0.9); // 3 Mbps (0.75 score), 90% ping + let score = calculate_score(&gateway, &probe); + assert_eq!( + score, + ScoreValue::High, + "Good node should be High with weighted scoring" + ); + + // Test case 3: Medium node + let gateway = create_test_gateway(80); // 80% mixnet performance + let probe = create_test_probe_outcome(1.5, 0.8); // 1.5 Mbps (0.5 score), 80% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Medium, "Medium node should be Medium"); + + // Test case 4: Poor node + let gateway = create_test_gateway(60); // 60% mixnet performance + let probe = create_test_probe_outcome(0.3, 0.3); // 0.3 Mbps (0.1 score), 30% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Low, "Poor node should be Low"); + + // Test case 5: Failed node + let gateway = create_test_gateway(10); // 10% mixnet performance + let probe = create_test_probe_outcome(0.1, 0.0); // 0.1 Mbps (0.1 score), 0% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Offline, "Failed node should be Offline"); + + // Test case 6: Edge case - just above threshold + let gateway = create_test_gateway(76); // 76% mixnet performance + let probe = create_test_probe_outcome(2.1, 0.75); // 2.1 Mbps (0.75 score), 75% ping + let score = calculate_score(&gateway, &probe); + // Weighted: (0.76 * 0.4) + (0.75 * 0.3) + (0.75 * 0.3) = 0.304 + 0.225 + 0.225 = 0.754 + assert_eq!( + score, + ScoreValue::High, + "Edge case just above 0.75 threshold should be High" + ); + } } #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] From b00e1f2fffa05f0bc24f28b134bddba69f1028ed Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 20 Nov 2025 17:32:57 +0100 Subject: [PATCH 2/4] addressing comment --- .../nym-node-status-api/src/http/models.rs | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index 3d4b9fbf68b..2f0fe091831 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -372,6 +372,21 @@ impl DVpnGateway { } } +struct NodeScore { + download_speed_score: f64, + ping_ips_score: f64, + mixnet_performance: f64, +} + +impl NodeScore { + // Weighted scoring: mixnet performance (40%), download speed (30%), ping performance (30%) + fn calculate_weighted_score(&self) -> f64 { + (self.mixnet_performance * 0.4) + + (self.download_speed_score * 0.3) + + (self.ping_ips_score * 0.3) + } +} + /// calculates the gateway probe score for mixnet mode fn calculate_mixnet_score(gateway: &Gateway) -> ScoreValue { let mixnet_performance = gateway.performance as f64 / 100.0; @@ -391,7 +406,7 @@ fn calculate_mixnet_score(gateway: &Gateway) -> ScoreValue { fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreValue { let mixnet_performance = gateway.performance as f64 / 100.0; - let (download_speed_score, ping_ips_performance) = probe_outcome + let node_score = probe_outcome .outcome .wg .as_ref() @@ -420,13 +435,19 @@ fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreV 0.1 }; - (file_download_score, ping_ips_performance) + NodeScore { + download_speed_score: file_download_score, + ping_ips_score: ping_ips_performance, + mixnet_performance, + } }) - .unwrap_or((0.0, 0.0)); + .unwrap_or(NodeScore { + download_speed_score: 0.0, + ping_ips_score: 0.0, + mixnet_performance, + }); - // Weighted scoring: mixnet performance (40%), download speed (30%), ping performance (30%) - let weighted_score = - (mixnet_performance * 0.4) + (download_speed_score * 0.3) + (ping_ips_performance * 0.3); + let weighted_score = node_score.calculate_weighted_score(); if weighted_score > 0.75 { ScoreValue::High From 54c7a0148279384650e1b1d1b9e07d77ff3387a4 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 20 Nov 2025 17:36:30 +0100 Subject: [PATCH 3/4] update version --- Cargo.lock | 2 +- nym-node-status-api/nym-node-status-api/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ec3531bb6d..54a16eafe2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6552,7 +6552,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "4.0.11-rc1" +version = "4.0.11-testing" dependencies = [ "ammonia", "anyhow", 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 c92ebb4dd9d..b32100a6bae 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.0.11-rc1" +version = "4.0.11-testing" authors.workspace = true repository.workspace = true homepage.workspace = true From 6a96d8205b7fdc3b745667a9d67e9b26b218cbe8 Mon Sep 17 00:00:00 2001 From: benedetta davico <46782255+benedettadavico@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:33:17 +0100 Subject: [PATCH 4/4] Bump version from 4.0.11-testing to 4.0.12 --- nym-node-status-api/nym-node-status-api/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b32100a6bae..df6d5c12cfe 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.0.11-testing" +version = "4.0.12" authors.workspace = true repository.workspace = true homepage.workspace = true