1use serde::{Deserialize, Serialize};
20
21use crate::ipc::{KernelMessage, MessageTarget};
22
23const MAX_IPC_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
25
26const MAX_HOPS: u8 = 8;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39#[repr(u8)]
40pub enum MeshIpcEncoding {
41 #[default]
43 Json = 0x01,
44 #[cfg(feature = "mesh-rvf")]
48 Rvf = 0x02,
49}
50
51pub const DEFAULT_MESH_IPC_ENCODING: MeshIpcEncoding = MeshIpcEncoding::Json;
53
54impl MeshIpcEncoding {
55 pub const fn as_str(self) -> &'static str {
57 match self {
58 Self::Json => "json",
59 #[cfg(feature = "mesh-rvf")]
60 Self::Rvf => "rvf",
61 }
62 }
63
64 pub fn from_byte(b: u8) -> Option<Self> {
66 match b {
67 0x01 => Some(Self::Json),
68 #[cfg(feature = "mesh-rvf")]
69 0x02 => Some(Self::Rvf),
70 _ => None,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct Scope {
82 pub user_id: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub project_id: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct MeshIpcEnvelope {
92 pub source_node: String,
94 pub dest_node: String,
96 pub message: KernelMessage,
98 pub hop_count: u8,
100 pub envelope_id: String,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub dest_scope: Option<Scope>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub src_scope: Option<Scope>,
110}
111
112impl MeshIpcEnvelope {
113 pub fn new(source_node: String, dest_node: String, message: KernelMessage) -> Self {
115 Self {
116 source_node,
117 dest_node,
118 message,
119 hop_count: 0,
120 envelope_id: uuid::Uuid::new_v4().to_string(),
121 dest_scope: None,
122 src_scope: None,
123 }
124 }
125
126 pub fn to_bytes(&self) -> Result<Vec<u8>, MeshIpcError> {
128 self.to_bytes_with_encoding(DEFAULT_MESH_IPC_ENCODING)
129 }
130
131 pub fn to_bytes_with_encoding(
137 &self,
138 encoding: MeshIpcEncoding,
139 ) -> Result<Vec<u8>, MeshIpcError> {
140 match encoding {
141 MeshIpcEncoding::Json => {
142 let bytes = serde_json::to_vec(self)
143 .map_err(|e| MeshIpcError::Serialization(e.to_string()))?;
144 if bytes.len() > MAX_IPC_MESSAGE_SIZE {
145 return Err(MeshIpcError::MessageTooLarge {
146 size: bytes.len(),
147 max: MAX_IPC_MESSAGE_SIZE,
148 });
149 }
150 Ok(bytes)
151 }
152 #[cfg(feature = "mesh-rvf")]
153 MeshIpcEncoding::Rvf => Err(MeshIpcError::UnsupportedEncoding {
154 encoding: encoding.as_str(),
155 }),
156 }
157 }
158
159 pub fn from_bytes(data: &[u8]) -> Result<Self, MeshIpcError> {
161 Self::from_bytes_with_encoding(data, DEFAULT_MESH_IPC_ENCODING)
162 }
163
164 pub fn from_bytes_with_encoding(
166 data: &[u8],
167 encoding: MeshIpcEncoding,
168 ) -> Result<Self, MeshIpcError> {
169 if data.len() > MAX_IPC_MESSAGE_SIZE {
170 return Err(MeshIpcError::MessageTooLarge {
171 size: data.len(),
172 max: MAX_IPC_MESSAGE_SIZE,
173 });
174 }
175 match encoding {
176 MeshIpcEncoding::Json => {
177 let envelope: Self = serde_json::from_slice(data)
178 .map_err(|e| MeshIpcError::Deserialization(e.to_string()))?;
179 Self::validate_boundary(&envelope)?;
180 Ok(envelope)
181 }
182 #[cfg(feature = "mesh-rvf")]
183 MeshIpcEncoding::Rvf => Err(MeshIpcError::UnsupportedEncoding {
184 encoding: encoding.as_str(),
185 }),
186 }
187 }
188
189 fn validate_boundary(envelope: &Self) -> Result<(), MeshIpcError> {
191 if envelope.envelope_id.is_empty() {
192 return Err(MeshIpcError::Deserialization(
193 "envelope_id must not be empty".into(),
194 ));
195 }
196 if envelope.source_node.is_empty() {
197 return Err(MeshIpcError::Deserialization(
198 "source_node must not be empty".into(),
199 ));
200 }
201 Ok(())
202 }
203
204 pub fn increment_hop(&mut self) -> Result<(), MeshIpcError> {
206 self.hop_count += 1;
207 if self.hop_count > MAX_HOPS {
208 return Err(MeshIpcError::MaxHopsExceeded {
209 hops: self.hop_count,
210 });
211 }
212 Ok(())
213 }
214
215 pub fn inner_target(&self) -> &MessageTarget {
217 match &self.message.target {
218 MessageTarget::RemoteNode { target, .. } => target.as_ref(),
219 other => other,
220 }
221 }
222}
223
224#[non_exhaustive]
226#[derive(Debug, thiserror::Error)]
227pub enum MeshIpcError {
228 #[error("serialization error: {0}")]
229 Serialization(String),
230 #[error("deserialization error: {0}")]
231 Deserialization(String),
232 #[error("message too large: {size} bytes (max {max})")]
233 MessageTooLarge { size: usize, max: usize },
234 #[error("max hops exceeded: {hops}")]
235 MaxHopsExceeded { hops: u8 },
236 #[error("duplicate message: {envelope_id}")]
237 DuplicateMessage { envelope_id: String },
238 #[error("unsupported mesh IPC encoding: {encoding} (ADR-031 / WEFT-683 — deferred)")]
243 UnsupportedEncoding { encoding: &'static str },
244}
245
246#[derive(Debug)]
248pub struct MeshRequest {
249 pub request: MeshIpcEnvelope,
251 pub correlation_id: String,
253 pub sent_at: std::time::Instant,
255 pub timeout: std::time::Duration,
257}
258
259impl MeshRequest {
260 pub fn new(mut envelope: MeshIpcEnvelope, timeout: std::time::Duration) -> Self {
262 let correlation_id = uuid::Uuid::new_v4().to_string();
263 envelope.message.correlation_id = Some(correlation_id.clone());
264 Self {
265 request: envelope,
266 correlation_id,
267 sent_at: std::time::Instant::now(),
268 timeout,
269 }
270 }
271
272 pub fn is_timed_out(&self) -> bool {
274 self.sent_at.elapsed() >= self.timeout
278 }
279
280 pub fn matches_response(&self, response: &MeshIpcEnvelope) -> bool {
282 response.message.correlation_id.as_deref() == Some(&self.correlation_id)
283 }
284}
285
286pub struct PendingRequests {
288 requests: std::collections::HashMap<String, MeshRequest>,
289 #[cfg(feature = "exochain")]
291 chain_manager: Option<std::sync::Arc<crate::chain::ChainManager>>,
292}
293
294impl PendingRequests {
295 pub fn new() -> Self {
297 Self {
298 requests: std::collections::HashMap::new(),
299 #[cfg(feature = "exochain")]
300 chain_manager: None,
301 }
302 }
303
304 #[cfg(feature = "exochain")]
306 pub fn set_chain_manager(&mut self, cm: std::sync::Arc<crate::chain::ChainManager>) {
307 self.chain_manager = Some(cm);
308 }
309
310 pub fn register(&mut self, request: MeshRequest) {
312 #[cfg(feature = "exochain")]
313 if let Some(ref cm) = self.chain_manager {
314 cm.append(
315 "mesh_ipc",
316 crate::chain::EVENT_KIND_MESH_IPC_SEND,
317 Some(serde_json::json!({
318 "correlation_id": &request.correlation_id,
319 "source_node": &request.request.source_node,
320 "dest_node": &request.request.dest_node,
321 "envelope_id": &request.request.envelope_id,
322 })),
323 );
324 }
325 self.requests
326 .insert(request.correlation_id.clone(), request);
327 }
328
329 pub fn try_complete(&mut self, response: &MeshIpcEnvelope) -> Option<MeshRequest> {
331 if let Some(corr_id) = &response.message.correlation_id {
332 self.requests.remove(corr_id)
333 } else {
334 None
335 }
336 }
337
338 pub fn evict_timed_out(&mut self) -> Vec<String> {
340 let timed_out: Vec<String> = self
341 .requests
342 .iter()
343 .filter(|(_, r)| r.is_timed_out())
344 .map(|(id, _)| id.clone())
345 .collect();
346 for id in &timed_out {
347 self.requests.remove(id);
348 }
349 timed_out
350 }
351
352 pub fn len(&self) -> usize {
354 self.requests.len()
355 }
356
357 pub fn is_empty(&self) -> bool {
359 self.requests.is_empty()
360 }
361}
362
363impl Default for PendingRequests {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
373
374 fn make_test_message() -> KernelMessage {
375 KernelMessage::text(1, MessageTarget::Process(2), "hello mesh")
376 }
377
378 #[test]
379 fn envelope_new_sets_defaults() {
380 let msg = make_test_message();
381 let env = MeshIpcEnvelope::new("node-a".into(), "node-b".into(), msg);
382 assert_eq!(env.source_node, "node-a");
383 assert_eq!(env.dest_node, "node-b");
384 assert_eq!(env.hop_count, 0);
385 assert!(!env.envelope_id.is_empty());
386 }
387
388 #[test]
389 fn to_bytes_from_bytes_roundtrip() {
390 let msg = make_test_message();
391 let env = MeshIpcEnvelope::new("src".into(), "dst".into(), msg);
392 let bytes = env.to_bytes().unwrap();
393 let restored = MeshIpcEnvelope::from_bytes(&bytes).unwrap();
394 assert_eq!(restored.source_node, "src");
395 assert_eq!(restored.dest_node, "dst");
396 assert_eq!(restored.envelope_id, env.envelope_id);
397 assert_eq!(restored.message.id, env.message.id);
398 }
399
400 #[test]
401 fn from_bytes_rejects_oversized() {
402 let big = vec![0u8; MAX_IPC_MESSAGE_SIZE + 1];
403 let err = MeshIpcEnvelope::from_bytes(&big).unwrap_err();
404 assert!(matches!(err, MeshIpcError::MessageTooLarge { .. }));
405 }
406
407 #[test]
408 fn from_bytes_rejects_invalid_json() {
409 let err = MeshIpcEnvelope::from_bytes(b"not json").unwrap_err();
410 assert!(matches!(err, MeshIpcError::Deserialization(_)));
411 }
412
413 #[test]
414 fn increment_hop_within_limit() {
415 let msg = make_test_message();
416 let mut env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
417 for _ in 0..MAX_HOPS {
418 env.increment_hop().unwrap();
419 }
420 assert_eq!(env.hop_count, MAX_HOPS);
421 }
422
423 #[test]
424 fn increment_hop_exceeds_max() {
425 let msg = make_test_message();
426 let mut env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
427 env.hop_count = MAX_HOPS;
428 let err = env.increment_hop().unwrap_err();
429 assert!(matches!(err, MeshIpcError::MaxHopsExceeded { .. }));
430 }
431
432 #[test]
433 fn inner_target_unwraps_remote_node() {
434 let inner = MessageTarget::Process(7);
435 let remote_target = MessageTarget::RemoteNode {
436 node_id: "node-42".into(),
437 target: Box::new(inner),
438 };
439 let msg = KernelMessage::new(1, remote_target, MessagePayload::Text("hi".into()));
440 let env = MeshIpcEnvelope::new("a".into(), "node-42".into(), msg);
441 assert!(matches!(env.inner_target(), MessageTarget::Process(7)));
442 }
443
444 #[test]
445 fn inner_target_returns_non_remote_as_is() {
446 let msg = KernelMessage::text(1, MessageTarget::Broadcast, "hi");
447 let env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
448 assert!(matches!(env.inner_target(), MessageTarget::Broadcast));
449 }
450
451 #[test]
452 fn from_bytes_rejects_empty_source_node() {
453 let msg = make_test_message();
454 let mut env = MeshIpcEnvelope::new("".into(), "b".into(), msg);
455 env.source_node = "".into();
456 let bytes = serde_json::to_vec(&env).unwrap();
457 let err = MeshIpcEnvelope::from_bytes(&bytes).unwrap_err();
458 assert!(matches!(err, MeshIpcError::Deserialization(_)));
459 }
460
461 #[test]
462 fn from_bytes_rejects_empty_envelope_id() {
463 let msg = make_test_message();
464 let mut env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
465 env.envelope_id = "".into();
466 let bytes = serde_json::to_vec(&env).unwrap();
467 let err = MeshIpcEnvelope::from_bytes(&bytes).unwrap_err();
468 assert!(matches!(err, MeshIpcError::Deserialization(_)));
469 }
470
471 #[test]
472 fn k6_cross_node_ipc_via_did_addressing() {
473 use crate::ipc::GlobalPid;
477
478 let node_a_did = "a1b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8";
480 let node_b_did = "b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8a1";
481
482 let target_pid = GlobalPid::local(42, node_b_did);
484 assert_eq!(target_pid.node_id, node_b_did);
485 assert!(!target_pid.is_local(node_a_did));
486 assert!(target_pid.is_local(node_b_did));
487
488 let msg = KernelMessage::text(
490 1,
491 MessageTarget::RemoteNode {
492 node_id: node_b_did.to_string(),
493 target: Box::new(MessageTarget::Process(42)),
494 },
495 "hello via DID",
496 );
497
498 let envelope = MeshIpcEnvelope::new(node_a_did.to_string(), node_b_did.to_string(), msg);
500
501 let bytes = envelope.to_bytes().unwrap();
503 let restored = MeshIpcEnvelope::from_bytes(&bytes).unwrap();
504 assert_eq!(restored.dest_node, node_b_did);
505 assert_eq!(restored.source_node, node_a_did);
506
507 assert!(matches!(
509 restored.inner_target(),
510 MessageTarget::Process(42)
511 ));
512 }
513
514 #[test]
515 fn mesh_request_correlation() {
516 let msg = make_test_message();
517 let env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
518 let req = MeshRequest::new(env, std::time::Duration::from_secs(5));
519 assert!(!req.correlation_id.is_empty());
520 assert_eq!(
521 req.request.message.correlation_id.as_deref(),
522 Some(req.correlation_id.as_str())
523 );
524 }
525
526 #[test]
527 fn mesh_request_timeout() {
528 let msg = make_test_message();
529 let env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
530 let req = MeshRequest::new(env, std::time::Duration::from_secs(0));
532 assert!(req.is_timed_out());
533 }
534
535 #[test]
536 fn pending_requests_complete() {
537 let msg = make_test_message();
538 let env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
539 let req = MeshRequest::new(env, std::time::Duration::from_secs(30));
540 let corr_id = req.correlation_id.clone();
541
542 let mut pending = PendingRequests::new();
543 pending.register(req);
544 assert_eq!(pending.len(), 1);
545
546 let resp_msg = KernelMessage::with_correlation(
548 2,
549 MessageTarget::Process(1),
550 MessagePayload::Text("ok".into()),
551 corr_id.clone(),
552 );
553 let resp_env = MeshIpcEnvelope::new("b".into(), "a".into(), resp_msg);
554 let completed = pending.try_complete(&resp_env);
555 assert!(completed.is_some());
556 assert_eq!(completed.unwrap().correlation_id, corr_id);
557 assert!(pending.is_empty());
558 }
559
560 #[test]
561 fn pending_requests_evict_timed_out() {
562 let msg = make_test_message();
563 let env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
564 let req = MeshRequest::new(env, std::time::Duration::from_secs(0));
565 let corr_id = req.correlation_id.clone();
566
567 let mut pending = PendingRequests::new();
568 pending.register(req);
569 assert_eq!(pending.len(), 1);
570
571 let evicted = pending.evict_timed_out();
572 assert_eq!(evicted.len(), 1);
573 assert_eq!(evicted[0], corr_id);
574 assert!(pending.is_empty());
575 }
576
577 #[test]
578 fn envelope_serde_with_correlation_id() {
579 let msg = KernelMessage::with_correlation(
580 1,
581 MessageTarget::Process(2),
582 MessagePayload::Text("req".into()),
583 "corr-99".into(),
584 );
585 let env = MeshIpcEnvelope::new("n1".into(), "n2".into(), msg);
586 let bytes = env.to_bytes().unwrap();
587 let restored = MeshIpcEnvelope::from_bytes(&bytes).unwrap();
588 assert_eq!(restored.message.correlation_id, Some("corr-99".into()));
589 }
590
591 #[test]
592 fn default_encoding_is_json() {
593 assert_eq!(DEFAULT_MESH_IPC_ENCODING, MeshIpcEncoding::Json);
594 assert_eq!(MeshIpcEncoding::default(), MeshIpcEncoding::Json);
595 assert_eq!(MeshIpcEncoding::Json.as_str(), "json");
596 assert_eq!(
597 MeshIpcEncoding::from_byte(0x01),
598 Some(MeshIpcEncoding::Json)
599 );
600 #[cfg(not(feature = "mesh-rvf"))]
602 assert_eq!(MeshIpcEncoding::from_byte(0x02), None);
603 #[cfg(feature = "mesh-rvf")]
604 assert_eq!(MeshIpcEncoding::from_byte(0x02), Some(MeshIpcEncoding::Rvf));
605 }
606
607 #[test]
608 fn to_bytes_with_encoding_json_matches_to_bytes() {
609 let msg = make_test_message();
610 let env = MeshIpcEnvelope::new("src".into(), "dst".into(), msg);
611 let a = env.to_bytes().unwrap();
612 let b = env.to_bytes_with_encoding(MeshIpcEncoding::Json).unwrap();
613 assert_eq!(a, b);
614 let restored =
615 MeshIpcEnvelope::from_bytes_with_encoding(&a, MeshIpcEncoding::Json).unwrap();
616 assert_eq!(restored.envelope_id, env.envelope_id);
617 }
618
619 #[cfg(feature = "mesh-rvf")]
620 #[test]
621 fn rvf_encoding_returns_unsupported() {
622 let msg = make_test_message();
623 let env = MeshIpcEnvelope::new("src".into(), "dst".into(), msg);
624 let err = env
625 .to_bytes_with_encoding(MeshIpcEncoding::Rvf)
626 .unwrap_err();
627 assert!(matches!(
628 err,
629 MeshIpcError::UnsupportedEncoding { encoding: "rvf" }
630 ));
631 let err =
632 MeshIpcEnvelope::from_bytes_with_encoding(b"{}", MeshIpcEncoding::Rvf).unwrap_err();
633 assert!(matches!(
634 err,
635 MeshIpcError::UnsupportedEncoding { encoding: "rvf" }
636 ));
637 }
638
639 const FROZEN_PRE_SCOPE_ENVELOPE: &str = concat!(
644 r#"{"source_node":"a","dest_node":"b","message":{"id":"m1","from":1,"#,
645 r#""target":{"Topic":"t"},"payload":{"Text":"x"},"#,
646 r#""timestamp":"2026-01-02T03:04:05Z"},"hop_count":0,"envelope_id":"fixed-id"}"#
647 );
648
649 #[test]
650 fn envelope_without_scopes_serializes_byte_identical_to_old_format() {
651 let env = MeshIpcEnvelope::from_bytes(FROZEN_PRE_SCOPE_ENVELOPE.as_bytes()).unwrap();
652 assert!(env.dest_scope.is_none() && env.src_scope.is_none());
653 assert_eq!(serde_json::to_string(&env).unwrap(), FROZEN_PRE_SCOPE_ENVELOPE);
654 assert!(!FROZEN_PRE_SCOPE_ENVELOPE.contains("scope"));
655 }
656
657 #[test]
658 fn old_json_without_scopes_deserializes() {
659 let msg = KernelMessage::text(1, MessageTarget::Topic("t".into()), "x");
660 let env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
661 let bytes = env.to_bytes().unwrap();
662 let back = MeshIpcEnvelope::from_bytes(&bytes).unwrap();
663 assert!(back.dest_scope.is_none() && back.src_scope.is_none());
664 }
665
666 #[test]
667 fn scoped_envelope_round_trips() {
668 let msg = KernelMessage::text(1, MessageTarget::Topic("t".into()), "x");
669 let mut env = MeshIpcEnvelope::new("a".into(), "b".into(), msg);
670 env.dest_scope = Some(Scope {
671 user_id: "u".repeat(32),
672 project_id: Some("P".repeat(26)),
673 });
674 env.src_scope = Some(Scope {
675 user_id: "v".repeat(32),
676 project_id: None,
677 });
678 let back = MeshIpcEnvelope::from_bytes(&env.to_bytes().unwrap()).unwrap();
679 assert_eq!(back.dest_scope, env.dest_scope);
680 assert_eq!(back.src_scope, env.src_scope);
681 }
682}