1use 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 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 pub boundary: Vec<(u32, Flat<F>, Flat<F>)>,
55
56 pub telescope: Vec<(u32, u32)>,
58
59 pub buses: Vec<BusRow<F>>,
60
61 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 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 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 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 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 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 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#[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
304pub struct TableRecord<F> {
306 pub claims_masked: Vec<Flat<F>>,
307 pub predicate: PredicateRows<F>,
308 pub consistency: ConsistencyInputs<F>,
309
310 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 pub claimed_sums: Vec<(String, u32, Flat<F>)>,
319
320 pub mul_wires: u32,
321}
322
323pub struct OuterRows<F> {
327 pub affine: Vec<AffineRow<F>>,
328 pub gadgets: Vec<RingGadget<F>>,
329}
330
331pub struct InitialForm<F> {
333 pub form: Vec<(Unknown, Flat<F>)>,
334 pub gadget: Option<RingGadget<F>>,
335}
336
337#[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
347pub 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
366pub 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
439pub 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
653pub 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 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
829pub 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
866pub 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
880pub 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
895pub 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
913pub 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
957pub 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
1001pub 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
1032pub 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 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 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}