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, 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 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, 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 + self.num_buses
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 2 * self.num_buses + zerocheck + claims + trace_eval
144 }
145
146 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 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#[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
297pub struct TableRecord<F> {
299 pub claims_masked: Vec<Flat<F>>,
300 pub predicate: PredicateRows<F>,
301 pub consistency: ConsistencyInputs<F>,
302
303 pub fixed: Vec<(u32, u32, Flat<F>)>,
305
306 pub trace_eval: EvalRecord<F>,
307 pub gadget: Option<RingGadget<F>>,
308
309 pub claimed_sums: Vec<(String, u32, Flat<F>)>,
311
312 pub mul_wires: u32,
313}
314
315pub struct OuterRows<F> {
319 pub affine: Vec<AffineRow<F>>,
320 pub gadgets: Vec<RingGadget<F>>,
321}
322
323pub struct InitialForm<F> {
325 pub form: Vec<(Unknown, Flat<F>)>,
326 pub gadget: Option<RingGadget<F>>,
327}
328
329#[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
339pub 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
358pub 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
431pub 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
650pub 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 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
843pub 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
880pub 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
894pub 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
909pub 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
926pub 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
944pub 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
988pub 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
1032pub 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
1063pub 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 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
1139pub 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 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 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}