1use crate::pagination::PaginatedResponse;
5use nym_crypto::asymmetric::ed25519;
6use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
7use nym_mixnet_contract_common::NodeId;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use utoipa::ToSchema;
12
13#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, Default, ToSchema)]
14pub struct TestNode {
15 pub node_id: Option<u32>,
16 pub identity_key: Option<String>,
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
20pub struct TestRoute {
21 pub gateway: TestNode,
22 pub layer1: TestNode,
23 pub layer2: TestNode,
24 pub layer3: TestNode,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
28pub struct PartialTestResult {
29 pub monitor_run_id: i64,
30 pub timestamp: i64,
31 pub overall_reliability_for_all_routes_in_monitor_run: Option<u8>,
32 pub test_routes: TestRoute,
33}
34
35pub type MixnodeTestResultResponse = PaginatedResponse<PartialTestResult>;
36pub type GatewayTestResultResponse = PaginatedResponse<PartialTestResult>;
37
38#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
39pub struct NetworkMonitorRunDetailsResponse {
40 pub monitor_run_id: i64,
41 pub network_reliability: f64,
42 pub total_sent: usize,
43 pub total_received: usize,
44
45 pub mixnode_results: BTreeMap<u8, usize>,
47 pub gateway_results: BTreeMap<u8, usize>,
48}
49
50#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
51#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
52#[cfg_attr(
53 feature = "generate-ts",
54 ts(
55 export,
56 export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts"
57 )
58)]
59pub struct MixnodeCoreStatusResponse {
60 pub mix_id: NodeId,
61 pub count: i64,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
65#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
66#[cfg_attr(
67 feature = "generate-ts",
68 ts(
69 export,
70 export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts"
71 )
72)]
73pub struct GatewayCoreStatusResponse {
74 pub identity: String,
75 pub count: i64,
76}
77
78pub use v3::*;
79
80pub mod v3 {
83 use super::*;
84 use crate::signable::SignedMessage;
85 use std::time::Duration;
86 use time::OffsetDateTime;
87
88 pub type StressTestBatchSubmission = SignedMessage<StressTestBatchSubmissionContent>;
95
96 #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
100 pub struct StressTestBatchSubmissionResponse {}
101
102 #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
104 pub struct StressTestResult {
105 pub testrun_id: i64,
109
110 pub node_id: NodeId,
112
113 pub is_mixnode: bool,
118
119 #[schema(value_type = String)]
120 #[serde(with = "time::serde::rfc3339")]
121 pub test_timestamp: OffsetDateTime,
122
123 pub test_performance: f64,
125
126 pub was_reachable: bool,
131 }
132
133 #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
135 pub struct StressTestBatchSubmissionContent {
136 #[schema(value_type = String)]
139 #[serde(with = "ed25519::bs58_ed25519_pubkey")]
140 pub signer: ed25519::PublicKey,
141
142 #[schema(value_type = String)]
146 #[serde(with = "time::serde::rfc3339")]
147 pub timestamp: OffsetDateTime,
148
149 pub results: Vec<StressTestResult>,
150 }
151
152 impl StressTestBatchSubmissionContent {
153 pub fn new(signer: ed25519::PublicKey, results: Vec<StressTestResult>) -> Self {
155 StressTestBatchSubmissionContent {
156 signer,
157 timestamp: OffsetDateTime::now_utc(),
158 results,
159 }
160 }
161
162 pub fn is_stale(&self, max_age: Duration) -> bool {
167 self.timestamp + max_age < OffsetDateTime::now_utc()
168 }
169 }
170
171 #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
174 pub struct KnownNetworkMonitorResponse {
175 #[serde(with = "bs58_ed25519_pubkey")]
177 #[schema(value_type = String)]
178 pub identity_key: ed25519::PublicKey,
179
180 pub authorised: bool,
183 }
184
185 #[cfg(test)]
186 mod tests {
187 use super::*;
188 use crate::signable::SignableMessageBody;
189 use nym_test_utils::helpers::deterministic_rng;
190 use time::macros::datetime;
191
192 fn dummy_results() -> Vec<StressTestResult> {
193 vec![
197 StressTestResult {
198 testrun_id: 1,
199 node_id: 42,
200 is_mixnode: true,
201 test_timestamp: datetime!(2026-06-01 12:34:56.123456789 UTC),
202 test_performance: 0.6666666666666666,
203 was_reachable: true,
204 },
205 StressTestResult {
206 testrun_id: 2,
207 node_id: 7,
208 is_mixnode: true,
209 test_timestamp: datetime!(2026-06-01 12:34:56 UTC),
210 test_performance: 0.0,
211 was_reachable: false,
212 },
213 StressTestResult {
214 testrun_id: 3,
215 node_id: u32::MAX,
216 is_mixnode: true,
217 test_timestamp: datetime!(2026-06-01 12:34:56.999999999 UTC),
218 test_performance: 1.0,
219 was_reachable: true,
220 },
221 ]
222 }
223
224 #[test]
229 fn signed_batch_submission_roundtrips_through_json() {
230 let mut rng = deterministic_rng();
231 let keys = ed25519::KeyPair::new(&mut rng);
232
233 let timestamps = [
234 datetime!(2026-06-01 12:34:56 UTC),
235 datetime!(2026-06-01 12:34:56.000000001 UTC),
236 datetime!(2026-06-01 12:34:56.999999999 UTC),
237 datetime!(2026-06-01 12:34:56.123456789 UTC),
238 OffsetDateTime::now_utc(),
239 OffsetDateTime::now_utc() + time::Duration::NANOSECOND,
240 ];
241
242 for timestamp in timestamps {
243 let body = StressTestBatchSubmissionContent {
244 signer: *keys.public_key(),
245 timestamp,
246 results: dummy_results(),
247 };
248 let signed = body.clone().sign(keys.private_key());
249
250 let bytes = serde_json::to_vec(&signed).unwrap();
251 let deserialised: StressTestBatchSubmission =
252 serde_json::from_slice(&bytes).unwrap();
253
254 assert!(
256 deserialised.verify_signature(&deserialised.body.signer),
257 "signature failed to verify after JSON round-trip for timestamp {timestamp}",
258 );
259 assert_eq!(deserialised.body.timestamp, timestamp);
260 }
261 }
262
263 #[test]
267 fn computed_test_performance_values_roundtrip() {
268 for sent in 1u64..=200 {
269 for received in 0u64..=(sent * 2) {
270 let perf = received as f64 / sent as f64;
271 let s = serde_json::to_string(&perf).unwrap();
272 let perf2: f64 = serde_json::from_str(&s).unwrap();
273 let s2 = serde_json::to_string(&perf2).unwrap();
274 assert_eq!(
275 s, s2,
276 "f64 round-trip mismatch for {received}/{sent} = {perf}: {s} -> {s2}",
277 );
278 }
279 }
280 }
281
282 #[test]
287 fn non_finite_test_performance_breaks_loudly_not_silently() {
288 let nan_result = StressTestResult {
289 testrun_id: 1,
290 node_id: 1,
291 is_mixnode: true,
292 test_timestamp: datetime!(2026-06-01 12:34:56 UTC),
293 test_performance: f64::NAN,
294 was_reachable: true,
295 };
296 let json = serde_json::to_string(&nan_result).unwrap();
297 assert!(
299 json.contains(r#""test_performance":null"#),
300 "expected NaN to serialise as null: {json}",
301 );
302 let deserialised: Result<StressTestResult, _> = serde_json::from_str(&json);
305 assert!(
306 deserialised.is_err(),
307 "deserialising null into f64 unexpectedly succeeded - signature verification \
308 would silently fail for any submission containing a non-finite test_performance",
309 );
310 }
311
312 #[test]
319 fn batch_body_serialisation_is_a_byte_exact_fixed_point() {
320 let mut rng = deterministic_rng();
321 let keys = ed25519::KeyPair::new(&mut rng);
322
323 let body = StressTestBatchSubmissionContent {
324 signer: *keys.public_key(),
325 timestamp: datetime!(2026-06-01 12:34:56.123456789 UTC),
326 results: dummy_results(),
327 };
328
329 let signed_bytes = body.plaintext();
330 let body_str = std::str::from_utf8(&signed_bytes).unwrap();
331
332 let pos1 = body_str.find(r#""testrun_id":1"#).unwrap();
334 let pos2 = body_str.find(r#""testrun_id":2"#).unwrap();
335 let pos3 = body_str.find(r#""testrun_id":3"#).unwrap();
336 assert!(pos1 < pos2 && pos2 < pos3, "JSON: {body_str}");
337
338 let deserialised: StressTestBatchSubmissionContent =
340 serde_json::from_slice(&signed_bytes).unwrap();
341 let resigned_bytes = deserialised.plaintext();
342 assert_eq!(
343 signed_bytes, resigned_bytes,
344 "deserialise-then-re-serialise was not a fixed point"
345 );
346 }
347 }
348}