1use core::{array, iter};
27use std::collections::HashMap;
28
29use halo2_base::{
30 halo2_proofs::{
31 arithmetic::Field,
32 halo2curves::{bn256::Fr, ff::PrimeField as _},
33 },
34 utils::{bit_length, fe_to_biguint},
35};
36use itertools::Itertools;
37use openvm_stark_sdk::{
38 openvm_stark_backend::p3_field::{
39 extension::BinomiallyExtendable, BasedVectorSpace, PrimeField64,
40 },
41 p3_baby_bear::BabyBear,
42};
43use serde::{Deserialize, Serialize};
44
45use crate::{
46 chip_traits::{
47 BabyBearExt4Inst, BabyBearInst, ChipBase, GateInst, PopulateInputs, Poseidon2Inst,
48 TranscriptInst,
49 },
50 field::baby_bear::{
51 BabyBearExt4, BabyBearExt4Wire, BabyBearWire, ReducedBabyBearExt4Wire, ReducedBabyBearWire,
52 BABYBEAR_MAX_BITS, BABY_BEAR_MODULUS_U64, RESERVED_HIGH_BITS,
53 },
54 hash::{
55 poseidon2::{MULTI_FIELD32_NUM_F_ELMS, MULTI_FIELD32_RATE, POSEIDON2_RATE},
56 POSEIDON2_WIDTH,
57 },
58 tracegen::opcode_impl::{derive_opcode_metadata, UNMATERIALIZED},
59 transcript::{DigestWire, NUM_OBS_PER_WORD, NUM_SAMPLES_PER_WORD},
60};
61
62pub type NodeId = u32;
64
65const REDUCE_THRESHOLD: usize = Fr::CAPACITY as usize - RESERVED_HIGH_BITS;
67const RAW_MAX_BITS: usize = Fr::NUM_BITS as usize;
69
70const _: () = assert!(POSEIDON2_WIDTH == 3);
71
72#[derive(Copy, Clone, Debug, PartialEq, Eq)]
74pub enum GraphCell {
75 Cell(NodeId, usize, u16),
79 Const(Fr),
81}
82
83impl GraphCell {
84 pub fn bits(&self) -> usize {
85 match self {
86 Self::Cell(_, _, bits) => *bits as usize,
87 Self::Const(value) => fr_bits(value),
88 }
89 }
90}
91
92#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95pub enum Halo2Opcode {
96 Const,
99 Select,
101 Num2Bits(u16),
103 BBReduce,
107 BBAdd,
108 BBNeg,
109 BBSub,
110 BBMul,
111 BBMulAdd,
112 BBDiv,
115 BBAssertZero,
117 ExtMul,
121 ExtDiv,
125 PoseidonPermute2T2,
128 PoseidonPermute2T3,
130 LoadWitness,
133 CheckLessThanSafe,
135 InnerProduct(u16),
138 DecomposeBn254ToBabyBear,
142 RangeDiv(u16),
144}
145
146impl Halo2Opcode {
147 pub fn num_operands(&self) -> usize {
148 match self {
149 Self::LoadWitness => 0,
150 Self::CheckLessThanSafe => 1,
151 Self::Const
152 | Self::Num2Bits(_)
153 | Self::BBReduce
154 | Self::BBNeg
155 | Self::BBAssertZero
156 | Self::DecomposeBn254ToBabyBear
157 | Self::RangeDiv(_) => 1,
158 Self::BBAdd | Self::BBSub | Self::BBMul | Self::BBDiv | Self::PoseidonPermute2T2 => 2,
159 Self::Select | Self::BBMulAdd | Self::PoseidonPermute2T3 => 3,
160 Self::ExtMul | Self::ExtDiv => 8,
161 Self::InnerProduct(n) => 2 * *n as usize,
162 }
163 }
164
165 pub fn num_results(&self) -> usize {
166 match self {
167 Self::BBAssertZero | Self::CheckLessThanSafe => 0,
168 Self::Const
169 | Self::Select
170 | Self::BBReduce
171 | Self::BBAdd
172 | Self::BBNeg
173 | Self::BBSub
174 | Self::BBMul
175 | Self::BBMulAdd
176 | Self::LoadWitness
177 | Self::InnerProduct(_)
178 | Self::RangeDiv(_) => 1,
179 Self::BBDiv | Self::PoseidonPermute2T2 => 2,
180 Self::PoseidonPermute2T3 => 3,
181 Self::ExtMul => 5,
182 Self::DecomposeBn254ToBabyBear => NUM_SAMPLES_PER_WORD,
183 Self::ExtDiv => 7,
184 Self::Num2Bits(n) => *n as usize,
185 }
186 }
187
188 pub fn name(&self) -> &'static str {
189 match self {
190 Self::Const => "Const",
191 Self::Select => "Select",
192 Self::Num2Bits(_) => "Num2Bits",
193 Self::BBReduce => "BBReduce",
194 Self::BBAdd => "BBAdd",
195 Self::BBNeg => "BBNeg",
196 Self::BBSub => "BBSub",
197 Self::BBMul => "BBMul",
198 Self::BBMulAdd => "BBMulAdd",
199 Self::BBDiv => "BBDiv",
200 Self::BBAssertZero => "BBAssertZero",
201 Self::ExtMul => "ExtMul",
202 Self::ExtDiv => "ExtDiv",
203 Self::PoseidonPermute2T2 => "PoseidonPermute2T2",
204 Self::PoseidonPermute2T3 => "PoseidonPermute2T3",
205 Self::LoadWitness => "LoadWitness",
206 Self::CheckLessThanSafe => "CheckLessThanSafe",
207 Self::InnerProduct(_) => "InnerProduct",
208 Self::DecomposeBn254ToBabyBear => "DecomposeBn254ToBabyBear",
209 Self::RangeDiv(_) => "RangeDiv",
210 }
211 }
212}
213
214#[derive(Clone, Debug)]
215pub struct Halo2GraphNode {
216 pub opcode: Halo2Opcode,
217 pub operands: Vec<GraphCell>,
218 pub id: NodeId,
219}
220
221#[derive(Clone, Debug)]
225pub struct NodeMeta {
226 pub ctx_len: usize,
228 pub ctx_offset: usize,
230 pub lookups_len: usize,
232 pub lookup_offset: usize,
234 pub constant_skip_inds: Vec<u32>,
237 pub arg_offsets: Vec<usize>,
240 pub level: u32,
242}
243
244#[derive(Clone, Debug)]
246struct IrTranscript {
247 sponge_state: [GraphCell; POSEIDON2_WIDTH],
248 absorb_idx: usize,
249 sample_idx: usize,
250 observe_buf: Vec<ReducedBabyBearWire<GraphCell>>,
251 sample_buf: Vec<BabyBearWire<GraphCell>>,
252}
253
254pub struct Halo2IRBuilder {
256 pub nodes: Vec<Halo2GraphNode>,
258 pub node_meta: Vec<NodeMeta>,
260 pub input_values: Vec<Fr>,
262 lookup_bits: usize,
264 ctx_offset: usize,
266 lookup_offset: usize,
268 zero_cell: Option<GraphCell>,
270 bb_const_cache: HashMap<Fr, BabyBearWire<GraphCell>>,
272 transcript_bb_constant_cache: HashMap<Fr, BabyBearWire<GraphCell>>,
276 transcript: Option<IrTranscript>,
277}
278
279fn fr_bits(value: &Fr) -> usize {
280 fe_to_biguint(value).bits() as usize
281}
282
283fn bb_wire(value: GraphCell, max_bits: usize) -> BabyBearWire<GraphCell> {
284 BabyBearWire { value, max_bits }
285}
286
287enum ConstCacheType {
288 Transcript,
289 BabyBear,
290 Gate,
291}
292impl Halo2IRBuilder {
293 pub fn new(lookup_bits: usize) -> Self {
294 Halo2IRBuilder {
295 nodes: Vec::new(),
296 node_meta: Vec::new(),
297 input_values: Vec::new(),
298 lookup_bits,
299 ctx_offset: 0,
300 lookup_offset: 0,
301 zero_cell: None,
302 bb_const_cache: HashMap::new(),
303 transcript_bb_constant_cache: HashMap::new(),
304 transcript: None,
305 }
306 }
307
308 pub fn total_ctx_len(&self) -> usize {
310 self.ctx_offset
311 }
312
313 pub fn total_lookups_len(&self) -> usize {
315 self.lookup_offset
316 }
317
318 pub fn lookup_bits(&self) -> usize {
320 self.lookup_bits
321 }
322
323 fn emit(
328 &mut self,
329 opcode: Halo2Opcode,
330 operands: Vec<GraphCell>,
331 cache: ConstCacheType,
332 ) -> (NodeId, Vec<usize>) {
333 debug_assert_eq!(operands.len(), opcode.num_operands());
334 let id = NodeId::try_from(self.nodes.len()).expect("node count exceeds NodeId range");
335
336 let mut level = 0u32;
339 let mut args: Vec<Fr> = Vec::with_capacity(operands.len());
340 let mut bits: Vec<u16> = Vec::with_capacity(operands.len());
341 let mut arg_offsets: Vec<usize> = Vec::with_capacity(operands.len());
342 for cell in &operands {
343 match cell {
344 GraphCell::Cell(node, offset, _) => {
345 level = level.max(self.node_meta[*node as usize].level + 1);
346 args.push(Fr::ONE);
347 arg_offsets.push(*offset);
348 }
349 GraphCell::Const(value) => {
350 args.push(*value);
351 arg_offsets.push(UNMATERIALIZED);
352 }
353 }
354 bits.push(cell.bits() as u16);
355 }
356
357 let meta = match cache {
358 ConstCacheType::Transcript => derive_opcode_metadata(
359 &opcode,
360 &args,
361 &bits,
362 self.lookup_bits,
363 self.transcript_bb_constant_cache.keys(),
364 ),
365 ConstCacheType::BabyBear => derive_opcode_metadata(
366 &opcode,
367 &args,
368 &bits,
369 self.lookup_bits,
370 self.bb_const_cache.keys(),
371 ),
372 ConstCacheType::Gate => {
374 let zero = self.zero_cell.map(|_| Fr::ZERO);
375 derive_opcode_metadata(&opcode, &args, &bits, self.lookup_bits, zero.iter())
376 }
377 };
378
379 let ctx_offset = self.ctx_offset;
380 let output_offsets: Vec<usize> = meta
381 .output_offsets
382 .iter()
383 .map(|&rel| {
384 if rel == UNMATERIALIZED {
385 UNMATERIALIZED
386 } else {
387 ctx_offset + rel
388 }
389 })
390 .collect();
391 self.node_meta.push(NodeMeta {
392 ctx_len: meta.ctx_len,
393 ctx_offset,
394 lookups_len: meta.lookups_len,
395 lookup_offset: self.lookup_offset,
396 constant_skip_inds: meta.constant_skip_inds,
397 arg_offsets,
398 level,
399 });
400 self.ctx_offset += meta.ctx_len;
401 self.lookup_offset += meta.lookups_len;
402
403 self.nodes.push(Halo2GraphNode {
404 opcode,
405 operands,
406 id,
407 });
408 (id, output_offsets)
409 }
410
411 fn emit1(
412 &mut self,
413 opcode: Halo2Opcode,
414 operands: Vec<GraphCell>,
415 bits: usize,
416 cache: ConstCacheType,
417 ) -> GraphCell {
418 let (id, offsets) = self.emit(opcode, operands, cache);
419 GraphCell::Cell(id, offsets[0], bits as u16)
420 }
421
422 fn load_zero(&mut self) -> GraphCell {
424 if let Some(zero) = self.zero_cell {
425 return zero;
426 }
427 let zero = self.emit1(
428 Halo2Opcode::Const,
429 vec![GraphCell::Const(Fr::ZERO)],
430 0,
431 ConstCacheType::BabyBear,
432 );
433 self.zero_cell = Some(zero);
434 self.bb_const_cache.insert(Fr::ZERO, bb_wire(zero, 0));
435 self.transcript_bb_constant_cache
436 .insert(Fr::ZERO, bb_wire(zero, 0));
437 zero
438 }
439
440 fn note_internal_bb_const(&mut self, id: NodeId, offset: usize, value: BabyBear) {
444 let key_u64 = value.as_canonical_u64();
445 let key = Fr::from(key_u64);
446 if self.bb_const_cache.contains_key(&key) {
447 return;
448 }
449 let max_bits = bit_length(key_u64);
450 let (cell, wire_bits) = if key_u64 == 0 {
451 let zero = match self.zero_cell {
452 Some(zero) => zero,
453 None => {
454 debug_assert_ne!(offset, UNMATERIALIZED);
455 let zero = GraphCell::Cell(id, offset, 0);
456 self.zero_cell = Some(zero);
457 zero
458 }
459 };
460 (zero, 0)
461 } else {
462 debug_assert_ne!(offset, UNMATERIALIZED);
463 (GraphCell::Cell(id, offset, max_bits as u16), max_bits)
464 };
465 self.bb_const_cache.insert(key, bb_wire(cell, wire_bits));
466 }
467
468 fn bb_reduce_wire(&mut self, a: BabyBearWire<GraphCell>) -> BabyBearWire<GraphCell> {
469 assert!(a.max_bits <= REDUCE_THRESHOLD);
470 let cell = self.emit1(
471 Halo2Opcode::BBReduce,
472 vec![a.value],
473 BABYBEAR_MAX_BITS,
474 ConstCacheType::BabyBear,
475 );
476 bb_wire(cell, BABYBEAR_MAX_BITS)
477 }
478
479 fn inner_product(&mut self, values: &[GraphCell], coeffs: &[Fr]) -> GraphCell {
480 assert_eq!(values.len(), coeffs.len());
481 let operands = values
482 .iter()
483 .zip(coeffs)
484 .flat_map(|(&value, &coeff)| [value, GraphCell::Const(coeff)])
485 .collect_vec();
486 self.emit1(
487 Halo2Opcode::InnerProduct(values.len() as u16),
488 operands,
489 RAW_MAX_BITS,
490 ConstCacheType::BabyBear,
491 )
492 }
493
494 fn pack_base_2_31(&mut self, values: &[ReducedBabyBearWire<GraphCell>]) -> GraphCell {
496 assert!(values.len() <= MULTI_FIELD32_NUM_F_ELMS);
497 let base = Fr::from(1u64 << 31);
498 let coeffs = iter::successors(Some(Fr::ONE), |power| Some(*power * base))
499 .take(values.len())
500 .collect_vec();
501 let operands = values.iter().map(|value| value.value()).collect_vec();
502 self.inner_product(&operands, &coeffs)
503 }
504
505 fn permute_t3(&mut self, state: &mut [GraphCell; POSEIDON2_WIDTH]) {
506 let (id, offsets) = self.emit(
507 Halo2Opcode::PoseidonPermute2T3,
508 state.to_vec(),
509 ConstCacheType::BabyBear,
510 );
511 *state = array::from_fn(|i| GraphCell::Cell(id, offsets[i], RAW_MAX_BITS as u16));
512 }
513
514 fn take_transcript(&mut self) -> IrTranscript {
517 self.transcript
518 .take()
519 .expect("transcript not initialized; call init_transcript first")
520 }
521
522 fn sponge_absorb(&mut self, t: &mut IrTranscript, value: GraphCell) {
523 t.sponge_state[t.absorb_idx] = value;
524 t.absorb_idx += 1;
525 if t.absorb_idx == POSEIDON2_RATE {
526 self.permute_t3(&mut t.sponge_state);
527 t.absorb_idx = 0;
528 t.sample_idx = POSEIDON2_RATE;
529 }
530 }
531
532 fn sponge_squeeze(&mut self, t: &mut IrTranscript) -> GraphCell {
533 if t.absorb_idx != 0 || t.sample_idx == 0 {
534 self.permute_t3(&mut t.sponge_state);
535 t.absorb_idx = 0;
536 t.sample_idx = POSEIDON2_RATE;
537 }
538 t.sample_idx -= 1;
539 t.sponge_state[t.sample_idx]
540 }
541
542 fn flush_observe_buf(&mut self, t: &mut IrTranscript) {
543 if !t.observe_buf.is_empty() {
544 let packed = self.pack_base_2_31(&t.observe_buf);
545 self.sponge_absorb(t, packed);
546 t.observe_buf.clear();
547 }
548 }
549
550 fn observe_inner(&mut self, t: &mut IrTranscript, value: &ReducedBabyBearWire<GraphCell>) {
551 t.sample_buf.clear();
552 t.observe_buf.push(*value);
553 if t.observe_buf.len() == NUM_OBS_PER_WORD {
554 self.flush_observe_buf(t);
555 }
556 }
557
558 fn sample_inner(&mut self, t: &mut IrTranscript) -> BabyBearWire<GraphCell> {
559 if let Some(val) = t.sample_buf.pop() {
560 return val;
561 }
562 self.flush_observe_buf(t);
563 let squeezed = self.sponge_squeeze(t);
564 let (id, offsets) = self.emit(
565 Halo2Opcode::DecomposeBn254ToBabyBear,
566 vec![squeezed],
567 ConstCacheType::BabyBear,
568 );
569 t.sample_buf = (0..NUM_SAMPLES_PER_WORD)
570 .map(|i| {
571 bb_wire(
572 GraphCell::Cell(id, offsets[i], BABYBEAR_MAX_BITS as u16),
573 BABYBEAR_MAX_BITS,
574 )
575 })
576 .collect();
577 t.sample_buf.reverse();
579 t.sample_buf.pop().expect("sample_buf should be non-empty")
580 }
581
582 fn sample_bits_inner(&mut self, t: &mut IrTranscript, bits: usize) -> GraphCell {
583 assert!(
584 bits < (u32::BITS as usize),
585 "sample_bits requires bits < 32: {bits}"
586 );
587 assert!(
588 (1u64 << bits) < BABY_BEAR_MODULUS_U64,
589 "sample_bits requires (1 << bits) < modulus: bits={bits}"
590 );
591 let sampled = self.sample_inner(t);
592 if bits == 0 {
593 return self.load_zero();
594 }
595 self.emit1(
596 Halo2Opcode::RangeDiv(bits as u16),
597 vec![sampled.value],
598 bits,
599 ConstCacheType::BabyBear,
600 )
601 }
602
603 #[cfg(test)]
606 pub fn stats(&self) -> IrStats {
607 let mut level_widths: Vec<usize> = Vec::new();
608 for level in self.node_meta.iter().map(|meta| meta.level) {
609 let level = level as usize;
610 if level >= level_widths.len() {
611 level_widths.resize(level + 1, 0);
612 }
613 level_widths[level] += 1;
614 }
615 let mut counts: HashMap<&'static str, usize> = HashMap::new();
616 for node in &self.nodes {
617 *counts.entry(node.opcode.name()).or_default() += 1;
618 }
619 let mut opcode_counts = counts.into_iter().collect_vec();
620 opcode_counts.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
621 let num_levels = level_widths.len();
622 IrStats {
623 num_nodes: self.nodes.len(),
624 num_inputs: self.input_values.len(),
625 num_levels,
626 max_width: level_widths.iter().copied().max().unwrap_or(0),
627 avg_width: self.nodes.len() as f64 / num_levels.max(1) as f64,
628 opcode_counts,
629 level_widths,
630 }
631 }
632}
633
634#[cfg(test)]
636#[derive(Clone, Debug)]
637pub struct IrStats {
638 pub num_nodes: usize,
639 pub num_inputs: usize,
640 pub num_levels: usize,
642 pub max_width: usize,
644 pub avg_width: f64,
646 pub opcode_counts: Vec<(&'static str, usize)>,
648 pub level_widths: Vec<usize>,
650}
651
652#[cfg(test)]
653impl core::fmt::Display for IrStats {
654 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
655 writeln!(f, "nodes: {}", self.num_nodes)?;
656 writeln!(f, "proof-input witnesses: {}", self.num_inputs)?;
657 writeln!(f, "max depth (levels): {}", self.num_levels)?;
658 writeln!(f, "max width: {}", self.max_width)?;
659 writeln!(f, "avg width per level: {:.1}", self.avg_width)?;
660 writeln!(f, "opcode distribution:")?;
661 for (name, count) in &self.opcode_counts {
662 writeln!(
663 f,
664 " {name:<22} {count:>10} ({:.2}%)",
665 100.0 * *count as f64 / self.num_nodes as f64
666 )?;
667 }
668 Ok(())
669 }
670}
671
672fn w_bits() -> usize {
675 bit_length(<BabyBear as BinomiallyExtendable<4>>::W.as_canonical_u64())
676}
677
678fn mul_add_result_bits(mut a: usize, mut b: usize, mut c: usize) -> usize {
680 if a < b {
681 core::mem::swap(&mut a, &mut b);
682 }
683 if a + b + 1 > REDUCE_THRESHOLD {
684 a = BABYBEAR_MAX_BITS;
685 if a + b + 1 > REDUCE_THRESHOLD {
686 b = BABYBEAR_MAX_BITS;
687 }
688 }
689 if c + 1 > REDUCE_THRESHOLD {
690 c = BABYBEAR_MAX_BITS;
691 }
692 c.max(a + b) + 1
693}
694
695fn special_inner_product_bits(a: &mut [usize; 4], b: &mut [usize; 4], s: usize) -> usize {
698 let mut max_bits = 0;
699 let lb = s.saturating_sub(3);
700 let ub = 4.min(s + 1);
701 let len = if s < 3 { s + 1 } else { 7 - s };
702 for (i, (ci, di)) in (lb..ub).zip(((s + 1 - ub)..(s + 1 - lb)).rev()).enumerate() {
703 let limit = REDUCE_THRESHOLD - len + i;
704 if a[ci] + b[di] > limit {
705 if a[ci] >= b[di] {
706 a[ci] = BABYBEAR_MAX_BITS;
707 if a[ci] + b[di] > limit {
708 b[di] = BABYBEAR_MAX_BITS;
709 }
710 } else {
711 b[di] = BABYBEAR_MAX_BITS;
712 if a[ci] + b[di] > limit {
713 a[ci] = BABYBEAR_MAX_BITS;
714 }
715 }
716 }
717 max_bits = if i == 0 {
718 a[ci] + b[di]
719 } else {
720 max_bits.max(a[ci] + b[di]) + 1
721 };
722 }
723 max_bits
724}
725
726fn ext_mul_result_bits(mut a: [usize; 4], mut b: [usize; 4]) -> [usize; 4] {
728 let mut coeffs = Vec::with_capacity(7);
729 for s in 0..7 {
730 coeffs.push(special_inner_product_bits(&mut a, &mut b, s));
731 }
732 let w = w_bits();
733 for i in 4..7 {
734 coeffs[i - 4] = mul_add_result_bits(coeffs[i], w, coeffs[i - 4]);
735 }
736 [coeffs[0], coeffs[1], coeffs[2], coeffs[3]]
737}
738
739impl ChipBase for Halo2IRBuilder {
740 type F = GraphCell;
741}
742
743impl PopulateInputs for Halo2IRBuilder {
744 fn load_witness(&mut self, value: Fr) -> GraphCell {
745 self.input_values.push(value);
746 self.emit1(
747 Halo2Opcode::LoadWitness,
748 vec![],
749 RAW_MAX_BITS,
750 ConstCacheType::BabyBear,
751 )
752 }
753
754 fn bb_load_reduced_witness(&mut self, value: BabyBear) -> ReducedBabyBearWire<GraphCell> {
755 self.input_values.push(Fr::from(value.as_canonical_u64()));
756 let cell = self.emit1(
757 Halo2Opcode::LoadWitness,
758 vec![],
759 BABYBEAR_MAX_BITS,
760 ConstCacheType::BabyBear,
761 );
762 self.emit(
763 Halo2Opcode::CheckLessThanSafe,
764 vec![cell],
765 ConstCacheType::BabyBear,
766 );
767 ReducedBabyBearWire::assume_reduced(bb_wire(cell, BABYBEAR_MAX_BITS))
768 }
769
770 fn ext_load_reduced_witness(
771 &mut self,
772 value: BabyBearExt4,
773 ) -> ReducedBabyBearExt4Wire<GraphCell> {
774 let coeffs = value.as_basis_coefficients_slice();
775 ReducedBabyBearExt4Wire::assume_reduced(array::from_fn(|i| {
776 self.bb_load_reduced_witness(coeffs[i])
777 }))
778 }
779}
780
781impl GateInst for Halo2IRBuilder {
782 fn load_constant(&mut self, value: Fr) -> GraphCell {
784 let bits = fr_bits(&value);
785 self.emit1(
786 Halo2Opcode::Const,
787 vec![GraphCell::Const(value)],
788 bits,
789 ConstCacheType::Gate,
790 )
791 }
792
793 fn constrain_equal(&mut self, _a: GraphCell, _b: GraphCell) {}
795
796 fn select(
797 &mut self,
798 when_true: GraphCell,
799 when_false: GraphCell,
800 cond: GraphCell,
801 ) -> GraphCell {
802 let bits = when_true.bits().max(when_false.bits());
803 self.emit1(
804 Halo2Opcode::Select,
805 vec![when_true, when_false, cond],
806 bits,
807 ConstCacheType::Gate,
808 )
809 }
810
811 fn select_const(&mut self, when_true: Fr, when_false: Fr, cond: GraphCell) -> GraphCell {
812 self.select(
813 GraphCell::Const(when_true),
814 GraphCell::Const(when_false),
815 cond,
816 )
817 }
818
819 fn num_to_bits(&mut self, a: GraphCell, range_bits: usize) -> Vec<GraphCell> {
820 let (id, offsets) = self.emit(
821 Halo2Opcode::Num2Bits(range_bits as u16),
822 vec![a],
823 ConstCacheType::Gate,
824 );
825 (0..range_bits)
826 .map(|i| GraphCell::Cell(id, offsets[i], 1))
827 .collect()
828 }
829
830 fn inner_product_const(&mut self, values: &[GraphCell], coeffs: &[Fr]) -> GraphCell {
831 self.inner_product(values, coeffs)
832 }
833
834 fn cell_count(&self) -> usize {
835 self.nodes.len()
836 }
837}
838
839impl BabyBearInst for Halo2IRBuilder {
840 fn bb_load_constant(&mut self, value: BabyBear) -> BabyBearWire<GraphCell> {
842 let key_u64 = value.as_canonical_u64();
843 let key = Fr::from(key_u64);
844 if let Some(&cached) = self.bb_const_cache.get(&key) {
845 return cached;
846 }
847 let max_bits = bit_length(key_u64);
848 let cell = if key_u64 == 0 {
849 self.load_zero()
850 } else {
851 self.emit1(
852 Halo2Opcode::Const,
853 vec![GraphCell::Const(key)],
854 max_bits,
855 ConstCacheType::BabyBear,
856 )
857 };
858 let wire = bb_wire(cell, max_bits);
859 self.bb_const_cache.insert(key, wire);
860 wire
861 }
862
863 fn bb_load_reduced_constant(&mut self, value: BabyBear) -> ReducedBabyBearWire<GraphCell> {
864 ReducedBabyBearWire::assume_reduced(self.bb_load_constant(value))
866 }
867
868 fn bb_reduce(&mut self, a: BabyBearWire<GraphCell>) -> BabyBearWire<GraphCell> {
869 self.bb_reduce_wire(a)
870 }
871
872 fn bb_reduce_max_bits(&mut self, a: BabyBearWire<GraphCell>) -> BabyBearWire<GraphCell> {
873 if a.max_bits > BABYBEAR_MAX_BITS {
874 self.bb_reduce_wire(a)
875 } else {
876 a
877 }
878 }
879
880 fn bb_add(
881 &mut self,
882 mut a: BabyBearWire<GraphCell>,
883 mut b: BabyBearWire<GraphCell>,
884 ) -> BabyBearWire<GraphCell> {
885 if a.max_bits + 1 > REDUCE_THRESHOLD {
886 a = self.bb_reduce_wire(a);
887 }
888 if b.max_bits + 1 > REDUCE_THRESHOLD {
889 b = self.bb_reduce_wire(b);
890 }
891 let max_bits = a.max_bits.max(b.max_bits) + 1;
892 let cell = self.emit1(
893 Halo2Opcode::BBAdd,
894 vec![a.value, b.value],
895 max_bits,
896 ConstCacheType::BabyBear,
897 );
898 bb_wire(cell, max_bits)
899 }
900
901 fn bb_neg(&mut self, a: BabyBearWire<GraphCell>) -> BabyBearWire<GraphCell> {
902 let cell = self.emit1(
903 Halo2Opcode::BBNeg,
904 vec![a.value],
905 a.max_bits,
906 ConstCacheType::BabyBear,
907 );
908 bb_wire(cell, a.max_bits)
909 }
910
911 fn bb_sub(
912 &mut self,
913 mut a: BabyBearWire<GraphCell>,
914 mut b: BabyBearWire<GraphCell>,
915 ) -> BabyBearWire<GraphCell> {
916 if a.max_bits + 1 > REDUCE_THRESHOLD {
917 a = self.bb_reduce_wire(a);
918 }
919 if b.max_bits + 1 > REDUCE_THRESHOLD {
920 b = self.bb_reduce_wire(b);
921 }
922 let max_bits = a.max_bits.max(b.max_bits) + 1;
923 let cell = self.emit1(
924 Halo2Opcode::BBSub,
925 vec![a.value, b.value],
926 max_bits,
927 ConstCacheType::BabyBear,
928 );
929 bb_wire(cell, max_bits)
930 }
931
932 fn bb_mul(
933 &mut self,
934 mut a: BabyBearWire<GraphCell>,
935 mut b: BabyBearWire<GraphCell>,
936 ) -> BabyBearWire<GraphCell> {
937 if a.max_bits < b.max_bits {
938 core::mem::swap(&mut a, &mut b);
939 }
940 if a.max_bits + b.max_bits > REDUCE_THRESHOLD {
941 a = self.bb_reduce_wire(a);
942 if a.max_bits + b.max_bits > REDUCE_THRESHOLD {
943 b = self.bb_reduce_wire(b);
944 }
945 }
946 let max_bits = a.max_bits + b.max_bits;
947 let cell = self.emit1(
948 Halo2Opcode::BBMul,
949 vec![a.value, b.value],
950 max_bits,
951 ConstCacheType::BabyBear,
952 );
953 bb_wire(cell, max_bits)
954 }
955
956 fn bb_mul_add(
957 &mut self,
958 mut a: BabyBearWire<GraphCell>,
959 mut b: BabyBearWire<GraphCell>,
960 mut c: BabyBearWire<GraphCell>,
961 ) -> BabyBearWire<GraphCell> {
962 if a.max_bits < b.max_bits {
963 core::mem::swap(&mut a, &mut b);
964 }
965 if a.max_bits + b.max_bits + 1 > REDUCE_THRESHOLD {
966 a = self.bb_reduce_wire(a);
967 if a.max_bits + b.max_bits + 1 > REDUCE_THRESHOLD {
968 b = self.bb_reduce_wire(b);
969 }
970 }
971 if c.max_bits + 1 > REDUCE_THRESHOLD {
972 c = self.bb_reduce_wire(c);
973 }
974 let max_bits = c.max_bits.max(a.max_bits + b.max_bits) + 1;
975 let cell = self.emit1(
976 Halo2Opcode::BBMulAdd,
977 vec![a.value, b.value, c.value],
978 max_bits,
979 ConstCacheType::BabyBear,
980 );
981 bb_wire(cell, max_bits)
982 }
983
984 fn bb_div(
985 &mut self,
986 a: BabyBearWire<GraphCell>,
987 b: BabyBearWire<GraphCell>,
988 ) -> BabyBearWire<GraphCell> {
989 let (id, offsets) = self.emit(
990 Halo2Opcode::BBDiv,
991 vec![a.value, b.value],
992 ConstCacheType::BabyBear,
993 );
994 self.note_internal_bb_const(id, offsets[1], BabyBear::new(1));
996 bb_wire(
997 GraphCell::Cell(id, offsets[0], BABYBEAR_MAX_BITS as u16),
998 BABYBEAR_MAX_BITS,
999 )
1000 }
1001
1002 fn bb_assert_zero(&mut self, a: BabyBearWire<GraphCell>) {
1003 assert!(a.max_bits <= REDUCE_THRESHOLD);
1004 self.emit(
1005 Halo2Opcode::BBAssertZero,
1006 vec![a.value],
1007 ConstCacheType::BabyBear,
1008 );
1009 }
1010
1011 fn bb_assert_equal(&mut self, a: BabyBearWire<GraphCell>, b: BabyBearWire<GraphCell>) {
1012 let diff = self.bb_sub(a, b);
1013 self.bb_assert_zero(diff);
1014 }
1015
1016 fn bb_zero(&mut self) -> BabyBearWire<GraphCell> {
1017 self.bb_load_constant(BabyBear::new(0))
1018 }
1019
1020 fn bb_one(&mut self) -> BabyBearWire<GraphCell> {
1021 self.bb_load_constant(BabyBear::new(1))
1022 }
1023
1024 fn bb_mul_const(&mut self, a: BabyBearWire<GraphCell>, c: BabyBear) -> BabyBearWire<GraphCell> {
1025 let c_wire = self.bb_load_constant(c);
1026 self.bb_mul(a, c_wire)
1027 }
1028
1029 fn bb_square(&mut self, a: BabyBearWire<GraphCell>) -> BabyBearWire<GraphCell> {
1030 self.bb_mul(a, a)
1031 }
1032
1033 fn bb_pow_power_of_two(
1034 &mut self,
1035 a: BabyBearWire<GraphCell>,
1036 n: usize,
1037 ) -> BabyBearWire<GraphCell> {
1038 let mut result = a;
1039 for _ in 0..n {
1040 result = self.bb_square(result);
1041 }
1042 result
1043 }
1044}
1045
1046impl BabyBearExt4Inst for Halo2IRBuilder {
1047 fn ext_load_constant(&mut self, value: BabyBearExt4) -> BabyBearExt4Wire<GraphCell> {
1048 let coeffs = value.as_basis_coefficients_slice();
1049 BabyBearExt4Wire(array::from_fn(|i| self.bb_load_constant(coeffs[i])))
1050 }
1051
1052 fn ext_load_reduced_constant(
1053 &mut self,
1054 value: BabyBearExt4,
1055 ) -> ReducedBabyBearExt4Wire<GraphCell> {
1056 let coeffs = value.as_basis_coefficients_slice();
1057 ReducedBabyBearExt4Wire::assume_reduced(array::from_fn(|i| {
1058 self.bb_load_reduced_constant(coeffs[i])
1059 }))
1060 }
1061
1062 fn ext_add(
1063 &mut self,
1064 a: BabyBearExt4Wire<GraphCell>,
1065 b: BabyBearExt4Wire<GraphCell>,
1066 ) -> BabyBearExt4Wire<GraphCell> {
1067 BabyBearExt4Wire(array::from_fn(|i| self.bb_add(a.0[i], b.0[i])))
1068 }
1069
1070 fn ext_neg(&mut self, a: BabyBearExt4Wire<GraphCell>) -> BabyBearExt4Wire<GraphCell> {
1071 BabyBearExt4Wire(array::from_fn(|i| self.bb_neg(a.0[i])))
1072 }
1073
1074 fn ext_sub(
1075 &mut self,
1076 a: BabyBearExt4Wire<GraphCell>,
1077 b: BabyBearExt4Wire<GraphCell>,
1078 ) -> BabyBearExt4Wire<GraphCell> {
1079 BabyBearExt4Wire(array::from_fn(|i| self.bb_sub(a.0[i], b.0[i])))
1080 }
1081
1082 fn ext_scalar_mul(
1083 &mut self,
1084 a: BabyBearExt4Wire<GraphCell>,
1085 b: BabyBearWire<GraphCell>,
1086 ) -> BabyBearExt4Wire<GraphCell> {
1087 BabyBearExt4Wire(array::from_fn(|i| self.bb_mul(a.0[i], b)))
1088 }
1089
1090 fn ext_scalar_mul_add(
1091 &mut self,
1092 a: BabyBearExt4Wire<GraphCell>,
1093 b: BabyBearWire<GraphCell>,
1094 c: BabyBearExt4Wire<GraphCell>,
1095 ) -> BabyBearExt4Wire<GraphCell> {
1096 BabyBearExt4Wire(array::from_fn(|i| self.bb_mul_add(a.0[i], b, c.0[i])))
1097 }
1098
1099 fn ext_assert_zero(&mut self, a: BabyBearExt4Wire<GraphCell>) {
1100 for x in a.0 {
1101 self.bb_assert_zero(x);
1102 }
1103 }
1104
1105 fn ext_assert_equal(&mut self, a: BabyBearExt4Wire<GraphCell>, b: BabyBearExt4Wire<GraphCell>) {
1106 for (a, b) in a.0.into_iter().zip(b.0) {
1107 self.bb_assert_equal(a, b);
1108 }
1109 }
1110
1111 fn ext_mul(
1112 &mut self,
1113 a: BabyBearExt4Wire<GraphCell>,
1114 b: BabyBearExt4Wire<GraphCell>,
1115 ) -> BabyBearExt4Wire<GraphCell> {
1116 let operands = a.0.iter().chain(b.0.iter()).map(|w| w.value).collect_vec();
1117 let (id, offsets) = self.emit(Halo2Opcode::ExtMul, operands, ConstCacheType::BabyBear);
1118 self.note_internal_bb_const(id, offsets[4], <BabyBear as BinomiallyExtendable<4>>::W);
1120 let bits = ext_mul_result_bits(a.0.map(|w| w.max_bits), b.0.map(|w| w.max_bits));
1121 BabyBearExt4Wire(array::from_fn(|i| {
1122 bb_wire(GraphCell::Cell(id, offsets[i], bits[i] as u16), bits[i])
1123 }))
1124 }
1125
1126 fn ext_div(
1127 &mut self,
1128 a: BabyBearExt4Wire<GraphCell>,
1129 b: BabyBearExt4Wire<GraphCell>,
1130 ) -> BabyBearExt4Wire<GraphCell> {
1131 let operands = a.0.iter().chain(b.0.iter()).map(|w| w.value).collect_vec();
1132 let (id, offsets) = self.emit(Halo2Opcode::ExtDiv, operands, ConstCacheType::BabyBear);
1133 self.note_internal_bb_const(id, offsets[4], BabyBear::new(1));
1136 self.note_internal_bb_const(id, offsets[5], BabyBear::new(0));
1137 self.note_internal_bb_const(id, offsets[6], <BabyBear as BinomiallyExtendable<4>>::W);
1138 BabyBearExt4Wire(array::from_fn(|i| {
1140 bb_wire(
1141 GraphCell::Cell(id, offsets[i], BABYBEAR_MAX_BITS as u16),
1142 BABYBEAR_MAX_BITS,
1143 )
1144 }))
1145 }
1146
1147 fn ext_reduce_max_bits(
1148 &mut self,
1149 a: BabyBearExt4Wire<GraphCell>,
1150 ) -> BabyBearExt4Wire<GraphCell> {
1151 BabyBearExt4Wire(array::from_fn(|i| self.bb_reduce_max_bits(a.0[i])))
1152 }
1153
1154 fn ext_zero(&mut self) -> BabyBearExt4Wire<GraphCell> {
1155 self.ext_from_base_const(BabyBear::new(0))
1156 }
1157
1158 fn ext_from_base_const(&mut self, value: BabyBear) -> BabyBearExt4Wire<GraphCell> {
1159 let base_val = self.bb_load_constant(value);
1160 let z = self.bb_load_constant(BabyBear::new(0));
1161 BabyBearExt4Wire([base_val, z, z, z])
1162 }
1163
1164 fn ext_from_base_var(&mut self, value: BabyBearWire<GraphCell>) -> BabyBearExt4Wire<GraphCell> {
1165 let z = self.bb_load_constant(BabyBear::new(0));
1166 BabyBearExt4Wire([value, z, z, z])
1167 }
1168
1169 fn ext_mul_base_const(
1170 &mut self,
1171 a: BabyBearExt4Wire<GraphCell>,
1172 c: BabyBear,
1173 ) -> BabyBearExt4Wire<GraphCell> {
1174 let c_wire = self.bb_load_constant(c);
1175 self.ext_scalar_mul(a, c_wire)
1176 }
1177
1178 fn ext_square(&mut self, a: BabyBearExt4Wire<GraphCell>) -> BabyBearExt4Wire<GraphCell> {
1179 self.ext_mul(a, a)
1180 }
1181
1182 fn ext_pow_power_of_two(
1183 &mut self,
1184 a: BabyBearExt4Wire<GraphCell>,
1185 n: usize,
1186 ) -> BabyBearExt4Wire<GraphCell> {
1187 let mut result = a;
1188 for _ in 0..n {
1189 result = self.ext_square(result);
1190 }
1191 result
1192 }
1193}
1194
1195impl Poseidon2Inst for Halo2IRBuilder {
1196 fn hash_babybear_slice_to_digest(
1198 &mut self,
1199 values: &[ReducedBabyBearWire<GraphCell>],
1200 ) -> GraphCell {
1201 let zero = self.load_zero();
1202 let mut state = [zero; POSEIDON2_WIDTH];
1203 for block_chunk in values.chunks(MULTI_FIELD32_RATE) {
1204 for (chunk_id, chunk) in block_chunk.chunks(MULTI_FIELD32_NUM_F_ELMS).enumerate() {
1205 state[chunk_id] = self.pack_base_2_31(chunk);
1206 }
1207 self.permute_t3(&mut state);
1208 }
1209 state[0]
1210 }
1211
1212 fn compress_digests(&mut self, left: GraphCell, right: GraphCell) -> GraphCell {
1213 let (id, offsets) = self.emit(
1214 Halo2Opcode::PoseidonPermute2T2,
1215 vec![left, right],
1216 ConstCacheType::BabyBear,
1217 );
1218 GraphCell::Cell(id, offsets[0], RAW_MAX_BITS as u16)
1219 }
1220}
1221
1222impl TranscriptInst for Halo2IRBuilder {
1223 fn init_transcript(&mut self) {
1224 let zero = self.load_zero();
1225 self.transcript = Some(IrTranscript {
1226 sponge_state: [zero; POSEIDON2_WIDTH],
1227 absorb_idx: 0,
1228 sample_idx: 0,
1229 observe_buf: Vec::with_capacity(NUM_OBS_PER_WORD),
1230 sample_buf: Vec::with_capacity(NUM_SAMPLES_PER_WORD),
1231 });
1232 }
1233
1234 fn observe(&mut self, value: &ReducedBabyBearWire<GraphCell>) {
1235 let mut t = self.take_transcript();
1236 self.observe_inner(&mut t, value);
1237 self.transcript = Some(t);
1238 }
1239
1240 fn observe_ext(&mut self, value: &ReducedBabyBearExt4Wire<GraphCell>) {
1241 let mut t = self.take_transcript();
1242 for coeff in value.coeffs() {
1243 self.observe_inner(&mut t, coeff);
1244 }
1245 self.transcript = Some(t);
1246 }
1247
1248 fn observe_commit(&mut self, digest: &DigestWire<GraphCell>) {
1249 let mut t = self.take_transcript();
1250 t.sample_buf.clear();
1251 self.flush_observe_buf(&mut t);
1252 for &elem in &digest.elems {
1253 self.sponge_absorb(&mut t, elem);
1254 }
1255 self.transcript = Some(t);
1256 }
1257
1258 fn sample(&mut self) -> BabyBearWire<GraphCell> {
1259 let mut t = self.take_transcript();
1260 let out = self.sample_inner(&mut t);
1261 self.transcript = Some(t);
1262 out
1263 }
1264
1265 fn sample_ext(&mut self) -> BabyBearExt4Wire<GraphCell> {
1266 let mut t = self.take_transcript();
1267 let coeffs = array::from_fn(|_| self.sample_inner(&mut t));
1268 self.transcript = Some(t);
1269 BabyBearExt4Wire(coeffs)
1270 }
1271
1272 fn sample_bits(&mut self, bits: usize) -> GraphCell {
1273 let mut t = self.take_transcript();
1274 let out = self.sample_bits_inner(&mut t, bits);
1275 self.transcript = Some(t);
1276 out
1277 }
1278
1279 fn check_witness(&mut self, bits: usize, witness: &ReducedBabyBearWire<GraphCell>) {
1280 if bits == 0 {
1281 return;
1282 }
1283 let mut t = self.take_transcript();
1284 self.observe_inner(&mut t, witness);
1285 let _sampled_bits = self.sample_bits_inner(&mut t, bits);
1288 self.transcript = Some(t);
1289 }
1290
1291 fn transcript_load_reduced_constant(
1292 &mut self,
1293 value: BabyBear,
1294 ) -> ReducedBabyBearWire<Self::F> {
1295 let key_u64 = value.as_canonical_u64();
1296 let key = Fr::from(key_u64);
1297 if let Some(&cached) = self.transcript_bb_constant_cache.get(&key) {
1298 return ReducedBabyBearWire::assume_reduced(cached);
1299 }
1300 let max_bits = bit_length(key_u64);
1301 let cell = if key_u64 == 0 {
1302 self.load_zero()
1303 } else {
1304 self.emit1(
1305 Halo2Opcode::Const,
1306 vec![GraphCell::Const(key)],
1307 max_bits,
1308 ConstCacheType::Transcript,
1309 )
1310 };
1311 let wire = bb_wire(cell, max_bits);
1312 self.transcript_bb_constant_cache.insert(key, wire);
1313 ReducedBabyBearWire::assume_reduced(wire)
1314 }
1315}
1316
1317#[cfg(test)]
1318mod tests {
1319 use super::*;
1320 use crate::test_fixtures::{fixture_circuit_and_proof, FIXTURE_K};
1321
1322 #[test]
1323 fn ir_generation_stats_for_fixture_proof() {
1324 let (circuit, proof) = fixture_circuit_and_proof();
1325
1326 let mut builder = Halo2IRBuilder::new(FIXTURE_K - 1);
1327 circuit.populate_verify_stark_constraints(&mut builder, &proof);
1328
1329 let stats = builder.stats();
1330 println!("=== static verifier IR stats ===");
1331 print!("{stats}");
1332 println!("ctx cells: {}", builder.total_ctx_len());
1333 println!("lookup cells: {}", builder.total_lookups_len());
1334 assert!(stats.num_nodes > 0);
1335 assert_eq!(builder.nodes.len(), builder.node_meta.len());
1336 }
1337}