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::{chunk_size as encoded_chunk_size, encode as encode_chunks, encode_into as encode_chunks_into};
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    /// Bytes in each chunk an encode of `data_len` bytes produces
183    pub fn chunk_size(&self, data_len: usize) -> usize {
184        encoded_chunk_size(&self.encode_params(), data_len)
185    }
186
187    /// Encode data into n caller-owned chunks of `chunk_size(data.len())` bytes each
188    ///
189    /// The allocation-free form of encode: the data goes straight into the
190    /// first k chunks and the parity into the last m, so a caller that encodes
191    /// into its own buffers holds no second copy of the coded bytes.
192    pub fn encode_into(&self, data: &[u8], chunks: &mut [&mut [u8]]) -> Result<(), ClayError> {
193        encode_chunks_into(&self.encode_params(), &self.rs, data, chunks)
194    }
195
196    /// Decode data from available chunks
197    ///
198    /// # Parameters
199    /// - `available`: Map from chunk index to chunk data
200    /// - `erasures`: Set of erased chunk indices
201    ///
202    /// # Returns
203    /// Recovered original data, or error if decoding fails
204    pub fn decode(
205        &self,
206        available: &HashMap<usize, Vec<u8>>,
207        erasures: &[usize],
208    ) -> Result<Vec<u8>, ClayError> {
209        decode_chunks(&self.encode_params(), &self.rs, available, erasures)
210    }
211
212    /// Recover original data from chunks indexed by node, `None` for erasures
213    ///
214    /// The zero-copy form of decode. The caller lends the chunks it already
215    /// holds instead of building a map of owned buffers. Needs exactly n slots.
216    pub fn decode_rows(&self, chunks: &[Option<&[u8]>]) -> Result<Vec<u8>, ClayError> {
217        decode_rows_impl(&self.encode_params(), &self.rs, chunks)
218    }
219
220    /// Determine minimum sub-chunks needed to repair a lost node
221    ///
222    /// # Parameters
223    /// - `lost_node`: Index of the lost node (0 to n-1)
224    /// - `available`: Available node indices
225    ///
226    /// # Returns
227    /// Vector of (helper_node_idx, sub_chunk_indices) where sub_chunk_indices
228    /// is a vector of the specific sub-chunk indices needed from that helper.
229    /// The repair() function expects helper data to contain these sub-chunks
230    /// concatenated in the ORDER they appear in sub_chunk_indices.
231    pub fn minimum_to_repair(
232        &self,
233        lost_node: usize,
234        available: &[usize],
235    ) -> Result<Vec<(usize, Vec<usize>)>, ClayError> {
236        min_repair(&self.encode_params(), lost_node, available)
237    }
238
239    /// Repair a lost chunk using partial data from helper nodes
240    ///
241    /// # Parameters
242    /// - `lost_node`: Index of the lost node (0 to n-1)
243    /// - `helper_data`: Map from helper node index to partial chunk data.
244    ///   Each helper's data must be the concatenation of sub-chunks at the
245    ///   indices returned by minimum_to_repair(), in that exact order.
246    /// - `chunk_size`: Full chunk size
247    ///
248    /// # Returns
249    /// The recovered full chunk, or error if repair fails
250    pub fn repair(
251        &self,
252        lost_node: usize,
253        helper_data: &HashMap<usize, Vec<u8>>,
254        chunk_size: usize,
255    ) -> Result<Vec<u8>, ClayError> {
256        repair_chunk(&self.encode_params(), &self.rs, lost_node, helper_data, chunk_size)
257    }
258
259    /// Repair a lost chunk from helper partials indexed by node
260    ///
261    /// The zero-copy form of repair. One slot per node, `None` where a node is
262    /// not contributing, and each entry holds that helper's sub-chunks in the
263    /// order minimum_to_repair returned them.
264    pub fn repair_rows(
265        &self,
266        lost_node: usize,
267        helpers: &[Option<&[u8]>],
268        chunk_size: usize,
269    ) -> Result<Vec<u8>, ClayError> {
270        repair_rows_impl(&self.encode_params(), &self.rs, lost_node, helpers, chunk_size)
271    }
272
273    /// Calculate normalized repair bandwidth
274    ///
275    /// This is the ratio of data downloaded for repair to the size of the
276    /// repaired chunk. For Clay codes, this is d / (k * q).
277    pub fn normalized_repair_bandwidth(&self) -> f64 {
278        (self.d as f64) / ((self.k as f64) * (self.d - self.k + 1) as f64)
279    }
280}
281
282/// Integer power function with overflow checking
283fn checked_pow(base: usize, exp: usize) -> Option<usize> {
284    let mut result: usize = 1;
285    let mut b = base;
286    let mut e = exp;
287    while e > 0 {
288        if e & 1 == 1 {
289            result = result.checked_mul(b)?;
290        }
291        e >>= 1;
292        if e > 0 {
293            b = b.checked_mul(b)?;
294        }
295    }
296    Some(result)
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_basic_encode_decode() {
305        let clay = ClayCode::new(4, 2, 5).unwrap();
306        let data = b"Test data for Clay codes - not empty!";
307        let chunks = clay.encode(data);
308        assert_eq!(chunks.len(), 6); // k + m = 6
309
310        // Decode with all chunks
311        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
312        for (i, chunk) in chunks.iter().enumerate() {
313            available.insert(i, chunk.clone());
314        }
315        let decoded = clay.decode(&available, &[]).unwrap();
316
317        // Check prefix matches (may have padding)
318        assert_eq!(&decoded[..data.len()], &data[..]);
319    }
320
321    #[test]
322    fn test_decode_with_erasures() {
323        let clay = ClayCode::new(4, 2, 5).unwrap();
324        let data = b"Test data for Clay codes - testing erasure recovery!";
325        let chunks = clay.encode(data);
326
327        // Lose node 0
328        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
329        for (i, chunk) in chunks.iter().enumerate() {
330            if i != 0 {
331                available.insert(i, chunk.clone());
332            }
333        }
334        let decoded = clay.decode(&available, &[0]).unwrap();
335        assert_eq!(&decoded[..data.len()], &data[..]);
336
337        // Lose node 5 (parity)
338        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
339        for (i, chunk) in chunks.iter().enumerate() {
340            if i != 5 {
341                available.insert(i, chunk.clone());
342            }
343        }
344        let decoded = clay.decode(&available, &[5]).unwrap();
345        assert_eq!(&decoded[..data.len()], &data[..]);
346
347        // Lose two nodes
348        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
349        for (i, chunk) in chunks.iter().enumerate() {
350            if i != 0 && i != 5 {
351                available.insert(i, chunk.clone());
352            }
353        }
354        let decoded = clay.decode(&available, &[0, 5]).unwrap();
355        assert_eq!(&decoded[..data.len()], &data[..]);
356    }
357
358    #[test]
359    fn test_parameters() {
360        // Test (6, 4, 5) - from paper
361        let clay = ClayCode::new(4, 2, 5).unwrap();
362        assert_eq!(clay.q, 2);
363        assert_eq!(clay.t, 3);
364        assert_eq!(clay.sub_chunk_no, 8); // 2^3 = 8
365        assert_eq!(clay.beta, 4); // 8 / 2 = 4
366
367        // Test (14, 10, 13)
368        let clay2 = ClayCode::new(10, 4, 13).unwrap();
369        assert_eq!(clay2.q, 4);
370        assert_eq!(clay2.t, 4);
371        assert_eq!(clay2.sub_chunk_no, 256); // 4^4 = 256
372        assert_eq!(clay2.beta, 64); // 256 / 4 = 64
373    }
374
375    #[test]
376    fn test_minimum_to_repair() {
377        let clay = ClayCode::new(4, 2, 5).unwrap();
378        let available: Vec<usize> = vec![1, 2, 3, 4, 5];
379        let helper_info = clay.minimum_to_repair(0, &available).unwrap();
380
381        // Should return d = 5 helpers
382        assert_eq!(helper_info.len(), 5);
383
384        // Each helper should provide β = 4 sub-chunks
385        for (_, indices) in &helper_info {
386            assert_eq!(indices.len(), 4);
387        }
388    }
389
390    #[test]
391    fn test_repair_bandwidth_verification() {
392        // This test verifies we're actually using Clay's repair advantage
393        let clay = ClayCode::new(4, 2, 5).unwrap();
394        let data = b"Test data for bandwidth verification of Clay codes repair!";
395        let chunks = clay.encode(data);
396        let chunk_size = chunks[0].len();
397
398        // Get minimum data needed to repair node 0
399        let available: Vec<usize> = vec![1, 2, 3, 4, 5];
400        let helper_info = clay.minimum_to_repair(0, &available).unwrap();
401
402        // Calculate total sub-chunks requested
403        let sub_chunk_size = chunk_size / clay.sub_chunk_no;
404        let total_repair_subchunks: usize = helper_info
405            .iter()
406            .map(|(_, indices)| indices.len())
407            .sum();
408        let total_repair_bytes = total_repair_subchunks * sub_chunk_size;
409
410        let full_decode_bytes = clay.k * chunk_size;
411
412        // Clay repair should use significantly less data
413        let ratio = total_repair_bytes as f64 / full_decode_bytes as f64;
414        println!(
415            "Repair bandwidth: {} bytes, Full decode: {} bytes, Ratio: {:.3}",
416            total_repair_bytes, full_decode_bytes, ratio
417        );
418
419        assert!(
420            total_repair_bytes < full_decode_bytes * 7 / 10,
421            "Repair bandwidth {} should be < 70% of full decode {}",
422            total_repair_bytes,
423            full_decode_bytes
424        );
425    }
426
427    #[test]
428    fn test_repair_correctness() {
429        let clay = ClayCode::new(4, 2, 5).unwrap();
430        let data = b"Test data for repair correctness verification!!!!";
431        let chunks = clay.encode(data);
432        let chunk_size = chunks[0].len();
433        let sub_chunk_size = chunk_size / clay.sub_chunk_no;
434
435        // Test repairing each node
436        for lost_node in 0..clay.n {
437            let available: Vec<usize> = (0..clay.n).filter(|&i| i != lost_node).collect();
438            let helper_info = clay.minimum_to_repair(lost_node, &available).unwrap();
439
440            // Extract only the required sub-chunks from each helper
441            let mut partial_data: HashMap<usize, Vec<u8>> = HashMap::new();
442            for (helper_idx, indices) in &helper_info {
443                let mut helper_partial = Vec::new();
444                for &sc_idx in indices {
445                    let start_byte = sc_idx * sub_chunk_size;
446                    let end_byte = (sc_idx + 1) * sub_chunk_size;
447                    helper_partial.extend_from_slice(&chunks[*helper_idx][start_byte..end_byte]);
448                }
449                partial_data.insert(*helper_idx, helper_partial);
450            }
451
452            // Repair using ONLY partial data
453            let recovered = clay.repair(lost_node, &partial_data, chunk_size).unwrap();
454
455            // Verify recovered chunk matches original
456            assert_eq!(
457                recovered, chunks[lost_node],
458                "Repair failed for node {}",
459                lost_node
460            );
461        }
462    }
463
464    #[test]
465    fn test_various_parameters() {
466        // Test different parameter combinations from the paper
467        let params = vec![
468            (4, 2, 5),   // (6, 4, 5) - α=8, β=4
469            (9, 3, 11),  // (12, 9, 11) - α=81, β=27
470            (10, 4, 13), // (14, 10, 13) - α=256, β=64
471        ];
472
473        for (k, m, d) in params {
474            let clay = ClayCode::new(k, m, d).unwrap();
475            let data_size = k * clay.sub_chunk_no * 2;
476            let data: Vec<u8> = (0..data_size).map(|i| (i % 256) as u8).collect();
477            let chunks = clay.encode(&data);
478
479            // Test decode with one erasure
480            let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
481            for (i, chunk) in chunks.iter().enumerate() {
482                if i != 0 {
483                    available.insert(i, chunk.clone());
484                }
485            }
486            let decoded = clay.decode(&available, &[0]).unwrap();
487            assert_eq!(
488                &decoded[..data.len()],
489                &data[..],
490                "Failed for params ({}, {}, {})",
491                k,
492                m,
493                d
494            );
495        }
496    }
497
498    #[test]
499    fn test_repair_all_nodes_various_params() {
500        let params = vec![(4, 2, 5), (9, 3, 11)];
501
502        for (k, m, d) in params {
503            let clay = ClayCode::new(k, m, d).unwrap();
504            let data_size = k * clay.sub_chunk_no;
505            let data: Vec<u8> = (0..data_size).map(|i| ((i * 7 + 13) % 256) as u8).collect();
506            let chunks = clay.encode(&data);
507            let chunk_size = chunks[0].len();
508            let sub_chunk_size = chunk_size / clay.sub_chunk_no;
509
510            for lost_node in 0..clay.n {
511                let available: Vec<usize> = (0..clay.n).filter(|&i| i != lost_node).collect();
512                let helper_info = clay.minimum_to_repair(lost_node, &available).unwrap();
513
514                let mut partial_data: HashMap<usize, Vec<u8>> = HashMap::new();
515                for (helper_idx, indices) in &helper_info {
516                    let mut helper_partial = Vec::new();
517                    for &sc_idx in indices {
518                        let start_byte = sc_idx * sub_chunk_size;
519                        let end_byte = (sc_idx + 1) * sub_chunk_size;
520                        helper_partial.extend_from_slice(&chunks[*helper_idx][start_byte..end_byte]);
521                    }
522                    partial_data.insert(*helper_idx, helper_partial);
523                }
524
525                let recovered = clay.repair(lost_node, &partial_data, chunk_size).unwrap();
526                assert_eq!(
527                    recovered, chunks[lost_node],
528                    "Repair failed for node {} with params ({}, {}, {})",
529                    lost_node, k, m, d
530                );
531            }
532        }
533    }
534
535    #[test]
536    fn test_decode_max_erasures() {
537        let clay = ClayCode::new(4, 2, 5).unwrap();
538        let data: Vec<u8> = (0..256).map(|i| (i % 256) as u8).collect();
539        let chunks = clay.encode(&data);
540
541        // Lose exactly m = 2 nodes in different patterns
542        let patterns = vec![vec![0, 5], vec![0, 1], vec![4, 5], vec![1, 3]];
543
544        for erasures in patterns {
545            let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
546            for (i, chunk) in chunks.iter().enumerate() {
547                if !erasures.contains(&i) {
548                    available.insert(i, chunk.clone());
549                }
550            }
551            let decoded = clay.decode(&available, &erasures).unwrap();
552            assert_eq!(
553                &decoded[..data.len()],
554                &data[..],
555                "Failed for erasures {:?}",
556                erasures
557            );
558        }
559    }
560
561    #[test]
562    fn test_normalized_repair_bandwidth() {
563        let test_cases = vec![
564            ((4, 2, 5), 0.625),
565            ((9, 3, 11), 0.407),
566            ((10, 4, 13), 0.325),
567        ];
568
569        for ((k, m, d), expected) in test_cases {
570            let clay = ClayCode::new(k, m, d).unwrap();
571            let actual = clay.normalized_repair_bandwidth();
572            assert!(
573                (actual - expected).abs() < 0.01,
574                "Expected {}, got {} for ({}, {}, {})",
575                expected,
576                actual,
577                k,
578                m,
579                d
580            );
581        }
582    }
583
584    #[test]
585    fn test_random_data() {
586        use rand::Rng;
587        let mut rng = rand::thread_rng();
588
589        let clay = ClayCode::new(4, 2, 5).unwrap();
590        let data_size = clay.k * clay.sub_chunk_no * 4;
591        let data: Vec<u8> = (0..data_size).map(|_| rng.gen()).collect();
592        let chunks = clay.encode(&data);
593
594        // Test full decode
595        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
596        for (i, chunk) in chunks.iter().enumerate() {
597            available.insert(i, chunk.clone());
598        }
599        let decoded = clay.decode(&available, &[]).unwrap();
600        assert_eq!(&decoded[..data.len()], &data[..]);
601
602        // Test decode with erasure
603        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
604        for (i, chunk) in chunks.iter().enumerate() {
605            if i != 2 {
606                available.insert(i, chunk.clone());
607            }
608        }
609        let decoded = clay.decode(&available, &[2]).unwrap();
610        assert_eq!(&decoded[..data.len()], &data[..]);
611    }
612
613    #[test]
614    fn test_checked_pow_overflow() {
615        // Test that checked_pow handles overflow gracefully
616        assert!(checked_pow(2, 63).is_some());
617        assert!(checked_pow(2, 64).is_none()); // Would overflow
618        assert!(checked_pow(10, 20).is_none()); // Would overflow
619    }
620
621    #[test]
622    fn test_invalid_parameters() {
623        // k must be >= 1
624        assert!(ClayCode::new(0, 2, 1).is_err());
625
626        // m must be >= 1
627        assert!(ClayCode::new(4, 0, 3).is_err());
628
629        // d must be in range
630        assert!(ClayCode::new(4, 2, 4).is_err()); // d < k+1
631        assert!(ClayCode::new(4, 2, 6).is_err()); // d > k+m-1
632    }
633
634    #[test]
635    fn test_clone_and_debug() {
636        let clay = ClayCode::new(4, 2, 5).unwrap();
637        let clay2 = clay.clone();
638        assert_eq!(clay2.k, clay.k);
639        assert_eq!(clay2.m, clay.m);
640        assert_eq!(clay2.d, clay.d);
641        // Verify Debug is implemented
642        let debug_str = format!("{:?}", clay);
643        assert!(debug_str.contains("ClayCode"));
644    }
645
646    #[test]
647    fn test_new_default() {
648        let clay_default = ClayCode::new_default(4, 2).unwrap();
649        let clay_explicit = ClayCode::new(4, 2, 4 + 2 - 1).unwrap();
650        assert_eq!(clay_default.k, clay_explicit.k);
651        assert_eq!(clay_default.m, clay_explicit.m);
652        assert_eq!(clay_default.d, clay_explicit.d);
653        assert_eq!(clay_default.q, clay_explicit.q);
654        assert_eq!(clay_default.t, clay_explicit.t);
655        assert_eq!(clay_default.sub_chunk_no, clay_explicit.sub_chunk_no);
656        assert_eq!(clay_default.beta, clay_explicit.beta);
657
658        // Also test with different params
659        let clay_default2 = ClayCode::new_default(10, 4).unwrap();
660        let clay_explicit2 = ClayCode::new(10, 4, 13).unwrap();
661        assert_eq!(clay_default2.d, clay_explicit2.d);
662        assert_eq!(clay_default2.sub_chunk_no, clay_explicit2.sub_chunk_no);
663    }
664
665    #[test]
666    fn test_decode_empty_available_with_erasures() {
667        let clay = ClayCode::new(4, 2, 5).unwrap();
668        let available: HashMap<usize, Vec<u8>> = HashMap::new();
669        let result = clay.decode(&available, &[0]);
670        assert!(
671            matches!(result, Err(ClayError::InvalidParameters(_))),
672            "Expected InvalidParameters error when available is empty but erasures is non-empty, got {:?}",
673            result
674        );
675    }
676
677    // ============ Adversarial Tests ============
678
679    #[test]
680    fn test_decode_too_many_erasures() {
681        let clay = ClayCode::new(4, 2, 5).unwrap();
682        let data: Vec<u8> = (0..128).map(|i| (i % 256) as u8).collect();
683        let chunks = clay.encode(&data);
684
685        // Try to decode with 3 erasures (more than m=2)
686        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
687        for (i, chunk) in chunks.iter().enumerate() {
688            if i > 2 {
689                available.insert(i, chunk.clone());
690            }
691        }
692
693        let result = clay.decode(&available, &[0, 1, 2]);
694        assert!(
695            matches!(result, Err(ClayError::TooManyErasures { max: 2, actual: 3 })),
696            "Expected TooManyErasures error, got {:?}",
697            result
698        );
699    }
700
701    #[test]
702    fn test_decode_inconsistent_chunk_sizes() {
703        let clay = ClayCode::new(4, 2, 5).unwrap();
704        let data: Vec<u8> = (0..128).map(|i| (i % 256) as u8).collect();
705        let chunks = clay.encode(&data);
706
707        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
708        for (i, chunk) in chunks.iter().enumerate() {
709            if i != 0 {
710                if i == 5 {
711                    // Deliberately corrupt chunk 5 with wrong size
712                    let mut bad_chunk = chunk.clone();
713                    bad_chunk.push(0); // Add extra byte
714                    available.insert(i, bad_chunk);
715                } else {
716                    available.insert(i, chunk.clone());
717                }
718            }
719        }
720
721        let result = clay.decode(&available, &[0]);
722        // Either InconsistentChunkSizes or InvalidChunkSize depending on iteration order
723        assert!(
724            matches!(result, Err(ClayError::InconsistentChunkSizes { .. }))
725                || matches!(result, Err(ClayError::InvalidChunkSize { .. })),
726            "Expected InconsistentChunkSizes or InvalidChunkSize error, got {:?}",
727            result
728        );
729    }
730
731    #[test]
732    fn test_decode_invalid_chunk_index() {
733        let clay = ClayCode::new(4, 2, 5).unwrap();
734        let data: Vec<u8> = (0..128).collect();
735        let chunks = clay.encode(&data);
736
737        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
738        for (i, chunk) in chunks.iter().enumerate() {
739            available.insert(i, chunk.clone());
740        }
741        // Add a chunk with invalid index
742        available.insert(100, vec![0u8; chunks[0].len()]);
743
744        let result = clay.decode(&available, &[]);
745        assert!(
746            matches!(result, Err(ClayError::InvalidParameters(_))),
747            "Expected InvalidParameters error for out-of-range index, got {:?}",
748            result
749        );
750    }
751
752    #[test]
753    fn test_decode_invalid_erasure_index() {
754        let clay = ClayCode::new(4, 2, 5).unwrap();
755        let data: Vec<u8> = (0..128).collect();
756        let chunks = clay.encode(&data);
757
758        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
759        for (i, chunk) in chunks.iter().enumerate() {
760            if i != 0 {
761                available.insert(i, chunk.clone());
762            }
763        }
764
765        // Declare an out-of-range erasure
766        let result = clay.decode(&available, &[100]);
767        assert!(
768            matches!(result, Err(ClayError::InvalidParameters(_))),
769            "Expected InvalidParameters error for out-of-range erasure, got {:?}",
770            result
771        );
772    }
773
774    #[test]
775    fn test_decode_available_erasure_overlap() {
776        let clay = ClayCode::new(4, 2, 5).unwrap();
777        let data: Vec<u8> = (0..128).collect();
778        let chunks = clay.encode(&data);
779
780        // Include node 0 in both available AND erasures - should be an error
781        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
782        for (i, chunk) in chunks.iter().enumerate() {
783            available.insert(i, chunk.clone());
784        }
785
786        let result = clay.decode(&available, &[0]);
787        assert!(
788            matches!(result, Err(ClayError::InvalidParameters(ref msg)) if msg.contains("both")),
789            "Expected InvalidParameters error for overlap, got {:?}",
790            result
791        );
792    }
793
794    #[test]
795    fn test_decode_wrong_available_count() {
796        let clay = ClayCode::new(4, 2, 5).unwrap();
797        let data: Vec<u8> = (0..128).collect();
798        let chunks = clay.encode(&data);
799
800        // Provide too few chunks for the declared erasures
801        let mut available: HashMap<usize, Vec<u8>> = HashMap::new();
802        for (i, chunk) in chunks.iter().enumerate() {
803            if i > 1 {
804                available.insert(i, chunk.clone());
805            }
806        }
807
808        // Say only node 0 is erased, but we only have 4 chunks (should have 5)
809        let result = clay.decode(&available, &[0]);
810        assert!(
811            matches!(result, Err(ClayError::InvalidParameters(ref msg)) if msg.contains("Expected")),
812            "Expected InvalidParameters error for wrong count, got {:?}",
813            result
814        );
815    }
816}