Skip to main content

p3_binary_pcs/whir/
shape.rs

1//! What a derived schedule fixes about the proofs it can produce.
2
3use p3_binary_field::TowerLevel;
4use p3_challenger::{FieldChallenger, GrindingChallenger};
5use p3_field::{ExtensionField, Field};
6use p3_sumcheck::ring_switch::bits::BitTensor;
7use p3_whir::WhirConfig;
8use p3_whir::transcript::query_draws;
9
10/// Everything a schedule fixes about the size of its proofs and the work of checking one.
11///
12/// Each figure is read off the derived schedule and the claim count, never off a witness.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct ProofShape {
15    /// Codeword positions opened across every round, one Merkle check each.
16    pub stir_queries: usize,
17
18    /// Alphabet elements opened from the first codeword.
19    pub opened_base_elements: usize,
20
21    /// Challenge-field elements opened from the folded codewords.
22    pub opened_extension_elements: usize,
23
24    /// Digests the proof carries, which are the folded codewords' roots and their opened paths.
25    ///
26    /// No prefix a path shares with another is discounted.
27    pub merkle_digests: usize,
28
29    /// Alphabet elements the proof carries outside the openings, which are its grinding witnesses.
30    pub sent_base_elements: usize,
31
32    /// Challenge-field elements the proof carries outside the openings.
33    pub sent_extension_elements: usize,
34
35    /// Hardest grinding any phase of the schedule demands.
36    pub grinding_bits: usize,
37}
38
39impl ProofShape {
40    /// Read the shape of every proximity opening a schedule can produce.
41    ///
42    /// The claim count fixes how many opened values travel beside the transcript.
43    #[must_use]
44    pub fn of<EF, F, Challenger>(
45        config: &WhirConfig<EF, F, Challenger>,
46        opened_values: usize,
47    ) -> Self
48    where
49        F: Field,
50        EF: ExtensionField<F>,
51        Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
52    {
53        let num_rounds = config.round_parameters().len();
54        let mut shape = Self {
55            stir_queries: 0,
56            opened_base_elements: 0,
57            opened_extension_elements: 0,
58            // One root per folded codeword, before any authentication node is counted.
59            merkle_digests: num_rounds,
60            // One witness per folding round, one per folded codeword, and one closing the run.
61            sent_base_elements: config.num_variables() + num_rounds + 1,
62            // Two coefficients bound one degree-two round, and one value closes the run.
63            sent_extension_elements: 2 * config.num_variables()
64                + (1 << config.final_sumcheck_rounds())
65                + config.commitment_ood_samples()
66                + opened_values,
67            grinding_bits: config.max_pow_bits(),
68        };
69
70        let rounds = config
71            .round_parameters()
72            .iter()
73            .map(|round| {
74                (
75                    round.log_folded_domain_size,
76                    round.folding_factor,
77                    round.num_queries,
78                    round.ood_samples,
79                )
80            })
81            .chain(core::iter::once({
82                let final_round = config.final_round_config();
83                (
84                    final_round.log_folded_domain_size,
85                    final_round.folding_factor,
86                    config.terminal().num_queries,
87                    0,
88                )
89            }));
90
91        for (index, (log_folded, folding, queries, ood)) in rounds.enumerate() {
92            // A round drawing nothing opens the whole folded domain instead.
93            let positions = if query_draws(1usize << log_folded, queries) == 0 {
94                1usize << log_folded
95            } else {
96                queries
97            };
98            let elements = positions << folding;
99
100            shape.stir_queries += positions;
101            if index == 0 {
102                shape.opened_base_elements += elements;
103            } else {
104                shape.opened_extension_elements += elements;
105            }
106            shape.merkle_digests += positions * log_folded;
107            shape.sent_extension_elements += ood;
108        }
109
110        shape
111    }
112
113    /// Read the shape of every bit-witness opening a schedule can produce for this many claims.
114    ///
115    /// One batched reduction travels beside a proximity opening of its one surviving point.
116    ///
117    /// Each claim sends its elements by rows.
118    /// The batch sends one run of degree-two rounds and the value it survives with.
119    #[must_use]
120    pub fn of_bit_readings<EF, Challenger>(
121        config: &WhirConfig<EF, EF, Challenger>,
122        num_claims: usize,
123        successor_tensors: bool,
124    ) -> Self
125    where
126        EF: Field + TowerLevel,
127        Challenger: FieldChallenger<EF> + GrindingChallenger<Witness = EF>,
128    {
129        // The batch leaves one surviving claim, however many claims it folds.
130        let surviving = num_claims.min(1);
131        let mut shape = Self::of(config, surviving);
132        // The element alone, or the element with carry and last.
133        let num_tensors = if successor_tensors { 3 } else { 1 };
134        let rows = num_tensors * BitTensor::<EF>::DIMENSION;
135        // A Boolean prefix only removes rounds, so the packing's arity bounds them.
136        shape.sent_extension_elements +=
137            num_claims * rows + surviving * (2 * config.num_variables() + 1);
138        shape
139    }
140
141    /// Bytes a proof of this shape cannot exceed, given the width of each thing it carries.
142    ///
143    /// A serializer that pads a value beyond its own width invalidates the figure.
144    ///
145    /// Framing the serializer adds around a list is the caller's to allow for.
146    #[must_use]
147    pub const fn max_bytes(
148        &self,
149        base_element_bytes: usize,
150        extension_element_bytes: usize,
151        digest_bytes: usize,
152    ) -> usize {
153        (self.opened_base_elements + self.sent_base_elements) * base_element_bytes
154            + (self.opened_extension_elements + self.sent_extension_elements)
155                * extension_element_bytes
156            + self.merkle_digests * digest_bytes
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::ProofShape;
163
164    // A hand-built shape whose byte total is computable without the implementation.
165    const SHAPE: ProofShape = ProofShape {
166        stir_queries: 40,
167        opened_base_elements: 100,
168        opened_extension_elements: 200,
169        merkle_digests: 300,
170        sent_base_elements: 10,
171        sent_extension_elements: 50,
172        grinding_bits: 16,
173    };
174
175    #[test]
176    fn the_estimate_prices_each_part_at_its_own_width() {
177        // (100 + 10)*4 + (200 + 50)*16 + 300*32 = 440 + 4000 + 9600.
178        assert_eq!(SHAPE.max_bytes(4, 16, 32), 14_040);
179    }
180
181    #[test]
182    fn a_wider_alphabet_raises_the_estimate_by_exactly_the_alphabet_run() {
183        // Only the first codeword and the grinding witnesses use the alphabet's width.
184        assert_eq!(
185            SHAPE.max_bytes(16, 16, 32) - SHAPE.max_bytes(4, 16, 32),
186            110 * (16 - 4)
187        );
188    }
189}