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}