Skip to main content

commonware_cryptography/reed_solomon/rate/
rate_low.rs

1use crate::reed_solomon::{
2    engine::{self, Engine, GF_MODULUS, GF_ORDER, SHARD_CHUNK_BYTES},
3    rate::{DecoderWork, EncoderWork, Rate, RateDecoder, RateEncoder},
4    DecoderResult, EncoderResult, Error,
5};
6use core::marker::PhantomData;
7
8// ======================================================================
9// LowRate - PUBLIC
10
11/// Reed-Solomon encoder/decoder generator using only low rate.
12pub struct LowRate<E: Engine>(PhantomData<E>);
13
14impl<E: Engine> Rate<E> for LowRate<E> {
15    type RateEncoder = LowRateEncoder<E>;
16    type RateDecoder = LowRateDecoder<E>;
17
18    fn supports(original_count: usize, recovery_count: usize) -> bool {
19        original_count > 0
20            && recovery_count > 0
21            && original_count < GF_ORDER
22            && recovery_count < GF_ORDER
23            && original_count.next_power_of_two() + recovery_count <= GF_ORDER
24    }
25}
26
27// ======================================================================
28// LowRateEncoder - PUBLIC
29
30/// Reed-Solomon encoder using only low rate.
31pub struct LowRateEncoder<E: Engine> {
32    engine: E,
33    work: EncoderWork,
34}
35
36impl<E: Engine> RateEncoder<E> for LowRateEncoder<E> {
37    type Rate = LowRate<E>;
38
39    fn add_original_shard<T: AsRef<[u8]>>(&mut self, original_shard: T) -> Result<(), Error> {
40        self.work.add_original_shard(original_shard)
41    }
42
43    fn encode(&mut self) -> Result<EncoderResult<'_>, Error> {
44        let (mut work, original_count, recovery_count) = self.work.encode_begin()?;
45        let chunk_size = original_count.next_power_of_two();
46        let engine = &self.engine;
47
48        // ZEROPAD ORIGINAL
49
50        work.zero(original_count..chunk_size);
51
52        // IFFT - ORIGINAL
53
54        engine.ifft(&mut work, 0, chunk_size, original_count, 0);
55
56        // COPY IFFT RESULT TO OTHER CHUNKS
57
58        let mut chunk_start = chunk_size;
59        while chunk_start < recovery_count {
60            work.copy_within(0, chunk_start, chunk_size);
61            chunk_start += chunk_size;
62        }
63
64        // FFT - FULL CHUNKS
65
66        let mut chunk_start = 0;
67        while chunk_start + chunk_size <= recovery_count {
68            engine::fft_skew_end(engine, &mut work, chunk_start, chunk_size, chunk_size);
69            chunk_start += chunk_size;
70        }
71
72        // FFT - FINAL PARTIAL CHUNK
73
74        let last_count = recovery_count % chunk_size;
75        if last_count > 0 {
76            engine::fft_skew_end(engine, &mut work, chunk_start, chunk_size, last_count);
77        }
78
79        // UNDO LAST CHUNK ENCODING
80
81        self.work.undo_last_chunk_encoding();
82
83        // DONE
84
85        Ok(EncoderResult::new(&mut self.work))
86    }
87
88    fn into_parts(self) -> (E, EncoderWork) {
89        (self.engine, self.work)
90    }
91
92    fn new(
93        original_count: usize,
94        recovery_count: usize,
95        shard_bytes: usize,
96        engine: E,
97        work: Option<EncoderWork>,
98    ) -> Result<Self, Error> {
99        let mut work = work.unwrap_or_default();
100        Self::reset_work(original_count, recovery_count, shard_bytes, &mut work)?;
101        Ok(Self { engine, work })
102    }
103
104    fn reset(
105        &mut self,
106        original_count: usize,
107        recovery_count: usize,
108        shard_bytes: usize,
109    ) -> Result<(), Error> {
110        Self::reset_work(original_count, recovery_count, shard_bytes, &mut self.work)
111    }
112}
113
114// ======================================================================
115// LowRateEncoder - PRIVATE
116
117impl<E: Engine> LowRateEncoder<E> {
118    fn reset_work(
119        original_count: usize,
120        recovery_count: usize,
121        shard_bytes: usize,
122        work: &mut EncoderWork,
123    ) -> Result<(), Error> {
124        Self::validate(original_count, recovery_count, shard_bytes)?;
125        work.reset(
126            original_count,
127            recovery_count,
128            shard_bytes,
129            Self::work_count(original_count, recovery_count),
130        );
131        Ok(())
132    }
133
134    fn work_count(original_count: usize, recovery_count: usize) -> usize {
135        assert!(Self::supports(original_count, recovery_count));
136
137        let chunk_size = original_count.next_power_of_two();
138
139        recovery_count.next_multiple_of(chunk_size)
140    }
141}
142
143// ======================================================================
144// LowRateDecoder - PUBLIC
145
146/// Reed-Solomon decoder using only low rate.
147pub struct LowRateDecoder<E: Engine> {
148    engine: E,
149    work: DecoderWork,
150}
151
152impl<E: Engine> RateDecoder<E> for LowRateDecoder<E> {
153    type Rate = LowRate<E>;
154
155    fn add_original_shard<T: AsRef<[u8]>>(
156        &mut self,
157        index: usize,
158        original_shard: T,
159    ) -> Result<(), Error> {
160        self.work.add_original_shard(index, original_shard)
161    }
162
163    fn add_recovery_shard<T: AsRef<[u8]>>(
164        &mut self,
165        index: usize,
166        recovery_shard: T,
167    ) -> Result<(), Error> {
168        self.work.add_recovery_shard(index, recovery_shard)
169    }
170
171    fn decode(&mut self, compute_recovery: bool) -> Result<Option<DecoderResult<'_>>, Error> {
172        let Some((mut work, original_count, recovery_count, received)) =
173            self.work.decode_begin()?
174        else {
175            // Every original was provided: nothing to reconstruct. Clear the received state and
176            // report nothing.
177            self.work.reset_received();
178            return Ok(None);
179        };
180
181        let chunk_size = original_count.next_power_of_two();
182        let recovery_end = chunk_size + recovery_count;
183        let work_count = work.len();
184
185        // ERASURE LOCATIONS
186
187        let mut erasures = [0; GF_ORDER];
188
189        for i in 0..original_count {
190            if !received[i] {
191                erasures[i] = 1;
192            }
193        }
194
195        for i in chunk_size..recovery_end {
196            if !received[i] {
197                erasures[i] = 1;
198            }
199        }
200
201        erasures[recovery_end..].fill(1);
202
203        // EVALUATE POLYNOMIAL
204
205        E::eval_poly(&mut erasures, GF_ORDER);
206
207        // MULTIPLY SHARDS
208
209        // work[               .. original_count] = original * erasures
210        // work[original_count .. chunk_size    ] = 0
211        // work[chunk_size     .. original_end  ] = recovery * erasures
212        // work[recovery_end   ..               ] = 0
213
214        for i in 0..original_count {
215            if received[i] {
216                self.engine.mul(&mut work[i], erasures[i]);
217            } else {
218                work[i].fill([0; SHARD_CHUNK_BYTES]);
219            }
220        }
221
222        work.zero(original_count..chunk_size);
223
224        for i in chunk_size..recovery_end {
225            if received[i] {
226                self.engine.mul(&mut work[i], erasures[i]);
227            } else {
228                work[i].fill([0; SHARD_CHUNK_BYTES]);
229            }
230        }
231
232        work.zero(recovery_end..);
233
234        // IFFT / FORMAL DERIVATIVE / FFT
235
236        self.engine.ifft(&mut work, 0, work_count, recovery_end, 0);
237        engine::formal_derivative(&mut work);
238        self.engine.fft(&mut work, 0, work_count, recovery_end, 0);
239
240        // REVEAL ERASURES
241
242        for i in 0..original_count {
243            if !received[i] {
244                self.engine.mul(&mut work[i], GF_MODULUS - erasures[i]);
245            }
246        }
247
248        // REVEAL ERASURES (RECOVERY)
249        //
250        // Only when the caller passed `compute_recovery = true` to `decode`. Recovery shards
251        // live at `work[chunk_size..recovery_end]`. Un-scale the missing ones by the inverse
252        // locator so they hold the canonical recovery values, mirroring the original reveal above.
253        // This lets `DecoderResult::recovery` return them without a separate re-encode.
254
255        if compute_recovery {
256            for i in chunk_size..recovery_end {
257                if !received[i] {
258                    self.engine.mul(&mut work[i], GF_MODULUS - erasures[i]);
259                }
260            }
261        }
262
263        // UNDO LAST CHUNK ENCODING
264
265        self.work.undo_last_chunk_encoding();
266        if compute_recovery {
267            self.work.undo_last_chunk_encoding_recovery();
268        }
269
270        // DONE
271
272        Ok(Some(DecoderResult::new(&mut self.work)))
273    }
274
275    fn into_parts(self) -> (E, DecoderWork) {
276        (self.engine, self.work)
277    }
278
279    fn new(
280        original_count: usize,
281        recovery_count: usize,
282        shard_bytes: usize,
283        engine: E,
284        work: Option<DecoderWork>,
285    ) -> Result<Self, Error> {
286        let mut work = work.unwrap_or_default();
287        Self::reset_work(original_count, recovery_count, shard_bytes, &mut work)?;
288        Ok(Self { engine, work })
289    }
290
291    fn reset(
292        &mut self,
293        original_count: usize,
294        recovery_count: usize,
295        shard_bytes: usize,
296    ) -> Result<(), Error> {
297        Self::reset_work(original_count, recovery_count, shard_bytes, &mut self.work)
298    }
299}
300
301// ======================================================================
302// LowRateDecoder - PRIVATE
303
304impl<E: Engine> LowRateDecoder<E> {
305    fn reset_work(
306        original_count: usize,
307        recovery_count: usize,
308        shard_bytes: usize,
309        work: &mut DecoderWork,
310    ) -> Result<(), Error> {
311        Self::validate(original_count, recovery_count, shard_bytes)?;
312
313        // work[..original_count     ]  =  original
314        // work[original_count_pow2..]  =  recovery
315        work.reset(
316            original_count,
317            recovery_count,
318            shard_bytes,
319            0,
320            original_count.next_power_of_two(),
321            Self::work_count(original_count, recovery_count),
322        );
323
324        Ok(())
325    }
326
327    fn work_count(original_count: usize, recovery_count: usize) -> usize {
328        assert!(Self::supports(original_count, recovery_count));
329
330        (original_count.next_power_of_two() + recovery_count).next_power_of_two()
331    }
332}
333
334// ======================================================================
335// TESTS
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::reed_solomon::test_util;
341
342    // ============================================================
343    // ROUNDTRIPS - SINGLE ROUND
344
345    #[test]
346    fn roundtrip_all_originals_missing() {
347        roundtrip_single!(
348            LowRate,
349            3,
350            3,
351            1024,
352            test_util::EITHER_3_3,
353            &[],
354            &[test_util::range(0, 3)],
355            133
356        );
357    }
358
359    #[test]
360    fn roundtrip_no_originals_missing() {
361        roundtrip_single!(
362            LowRate,
363            2,
364            3,
365            1024,
366            test_util::LOW_2_3,
367            &[test_util::index(0), test_util::index(1)],
368            &[],
369            123
370        );
371    }
372
373    #[test]
374    fn roundtrips_tiny() {
375        for (original_count, recovery_count, seed, recovery_hash) in test_util::LOW_TINY {
376            roundtrip_single!(
377                LowRate,
378                *original_count,
379                *recovery_count,
380                1024,
381                recovery_hash,
382                &[test_util::range(*recovery_count, *original_count)],
383                &[test_util::range(
384                    0,
385                    core::cmp::min(*original_count, *recovery_count)
386                )],
387                *seed,
388            );
389        }
390    }
391
392    #[test]
393    #[ignore]
394    fn roundtrip_3000_60000() {
395        roundtrip_single!(
396            LowRate,
397            3000,
398            60000,
399            crate::reed_solomon::SHARD_CHUNK_BYTES,
400            test_util::LOW_3000_60000_13,
401            &[],
402            &[test_util::range(0, 3000)],
403            13,
404        );
405    }
406
407    #[test]
408    #[ignore]
409    fn roundtrip_30000_3000() {
410        roundtrip_single!(
411            LowRate,
412            30000,
413            3000,
414            crate::reed_solomon::SHARD_CHUNK_BYTES,
415            test_util::LOW_30000_3000_15,
416            &[test_util::range(3000, 30000)],
417            &[test_util::range(0, 3000)],
418            15,
419        );
420    }
421
422    #[test]
423    #[ignore]
424    fn roundtrip_32768_32768() {
425        roundtrip_single!(
426            LowRate,
427            32768,
428            32768,
429            crate::reed_solomon::SHARD_CHUNK_BYTES,
430            test_util::EITHER_32768_32768_11,
431            &[],
432            &[test_util::range(0, 32768)],
433            11,
434        );
435    }
436
437    #[test]
438    fn roundtrip_2000_34000_shard_size_8() {
439        roundtrip_single!(
440            LowRate,
441            2000,
442            34000,
443            8,
444            test_util::LOW_2000_34000_123_8,
445            &[test_util::range(0, 2000)],
446            &[test_util::range(0, 32000)],
447            123
448        );
449    }
450
451    // ============================================================
452    // ROUNDTRIPS - TWO ROUNDS
453
454    #[test]
455    fn two_rounds_implicit_reset() {
456        roundtrip_two_rounds!(
457            LowRate,
458            false,
459            (
460                2,
461                3,
462                1024,
463                test_util::LOW_2_3,
464                &[],
465                &[test_util::index(0), test_util::index(2)],
466                123
467            ),
468            (
469                2,
470                3,
471                1024,
472                test_util::LOW_2_3_223,
473                &[],
474                &[test_util::index(1), test_util::index(2)],
475                223
476            ),
477        );
478    }
479
480    #[test]
481    fn two_rounds_explicit_reset() {
482        roundtrip_two_rounds!(
483            LowRate,
484            true,
485            (
486                2,
487                3,
488                1024,
489                test_util::LOW_2_3,
490                &[],
491                &[test_util::index(0), test_util::index(2)],
492                123
493            ),
494            (
495                2,
496                5,
497                1024,
498                test_util::LOW_2_5,
499                &[],
500                &[test_util::index(0), test_util::index(4)],
501                125
502            ),
503        );
504    }
505
506    // ============================================================
507    // LowRate
508
509    mod low_rate {
510        use crate::reed_solomon::{
511            engine::NoSimd,
512            rate::{LowRate, Rate},
513            Error, SHARD_CHUNK_BYTES,
514        };
515
516        #[test]
517        fn decoder() {
518            assert!(LowRate::<NoSimd>::decoder(
519                4096,
520                61440,
521                SHARD_CHUNK_BYTES,
522                NoSimd::new(),
523                None
524            )
525            .is_ok());
526
527            assert_eq!(
528                LowRate::<NoSimd>::decoder(61440, 4096, SHARD_CHUNK_BYTES, NoSimd::new(), None)
529                    .err(),
530                Some(Error::UnsupportedShardCount {
531                    original_count: 61440,
532                    recovery_count: 4096,
533                })
534            );
535        }
536
537        #[test]
538        fn encoder() {
539            assert!(LowRate::<NoSimd>::encoder(
540                4096,
541                61440,
542                SHARD_CHUNK_BYTES,
543                NoSimd::new(),
544                None
545            )
546            .is_ok());
547
548            assert_eq!(
549                LowRate::<NoSimd>::encoder(61440, 4096, SHARD_CHUNK_BYTES, NoSimd::new(), None)
550                    .err(),
551                Some(Error::UnsupportedShardCount {
552                    original_count: 61440,
553                    recovery_count: 4096,
554                })
555            );
556        }
557
558        #[test]
559        fn supports() {
560            assert!(!LowRate::<NoSimd>::supports(0, 1));
561            assert!(!LowRate::<NoSimd>::supports(1, 0));
562
563            assert!(LowRate::<NoSimd>::supports(4096, 61440));
564            assert!(!LowRate::<NoSimd>::supports(4096, 61441));
565            assert!(!LowRate::<NoSimd>::supports(4097, 61440));
566
567            assert!(!LowRate::<NoSimd>::supports(61440, 4096));
568
569            assert!(!LowRate::<NoSimd>::supports(usize::MAX, usize::MAX));
570        }
571
572        #[test]
573        fn validate() {
574            assert_eq!(
575                LowRate::<NoSimd>::validate(1, 1, 123).err(),
576                Some(Error::InvalidShardSize { shard_bytes: 123 })
577            );
578
579            assert!(LowRate::<NoSimd>::validate(4096, 61440, SHARD_CHUNK_BYTES).is_ok());
580
581            assert_eq!(
582                LowRate::<NoSimd>::validate(61440, 4096, SHARD_CHUNK_BYTES).err(),
583                Some(Error::UnsupportedShardCount {
584                    original_count: 61440,
585                    recovery_count: 4096,
586                })
587            );
588        }
589    }
590
591    // ============================================================
592    // LowRateEncoder
593
594    mod low_rate_encoder {
595        use crate::reed_solomon::{
596            engine::NoSimd,
597            rate::{LowRateEncoder, RateEncoder},
598            Error, SHARD_CHUNK_BYTES,
599        };
600
601        // ==================================================
602        // ERRORS
603
604        test_rate_encoder_errors! {LowRateEncoder}
605
606        // ==================================================
607        // supports
608
609        #[test]
610        fn supports() {
611            assert!(LowRateEncoder::<NoSimd>::supports(4096, 61440));
612            assert!(!LowRateEncoder::<NoSimd>::supports(61440, 4096));
613        }
614
615        // ==================================================
616        // validate
617
618        #[test]
619        fn validate() {
620            assert_eq!(
621                LowRateEncoder::<NoSimd>::validate(1, 1, 123).err(),
622                Some(Error::InvalidShardSize { shard_bytes: 123 })
623            );
624
625            assert!(LowRateEncoder::<NoSimd>::validate(4096, 61440, SHARD_CHUNK_BYTES).is_ok());
626
627            assert_eq!(
628                LowRateEncoder::<NoSimd>::validate(61440, 4096, SHARD_CHUNK_BYTES).err(),
629                Some(Error::UnsupportedShardCount {
630                    original_count: 61440,
631                    recovery_count: 4096,
632                })
633            );
634        }
635
636        // ==================================================
637        // work_count
638
639        #[test]
640        fn work_count() {
641            assert_eq!(LowRateEncoder::<NoSimd>::work_count(1, 1), 1);
642            assert_eq!(LowRateEncoder::<NoSimd>::work_count(1024, 4096), 4096);
643            assert_eq!(LowRateEncoder::<NoSimd>::work_count(1024, 4097), 5120);
644            assert_eq!(LowRateEncoder::<NoSimd>::work_count(1025, 4097), 6144);
645            assert_eq!(LowRateEncoder::<NoSimd>::work_count(32768, 32768), 32768);
646        }
647    }
648
649    // ============================================================
650    // LowRateDecoder
651
652    mod low_rate_decoder {
653        use crate::reed_solomon::{
654            engine::NoSimd,
655            rate::{LowRateDecoder, RateDecoder},
656            Error, SHARD_CHUNK_BYTES,
657        };
658
659        // ==================================================
660        // ERRORS
661
662        test_rate_decoder_errors! {LowRateDecoder}
663
664        // ==================================================
665        // supports
666
667        #[test]
668        fn supports() {
669            assert!(LowRateDecoder::<NoSimd>::supports(4096, 61440));
670            assert!(!LowRateDecoder::<NoSimd>::supports(61440, 4096));
671        }
672
673        // ==================================================
674        // validate
675
676        #[test]
677        fn validate() {
678            assert_eq!(
679                LowRateDecoder::<NoSimd>::validate(1, 1, 123).err(),
680                Some(Error::InvalidShardSize { shard_bytes: 123 })
681            );
682
683            assert!(LowRateDecoder::<NoSimd>::validate(4096, 61440, SHARD_CHUNK_BYTES).is_ok());
684
685            assert_eq!(
686                LowRateDecoder::<NoSimd>::validate(61440, 4096, SHARD_CHUNK_BYTES).err(),
687                Some(Error::UnsupportedShardCount {
688                    original_count: 61440,
689                    recovery_count: 4096,
690                })
691            );
692        }
693
694        // ==================================================
695        // work_count
696
697        #[test]
698        fn work_count() {
699            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1, 1), 2);
700            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1024, 3072), 4096);
701            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1024, 3073), 8192);
702            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1025, 2048), 4096);
703            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1025, 2049), 8192);
704            assert_eq!(LowRateDecoder::<NoSimd>::work_count(32768, 32768), 65536);
705        }
706    }
707}