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