Skip to main content

saorsa_node/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 serde::{Deserialize, Serialize};
10
11/// Protocol identifier for chunk operations.
12pub const CHUNK_PROTOCOL_ID: &str = "saorsa/ant/chunk/v1";
13
14/// Current protocol version.
15pub const PROTOCOL_VERSION: u16 = 1;
16
17/// Maximum chunk size in bytes (4MB).
18pub const MAX_CHUNK_SIZE: usize = 4 * 1024 * 1024;
19
20/// Maximum wire message size in bytes (5MB).
21///
22/// Limits the input buffer accepted by [`ChunkMessage::decode`] to prevent
23/// unbounded allocation from malicious or corrupted payloads. Set slightly
24/// above [`MAX_CHUNK_SIZE`] to accommodate message envelope overhead.
25pub const MAX_WIRE_MESSAGE_SIZE: usize = 5 * 1024 * 1024;
26
27/// Data type identifier for chunks.
28pub const DATA_TYPE_CHUNK: u32 = 0;
29
30/// Content-addressed identifier (32 bytes).
31pub type XorName = [u8; 32];
32
33/// Enum of all chunk protocol message types.
34///
35/// Uses a single-byte discriminant for efficient wire encoding.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum ChunkMessageBody {
38    /// Request to store a chunk.
39    PutRequest(ChunkPutRequest),
40    /// Response to a PUT request.
41    PutResponse(ChunkPutResponse),
42    /// Request to retrieve a chunk.
43    GetRequest(ChunkGetRequest),
44    /// Response to a GET request.
45    GetResponse(ChunkGetResponse),
46    /// Request a storage quote.
47    QuoteRequest(ChunkQuoteRequest),
48    /// Response with a storage quote.
49    QuoteResponse(ChunkQuoteResponse),
50}
51
52/// Wire-format wrapper that pairs a sender-assigned `request_id` with
53/// a [`ChunkMessageBody`].
54///
55/// The sender picks a unique `request_id`; the handler echoes it back
56/// in the response so callers can correlate replies by ID rather than
57/// by source peer.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ChunkMessage {
60    /// Sender-assigned identifier, echoed back in the response.
61    pub request_id: u64,
62    /// The protocol message body.
63    pub body: ChunkMessageBody,
64}
65
66impl ChunkMessage {
67    /// Encode the message to bytes using postcard.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if serialization fails.
72    pub fn encode(&self) -> Result<Vec<u8>, ProtocolError> {
73        postcard::to_stdvec(self).map_err(|e| ProtocolError::SerializationFailed(e.to_string()))
74    }
75
76    /// Decode a message from bytes using postcard.
77    ///
78    /// Rejects payloads larger than [`MAX_WIRE_MESSAGE_SIZE`] before
79    /// attempting deserialization.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`ProtocolError::MessageTooLarge`] if the input exceeds the
84    /// size limit, or [`ProtocolError::DeserializationFailed`] if postcard
85    /// cannot parse the data.
86    pub fn decode(data: &[u8]) -> Result<Self, ProtocolError> {
87        if data.len() > MAX_WIRE_MESSAGE_SIZE {
88            return Err(ProtocolError::MessageTooLarge {
89                size: data.len(),
90                max_size: MAX_WIRE_MESSAGE_SIZE,
91            });
92        }
93        postcard::from_bytes(data).map_err(|e| ProtocolError::DeserializationFailed(e.to_string()))
94    }
95}
96
97// =============================================================================
98// PUT Request/Response
99// =============================================================================
100
101/// Request to store a chunk.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ChunkPutRequest {
104    /// The content-addressed identifier (BLAKE3 of content).
105    pub address: XorName,
106    /// The chunk data.
107    pub content: Vec<u8>,
108    /// Optional payment proof (serialized `ProofOfPayment`).
109    /// Required for new chunks unless already verified.
110    pub payment_proof: Option<Vec<u8>>,
111}
112
113impl ChunkPutRequest {
114    /// Create a new PUT request.
115    #[must_use]
116    pub fn new(address: XorName, content: Vec<u8>) -> Self {
117        Self {
118            address,
119            content,
120            payment_proof: None,
121        }
122    }
123
124    /// Create a new PUT request with payment proof.
125    #[must_use]
126    pub fn with_payment(address: XorName, content: Vec<u8>, payment_proof: Vec<u8>) -> Self {
127        Self {
128            address,
129            content,
130            payment_proof: Some(payment_proof),
131        }
132    }
133}
134
135/// Response to a PUT request.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub enum ChunkPutResponse {
138    /// Chunk stored successfully.
139    Success {
140        /// The address where the chunk was stored.
141        address: XorName,
142    },
143    /// Chunk already exists (idempotent success).
144    AlreadyExists {
145        /// The existing chunk address.
146        address: XorName,
147    },
148    /// Payment is required to store this chunk.
149    PaymentRequired {
150        /// Error message.
151        message: String,
152    },
153    /// An error occurred.
154    Error(ProtocolError),
155}
156
157// =============================================================================
158// GET Request/Response
159// =============================================================================
160
161/// Request to retrieve a chunk.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ChunkGetRequest {
164    /// The content-addressed identifier to retrieve.
165    pub address: XorName,
166}
167
168impl ChunkGetRequest {
169    /// Create a new GET request.
170    #[must_use]
171    pub fn new(address: XorName) -> Self {
172        Self { address }
173    }
174}
175
176/// Response to a GET request.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub enum ChunkGetResponse {
179    /// Chunk found and returned.
180    Success {
181        /// The chunk address.
182        address: XorName,
183        /// The chunk data.
184        content: Vec<u8>,
185    },
186    /// Chunk not found.
187    NotFound {
188        /// The requested address.
189        address: XorName,
190    },
191    /// An error occurred.
192    Error(ProtocolError),
193}
194
195// =============================================================================
196// Quote Request/Response
197// =============================================================================
198
199/// Request a storage quote for a chunk.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ChunkQuoteRequest {
202    /// The content address of the data to store.
203    pub address: XorName,
204    /// Size of the data in bytes.
205    pub data_size: u64,
206    /// Data type identifier (0 for chunks).
207    pub data_type: u32,
208}
209
210impl ChunkQuoteRequest {
211    /// Create a new quote request.
212    #[must_use]
213    pub fn new(address: XorName, data_size: u64) -> Self {
214        Self {
215            address,
216            data_size,
217            data_type: DATA_TYPE_CHUNK,
218        }
219    }
220}
221
222/// Response with a storage quote.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub enum ChunkQuoteResponse {
225    /// Quote generated successfully.
226    ///
227    /// When `already_stored` is `true` the node already holds this chunk and no
228    /// payment is required — the client should skip the pay-then-PUT cycle for
229    /// this address. The quote is still included for informational purposes.
230    Success {
231        /// Serialized `PaymentQuote`.
232        quote: Vec<u8>,
233        /// `true` when the chunk already exists on this node (skip payment).
234        already_stored: bool,
235    },
236    /// Quote generation failed.
237    Error(ProtocolError),
238}
239
240// =============================================================================
241// Protocol Errors
242// =============================================================================
243
244/// Errors that can occur during protocol operations.
245#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
246pub enum ProtocolError {
247    /// Message serialization failed.
248    SerializationFailed(String),
249    /// Message deserialization failed.
250    DeserializationFailed(String),
251    /// Wire message exceeds the maximum allowed size.
252    MessageTooLarge {
253        /// Actual size of the message in bytes.
254        size: usize,
255        /// Maximum allowed size.
256        max_size: usize,
257    },
258    /// Chunk exceeds maximum size.
259    ChunkTooLarge {
260        /// Size of the chunk in bytes.
261        size: usize,
262        /// Maximum allowed size.
263        max_size: usize,
264    },
265    /// Content address mismatch (hash(content) != address).
266    AddressMismatch {
267        /// Expected address.
268        expected: XorName,
269        /// Actual address computed from content.
270        actual: XorName,
271    },
272    /// Storage operation failed.
273    StorageFailed(String),
274    /// Payment verification failed.
275    PaymentFailed(String),
276    /// Quote generation failed.
277    QuoteFailed(String),
278    /// Internal error.
279    Internal(String),
280}
281
282impl std::fmt::Display for ProtocolError {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        match self {
285            Self::SerializationFailed(msg) => write!(f, "serialization failed: {msg}"),
286            Self::DeserializationFailed(msg) => write!(f, "deserialization failed: {msg}"),
287            Self::MessageTooLarge { size, max_size } => {
288                write!(f, "message size {size} exceeds maximum {max_size}")
289            }
290            Self::ChunkTooLarge { size, max_size } => {
291                write!(f, "chunk size {size} exceeds maximum {max_size}")
292            }
293            Self::AddressMismatch { expected, actual } => {
294                write!(
295                    f,
296                    "address mismatch: expected {}, got {}",
297                    hex::encode(expected),
298                    hex::encode(actual)
299                )
300            }
301            Self::StorageFailed(msg) => write!(f, "storage failed: {msg}"),
302            Self::PaymentFailed(msg) => write!(f, "payment failed: {msg}"),
303            Self::QuoteFailed(msg) => write!(f, "quote failed: {msg}"),
304            Self::Internal(msg) => write!(f, "internal error: {msg}"),
305        }
306    }
307}
308
309impl std::error::Error for ProtocolError {}
310
311#[cfg(test)]
312#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_put_request_encode_decode() {
318        let address = [0xAB; 32];
319        let content = vec![1, 2, 3, 4, 5];
320        let request = ChunkPutRequest::new(address, content.clone());
321        let msg = ChunkMessage {
322            request_id: 42,
323            body: ChunkMessageBody::PutRequest(request),
324        };
325
326        let encoded = msg.encode().expect("encode should succeed");
327        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
328
329        assert_eq!(decoded.request_id, 42);
330        if let ChunkMessageBody::PutRequest(req) = decoded.body {
331            assert_eq!(req.address, address);
332            assert_eq!(req.content, content);
333            assert!(req.payment_proof.is_none());
334        } else {
335            panic!("expected PutRequest");
336        }
337    }
338
339    #[test]
340    fn test_put_request_with_payment() {
341        let address = [0xAB; 32];
342        let content = vec![1, 2, 3, 4, 5];
343        let payment = vec![10, 20, 30];
344        let request = ChunkPutRequest::with_payment(address, content.clone(), payment.clone());
345
346        assert_eq!(request.address, address);
347        assert_eq!(request.content, content);
348        assert_eq!(request.payment_proof, Some(payment));
349    }
350
351    #[test]
352    fn test_get_request_encode_decode() {
353        let address = [0xCD; 32];
354        let request = ChunkGetRequest::new(address);
355        let msg = ChunkMessage {
356            request_id: 7,
357            body: ChunkMessageBody::GetRequest(request),
358        };
359
360        let encoded = msg.encode().expect("encode should succeed");
361        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
362
363        assert_eq!(decoded.request_id, 7);
364        if let ChunkMessageBody::GetRequest(req) = decoded.body {
365            assert_eq!(req.address, address);
366        } else {
367            panic!("expected GetRequest");
368        }
369    }
370
371    #[test]
372    fn test_put_response_success() {
373        let address = [0xEF; 32];
374        let response = ChunkPutResponse::Success { address };
375        let msg = ChunkMessage {
376            request_id: 99,
377            body: ChunkMessageBody::PutResponse(response),
378        };
379
380        let encoded = msg.encode().expect("encode should succeed");
381        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
382
383        assert_eq!(decoded.request_id, 99);
384        if let ChunkMessageBody::PutResponse(ChunkPutResponse::Success { address: addr }) =
385            decoded.body
386        {
387            assert_eq!(addr, address);
388        } else {
389            panic!("expected PutResponse::Success");
390        }
391    }
392
393    #[test]
394    fn test_get_response_not_found() {
395        let address = [0x12; 32];
396        let response = ChunkGetResponse::NotFound { address };
397        let msg = ChunkMessage {
398            request_id: 0,
399            body: ChunkMessageBody::GetResponse(response),
400        };
401
402        let encoded = msg.encode().expect("encode should succeed");
403        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
404
405        assert_eq!(decoded.request_id, 0);
406        if let ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { address: addr }) =
407            decoded.body
408        {
409            assert_eq!(addr, address);
410        } else {
411            panic!("expected GetResponse::NotFound");
412        }
413    }
414
415    #[test]
416    fn test_quote_request_encode_decode() {
417        let address = [0x34; 32];
418        let request = ChunkQuoteRequest::new(address, 1024);
419        let msg = ChunkMessage {
420            request_id: 1,
421            body: ChunkMessageBody::QuoteRequest(request),
422        };
423
424        let encoded = msg.encode().expect("encode should succeed");
425        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
426
427        assert_eq!(decoded.request_id, 1);
428        if let ChunkMessageBody::QuoteRequest(req) = decoded.body {
429            assert_eq!(req.address, address);
430            assert_eq!(req.data_size, 1024);
431            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
432        } else {
433            panic!("expected QuoteRequest");
434        }
435    }
436
437    #[test]
438    fn test_protocol_error_display() {
439        let err = ProtocolError::ChunkTooLarge {
440            size: 5_000_000,
441            max_size: MAX_CHUNK_SIZE,
442        };
443        assert!(err.to_string().contains("5000000"));
444        assert!(err.to_string().contains(&MAX_CHUNK_SIZE.to_string()));
445
446        let err = ProtocolError::AddressMismatch {
447            expected: [0xAA; 32],
448            actual: [0xBB; 32],
449        };
450        let display = err.to_string();
451        assert!(display.contains("address mismatch"));
452    }
453
454    #[test]
455    fn test_decode_rejects_oversized_payload() {
456        let oversized = vec![0u8; MAX_WIRE_MESSAGE_SIZE + 1];
457        let result = ChunkMessage::decode(&oversized);
458        assert!(result.is_err());
459        let err = result.unwrap_err();
460        assert!(
461            matches!(err, ProtocolError::MessageTooLarge { .. }),
462            "expected MessageTooLarge, got {err:?}"
463        );
464    }
465
466    #[test]
467    fn test_invalid_decode() {
468        let invalid_data = vec![0xFF, 0xFF, 0xFF];
469        let result = ChunkMessage::decode(&invalid_data);
470        assert!(result.is_err());
471    }
472
473    #[test]
474    fn test_constants() {
475        assert_eq!(CHUNK_PROTOCOL_ID, "saorsa/ant/chunk/v1");
476        assert_eq!(PROTOCOL_VERSION, 1);
477        assert_eq!(MAX_CHUNK_SIZE, 4 * 1024 * 1024);
478        assert_eq!(DATA_TYPE_CHUNK, 0);
479    }
480}