Skip to main content

commonware_cryptography/reed_solomon/rate/
rate_low.rs

1use crate::reed_solomon::{
2    DecoderResult, EncoderResult, Error,
3    engine::{self, Engine, GF_MODULUS, GF_ORDER, SHARD_CHUNK_BYTES},
4    rate::{DecoderWork, EncoderWork, Rate, RateDecoder, RateEncoder},
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            Error, SHARD_CHUNK_BYTES,
512            engine::NoSimd,
513            rate::{LowRate, Rate},
514        };
515
516        #[test]
517        fn decoder() {
518            assert!(
519                LowRate::<NoSimd>::decoder(4096, 61440, SHARD_CHUNK_BYTES, NoSimd::new(), None)
520                    .is_ok()
521            );
522
523            assert_eq!(
524                LowRate::<NoSimd>::decoder(61440, 4096, SHARD_CHUNK_BYTES, NoSimd::new(), None)
525                    .err(),
526                Some(Error::UnsupportedShardCount {
527                    original_count: 61440,
528                    recovery_count: 4096,
529                })
530            );
531        }
532
533        #[test]
534        fn encoder() {
535            assert!(
536                LowRate::<NoSimd>::encoder(4096, 61440, SHARD_CHUNK_BYTES, NoSimd::new(), None)
537                    .is_ok()
538            );
539
540            assert_eq!(
541                LowRate::<NoSimd>::encoder(61440, 4096, SHARD_CHUNK_BYTES, NoSimd::new(), None)
542                    .err(),
543                Some(Error::UnsupportedShardCount {
544                    original_count: 61440,
545                    recovery_count: 4096,
546                })
547            );
548        }
549
550        #[test]
551        fn supports() {
552            assert!(!LowRate::<NoSimd>::supports(0, 1));
553            assert!(!LowRate::<NoSimd>::supports(1, 0));
554
555            assert!(LowRate::<NoSimd>::supports(4096, 61440));
556            assert!(!LowRate::<NoSimd>::supports(4096, 61441));
557            assert!(!LowRate::<NoSimd>::supports(4097, 61440));
558
559            assert!(!LowRate::<NoSimd>::supports(61440, 4096));
560
561            assert!(!LowRate::<NoSimd>::supports(usize::MAX, usize::MAX));
562        }
563
564        #[test]
565        fn validate() {
566            assert_eq!(
567                LowRate::<NoSimd>::validate(1, 1, 123).err(),
568                Some(Error::InvalidShardSize { shard_bytes: 123 })
569            );
570
571            assert!(LowRate::<NoSimd>::validate(4096, 61440, SHARD_CHUNK_BYTES).is_ok());
572
573            assert_eq!(
574                LowRate::<NoSimd>::validate(61440, 4096, SHARD_CHUNK_BYTES).err(),
575                Some(Error::UnsupportedShardCount {
576                    original_count: 61440,
577                    recovery_count: 4096,
578                })
579            );
580        }
581    }
582
583    // ============================================================
584    // LowRateEncoder
585
586    mod low_rate_encoder {
587        use crate::reed_solomon::{
588            Error, SHARD_CHUNK_BYTES,
589            engine::NoSimd,
590            rate::{LowRateEncoder, RateEncoder},
591        };
592
593        // ==================================================
594        // ERRORS
595
596        test_rate_encoder_errors! {LowRateEncoder}
597
598        // ==================================================
599        // supports
600
601        #[test]
602        fn supports() {
603            assert!(LowRateEncoder::<NoSimd>::supports(4096, 61440));
604            assert!(!LowRateEncoder::<NoSimd>::supports(61440, 4096));
605        }
606
607        // ==================================================
608        // validate
609
610        #[test]
611        fn validate() {
612            assert_eq!(
613                LowRateEncoder::<NoSimd>::validate(1, 1, 123).err(),
614                Some(Error::InvalidShardSize { shard_bytes: 123 })
615            );
616
617            assert!(LowRateEncoder::<NoSimd>::validate(4096, 61440, SHARD_CHUNK_BYTES).is_ok());
618
619            assert_eq!(
620                LowRateEncoder::<NoSimd>::validate(61440, 4096, SHARD_CHUNK_BYTES).err(),
621                Some(Error::UnsupportedShardCount {
622                    original_count: 61440,
623                    recovery_count: 4096,
624                })
625            );
626        }
627
628        // ==================================================
629        // work_count
630
631        #[test]
632        fn work_count() {
633            assert_eq!(LowRateEncoder::<NoSimd>::work_count(1, 1), 1);
634            assert_eq!(LowRateEncoder::<NoSimd>::work_count(1024, 4096), 4096);
635            assert_eq!(LowRateEncoder::<NoSimd>::work_count(1024, 4097), 5120);
636            assert_eq!(LowRateEncoder::<NoSimd>::work_count(1025, 4097), 6144);
637            assert_eq!(LowRateEncoder::<NoSimd>::work_count(32768, 32768), 32768);
638        }
639    }
640
641    // ============================================================
642    // LowRateDecoder
643
644    mod low_rate_decoder {
645        use crate::reed_solomon::{
646            Error, SHARD_CHUNK_BYTES,
647            engine::NoSimd,
648            rate::{LowRateDecoder, RateDecoder},
649        };
650
651        // ==================================================
652        // ERRORS
653
654        test_rate_decoder_errors! {LowRateDecoder}
655
656        // ==================================================
657        // supports
658
659        #[test]
660        fn supports() {
661            assert!(LowRateDecoder::<NoSimd>::supports(4096, 61440));
662            assert!(!LowRateDecoder::<NoSimd>::supports(61440, 4096));
663        }
664
665        // ==================================================
666        // validate
667
668        #[test]
669        fn validate() {
670            assert_eq!(
671                LowRateDecoder::<NoSimd>::validate(1, 1, 123).err(),
672                Some(Error::InvalidShardSize { shard_bytes: 123 })
673            );
674
675            assert!(LowRateDecoder::<NoSimd>::validate(4096, 61440, SHARD_CHUNK_BYTES).is_ok());
676
677            assert_eq!(
678                LowRateDecoder::<NoSimd>::validate(61440, 4096, SHARD_CHUNK_BYTES).err(),
679                Some(Error::UnsupportedShardCount {
680                    original_count: 61440,
681                    recovery_count: 4096,
682                })
683            );
684        }
685
686        // ==================================================
687        // work_count
688
689        #[test]
690        fn work_count() {
691            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1, 1), 2);
692            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1024, 3072), 4096);
693            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1024, 3073), 8192);
694            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1025, 2048), 4096);
695            assert_eq!(LowRateDecoder::<NoSimd>::work_count(1025, 2049), 8192);
696            assert_eq!(LowRateDecoder::<NoSimd>::work_count(32768, 32768), 65536);
697        }
698    }
699}