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