Skip to main content

commonware_cryptography/reed_solomon/
rate.rs

1//! Advanced encoding/decoding using chosen [`Engine`] and [`Rate`].
2//!
3//! **This is an advanced module which is not needed for [basic usage].**
4//!
5//! This module is relevant if you want to
6//! - encode/decode using other [`Engine`] than [`DefaultEngine`].
7//! - re-use working space of one encoder/decoder in another.
8//! - understand/benchmark/test high or low rate directly.
9//!
10//! # Rates
11//!
12//! See [algorithm > Rate] for details about high/low rate.
13//!
14//! - [`DefaultRate`], [`DefaultRateEncoder`], [`DefaultRateDecoder`]
15//!     - Encoding/decoding using high or low rate as appropriate.
16//!     - These are basically same as [`Encoder`]
17//!       and [`Decoder`] except with slightly different API
18//!       which allows specifying [`Engine`] and working space.
19//! - [`HighRate`], [`HighRateEncoder`], [`HighRateDecoder`]
20//!     - Encoding/decoding using only high rate.
21//! - [`LowRate`], [`LowRateEncoder`], [`LowRateDecoder`]
22//!     - Encoding/decoding using only low rate.
23//!
24//! [basic usage]: crate::reed_solomon#basic-usage
25//! [algorithm > Rate]: crate::reed_solomon::algorithm#rate
26//! [`Encoder`]: crate::reed_solomon::Encoder
27//! [`Decoder`]: crate::reed_solomon::Decoder
28//! [`DefaultEngine`]: crate::reed_solomon::engine::DefaultEngine
29
30pub use self::{
31    decoder_work::DecoderWork,
32    encoder_work::EncoderWork,
33    rate_default::{DefaultRate, DefaultRateDecoder, DefaultRateEncoder},
34    rate_high::{HighRate, HighRateDecoder, HighRateEncoder},
35    rate_low::{LowRate, LowRateDecoder, LowRateEncoder},
36};
37use crate::reed_solomon::{
38    DecoderResult, EncoderResult, Error,
39    engine::{self, Engine, GF_ORDER, GfElement, SHARD_CHUNK_BYTES},
40};
41use commonware_utils::MAX_ALLOCATION_BYTES;
42
43mod decoder_work;
44mod encoder_work;
45pub(crate) mod rate_default;
46pub(crate) mod rate_high;
47pub(crate) mod rate_low;
48
49/// Returns [`Error::InvalidShardSize`] if `work_count` shards of `shard_bytes` bytes, each
50/// rounded up to whole `SHARD_CHUNK_BYTES` chunks, exceed [`MAX_ALLOCATION_BYTES`] in total.
51///
52/// # Panics
53///
54/// Panics if `work_count` is zero.
55const fn validate_work_size(shard_bytes: usize, work_count: usize) -> Result<(), Error> {
56    // The chunk array must fit within Vec's maximum allocation size.
57    let max_chunks = MAX_ALLOCATION_BYTES / SHARD_CHUNK_BYTES;
58    if shard_bytes.div_ceil(SHARD_CHUNK_BYTES) > max_chunks / work_count {
59        return Err(Error::InvalidShardSize { shard_bytes });
60    }
61    Ok(())
62}
63
64/// XOR-convolve `erasures` with the field logarithm table.
65///
66/// `erasures` holds `n = end.next_power_of_two()` entries, and entries at and after `end` must
67/// be zero. Because `i ^ j < n` for all `i, j < n`, the `n`-point transform matches the first
68/// `n` outputs of the full-field one.
69///
70/// # Panics
71///
72/// If `erasures.len() != end.next_power_of_two()` or `end > GF_ORDER`.
73fn eval_locator<E: Engine>(erasures: &mut [GfElement], end: usize) {
74    let n = erasures.len();
75    assert_eq!(n, end.next_power_of_two());
76    if n == GF_ORDER {
77        E::eval_poly(erasures.try_into().expect("length is GF_ORDER"), end);
78    } else {
79        engine::utils::eval_poly_short(erasures, end);
80    }
81}
82
83/// Reed-Solomon encoder/decoder generator using specific rate.
84pub trait Rate<E: Engine> {
85    /// Encoder of this rate.
86    type RateEncoder: RateEncoder<E>;
87    /// Decoder of this rate.
88    type RateDecoder: RateDecoder<E>;
89
90    /// Returns `true` if given `original_count` / `recovery_count`
91    /// combination is supported.
92    fn supports(original_count: usize, recovery_count: usize) -> bool;
93
94    /// Creates new encoder. This is same as [`RateEncoder::new`].
95    fn encoder(
96        original_count: usize,
97        recovery_count: usize,
98        shard_bytes: usize,
99        engine: E,
100        work: Option<EncoderWork>,
101    ) -> Result<Self::RateEncoder, Error> {
102        Self::RateEncoder::new(original_count, recovery_count, shard_bytes, engine, work)
103    }
104
105    /// Creates new decoder. This is same as [`RateDecoder::new`].
106    fn decoder(
107        original_count: usize,
108        recovery_count: usize,
109        shard_bytes: usize,
110        engine: E,
111        work: Option<DecoderWork>,
112    ) -> Result<Self::RateDecoder, Error> {
113        Self::RateDecoder::new(original_count, recovery_count, shard_bytes, engine, work)
114    }
115
116    /// Checks that the shard counts are supported and the shard size is nonzero and even.
117    ///
118    /// Use [`RateEncoder::validate`] or [`RateDecoder::validate`] to also check the
119    /// working-space requirements of the chosen operation.
120    fn validate(
121        original_count: usize,
122        recovery_count: usize,
123        shard_bytes: usize,
124    ) -> Result<(), Error> {
125        if !Self::supports(original_count, recovery_count) {
126            Err(Error::UnsupportedShardCount {
127                original_count,
128                recovery_count,
129            })
130        } else if shard_bytes == 0 || shard_bytes & 1 != 0 {
131            Err(Error::InvalidShardSize { shard_bytes })
132        } else {
133            Ok(())
134        }
135    }
136}
137
138/// Reed-Solomon encoder using specific rate.
139pub trait RateEncoder<E: Engine>
140where
141    Self: Sized,
142{
143    /// Rate of this encoder.
144    type Rate: Rate<E>;
145
146    /// Like [`Encoder::add_original_shard`](crate::reed_solomon::Encoder::add_original_shard).
147    fn add_original_shard<T: AsRef<[u8]>>(&mut self, original_shard: T) -> Result<(), Error>;
148
149    /// Like [`Encoder::encode`](crate::reed_solomon::Encoder::encode).
150    fn encode(&mut self) -> Result<EncoderResult<'_>, Error>;
151
152    /// Consumes this encoder returning its [`Engine`] and [`EncoderWork`]
153    /// so that they can be re-used by another encoder.
154    fn into_parts(self) -> (E, EncoderWork);
155
156    /// Like [`Encoder::new`](crate::reed_solomon::Encoder::new)
157    /// with [`Engine`] to use and optional working space to be re-used.
158    fn new(
159        original_count: usize,
160        recovery_count: usize,
161        shard_bytes: usize,
162        engine: E,
163        work: Option<EncoderWork>,
164    ) -> Result<Self, Error>;
165
166    /// Like [`Encoder::reset`](crate::reed_solomon::Encoder::reset).
167    fn reset(
168        &mut self,
169        original_count: usize,
170        recovery_count: usize,
171        shard_bytes: usize,
172    ) -> Result<(), Error>;
173
174    /// Checks that the shard counts are supported, the shard size is nonzero and even, and
175    /// this encoder's working space fits in a single allocation.
176    fn validate(
177        original_count: usize,
178        recovery_count: usize,
179        shard_bytes: usize,
180    ) -> Result<(), Error>;
181
182    /// Returns `true` if given `original_count` / `recovery_count`
183    /// combination is supported.
184    ///
185    /// This is same as [`Rate::supports`].
186    fn supports(original_count: usize, recovery_count: usize) -> bool {
187        Self::Rate::supports(original_count, recovery_count)
188    }
189}
190
191/// Reed-Solomon decoder using specific rate.
192pub trait RateDecoder<E: Engine>
193where
194    Self: Sized,
195{
196    /// Rate of this decoder.
197    type Rate: Rate<E>;
198
199    /// Like [`Decoder::add_original_shard`](crate::reed_solomon::Decoder::add_original_shard).
200    fn add_original_shard<T: AsRef<[u8]>>(
201        &mut self,
202        index: usize,
203        original_shard: T,
204    ) -> Result<(), Error>;
205
206    /// Like [`Decoder::add_recovery_shard`](crate::reed_solomon::Decoder::add_recovery_shard).
207    fn add_recovery_shard<T: AsRef<[u8]>>(
208        &mut self,
209        index: usize,
210        recovery_shard: T,
211    ) -> Result<(), Error>;
212
213    /// Like [`Decoder::decode`](crate::reed_solomon::Decoder::decode), this reconstructs the
214    /// missing shards, or returns `Ok(None)` if every original was already provided (nothing to
215    /// reconstruct). When `compute_recovery` is set, the missing recovery shards are also
216    /// reconstructed into the work buffers. The returned [`DecoderResult`] exposes only restored
217    /// original shards, so callers of this trait should pass `false`. Reconstructed recovery
218    /// shards are readable through
219    /// [`Decoder::decode_with_recovery`](crate::reed_solomon::Decoder::decode_with_recovery) and
220    /// [`Decoder::decode_with_recovery_plan`](crate::reed_solomon::Decoder::decode_with_recovery_plan).
221    fn decode(&mut self, compute_recovery: bool) -> Result<Option<DecoderResult<'_>>, Error>;
222
223    /// Consumes this decoder returning its [`Engine`] and [`DecoderWork`]
224    /// so that they can be re-used by another decoder.
225    fn into_parts(self) -> (E, DecoderWork);
226
227    /// Like [`Decoder::new`](crate::reed_solomon::Decoder::new)
228    /// with [`Engine`] to use and optional working space to be re-used.
229    fn new(
230        original_count: usize,
231        recovery_count: usize,
232        shard_bytes: usize,
233        engine: E,
234        work: Option<DecoderWork>,
235    ) -> Result<Self, Error>;
236
237    /// Like [`Decoder::reset`](crate::reed_solomon::Decoder::reset).
238    fn reset(
239        &mut self,
240        original_count: usize,
241        recovery_count: usize,
242        shard_bytes: usize,
243    ) -> Result<(), Error>;
244
245    /// Checks that the shard counts are supported, the shard size is nonzero and even, and
246    /// this decoder's working space fits in a single allocation.
247    fn validate(
248        original_count: usize,
249        recovery_count: usize,
250        shard_bytes: usize,
251    ) -> Result<(), Error>;
252
253    /// Returns `true` if given `original_count` / `recovery_count`
254    /// combination is supported.
255    ///
256    /// This is same as [`Rate::supports`].
257    fn supports(original_count: usize, recovery_count: usize) -> bool {
258        Self::Rate::supports(original_count, recovery_count)
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::reed_solomon::{engine::Scalar, test_util};
266
267    /// Checks that `validate` accepts the largest whole-chunk shard size for `work_count` work
268    /// shards and rejects that size plus 2 bytes, which needs one more chunk per shard.
269    fn check_capacity(validate: impl Fn(usize) -> Result<(), Error>, work_count: usize) {
270        let shard_bytes =
271            (MAX_ALLOCATION_BYTES / SHARD_CHUNK_BYTES / work_count) * SHARD_CHUNK_BYTES;
272        assert_eq!(validate(shard_bytes), Ok(()));
273        assert_eq!(
274            validate(shard_bytes + 2),
275            Err(Error::InvalidShardSize {
276                shard_bytes: shard_bytes + 2
277            })
278        );
279    }
280
281    /// Checks the shard size limit for counts such as (9, 3) and (3, 9), which encode with 12
282    /// work shards (9 rounded up to a multiple of the chunk of 4) and decode with 16 (4 + 9
283    /// rounded up to a power of two).
284    fn check_rate_capacity<R: Rate<Scalar>>(original_count: usize, recovery_count: usize) {
285        check_capacity(
286            |shard_bytes| R::RateEncoder::validate(original_count, recovery_count, shard_bytes),
287            12,
288        );
289        check_capacity(
290            |shard_bytes| R::RateDecoder::validate(original_count, recovery_count, shard_bytes),
291            16,
292        );
293    }
294
295    #[test]
296    fn working_space_capacity() {
297        check_rate_capacity::<HighRate<Scalar>>(9, 3);
298        check_rate_capacity::<LowRate<Scalar>>(3, 9);
299        check_rate_capacity::<DefaultRate<Scalar>>(9, 3);
300        check_rate_capacity::<DefaultRate<Scalar>>(3, 9);
301    }
302
303    #[test]
304    fn work_and_result_reuse() {
305        let mut encoder_work = None;
306        let mut decoder_work = None;
307        for (phase, (original_count, recovery_count, shard_bytes)) in
308            [(3, 2, 66), (2, 3, 34), (5, 3, 130), (4, 6, 2)]
309                .into_iter()
310                .enumerate()
311        {
312            let mut encoder = DefaultRate::<Scalar>::encoder(
313                original_count,
314                recovery_count,
315                shard_bytes,
316                Scalar::new(),
317                encoder_work,
318            )
319            .unwrap();
320            let mut decoder = DefaultRate::<Scalar>::decoder(
321                original_count,
322                recovery_count,
323                shard_bytes,
324                Scalar::new(),
325                decoder_work,
326            )
327            .unwrap();
328            for round in 0..2 {
329                let original = test_util::generate_original(
330                    original_count,
331                    shard_bytes,
332                    (phase * 2 + round) as u8,
333                );
334                let mut fresh = DefaultRate::<Scalar>::encoder(
335                    original_count,
336                    recovery_count,
337                    shard_bytes,
338                    Scalar::new(),
339                    None,
340                )
341                .unwrap();
342                for shard in &original {
343                    encoder.add_original_shard(shard).unwrap();
344                    fresh.add_original_shard(shard).unwrap();
345                }
346                let expected = fresh.encode().unwrap();
347                {
348                    let encoded = encoder.encode().unwrap();
349                    for index in 0..recovery_count {
350                        assert_eq!(encoded.recovery(index), expected.recovery(index));
351                    }
352                    let mut iter = encoded.recovery_iter();
353                    assert_eq!(iter.next(), expected.recovery(0));
354                    assert_eq!(iter.len(), recovery_count - 1);
355                }
356                assert!(matches!(
357                    encoder.encode(),
358                    Err(Error::TooFewOriginalShards {
359                        original_received_count: 0,
360                        ..
361                    })
362                ));
363
364                for (index, shard) in original.iter().enumerate() {
365                    decoder.add_original_shard(index, shard).unwrap();
366                }
367                assert!(decoder.decode(round == 0).unwrap().is_none());
368                assert!(matches!(
369                    decoder.decode(false),
370                    Err(Error::NotEnoughShards {
371                        original_received_count: 0,
372                        recovery_received_count: 0,
373                        ..
374                    })
375                ));
376                for (index, shard) in original.iter().enumerate().skip(2) {
377                    decoder.add_original_shard(index, shard).unwrap();
378                }
379                for index in 0..2 {
380                    decoder
381                        .add_recovery_shard(index, expected.recovery(index).unwrap())
382                        .unwrap();
383                }
384                {
385                    let decoded = decoder.decode(true).unwrap().unwrap();
386                    let mut iter = decoded.original_iter();
387                    assert_eq!(iter.next(), Some((0, original[0].as_slice())));
388                    assert_eq!(iter.len(), 1);
389                    assert_eq!(decoded.original(1), Some(original[1].as_slice()));
390                }
391                assert!(matches!(
392                    decoder.decode(false),
393                    Err(Error::NotEnoughShards {
394                        original_received_count: 0,
395                        recovery_received_count: 0,
396                        ..
397                    })
398                ));
399            }
400
401            // Hand the next phase work with a shard still pending. Reuse must drop the stale
402            // received state and keep the stale bytes out of its results.
403            let pending = vec![255; shard_bytes];
404            encoder.add_original_shard(&pending).unwrap();
405            decoder.add_original_shard(0, &pending).unwrap();
406            encoder_work = Some(encoder.into_parts().1);
407            decoder_work = Some(decoder.into_parts().1);
408        }
409    }
410}