Skip to main content

ant_node/replication/
protocol.rs

1//! Wire protocol messages for the replication subsystem.
2//!
3//! All messages use postcard serialization for compact, fast encoding.
4//! Peer IDs are transmitted as raw `[u8; 32]` byte arrays.
5
6use serde::{Deserialize, Serialize};
7
8use crate::ant_protocol::XorName;
9
10pub use super::config::MAX_REPLICATION_MESSAGE_SIZE;
11
12/// Sentinel digest value indicating the challenged key is absent from storage.
13///
14/// Used in [`AuditResponse::Digests`] for keys the peer does not hold.
15pub const ABSENT_KEY_DIGEST: [u8; 32] = [0u8; 32];
16
17// ---------------------------------------------------------------------------
18// Top-level envelope
19// ---------------------------------------------------------------------------
20
21/// Top-level replication message envelope.
22///
23/// Every replication wire message carries a sender-assigned `request_id` so
24/// that the receiver can correlate responses without relying on transport-layer
25/// ordering.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ReplicationMessage {
28    /// Sender-assigned request ID for correlation.
29    pub request_id: u64,
30    /// The message body.
31    pub body: ReplicationMessageBody,
32}
33
34impl ReplicationMessage {
35    /// Encode the message to bytes using postcard.
36    ///
37    /// # Errors
38    ///
39    /// Returns [`ReplicationProtocolError::SerializationFailed`] if postcard
40    /// serialization fails.
41    pub fn encode(&self) -> Result<Vec<u8>, ReplicationProtocolError> {
42        let bytes = postcard::to_stdvec(self)
43            .map_err(|e| ReplicationProtocolError::SerializationFailed(e.to_string()))?;
44
45        if bytes.len() > MAX_REPLICATION_MESSAGE_SIZE {
46            return Err(ReplicationProtocolError::MessageTooLarge {
47                size: bytes.len(),
48                max_size: MAX_REPLICATION_MESSAGE_SIZE,
49            });
50        }
51
52        Ok(bytes)
53    }
54
55    /// Decode a message from bytes using postcard.
56    ///
57    /// Rejects payloads larger than [`MAX_REPLICATION_MESSAGE_SIZE`] before
58    /// attempting deserialization.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`ReplicationProtocolError::MessageTooLarge`] if the input
63    /// exceeds the size limit, or
64    /// [`ReplicationProtocolError::DeserializationFailed`] if postcard cannot
65    /// parse the data.
66    pub fn decode(data: &[u8]) -> Result<Self, ReplicationProtocolError> {
67        if data.len() > MAX_REPLICATION_MESSAGE_SIZE {
68            return Err(ReplicationProtocolError::MessageTooLarge {
69                size: data.len(),
70                max_size: MAX_REPLICATION_MESSAGE_SIZE,
71            });
72        }
73        postcard::from_bytes(data)
74            .map_err(|e| ReplicationProtocolError::DeserializationFailed(e.to_string()))
75    }
76}
77
78// ---------------------------------------------------------------------------
79// Message body enum
80// ---------------------------------------------------------------------------
81
82/// All replication protocol message types.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub enum ReplicationMessageBody {
85    // === Fresh Replication (Section 6.1) ===
86    /// Fresh replication offer with `PoP` (sent to close group members).
87    FreshReplicationOffer(FreshReplicationOffer),
88    /// Response to a fresh replication offer.
89    FreshReplicationResponse(FreshReplicationResponse),
90
91    /// Paid-list notification with `PoP` (sent to `PaidCloseGroup` members).
92    PaidNotify(PaidNotify),
93
94    // === Neighbor Sync (Section 6.2) ===
95    /// Neighbor sync hint exchange (bidirectional).
96    NeighborSyncRequest(NeighborSyncRequest),
97    /// Response to neighbor sync with own hints.
98    NeighborSyncResponse(NeighborSyncResponse),
99
100    // === Verification (Section 9) ===
101    /// Batched verification request (presence + paid-list queries).
102    VerificationRequest(VerificationRequest),
103    /// Response to verification request with per-key evidence.
104    VerificationResponse(VerificationResponse),
105
106    // === Fetch (record retrieval) ===
107    /// Request to fetch a record by key.
108    FetchRequest(FetchRequest),
109    /// Response with the record data.
110    FetchResponse(FetchResponse),
111
112    // === Audit (Section 15) ===
113    /// Storage audit challenge.
114    AuditChallenge(AuditChallenge),
115    /// Response to audit challenge.
116    AuditResponse(AuditResponse),
117}
118
119// ---------------------------------------------------------------------------
120// Fresh Replication Messages
121// ---------------------------------------------------------------------------
122
123/// Fresh replication offer (includes record + `PoP`).
124///
125/// Sent to close-group members when a node receives a new chunk via client PUT.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct FreshReplicationOffer {
128    /// The record key.
129    pub key: XorName,
130    /// The record data.
131    pub data: Vec<u8>,
132    /// Proof of Payment (required, validated by receiver).
133    pub proof_of_payment: Vec<u8>,
134}
135
136/// Response to a fresh replication offer.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub enum FreshReplicationResponse {
139    /// Record accepted and stored.
140    Accepted {
141        /// The accepted record key.
142        key: XorName,
143    },
144    /// Record rejected (with reason).
145    Rejected {
146        /// The rejected record key.
147        key: XorName,
148        /// Human-readable rejection reason.
149        reason: String,
150    },
151}
152
153/// Paid-list notification carrying key + `PoP` (Section 7.3).
154///
155/// Sent to `PaidCloseGroup` members so they record the key in their
156/// `PaidForList` without needing to hold the record data.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct PaidNotify {
159    /// The record key.
160    pub key: XorName,
161    /// Proof of Payment for receiver-side verification.
162    pub proof_of_payment: Vec<u8>,
163}
164
165// ---------------------------------------------------------------------------
166// Neighbor Sync Messages
167// ---------------------------------------------------------------------------
168
169/// Neighbor sync request carrying hint sets (Section 6.2).
170///
171/// Exchanged between close neighbors to detect and repair missing replicas.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct NeighborSyncRequest {
174    /// Keys sender believes receiver should hold (replica hints).
175    pub replica_hints: Vec<XorName>,
176    /// Keys sender believes receiver should track in `PaidForList` (paid hints).
177    pub paid_hints: Vec<XorName>,
178    /// Whether sender is currently bootstrapping.
179    pub bootstrapping: bool,
180}
181
182/// Neighbor sync response carrying own hint sets.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct NeighborSyncResponse {
185    /// Keys receiver believes sender should hold (replica hints).
186    pub replica_hints: Vec<XorName>,
187    /// Keys receiver believes sender should track in `PaidForList` (paid hints).
188    pub paid_hints: Vec<XorName>,
189    /// Whether receiver is currently bootstrapping.
190    pub bootstrapping: bool,
191    /// Keys that receiver rejected (optional feedback to sender).
192    pub rejected_keys: Vec<XorName>,
193}
194
195// ---------------------------------------------------------------------------
196// Verification Messages
197// ---------------------------------------------------------------------------
198
199/// Batched verification request for multiple keys (Section 9).
200///
201/// Sent to peers in `VerifyTargets` (union of `QuorumTargets` and
202/// `PaidTargets`). Each peer returns per-key presence and optionally
203/// paid-list status.
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct VerificationRequest {
206    /// Keys to verify (batched).
207    pub keys: Vec<XorName>,
208    /// Which keys need paid-list status in addition to presence.
209    /// Each value is an index into the `keys` vector.
210    pub paid_list_check_indices: Vec<u32>,
211}
212
213/// Per-key verification result from a peer.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct KeyVerificationResult {
216    /// The key being verified.
217    pub key: XorName,
218    /// Whether this peer holds the record.
219    pub present: bool,
220    /// Paid-list status (only set if peer was asked for paid-list check).
221    ///
222    /// - `Some(true)` -- key is in peer's `PaidForList`.
223    /// - `Some(false)` -- key is NOT in peer's `PaidForList`.
224    /// - `None` -- paid-list check was not requested for this key.
225    pub paid: Option<bool>,
226}
227
228/// Batched verification response with per-key results.
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct VerificationResponse {
231    /// Per-key results (one per requested key, in request order).
232    pub results: Vec<KeyVerificationResult>,
233}
234
235// ---------------------------------------------------------------------------
236// Fetch Messages
237// ---------------------------------------------------------------------------
238
239/// Request to fetch a specific record by key.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct FetchRequest {
242    /// The key of the record to fetch.
243    pub key: XorName,
244}
245
246/// Response to a fetch request.
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub enum FetchResponse {
249    /// Record found and returned.
250    Success {
251        /// The record key.
252        key: XorName,
253        /// The record data.
254        data: Vec<u8>,
255    },
256    /// Record not found on this peer.
257    NotFound {
258        /// The requested key.
259        key: XorName,
260    },
261    /// Error during fetch.
262    Error {
263        /// The requested key.
264        key: XorName,
265        /// Human-readable error description.
266        reason: String,
267    },
268}
269
270// ---------------------------------------------------------------------------
271// Audit Messages
272// ---------------------------------------------------------------------------
273
274/// Storage audit challenge (Section 15).
275///
276/// The challenger picks a random nonce and a set of keys the challenged peer
277/// should hold, then sends this challenge. The challenged peer must prove
278/// storage by returning per-key BLAKE3 digests.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct AuditChallenge {
281    /// Unique challenge identifier.
282    pub challenge_id: u64,
283    /// Random nonce for digest computation.
284    pub nonce: [u8; 32],
285    /// Challenged peer ID (included in digest computation).
286    pub challenged_peer_id: [u8; 32],
287    /// Ordered list of keys to prove storage of.
288    pub keys: Vec<XorName>,
289}
290
291/// Response to audit challenge.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub enum AuditResponse {
294    /// Per-key digests proving storage.
295    ///
296    /// `digests[i]` corresponds to `challenge.keys[i]`.
297    /// An [`ABSENT_KEY_DIGEST`] sentinel signals key absence.
298    Digests {
299        /// The challenge this response answers.
300        challenge_id: u64,
301        /// One 32-byte digest per challenged key, in challenge order.
302        digests: Vec<[u8; 32]>,
303    },
304    /// Peer is still bootstrapping (not ready for audit).
305    Bootstrapping {
306        /// The challenge this response answers.
307        challenge_id: u64,
308    },
309    /// Challenge rejected (wrong target peer or too many keys).
310    ///
311    /// Distinct from empty `Digests` so the challenger can distinguish a
312    /// legitimate rejection from misbehavior.
313    Rejected {
314        /// The challenge this response answers.
315        challenge_id: u64,
316        /// Human-readable rejection reason.
317        reason: String,
318    },
319}
320
321// ---------------------------------------------------------------------------
322// Audit digest helper
323// ---------------------------------------------------------------------------
324
325/// Compute `AuditKeyDigest(K_i) = BLAKE3(nonce || challenged_peer_id || K_i || record_bytes_i)`.
326///
327/// Returns the 32-byte BLAKE3 digest binding the nonce, peer identity, key,
328/// and record content together so a peer cannot forge proofs without holding
329/// the actual data.
330#[must_use]
331pub fn compute_audit_digest(
332    nonce: &[u8; 32],
333    challenged_peer_id: &[u8; 32],
334    key: &XorName,
335    record_bytes: &[u8],
336) -> [u8; 32] {
337    let mut hasher = blake3::Hasher::new();
338    hasher.update(nonce);
339    hasher.update(challenged_peer_id);
340    hasher.update(key);
341    hasher.update(record_bytes);
342    *hasher.finalize().as_bytes()
343}
344
345// ---------------------------------------------------------------------------
346// Error type
347// ---------------------------------------------------------------------------
348
349/// Errors from replication protocol encode/decode operations.
350#[derive(Debug, Clone)]
351pub enum ReplicationProtocolError {
352    /// Postcard serialization failed.
353    SerializationFailed(String),
354    /// Postcard deserialization failed.
355    DeserializationFailed(String),
356    /// Wire message exceeds the maximum allowed size.
357    MessageTooLarge {
358        /// Actual size of the message in bytes.
359        size: usize,
360        /// Maximum allowed size.
361        max_size: usize,
362    },
363}
364
365impl std::fmt::Display for ReplicationProtocolError {
366    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367        match self {
368            Self::SerializationFailed(msg) => {
369                write!(f, "replication serialization failed: {msg}")
370            }
371            Self::DeserializationFailed(msg) => {
372                write!(f, "replication deserialization failed: {msg}")
373            }
374            Self::MessageTooLarge { size, max_size } => {
375                write!(
376                    f,
377                    "replication message size {size} exceeds maximum {max_size}"
378                )
379            }
380        }
381    }
382}
383
384impl std::error::Error for ReplicationProtocolError {}
385
386// ---------------------------------------------------------------------------
387// Tests
388// ---------------------------------------------------------------------------
389
390#[cfg(test)]
391#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
392mod tests {
393    use super::*;
394
395    // === Fresh Replication roundtrip ===
396
397    #[test]
398    fn fresh_replication_offer_roundtrip() {
399        let msg = ReplicationMessage {
400            request_id: 1,
401            body: ReplicationMessageBody::FreshReplicationOffer(FreshReplicationOffer {
402                key: [0xAA; 32],
403                data: vec![1, 2, 3, 4, 5],
404                proof_of_payment: vec![10, 20, 30],
405            }),
406        };
407        let encoded = msg.encode().expect("encode should succeed");
408        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
409
410        assert_eq!(decoded.request_id, 1);
411        if let ReplicationMessageBody::FreshReplicationOffer(offer) = decoded.body {
412            assert_eq!(offer.key, [0xAA; 32]);
413            assert_eq!(offer.data, vec![1, 2, 3, 4, 5]);
414            assert_eq!(offer.proof_of_payment, vec![10, 20, 30]);
415        } else {
416            panic!("expected FreshReplicationOffer");
417        }
418    }
419
420    #[test]
421    fn fresh_replication_response_accepted_roundtrip() {
422        let msg = ReplicationMessage {
423            request_id: 2,
424            body: ReplicationMessageBody::FreshReplicationResponse(
425                FreshReplicationResponse::Accepted { key: [0xBB; 32] },
426            ),
427        };
428        let encoded = msg.encode().expect("encode should succeed");
429        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
430
431        assert_eq!(decoded.request_id, 2);
432        if let ReplicationMessageBody::FreshReplicationResponse(
433            FreshReplicationResponse::Accepted { key },
434        ) = decoded.body
435        {
436            assert_eq!(key, [0xBB; 32]);
437        } else {
438            panic!("expected FreshReplicationResponse::Accepted");
439        }
440    }
441
442    #[test]
443    fn fresh_replication_response_rejected_roundtrip() {
444        let msg = ReplicationMessage {
445            request_id: 3,
446            body: ReplicationMessageBody::FreshReplicationResponse(
447                FreshReplicationResponse::Rejected {
448                    key: [0xCC; 32],
449                    reason: "out of range".to_string(),
450                },
451            ),
452        };
453        let encoded = msg.encode().expect("encode should succeed");
454        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
455
456        assert_eq!(decoded.request_id, 3);
457        if let ReplicationMessageBody::FreshReplicationResponse(
458            FreshReplicationResponse::Rejected { key, reason },
459        ) = decoded.body
460        {
461            assert_eq!(key, [0xCC; 32]);
462            assert_eq!(reason, "out of range");
463        } else {
464            panic!("expected FreshReplicationResponse::Rejected");
465        }
466    }
467
468    // === PaidNotify roundtrip ===
469
470    #[test]
471    fn paid_notify_roundtrip() {
472        let msg = ReplicationMessage {
473            request_id: 4,
474            body: ReplicationMessageBody::PaidNotify(PaidNotify {
475                key: [0xDD; 32],
476                proof_of_payment: vec![99, 100],
477            }),
478        };
479        let encoded = msg.encode().expect("encode should succeed");
480        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
481
482        assert_eq!(decoded.request_id, 4);
483        if let ReplicationMessageBody::PaidNotify(notify) = decoded.body {
484            assert_eq!(notify.key, [0xDD; 32]);
485            assert_eq!(notify.proof_of_payment, vec![99, 100]);
486        } else {
487            panic!("expected PaidNotify");
488        }
489    }
490
491    // === Neighbor Sync roundtrips ===
492
493    #[test]
494    fn neighbor_sync_request_roundtrip() {
495        let msg = ReplicationMessage {
496            request_id: 5,
497            body: ReplicationMessageBody::NeighborSyncRequest(NeighborSyncRequest {
498                replica_hints: vec![[0x01; 32], [0x02; 32]],
499                paid_hints: vec![[0x03; 32]],
500                bootstrapping: true,
501            }),
502        };
503        let encoded = msg.encode().expect("encode should succeed");
504        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
505
506        assert_eq!(decoded.request_id, 5);
507        if let ReplicationMessageBody::NeighborSyncRequest(req) = decoded.body {
508            assert_eq!(req.replica_hints.len(), 2);
509            assert_eq!(req.paid_hints.len(), 1);
510            assert!(req.bootstrapping);
511        } else {
512            panic!("expected NeighborSyncRequest");
513        }
514    }
515
516    #[test]
517    fn neighbor_sync_response_roundtrip() {
518        let msg = ReplicationMessage {
519            request_id: 6,
520            body: ReplicationMessageBody::NeighborSyncResponse(NeighborSyncResponse {
521                replica_hints: vec![[0x04; 32]],
522                paid_hints: vec![],
523                bootstrapping: false,
524                rejected_keys: vec![[0x05; 32], [0x06; 32]],
525            }),
526        };
527        let encoded = msg.encode().expect("encode should succeed");
528        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
529
530        assert_eq!(decoded.request_id, 6);
531        if let ReplicationMessageBody::NeighborSyncResponse(resp) = decoded.body {
532            assert_eq!(resp.replica_hints.len(), 1);
533            assert!(resp.paid_hints.is_empty());
534            assert!(!resp.bootstrapping);
535            assert_eq!(resp.rejected_keys.len(), 2);
536        } else {
537            panic!("expected NeighborSyncResponse");
538        }
539    }
540
541    // === Verification roundtrips ===
542
543    #[test]
544    fn verification_request_roundtrip() {
545        let msg = ReplicationMessage {
546            request_id: 7,
547            body: ReplicationMessageBody::VerificationRequest(VerificationRequest {
548                keys: vec![[0x10; 32], [0x20; 32], [0x30; 32]],
549                paid_list_check_indices: vec![0, 2],
550            }),
551        };
552        let encoded = msg.encode().expect("encode should succeed");
553        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
554
555        assert_eq!(decoded.request_id, 7);
556        if let ReplicationMessageBody::VerificationRequest(req) = decoded.body {
557            assert_eq!(req.keys.len(), 3);
558            assert_eq!(req.paid_list_check_indices, vec![0, 2]);
559        } else {
560            panic!("expected VerificationRequest");
561        }
562    }
563
564    #[test]
565    fn verification_response_roundtrip() {
566        let results = vec![
567            KeyVerificationResult {
568                key: [0x10; 32],
569                present: true,
570                paid: Some(true),
571            },
572            KeyVerificationResult {
573                key: [0x20; 32],
574                present: false,
575                paid: None,
576            },
577            KeyVerificationResult {
578                key: [0x30; 32],
579                present: true,
580                paid: Some(false),
581            },
582        ];
583        let msg = ReplicationMessage {
584            request_id: 8,
585            body: ReplicationMessageBody::VerificationResponse(VerificationResponse { results }),
586        };
587        let encoded = msg.encode().expect("encode should succeed");
588        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
589
590        assert_eq!(decoded.request_id, 8);
591        if let ReplicationMessageBody::VerificationResponse(resp) = decoded.body {
592            assert_eq!(resp.results.len(), 3);
593            assert!(resp.results[0].present);
594            assert_eq!(resp.results[0].paid, Some(true));
595            assert!(!resp.results[1].present);
596            assert_eq!(resp.results[1].paid, None);
597            assert!(resp.results[2].present);
598            assert_eq!(resp.results[2].paid, Some(false));
599        } else {
600            panic!("expected VerificationResponse");
601        }
602    }
603
604    // === Fetch roundtrips ===
605
606    #[test]
607    fn fetch_request_roundtrip() {
608        let msg = ReplicationMessage {
609            request_id: 9,
610            body: ReplicationMessageBody::FetchRequest(FetchRequest { key: [0x40; 32] }),
611        };
612        let encoded = msg.encode().expect("encode should succeed");
613        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
614
615        assert_eq!(decoded.request_id, 9);
616        if let ReplicationMessageBody::FetchRequest(req) = decoded.body {
617            assert_eq!(req.key, [0x40; 32]);
618        } else {
619            panic!("expected FetchRequest");
620        }
621    }
622
623    #[test]
624    fn fetch_response_success_roundtrip() {
625        let msg = ReplicationMessage {
626            request_id: 10,
627            body: ReplicationMessageBody::FetchResponse(FetchResponse::Success {
628                key: [0x50; 32],
629                data: vec![7, 8, 9],
630            }),
631        };
632        let encoded = msg.encode().expect("encode should succeed");
633        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
634
635        assert_eq!(decoded.request_id, 10);
636        if let ReplicationMessageBody::FetchResponse(FetchResponse::Success { key, data }) =
637            decoded.body
638        {
639            assert_eq!(key, [0x50; 32]);
640            assert_eq!(data, vec![7, 8, 9]);
641        } else {
642            panic!("expected FetchResponse::Success");
643        }
644    }
645
646    #[test]
647    fn fetch_response_not_found_roundtrip() {
648        let msg = ReplicationMessage {
649            request_id: 11,
650            body: ReplicationMessageBody::FetchResponse(FetchResponse::NotFound {
651                key: [0x60; 32],
652            }),
653        };
654        let encoded = msg.encode().expect("encode should succeed");
655        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
656
657        assert_eq!(decoded.request_id, 11);
658        if let ReplicationMessageBody::FetchResponse(FetchResponse::NotFound { key }) = decoded.body
659        {
660            assert_eq!(key, [0x60; 32]);
661        } else {
662            panic!("expected FetchResponse::NotFound");
663        }
664    }
665
666    #[test]
667    fn fetch_response_error_roundtrip() {
668        let msg = ReplicationMessage {
669            request_id: 12,
670            body: ReplicationMessageBody::FetchResponse(FetchResponse::Error {
671                key: [0x70; 32],
672                reason: "disk full".to_string(),
673            }),
674        };
675        let encoded = msg.encode().expect("encode should succeed");
676        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
677
678        assert_eq!(decoded.request_id, 12);
679        if let ReplicationMessageBody::FetchResponse(FetchResponse::Error { key, reason }) =
680            decoded.body
681        {
682            assert_eq!(key, [0x70; 32]);
683            assert_eq!(reason, "disk full");
684        } else {
685            panic!("expected FetchResponse::Error");
686        }
687    }
688
689    // === Audit roundtrips ===
690
691    #[test]
692    fn audit_challenge_roundtrip() {
693        let msg = ReplicationMessage {
694            request_id: 13,
695            body: ReplicationMessageBody::AuditChallenge(AuditChallenge {
696                challenge_id: 999,
697                nonce: [0xAB; 32],
698                challenged_peer_id: [0xCD; 32],
699                keys: vec![[0x01; 32], [0x02; 32]],
700            }),
701        };
702        let encoded = msg.encode().expect("encode should succeed");
703        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
704
705        assert_eq!(decoded.request_id, 13);
706        if let ReplicationMessageBody::AuditChallenge(challenge) = decoded.body {
707            assert_eq!(challenge.challenge_id, 999);
708            assert_eq!(challenge.nonce, [0xAB; 32]);
709            assert_eq!(challenge.challenged_peer_id, [0xCD; 32]);
710            assert_eq!(challenge.keys.len(), 2);
711        } else {
712            panic!("expected AuditChallenge");
713        }
714    }
715
716    #[test]
717    fn audit_response_digests_roundtrip() {
718        let digests = vec![[0x11; 32], ABSENT_KEY_DIGEST];
719        let msg = ReplicationMessage {
720            request_id: 14,
721            body: ReplicationMessageBody::AuditResponse(AuditResponse::Digests {
722                challenge_id: 999,
723                digests: digests.clone(),
724            }),
725        };
726        let encoded = msg.encode().expect("encode should succeed");
727        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
728
729        assert_eq!(decoded.request_id, 14);
730        if let ReplicationMessageBody::AuditResponse(AuditResponse::Digests {
731            challenge_id,
732            digests: decoded_digests,
733        }) = decoded.body
734        {
735            assert_eq!(challenge_id, 999);
736            assert_eq!(decoded_digests, digests);
737        } else {
738            panic!("expected AuditResponse::Digests");
739        }
740    }
741
742    #[test]
743    fn audit_response_bootstrapping_roundtrip() {
744        let msg = ReplicationMessage {
745            request_id: 15,
746            body: ReplicationMessageBody::AuditResponse(AuditResponse::Bootstrapping {
747                challenge_id: 42,
748            }),
749        };
750        let encoded = msg.encode().expect("encode should succeed");
751        let decoded = ReplicationMessage::decode(&encoded).expect("decode should succeed");
752
753        assert_eq!(decoded.request_id, 15);
754        if let ReplicationMessageBody::AuditResponse(AuditResponse::Bootstrapping {
755            challenge_id,
756        }) = decoded.body
757        {
758            assert_eq!(challenge_id, 42);
759        } else {
760            panic!("expected AuditResponse::Bootstrapping");
761        }
762    }
763
764    // === Oversized message rejection ===
765
766    #[test]
767    fn decode_rejects_oversized_payload() {
768        let oversized = vec![0u8; MAX_REPLICATION_MESSAGE_SIZE + 1];
769        let result = ReplicationMessage::decode(&oversized);
770        assert!(result.is_err());
771        let err = result.unwrap_err();
772        assert!(
773            matches!(err, ReplicationProtocolError::MessageTooLarge { .. }),
774            "expected MessageTooLarge, got {err:?}"
775        );
776    }
777
778    #[test]
779    fn encode_rejects_oversized_message() {
780        // Build a message whose serialized form exceeds the limit.
781        let msg = ReplicationMessage {
782            request_id: 0,
783            body: ReplicationMessageBody::FreshReplicationOffer(FreshReplicationOffer {
784                key: [0; 32],
785                data: vec![0xFF; MAX_REPLICATION_MESSAGE_SIZE],
786                proof_of_payment: vec![],
787            }),
788        };
789        let result = msg.encode();
790        assert!(result.is_err());
791        let err = result.unwrap_err();
792        assert!(
793            matches!(err, ReplicationProtocolError::MessageTooLarge { .. }),
794            "expected MessageTooLarge, got {err:?}"
795        );
796    }
797
798    // === Invalid data rejection ===
799
800    #[test]
801    fn decode_rejects_invalid_data() {
802        let invalid = vec![0xFF, 0xFF, 0xFF];
803        let result = ReplicationMessage::decode(&invalid);
804        assert!(result.is_err());
805        let err = result.unwrap_err();
806        assert!(
807            matches!(err, ReplicationProtocolError::DeserializationFailed(_)),
808            "expected DeserializationFailed, got {err:?}"
809        );
810    }
811
812    // === Audit digest computation ===
813
814    #[test]
815    fn audit_digest_is_deterministic() {
816        let nonce = [0x01; 32];
817        let peer_id = [0x02; 32];
818        let key: XorName = [0x03; 32];
819        let record_bytes = b"hello world";
820
821        let digest_a = compute_audit_digest(&nonce, &peer_id, &key, record_bytes);
822        let digest_b = compute_audit_digest(&nonce, &peer_id, &key, record_bytes);
823
824        assert_eq!(digest_a, digest_b, "same inputs must produce same digest");
825    }
826
827    #[test]
828    fn audit_digest_differs_with_different_nonce() {
829        let peer_id = [0x02; 32];
830        let key: XorName = [0x03; 32];
831        let record_bytes = b"hello world";
832
833        let digest_a = compute_audit_digest(&[0x01; 32], &peer_id, &key, record_bytes);
834        let digest_b = compute_audit_digest(&[0xFF; 32], &peer_id, &key, record_bytes);
835
836        assert_ne!(
837            digest_a, digest_b,
838            "different nonces must produce different digests"
839        );
840    }
841
842    #[test]
843    fn audit_digest_differs_with_different_data() {
844        let nonce = [0x01; 32];
845        let peer_id = [0x02; 32];
846        let key: XorName = [0x03; 32];
847
848        let digest_a = compute_audit_digest(&nonce, &peer_id, &key, b"data-A");
849        let digest_b = compute_audit_digest(&nonce, &peer_id, &key, b"data-B");
850
851        assert_ne!(
852            digest_a, digest_b,
853            "different data must produce different digests"
854        );
855    }
856
857    #[test]
858    fn audit_digest_differs_with_different_peer() {
859        let nonce = [0x01; 32];
860        let key: XorName = [0x03; 32];
861        let record_bytes = b"hello";
862
863        let digest_a = compute_audit_digest(&nonce, &[0x02; 32], &key, record_bytes);
864        let digest_b = compute_audit_digest(&nonce, &[0xFF; 32], &key, record_bytes);
865
866        assert_ne!(
867            digest_a, digest_b,
868            "different peer IDs must produce different digests"
869        );
870    }
871
872    #[test]
873    fn audit_digest_differs_with_different_key() {
874        let nonce = [0x01; 32];
875        let peer_id = [0x02; 32];
876        let record_bytes = b"hello";
877
878        let digest_a = compute_audit_digest(&nonce, &peer_id, &[0x03; 32], record_bytes);
879        let digest_b = compute_audit_digest(&nonce, &peer_id, &[0xFF; 32], record_bytes);
880
881        assert_ne!(
882            digest_a, digest_b,
883            "different keys must produce different digests"
884        );
885    }
886
887    // === Absent key digest sentinel ===
888
889    #[test]
890    fn absent_key_digest_is_all_zeros() {
891        assert_eq!(ABSENT_KEY_DIGEST, [0u8; 32]);
892    }
893
894    #[test]
895    fn real_digest_differs_from_absent_sentinel() {
896        let nonce = [0x01; 32];
897        let peer_id = [0x02; 32];
898        let key: XorName = [0x03; 32];
899        let record_bytes = b"non-empty data";
900
901        let digest = compute_audit_digest(&nonce, &peer_id, &key, record_bytes);
902        assert_ne!(
903            digest, ABSENT_KEY_DIGEST,
904            "a real digest should not collide with the all-zeros sentinel"
905        );
906    }
907
908    // === Error Display ===
909
910    #[test]
911    fn error_display_serialization_failed() {
912        let err = ReplicationProtocolError::SerializationFailed("boom".to_string());
913        assert_eq!(err.to_string(), "replication serialization failed: boom");
914    }
915
916    #[test]
917    fn error_display_deserialization_failed() {
918        let err = ReplicationProtocolError::DeserializationFailed("bad data".to_string());
919        assert_eq!(
920            err.to_string(),
921            "replication deserialization failed: bad data"
922        );
923    }
924
925    #[test]
926    fn error_display_message_too_large() {
927        let err = ReplicationProtocolError::MessageTooLarge {
928            size: 20_000_000,
929            max_size: MAX_REPLICATION_MESSAGE_SIZE,
930        };
931        let display = err.to_string();
932        assert!(display.contains("20000000"));
933        assert!(display.contains(&MAX_REPLICATION_MESSAGE_SIZE.to_string()));
934    }
935}