Skip to main content

zc_rlnc/
encode.rs

1//! Module that implements the RLNC encoding algorithm.
2use rand::Rng;
3
4use crate::{
5    common::RLNCError,
6    primitives::{Chunks, field::Field, packet::RLNCPacket},
7};
8
9/// RLNC encoder that's generic over the [`Field`] type. An ancoder should be instantiated
10/// per piece of data the caller wants to encode, then used to generate the encoded chunks.
11#[derive(Debug)]
12pub struct Encoder<F: Field> {
13    // The chunks of data to be encoded.
14    chunks: Chunks<F>,
15    // The number of chunks to split the data into (also known as the generation size).
16    chunk_count: usize,
17    // The size of each chunk in bytes.
18    chunk_size: usize,
19}
20
21impl<F: Field> Encoder<F> {
22    /// Creates a new encoder for the given data and chunk count.
23    pub fn new(data: impl AsRef<[u8]>, chunk_count: usize) -> Result<Self, RLNCError> {
24        let chunks = Self::prepare(data, chunk_count)?;
25        let chunk_count = chunks.len();
26        let chunk_size = chunks.chunk_size();
27
28        Ok(Self { chunks, chunk_count, chunk_size })
29    }
30
31    /// Creates a new encoder from a vector of chunks.
32    pub fn from_chunks(chunks: Chunks<F>) -> Self {
33        let chunk_count = chunks.len();
34        let chunk_size = chunks.chunk_size();
35
36        Self { chunks, chunk_count, chunk_size }
37    }
38
39    /// Returns true if the encoder should parallelize the encoding process.
40    ///
41    /// This is determined by the chunk count (collection size), chunk size (work unit size), and
42    /// the number of threads.
43    #[cfg(feature = "parallel")]
44    fn should_parallelize(&self) -> bool {
45        // Min total work: 512KiB
46        let min_total_work = 1024 * 512;
47        // Min chunks: 2
48        let min_chunks = 2;
49        // Min work unit: 128KiB
50        let min_work_unit = 1024 * 128;
51
52        let total_work = self.chunk_count * self.chunk_size;
53
54        total_work >= min_total_work &&
55            self.chunk_size >= min_work_unit &&
56            self.chunk_count >= min_chunks
57    }
58
59    /// Sequentially encodes the data with the given coding vector using linear combinations.
60    fn encode_inner(&self, coding_vector: &[F]) -> Vec<F> {
61        let mut result = vec![F::ZERO; self.chunk_size.div_ceil(F::SAFE_CAPACITY)];
62
63        for (chunk, &coefficient) in self.chunks.inner().iter().zip(coding_vector) {
64            if coefficient.is_zero_vartime() {
65                continue;
66            }
67
68            for (i, symbol) in chunk.symbols().iter().enumerate() {
69                result[i] += *symbol * coefficient;
70            }
71        }
72
73        result
74    }
75
76    /// Returns the number of chunks in the encoder.
77    pub fn chunk_count(&self) -> usize {
78        self.chunk_count
79    }
80
81    /// Returns the size of each chunk in the encoder.
82    pub fn chunk_size(&self) -> usize {
83        self.chunk_size
84    }
85
86    /// Prepares the data for encoding by splitting it into equally sized chunks and padding with
87    /// zeros. Also converts the data into symbols in the chosen finite field.
88    pub fn prepare(data: impl AsRef<[u8]>, chunk_count: usize) -> Result<Chunks<F>, RLNCError> {
89        Ok(Chunks::new(data.as_ref(), chunk_count)?)
90    }
91
92    /// Encodes the data with the given coding vector using linear combinations.
93    ///
94    /// This method computes a coded packet by taking a linear combination of all chunks
95    /// using the coefficients from the coding vector. The operation is performed in
96    /// the field of BLS12-381.
97    ///
98    /// # Mathematical Representation
99    ///
100    /// Given original chunks X₁, X₂, ..., Xₖ and coding vector coefficients c₁, c₂, ..., cₖ,
101    /// the coded packet Y is computed as:
102    ///
103    /// ```text
104    /// Y = c₁ ⊗ X₁ ⊕ c₂ ⊗ X₂ ⊕ ... ⊕ cₖ ⊗ Xₖ
105    /// ```
106    ///
107    /// Where:
108    /// - ⊗ denotes multiplication in the field of BLS12-381
109    /// - ⊕ denotes addition in the field of BLS12-381
110    /// - k is the chunk count (generation size)
111    ///
112    /// Each byte position j in the coded packet is computed as:
113    /// ```text
114    /// Y[j] = Σᵢ₌₁ᵏ (cᵢ ⊗ Xᵢ[j])  (mod p)
115    /// ```
116    ///
117    /// # Algorithm Complexity
118    /// O(k * n) where k is the chunk count and n is the chunk size.
119    /// ```
120    pub fn encode_with_vector(&self, coding_vector: &[F]) -> Result<RLNCPacket<F>, RLNCError> {
121        if coding_vector.len() != self.chunk_count {
122            return Err(RLNCError::InvalidCodingVectorLength(coding_vector.len(), self.chunk_count));
123        }
124
125        let symbol_count = self.chunk_size.div_ceil(F::SAFE_CAPACITY);
126
127        // Compute the encoded result either sequentially or in parallel, depending on the
128        // enabled feature flag. We avoid sharing mutable state across threads by letting each
129        // worker produce a partial vector and then combining (reducing) the partial results.
130        #[cfg(feature = "parallel")]
131        let result = {
132            use rayon::prelude::*;
133
134            if !self.should_parallelize() {
135                self.encode_inner(coding_vector)
136            } else {
137                // Map each (chunk, coefficient) pair to its contribution and then reduce all
138                // contributions into the final result.
139                self.chunks
140                    .inner()
141                    .par_iter()
142                    .zip(coding_vector)
143                    .filter_map(|(chunk, &coefficient)| {
144                        // Skip the work if the coefficient is zero.
145                        if coefficient.is_zero_vartime() {
146                            return None;
147                        }
148
149                        let mut acc = Vec::with_capacity(symbol_count);
150
151                        for symbol in chunk.symbols().iter() {
152                            acc.push(*symbol * coefficient);
153                        }
154
155                        Some(acc)
156                    })
157                    .reduce(
158                        || vec![F::ZERO; symbol_count],
159                        |mut a, b| {
160                            // Element-wise addition of two partial results.
161                            a.iter_mut().zip(b).for_each(|(x, y)| *x += y);
162                            a
163                        },
164                    )
165            }
166        };
167
168        #[cfg(not(feature = "parallel"))]
169        let result = self.encode_inner(coding_vector);
170
171        Ok(RLNCPacket { coding_vector: coding_vector.to_vec(), data: result })
172    }
173
174    /// Encodes the data with a random coding vector, using the provided random number generator.
175    pub fn encode<R: Rng>(&self, mut rng: R) -> Result<RLNCPacket<F>, RLNCError> {
176        let coding_vector: Vec<F> = (0..self.chunk_count)
177            .map(|_| {
178                let mut bytes = [0u8; 32];
179                rng.fill(&mut bytes[..F::SAFE_CAPACITY]);
180                F::from_bytes(&bytes)
181            })
182            .collect();
183
184        self.encode_with_vector(&coding_vector)
185    }
186}