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 mod v3 {
81 use super::*;
82 use crate::signable::SignedMessage;
83 use std::time::Duration;
84 use time::OffsetDateTime;
85
86 pub type StressTestBatchSubmission = SignedMessage<StressTestBatchSubmissionContent>;
93
94 #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
98 pub struct StressTestBatchSubmissionResponse {}
99
100 #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
102 pub struct StressTestResult {
103 pub testrun_id: i64,
107
108 pub node_id: NodeId,
110
111 pub is_mixnode: bool,
116
117 #[schema(value_type = String)]
118 #[serde(with = "time::serde::rfc3339")]
119 pub test_timestamp: OffsetDateTime,
120
121 pub test_performance: f64,
123
124 pub was_reachable: bool,
129 }
130
131 #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
133 pub struct StressTestBatchSubmissionContent {
134 #[schema(value_type = String)]
137 #[serde(with = "ed25519::bs58_ed25519_pubkey")]
138 pub signer: ed25519::PublicKey,
139
140 #[schema(value_type = String)]
144 #[serde(with = "time::serde::rfc3339")]
145 pub timestamp: OffsetDateTime,
146
147 pub results: Vec<StressTestResult>,
148 }
149
150 impl StressTestBatchSubmissionContent {
151 pub fn new(signer: ed25519::PublicKey, results: Vec<StressTestResult>) -> Self {
153 StressTestBatchSubmissionContent {
154 signer,
155 timestamp: OffsetDateTime::now_utc(),
156 results,
157 }
158 }
159
160 pub fn is_stale(&self, max_age: Duration) -> bool {
165 self.timestamp + max_age < OffsetDateTime::now_utc()
166 }
167 }
168
169 #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
172 pub struct KnownNetworkMonitorResponse {
173 #[serde(with = "bs58_ed25519_pubkey")]
175 #[schema(value_type = String)]
176 pub identity_key: ed25519::PublicKey,
177
178 pub authorised: bool,
181 }
182
183 #[cfg(test)]
184 mod tests {
185 use super::*;
186 use crate::signable::SignableMessageBody;
187 use nym_test_utils::helpers::deterministic_rng;
188 use time::macros::datetime;
189
190 fn dummy_results() -> Vec<StressTestResult> {
191 vec![
195 StressTestResult {
196 testrun_id: 1,
197 node_id: 42,
198 is_mixnode: true,
199 test_timestamp: datetime!(2026-06-01 12:34:56.123456789 UTC),
200 test_performance: 0.6666666666666666,
201 was_reachable: true,
202 },
203 StressTestResult {
204 testrun_id: 2,
205 node_id: 7,
206 is_mixnode: true,
207 test_timestamp: datetime!(2026-06-01 12:34:56 UTC),
208 test_performance: 0.0,
209 was_reachable: false,
210 },
211 StressTestResult {
212 testrun_id: 3,
213 node_id: u32::MAX,
214 is_mixnode: true,
215 test_timestamp: datetime!(2026-06-01 12:34:56.999999999 UTC),
216 test_performance: 1.0,
217 was_reachable: true,
218 },
219 ]
220 }
221
222 #[test]
227 fn signed_batch_submission_roundtrips_through_json() {
228 let mut rng = deterministic_rng();
229 let keys = ed25519::KeyPair::new(&mut rng);
230
231 let timestamps = [
232 datetime!(2026-06-01 12:34:56 UTC),
233 datetime!(2026-06-01 12:34:56.000000001 UTC),
234 datetime!(2026-06-01 12:34:56.999999999 UTC),
235 datetime!(2026-06-01 12:34:56.123456789 UTC),
236 OffsetDateTime::now_utc(),
237 OffsetDateTime::now_utc() + time::Duration::NANOSECOND,
238 ];
239
240 for timestamp in timestamps {
241 let body = StressTestBatchSubmissionContent {
242 signer: *keys.public_key(),
243 timestamp,
244 results: dummy_results(),
245 };
246 let signed = body.clone().sign(keys.private_key());
247
248 let bytes = serde_json::to_vec(&signed).unwrap();
249 let deserialised: StressTestBatchSubmission =
250 serde_json::from_slice(&bytes).unwrap();
251
252 assert!(
254 deserialised.verify_signature(&deserialised.body.signer),
255 "signature failed to verify after JSON round-trip for timestamp {timestamp}",
256 );
257 assert_eq!(deserialised.body.timestamp, timestamp);
258 }
259 }
260
261 #[test]
265 fn computed_test_performance_values_roundtrip() {
266 for sent in 1u64..=200 {
267 for received in 0u64..=(sent * 2) {
268 let perf = received as f64 / sent as f64;
269 let s = serde_json::to_string(&perf).unwrap();
270 let perf2: f64 = serde_json::from_str(&s).unwrap();
271 let s2 = serde_json::to_string(&perf2).unwrap();
272 assert_eq!(
273 s, s2,
274 "f64 round-trip mismatch for {received}/{sent} = {perf}: {s} -> {s2}",
275 );
276 }
277 }
278 }
279
280 #[test]
285 fn non_finite_test_performance_breaks_loudly_not_silently() {
286 let nan_result = StressTestResult {
287 testrun_id: 1,
288 node_id: 1,
289 is_mixnode: true,
290 test_timestamp: datetime!(2026-06-01 12:34:56 UTC),
291 test_performance: f64::NAN,
292 was_reachable: true,
293 };
294 let json = serde_json::to_string(&nan_result).unwrap();
295 assert!(
297 json.contains(r#""test_performance":null"#),
298 "expected NaN to serialise as null: {json}",
299 );
300 let deserialised: Result<StressTestResult, _> = serde_json::from_str(&json);
303 assert!(
304 deserialised.is_err(),
305 "deserialising null into f64 unexpectedly succeeded - signature verification \
306 would silently fail for any submission containing a non-finite test_performance",
307 );
308 }
309
310 #[test]
317 fn batch_body_serialisation_is_a_byte_exact_fixed_point() {
318 let mut rng = deterministic_rng();
319 let keys = ed25519::KeyPair::new(&mut rng);
320
321 let body = StressTestBatchSubmissionContent {
322 signer: *keys.public_key(),
323 timestamp: datetime!(2026-06-01 12:34:56.123456789 UTC),
324 results: dummy_results(),
325 };
326
327 let signed_bytes = body.plaintext();
328 let body_str = std::str::from_utf8(&signed_bytes).unwrap();
329
330 let pos1 = body_str.find(r#""testrun_id":1"#).unwrap();
332 let pos2 = body_str.find(r#""testrun_id":2"#).unwrap();
333 let pos3 = body_str.find(r#""testrun_id":3"#).unwrap();
334 assert!(pos1 < pos2 && pos2 < pos3, "JSON: {body_str}");
335
336 let deserialised: StressTestBatchSubmissionContent =
338 serde_json::from_slice(&signed_bytes).unwrap();
339 let resigned_bytes = deserialised.plaintext();
340 assert_eq!(
341 signed_bytes, resigned_bytes,
342 "deserialise-then-re-serialise was not a fixed point"
343 );
344 }
345 }
346}