Skip to main content

p3_binary_dft/
interleaved.rs

1//! The interleaved Reed–Solomon codeword a multi-rate commitment's levels read.
2
3use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixView, RowMajorMatrixViewMut};
4
5use crate::butterfly::ButterflyField;
6use crate::encoder::padded_message_len;
7use crate::lch::transform_cosets;
8
9/// Reed–Solomon encode a message held one folding column at a time.
10///
11/// # Overview
12///
13/// A commitment that folds `f` variables at a time opens whole folding blocks at once.
14/// Its codeword is therefore a matrix of `2^f` columns, one row per domain point.
15///
16/// A prover holds the opposite layout, with each column's coefficients contiguous.
17/// That is what binding a prefix of the variables leaves behind.
18///
19/// Bringing the two layouts together as a pass of its own writes the message once more.
20///
21/// The transform this hands the result to skips the layers that cross the padding.
22/// So the interleaving rides along with a copy that has to happen anyway.
23///
24/// # Arguments
25///
26/// - `columns`: the message's columns back to back, each `2^log_message` values long.
27/// - `log_message`: base-two logarithm of the values one column holds.
28/// - `log_inv_rate`: dimensions added to the message's own domain.
29///
30/// # Returns
31///
32/// One row per domain point, holding every column's value at that point.
33///
34/// # Panics
35///
36/// Panics if there is no column, or if the values do not divide into whole columns.
37/// Panics if the extended domain exceeds the bit width of the level.
38///
39/// Panics if the codeword length overflows the address space.
40#[must_use]
41pub fn interleaved_encode_batch<F: ButterflyField>(
42    columns: &[F],
43    log_message: usize,
44    log_inv_rate: usize,
45) -> RowMajorMatrix<F> {
46    // A codeword of no columns has neither a width to interleave into nor a height to read.
47    assert!(!columns.is_empty(), "at least one message column");
48    assert!(
49        log_message + log_inv_rate <= 1 << F::LOG_BITS,
50        "domain exceeds field dimension"
51    );
52
53    // The widest level admits 128 dimensions, which is past what a length can address.
54    // So the row count is what bounds the dimension, not the level.
55    let rows = u32::try_from(log_message)
56        .ok()
57        .and_then(|bits| 1usize.checked_shl(bits))
58        .expect("message dimension overflows usize");
59    assert_eq!(columns.len() % rows, 0, "whole message columns");
60
61    let width = columns.len() / rows;
62    let len = columns.len();
63    let mut values = F::zero_vec(padded_message_len(len, log_inv_rate));
64
65    // The first coset is the message with its columns interleaved, blocked by the transpose.
66    let source = RowMajorMatrixView::new(columns, rows);
67    let mut target = RowMajorMatrixViewMut::new(&mut values[..len], width);
68    source.transpose_into(&mut target);
69
70    // Every coset above it is a copy of that one, then its own shifted transform.
71    transform_cosets::<F>(&mut values, width, log_message);
72
73    RowMajorMatrix::new(values, width)
74}
75
76#[cfg(test)]
77mod tests {
78    use alloc::format;
79    use alloc::vec::Vec;
80
81    use p3_binary_field::{BinaryField16, BinaryField32, BinaryField128, Ghash128, TowerLevel};
82    use p3_field::PrimeCharacteristicRing;
83    use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixView, RowMajorMatrixViewMut};
84    use proptest::prelude::*;
85
86    use super::interleaved_encode_batch;
87    use crate::butterfly::ButterflyField;
88    use crate::lch::LchNtt;
89    use crate::naive::NaiveAdditiveNtt;
90    use crate::traits::AdditiveNtt;
91
92    /// Widths covering one column, an odd count, a register-sized row and a folding block.
93    const WIDTHS: [usize; 4] = [1, 3, 8, 16];
94
95    /// A column-major message whose entries depend on both the position and the seed.
96    fn columns<F: TowerLevel>(log_message: usize, width: usize, seed: u64) -> Vec<F> {
97        (0..(width << log_message))
98            .map(|i| {
99                let bits = seed
100                    .wrapping_mul(0x9e37_79b9_7f4a_7c15)
101                    .wrapping_add(i as u64 + 1)
102                    .wrapping_mul(0xbf58_476d_1ce4_e5b9);
103                F::from_le_byte_iter(bits.to_le_bytes().into_iter().cycle())
104            })
105            .collect()
106    }
107
108    /// The reference: interleave in a pass of its own, pad with zeros, transform the whole lot.
109    fn transpose_then_encode<F: ButterflyField, N: AdditiveNtt<F>>(
110        ntt: &N,
111        columns: &[F],
112        log_message: usize,
113        log_inv_rate: usize,
114    ) -> RowMajorMatrix<F> {
115        let rows = 1usize << log_message;
116        let width = columns.len() / rows;
117
118        let mut values = F::zero_vec(columns.len() << log_inv_rate);
119        if width != 0 {
120            let source = RowMajorMatrixView::new(columns, rows);
121            let mut target = RowMajorMatrixViewMut::new(&mut values[..columns.len()], width);
122            source.transpose_into(&mut target);
123        }
124        ntt.ntt_batch(RowMajorMatrix::new(values, width))
125    }
126
127    /// The fused encoder against the two-pass reference, at one shape.
128    fn check_against_the_two_pass<F: ButterflyField>(
129        log_message: usize,
130        width: usize,
131        log_inv_rate: usize,
132        seed: u64,
133    ) {
134        let columns = columns::<F>(log_message, width, seed);
135        let expected =
136            transpose_then_encode(&LchNtt::<F>::default(), &columns, log_message, log_inv_rate);
137        let actual = interleaved_encode_batch::<F>(&columns, log_message, log_inv_rate);
138        assert_eq!(
139            actual, expected,
140            "log_message={log_message} width={width} rate={log_inv_rate}"
141        );
142    }
143
144    #[test]
145    fn the_fused_encoder_matches_the_two_pass_one() {
146        // Rates from none at all, where no coset is copied, up to three added dimensions.
147        for width in WIDTHS {
148            for log_message in 0..=6 {
149                for log_inv_rate in 0..=3 {
150                    check_against_the_two_pass::<BinaryField32>(
151                        log_message,
152                        width,
153                        log_inv_rate,
154                        7,
155                    );
156                    check_against_the_two_pass::<BinaryField128>(
157                        log_message,
158                        width,
159                        log_inv_rate,
160                        11,
161                    );
162                    check_against_the_two_pass::<Ghash128>(log_message, width, log_inv_rate, 13);
163                }
164            }
165        }
166    }
167
168    #[test]
169    fn the_fused_encoder_matches_the_reference_oracle() {
170        // The oracle evaluates the novel basis from its product definition.
171        // So it pins the coset split to that basis, not to another split of the network.
172        for width in [1usize, 3] {
173            for log_message in 0..=4 {
174                for log_inv_rate in 0..=2 {
175                    let columns = columns::<BinaryField16>(log_message, width, 17);
176                    let expected = transpose_then_encode(
177                        &NaiveAdditiveNtt::<BinaryField16>::default(),
178                        &columns,
179                        log_message,
180                        log_inv_rate,
181                    );
182                    let actual = interleaved_encode_batch::<BinaryField16>(
183                        &columns,
184                        log_message,
185                        log_inv_rate,
186                    );
187                    let label = format!("log_message={log_message} width={width}");
188                    assert_eq!(actual, expected, "{label} rate={log_inv_rate}");
189                }
190            }
191        }
192    }
193
194    #[test]
195    fn a_column_reappears_as_the_codeword_column() {
196        // Invariant: column `j` of the codeword is that column's own encoding, on its own.
197        //
198        // Fixture state: four columns of 2^5 values at rate 1/2.
199        //
200        //     columns:  [ c0 | c1 | c2 | c3 ]      each 32 values, contiguous
201        //     codeword:  row i = [ C0[i], C1[i], C2[i], C3[i] ]
202        const LOG_MESSAGE: usize = 5;
203        const WIDTH: usize = 4;
204        let columns = columns::<BinaryField128>(LOG_MESSAGE, WIDTH, 23);
205        let codeword = interleaved_encode_batch::<BinaryField128>(&columns, LOG_MESSAGE, 1);
206
207        for (j, column) in columns
208            .as_chunks::<{ 1 << LOG_MESSAGE }>()
209            .0
210            .iter()
211            .enumerate()
212        {
213            // That column alone, padded and transformed as a single-column message.
214            let mut padded = column.to_vec();
215            padded.resize(padded.len() * 2, BinaryField128::ZERO);
216            let alone =
217                LchNtt::<BinaryField128>::default().ntt_batch(RowMajorMatrix::new(padded, 1));
218
219            for (i, value) in alone.values.iter().enumerate() {
220                assert_eq!(codeword.values[i * WIDTH + j], *value, "column={j} row={i}");
221            }
222        }
223    }
224
225    #[test]
226    #[should_panic = "at least one message column"]
227    fn a_message_with_no_column_is_refused() {
228        // A codeword of no columns has no width to interleave into and no height to read off.
229        // Every later step divides by one of the two, so the shape has to be rejected here.
230        let _ = interleaved_encode_batch::<BinaryField32>(&[], 3, 1);
231    }
232
233    #[test]
234    #[should_panic = "message dimension overflows usize"]
235    fn a_message_dimension_past_the_address_space_is_refused() {
236        // The widest level admits 128 domain dimensions, and a length addresses at most 64.
237        // So a dimension the level allows can still have no row count to describe it.
238        let values = columns::<BinaryField128>(1, 1, 0);
239        let _ = interleaved_encode_batch::<BinaryField128>(&values, 100, 1);
240    }
241
242    #[test]
243    #[should_panic = "whole message columns"]
244    fn a_partial_column_is_refused() {
245        // Nine values do not divide into columns of four, so the interleaving is ill-posed.
246        let values = columns::<BinaryField32>(2, 3, 0);
247        let _ = interleaved_encode_batch::<BinaryField32>(&values[..9], 2, 0);
248    }
249
250    #[test]
251    #[should_panic = "domain exceeds field dimension"]
252    fn a_domain_past_the_level_dimension_is_refused() {
253        // A byte level has sixteen Cantor basis vectors, and this asks for seventeen.
254        let values = columns::<BinaryField16>(15, 1, 0);
255        let _ = interleaved_encode_batch::<BinaryField16>(&values, 15, 2);
256    }
257
258    proptest! {
259        #![proptest_config(ProptestConfig::with_cases(32))]
260
261        /// Random shapes against the two-pass reference, over two representations.
262        #[test]
263        fn random_shapes_match_the_two_pass_encoder(
264            log_message in 0usize..=7,
265            width in 1usize..=5,
266            log_inv_rate in 0usize..=2,
267            seed in any::<u64>(),
268        ) {
269            check_against_the_two_pass::<BinaryField128>(log_message, width, log_inv_rate, seed);
270            check_against_the_two_pass::<Ghash128>(log_message, width, log_inv_rate, seed);
271        }
272    }
273}