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