Skip to main content

hekate_program/
outer.rs

1// SPDX-FileCopyrightText: 2026 Andrei Kochergin <andrei@oumuamua.dev>
2// SPDX-FileCopyrightText: 2026 Oumuamua Labs <info@oumuamua.dev>
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use crate::chiplet::ChipletDef;
6use crate::constraint::{ConstraintAst, ConstraintExpr, ExprId};
7use crate::expander::{RING_BLIND_BITS, RingSwitchPlan, claim_weights, eq_tensor_b};
8use crate::linearized::{self, RingGadget, linearized_coeffs};
9use crate::permutation::{BusKind, Source, eval_row_idx_byte_mle, eval_row_idx_le_mle};
10use crate::predicate::{AffineRow, ClaimLayout, Form, PredicateRows, Unknown, WireRole, compile};
11use crate::{Air, ProgramInstance};
12use alloc::collections::BTreeMap;
13use alloc::string::String;
14use alloc::vec;
15use alloc::vec::Vec;
16use core::mem;
17use hekate_core::errors;
18use hekate_core::outer::{OUTER_MASK_ROWS, OuterGeometry};
19use hekate_core::poly::univariate::{MAX_POINTS, UnivariatePoly};
20use hekate_core::tensor::TensorProduct;
21use hekate_math::{Block128, Flat, HardwareField, TowerField};
22
23pub struct LinearBatch<F> {
24    pub weights: Vec<Vec<Flat<F>>>,
25    pub rows: Vec<usize>,
26    pub target: Flat<F>,
27}
28
29pub enum BusSource<F> {
30    Claim(u32),
31    Public(Flat<F>),
32}
33
34pub struct BusRow<F> {
35    pub h_pad: u32,
36    pub h_masked: Flat<F>,
37    pub h_wire: u32,
38    pub sources: Vec<BusSource<F>>,
39    pub selector: Option<u32>,
40    pub recv_selector: Option<u32>,
41    pub eq_lookup: Flat<F>,
42}
43
44pub struct ConsistencyInputs<F> {
45    /// Pad index of claim 0, matching `ClaimLayout`.
46    pub pad_first: u32,
47
48    pub eq_zc: Flat<F>,
49    pub alpha: Flat<F>,
50    pub gamma: Flat<F>,
51    pub beta: Flat<F>,
52
53    /// `(claim index, public value, eq(r_final, row))`.
54    pub boundary: Vec<(u32, Flat<F>, Flat<F>)>,
55
56    /// `(b_k claim index, b_k_next claim index)`.
57    pub telescope: Vec<(u32, u32)>,
58
59    pub buses: Vec<BusRow<F>>,
60
61    /// Mask form of the ZeroCheck's final claim,
62    /// and the masked value the replay reaches.
63    pub val_final_form: Vec<(Unknown, Flat<F>)>,
64    pub val_final_masked: Flat<F>,
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub struct TableShape {
69    pub num_vars: usize,
70
71    /// Virtual columns, after expansion.
72    pub num_columns: usize,
73
74    pub sumcheck_degree: usize,
75    pub num_buses: usize,
76    pub mul_nodes: usize,
77    pub ring_units: bool,
78}
79
80impl TableShape {
81    pub fn from_air<F: TowerField>(
82        air: &impl Air<F>,
83        num_vars: usize,
84        ast: &ConstraintAst<F>,
85    ) -> errors::Result<Self> {
86        let num_buses = air.permutation_checks().len();
87
88        let mut sumcheck_degree = ast.max_degree() + 1;
89
90        if !air.boundary_constraints().is_empty() {
91            sumcheck_degree = sumcheck_degree.max(2);
92        }
93
94        if num_buses > 0 {
95            sumcheck_degree = sumcheck_degree.max(3);
96        }
97
98        if sumcheck_degree + 1 > MAX_POINTS {
99            return Err(errors::Error::Protocol {
100                protocol: "air",
101                message: "sumcheck degree exceeds the interpolation stack bound",
102            });
103        }
104
105        if air.num_columns() != air.virtual_column_layout().len() {
106            return Err(errors::Error::Protocol {
107                protocol: "air",
108                message: "num_columns does not match the virtual column layout",
109            });
110        }
111
112        let entries = air.virtual_expander().map(|e| e.expansion_entries());
113        let plan = RingSwitchPlan::new(air.column_layout(), entries.as_deref(), 0)?;
114
115        Ok(Self {
116            num_vars,
117            num_columns: air.num_columns(),
118            sumcheck_degree,
119            num_buses,
120            mul_nodes: mul_node_count(ast),
121            ring_units: plan.has_ring(),
122        })
123    }
124
125    /// Eval claims per half: virtual columns, whole
126    /// blind units, the ring blind unit's planes.
127    pub fn eval_claims(&self, blinding_columns: usize) -> usize {
128        let ring_blind = match self.ring_units && blinding_columns > 0 {
129            true => RING_BLIND_BITS,
130            false => 0,
131        };
132
133        self.num_columns + blinding_columns + ring_blind
134    }
135
136    /// Final sumcheck values are the masked running
137    /// claims themselves and consume no pad entry.
138    pub fn masked_scalars(&self, blinding_columns: usize) -> usize {
139        let zerocheck = self.num_vars * self.sumcheck_degree;
140        let claims = 2 * self.eval_claims(blinding_columns);
141        let trace_eval = 2 * self.num_vars;
142
143        // The h opening's bus claims
144        // reuse the h_eval pad entries.
145        let h_open = match self.num_buses {
146            0 => 0,
147            _ => blinding_columns + 2 * self.num_vars,
148        };
149
150        2 * self.num_buses + zerocheck + h_open + claims + trace_eval
151    }
152
153    /// Each bus contributes one `h · key` product;
154    /// ring units add the gadget's squaring chain.
155    pub fn mul_wires(&self) -> usize {
156        let gadget = match self.ring_units {
157            true => linearized::BITS - 1,
158            false => 0,
159        };
160
161        self.mul_nodes + self.num_buses + gadget
162    }
163}
164
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
166pub struct OuterStatement {
167    pub masked_scalars: usize,
168    pub mul_wires: usize,
169}
170
171impl OuterStatement {
172    pub fn new(shapes: &[TableShape], blinding_columns: usize) -> Self {
173        Self {
174            masked_scalars: shapes
175                .iter()
176                .map(|s| s.masked_scalars(blinding_columns))
177                .sum(),
178            mul_wires: shapes.iter().map(TableShape::mul_wires).sum(),
179        }
180    }
181
182    /// Statement size for a whole proof.
183    pub fn for_tables<F: TowerField, A: Air<F>>(
184        main: &A,
185        main_num_vars: usize,
186        chiplets: &[ChipletDef<F>],
187        chiplet_num_vars: &[usize],
188        blinding_columns: usize,
189    ) -> errors::Result<Self> {
190        if chiplets.len() != chiplet_num_vars.len() {
191            return Err(errors::Error::Protocol {
192                protocol: "outer",
193                message: "one height per chiplet is required",
194            });
195        }
196
197        let mut shapes = vec![TableShape::from_air(
198            main,
199            main_num_vars,
200            &main.constraint_ast(),
201        )?];
202
203        for (def, &num_vars) in chiplets.iter().zip(chiplet_num_vars) {
204            shapes.push(TableShape::from_air(def, num_vars, &def.constraint_ast())?);
205        }
206
207        Ok(Self::new(&shapes, blinding_columns))
208    }
209}
210
211/// Oracle row order: pad, then the `Lhs`, `Rhs` and
212/// `Product` wire blocks, then the five mask rows.
213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214pub struct OuterLayout {
215    pub message_len: usize,
216    pub pad_rows: usize,
217    pub aux_rows: usize,
218    pub domain_len: usize,
219}
220
221impl OuterLayout {
222    pub fn new(
223        geom: &OuterGeometry,
224        masked_scalars: usize,
225        mul_wires: usize,
226    ) -> errors::Result<Self> {
227        if geom.message_len == 0 {
228            return Err(errors::Error::Protocol {
229                protocol: "outer",
230                message: "message length must be positive",
231            });
232        }
233
234        Ok(Self {
235            message_len: geom.message_len,
236            pad_rows: masked_scalars.div_ceil(geom.message_len),
237            aux_rows: mul_wires.div_ceil(geom.message_len).max(1),
238            domain_len: geom.domain_len,
239        })
240    }
241
242    pub fn slot(&self, unknown: Unknown) -> (usize, usize) {
243        match unknown {
244            Unknown::Pad(i) => {
245                let i = i as usize;
246
247                (i / self.message_len, i % self.message_len)
248            }
249            Unknown::Wire { mul, role } => {
250                let mul = mul as usize;
251                let block = role as usize;
252
253                (
254                    self.pad_rows + block * self.aux_rows + mul / self.message_len,
255                    mul % self.message_len,
256                )
257            }
258        }
259    }
260
261    pub fn interleaved_mask(&self) -> usize {
262        self.pad_rows + 3 * self.aux_rows
263    }
264
265    pub fn linear_mask(&self) -> usize {
266        self.interleaved_mask() + 1
267    }
268
269    pub fn linear_mask_hi(&self) -> usize {
270        self.interleaved_mask() + 2
271    }
272
273    pub fn quadratic_mask(&self) -> usize {
274        self.interleaved_mask() + 3
275    }
276
277    pub fn quadratic_mask_hi(&self) -> usize {
278        self.interleaved_mask() + 4
279    }
280
281    pub fn total_rows(&self) -> usize {
282        self.interleaved_mask() + OUTER_MASK_ROWS
283    }
284
285    pub fn hadamard_triples(&self) -> Vec<[usize; 3]> {
286        (0..self.aux_rows)
287            .map(|i| {
288                [
289                    self.pad_rows + i,
290                    self.pad_rows + self.aux_rows + i,
291                    self.pad_rows + 2 * self.aux_rows + i,
292                ]
293            })
294            .collect()
295    }
296}
297
298pub struct EvalRecord<F> {
299    pub mask_form: Vec<(Unknown, Flat<F>)>,
300    pub claim_masked: Flat<F>,
301    pub fin: Flat<F>,
302}
303
304/// One table's share of the statement, in transcript order.
305pub struct TableRecord<F> {
306    pub claims_masked: Vec<Flat<F>>,
307    pub predicate: PredicateRows<F>,
308    pub consistency: ConsistencyInputs<F>,
309
310    /// `(pad, claim, public value)` per fixed column.
311    pub fixed: Vec<(u32, u32, Flat<F>)>,
312
313    pub trace_eval: EvalRecord<F>,
314    pub h_eval: Option<EvalRecord<F>>,
315    pub gadget: Option<RingGadget<F>>,
316
317    /// `(bus id, pad, masked claimed sum)` per bus.
318    pub claimed_sums: Vec<(String, u32, Flat<F>)>,
319
320    pub mul_wires: u32,
321}
322
323/// The assembled statement: affine rows, and the ring
324/// gadgets whose tie coefficients [`linear_weights`]
325/// streams onto their chain rows.
326pub struct OuterRows<F> {
327    pub affine: Vec<AffineRow<F>>,
328    pub gadgets: Vec<RingGadget<F>>,
329}
330
331/// `H_0` of an eval sumcheck over the pad.
332pub struct InitialForm<F> {
333    pub form: Vec<(Unknown, Flat<F>)>,
334    pub gadget: Option<RingGadget<F>>,
335}
336
337/// First pad index of each masked group of a table,
338/// in the prover's masking order.
339#[derive(Clone, Copy, Debug)]
340pub struct TablePads {
341    pub claimed_sums: u32,
342    pub zerocheck: u32,
343    pub h_evals: u32,
344    pub claims: u32,
345}
346
347/// Public data of one eval sumcheck on masked claims.
348pub struct EvalInputs<'a, F> {
349    pub plan: &'a RingSwitchPlan,
350    pub eta: F,
351    pub r_mix: &'a [Block128],
352    pub shifted_claims: bool,
353    pub challenges: &'a [Flat<F>],
354    pub claim_masked: Flat<F>,
355    pub fin: Flat<F>,
356    pub claim_pads: &'a [u32],
357    pub round_first: u32,
358    pub first_wire: u32,
359}
360
361pub struct EvalOutput<F> {
362    pub record: EvalRecord<F>,
363    pub gadget: Option<RingGadget<F>>,
364}
365
366/// Everything a table's record is built from; both
367/// sides hold all of it after the table's eval phase.
368pub struct TableInputs<'a, F: TowerField, A> {
369    pub air: &'a A,
370    pub ast: &'a ConstraintAst<F>,
371    pub instance: &'a ProgramInstance<F>,
372    pub num_vars: usize,
373    pub trace_width: usize,
374    pub blinding_columns: usize,
375
376    pub alpha: Flat<F>,
377    pub gamma: Flat<F>,
378    pub beta: Flat<F>,
379    pub r_zerocheck: &'a [Flat<F>],
380    pub r_final: &'a [Flat<F>],
381    pub lookup_bus_points: &'a BTreeMap<String, Vec<Flat<F>>>,
382
383    pub claimed_sums_masked: &'a [(String, F)],
384    pub h_evals_masked: &'a [(String, F)],
385    pub val_final_masked: Flat<F>,
386    pub claims_masked: Vec<Flat<F>>,
387
388    pub pads: TablePads,
389    pub trace_eval: EvalRecord<F>,
390    pub h_eval: Option<EvalRecord<F>>,
391    pub gadget: Option<RingGadget<F>>,
392}
393
394pub fn eval_initial_form<F: HardwareField + Into<Block128> + From<u128>>(
395    plan: &RingSwitchPlan,
396    eta: F,
397    r_mix: &[Block128],
398    shifted_claims: bool,
399    claim_pads: &[u32],
400    first_wire: u32,
401) -> errors::Result<InitialForm<F>> {
402    let weights = claim_weights(plan, eta, shifted_claims);
403    if weights.len() != claim_pads.len() {
404        return Err(errors::Error::Protocol {
405            protocol: "outer",
406            message: "one pad entry per eval claim is required",
407        });
408    }
409
410    let mut form = Vec::new();
411    let mut ring = Vec::new();
412
413    for ((is_ring, weight), &pad) in weights.iter().zip(claim_pads) {
414        let a: Flat<F> = F::from(weight.0).to_hardware();
415        match is_ring {
416            true => ring.push((pad, a)),
417            false => form.push((Unknown::Pad(pad), a)),
418        }
419    }
420
421    if ring.is_empty() {
422        return Ok(InitialForm { form, gadget: None });
423    }
424
425    let mu: Vec<Flat<F>> = linearized_coeffs(&eq_tensor_b(r_mix))
426        .iter()
427        .map(|m| F::from(m.to_tower().0).to_hardware())
428        .collect();
429
430    let gadget = RingGadget::new(ring, mu, first_wire);
431    form.extend(gadget.delta_form());
432
433    Ok(InitialForm {
434        form,
435        gadget: Some(gadget),
436    })
437}
438
439/// The eval row's mask form propagated
440/// through the rounds, plus the ring gadget.
441pub fn eval_record<F>(inputs: &EvalInputs<'_, F>) -> errors::Result<EvalOutput<F>>
442where
443    F: HardwareField + Into<Block128> + From<u128>,
444{
445    let initial = eval_initial_form(
446        inputs.plan,
447        inputs.eta,
448        inputs.r_mix,
449        inputs.shifted_claims,
450        inputs.claim_pads,
451        inputs.first_wire,
452    )?;
453
454    let round_pads: Vec<[u32; 2]> = (0..inputs.challenges.len() as u32)
455        .map(|i| [inputs.round_first + 2 * i, inputs.round_first + 2 * i + 1])
456        .collect();
457    let round_refs: Vec<&[u32]> = round_pads.iter().map(|p| p.as_slice()).collect();
458
459    Ok(EvalOutput {
460        record: EvalRecord {
461            mask_form: sumcheck_mask_form(&initial.form, &round_refs, inputs.challenges, 2),
462            claim_masked: inputs.claim_masked,
463            fin: inputs.fin,
464        },
465        gadget: initial.gadget,
466    })
467}
468
469pub fn table_record<F, A>(inputs: TableInputs<'_, F, A>) -> errors::Result<TableRecord<F>>
470where
471    F: TowerField + HardwareField,
472    A: Air<F>,
473{
474    let air = inputs.air;
475    let ast = inputs.ast;
476
477    let boundary_constraints = air.boundary_constraints();
478    let bus_specs = air.permutation_checks();
479    let shape = TableShape::from_air(air, inputs.num_vars, ast)?;
480
481    let num_vars = inputs.num_vars;
482    let trace_width = inputs.trace_width;
483    let r_final = inputs.r_final;
484    let pad_first = inputs.pads.claims;
485    let half = shape.eval_claims(inputs.blinding_columns) as u32;
486
487    if inputs.claims_masked.len() != 2 * half as usize {
488        return Err(errors::Error::Protocol {
489            protocol: "outer",
490            message: "eval claim count does not match the table shape",
491        });
492    }
493
494    let eq_row_checker = TensorProduct::new(r_final.to_vec());
495
496    let mut fixed = Vec::new();
497    for fc in &air.fixed_columns() {
498        if fc.col_idx >= trace_width {
499            return Err(errors::Error::Protocol {
500                protocol: "outer",
501                message: "fixed column col_idx out of trace_width",
502            });
503        }
504
505        fixed.push((
506            pad_first + fc.col_idx as u32,
507            fc.col_idx as u32,
508            fc.shape.evaluate(r_final),
509        ));
510    }
511
512    let mut boundary = Vec::with_capacity(boundary_constraints.len());
513    for bc in &boundary_constraints {
514        if bc.col_idx >= trace_width || bc.row_idx >= 1 << num_vars {
515            return Err(errors::Error::Protocol {
516                protocol: "outer",
517                message: "boundary constraint out of the table",
518            });
519        }
520
521        boundary.push((
522            bc.col_idx as u32,
523            bc.resolve_target(inputs.instance)?.to_hardware(),
524            eq_row_checker.evaluate_at_index(bc.row_idx),
525        ));
526    }
527
528    let mut buses = Vec::with_capacity(bus_specs.len());
529    for (spec_idx, (bus_id, spec)) in bus_specs.iter().enumerate() {
530        let eq_lookup = match spec.kind {
531            BusKind::Permutation => Flat::from_raw(F::ONE),
532            BusKind::Lookup => {
533                let r_bus =
534                    inputs
535                        .lookup_bus_points
536                        .get(bus_id)
537                        .ok_or(errors::Error::Protocol {
538                            protocol: "outer",
539                            message: "lookup bus spec missing r_bus point",
540                        })?;
541
542                if r_bus.len() < num_vars {
543                    return Err(errors::Error::Protocol {
544                        protocol: "outer",
545                        message: "r_bus shorter than table num_vars",
546                    });
547                }
548
549                let one = Flat::from_raw(F::ONE);
550                let mut eq_r_hi_at_0 = one;
551                for r_j in &r_bus[num_vars..] {
552                    eq_r_hi_at_0 *= one - *r_j;
553                }
554
555                eq_r_hi_at_0 * TensorProduct::evaluate_eq_slice(&r_bus[0..num_vars], r_final)
556            }
557        };
558
559        let mut sources = Vec::new();
560        for (source, _) in &spec.sources {
561            match source {
562                Source::Column(col) => sources.push(BusSource::Claim(*col as u32)),
563                Source::Columns(indices) => {
564                    sources.extend(indices.iter().map(|&col| BusSource::Claim(col as u32)));
565                }
566                Source::Const(v) => sources.push(BusSource::Public(F::from(*v).to_hardware())),
567                Source::RowIndexLeBytes(n) => {
568                    sources.push(BusSource::Public(eval_row_idx_le_mle::<F>(*n, r_final)));
569                }
570                Source::RowIndexByte(n) => {
571                    sources.push(BusSource::Public(eval_row_idx_byte_mle::<F>(*n, r_final)));
572                }
573            }
574        }
575
576        buses.push(BusRow {
577            h_pad: inputs.pads.h_evals + spec_idx as u32,
578            h_masked: inputs.h_evals_masked[spec_idx].1.to_hardware(),
579            h_wire: (shape.mul_nodes + spec_idx) as u32,
580            sources,
581            selector: spec.selector.map(|i| i as u32),
582            recv_selector: spec.recv_selector.map(|i| i as u32),
583            eq_lookup,
584        });
585    }
586
587    let logup_alpha_offset = ast.roots.len() + boundary_constraints.len() + inputs.blinding_columns;
588    let mut alpha_pow = Flat::from_raw(F::ONE);
589
590    for _ in 0..logup_alpha_offset {
591        alpha_pow *= inputs.alpha;
592    }
593
594    let mut initial = Vec::with_capacity(bus_specs.len());
595    for k in 0..bus_specs.len() as u32 {
596        initial.push((Unknown::Pad(inputs.pads.claimed_sums + k), alpha_pow));
597        alpha_pow *= inputs.alpha;
598    }
599
600    let degree = shape.sumcheck_degree;
601    let round_pads: Vec<Vec<u32>> = (0..num_vars)
602        .map(|i| {
603            let first = inputs.pads.zerocheck + (i * degree) as u32;
604
605            (0..degree as u32).map(|j| first + j).collect()
606        })
607        .collect();
608    let round_refs: Vec<&[u32]> = round_pads.iter().map(|p| p.as_slice()).collect();
609
610    let consistency = ConsistencyInputs {
611        pad_first,
612        eq_zc: TensorProduct::evaluate_eq_slice(inputs.r_zerocheck, r_final),
613        alpha: inputs.alpha,
614        gamma: inputs.gamma,
615        beta: inputs.beta,
616        boundary,
617        telescope: (0..inputs.blinding_columns as u32)
618            .map(|k| {
619                let b = trace_width as u32 + k;
620
621                (b, half + b)
622            })
623            .collect(),
624        buses,
625        val_final_form: sumcheck_mask_form(&initial, &round_refs, r_final, degree),
626        val_final_masked: inputs.val_final_masked,
627    };
628
629    Ok(TableRecord {
630        claims_masked: inputs.claims_masked,
631        predicate: compile(ast, ClaimLayout { pad_first, half }),
632        consistency,
633        fixed,
634        trace_eval: inputs.trace_eval,
635        h_eval: inputs.h_eval,
636        gadget: inputs.gadget,
637        claimed_sums: inputs
638            .claimed_sums_masked
639            .iter()
640            .enumerate()
641            .map(|(k, (bus_id, sum))| {
642                (
643                    bus_id.clone(),
644                    inputs.pads.claimed_sums + k as u32,
645                    sum.to_hardware(),
646                )
647            })
648            .collect(),
649        mul_wires: shape.mul_wires() as u32,
650    })
651}
652
653/// Every affine row of the statement, claims folded
654/// into constants and wire indices offset per table.
655pub fn assemble<F: TowerField + HardwareField>(
656    records: Vec<TableRecord<F>>,
657    statement: &OuterStatement,
658) -> errors::Result<OuterRows<F>> {
659    let mut affine = Vec::new();
660    let mut gadgets = Vec::new();
661    let mut offset = 0u32;
662    let mut buses: BTreeMap<String, Vec<(u32, Flat<F>)>> = BTreeMap::new();
663
664    for mut record in records {
665        let first = affine.len();
666
667        affine.append(&mut record.predicate.affine);
668        affine.push(consistency_row(
669            &record.predicate.roots,
670            &record.consistency,
671        ));
672
673        for bus in &record.consistency.buses {
674            let [lhs, rhs] =
675                bus_operand_rows(bus, record.consistency.pad_first, record.consistency.beta);
676
677            affine.push(lhs);
678            affine.push(rhs);
679        }
680
681        for &(pad, claim, public) in &record.fixed {
682            affine.push(fixed_column_row(pad, claim, public));
683        }
684
685        for eval in core::iter::once(&record.trace_eval).chain(record.h_eval.iter()) {
686            affine.push(eval_final_row(&eval.mask_form, eval.claim_masked, eval.fin));
687        }
688
689        if let Some(mut gadget) = record.gadget {
690            gadget.first_row = affine.len();
691            affine.extend(gadget.wire_rows());
692            gadgets.push(gadget);
693        }
694
695        for row in &mut affine[first..] {
696            resolve_table_row(row, &record.claims_masked, offset);
697        }
698
699        offset += record.mul_wires;
700
701        for (bus_id, pad, masked) in record.claimed_sums {
702            buses.entry(bus_id).or_default().push((pad, masked));
703        }
704    }
705
706    if offset as usize != statement.mul_wires {
707        return Err(errors::Error::Protocol {
708            protocol: "outer",
709            message: "table wire counts do not sum to the statement",
710        });
711    }
712
713    for endpoints in buses.values() {
714        affine.push(bus_sum_row(endpoints));
715    }
716
717    Ok(OuterRows { affine, gadgets })
718}
719
720pub fn reachable<F: TowerField>(ast: &ConstraintAst<F>) -> Vec<bool> {
721    let n = ast.arena.len();
722    let mut live = vec![false; n];
723
724    for root in &ast.roots {
725        live[root.0 as usize] = true;
726    }
727
728    // Children sit at lower indices than their parent
729    for i in (0..n).rev() {
730        if !live[i] {
731            continue;
732        }
733
734        let mut mark = |id: &ExprId| live[id.0 as usize] = true;
735        match ast.arena.get(ExprId(i as u32)) {
736            ConstraintExpr::Cell(_) | ConstraintExpr::Const(_) => {}
737            ConstraintExpr::Add(a, b) | ConstraintExpr::Mul(a, b) => {
738                mark(a);
739                mark(b);
740            }
741            ConstraintExpr::Scale(_, a) => mark(a),
742            ConstraintExpr::Sum(children) => children.iter().for_each(mark),
743        }
744    }
745
746    live
747}
748
749pub fn mul_node_count<F: TowerField>(ast: &ConstraintAst<F>) -> usize {
750    reachable(ast)
751        .iter()
752        .enumerate()
753        .filter(|&(i, &live)| {
754            live && matches!(ast.arena.get(ExprId(i as u32)), ConstraintExpr::Mul(..))
755        })
756        .count()
757}
758
759pub fn consistency_row<F: TowerField + HardwareField>(
760    roots: &[Form<F>],
761    inputs: &ConsistencyInputs<F>,
762) -> AffineRow<F> {
763    let mut row = AffineRow {
764        unknowns: inputs.val_final_form.to_vec(),
765        claims: Vec::new(),
766        constant: inputs.val_final_masked,
767    };
768
769    let mut alpha_pow = Flat::from_raw(F::ONE);
770
771    for form in roots {
772        scale_into(&mut row, form, alpha_pow * inputs.eq_zc);
773        alpha_pow *= inputs.alpha;
774    }
775
776    for &(claim, public, eq_row) in &inputs.boundary {
777        let weight = eq_row * alpha_pow;
778
779        push_claim(&mut row, inputs.pad_first, claim, weight);
780        row.constant += public * weight;
781
782        alpha_pow *= inputs.alpha;
783    }
784
785    for &(b, b_next) in &inputs.telescope {
786        push_claim(&mut row, inputs.pad_first, b, alpha_pow);
787        push_claim(&mut row, inputs.pad_first, b_next, alpha_pow);
788
789        alpha_pow *= inputs.alpha;
790    }
791
792    for bus in &inputs.buses {
793        let consistency = alpha_pow * inputs.eq_zc;
794        let gamma_weight = consistency * inputs.gamma;
795
796        row.unknowns.push((Unknown::Pad(bus.h_pad), gamma_weight));
797
798        row.constant += gamma_weight * bus.h_masked;
799
800        row.unknowns.push((
801            Unknown::Wire {
802                mul: bus.h_wire,
803                role: WireRole::Product,
804            },
805            consistency,
806        ));
807
808        match bus.selector {
809            Some(s) => push_claim(&mut row, inputs.pad_first, s, consistency),
810            None => row.constant += consistency,
811        }
812
813        if let Some(s) = bus.recv_selector {
814            push_claim(&mut row, inputs.pad_first, s, consistency);
815        }
816
817        let lookup_weight = alpha_pow * bus.eq_lookup;
818
819        row.unknowns.push((Unknown::Pad(bus.h_pad), lookup_weight));
820
821        row.constant += lookup_weight * bus.h_masked;
822
823        alpha_pow *= inputs.alpha;
824    }
825
826    row
827}
828
829/// Pad form of `claim'_n - claim_n` for a sumcheck whose
830/// replay reconstructs `p'_0 = claim' - p'_1` each round:
831/// entries of round `i` carry `Π_{j>i} L_0(r_j)`.
832pub fn sumcheck_mask_form<F: TowerField + HardwareField>(
833    initial: &[(Unknown, Flat<F>)],
834    round_pads: &[&[u32]],
835    challenges: &[Flat<F>],
836    degree: usize,
837) -> Vec<(Unknown, Flat<F>)> {
838    let rounds: Vec<Vec<Flat<F>>> = challenges
839        .iter()
840        .map(|&r| lagrange_weights::<F>(degree, r))
841        .collect();
842
843    let mut suffix = vec![Flat::from_raw(F::ONE); rounds.len() + 1];
844    for i in (0..rounds.len()).rev() {
845        suffix[i] = suffix[i + 1] * rounds[i][0];
846    }
847
848    let mut form: Vec<(Unknown, Flat<F>)> = initial
849        .iter()
850        .map(|&(u, coeff)| (u, coeff * suffix[0]))
851        .collect();
852
853    for (i, (pads, weights)) in round_pads.iter().zip(&rounds).enumerate() {
854        let after = suffix[i + 1];
855
856        form.push((Unknown::Pad(pads[0]), weights[0] * after));
857
858        for (j, &pad) in pads.iter().enumerate() {
859            form.push((Unknown::Pad(pad), weights[j + 1] * after));
860        }
861    }
862
863    form
864}
865
866/// `claim_n = fin` for an eval sumcheck: `fin` is public,
867/// `claim_n` is the masked running claim minus its form.
868pub fn eval_final_row<F: TowerField + HardwareField>(
869    mask_form: &[(Unknown, Flat<F>)],
870    claim_masked: Flat<F>,
871    fin: Flat<F>,
872) -> AffineRow<F> {
873    AffineRow {
874        unknowns: mask_form.to_vec(),
875        claims: Vec::new(),
876        constant: claim_masked + fin,
877    }
878}
879
880/// `c[claim] = public`, i.e. `h[pad] = c'[claim] + public`.
881pub fn fixed_column_row<F: TowerField + HardwareField>(
882    pad: u32,
883    claim: u32,
884    public: Flat<F>,
885) -> AffineRow<F> {
886    let one = Flat::from_raw(F::ONE);
887
888    AffineRow {
889        unknowns: vec![(Unknown::Pad(pad), one)],
890        claims: vec![(claim, one)],
891        constant: public,
892    }
893}
894
895/// `Σ claimed_sum = 0` over one bus id:
896/// `(pad, masked sum)` per endpoint.
897pub fn bus_sum_row<F: TowerField + HardwareField>(endpoints: &[(u32, Flat<F>)]) -> AffineRow<F> {
898    let mut constant = Flat::from_raw(F::ZERO);
899    for &(_, masked) in endpoints {
900        constant += masked;
901    }
902
903    AffineRow {
904        unknowns: endpoints
905            .iter()
906            .map(|&(pad, _)| (Unknown::Pad(pad), Flat::from_raw(F::ONE)))
907            .collect(),
908        claims: Vec::new(),
909        constant,
910    }
911}
912
913/// Folds a table's public masked claims into the constant
914/// and shifts its table-local wire indices by `mul_offset`.
915pub fn resolve_table_row<F: TowerField + HardwareField>(
916    row: &mut AffineRow<F>,
917    claims: &[Flat<F>],
918    mul_offset: u32,
919) {
920    for &(idx, coeff) in &row.claims {
921        row.constant += coeff * claims[idx as usize];
922    }
923
924    row.claims.clear();
925
926    for (unknown, _) in row.unknowns.iter_mut() {
927        if let Unknown::Wire { mul, .. } = unknown {
928            *mul += mul_offset;
929        }
930    }
931}
932
933pub fn bus_key_form<F: TowerField + HardwareField>(
934    bus: &BusRow<F>,
935    pad_first: u32,
936    beta: Flat<F>,
937) -> Form<F> {
938    let mut form = Form::default();
939    let mut beta_pow = Flat::from_raw(F::ONE);
940
941    for source in &bus.sources {
942        match source {
943            BusSource::Claim(idx) => {
944                form.unknowns
945                    .push((Unknown::Pad(pad_first + idx), beta_pow));
946                form.claims.push((*idx, beta_pow));
947            }
948            BusSource::Public(v) => form.constant += *v * beta_pow,
949        }
950
951        beta_pow *= beta;
952    }
953
954    form
955}
956
957/// Pins the bus Hadamard wire `h · key`:
958/// `Lhs = h` and `Rhs = key` over the pad.
959pub fn bus_operand_rows<F: TowerField + HardwareField>(
960    bus: &BusRow<F>,
961    pad_first: u32,
962    beta: Flat<F>,
963) -> [AffineRow<F>; 2] {
964    let one = Flat::from_raw(F::ONE);
965
966    let lhs = AffineRow {
967        unknowns: vec![
968            (
969                Unknown::Wire {
970                    mul: bus.h_wire,
971                    role: WireRole::Lhs,
972                },
973                one,
974            ),
975            (Unknown::Pad(bus.h_pad), one),
976        ],
977        claims: Vec::new(),
978        constant: bus.h_masked,
979    };
980
981    let key = bus_key_form(bus, pad_first, beta);
982
983    let mut unknowns = key.unknowns;
984    unknowns.push((
985        Unknown::Wire {
986            mul: bus.h_wire,
987            role: WireRole::Rhs,
988        },
989        one,
990    ));
991
992    let rhs = AffineRow {
993        unknowns,
994        claims: key.claims,
995        constant: key.constant,
996    };
997
998    [lhs, rhs]
999}
1000
1001/// PAD oracle rows: the pad in [`OuterLayout::slot`] order,
1002/// `filler` (exactly `pad_rows · (code_len - message_len)`
1003/// entries) on the message tail.
1004pub fn build_pad_rows<F: TowerField + HardwareField>(
1005    layout: &OuterLayout,
1006    pad: &[Flat<F>],
1007    code_len: usize,
1008    filler: &[Flat<F>],
1009) -> errors::Result<Vec<Vec<Flat<F>>>> {
1010    let tail = code_len - layout.message_len;
1011
1012    if pad.len() > layout.pad_rows * layout.message_len || filler.len() != layout.pad_rows * tail {
1013        return Err(errors::Error::Protocol {
1014            protocol: "outer",
1015            message: "pad rows do not hold the pad and its filler",
1016        });
1017    }
1018
1019    let mut rows = vec![vec![Flat::from_raw(F::ZERO); layout.domain_len]; layout.pad_rows];
1020    for (i, value) in pad.iter().enumerate() {
1021        let (r, c) = layout.slot(Unknown::Pad(i as u32));
1022        rows[r][c] = *value;
1023    }
1024
1025    for (row, fill) in rows.iter_mut().zip(filler.chunks_exact(tail)) {
1026        row[layout.message_len..code_len].copy_from_slice(fill);
1027    }
1028
1029    Ok(rows)
1030}
1031
1032/// AUX oracle rows: the wire blocks and the five mask
1033/// rows, `filler` on every message tail, then the
1034/// zero-sum mask row's first `code_len - 1` cells and
1035/// the messages of the interleaved and both high masks.
1036pub fn build_aux_rows<F: TowerField + HardwareField>(
1037    layout: &OuterLayout,
1038    wires: &[[Flat<F>; 3]],
1039    code_len: usize,
1040    filler: &[Flat<F>],
1041) -> errors::Result<Vec<Vec<Flat<F>>>> {
1042    let count = layout.total_rows() - layout.pad_rows;
1043    let tail = code_len - layout.message_len;
1044
1045    if wires.len() > layout.aux_rows * layout.message_len
1046        || filler.len() != aux_filler_len(layout, code_len)
1047    {
1048        return Err(errors::Error::Protocol {
1049            protocol: "outer",
1050            message: "aux rows do not hold the wires and their filler",
1051        });
1052    }
1053
1054    let mut rows = vec![vec![Flat::from_raw(F::ZERO); layout.domain_len]; count];
1055
1056    for (mul, wire) in wires.iter().enumerate() {
1057        for role in [WireRole::Lhs, WireRole::Rhs, WireRole::Product] {
1058            let (r, c) = layout.slot(Unknown::Wire {
1059                mul: mul as u32,
1060                role,
1061            });
1062
1063            rows[r - layout.pad_rows][c] = wire[role as usize];
1064        }
1065    }
1066
1067    let (tails, rest) = filler.split_at(count * tail);
1068    for (row, fill) in rows.iter_mut().zip(tails.chunks_exact(tail)) {
1069        row[layout.message_len..code_len].copy_from_slice(fill);
1070    }
1071
1072    let (zero_sum_fill, rest) = rest.split_at(code_len - 1);
1073
1074    let zero_sum = layout.linear_mask() - layout.pad_rows;
1075    let mut acc = Flat::from_raw(F::ZERO);
1076
1077    for (slot, &v) in rows[zero_sum][..code_len - 1].iter_mut().zip(zero_sum_fill) {
1078        *slot = v;
1079        acc += v;
1080    }
1081
1082    rows[zero_sum][code_len - 1] = acc;
1083
1084    // The quadratic mask keeps its zero message; every
1085    // other mask row is uniform over the whole message.
1086    let uniform = [
1087        layout.interleaved_mask(),
1088        layout.linear_mask_hi(),
1089        layout.quadratic_mask_hi(),
1090    ];
1091
1092    for (row, fill) in uniform
1093        .into_iter()
1094        .zip(rest.chunks_exact(layout.message_len))
1095    {
1096        rows[row - layout.pad_rows][..layout.message_len].copy_from_slice(fill);
1097    }
1098
1099    Ok(rows)
1100}
1101
1102pub fn aux_filler_len(layout: &OuterLayout, code_len: usize) -> usize {
1103    let count = layout.total_rows() - layout.pad_rows;
1104
1105    count * (code_len - layout.message_len) + code_len - 1 + 3 * layout.message_len
1106}
1107
1108pub fn linear_weights<F: TowerField + HardwareField>(
1109    layout: &OuterLayout,
1110    rows: &OuterRows<F>,
1111    batch: &[F],
1112) -> errors::Result<LinearBatch<F>> {
1113    if batch.len() != rows.affine.len() {
1114        return Err(errors::Error::Protocol {
1115            protocol: "outer",
1116            message: "one batch challenge per affine row",
1117        });
1118    }
1119
1120    if rows.affine.iter().any(|row| !row.claims.is_empty()) {
1121        return Err(errors::Error::Protocol {
1122            protocol: "outer",
1123            message: "affine rows must be resolved before weighting",
1124        });
1125    }
1126
1127    let scales: Vec<Flat<F>> = batch.iter().map(|r| r.to_hardware()).collect();
1128
1129    let mut weights = vec![vec![Flat::from_raw(F::ZERO); layout.message_len]; layout.total_rows()];
1130    let mut target = Flat::from_raw(F::ZERO);
1131
1132    for (row, &scale) in rows.affine.iter().zip(&scales) {
1133        for &(unknown, coeff) in &row.unknowns {
1134            let (orow, ocol) = layout.slot(unknown);
1135            weights[orow][ocol] += scale * coeff;
1136        }
1137
1138        target += scale * row.constant;
1139    }
1140
1141    for gadget in &rows.gadgets {
1142        gadget.for_each_tie_row(|j, tie| {
1143            let scale = scales[gadget.row_of(j)];
1144            for (&(pad, _), &t) in gadget.ring.iter().zip(tie) {
1145                let (orow, ocol) = layout.slot(Unknown::Pad(pad));
1146                weights[orow][ocol] += scale * t;
1147            }
1148        });
1149    }
1150
1151    let used: Vec<usize> = (0..layout.total_rows())
1152        .filter(|&r| weights[r].iter().any(|v| *v != Flat::from_raw(F::ZERO)))
1153        .collect();
1154
1155    let selected = used.iter().map(|&r| mem::take(&mut weights[r])).collect();
1156
1157    Ok(LinearBatch {
1158        weights: selected,
1159        rows: used,
1160        target,
1161    })
1162}
1163
1164fn push_claim<F: TowerField>(row: &mut AffineRow<F>, pad_first: u32, idx: u32, coeff: Flat<F>) {
1165    row.unknowns.push((Unknown::Pad(pad_first + idx), coeff));
1166    row.claims.push((idx, coeff));
1167}
1168
1169fn scale_into<F: TowerField + HardwareField>(dst: &mut AffineRow<F>, src: &Form<F>, c: Flat<F>) {
1170    for &(u, k) in &src.unknowns {
1171        dst.unknowns.push((u, k * c));
1172    }
1173
1174    for &(i, k) in &src.claims {
1175        dst.claims.push((i, k * c));
1176    }
1177
1178    dst.constant += src.constant * c;
1179}
1180
1181fn lagrange_weights<F: TowerField + HardwareField>(degree: usize, r: Flat<F>) -> Vec<Flat<F>> {
1182    (0..=degree)
1183        .map(|j| {
1184            let mut evals = vec![F::ZERO; degree + 1];
1185            evals[j] = F::ONE;
1186
1187            UnivariatePoly::new(evals).evaluate_hw(r)
1188        })
1189        .collect()
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194    use super::*;
1195    use crate::ProgramCell;
1196    use crate::constraint::ConstraintArena;
1197    use hekate_math::Block128;
1198
1199    type F = Block128;
1200
1201    const BUS_PAD_FIRST: u32 = 10;
1202    const BUS_H_PAD: u32 = 20;
1203    const BUS_PUBLIC: u128 = 77;
1204
1205    fn shape(num_vars: usize, num_columns: usize, degree: usize, buses: usize) -> TableShape {
1206        TableShape {
1207            num_vars,
1208            num_columns,
1209            sumcheck_degree: degree,
1210            num_buses: buses,
1211            mul_nodes: 0,
1212            ring_units: false,
1213        }
1214    }
1215
1216    fn mix(seed: u128) -> Flat<F> {
1217        F::from(
1218            seed.wrapping_mul(0x9e37_79b9_7f4a_7c15)
1219                .wrapping_add(0x51ed_2701),
1220        )
1221        .to_hardware()
1222    }
1223
1224    /// One table:
1225    /// `c0 · c1` as the only constraint, two columns,
1226    /// one Permutation bus over `[Claim(0), Public(77)]`
1227    /// with selector `c1`.
1228    fn bus_record(claims_masked: Vec<Flat<F>>, h_masked: Flat<F>, beta: Flat<F>) -> TableRecord<F> {
1229        let mut arena = ConstraintArena::<F>::new();
1230
1231        let a = arena.cell(ProgramCell::current(0));
1232        let b = arena.cell(ProgramCell::current(1));
1233        let root = arena.mul(a, b);
1234
1235        let ast = ConstraintAst {
1236            arena,
1237            roots: vec![root],
1238            labels: vec![None],
1239        };
1240
1241        let predicate = compile(
1242            &ast,
1243            ClaimLayout {
1244                pad_first: BUS_PAD_FIRST,
1245                half: 2,
1246            },
1247        );
1248
1249        let bus = BusRow {
1250            h_pad: BUS_H_PAD,
1251            h_masked,
1252            h_wire: predicate.mul_nodes,
1253            sources: vec![BusSource::Claim(0), BusSource::Public(mix(BUS_PUBLIC))],
1254            selector: Some(1),
1255            recv_selector: None,
1256            eq_lookup: Flat::from_raw(F::ONE),
1257        };
1258
1259        let mul_wires = predicate.mul_nodes + 1;
1260
1261        TableRecord {
1262            claims_masked,
1263            predicate,
1264            consistency: ConsistencyInputs {
1265                pad_first: BUS_PAD_FIRST,
1266                eq_zc: mix(1),
1267                alpha: mix(2),
1268                gamma: mix(3),
1269                beta,
1270                boundary: Vec::new(),
1271                telescope: Vec::new(),
1272                buses: vec![bus],
1273                val_final_form: Vec::new(),
1274                val_final_masked: mix(4),
1275            },
1276            fixed: Vec::new(),
1277            trace_eval: EvalRecord {
1278                mask_form: Vec::new(),
1279                claim_masked: mix(5),
1280                fin: mix(6),
1281            },
1282            h_eval: None,
1283            gadget: None,
1284            claimed_sums: vec![(String::from("bus"), 0, mix(7))],
1285            mul_wires,
1286        }
1287    }
1288
1289    fn mentions(rows: &[AffineRow<F>], mul: u32, role: WireRole) -> usize {
1290        rows.iter()
1291            .filter(|r| {
1292                r.unknowns
1293                    .iter()
1294                    .any(|&(u, _)| u == Unknown::Wire { mul, role })
1295            })
1296            .count()
1297    }
1298
1299    #[test]
1300    fn ring_units_add_squaring_chain() {
1301        let plain = shape(12, 100, 3, 0);
1302        let ring = TableShape {
1303            ring_units: true,
1304            ..plain
1305        };
1306
1307        assert_eq!(ring.mul_wires() - plain.mul_wires(), 127);
1308    }
1309
1310    #[test]
1311    fn ring_blind_planes_are_masked_only_under_blinding() {
1312        let plain = shape(12, 100, 3, 0);
1313        let ring = TableShape {
1314            ring_units: true,
1315            ..plain
1316        };
1317
1318        assert_eq!(ring.masked_scalars(0), plain.masked_scalars(0));
1319        assert_eq!(
1320            ring.masked_scalars(1) - plain.masked_scalars(1),
1321            2 * RING_BLIND_BITS
1322        );
1323    }
1324
1325    #[test]
1326    fn empty_arena_has_no_wires() {
1327        let ast = ConstraintAst::<F> {
1328            arena: ConstraintArena::new(),
1329            roots: vec![],
1330            labels: vec![],
1331        };
1332
1333        assert_eq!(mul_node_count(&ast), 0);
1334    }
1335
1336    #[test]
1337    fn unreachable_mul_nodes_carry_no_wire() {
1338        let mut arena = ConstraintArena::<F>::new();
1339
1340        let a = arena.cell(ProgramCell::current(0));
1341        let b = arena.cell(ProgramCell::current(1));
1342        let live = arena.mul(a, b);
1343        let orphan = arena.cell(ProgramCell::current(2));
1344
1345        arena.mul(orphan, b);
1346
1347        let ast = ConstraintAst {
1348            arena,
1349            roots: vec![live],
1350            labels: vec![None],
1351        };
1352
1353        assert_eq!(mul_node_count(&ast), 1);
1354    }
1355
1356    #[test]
1357    fn shared_mul_node_counts_once() {
1358        let mut arena = ConstraintArena::<F>::new();
1359
1360        let a = arena.cell(ProgramCell::current(0));
1361        let b = arena.cell(ProgramCell::current(1));
1362        let shared = arena.mul(a, b);
1363        let left = arena.add(shared, a);
1364        let right = arena.add(shared, b);
1365
1366        let ast = ConstraintAst {
1367            arena,
1368            roots: vec![left, right],
1369            labels: vec![None, None],
1370        };
1371
1372        assert_eq!(mul_node_count(&ast), 1);
1373    }
1374
1375    #[test]
1376    fn nested_mul_nodes_each_carry_wire() {
1377        let mut arena = ConstraintArena::<F>::new();
1378
1379        let a = arena.cell(ProgramCell::current(0));
1380        let b = arena.cell(ProgramCell::current(1));
1381        let c = arena.cell(ProgramCell::current(2));
1382        let inner = arena.mul(a, b);
1383        let outer = arena.mul(inner, c);
1384
1385        let ast = ConstraintAst {
1386            arena,
1387            roots: vec![outer],
1388            labels: vec![None],
1389        };
1390
1391        assert_eq!(mul_node_count(&ast), 2);
1392    }
1393
1394    #[test]
1395    fn mul_under_sum_and_scale_is_reachable() {
1396        let mut arena = ConstraintArena::<F>::new();
1397
1398        let a = arena.cell(ProgramCell::current(0));
1399        let b = arena.cell(ProgramCell::current(1));
1400        let product = arena.mul(a, b);
1401        let scaled = arena.scale(F::ONE, product);
1402        let summed = arena.sum(vec![scaled, a]);
1403
1404        let ast = ConstraintAst {
1405            arena,
1406            roots: vec![summed],
1407            labels: vec![None],
1408        };
1409
1410        assert_eq!(mul_node_count(&ast), 1);
1411    }
1412
1413    #[test]
1414    fn busless_table_pays_for_no_h_opening() {
1415        let with_bus = shape(12, 100, 3, 1);
1416        let without = TableShape {
1417            num_buses: 0,
1418            ..with_bus
1419        };
1420
1421        let blind_units = 1;
1422        let claimed_sum_and_h_eval = 2;
1423        let h_open = blind_units + 2 * 12;
1424
1425        assert_eq!(
1426            with_bus.masked_scalars(blind_units) - without.masked_scalars(blind_units),
1427            claimed_sum_and_h_eval + h_open
1428        );
1429    }
1430
1431    #[test]
1432    fn masked_scalars_match_transcript_tally() {
1433        let s = shape(12, 1977, 5, 3);
1434
1435        let blind_units = 1;
1436        let claimed_sums_and_h_evals = 2 * 3;
1437        let zerocheck_rounds = 12 * 5;
1438        let h_open = blind_units + 2 * 12;
1439        let claims = 2 * (1977 + blind_units);
1440        let trace_eval_rounds = 2 * 12;
1441
1442        assert_eq!(
1443            s.masked_scalars(blind_units),
1444            claimed_sums_and_h_evals + zerocheck_rounds + h_open + claims + trace_eval_rounds
1445        );
1446    }
1447
1448    #[test]
1449    fn every_bus_adds_hadamard_row() {
1450        let s = TableShape {
1451            mul_nodes: 40,
1452            ..shape(12, 100, 3, 3)
1453        };
1454
1455        assert_eq!(s.mul_wires(), 43);
1456    }
1457
1458    #[test]
1459    fn statement_sums_over_every_table() {
1460        let main = TableShape {
1461            mul_nodes: 40,
1462            ..shape(12, 100, 3, 2)
1463        };
1464        let chiplet = TableShape {
1465            mul_nodes: 7,
1466            ..shape(10, 30, 4, 1)
1467        };
1468
1469        let statement = OuterStatement::new(&[main, chiplet], 2);
1470
1471        assert_eq!(
1472            statement.masked_scalars,
1473            main.masked_scalars(2) + chiplet.masked_scalars(2)
1474        );
1475        assert_eq!(statement.mul_wires, 50);
1476    }
1477
1478    #[test]
1479    fn every_hadamard_wire_operand_is_pinned_by_row() {
1480        let claims: Vec<Flat<F>> = (0..4).map(|i| mix(100 + i)).collect();
1481        let record = bus_record(claims, mix(8), mix(9));
1482        let statement = OuterStatement {
1483            masked_scalars: 32,
1484            mul_wires: record.mul_wires as usize,
1485        };
1486
1487        let rows = assemble(vec![record], &statement).unwrap();
1488
1489        for mul in 0..statement.mul_wires as u32 {
1490            for role in [WireRole::Lhs, WireRole::Rhs, WireRole::Product] {
1491                assert!(
1492                    mentions(&rows.affine, mul, role) >= 1,
1493                    "wire {mul} {role:?}"
1494                );
1495            }
1496        }
1497    }
1498
1499    #[test]
1500    fn bus_operand_rows_hold_on_honest_wires() {
1501        let plain: Vec<Flat<F>> = (0..4).map(|i| mix(200 + i)).collect();
1502        let pad: Vec<Flat<F>> = (0..32).map(|i| mix(300 + i)).collect();
1503        let claims_masked: Vec<Flat<F>> = plain
1504            .iter()
1505            .enumerate()
1506            .map(|(i, &c)| c + pad[BUS_PAD_FIRST as usize + i])
1507            .collect();
1508
1509        let beta = mix(9);
1510        let h = mix(400);
1511        let h_masked = h + pad[BUS_H_PAD as usize];
1512        let key = plain[0] + beta * mix(BUS_PUBLIC);
1513
1514        let record = bus_record(claims_masked.clone(), h_masked, beta);
1515        let bus = &record.consistency.buses[0];
1516        let [lhs, rhs] = bus_operand_rows(bus, BUS_PAD_FIRST, beta);
1517
1518        let wire = |mul: u32, role: WireRole| -> Flat<F> {
1519            assert_eq!(mul, bus.h_wire);
1520            match role {
1521                WireRole::Lhs => h,
1522                WireRole::Rhs => key,
1523                WireRole::Product => h * key,
1524            }
1525        };
1526
1527        for row in [&lhs, &rhs] {
1528            let mut left = Flat::from_raw(F::ZERO);
1529            for &(u, coeff) in &row.unknowns {
1530                let value = match u {
1531                    Unknown::Pad(i) => pad[i as usize],
1532                    Unknown::Wire { mul, role } => wire(mul, role),
1533                };
1534
1535                left += coeff * value;
1536            }
1537
1538            let mut right = row.constant;
1539            for &(idx, coeff) in &row.claims {
1540                right += coeff * claims_masked[idx as usize];
1541            }
1542
1543            assert_eq!(left, right);
1544        }
1545
1546        assert_eq!(mentions(&[lhs], bus.h_wire, WireRole::Lhs), 1);
1547        assert_eq!(mentions(&[rhs], bus.h_wire, WireRole::Rhs), 1);
1548    }
1549
1550    #[test]
1551    fn bus_operand_rows_reject_free_operand() {
1552        let plain: Vec<Flat<F>> = (0..4).map(|i| mix(200 + i)).collect();
1553        let pad: Vec<Flat<F>> = (0..32).map(|i| mix(300 + i)).collect();
1554        let claims_masked: Vec<Flat<F>> = plain
1555            .iter()
1556            .enumerate()
1557            .map(|(i, &c)| c + pad[BUS_PAD_FIRST as usize + i])
1558            .collect();
1559
1560        let beta = mix(9);
1561        let h = mix(400);
1562        let h_masked = h + pad[BUS_H_PAD as usize];
1563        let key = plain[0] + beta * mix(BUS_PUBLIC);
1564
1565        let record = bus_record(claims_masked.clone(), h_masked, beta);
1566        let bus = &record.consistency.buses[0];
1567        let [lhs, _] = bus_operand_rows(bus, BUS_PAD_FIRST, beta);
1568
1569        let one = Flat::from_raw(F::ONE);
1570        let forged = |role: WireRole| match role {
1571            WireRole::Lhs => one,
1572            WireRole::Rhs | WireRole::Product => h * key,
1573        };
1574
1575        assert_eq!(
1576            forged(WireRole::Lhs) * forged(WireRole::Rhs),
1577            forged(WireRole::Product)
1578        );
1579
1580        let mut left = Flat::from_raw(F::ZERO);
1581        for &(u, coeff) in &lhs.unknowns {
1582            let value = match u {
1583                Unknown::Pad(i) => pad[i as usize],
1584                Unknown::Wire { role, .. } => forged(role),
1585            };
1586
1587            left += coeff * value;
1588        }
1589
1590        let mut right = lhs.constant;
1591        for &(idx, coeff) in &lhs.claims {
1592            right += coeff * claims_masked[idx as usize];
1593        }
1594
1595        assert_ne!(left, right);
1596    }
1597
1598    #[test]
1599    fn layout_holds_whole_statement() {
1600        let cfg = hekate_core::config::Config::prod();
1601
1602        for (scalars, wires) in [(1usize, 0usize), (4_075, 2_384), (12_000, 20_000)] {
1603            let geom = cfg.outer_geom(scalars, wires, 128).unwrap();
1604            let layout = OuterLayout::new(&geom, scalars, wires).unwrap();
1605
1606            assert!(layout.pad_rows * layout.message_len >= scalars);
1607            assert!(layout.aux_rows * layout.message_len >= wires);
1608
1609            let (r, _) = layout.slot(Unknown::Pad(scalars as u32 - 1));
1610            assert!(r < layout.pad_rows);
1611
1612            if wires > 0 {
1613                let (r, _) = layout.slot(Unknown::Wire {
1614                    mul: wires as u32 - 1,
1615                    role: WireRole::Product,
1616                });
1617
1618                assert!(r < layout.interleaved_mask());
1619            }
1620
1621            assert_eq!(layout.total_rows(), layout.quadratic_mask_hi() + 1);
1622        }
1623    }
1624
1625    #[test]
1626    fn every_mask_row_interleaved_test_reveals_is_uniform() {
1627        let cfg = hekate_core::config::Config::prod();
1628        let (scalars, wires) = (4_075usize, 2_384usize);
1629
1630        let geom = cfg.outer_geom(scalars, wires, 128).unwrap();
1631        let layout = OuterLayout::new(&geom, scalars, wires).unwrap();
1632
1633        let triples: Vec<[Flat<F>; 3]> = (0..wires as u128)
1634            .map(|i| [mix(i), mix(i + (1 << 20)), mix(i + (1 << 40))])
1635            .collect();
1636
1637        let filler: Vec<Flat<F>> = (0..aux_filler_len(&layout, geom.code_len) as u128)
1638            .map(|i| mix(i + (1 << 60)))
1639            .collect();
1640
1641        let rows = build_aux_rows(&layout, &triples, geom.code_len, &filler).unwrap();
1642
1643        let zero = Flat::from_raw(F::ZERO);
1644
1645        for mask in [
1646            layout.interleaved_mask(),
1647            layout.linear_mask_hi(),
1648            layout.quadratic_mask_hi(),
1649        ] {
1650            assert!(
1651                rows[mask - layout.pad_rows][..layout.message_len]
1652                    .iter()
1653                    .all(|v| *v != zero)
1654            );
1655        }
1656
1657        let quadratic = layout.quadratic_mask() - layout.pad_rows;
1658        assert!(
1659            rows[quadratic][..layout.message_len]
1660                .iter()
1661                .all(|v| *v == zero)
1662        );
1663
1664        assert!(
1665            build_aux_rows(&layout, &triples, geom.code_len, &filler[1..]).is_err(),
1666            "filler length is exact"
1667        );
1668    }
1669
1670    #[test]
1671    fn mask_form_matches_round_by_round_recursion() {
1672        let degree = 3;
1673        let initial: Vec<(Unknown, Flat<F>)> = (0..4u32)
1674            .map(|i| (Unknown::Pad(i), mix(500 + i as u128)))
1675            .collect();
1676        let round_pads: Vec<Vec<u32>> = (0..5u32)
1677            .map(|i| {
1678                (0..degree as u32)
1679                    .map(|j| 100 + i * degree as u32 + j)
1680                    .collect()
1681            })
1682            .collect();
1683        let round_refs: Vec<&[u32]> = round_pads.iter().map(|p| p.as_slice()).collect();
1684        let challenges: Vec<Flat<F>> = (0..5).map(|i| mix(600 + i)).collect();
1685
1686        let mut expected: Vec<(Unknown, Flat<F>)> = initial.clone();
1687        for (pads, &r) in round_refs.iter().zip(&challenges) {
1688            let weights = lagrange_weights::<F>(degree, r);
1689            for (_, coeff) in expected.iter_mut() {
1690                *coeff *= weights[0];
1691            }
1692
1693            expected.push((Unknown::Pad(pads[0]), weights[0]));
1694
1695            for (j, &pad) in pads.iter().enumerate() {
1696                expected.push((Unknown::Pad(pad), weights[j + 1]));
1697            }
1698        }
1699
1700        assert_eq!(
1701            sumcheck_mask_form(&initial, &round_refs, &challenges, degree),
1702            expected
1703        );
1704    }
1705}