Skip to main content

p3_binary_field/
transcript.rs

1//! Canonical typed-transcript encodings for the byte-aligned levels of the tower.
2//!
3//! # Overview
4//!
5//! A typed transcript needs three things of a field.
6//!
7//! - A compiler-independent name for an algebra over it.
8//! - An injective way to absorb a raw byte string into a sponge that speaks it.
9//! - A fixed-width canonical wire encoding of one element.
10//!
11//! # Seeding
12//!
13//! A seed is a byte string, and the sponge here eats tower elements.
14//!
15//! The string is therefore packed one element per `wire_len` bytes, least significant byte first.
16//! A fixed-width length field goes in ahead of it, so no packed string is a prefix of another.
17//!
18//! The length is written as eight little-endian bytes, at every level.
19//!
20//! A level narrower than eight bytes spreads that field over several elements.
21//! A wider one holds it in a single zero-padded element.
22//!
23//! # Cost at the narrow levels
24//!
25//! One element carries one byte at the narrowest level, so a seed costs one absorption per byte.
26//!
27//! A typed transcript seeds on a fixed-size string, 106 bytes for the quadratic sumcheck:
28//!
29//! ```text
30//!     level     bytes per element     absorptions
31//!     8-bit     1                     114
32//!     16-bit    2                     57
33//!     128-bit   16                    8
34//! ```
35//!
36//! A protocol that seeds a sub-transcript per round pays that once per round.
37//!
38//! Only the ring-switch tests and benchmarks instantiate a sumcheck at the byte level today.
39//! Every shipped prover and verifier seeds at the 128-bit level.
40
41use alloc::vec::Vec;
42
43use p3_challenger::CanObserve;
44use p3_challenger::fs::{TranscriptError, TranscriptField, TypeTag};
45
46use crate::{
47    BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Poly64, TowerLevel,
48};
49
50/// Implement the typed-transcript hooks for one byte-aligned level of the tower.
51///
52/// The level is named by its Rust type, its backing integer, and its bit width.
53///
54/// The last argument names the algebra, which is the only part a level chooses.
55///
56/// Every level shares one seeding rule, so every level is generated from here.
57macro_rules! impl_transcript_field {
58    ($name:ty, $repr:ty, $bits:literal, |$degree:ident, $basis:ident| $tag:expr) => {
59        impl TranscriptField for $name {
60            fn algebra_tag($degree: usize, $basis: [u8; 32]) -> TypeTag {
61                $tag
62            }
63
64            fn observe_seed<C: CanObserve<Self>>(challenger: &mut C, bytes: &[u8]) {
65                // Bytes carried by one element of this level.
66                const WIDTH: usize = $bits / 8;
67
68                // Read one element off a chunk of at most `WIDTH` bytes, zero-padded on the left.
69                //
70                // The bytes are reinterpreted in the tower basis, not embedded as an integer.
71                // An integer embedding lands in GF(2) and would keep only the parity of each byte.
72                let element = |chunk: &[u8]| {
73                    let mut padded = [0u8; WIDTH];
74                    padded[..chunk.len()].copy_from_slice(chunk);
75                    <$name>::from_repr(<$repr>::from_le_bytes(padded))
76                };
77
78                // The length goes first, as eight bytes, at every level.
79                //
80                // A level narrower than eight bytes spreads it over several elements.
81                // A wider one holds it in a single zero-padded element.
82                for chunk in (bytes.len() as u64).to_le_bytes().chunks(WIDTH) {
83                    challenger.observe(element(chunk));
84                }
85
86                // Then the string itself, one element per chunk.
87                for chunk in bytes.chunks(WIDTH) {
88                    challenger.observe(element(chunk));
89                }
90            }
91
92            fn wire_len() -> usize {
93                $bits / 8
94            }
95
96            fn encode(value: &Self, out: &mut Vec<u8>) {
97                out.extend_from_slice(&value.to_repr().to_be_bytes());
98            }
99
100            fn decode(bytes: &[u8]) -> Result<Self, TranscriptError> {
101                let prefix = bytes
102                    .get(..$bits / 8)
103                    .ok_or(TranscriptError::BadProofShape {
104                        reason: "not enough bytes for a canonical binary field encoding",
105                    })?;
106                // The element is exactly as wide as its backing integer, so every pattern is canonical.
107                Ok(Self::from_repr(<$repr>::from_be_bytes(
108                    prefix.try_into().unwrap(),
109                )))
110            }
111        }
112    };
113}
114
115impl_transcript_field!(BinaryField8, u8, 8, |degree, basis| TypeTag::BinaryTower {
116    bits: 8,
117    degree,
118    basis,
119});
120impl_transcript_field!(BinaryField16, u16, 16, |degree, basis| {
121    TypeTag::BinaryTower {
122        bits: 16,
123        degree,
124        basis,
125    }
126});
127impl_transcript_field!(BinaryField32, u32, 32, |degree, basis| {
128    TypeTag::BinaryTower {
129        bits: 32,
130        degree,
131        basis,
132    }
133});
134impl_transcript_field!(BinaryField64, u64, 64, |degree, basis| {
135    TypeTag::BinaryTower {
136        bits: 64,
137        degree,
138        basis,
139    }
140});
141impl_transcript_field!(BinaryField128, u128, 128, |degree, basis| {
142    TypeTag::BinaryTower {
143        bits: 128,
144        degree,
145        basis,
146    }
147});
148impl_transcript_field!(Poly64, u64, 64, |degree, basis| TypeTag::BinaryPolynomial {
149    bits: 64,
150    modulus: 0x1b,
151    degree,
152    basis,
153});
154
155#[cfg(test)]
156mod tests {
157    use alloc::vec;
158    use alloc::vec::Vec;
159
160    use p3_challenger::testing::Recorder;
161
162    use super::*;
163
164    /// Width of the length field written ahead of a seed, in bytes.
165    const SEED_LEN_BYTES: usize = 8;
166
167    /// The elements one byte string packs into at a given level.
168    fn packed<F: TranscriptField>(bytes: &[u8]) -> Vec<F> {
169        let mut recorder = Recorder::default();
170        F::observe_seed(&mut recorder, bytes);
171        recorder.into_absorbed()
172    }
173
174    #[test]
175    fn a_seed_carries_its_length_ahead_of_its_bytes() {
176        // At the narrowest level one element holds one byte.
177        //
178        //     length 3, little-endian over 8 bytes  ->  3, 0, 0, 0, 0, 0, 0, 0
179        //     then the string itself                ->  1, 2, 3
180        let seen = packed::<BinaryField8>(&[1, 2, 3]);
181
182        assert_eq!(seen.len(), SEED_LEN_BYTES + 3);
183        assert_eq!(seen[0], BinaryField8::from_repr(3));
184        assert_eq!(&seen[SEED_LEN_BYTES..], &packed_bytes(&[1, 2, 3])[..]);
185    }
186
187    /// The tail elements of a packing, without the length field in front.
188    fn packed_bytes(bytes: &[u8]) -> Vec<BinaryField8> {
189        bytes.iter().map(|&b| BinaryField8::from_repr(b)).collect()
190    }
191
192    #[test]
193    fn a_shorter_string_is_never_a_prefix_of_a_longer_one() {
194        // Invariant: absorption is injective, so a padded string cannot impersonate another.
195        //
196        // Without the length field, `[1]` and `[1, 0]` would pack to the same elements at
197        // every level whose width divides neither.
198        assert_ne!(
199            packed::<BinaryField8>(&[1]),
200            packed::<BinaryField8>(&[1, 0])
201        );
202        assert_ne!(
203            packed::<BinaryField16>(&[1]),
204            packed::<BinaryField16>(&[1, 0])
205        );
206        assert_ne!(
207            packed::<BinaryField32>(&[1]),
208            packed::<BinaryField32>(&[1, 0])
209        );
210        assert_ne!(
211            packed::<BinaryField64>(&[1]),
212            packed::<BinaryField64>(&[1, 0])
213        );
214        assert_ne!(
215            packed::<BinaryField128>(&[1]),
216            packed::<BinaryField128>(&[1, 0])
217        );
218    }
219
220    #[test]
221    fn the_widest_level_packs_a_seed_by_the_same_rule_as_the_narrow_ones() {
222        // Invariant: one seeding rule covers the whole tower, so the widest level is not special.
223        //
224        // Sixteen bytes fit one element, so a 20-byte seed spans two of them.
225        //
226        //     length 20, little-endian over 8 bytes, zero-padded to 16  ->  1 element
227        //     bytes 0..16                                               ->  1 element
228        //     bytes 16..20, zero-padded on the right                    ->  1 element
229        let bytes: Vec<u8> = (0..20).collect();
230        let seen = packed::<BinaryField128>(&bytes);
231
232        assert_eq!(seen.len(), 3);
233        assert_eq!(seen[0], BinaryField128::from_repr(20));
234
235        // The payload elements read the chunk least significant byte first.
236        let mut first = [0u8; 16];
237        first.copy_from_slice(&bytes[..16]);
238        assert_eq!(
239            seen[1],
240            BinaryField128::from_repr(u128::from_le_bytes(first))
241        );
242
243        let mut last = [0u8; 16];
244        last[..4].copy_from_slice(&bytes[16..]);
245        assert_eq!(
246            seen[2],
247            BinaryField128::from_repr(u128::from_le_bytes(last))
248        );
249    }
250
251    #[test]
252    fn a_wire_encoding_round_trips_at_every_level() {
253        // Fixture: one element per level, written out and read back.
254        let mut out = Vec::new();
255        BinaryField8::encode(&BinaryField8::from_repr(0xA5), &mut out);
256        assert_eq!(out, vec![0xA5]);
257        assert_eq!(
258            BinaryField8::decode(&out).unwrap(),
259            BinaryField8::from_repr(0xA5)
260        );
261
262        let mut out = Vec::new();
263        BinaryField16::encode(&BinaryField16::from_repr(0xA5C3), &mut out);
264        assert_eq!(out, vec![0xA5, 0xC3]);
265        assert_eq!(
266            BinaryField16::decode(&out).unwrap(),
267            BinaryField16::from_repr(0xA5C3)
268        );
269
270        let mut out = Vec::new();
271        BinaryField32::encode(&BinaryField32::from_repr(0x0123_4567), &mut out);
272        assert_eq!(
273            BinaryField32::decode(&out).unwrap(),
274            BinaryField32::from_repr(0x0123_4567)
275        );
276
277        let mut out = Vec::new();
278        BinaryField64::encode(&BinaryField64::from_repr(0x0123_4567_89AB_CDEF), &mut out);
279        assert_eq!(
280            BinaryField64::decode(&out).unwrap(),
281            BinaryField64::from_repr(0x0123_4567_89AB_CDEF)
282        );
283
284        let mut out = Vec::new();
285        Poly64::encode(&Poly64::new(0xFEDC_BA98_7654_3210), &mut out);
286        assert_eq!(
287            Poly64::decode(&out).unwrap(),
288            Poly64::new(0xFEDC_BA98_7654_3210)
289        );
290    }
291
292    #[test]
293    fn a_short_wire_encoding_is_rejected() {
294        // The caller owes a whole element; a truncated one is malformed input, not a panic.
295        assert!(BinaryField64::decode(&[0; 7]).is_err());
296        assert!(BinaryField32::decode(&[0; 3]).is_err());
297        assert!(BinaryField16::decode(&[0; 1]).is_err());
298        assert!(BinaryField8::decode(&[]).is_err());
299        assert!(Poly64::decode(&[0; 7]).is_err());
300    }
301
302    #[test]
303    fn each_level_names_its_own_width() {
304        // Two levels must never share an algebra tag, or one could stand in for the other.
305        assert_eq!(
306            BinaryField8::algebra_tag(1, [0; 32]),
307            TypeTag::BinaryTower {
308                bits: 8,
309                degree: 1,
310                basis: [0; 32]
311            }
312        );
313        assert_ne!(
314            BinaryField8::algebra_tag(1, [0; 32]),
315            BinaryField16::algebra_tag(1, [0; 32])
316        );
317        assert_ne!(
318            BinaryField32::algebra_tag(1, [0; 32]),
319            BinaryField64::algebra_tag(1, [0; 32])
320        );
321        assert_ne!(
322            BinaryField64::algebra_tag(1, [0; 32]),
323            BinaryField128::algebra_tag(1, [0; 32])
324        );
325        assert_ne!(
326            BinaryField64::algebra_tag(1, [0; 32]),
327            Poly64::algebra_tag(1, [0; 32])
328        );
329    }
330}