Skip to main content

ant_protocol/
chunk.rs

1//! Chunk message types for the ANT protocol.
2//!
3//! Chunks are immutable, content-addressed data blocks where the address
4//! is the BLAKE3 hash of the content. Maximum size is 4MB.
5//!
6//! This module defines the wire protocol messages for chunk operations
7//! using postcard serialization for compact, fast encoding.
8
9use bytes::Bytes;
10use serde::{Deserialize, Serialize};
11
12/// Protocol identifier for chunk operations.
13pub const CHUNK_PROTOCOL_ID: &str = "autonomi.ant.chunk.v1";
14
15/// Current protocol version.
16pub const PROTOCOL_VERSION: u16 = 1;
17
18/// Maximum chunk size in bytes (4MB).
19pub const MAX_CHUNK_SIZE: usize = 4 * 1024 * 1024;
20
21/// Maximum wire message size in bytes (5MB).
22///
23/// Limits the input buffer accepted by [`ChunkMessage::decode`] to prevent
24/// unbounded allocation from malicious or corrupted payloads. Set slightly
25/// above [`MAX_CHUNK_SIZE`] to accommodate message envelope overhead.
26pub const MAX_WIRE_MESSAGE_SIZE: usize = 5 * 1024 * 1024;
27
28/// Data type identifier for chunks.
29pub const DATA_TYPE_CHUNK: u32 = 0;
30
31/// Number of nodes in a Kademlia close group.
32///
33/// Clients fetch quotes from the `CLOSE_GROUP_SIZE` closest nodes to a target
34/// address and select the median-priced quote for payment.
35pub const CLOSE_GROUP_SIZE: usize = 7;
36
37/// Minimum number of close group members that must agree for a decision to be valid.
38///
39/// This is a simple majority: `(CLOSE_GROUP_SIZE / 2) + 1`.
40pub const CLOSE_GROUP_MAJORITY: usize = (CLOSE_GROUP_SIZE / 2) + 1;
41
42/// Content-addressed identifier (32 bytes).
43pub type XorName = [u8; 32];
44
45/// Byte length of an [`XorName`].
46pub const XORNAME_LEN: usize = std::mem::size_of::<XorName>();
47
48/// Enum of all chunk protocol message types.
49///
50/// Uses a single-byte discriminant for efficient wire encoding.
51///
52/// Marked `#[non_exhaustive]` so new message variants can be added
53/// in a minor release without breaking downstream `match` expressions.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[non_exhaustive]
56pub enum ChunkMessageBody {
57    /// Request to store a chunk.
58    PutRequest(ChunkPutRequest),
59    /// Response to a PUT request.
60    PutResponse(ChunkPutResponse),
61    /// Request to retrieve a chunk.
62    GetRequest(ChunkGetRequest),
63    /// Response to a GET request.
64    GetResponse(ChunkGetResponse),
65    /// Request a storage quote.
66    QuoteRequest(ChunkQuoteRequest),
67    /// Response with a storage quote.
68    QuoteResponse(ChunkQuoteResponse),
69    /// Request a merkle candidate quote for batch payments.
70    MerkleCandidateQuoteRequest(MerkleCandidateQuoteRequest),
71    /// Response with a merkle candidate quote.
72    MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse),
73}
74
75/// Wire-format wrapper that pairs a sender-assigned `request_id` with
76/// a [`ChunkMessageBody`].
77///
78/// The sender picks a unique `request_id`; the handler echoes it back
79/// in the response so callers can correlate replies by ID rather than
80/// by source peer.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ChunkMessage {
83    /// Sender-assigned identifier, echoed back in the response.
84    pub request_id: u64,
85    /// The protocol message body.
86    pub body: ChunkMessageBody,
87}
88
89impl ChunkMessage {
90    /// Encode the message to bytes using postcard.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if serialization fails.
95    pub fn encode(&self) -> Result<Vec<u8>, ProtocolError> {
96        postcard::to_stdvec(self).map_err(|e| ProtocolError::SerializationFailed(e.to_string()))
97    }
98
99    /// Decode a message from bytes using postcard.
100    ///
101    /// Rejects payloads larger than [`MAX_WIRE_MESSAGE_SIZE`] before
102    /// attempting deserialization.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`ProtocolError::MessageTooLarge`] if the input exceeds the
107    /// size limit, or [`ProtocolError::DeserializationFailed`] if postcard
108    /// cannot parse the data.
109    pub fn decode(data: &[u8]) -> Result<Self, ProtocolError> {
110        if data.len() > MAX_WIRE_MESSAGE_SIZE {
111            return Err(ProtocolError::MessageTooLarge {
112                size: data.len(),
113                max_size: MAX_WIRE_MESSAGE_SIZE,
114            });
115        }
116        postcard::from_bytes(data).map_err(|e| ProtocolError::DeserializationFailed(e.to_string()))
117    }
118}
119
120// =============================================================================
121// PUT Request/Response
122// =============================================================================
123
124/// Request to store a chunk.
125///
126/// `content` is held as `bytes::Bytes` so that callers fanning the same
127/// chunk out to multiple recipients (e.g. close-group replication) share a
128/// single backing buffer via refcount instead of deep-copying the 4 MB
129/// payload per peer. Wire format is unchanged: `Bytes` serializes as a
130/// byte sequence, identical to `Vec<u8>` under postcard/serde.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ChunkPutRequest {
133    /// The content-addressed identifier (BLAKE3 of content).
134    pub address: XorName,
135    /// The chunk data.
136    pub content: Bytes,
137    /// Optional payment proof (serialized `ProofOfPayment`).
138    /// Required for new chunks unless already verified.
139    pub payment_proof: Option<Vec<u8>>,
140}
141
142impl ChunkPutRequest {
143    /// Create a new PUT request.
144    #[must_use]
145    pub fn new(address: XorName, content: Bytes) -> Self {
146        Self {
147            address,
148            content,
149            payment_proof: None,
150        }
151    }
152
153    /// Create a new PUT request with payment proof.
154    #[must_use]
155    pub fn with_payment(address: XorName, content: Bytes, payment_proof: Vec<u8>) -> Self {
156        Self {
157            address,
158            content,
159            payment_proof: Some(payment_proof),
160        }
161    }
162}
163
164/// Response to a PUT request.
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[non_exhaustive]
167pub enum ChunkPutResponse {
168    /// Chunk stored successfully.
169    Success {
170        /// The address where the chunk was stored.
171        address: XorName,
172    },
173    /// Chunk already exists (idempotent success).
174    AlreadyExists {
175        /// The existing chunk address.
176        address: XorName,
177    },
178    /// Payment is required to store this chunk.
179    PaymentRequired {
180        /// Error message.
181        message: String,
182    },
183    /// An error occurred.
184    Error(ProtocolError),
185}
186
187// =============================================================================
188// GET Request/Response
189// =============================================================================
190
191/// Request to retrieve a chunk.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ChunkGetRequest {
194    /// The content-addressed identifier to retrieve.
195    pub address: XorName,
196}
197
198impl ChunkGetRequest {
199    /// Create a new GET request.
200    #[must_use]
201    pub fn new(address: XorName) -> Self {
202        Self { address }
203    }
204}
205
206/// Response to a GET request.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[non_exhaustive]
209pub enum ChunkGetResponse {
210    /// Chunk found and returned.
211    Success {
212        /// The chunk address.
213        address: XorName,
214        /// The chunk data.
215        content: Vec<u8>,
216    },
217    /// Chunk not found.
218    NotFound {
219        /// The requested address.
220        address: XorName,
221    },
222    /// An error occurred.
223    Error(ProtocolError),
224}
225
226// =============================================================================
227// Quote Request/Response
228// =============================================================================
229
230/// Request a storage quote for a chunk.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct ChunkQuoteRequest {
233    /// The content address of the data to store.
234    pub address: XorName,
235    /// Size of the data in bytes.
236    pub data_size: u64,
237    /// Data type identifier (0 for chunks).
238    pub data_type: u32,
239}
240
241impl ChunkQuoteRequest {
242    /// Create a new quote request.
243    #[must_use]
244    pub fn new(address: XorName, data_size: u64) -> Self {
245        Self {
246            address,
247            data_size,
248            data_type: DATA_TYPE_CHUNK,
249        }
250    }
251}
252
253/// Response with a storage quote.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[non_exhaustive]
256pub enum ChunkQuoteResponse {
257    /// Quote generated successfully.
258    ///
259    /// When `already_stored` is `true` the node already holds this chunk and no
260    /// payment is required — the client should skip the pay-then-PUT cycle for
261    /// this address. The quote is still included for informational purposes.
262    Success {
263        /// Serialized `PaymentQuote`.
264        quote: Vec<u8>,
265        /// `true` when the chunk already exists on this node (skip payment).
266        already_stored: bool,
267        /// ADR-0004: the serialized signed storage commitment the quote's price
268        /// was derived from, so the client can verify the binding before paying
269        /// ("the commitment arrived with the quote") and forward it as a sidecar
270        /// in the PUT bundle. `None` for a baseline quote (no commitment to
271        /// pin), or from a node that has not yet rotated a commitment. Opaque
272        /// bytes: `ant-protocol` stays agnostic of `ant-node`'s commitment type;
273        /// the client resolves it only to match the quote's `commitment_pin`.
274        ///
275        /// NOTE: this enum is encoded with **postcard** (see [`ChunkMessage::encode`]),
276        /// which is non-self-describing — `#[serde(default)]` does NOT make an
277        /// old-format `Success` (without this field) decode against new code, and
278        /// vice versa. ADR-0004 is a HARD CUTOVER: the whole fleet and clients
279        /// upgrade together, so old/new `ChunkQuoteResponse` never interoperate.
280        /// The attribute only keeps `Default`-based construction ergonomic; it is
281        /// not a wire-compat guarantee. (Contrast `PaymentQuote`/`PaymentProof`,
282        /// which ARE rmp-encoded, where tail `serde(default)` is decode-compatible.)
283        #[serde(default)]
284        commitment: Option<Vec<u8>>,
285    },
286    /// Quote generation failed.
287    Error(ProtocolError),
288}
289
290// =============================================================================
291// Merkle Candidate Quote Request/Response
292// =============================================================================
293
294/// Request a merkle candidate quote for batch payments.
295///
296/// Part of the merkle batch payment system where clients collect
297/// signed candidate quotes from 16 closest peers per pool.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct MerkleCandidateQuoteRequest {
300    /// The candidate pool address (hash of midpoint || root || timestamp).
301    pub address: XorName,
302    /// Data type identifier (0 for chunks).
303    pub data_type: u32,
304    /// Size of the data in bytes.
305    pub data_size: u64,
306    /// Client-provided merkle payment timestamp (unix seconds).
307    pub merkle_payment_timestamp: u64,
308}
309
310/// Response with a merkle candidate quote.
311#[derive(Debug, Clone, Serialize, Deserialize)]
312#[non_exhaustive]
313pub enum MerkleCandidateQuoteResponse {
314    /// Candidate quote generated successfully.
315    /// Contains the serialized `MerklePaymentCandidateNode`.
316    Success {
317        /// Serialized `MerklePaymentCandidateNode`.
318        candidate_node: Vec<u8>,
319        /// ADR-0004: the serialized signed storage commitment the candidate's
320        /// price was derived from, so the client can fully resolve the binding
321        /// BEFORE paying (resolve-before-pay). `None` for a baseline candidate.
322        /// Unlike the single-node path, this commitment is NOT forwarded in the
323        /// merkle PUT bundle — sixteen per-candidate sidecars exceeded the
324        /// storer's payment-proof size budget, so current clients omit them and
325        /// storers resolve merkle pins from gossip or a `GetCommitmentByPin`
326        /// fetch (`MerklePaymentProof.commitment_sidecars` stays for legacy
327        /// bundles). Same semantics as
328        /// [`ChunkQuoteResponse::Success::commitment`]; postcard-encoded, so
329        /// this is a hard-cutover field, not an interop guarantee.
330        #[serde(default)]
331        commitment: Option<Vec<u8>>,
332    },
333    /// Quote generation failed.
334    Error(ProtocolError),
335}
336
337// =============================================================================
338// Payment Proof Type Tags
339// =============================================================================
340
341/// Version byte prefix for payment proof serialization.
342/// Allows the verifier to detect proof type before deserialization.
343pub const PROOF_TAG_SINGLE_NODE: u8 = 0x01;
344/// Version byte prefix for merkle payment proofs.
345pub const PROOF_TAG_MERKLE: u8 = 0x02;
346
347// =============================================================================
348// Protocol Errors
349// =============================================================================
350
351/// Errors that can occur during protocol operations.
352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
353#[non_exhaustive]
354pub enum ProtocolError {
355    /// Message serialization failed.
356    SerializationFailed(String),
357    /// Message deserialization failed.
358    DeserializationFailed(String),
359    /// Wire message exceeds the maximum allowed size.
360    MessageTooLarge {
361        /// Actual size of the message in bytes.
362        size: usize,
363        /// Maximum allowed size.
364        max_size: usize,
365    },
366    /// Chunk exceeds maximum size.
367    ChunkTooLarge {
368        /// Size of the chunk in bytes.
369        size: usize,
370        /// Maximum allowed size.
371        max_size: usize,
372    },
373    /// Content address mismatch (hash(content) != address).
374    AddressMismatch {
375        /// Expected address.
376        expected: XorName,
377        /// Actual address computed from content.
378        actual: XorName,
379    },
380    /// Storage operation failed.
381    StorageFailed(String),
382    /// Payment verification failed.
383    PaymentFailed(String),
384    /// Quote generation failed.
385    QuoteFailed(String),
386    /// Internal error.
387    Internal(String),
388}
389
390impl std::fmt::Display for ProtocolError {
391    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392        match self {
393            Self::SerializationFailed(msg) => write!(f, "serialization failed: {msg}"),
394            Self::DeserializationFailed(msg) => write!(f, "deserialization failed: {msg}"),
395            Self::MessageTooLarge { size, max_size } => {
396                write!(f, "message size {size} exceeds maximum {max_size}")
397            }
398            Self::ChunkTooLarge { size, max_size } => {
399                write!(f, "chunk size {size} exceeds maximum {max_size}")
400            }
401            Self::AddressMismatch { expected, actual } => {
402                write!(
403                    f,
404                    "address mismatch: expected {}, got {}",
405                    hex::encode(expected),
406                    hex::encode(actual)
407                )
408            }
409            Self::StorageFailed(msg) => write!(f, "storage failed: {msg}"),
410            Self::PaymentFailed(msg) => write!(f, "payment failed: {msg}"),
411            Self::QuoteFailed(msg) => write!(f, "quote failed: {msg}"),
412            Self::Internal(msg) => write!(f, "internal error: {msg}"),
413        }
414    }
415}
416
417impl std::error::Error for ProtocolError {}
418
419#[cfg(test)]
420#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_put_request_encode_decode() {
426        let address = [0xAB; 32];
427        let content = Bytes::from_static(&[1, 2, 3, 4, 5]);
428        let request = ChunkPutRequest::new(address, content.clone());
429        let msg = ChunkMessage {
430            request_id: 42,
431            body: ChunkMessageBody::PutRequest(request),
432        };
433
434        let encoded = msg.encode().expect("encode should succeed");
435        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
436
437        assert_eq!(decoded.request_id, 42);
438        if let ChunkMessageBody::PutRequest(req) = decoded.body {
439            assert_eq!(req.address, address);
440            assert_eq!(req.content, content);
441            assert!(req.payment_proof.is_none());
442        } else {
443            panic!("expected PutRequest");
444        }
445    }
446
447    #[test]
448    fn test_put_request_with_payment() {
449        let address = [0xAB; 32];
450        let content = Bytes::from_static(&[1, 2, 3, 4, 5]);
451        let payment = vec![10, 20, 30];
452        let request = ChunkPutRequest::with_payment(address, content.clone(), payment.clone());
453
454        assert_eq!(request.address, address);
455        assert_eq!(request.content, content);
456        assert_eq!(request.payment_proof, Some(payment));
457    }
458
459    #[test]
460    fn test_get_request_encode_decode() {
461        let address = [0xCD; 32];
462        let request = ChunkGetRequest::new(address);
463        let msg = ChunkMessage {
464            request_id: 7,
465            body: ChunkMessageBody::GetRequest(request),
466        };
467
468        let encoded = msg.encode().expect("encode should succeed");
469        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
470
471        assert_eq!(decoded.request_id, 7);
472        if let ChunkMessageBody::GetRequest(req) = decoded.body {
473            assert_eq!(req.address, address);
474        } else {
475            panic!("expected GetRequest");
476        }
477    }
478
479    #[test]
480    fn test_put_response_success() {
481        let address = [0xEF; 32];
482        let response = ChunkPutResponse::Success { address };
483        let msg = ChunkMessage {
484            request_id: 99,
485            body: ChunkMessageBody::PutResponse(response),
486        };
487
488        let encoded = msg.encode().expect("encode should succeed");
489        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
490
491        assert_eq!(decoded.request_id, 99);
492        if let ChunkMessageBody::PutResponse(ChunkPutResponse::Success { address: addr }) =
493            decoded.body
494        {
495            assert_eq!(addr, address);
496        } else {
497            panic!("expected PutResponse::Success");
498        }
499    }
500
501    #[test]
502    fn test_get_response_not_found() {
503        let address = [0x12; 32];
504        let response = ChunkGetResponse::NotFound { address };
505        let msg = ChunkMessage {
506            request_id: 0,
507            body: ChunkMessageBody::GetResponse(response),
508        };
509
510        let encoded = msg.encode().expect("encode should succeed");
511        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
512
513        assert_eq!(decoded.request_id, 0);
514        if let ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { address: addr }) =
515            decoded.body
516        {
517            assert_eq!(addr, address);
518        } else {
519            panic!("expected GetResponse::NotFound");
520        }
521    }
522
523    #[test]
524    fn test_quote_request_encode_decode() {
525        let address = [0x34; 32];
526        let request = ChunkQuoteRequest::new(address, 1024);
527        let msg = ChunkMessage {
528            request_id: 1,
529            body: ChunkMessageBody::QuoteRequest(request),
530        };
531
532        let encoded = msg.encode().expect("encode should succeed");
533        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
534
535        assert_eq!(decoded.request_id, 1);
536        if let ChunkMessageBody::QuoteRequest(req) = decoded.body {
537            assert_eq!(req.address, address);
538            assert_eq!(req.data_size, 1024);
539            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
540        } else {
541            panic!("expected QuoteRequest");
542        }
543    }
544
545    #[test]
546    fn test_protocol_error_display() {
547        let err = ProtocolError::ChunkTooLarge {
548            size: 5_000_000,
549            max_size: MAX_CHUNK_SIZE,
550        };
551        assert!(err.to_string().contains("5000000"));
552        assert!(err.to_string().contains(&MAX_CHUNK_SIZE.to_string()));
553
554        let err = ProtocolError::AddressMismatch {
555            expected: [0xAA; 32],
556            actual: [0xBB; 32],
557        };
558        let display = err.to_string();
559        assert!(display.contains("address mismatch"));
560    }
561
562    #[test]
563    fn test_decode_rejects_oversized_payload() {
564        let oversized = vec![0u8; MAX_WIRE_MESSAGE_SIZE + 1];
565        let result = ChunkMessage::decode(&oversized);
566        assert!(result.is_err());
567        let err = result.unwrap_err();
568        assert!(
569            matches!(err, ProtocolError::MessageTooLarge { .. }),
570            "expected MessageTooLarge, got {err:?}"
571        );
572    }
573
574    #[test]
575    fn test_invalid_decode() {
576        let invalid_data = vec![0xFF, 0xFF, 0xFF];
577        let result = ChunkMessage::decode(&invalid_data);
578        assert!(result.is_err());
579    }
580
581    #[test]
582    fn test_constants() {
583        assert_eq!(CHUNK_PROTOCOL_ID, "autonomi.ant.chunk.v1");
584        assert_eq!(PROTOCOL_VERSION, 1);
585        assert_eq!(MAX_CHUNK_SIZE, 4 * 1024 * 1024);
586        assert_eq!(DATA_TYPE_CHUNK, 0);
587    }
588
589    #[test]
590    fn test_proof_tag_constants() {
591        // Tags must be distinct non-zero bytes
592        assert_ne!(PROOF_TAG_SINGLE_NODE, PROOF_TAG_MERKLE);
593        assert_ne!(PROOF_TAG_SINGLE_NODE, 0x00);
594        assert_ne!(PROOF_TAG_MERKLE, 0x00);
595        assert_eq!(PROOF_TAG_SINGLE_NODE, 0x01);
596        assert_eq!(PROOF_TAG_MERKLE, 0x02);
597    }
598
599    #[test]
600    fn test_merkle_candidate_quote_request_encode_decode() {
601        let address = [0x56; 32];
602        let request = MerkleCandidateQuoteRequest {
603            address,
604            data_type: DATA_TYPE_CHUNK,
605            data_size: 2048,
606            merkle_payment_timestamp: 1_700_000_000,
607        };
608        let msg = ChunkMessage {
609            request_id: 500,
610            body: ChunkMessageBody::MerkleCandidateQuoteRequest(request),
611        };
612
613        let encoded = msg.encode().expect("encode should succeed");
614        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
615
616        assert_eq!(decoded.request_id, 500);
617        if let ChunkMessageBody::MerkleCandidateQuoteRequest(req) = decoded.body {
618            assert_eq!(req.address, address);
619            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
620            assert_eq!(req.data_size, 2048);
621            assert_eq!(req.merkle_payment_timestamp, 1_700_000_000);
622        } else {
623            panic!("expected MerkleCandidateQuoteRequest");
624        }
625    }
626
627    #[test]
628    fn test_merkle_candidate_quote_response_success_encode_decode() {
629        let candidate_node_bytes = vec![0xAA, 0xBB, 0xCC, 0xDD];
630        let response = MerkleCandidateQuoteResponse::Success {
631            candidate_node: candidate_node_bytes.clone(),
632            commitment: Some(vec![0x11, 0x22]),
633        };
634        let msg = ChunkMessage {
635            request_id: 501,
636            body: ChunkMessageBody::MerkleCandidateQuoteResponse(response),
637        };
638
639        let encoded = msg.encode().expect("encode should succeed");
640        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
641
642        assert_eq!(decoded.request_id, 501);
643        if let ChunkMessageBody::MerkleCandidateQuoteResponse(
644            MerkleCandidateQuoteResponse::Success {
645                candidate_node,
646                commitment,
647            },
648        ) = decoded.body
649        {
650            assert_eq!(candidate_node, candidate_node_bytes);
651            assert_eq!(commitment, Some(vec![0x11, 0x22]));
652        } else {
653            panic!("expected MerkleCandidateQuoteResponse::Success");
654        }
655    }
656
657    #[test]
658    fn test_merkle_candidate_quote_response_error_encode_decode() {
659        let error = ProtocolError::QuoteFailed("no libp2p keypair".to_string());
660        let response = MerkleCandidateQuoteResponse::Error(error.clone());
661        let msg = ChunkMessage {
662            request_id: 502,
663            body: ChunkMessageBody::MerkleCandidateQuoteResponse(response),
664        };
665
666        let encoded = msg.encode().expect("encode should succeed");
667        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
668
669        assert_eq!(decoded.request_id, 502);
670        if let ChunkMessageBody::MerkleCandidateQuoteResponse(
671            MerkleCandidateQuoteResponse::Error(err),
672        ) = decoded.body
673        {
674            assert_eq!(err, error);
675        } else {
676            panic!("expected MerkleCandidateQuoteResponse::Error");
677        }
678    }
679}