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}