Skip to main content

clay_codes/
lib.rs

1//! Clay (Coupled-Layer) Erasure Codes
2//!
3//! Implementation of Clay codes based on the FAST'18 paper:
4//! "Clay Codes: Moulding MDS Codes to Yield an MSR Code"
5//!
6//! Clay codes are MSR (Minimum Storage Regenerating) codes that provide
7//! optimal repair bandwidth - recovering a lost node using only β sub-chunks
8//! from each of d helper nodes, rather than downloading k full chunks.
9//!
10//! # Example
11//!
12//! ```
13//! use clay_codes::ClayCode;
14//! use std::collections::HashMap;
15//!
16//! // Create a (6, 4, 5) Clay code: 4 data + 2 parity, repair with 5 helpers
17//! let clay = ClayCode::new(4, 2, 5).unwrap();
18//!
19//! // Encode data
20//! let data = b"Hello, Clay codes!";
21//! let chunks = clay.encode(data);
22//!
23//! // Decode with all chunks
24//! let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
25//! for (i, chunk) in chunks.iter().enumerate() {
26//!     available.insert(i, chunk.clone());
27//! }
28//! let decoded = clay.decode(&available, &[]).unwrap();
29//! assert_eq!(&decoded[..data.len()], &data[..]);
30//! ```
31//!
32//! # Modules
33//!
34//! - `error`: Error types for Clay code operations
35//! - `transforms`: Pairwise coupling transforms (PRT/PFT)
36//! - `encode`: Encoding implementation
37//! - `decode`: Decoding and erasure recovery
38//! - `repair`: Single-node optimal repair
39
40use std::collections::HashMap;
41
42mod coords;
43mod decode;
44mod encode;
45mod error;
46mod repair;
47mod transforms;
48
49pub use error::ClayError;
50
51const MAX_RS_SHARDS: usize = 32768;
52
53use decode::{decode as decode_chunks, decode_rows as decode_rows_impl, RsCodec};
54use encode::encode as encode_chunks;
55use repair::{minimum_to_repair as min_repair, repair as repair_chunk, repair_rows as repair_rows_impl};
56
57/// Clay (Coupled-Layer) erasure code
58#[derive(Clone, Debug)]
59pub struct ClayCode {
60    /// Number of data chunks
61    pub k: usize,
62    /// Number of parity chunks
63    pub m: usize,
64    /// Total nodes (k + m)
65    pub n: usize,
66    /// Number of helper nodes for repair (k <= d <= n-1)
67    pub d: usize,
68    /// Coupling factor: q = d - k + 1
69    pub q: usize,
70    /// Number of y-sections: t = (n + nu) / q
71    pub t: usize,
72    /// Shortening parameter: makes (k + m + nu) divisible by q
73    pub nu: usize,
74    /// Sub-packetization level: α = q^t (sub-chunks per chunk)
75    pub sub_chunk_no: usize,
76    /// Sub-chunks needed from each helper during repair: β = α / q
77    pub beta: usize,
78    /// Number of original shards for RS (k + nu)
79    original_count: usize,
80    /// Reed-Solomon codec reused by every encode, decode, and repair call
81    rs: RsCodec,
82}
83
84impl ClayCode {
85    /// Create a new Clay code with parameters (k, m, d)
86    ///
87    /// # Parameters
88    /// - `k`: Number of data chunks (systematic nodes)
89    /// - `m`: Number of parity chunks
90    /// - `d`: Number of helper nodes for repair
91    ///
92    /// # Returns
93    /// Result with ClayCode or error if parameters are invalid
94    pub fn new(k: usize, m: usize, d: usize) -> Result<Self, ClayError> {
95        if k < 1 {
96            return Err(ClayError::InvalidParameters("k must be at least 1".into()));
97        }
98        if m < 1 {
99            return Err(ClayError::InvalidParameters("m must be at least 1".into()));
100        }
101        if d < k + 1 || d > k + m - 1 {
102            return Err(ClayError::InvalidParameters(format!(
103                "d must be in range [{}, {}], got {}",
104                k + 1,
105                k + m - 1,
106                d
107            )));
108        }
109
110        let q = d - k + 1;
111        let n = k + m;
112
113        // Calculate nu for shortening (so that n + nu is divisible by q)
114        let nu = if n % q == 0 { 0 } else { q - (n % q) };
115
116        let t = (n + nu) / q;
117
118        // Use checked arithmetic for sub_chunk_no = q^t
119        let sub_chunk_no = checked_pow(q, t).ok_or_else(|| {
120            ClayError::Overflow(format!("q^t = {}^{} overflows", q, t))
121        })?;
122
123        let beta = sub_chunk_no / q; // β = α / q
124
125        // Validate that k+nu+m fits in reed-solomon limits (up to 32768 shards)
126        let original_count = k + nu;
127        let recovery_count = m;
128        if original_count > MAX_RS_SHARDS || recovery_count > MAX_RS_SHARDS {
129            return Err(ClayError::InvalidParameters(
130                "Total nodes exceeds reed-solomon limit of 32768".into(),
131            ));
132        }
133
134        let rs = RsCodec::new(original_count, recovery_count)
135            .map_err(|e| ClayError::InvalidParameters(format!("RS init failed: {:?}", e)))?;
136
137        Ok(ClayCode {
138            k,
139            m,
140            n,
141            d,
142            q,
143            t,
144            nu,
145            sub_chunk_no,
146            beta,
147            original_count,
148            rs,
149        })
150    }
151
152    /// Create with default d = k + m - 1 (maximum helpers)
153    pub fn new_default(k: usize, m: usize) -> Result<Self, ClayError> {
154        Self::new(k, m, k + m - 1)
155    }
156
157    /// Get encoding parameters for internal use
158    fn encode_params(&self) -> encode::EncodeParams {
159        encode::EncodeParams {
160            k: self.k,
161            m: self.m,
162            n: self.n,
163            q: self.q,
164            t: self.t,
165            nu: self.nu,
166            sub_chunk_no: self.sub_chunk_no,
167            original_count: self.original_count,
168        }
169    }
170
171    /// Encode data into n chunks
172    ///
173    /// # Parameters
174    /// - `data`: Raw data bytes to encode
175    ///
176    /// # Returns
177    /// Vector of n chunks, each containing α sub-chunks
178    pub fn encode(&self, data: &[u8]) -> Vec<Vec<u8>> {
179        encode_chunks(&self.encode_params(), &self.rs, data)
180    }
181
182    /// Decode data from available chunks
183    ///
184    /// # Parameters
185    /// - `available`: Map from chunk index to chunk data
186    /// - `erasures`: Set of erased chunk indices
187    ///
188    /// # Returns
189    /// Recovered original data, or error if decoding fails
190    pub fn decode(
191        &self,
192        available: &HashMap<usize, Vec<u8>>,
193        erasures: &[usize],
194    ) -> Result<Vec<u8>, ClayError> {
195        decode_chunks(&self.encode_params(), &self.rs, available, erasures)
196    }
197
198    /// Recover original data from chunks indexed by node, `None` for erasures
199    ///
200    /// The zero-copy form of decode. The caller lends the chunks it already
201    /// holds instead of building a map of owned buffers. Needs exactly n slots.
202    pub fn decode_rows(&self, chunks: &[Option<&[u8]>]) -> Result<Vec<u8>, ClayError> {
203        decode_rows_impl(&self.encode_params(), &self.rs, chunks)
204    }
205
206    /// Determine minimum sub-chunks needed to repair a lost node
207    ///
208    /// # Parameters
209    /// - `lost_node`: Index of the lost node (0 to n-1)
210    /// - `available`: Available node indices
211    ///
212    /// # Returns
213    /// Vector of (helper_node_idx, sub_chunk_indices) where sub_chunk_indices
214    /// is a vector of the specific sub-chunk indices needed from that helper.
215    /// The repair() function expects helper data to contain these sub-chunks
216    /// concatenated in the ORDER they appear in sub_chunk_indices.
217    pub fn minimum_to_repair(
218        &self,
219        lost_node: usize,
220        available: &[usize],
221    ) -> Result<Vec<(usize, Vec<usize>)>, ClayError> {
222        min_repair(&self.encode_params(), lost_node, available)
223    }
224
225    /// Repair a lost chunk using partial data from helper nodes
226    ///
227    /// # Parameters
228    /// - `lost_node`: Index of the lost node (0 to n-1)
229    /// - `helper_data`: Map from helper node index to partial chunk data.
230    ///   Each helper's data must be the concatenation of sub-chunks at the
231    ///   indices returned by minimum_to_repair(), in that exact order.
232    /// - `chunk_size`: Full chunk size
233    ///
234    /// # Returns
235    /// The recovered full chunk, or error if repair fails
236    pub fn repair(
237        &self,
238        lost_node: usize,
239        helper_data: &HashMap<usize, Vec<u8>>,
240        chunk_size: usize,
241    ) -> Result<Vec<u8>, ClayError> {
242        repair_chunk(&self.encode_params(), &self.rs, lost_node, helper_data, chunk_size)
243    }
244
245    /// Repair a lost chunk from helper partials indexed by node
246    ///
247    /// The zero-copy form of repair. One slot per node, `None` where a node is
248    /// not contributing, and each entry holds that helper's sub-chunks in the
249    /// order minimum_to_repair returned them.
250    pub fn repair_rows(
251        &self,
252        lost_node: usize,
253        helpers: &[Option<&[u8]>],
254        chunk_size: usize,
255    ) -> Result<Vec<u8>, ClayError> {
256        repair_rows_impl(&self.encode_params(), &self.rs, lost_node, helpers, chunk_size)
257    }
258
259    /// Calculate normalized repair bandwidth
260    ///
261    /// This is the ratio of data downloaded for repair to the size of the
262    /// repaired chunk. For Clay codes, this is d / (k * q).
263    pub fn normalized_repair_bandwidth(&self) -> f64 {
264        (self.d as f64) / ((self.k as f64) * (self.d - self.k + 1) as f64)
265    }
266}
267
268/// Integer power function with overflow checking
269fn checked_pow(base: usize, exp: usize) -> Option<usize> {
270    let mut result: usize = 1;
271    let mut b = base;
272    let mut e = exp;
273    while e > 0 {
274        if e & 1 == 1 {
275            result = result.checked_mul(b)?;
276        }
277        e >>= 1;
278        if e > 0 {
279            b = b.checked_mul(b)?;
280        }
281    }
282    Some(result)
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_basic_encode_decode() {
291        let clay = ClayCode::new(4, 2, 5).unwrap();
292        let data = b"Test data for Clay codes - not empty!";
293        let chunks = clay.encode(data);
294        assert_eq!(chunks.len(), 6); // k + m = 6
295
296        // Decode with all chunks
297        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
298        for (i, chunk) in chunks.iter().enumerate() {
299            available.insert(i, chunk.clone());
300        }
301        let decoded = clay.decode(&available, &[]).unwrap();
302
303        // Check prefix matches (may have padding)
304        assert_eq!(&decoded[..data.len()], &data[..]);
305    }
306
307    #[test]
308    fn test_decode_with_erasures() {
309        let clay = ClayCode::new(4, 2, 5).unwrap();
310        let data = b"Test data for Clay codes - testing erasure recovery!";
311        let chunks = clay.encode(data);
312
313        // Lose node 0
314        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
315        for (i, chunk) in chunks.iter().enumerate() {
316            if i != 0 {
317                available.insert(i, chunk.clone());
318            }
319        }
320        let decoded = clay.decode(&available, &[0]).unwrap();
321        assert_eq!(&decoded[..data.len()], &data[..]);
322
323        // Lose node 5 (parity)
324        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
325        for (i, chunk) in chunks.iter().enumerate() {
326            if i != 5 {
327                available.insert(i, chunk.clone());
328            }
329        }
330        let decoded = clay.decode(&available, &[5]).unwrap();
331        assert_eq!(&decoded[..data.len()], &data[..]);
332
333        // Lose two nodes
334        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
335        for (i, chunk) in chunks.iter().enumerate() {
336            if i != 0 && i != 5 {
337                available.insert(i, chunk.clone());
338            }
339        }
340        let decoded = clay.decode(&available, &[0, 5]).unwrap();
341        assert_eq!(&decoded[..data.len()], &data[..]);
342    }
343
344    #[test]
345    fn test_parameters() {
346        // Test (6, 4, 5) - from paper
347        let clay = ClayCode::new(4, 2, 5).unwrap();
348        assert_eq!(clay.q, 2);
349        assert_eq!(clay.t, 3);
350        assert_eq!(clay.sub_chunk_no, 8); // 2^3 = 8
351        assert_eq!(clay.beta, 4); // 8 / 2 = 4
352
353        // Test (14, 10, 13)
354        let clay2 = ClayCode::new(10, 4, 13).unwrap();
355        assert_eq!(clay2.q, 4);
356        assert_eq!(clay2.t, 4);
357        assert_eq!(clay2.sub_chunk_no, 256); // 4^4 = 256
358        assert_eq!(clay2.beta, 64); // 256 / 4 = 64
359    }
360
361    #[test]
362    fn test_minimum_to_repair() {
363        let clay = ClayCode::new(4, 2, 5).unwrap();
364        let available: Vec<usize> = vec![1, 2, 3, 4, 5];
365        let helper_info = clay.minimum_to_repair(0, &available).unwrap();
366
367        // Should return d = 5 helpers
368        assert_eq!(helper_info.len(), 5);
369
370        // Each helper should provide β = 4 sub-chunks
371        for (_, indices) in &helper_info {
372            assert_eq!(indices.len(), 4);
373        }
374    }
375
376    #[test]
377    fn test_repair_bandwidth_verification() {
378        // This test verifies we're actually using Clay's repair advantage
379        let clay = ClayCode::new(4, 2, 5).unwrap();
380        let data = b"Test data for bandwidth verification of Clay codes repair!";
381        let chunks = clay.encode(data);
382        let chunk_size = chunks[0].len();
383
384        // Get minimum data needed to repair node 0
385        let available: Vec<usize> = vec![1, 2, 3, 4, 5];
386        let helper_info = clay.minimum_to_repair(0, &available).unwrap();
387
388        // Calculate total sub-chunks requested
389        let sub_chunk_size = chunk_size / clay.sub_chunk_no;
390        let total_repair_subchunks: usize = helper_info
391            .iter()
392            .map(|(_, indices)| indices.len())
393            .sum();
394        let total_repair_bytes = total_repair_subchunks * sub_chunk_size;
395
396        let full_decode_bytes = clay.k * chunk_size;
397
398        // Clay repair should use significantly less data
399        let ratio = total_repair_bytes as f64 / full_decode_bytes as f64;
400        println!(
401            "Repair bandwidth: {} bytes, Full decode: {} bytes, Ratio: {:.3}",
402            total_repair_bytes, full_decode_bytes, ratio
403        );
404
405        assert!(
406            total_repair_bytes < full_decode_bytes * 7 / 10,
407            "Repair bandwidth {} should be < 70% of full decode {}",
408            total_repair_bytes,
409            full_decode_bytes
410        );
411    }
412
413    #[test]
414    fn test_repair_correctness() {
415        let clay = ClayCode::new(4, 2, 5).unwrap();
416        let data = b"Test data for repair correctness verification!!!!";
417        let chunks = clay.encode(data);
418        let chunk_size = chunks[0].len();
419        let sub_chunk_size = chunk_size / clay.sub_chunk_no;
420
421        // Test repairing each node
422        for lost_node in 0..clay.n {
423            let available: Vec<usize> = (0..clay.n).filter(|&i| i != lost_node).collect();
424            let helper_info = clay.minimum_to_repair(lost_node, &available).unwrap();
425
426            // Extract only the required sub-chunks from each helper
427            let mut partial_data: HashMap<usize, Vec<u8>> = HashMap::new();
428            for (helper_idx, indices) in &helper_info {
429                let mut helper_partial = Vec::new();
430                for &sc_idx in indices {
431                    let start_byte = sc_idx * sub_chunk_size;
432                    let end_byte = (sc_idx + 1) * sub_chunk_size;
433                    helper_partial.extend_from_slice(&chunks[*helper_idx][start_byte..end_byte]);
434                }
435                partial_data.insert(*helper_idx, helper_partial);
436            }
437
438            // Repair using ONLY partial data
439            let recovered = clay.repair(lost_node, &partial_data, chunk_size).unwrap();
440
441            // Verify recovered chunk matches original
442            assert_eq!(
443                recovered, chunks[lost_node],
444                "Repair failed for node {}",
445                lost_node
446            );
447        }
448    }
449
450    #[test]
451    fn test_various_parameters() {
452        // Test different parameter combinations from the paper
453        let params = vec![
454            (4, 2, 5),   // (6, 4, 5) - α=8, β=4
455            (9, 3, 11),  // (12, 9, 11) - α=81, β=27
456            (10, 4, 13), // (14, 10, 13) - α=256, β=64
457        ];
458
459        for (k, m, d) in params {
460            let clay = ClayCode::new(k, m, d).unwrap();
461            let data_size = k * clay.sub_chunk_no * 2;
462            let data: Vec<u8> = (0..data_size).map(|i| (i % 256) as u8).collect();
463            let chunks = clay.encode(&data);
464
465            // Test decode with one erasure
466            let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
467            for (i, chunk) in chunks.iter().enumerate() {
468                if i != 0 {
469                    available.insert(i, chunk.clone());
470                }
471            }
472            let decoded = clay.decode(&available, &[0]).unwrap();
473            assert_eq!(
474                &decoded[..data.len()],
475                &data[..],
476                "Failed for params ({}, {}, {})",
477                k,
478                m,
479                d
480            );
481        }
482    }
483
484    #[test]
485    fn test_repair_all_nodes_various_params() {
486        let params = vec![(4, 2, 5), (9, 3, 11)];
487
488        for (k, m, d) in params {
489            let clay = ClayCode::new(k, m, d).unwrap();
490            let data_size = k * clay.sub_chunk_no;
491            let data: Vec<u8> = (0..data_size).map(|i| ((i * 7 + 13) % 256) as u8).collect();
492            let chunks = clay.encode(&data);
493            let chunk_size = chunks[0].len();
494            let sub_chunk_size = chunk_size / clay.sub_chunk_no;
495
496            for lost_node in 0..clay.n {
497                let available: Vec<usize> = (0..clay.n).filter(|&i| i != lost_node).collect();
498                let helper_info = clay.minimum_to_repair(lost_node, &available).unwrap();
499
500                let mut partial_data: HashMap<usize, Vec<u8>> = HashMap::new();
501                for (helper_idx, indices) in &helper_info {
502                    let mut helper_partial = Vec::new();
503                    for &sc_idx in indices {
504                        let start_byte = sc_idx * sub_chunk_size;
505                        let end_byte = (sc_idx + 1) * sub_chunk_size;
506                        helper_partial.extend_from_slice(&chunks[*helper_idx][start_byte..end_byte]);
507                    }
508                    partial_data.insert(*helper_idx, helper_partial);
509                }
510
511                let recovered = clay.repair(lost_node, &partial_data, chunk_size).unwrap();
512                assert_eq!(
513                    recovered, chunks[lost_node],
514                    "Repair failed for node {} with params ({}, {}, {})",
515                    lost_node, k, m, d
516                );
517            }
518        }
519    }
520
521    #[test]
522    fn test_decode_max_erasures() {
523        let clay = ClayCode::new(4, 2, 5).unwrap();
524        let data: Vec<u8> = (0..256).map(|i| (i % 256) as u8).collect();
525        let chunks = clay.encode(&data);
526
527        // Lose exactly m = 2 nodes in different patterns
528        let patterns = vec![vec![0, 5], vec![0, 1], vec![4, 5], vec![1, 3]];
529
530        for erasures in patterns {
531            let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
532            for (i, chunk) in chunks.iter().enumerate() {
533                if !erasures.contains(&i) {
534                    available.insert(i, chunk.clone());
535                }
536            }
537            let decoded = clay.decode(&available, &erasures).unwrap();
538            assert_eq!(
539                &decoded[..data.len()],
540                &data[..],
541                "Failed for erasures {:?}",
542                erasures
543            );
544        }
545    }
546
547    #[test]
548    fn test_normalized_repair_bandwidth() {
549        let test_cases = vec![
550            ((4, 2, 5), 0.625),
551            ((9, 3, 11), 0.407),
552            ((10, 4, 13), 0.325),
553        ];
554
555        for ((k, m, d), expected) in test_cases {
556            let clay = ClayCode::new(k, m, d).unwrap();
557            let actual = clay.normalized_repair_bandwidth();
558            assert!(
559                (actual - expected).abs() < 0.01,
560                "Expected {}, got {} for ({}, {}, {})",
561                expected,
562                actual,
563                k,
564                m,
565                d
566            );
567        }
568    }
569
570    #[test]
571    fn test_random_data() {
572        use rand::Rng;
573        let mut rng = rand::thread_rng();
574
575        let clay = ClayCode::new(4, 2, 5).unwrap();
576        let data_size = clay.k * clay.sub_chunk_no * 4;
577        let data: Vec<u8> = (0..data_size).map(|_| rng.gen()).collect();
578        let chunks = clay.encode(&data);
579
580        // Test full decode
581        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
582        for (i, chunk) in chunks.iter().enumerate() {
583            available.insert(i, chunk.clone());
584        }
585        let decoded = clay.decode(&available, &[]).unwrap();
586        assert_eq!(&decoded[..data.len()], &data[..]);
587
588        // Test decode with erasure
589        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
590        for (i, chunk) in chunks.iter().enumerate() {
591            if i != 2 {
592                available.insert(i, chunk.clone());
593            }
594        }
595        let decoded = clay.decode(&available, &[2]).unwrap();
596        assert_eq!(&decoded[..data.len()], &data[..]);
597    }
598
599    #[test]
600    fn test_checked_pow_overflow() {
601        // Test that checked_pow handles overflow gracefully
602        assert!(checked_pow(2, 63).is_some());
603        assert!(checked_pow(2, 64).is_none()); // Would overflow
604        assert!(checked_pow(10, 20).is_none()); // Would overflow
605    }
606
607    #[test]
608    fn test_invalid_parameters() {
609        // k must be >= 1
610        assert!(ClayCode::new(0, 2, 1).is_err());
611
612        // m must be >= 1
613        assert!(ClayCode::new(4, 0, 3).is_err());
614
615        // d must be in range
616        assert!(ClayCode::new(4, 2, 4).is_err()); // d < k+1
617        assert!(ClayCode::new(4, 2, 6).is_err()); // d > k+m-1
618    }
619
620    #[test]
621    fn test_clone_and_debug() {
622        let clay = ClayCode::new(4, 2, 5).unwrap();
623        let clay2 = clay.clone();
624        assert_eq!(clay2.k, clay.k);
625        assert_eq!(clay2.m, clay.m);
626        assert_eq!(clay2.d, clay.d);
627        // Verify Debug is implemented
628        let debug_str = format!("{:?}", clay);
629        assert!(debug_str.contains("ClayCode"));
630    }
631
632    #[test]
633    fn test_new_default() {
634        let clay_default = ClayCode::new_default(4, 2).unwrap();
635        let clay_explicit = ClayCode::new(4, 2, 4 + 2 - 1).unwrap();
636        assert_eq!(clay_default.k, clay_explicit.k);
637        assert_eq!(clay_default.m, clay_explicit.m);
638        assert_eq!(clay_default.d, clay_explicit.d);
639        assert_eq!(clay_default.q, clay_explicit.q);
640        assert_eq!(clay_default.t, clay_explicit.t);
641        assert_eq!(clay_default.sub_chunk_no, clay_explicit.sub_chunk_no);
642        assert_eq!(clay_default.beta, clay_explicit.beta);
643
644        // Also test with different params
645        let clay_default2 = ClayCode::new_default(10, 4).unwrap();
646        let clay_explicit2 = ClayCode::new(10, 4, 13).unwrap();
647        assert_eq!(clay_default2.d, clay_explicit2.d);
648        assert_eq!(clay_default2.sub_chunk_no, clay_explicit2.sub_chunk_no);
649    }
650
651    #[test]
652    fn test_decode_empty_available_with_erasures() {
653        let clay = ClayCode::new(4, 2, 5).unwrap();
654        let available: HashMap<usize, Vec<u8>> = HashMap::new();
655        let result = clay.decode(&available, &[0]);
656        assert!(
657            matches!(result, Err(ClayError::InvalidParameters(_))),
658            "Expected InvalidParameters error when available is empty but erasures is non-empty, got {:?}",
659            result
660        );
661    }
662
663    // ============ Adversarial Tests ============
664
665    #[test]
666    fn test_decode_too_many_erasures() {
667        let clay = ClayCode::new(4, 2, 5).unwrap();
668        let data: Vec<u8> = (0..128).map(|i| (i % 256) as u8).collect();
669        let chunks = clay.encode(&data);
670
671        // Try to decode with 3 erasures (more than m=2)
672        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
673        for (i, chunk) in chunks.iter().enumerate() {
674            if i > 2 {
675                available.insert(i, chunk.clone());
676            }
677        }
678
679        let result = clay.decode(&available, &[0, 1, 2]);
680        assert!(
681            matches!(result, Err(ClayError::TooManyErasures { max: 2, actual: 3 })),
682            "Expected TooManyErasures error, got {:?}",
683            result
684        );
685    }
686
687    #[test]
688    fn test_decode_inconsistent_chunk_sizes() {
689        let clay = ClayCode::new(4, 2, 5).unwrap();
690        let data: Vec<u8> = (0..128).map(|i| (i % 256) as u8).collect();
691        let chunks = clay.encode(&data);
692
693        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
694        for (i, chunk) in chunks.iter().enumerate() {
695            if i != 0 {
696                if i == 5 {
697                    // Deliberately corrupt chunk 5 with wrong size
698                    let mut bad_chunk = chunk.clone();
699                    bad_chunk.push(0); // Add extra byte
700                    available.insert(i, bad_chunk);
701                } else {
702                    available.insert(i, chunk.clone());
703                }
704            }
705        }
706
707        let result = clay.decode(&available, &[0]);
708        // Either InconsistentChunkSizes or InvalidChunkSize depending on iteration order
709        assert!(
710            matches!(result, Err(ClayError::InconsistentChunkSizes { .. }))
711                || matches!(result, Err(ClayError::InvalidChunkSize { .. })),
712            "Expected InconsistentChunkSizes or InvalidChunkSize error, got {:?}",
713            result
714        );
715    }
716
717    #[test]
718    fn test_decode_invalid_chunk_index() {
719        let clay = ClayCode::new(4, 2, 5).unwrap();
720        let data: Vec<u8> = (0..128).collect();
721        let chunks = clay.encode(&data);
722
723        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
724        for (i, chunk) in chunks.iter().enumerate() {
725            available.insert(i, chunk.clone());
726        }
727        // Add a chunk with invalid index
728        available.insert(100, vec![0u8; chunks[0].len()]);
729
730        let result = clay.decode(&available, &[]);
731        assert!(
732            matches!(result, Err(ClayError::InvalidParameters(_))),
733            "Expected InvalidParameters error for out-of-range index, got {:?}",
734            result
735        );
736    }
737
738    #[test]
739    fn test_decode_invalid_erasure_index() {
740        let clay = ClayCode::new(4, 2, 5).unwrap();
741        let data: Vec<u8> = (0..128).collect();
742        let chunks = clay.encode(&data);
743
744        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
745        for (i, chunk) in chunks.iter().enumerate() {
746            if i != 0 {
747                available.insert(i, chunk.clone());
748            }
749        }
750
751        // Declare an out-of-range erasure
752        let result = clay.decode(&available, &[100]);
753        assert!(
754            matches!(result, Err(ClayError::InvalidParameters(_))),
755            "Expected InvalidParameters error for out-of-range erasure, got {:?}",
756            result
757        );
758    }
759
760    #[test]
761    fn test_decode_available_erasure_overlap() {
762        let clay = ClayCode::new(4, 2, 5).unwrap();
763        let data: Vec<u8> = (0..128).collect();
764        let chunks = clay.encode(&data);
765
766        // Include node 0 in both available AND erasures - should be an error
767        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
768        for (i, chunk) in chunks.iter().enumerate() {
769            available.insert(i, chunk.clone());
770        }
771
772        let result = clay.decode(&available, &[0]);
773        assert!(
774            matches!(result, Err(ClayError::InvalidParameters(ref msg)) if msg.contains("both")),
775            "Expected InvalidParameters error for overlap, got {:?}",
776            result
777        );
778    }
779
780    #[test]
781    fn test_decode_wrong_available_count() {
782        let clay = ClayCode::new(4, 2, 5).unwrap();
783        let data: Vec<u8> = (0..128).collect();
784        let chunks = clay.encode(&data);
785
786        // Provide too few chunks for the declared erasures
787        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
788        for (i, chunk) in chunks.iter().enumerate() {
789            if i > 1 {
790                available.insert(i, chunk.clone());
791            }
792        }
793
794        // Say only node 0 is erased, but we only have 4 chunks (should have 5)
795        let result = clay.decode(&available, &[0]);
796        assert!(
797            matches!(result, Err(ClayError::InvalidParameters(ref msg)) if msg.contains("Expected")),
798            "Expected InvalidParameters error for wrong count, got {:?}",
799            result
800        );
801    }
802}