Skip to main content

p3_security/
word.rs

1//! Soundness terms for the word-level relation proof.
2
3use alloc::vec::Vec;
4
5use crate::{CandidateSet, ErrorBits, SecurityTerm};
6
7/// Label for the complete word shift reduction error.
8pub const WORD_SHIFT_LABEL: &str = "word-shift-reduction";
9
10/// Label for batching relation families and operand positions.
11pub const WORD_SHIFT_BATCHING_LABEL: &str = "word-shift-batching";
12
13/// Label for the two quadratic sumchecks.
14pub const WORD_SHIFT_SUMCHECK_LABEL: &str = "word-shift-sumcheck";
15
16/// Checked dimensions of one word shift reduction.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct WordShiftSecurityModel {
19    /// Lower bound on the base-two logarithm of the challenge-field order.
20    field_bits: usize,
21    /// Variables selecting a relation family or operand position.
22    batching_variables: usize,
23    /// Quadratic rounds over bit and word indices.
24    sumcheck_rounds: usize,
25}
26
27impl WordShiftSecurityModel {
28    /// Creates a model when the challenge field has a nonzero security width.
29    #[must_use]
30    pub const fn new(
31        field_bits: usize,
32        batching_variables: usize,
33        sumcheck_rounds: usize,
34    ) -> Option<Self> {
35        // A zero-bit challenge field cannot support a statistical reduction.
36        if field_bits == 0 {
37            return None;
38        }
39
40        Some(Self {
41            field_bits,
42            batching_variables,
43            sumcheck_rounds,
44        })
45    }
46
47    /// Returns each algebraic error source for diagnostic reporting.
48    #[must_use]
49    pub fn components(self) -> Vec<SecurityTerm> {
50        // Evaluating the batching multilinear at a random point costs its total degree.
51        let mut terms = Vec::with_capacity(2);
52        if self.batching_variables != 0 {
53            terms.push(SecurityTerm::new(
54                WORD_SHIFT_BATCHING_LABEL,
55                error_from_numerator(self.field_bits, self.batching_variables as u128),
56            ));
57        }
58
59        // Each quadratic sumcheck round contributes at most two roots.
60        if self.sumcheck_rounds != 0 {
61            terms.push(SecurityTerm::new(
62                WORD_SHIFT_SUMCHECK_LABEL,
63                error_from_numerator(self.field_bits, 2 * self.sumcheck_rounds as u128),
64            ));
65        }
66
67        terms
68    }
69
70    /// Returns the union bound used by an enclosing proof system.
71    #[must_use]
72    pub fn combined_term(self) -> SecurityTerm {
73        // Compose the independently labelled failure events by probability addition.
74        let errors = self
75            .components()
76            .iter()
77            .map(|component| component.bits)
78            .collect::<Vec<_>>();
79        SecurityTerm::new(WORD_SHIFT_LABEL, ErrorBits::sum(&errors))
80    }
81}
82
83/// Label for the complete full-width unsigned multiplication reduction error.
84pub const WORD_INTEGER_MUL_LABEL: &str = "word-integer-mul";
85
86/// Label for the row point the two exponent lifts are compared at.
87pub const WORD_INTEGER_MUL_POINT_LABEL: &str = "word-integer-mul-point";
88
89/// Label for the layer, line, and leaf checks of both product trees.
90pub const WORD_INTEGER_MUL_PRODUCT_LABEL: &str = "word-integer-mul-product-check";
91
92/// Per-variable degree of every sumcheck the multiplication reduction runs.
93const INTEGER_MUL_DEGREE: u128 = 3;
94
95/// Checked dimensions of one full-width unsigned multiplication reduction.
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub struct WordIntegerMulSecurityModel {
98    /// Lower bound on the base-two logarithm of the challenge-field order.
99    field_bits: usize,
100    /// Variables selecting one padded multiplication row.
101    row_variables: usize,
102    /// Variables selecting one bit within a word.
103    bit_variables: usize,
104}
105
106impl WordIntegerMulSecurityModel {
107    /// Creates a model when the challenge field has a nonzero security width.
108    #[must_use]
109    pub const fn new(
110        field_bits: usize,
111        row_variables: usize,
112        bit_variables: usize,
113    ) -> Option<Self> {
114        // A zero-bit challenge field cannot support a statistical reduction.
115        if field_bits == 0 {
116            return None;
117        }
118
119        Some(Self {
120            field_bits,
121            row_variables,
122            bit_variables,
123        })
124    }
125
126    /// Returns the error numerator of one product tree of the given depth.
127    ///
128    /// ```text
129    ///     layer d      cubic sumcheck over m + d variables, then one line draw
130    ///     leaf check   cubic sumcheck over m + depth variables
131    ///
132    ///     numerator = sum_{d < depth} (3 * (m + d) + 1) + 3 * (m + depth)
133    /// ```
134    const fn tree_numerator(&self, depth: usize) -> u128 {
135        let rows = self.row_variables as u128;
136        let depth = depth as u128;
137
138        // The layer sum of 3 * (m + d) is 3 * (m * depth + depth * (depth - 1) / 2).
139        let layers = INTEGER_MUL_DEGREE * (rows * depth + depth * depth.saturating_sub(1) / 2);
140        layers + depth + INTEGER_MUL_DEGREE * (rows + depth)
141    }
142
143    /// Returns each algebraic error source for diagnostic reporting.
144    #[must_use]
145    pub fn components(self) -> Vec<SecurityTerm> {
146        let mut terms = Vec::with_capacity(2);
147
148        // Two lifts differing on some row agree at a random row point only at a root.
149        if self.row_variables != 0 {
150            terms.push(SecurityTerm::new(
151                WORD_INTEGER_MUL_POINT_LABEL,
152                error_from_numerator(self.field_bits, self.row_variables as u128),
153            ));
154        }
155
156        // The factor tree spans two bit indices and the result tree one index over twice the width.
157        let numerator = self.tree_numerator(2 * self.bit_variables)
158            + self.tree_numerator(self.bit_variables + 1);
159        terms.push(SecurityTerm::new(
160            WORD_INTEGER_MUL_PRODUCT_LABEL,
161            error_from_numerator(self.field_bits, numerator),
162        ));
163
164        terms
165    }
166
167    /// Returns the union bound used by an enclosing proof system.
168    #[must_use]
169    pub fn combined_term(self) -> SecurityTerm {
170        // Compose the independently labelled failure events by probability addition.
171        let errors = self
172            .components()
173            .iter()
174            .map(|component| component.bits)
175            .collect::<Vec<_>>();
176        SecurityTerm::new(WORD_INTEGER_MUL_LABEL, ErrorBits::sum(&errors))
177    }
178}
179
180/// Label for the complete word-level relation proof error.
181pub const WORD_PROOF_LABEL: &str = "word-proof";
182
183/// Label for separating the relation families of one statement.
184pub const WORD_RELATION_BATCHING_LABEL: &str = "word-relation-batching";
185
186/// Label for the point the batched relations are required to vanish at.
187pub const WORD_ZEROCHECK_POINT_LABEL: &str = "word-relation-zerocheck-point";
188
189/// Label for the rounds of the batched relation vanishing check.
190pub const WORD_ZEROCHECK_ROUNDS_LABEL: &str = "word-relation-zerocheck-rounds";
191
192/// Checked dimensions of one complete word-level relation proof.
193#[derive(Clone, Debug, PartialEq)]
194pub struct WordProofSecurityModel {
195    /// Lower bound on the base-two logarithm of the challenge-field order.
196    field_bits: usize,
197    /// Relation terms separated by the powers of one batching coefficient.
198    relation_families: usize,
199    /// Constraint and within-word variables the vanishing check binds.
200    zerocheck_variables: usize,
201    /// Per-variable degree of the batched relation polynomial.
202    zerocheck_degree: usize,
203    /// Dimensions of the multiplication reduction that precedes the vanishing check, if any.
204    integer_mul: Option<WordIntegerMulSecurityModel>,
205    /// Dimensions of the shift reduction that follows.
206    shift: WordShiftSecurityModel,
207    /// Labelled errors the commitment charges for discharging the one surviving claim.
208    commitment: Vec<SecurityTerm>,
209    /// The candidates the commitment still leaves open while this proof draws.
210    ///
211    /// Every draw below lands after the commitment and before the opening names one.
212    ///
213    /// Each is charged over this set, and the set itself comes through untouched.
214    candidates: CandidateSet,
215}
216
217impl WordProofSecurityModel {
218    /// Creates a model when every sampled experiment has a well-formed shape.
219    ///
220    /// The width argument is a lower bound on the base-two logarithm of the challenge-field order.
221    ///
222    /// Every batching, vanishing, and reduction challenge must be drawn from that same field.
223    ///
224    /// The last two arguments both come from the commitment.
225    ///
226    /// They are what it charges, and how many candidates it still leaves open.
227    ///
228    /// A statement without multiplication relations passes no multiplication model.
229    #[must_use]
230    #[allow(
231        clippy::too_many_arguments,
232        reason = "each argument is one independently derived stage of the schedule"
233    )]
234    pub fn new(
235        field_bits: usize,
236        relation_families: usize,
237        zerocheck_variables: usize,
238        zerocheck_degree: usize,
239        integer_mul: Option<WordIntegerMulSecurityModel>,
240        shift: WordShiftSecurityModel,
241        commitment: Vec<SecurityTerm>,
242        log2_candidates: f64,
243    ) -> Option<Self> {
244        // A degree-zero composition carries no round polynomial to separate against.
245        if field_bits == 0 || zerocheck_degree == 0 {
246            return None;
247        }
248
249        // A count that is not a real size prices nothing, so no number is reported at all.
250        let candidates = CandidateSet::from_log2(log2_candidates)?;
251
252        Some(Self {
253            field_bits,
254            relation_families,
255            zerocheck_variables,
256            zerocheck_degree,
257            integer_mul,
258            shift,
259            commitment,
260            candidates,
261        })
262    }
263
264    /// Returns each algebraic error source for diagnostic reporting.
265    #[must_use]
266    pub fn components(&self) -> Vec<SecurityTerm> {
267        let mut terms = Vec::new();
268
269        // The multiplication reduction runs first and publishes its own labelled experiments.
270        if let Some(integer_mul) = self.integer_mul {
271            terms.extend(integer_mul.components());
272        }
273
274        // Powers of one coefficient separate the terms, so its degree is one below their count.
275        let separated = self.relation_families.saturating_sub(1);
276        if separated != 0 {
277            terms.push(SecurityTerm::new(
278                WORD_RELATION_BATCHING_LABEL,
279                error_from_numerator(self.field_bits, separated as u128),
280            ));
281        }
282
283        // A relation that fails somewhere on the cube survives only at a root of its extension.
284        if self.zerocheck_variables != 0 {
285            terms.push(SecurityTerm::new(
286                WORD_ZEROCHECK_POINT_LABEL,
287                error_from_numerator(self.field_bits, self.zerocheck_variables as u128),
288            ));
289            terms.push(SecurityTerm::new(
290                WORD_ZEROCHECK_ROUNDS_LABEL,
291                error_from_numerator(
292                    self.field_bits,
293                    self.zerocheck_degree as u128 * self.zerocheck_variables as u128,
294                ),
295            ));
296        }
297
298        // The shift reduction publishes its own separately labelled experiments.
299        terms.extend(self.shift.components());
300
301        // Every draw above lands after the commitment and before the opening names one.
302        //
303        // A prover may therefore choose which candidate it is after seeing them.
304        for term in &mut terms {
305            *term = term.over_candidates(self.candidates);
306        }
307
308        // The commitment prices the ring switch and its own opening from its own schedule.
309        //
310        // Those terms arrive already charged by the layer that drew them.
311        //
312        // So this proof carries them through rather than charging the same set twice.
313        terms.extend(self.commitment.iter().copied());
314
315        terms
316    }
317
318    /// Returns the union bound over every experiment the proof runs.
319    #[must_use]
320    pub fn combined_term(&self) -> SecurityTerm {
321        // Compose the independently labelled failure events by probability addition.
322        let errors = self
323            .components()
324            .iter()
325            .map(|component| component.bits)
326            .collect::<Vec<_>>();
327        SecurityTerm::new(WORD_PROOF_LABEL, ErrorBits::sum(&errors))
328    }
329}
330
331/// Converts an exact numerator over the challenge-field order into bits.
332fn error_from_numerator(field_bits: usize, numerator: u128) -> ErrorBits {
333    // Round upward so floating-point conversion cannot understate the error.
334    let mut rounded = numerator as f64;
335    if rounded as u128 != numerator {
336        rounded = rounded.next_up();
337    }
338    ErrorBits::from_log2((field_bits as f64 - libm::log2(rounded)).max(0.0))
339}
340
341#[cfg(test)]
342mod tests {
343    use alloc::vec;
344
345    use super::*;
346    use crate::binary::BinaryPcsRegime;
347    use crate::multilinear::bit_ring_switch_term;
348
349    #[test]
350    fn word64_profile_charges_both_random_experiments() {
351        // Fixture state:
352        //
353        // ```text
354        //     batching point   2 operation bits + 2 operand bits
355        //     bit sumcheck     6 quadratic rounds
356        //     word sumcheck   20 quadratic rounds
357        // ```
358        let model = WordShiftSecurityModel::new(128, 4, 26).unwrap();
359        let components = model.components();
360
361        // Four batching variables give the numerator four.
362        assert_eq!(components[0].label, WORD_SHIFT_BATCHING_LABEL);
363        assert_eq!(components[0].bits.bits(), 126.0);
364
365        // Twenty-six quadratic rounds give the numerator fifty-two.
366        assert_eq!(components[1].label, WORD_SHIFT_SUMCHECK_LABEL);
367        assert_eq!(components[1].bits.bits(), 128.0 - libm::log2(52.0));
368
369        // The composable term is the union of both events.
370        let expected = ErrorBits::sum(&[components[0].bits, components[1].bits]);
371        assert_eq!(model.combined_term().bits, expected);
372    }
373
374    #[test]
375    fn the_complete_proof_charges_every_stage_once() {
376        // Fixture state:
377        //
378        // - two relation families, so one separating coefficient;
379        // - nine vanishing variables bound at per-variable degree three;
380        // - four shift batching variables and twenty-six quadratic rounds;
381        // - a commitment charging a ring switch beside its own single opening.
382        let shift = WordShiftSecurityModel::new(128, 4, 26).unwrap();
383        let pcs = BinaryPcsRegime::new(128, 6, 2, 1, 40, 0).unwrap();
384        let commitment = vec![bit_ring_switch_term(1, 7, 6, 128), pcs.opening_term(1)];
385        let model = WordProofSecurityModel::new(128, 2, 9, 3, None, shift, commitment.clone(), 0.0)
386            .unwrap();
387        let components = model.components();
388
389        // One coefficient separates two families, so its numerator is one.
390        assert_eq!(components[0].label, WORD_RELATION_BATCHING_LABEL);
391        assert_eq!(components[0].bits.bits(), 128.0);
392
393        // Nine variables give the vanishing point the numerator nine.
394        assert_eq!(components[1].label, WORD_ZEROCHECK_POINT_LABEL);
395        assert_eq!(components[1].bits.bits(), 128.0 - libm::log2(9.0));
396
397        // Nine cubic rounds give the numerator twenty-seven.
398        assert_eq!(components[2].label, WORD_ZEROCHECK_ROUNDS_LABEL);
399        assert_eq!(components[2].bits.bits(), 128.0 - libm::log2(27.0));
400
401        // Four shift batching variables give the numerator four.
402        assert_eq!(components[3].label, WORD_SHIFT_BATCHING_LABEL);
403        assert_eq!(components[3].bits.bits(), 126.0);
404
405        // Twenty-six quadratic shift rounds give the numerator fifty-two.
406        assert_eq!(components[4].label, WORD_SHIFT_SUMCHECK_LABEL);
407        assert_eq!(components[4].bits.bits(), 128.0 - libm::log2(52.0));
408
409        // The commitment's own terms close the list, in the order it supplied them.
410        assert_eq!(components[5..], commitment);
411        assert_eq!(components.len(), 7);
412
413        // The composable term is the union of every event.
414        let errors = components
415            .iter()
416            .map(|component| component.bits)
417            .collect::<Vec<_>>();
418        assert_eq!(model.combined_term().bits, ErrorBits::sum(&errors));
419    }
420
421    #[test]
422    fn a_proof_model_rejects_a_shape_it_cannot_charge_honestly() {
423        let shift = WordShiftSecurityModel::new(128, 4, 26).unwrap();
424
425        // A challenge field must expose at least one bit of entropy.
426        assert!(WordProofSecurityModel::new(0, 2, 9, 3, None, shift, Vec::new(), 0.0).is_none());
427
428        // A degree-zero composition carries no round polynomial to separate against.
429        assert!(WordProofSecurityModel::new(128, 2, 9, 0, None, shift, Vec::new(), 0.0).is_none());
430
431        // A candidate count that names no real set size prices nothing.
432        for count in [f64::NAN, f64::INFINITY, -1.0] {
433            assert!(
434                WordProofSecurityModel::new(128, 2, 9, 3, None, shift, Vec::new(), count).is_none()
435            );
436        }
437    }
438
439    #[test]
440    fn candidates_left_open_are_charged_to_every_draw_that_precedes_them() {
441        // Fixture state: the same shape, priced against a commitment leaving 2^5 candidates.
442        let shift = WordShiftSecurityModel::new(128, 4, 26).unwrap();
443        let pcs = BinaryPcsRegime::new(128, 6, 2, 1, 40, 0).unwrap();
444        let commitment = vec![bit_ring_switch_term(1, 7, 6, 128), pcs.opening_term(1)];
445        let settled =
446            WordProofSecurityModel::new(128, 2, 9, 3, None, shift, commitment.clone(), 0.0)
447                .unwrap();
448        let open = WordProofSecurityModel::new(128, 2, 9, 3, None, shift, commitment.clone(), 5.0)
449            .unwrap();
450
451        // Each of the proof's own five draws loses exactly the candidate bound.
452        let before = settled.components();
453        let after = open.components();
454        for (settled, open) in before.iter().zip(&after).take(5) {
455            assert_eq!(open.label, settled.label);
456            assert_eq!(open.bits.bits(), settled.bits.bits() - 5.0);
457        }
458
459        // The commitment already charged its own reductions, so its terms do not move.
460        assert_eq!(after[5..], commitment);
461        assert_eq!(before[5..], commitment);
462    }
463
464    #[test]
465    fn a_multiplication_reduction_charges_its_point_and_both_trees() {
466        // Fixture state: eight padded rows of 64-bit products.
467        //
468        //     factor tree   depth 12, layer d over 3 + d variables, leaf over 15
469        //     result tree   depth 7,  layer d over 3 + d variables, leaf over 10
470        let model = WordIntegerMulSecurityModel::new(128, 3, 6).unwrap();
471        let components = model.components();
472
473        // Three row variables give the comparison point the numerator three.
474        assert_eq!(components[0].label, WORD_INTEGER_MUL_POINT_LABEL);
475        assert_eq!(components[0].bits.bits(), 128.0 - libm::log2(3.0));
476
477        // Factor tree: 3 * (36 + 66) + 12 + 45 = 363.
478        // Result tree: 3 * (21 + 21) + 7 + 30 = 163.
479        assert_eq!(components[1].label, WORD_INTEGER_MUL_PRODUCT_LABEL);
480        assert_eq!(components[1].bits.bits(), 128.0 - libm::log2(526.0));
481
482        // A single row needs no comparison point, but both trees still run.
483        let single = WordIntegerMulSecurityModel::new(128, 0, 6).unwrap();
484        let components = single.components();
485        assert_eq!(components.len(), 1);
486        assert_eq!(components[0].label, WORD_INTEGER_MUL_PRODUCT_LABEL);
487
488        // A challenge field must expose at least one bit of entropy.
489        assert_eq!(WordIntegerMulSecurityModel::new(0, 3, 6), None);
490    }
491
492    #[test]
493    fn the_multiplication_terms_precede_the_vanishing_check() {
494        // Fixture state: the complete proof above, now also proving 64-bit products.
495        let shift = WordShiftSecurityModel::new(128, 4, 26).unwrap();
496        let integer_mul = WordIntegerMulSecurityModel::new(128, 3, 6).unwrap();
497        let model =
498            WordProofSecurityModel::new(128, 7, 9, 3, Some(integer_mul), shift, Vec::new(), 2.0)
499                .unwrap();
500        let components = model.components();
501
502        // The transcript runs the products first, so their terms lead the list.
503        let labels = components.iter().map(|term| term.label).collect::<Vec<_>>();
504        assert_eq!(
505            labels,
506            [
507                WORD_INTEGER_MUL_POINT_LABEL,
508                WORD_INTEGER_MUL_PRODUCT_LABEL,
509                WORD_RELATION_BATCHING_LABEL,
510                WORD_ZEROCHECK_POINT_LABEL,
511                WORD_ZEROCHECK_ROUNDS_LABEL,
512                WORD_SHIFT_BATCHING_LABEL,
513                WORD_SHIFT_SUMCHECK_LABEL,
514            ]
515        );
516
517        // Seven batched terms under powers of one coefficient give the numerator six.
518        assert_eq!(components[2].bits.bits(), 128.0 - libm::log2(6.0) - 2.0);
519
520        // The products are drawn before the opening too, so they pay the candidate bound.
521        let expected = integer_mul.components();
522        assert_eq!(components[0].bits.bits(), expected[0].bits.bits() - 2.0);
523        assert_eq!(components[1].bits.bits(), expected[1].bits.bits() - 2.0);
524    }
525
526    #[test]
527    fn deterministic_shape_omits_empty_terms() {
528        // No sampled variable means no statistical failure event.
529        let model = WordShiftSecurityModel::new(128, 0, 0).unwrap();
530        assert!(model.components().is_empty());
531
532        // A challenge field must expose at least one bit of entropy.
533        assert_eq!(WordShiftSecurityModel::new(0, 4, 6), None);
534    }
535}