Skip to main content

commonware_cryptography/reed_solomon/rate/
rate_high.rs

1use crate::reed_solomon::{
2    DecoderResult, EncoderResult, Error, Plan,
3    engine::{self, Engine, GF_MODULUS, GF_ORDER, GfElement, SHARD_CHUNK_BYTES},
4    rate::{DecoderWork, EncoderWork, Rate, RateDecoder, RateEncoder},
5};
6#[cfg(not(feature = "std"))]
7use alloc::vec;
8use core::marker::PhantomData;
9use fixedbitset::FixedBitSet;
10
11/// Write the log erasure locator for each position in `erasures[..end]`, where
12/// `end = recovery_count.next_power_of_two() + original_count`.
13///
14/// Missing recovery shards, the padding in `recovery_count..recovery_count.next_power_of_two()`,
15/// and missing originals are erased. `erasures` must hold `end.next_power_of_two()` zeroed
16/// entries. Entries at and after `end` are unspecified.
17///
18/// # Panics
19///
20/// If `erasures.len() != end.next_power_of_two()`.
21pub(crate) fn eval_erasures<E: Engine>(
22    erasures: &mut [GfElement],
23    original_count: usize,
24    recovery_count: usize,
25    received: &FixedBitSet,
26) {
27    let chunk_size = recovery_count.next_power_of_two();
28    let end = chunk_size + original_count;
29    for i in 0..recovery_count {
30        if !received[i] {
31            erasures[i] = 1;
32        }
33    }
34    erasures[recovery_count..chunk_size].fill(1);
35    for i in chunk_size..end {
36        if !received[i] {
37            erasures[i] = 1;
38        }
39    }
40    super::eval_locator::<E>(erasures, end);
41}
42
43/// Reed-Solomon encoder/decoder generator using only high rate.
44pub struct HighRate<E: Engine>(PhantomData<E>);
45
46impl<E: Engine> Rate<E> for HighRate<E> {
47    type RateEncoder = HighRateEncoder<E>;
48    type RateDecoder = HighRateDecoder<E>;
49
50    fn supports(original_count: usize, recovery_count: usize) -> bool {
51        original_count > 0
52            && recovery_count > 0
53            && original_count < GF_ORDER
54            && recovery_count < GF_ORDER
55            && recovery_count.next_power_of_two() + original_count <= GF_ORDER
56    }
57}
58
59/// Reed-Solomon encoder using only high rate.
60pub struct HighRateEncoder<E: Engine> {
61    engine: E,
62    /// Originals at `0..original_count`. Encoding leaves the recovery shards at
63    /// `0..recovery_count`.
64    work: EncoderWork,
65}
66
67impl<E: Engine> RateEncoder<E> for HighRateEncoder<E> {
68    type Rate = HighRate<E>;
69
70    fn validate(
71        original_count: usize,
72        recovery_count: usize,
73        shard_bytes: usize,
74    ) -> Result<(), Error> {
75        Self::Rate::validate(original_count, recovery_count, shard_bytes)?;
76        super::validate_work_size(
77            shard_bytes,
78            Self::work_count(original_count, recovery_count),
79        )
80    }
81
82    fn add_original_shard<T: AsRef<[u8]>>(&mut self, original_shard: T) -> Result<(), Error> {
83        self.work.add_original_shard(original_shard)
84    }
85
86    fn encode(&mut self) -> Result<EncoderResult<'_>, Error> {
87        let (mut work, original_count, recovery_count) = self.work.encode_begin()?;
88        let chunk_size = recovery_count.next_power_of_two();
89        let engine = &self.engine;
90
91        // Zero-pad the first chunk past `first_count` and IFFT it.
92        let first_count = core::cmp::min(original_count, chunk_size);
93        work.zero(first_count..chunk_size);
94        engine::ifft_skew_end(engine, &mut work, 0, chunk_size, first_count);
95
96        if original_count > chunk_size {
97            // IFFT each full chunk and XOR it into the first chunk.
98            let mut chunk_start = chunk_size;
99            while chunk_start + chunk_size <= original_count {
100                engine::ifft_skew_end(engine, &mut work, chunk_start, chunk_size, chunk_size);
101                engine::xor_within(&mut work, 0, chunk_start, chunk_size);
102                chunk_start += chunk_size;
103            }
104
105            // Zero-pad the final partial chunk, IFFT it, and XOR it into the first chunk.
106            let last_count = original_count % chunk_size;
107            if last_count > 0 {
108                work.zero(chunk_start + last_count..);
109                engine::ifft_skew_end(engine, &mut work, chunk_start, chunk_size, last_count);
110                engine::xor_within(&mut work, 0, chunk_start, chunk_size);
111            }
112        }
113
114        engine.fft(&mut work, 0, chunk_size, recovery_count, 0);
115
116        self.work.undo_last_chunk_encoding();
117
118        Ok(EncoderResult::new(&mut self.work))
119    }
120
121    fn into_parts(self) -> (E, EncoderWork) {
122        (self.engine, self.work)
123    }
124
125    fn new(
126        original_count: usize,
127        recovery_count: usize,
128        shard_bytes: usize,
129        engine: E,
130        work: Option<EncoderWork>,
131    ) -> Result<Self, Error> {
132        let mut work = work.unwrap_or_default();
133        Self::reset_work(original_count, recovery_count, shard_bytes, &mut work)?;
134        Ok(Self { engine, work })
135    }
136
137    fn reset(
138        &mut self,
139        original_count: usize,
140        recovery_count: usize,
141        shard_bytes: usize,
142    ) -> Result<(), Error> {
143        Self::reset_work(original_count, recovery_count, shard_bytes, &mut self.work)
144    }
145}
146
147impl<E: Engine> HighRateEncoder<E> {
148    /// Validates the parameters, then resets `work` for them with [`Self::work_count`] shards.
149    ///
150    /// Returns the error from [`RateEncoder::validate`] and leaves `work` unchanged on failure.
151    fn reset_work(
152        original_count: usize,
153        recovery_count: usize,
154        shard_bytes: usize,
155        work: &mut EncoderWork,
156    ) -> Result<(), Error> {
157        Self::validate(original_count, recovery_count, shard_bytes)?;
158        work.reset(
159            original_count,
160            recovery_count,
161            shard_bytes,
162            Self::work_count(original_count, recovery_count),
163        );
164        Ok(())
165    }
166
167    /// Returns the number of shards in the working space.
168    ///
169    /// This is `original_count` rounded up to a multiple of the chunk size
170    /// `recovery_count.next_power_of_two()`, so every chunk of originals, including a
171    /// zero-padded final chunk, has room for its IFFT.
172    ///
173    /// # Panics
174    ///
175    /// Panics if the counts are not supported by [`HighRate`].
176    fn work_count(original_count: usize, recovery_count: usize) -> usize {
177        assert!(Self::supports(original_count, recovery_count));
178
179        let chunk_size = recovery_count.next_power_of_two();
180
181        original_count.next_multiple_of(chunk_size)
182    }
183}
184
185/// Reed-Solomon decoder using only high rate.
186pub struct HighRateDecoder<E: Engine> {
187    engine: E,
188    /// Recovery shards at `0..recovery_count`. Originals start at
189    /// `recovery_count.next_power_of_two()`.
190    work: DecoderWork,
191}
192
193impl<E: Engine> RateDecoder<E> for HighRateDecoder<E> {
194    type Rate = HighRate<E>;
195
196    fn validate(
197        original_count: usize,
198        recovery_count: usize,
199        shard_bytes: usize,
200    ) -> Result<(), Error> {
201        Self::Rate::validate(original_count, recovery_count, shard_bytes)?;
202        super::validate_work_size(
203            shard_bytes,
204            Self::work_count(original_count, recovery_count),
205        )
206    }
207
208    fn add_original_shard<T: AsRef<[u8]>>(
209        &mut self,
210        index: usize,
211        original_shard: T,
212    ) -> Result<(), Error> {
213        self.work.add_original_shard(index, original_shard)
214    }
215
216    fn add_recovery_shard<T: AsRef<[u8]>>(
217        &mut self,
218        index: usize,
219        recovery_shard: T,
220    ) -> Result<(), Error> {
221        self.work.add_recovery_shard(index, recovery_shard)
222    }
223
224    fn decode(&mut self, compute_recovery: bool) -> Result<Option<DecoderResult<'_>>, Error> {
225        self.decode_impl(compute_recovery, None)
226    }
227
228    fn into_parts(self) -> (E, DecoderWork) {
229        (self.engine, self.work)
230    }
231
232    fn new(
233        original_count: usize,
234        recovery_count: usize,
235        shard_bytes: usize,
236        engine: E,
237        work: Option<DecoderWork>,
238    ) -> Result<Self, Error> {
239        let mut work = work.unwrap_or_default();
240        Self::reset_work(original_count, recovery_count, shard_bytes, &mut work)?;
241        Ok(Self { engine, work })
242    }
243
244    fn reset(
245        &mut self,
246        original_count: usize,
247        recovery_count: usize,
248        shard_bytes: usize,
249    ) -> Result<(), Error> {
250        Self::reset_work(original_count, recovery_count, shard_bytes, &mut self.work)
251    }
252}
253
254impl<E: Engine> HighRateDecoder<E> {
255    /// Decodes like [`RateDecoder::decode`], taking the erasure locators from `plan` instead of
256    /// evaluating them.
257    ///
258    /// Returns [`Error::PlanMismatch`] unless `plan` was built for these counts, high rate, and
259    /// the received shard indices.
260    pub(crate) fn decode_with_plan(
261        &mut self,
262        compute_recovery: bool,
263        plan: &Plan,
264    ) -> Result<Option<DecoderResult<'_>>, Error> {
265        self.decode_impl(compute_recovery, Some(plan))
266    }
267}
268
269impl<E: Engine> HighRateDecoder<E> {
270    /// Reconstructs the missing originals, and the missing recovery shards when
271    /// `compute_recovery` is set, taking the erasure locators from `plan` when given.
272    ///
273    /// Returns `Ok(None)` and clears the received state when every original was provided.
274    fn decode_impl(
275        &mut self,
276        compute_recovery: bool,
277        plan: Option<&Plan>,
278    ) -> Result<Option<DecoderResult<'_>>, Error> {
279        if let Some(plan) = plan {
280            self.work.validate_plan(plan, true)?;
281        }
282        let Some((mut work, original_count, recovery_count, received)) =
283            self.work.decode_begin()?
284        else {
285            // Every original was provided: nothing to reconstruct. Clear the received state and
286            // report nothing.
287            self.work.reset_received();
288            return Ok(None);
289        };
290
291        let chunk_size = recovery_count.next_power_of_two();
292        let original_end = chunk_size + original_count;
293        let work_count = work.len();
294
295        // Take the erasure locators from the plan, or evaluate them from the received shards.
296        let mut owned_erasures;
297        #[expect(
298            clippy::option_if_let_else,
299            reason = "The fallback initializes a scratch buffer and returns a borrow of it."
300        )]
301        let erasures: &[GfElement] = match plan {
302            Some(plan) => plan.coefficients(),
303            None => {
304                owned_erasures = vec![0; original_end.next_power_of_two()];
305                eval_erasures::<E>(
306                    &mut owned_erasures,
307                    original_count,
308                    recovery_count,
309                    received,
310                );
311                &owned_erasures
312            }
313        };
314
315        // Multiply received shards by their erasure locators and zero everything else:
316        //
317        // work[               .. recovery_count] = recovery * erasures
318        // work[recovery_count .. chunk_size    ] = 0
319        // work[chunk_size     .. original_end  ] = original * erasures
320        // work[original_end   ..               ] = 0
321        for i in 0..recovery_count {
322            if received[i] {
323                self.engine.mul(&mut work[i], erasures[i]);
324            } else {
325                work[i].fill([0; SHARD_CHUNK_BYTES]);
326            }
327        }
328        work.zero(recovery_count..chunk_size);
329        for i in chunk_size..original_end {
330            if received[i] {
331                self.engine.mul(&mut work[i], erasures[i]);
332            } else {
333                work[i].fill([0; SHARD_CHUNK_BYTES]);
334            }
335        }
336        work.zero(original_end..);
337
338        // Take the formal derivative between an IFFT and an FFT.
339        self.engine.ifft(&mut work, 0, work_count, original_end, 0);
340        engine::formal_derivative(&mut work);
341        self.engine.fft(&mut work, 0, work_count, original_end, 0);
342
343        // Reveal the missing originals by scaling them by the inverse locator.
344        for i in chunk_size..original_end {
345            if !received[i] {
346                self.engine.mul(&mut work[i], GF_MODULUS - erasures[i]);
347            }
348        }
349
350        // When the caller passed `compute_recovery = true` to `decode`, reveal the missing
351        // recovery shards at `work[0..recovery_count]`. Scale them by the inverse locator so they
352        // hold the canonical recovery values, mirroring the reveal of the originals above. This
353        // lets `RecoveryDecoderResult::recovery` return them without a separate re-encode.
354        if compute_recovery {
355            for i in 0..recovery_count {
356                if !received[i] {
357                    self.engine.mul(&mut work[i], GF_MODULUS - erasures[i]);
358                }
359            }
360        }
361
362        // Undo the last chunk encoding of the originals, and of the recovery shards if computed.
363        self.work.undo_last_chunk_encoding();
364        if compute_recovery {
365            self.work.undo_last_chunk_encoding_recovery();
366        }
367
368        Ok(Some(DecoderResult::new(&mut self.work)))
369    }
370
371    /// Validates the parameters, then resets `work` for them with [`Self::work_count`] shards.
372    ///
373    /// Returns the error from [`RateDecoder::validate`] and leaves `work` unchanged on failure.
374    fn reset_work(
375        original_count: usize,
376        recovery_count: usize,
377        shard_bytes: usize,
378        work: &mut DecoderWork,
379    ) -> Result<(), Error> {
380        Self::validate(original_count, recovery_count, shard_bytes)?;
381
382        // work[..recovery_count     ]  =  recovery
383        // work[recovery_count_pow2..]  =  original
384        work.reset(
385            original_count,
386            recovery_count,
387            shard_bytes,
388            recovery_count.next_power_of_two(),
389            0,
390            Self::work_count(original_count, recovery_count),
391        );
392
393        Ok(())
394    }
395
396    /// Returns the number of shards in the working space.
397    ///
398    /// This is `recovery_count.next_power_of_two() + original_count` rounded up to a power of two,
399    /// the size of the decoding transforms.
400    ///
401    /// # Panics
402    ///
403    /// Panics if the counts are not supported by [`HighRate`].
404    fn work_count(original_count: usize, recovery_count: usize) -> usize {
405        assert!(Self::supports(original_count, recovery_count));
406
407        (recovery_count.next_power_of_two() + original_count).next_power_of_two()
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::reed_solomon::test_util;
415    use commonware_macros::test_group;
416
417    #[test]
418    fn roundtrip_all_originals_missing() {
419        roundtrip_single!(
420            HighRate,
421            3,
422            3,
423            1024,
424            test_util::EITHER_3_3,
425            &[],
426            &[test_util::range(0, 3)],
427            133,
428        );
429    }
430
431    #[test]
432    fn roundtrip_no_originals_missing() {
433        roundtrip_single!(
434            HighRate,
435            3,
436            2,
437            1024,
438            test_util::HIGH_3_2,
439            &[test_util::range(0, 3)],
440            &[],
441            132
442        );
443    }
444
445    #[test]
446    fn roundtrips_tiny() {
447        for (original_count, recovery_count, seed, recovery_hash) in test_util::HIGH_TINY {
448            roundtrip_single!(
449                HighRate,
450                *original_count,
451                *recovery_count,
452                1024,
453                recovery_hash,
454                &[test_util::range(*recovery_count, *original_count)],
455                &[test_util::range(
456                    0,
457                    core::cmp::min(*original_count, *recovery_count)
458                )],
459                *seed,
460            );
461        }
462    }
463
464    #[test_group("slow")]
465    #[test]
466    fn roundtrip_3000_30000() {
467        roundtrip_single!(
468            HighRate,
469            3000,
470            30000,
471            crate::reed_solomon::SHARD_CHUNK_BYTES,
472            test_util::HIGH_3000_30000_14,
473            &[],
474            &[test_util::range(0, 3000)],
475            14,
476        );
477    }
478
479    #[test_group("slow")]
480    #[test]
481    fn roundtrip_32768_32768() {
482        roundtrip_single!(
483            HighRate,
484            32768,
485            32768,
486            crate::reed_solomon::SHARD_CHUNK_BYTES,
487            test_util::EITHER_32768_32768_11,
488            &[],
489            &[test_util::range(0, 32768)],
490            11,
491        );
492    }
493
494    #[test_group("slow")]
495    #[test]
496    fn roundtrip_60000_3000() {
497        roundtrip_single!(
498            HighRate,
499            60000,
500            3000,
501            crate::reed_solomon::SHARD_CHUNK_BYTES,
502            test_util::HIGH_60000_3000_12,
503            &[test_util::range(3000, 60000)],
504            &[test_util::range(0, 3000)],
505            12,
506        );
507    }
508
509    #[test]
510    fn roundtrip_34000_2000_shard_size_8() {
511        roundtrip_single!(
512            HighRate,
513            34000,
514            2000,
515            8,
516            test_util::HIGH_34000_2000_123_8,
517            &[test_util::range(0, 32000)],
518            &[test_util::range(0, 2000)],
519            123
520        );
521    }
522
523    #[test]
524    fn two_rounds_implicit_reset() {
525        roundtrip_two_rounds!(
526            HighRate,
527            false,
528            (
529                3,
530                2,
531                1024,
532                test_util::HIGH_3_2,
533                &[test_util::index(1)],
534                &[test_util::index(0), test_util::index(1)],
535                132
536            ),
537            (
538                3,
539                2,
540                1024,
541                test_util::HIGH_3_2_232,
542                &[test_util::index(0)],
543                &[test_util::index(0), test_util::index(1)],
544                232
545            ),
546        );
547    }
548
549    #[test]
550    fn two_rounds_explicit_reset() {
551        roundtrip_two_rounds!(
552            HighRate,
553            true,
554            (
555                3,
556                2,
557                1024,
558                test_util::HIGH_3_2,
559                &[test_util::index(1)],
560                &[test_util::index(0), test_util::index(1)],
561                132
562            ),
563            (
564                5,
565                2,
566                1024,
567                test_util::HIGH_5_2,
568                &[
569                    test_util::index(0),
570                    test_util::index(2),
571                    test_util::index(4)
572                ],
573                &[test_util::index(0), test_util::index(1)],
574                152
575            ),
576        );
577    }
578
579    mod high_rate {
580        use crate::reed_solomon::{
581            Error, SHARD_CHUNK_BYTES,
582            engine::Scalar,
583            rate::{HighRate, Rate},
584        };
585
586        #[test]
587        fn decoder() {
588            assert_eq!(
589                HighRate::<Scalar>::decoder(4096, 61440, SHARD_CHUNK_BYTES, Scalar::new(), None)
590                    .err(),
591                Some(Error::UnsupportedShardCount {
592                    original_count: 4096,
593                    recovery_count: 61440,
594                })
595            );
596
597            assert!(
598                HighRate::<Scalar>::decoder(61440, 4096, SHARD_CHUNK_BYTES, Scalar::new(), None)
599                    .is_ok()
600            );
601        }
602
603        #[test]
604        fn encoder() {
605            assert_eq!(
606                HighRate::<Scalar>::encoder(4096, 61440, SHARD_CHUNK_BYTES, Scalar::new(), None)
607                    .err(),
608                Some(Error::UnsupportedShardCount {
609                    original_count: 4096,
610                    recovery_count: 61440,
611                })
612            );
613
614            assert!(
615                HighRate::<Scalar>::encoder(61440, 4096, SHARD_CHUNK_BYTES, Scalar::new(), None)
616                    .is_ok()
617            );
618        }
619
620        #[test]
621        fn supports() {
622            assert!(!HighRate::<Scalar>::supports(0, 1));
623            assert!(!HighRate::<Scalar>::supports(1, 0));
624
625            assert!(!HighRate::<Scalar>::supports(4096, 61440));
626
627            assert!(HighRate::<Scalar>::supports(61440, 4096));
628            assert!(!HighRate::<Scalar>::supports(61440, 4097));
629            assert!(!HighRate::<Scalar>::supports(61441, 4096));
630
631            assert!(!HighRate::<Scalar>::supports(usize::MAX, usize::MAX));
632        }
633
634        #[test]
635        fn validate() {
636            assert_eq!(
637                HighRate::<Scalar>::validate(1, 1, 123).err(),
638                Some(Error::InvalidShardSize { shard_bytes: 123 })
639            );
640
641            assert_eq!(
642                HighRate::<Scalar>::validate(4096, 61440, SHARD_CHUNK_BYTES).err(),
643                Some(Error::UnsupportedShardCount {
644                    original_count: 4096,
645                    recovery_count: 61440,
646                })
647            );
648
649            assert!(HighRate::<Scalar>::validate(61440, 4096, SHARD_CHUNK_BYTES).is_ok());
650        }
651    }
652
653    mod high_rate_encoder {
654        use crate::reed_solomon::{
655            Error, SHARD_CHUNK_BYTES,
656            engine::Scalar,
657            rate::{HighRateEncoder, RateEncoder},
658        };
659
660        test_rate_encoder_errors! {HighRateEncoder}
661
662        #[test]
663        fn supports() {
664            assert!(!HighRateEncoder::<Scalar>::supports(4096, 61440));
665            assert!(HighRateEncoder::<Scalar>::supports(61440, 4096));
666        }
667
668        #[test]
669        fn validate() {
670            assert_eq!(
671                HighRateEncoder::<Scalar>::validate(1, 1, 123).err(),
672                Some(Error::InvalidShardSize { shard_bytes: 123 })
673            );
674
675            assert_eq!(
676                HighRateEncoder::<Scalar>::validate(4096, 61440, SHARD_CHUNK_BYTES).err(),
677                Some(Error::UnsupportedShardCount {
678                    original_count: 4096,
679                    recovery_count: 61440,
680                })
681            );
682
683            assert!(HighRateEncoder::<Scalar>::validate(61440, 4096, SHARD_CHUNK_BYTES).is_ok());
684        }
685
686        #[test]
687        fn work_count() {
688            assert_eq!(HighRateEncoder::<Scalar>::work_count(1, 1), 1);
689            assert_eq!(HighRateEncoder::<Scalar>::work_count(4096, 1024), 4096);
690            assert_eq!(HighRateEncoder::<Scalar>::work_count(4097, 1024), 5120);
691            assert_eq!(HighRateEncoder::<Scalar>::work_count(4097, 1025), 6144);
692            assert_eq!(HighRateEncoder::<Scalar>::work_count(32768, 32768), 32768);
693        }
694    }
695
696    mod high_rate_decoder {
697        use crate::reed_solomon::{
698            Error, SHARD_CHUNK_BYTES,
699            engine::Scalar,
700            rate::{HighRateDecoder, RateDecoder},
701        };
702
703        test_rate_decoder_errors! {HighRateDecoder}
704
705        #[test]
706        fn supports() {
707            assert!(!HighRateDecoder::<Scalar>::supports(4096, 61440));
708            assert!(HighRateDecoder::<Scalar>::supports(61440, 4096));
709        }
710
711        #[test]
712        fn validate() {
713            assert_eq!(
714                HighRateDecoder::<Scalar>::validate(1, 1, 123).err(),
715                Some(Error::InvalidShardSize { shard_bytes: 123 })
716            );
717
718            assert_eq!(
719                HighRateDecoder::<Scalar>::validate(4096, 61440, SHARD_CHUNK_BYTES).err(),
720                Some(Error::UnsupportedShardCount {
721                    original_count: 4096,
722                    recovery_count: 61440,
723                })
724            );
725
726            assert!(HighRateDecoder::<Scalar>::validate(61440, 4096, SHARD_CHUNK_BYTES).is_ok());
727        }
728
729        #[test]
730        fn work_count() {
731            assert_eq!(HighRateDecoder::<Scalar>::work_count(1, 1), 2);
732            assert_eq!(HighRateDecoder::<Scalar>::work_count(2048, 1025), 4096);
733            assert_eq!(HighRateDecoder::<Scalar>::work_count(2049, 1025), 8192);
734            assert_eq!(HighRateDecoder::<Scalar>::work_count(3072, 1024), 4096);
735            assert_eq!(HighRateDecoder::<Scalar>::work_count(3073, 1024), 8192);
736            assert_eq!(HighRateDecoder::<Scalar>::work_count(32768, 32768), 65536);
737        }
738    }
739}