miden_air/constraints/chiplets/columns.rs
1//! Column structs for all chiplet sub-components and periodic columns.
2
3use alloc::{vec, vec::Vec};
4use core::{
5 borrow::{Borrow, BorrowMut},
6 mem::size_of,
7};
8
9use miden_core::{Felt, WORD_SIZE, chiplets::hasher::Hasher, field::PrimeCharacteristicRing};
10
11use super::super::{columns::indices_arr, ext_field::QuadFeltExpr};
12use crate::trace::chiplets::{
13 bitwise::NUM_DECOMP_BITS,
14 hasher::{CAPACITY_LEN, DIGEST_LEN, HASH_CYCLE_LEN, NUM_SELECTORS, RATE_LEN, STATE_WIDTH},
15};
16
17// HELPERS
18// ================================================================================================
19
20/// Generates `Borrow<$cols<T>> for [T]` and the mutable counterpart for a chiplet column
21/// struct. The slice length must equal `size_of::<$cols<u8>>()` cells.
22macro_rules! impl_borrow_for_chiplet_cols {
23 ($cols:ident) => {
24 impl<T> Borrow<$cols<T>> for [T] {
25 fn borrow(&self) -> &$cols<T> {
26 debug_assert_eq!(self.len(), size_of::<$cols<u8>>());
27 let (prefix, cols, suffix) = unsafe { self.align_to::<$cols<T>>() };
28 debug_assert!(prefix.is_empty() && suffix.is_empty() && cols.len() == 1);
29 &cols[0]
30 }
31 }
32 impl<T> BorrowMut<$cols<T>> for [T] {
33 fn borrow_mut(&mut self) -> &mut $cols<T> {
34 debug_assert_eq!(self.len(), size_of::<$cols<u8>>());
35 let (prefix, cols, suffix) = unsafe { self.align_to_mut::<$cols<T>>() };
36 debug_assert!(prefix.is_empty() && suffix.is_empty() && cols.len() == 1);
37 &mut cols[0]
38 }
39 }
40 };
41}
42
43// PERMUTATION COLUMNS
44// ================================================================================================
45
46/// Permutation chiplet columns (19 columns), viewed from `chiplets[0..19]`.
47///
48/// Logical overlay for permutation segment rows (`s_00 = 1`). The 3 witness columns
49/// `w0..w2` share the same physical columns as the controller's `s0/s1/s2` selectors,
50/// and `multiplicity` shares the same physical column as the controller's `node_index`.
51///
52/// `s_00` and `s_01` (= `ChipletCols::s_00` and `ChipletCols::s_01`) are consumed by the chiplet
53/// selector system and are NOT part of this overlay.
54///
55/// The state holds a Poseidon2 sponge in `[RATE0, RATE1, CAPACITY]` layout.
56/// Helper methods `rate0()`, `rate1()`, `capacity()`, and `digest()` provide
57/// sub-views into the state array.
58///
59/// ## Layout
60///
61/// ```text
62/// | witnesses[3] | state[12] | extra cols |
63/// | | rate0[4] (= digest) | rate1[4] | capacity[4] | |
64/// | w0, w1, w2 | h0..h3 | h4..h7 | h8..h11 | m -- -- -- |
65/// ```
66#[repr(C)]
67#[derive(Clone, Debug)]
68pub struct PermutationCols<T> {
69 /// S-box witness columns (same physical columns as hasher selectors).
70 pub witnesses: [T; NUM_SELECTORS],
71 /// Poseidon2 state (12 field elements: 8 rate + 4 capacity).
72 pub state: [T; STATE_WIDTH],
73 /// Request multiplicity (same physical column as node_index).
74 pub multiplicity: T,
75 /// Physical slots for controller columns mrupdate_id, is_boundary, and direction_bit.
76 /// These must be zero on permutation rows; access via [`Self::unused_padding()`] only.
77 _unused: [T; 3],
78}
79
80impl<T: Copy> PermutationCols<T> {
81 /// Returns the rate portion of the state (state[0..8]).
82 pub fn rate(&self) -> [T; RATE_LEN] {
83 [
84 self.state[0],
85 self.state[1],
86 self.state[2],
87 self.state[3],
88 self.state[4],
89 self.state[5],
90 self.state[6],
91 self.state[7],
92 ]
93 }
94
95 /// Returns the capacity portion of the state (state[8..12]).
96 pub fn capacity(&self) -> [T; CAPACITY_LEN] {
97 [self.state[8], self.state[9], self.state[10], self.state[11]]
98 }
99
100 /// Returns the digest portion of the state (state[0..4]).
101 pub fn digest(&self) -> [T; DIGEST_LEN] {
102 [self.state[0], self.state[1], self.state[2], self.state[3]]
103 }
104
105 /// Returns rate0 (state[0..4]).
106 pub fn rate0(&self) -> [T; DIGEST_LEN] {
107 [self.state[0], self.state[1], self.state[2], self.state[3]]
108 }
109
110 /// Returns rate1 (state[4..8]).
111 pub fn rate1(&self) -> [T; DIGEST_LEN] {
112 [self.state[4], self.state[5], self.state[6], self.state[7]]
113 }
114
115 /// Returns the 3 padding columns (mrupdate_id, is_boundary, direction_bit) that must
116 /// be zero on permutation rows.
117 pub fn unused_padding(&self) -> [T; 3] {
118 self._unused
119 }
120
121 /// Sets the 3 padding columns (mrupdate_id, is_boundary, direction_bit) to the given value.
122 pub fn set_unused_padding(&mut self, value: T) {
123 self._unused.fill(value);
124 }
125}
126
127// CONTROLLER COLUMNS
128// ================================================================================================
129
130/// Controller chiplet columns (19 columns), viewed from `chiplets[0..19]`.
131///
132/// Logical overlay for controller rows (`s_01 = 1`). `s0` distinguishes input rows
133/// (`s0 = 1`) from output/padding rows (`s0 = 0`). The physical layout mirrors
134/// [`PermutationCols`], but column names reflect the controller/permutation split.
135///
136/// `s_00` and `s_01` (= `ChipletCols::s_00` and `ChipletCols::s_01`) are consumed by the chiplet
137/// selector system and are NOT part of this overlay. Because the chiplet-level
138/// non-hasher selector is only ever a virtual expression (`1 - s_00 - s_01`) and is
139/// never a named column or struct field, there is no name collision with the
140/// controller-internal `s0` defined here.
141///
142/// The state holds a Poseidon2 sponge in `[RATE0, RATE1, CAPACITY]` layout.
143/// Helper methods `rate0()`, `rate1()`, `capacity()`, and `digest()` provide
144/// sub-views into the state array.
145///
146/// ## Layout
147///
148/// ```text
149/// | s0 s1 s2 | state[12] | extra cols |
150/// | | rate0[4] (= digest) | rate1[4] | capacity[4] | |
151/// | | h0..h3 | h4..h7 | h8..h11 | i mr bnd dir |
152/// ```
153#[repr(C)]
154#[derive(Clone, Debug)]
155pub struct ControllerCols<T> {
156 /// Hasher-internal sub-selector: `s0 = 1` on controller input rows, 0 on output/padding.
157 pub s0: T,
158 /// Operation sub-selector s1.
159 pub s1: T,
160 /// Operation sub-selector s2.
161 pub s2: T,
162 /// Poseidon2 state (12 field elements: 8 rate + 4 capacity).
163 pub state: [T; STATE_WIDTH],
164 /// Merkle tree node index.
165 pub node_index: T,
166 /// Domain separator for sibling table across MRUPDATE ops.
167 pub mrupdate_id: T,
168 /// 1 on boundary rows (first input or last output of each permutation).
169 pub is_boundary: T,
170 /// Direction bit for Merkle path verification.
171 pub direction_bit: T,
172}
173
174impl<T: Copy> ControllerCols<T> {
175 /// Returns the rate portion of the state (state[0..8]).
176 pub fn rate(&self) -> [T; RATE_LEN] {
177 [
178 self.state[0],
179 self.state[1],
180 self.state[2],
181 self.state[3],
182 self.state[4],
183 self.state[5],
184 self.state[6],
185 self.state[7],
186 ]
187 }
188
189 /// Returns the capacity portion of the state (state[8..12]).
190 pub fn capacity(&self) -> [T; CAPACITY_LEN] {
191 [self.state[8], self.state[9], self.state[10], self.state[11]]
192 }
193
194 /// Returns the digest portion of the state (state[0..4]).
195 pub fn digest(&self) -> [T; DIGEST_LEN] {
196 [self.state[0], self.state[1], self.state[2], self.state[3]]
197 }
198
199 /// Returns rate0 (state[0..4]).
200 pub fn rate0(&self) -> [T; DIGEST_LEN] {
201 [self.state[0], self.state[1], self.state[2], self.state[3]]
202 }
203
204 /// Returns rate1 (state[4..8]).
205 pub fn rate1(&self) -> [T; DIGEST_LEN] {
206 [self.state[4], self.state[5], self.state[6], self.state[7]]
207 }
208
209 /// Merkle-update new-path flag: `s0 * s1 * s2`.
210 ///
211 /// Active on controller input rows that insert the new Merkle path into the sibling
212 /// table (request/remove side of the sibling bus).
213 pub fn f_mu<E: PrimeCharacteristicRing>(&self) -> E
214 where
215 T: Into<E>,
216 {
217 self.s0.into() * self.s1.into() * self.s2.into()
218 }
219
220 /// Merkle-verify / old-path flag: `s0 * s1 * (1 - s2)`.
221 ///
222 /// Active on controller input rows that extract the old Merkle path from the sibling
223 /// table (response/add side of the sibling bus).
224 pub fn f_mv<E: PrimeCharacteristicRing>(&self) -> E
225 where
226 T: Into<E>,
227 {
228 self.s0.into() * self.s1.into() * (E::ONE - self.s2.into())
229 }
230}
231
232// BITWISE COLUMNS
233// ================================================================================================
234
235/// Bitwise chiplet columns (13 columns), viewed from `chiplets[1..14]`.
236///
237/// Bit decomposition columns (`a_bits`, `b_bits`) are in **little-endian** order:
238/// `value = bits[0] + 2*bits[1] + 4*bits[2] + 8*bits[3]`.
239#[repr(C)]
240#[derive(Clone, Debug)]
241pub struct BitwiseCols<T> {
242 /// Operation flag: 0 = AND, 1 = XOR.
243 pub op_flag: T,
244 /// Aggregated input a.
245 pub a: T,
246 /// Aggregated input b.
247 pub b: T,
248 /// 4-bit decomposition of a.
249 pub a_bits: [T; NUM_DECOMP_BITS],
250 /// 4-bit decomposition of b.
251 pub b_bits: [T; NUM_DECOMP_BITS],
252 /// Previous aggregated output.
253 pub prev_output: T,
254 /// Current aggregated output.
255 pub output: T,
256}
257
258// MEMORY COLUMNS
259// ================================================================================================
260
261/// Memory chiplet columns (15 columns), viewed from `chiplets[2..17]`.
262///
263/// When reading from a new word address (first access to a context/addr pair), the
264/// `values` are initialized to zero.
265#[repr(C)]
266#[derive(Clone, Debug)]
267pub struct MemoryCols<T> {
268 /// Read/write flag (0 = write, 1 = read).
269 pub is_read: T,
270 /// Element/word flag (0 = element, 1 = word).
271 pub is_word: T,
272 /// Memory context ID.
273 pub ctx: T,
274 /// Word address.
275 pub word_addr: T,
276 /// First bit of the address index within the word.
277 pub idx0: T,
278 /// Second bit of the address index within the word.
279 pub idx1: T,
280 /// Clock cycle of the memory access.
281 pub clk: T,
282 /// Values stored at this context/word/clock after the operation.
283 pub values: [T; WORD_SIZE],
284 /// Lower 16 bits of delta.
285 pub d0: T,
286 /// Upper 16 bits of delta.
287 pub d1: T,
288 /// Inverse of delta.
289 pub d_inv: T,
290 /// Flag: same context and same word address as previous operation (docs: `f_sca`).
291 pub is_same_ctx_and_addr: T,
292}
293
294// ACE COLUMNS
295// ================================================================================================
296
297/// ACE chiplet columns (16 columns), viewed from `chiplets[3..19]`.
298///
299/// The ACE (Arithmetic Circuit Evaluator) chiplet evaluates arithmetic circuits over
300/// quadratic extension field elements. Each circuit evaluation consists of two phases:
301///
302/// 1. **READ** (`s_block=0`): loads wire values from memory into the chiplet.
303/// 2. **EVAL** (`s_block=1`): evaluates arithmetic gates on loaded wire values.
304///
305/// The first 12 columns are common to both modes. The last 4 (`mode`) are overlaid
306/// and reinterpreted depending on `s_block`:
307///
308/// ```text
309/// mode idx | READ (s_block=0) | EVAL (s_block=1)
310/// ---------+------------------------+-------------------
311/// 0 | num_eval | id_2
312/// 1 | (unused) | v_2.0
313/// 2 | m_1 (wire-1 mult) | v_2.1
314/// 3 | m_0 (wire-0 mult) | m_0 (wire-0 mult)
315/// ```
316///
317/// Use `ace.read()` / `ace.eval()` for typed overlays of the mode columns.
318#[repr(C)]
319#[derive(Clone, Debug)]
320pub struct AceCols<T> {
321 /// Start-of-circuit flag (1 on the first row of a new circuit evaluation).
322 pub s_start: T,
323 /// Block selector: 0 = READ (memory loads), 1 = EVAL (gate evaluation).
324 pub s_block: T,
325 /// Memory context for the current circuit evaluation.
326 pub ctx: T,
327 /// Memory pointer from which to read the next two wire values or instruction.
328 pub ptr: T,
329 /// Clock cycle at which the memory read is performed.
330 pub clk: T,
331 /// Arithmetic operation selector (determines which gate to evaluate in EVAL mode).
332 pub eval_op: T,
333 /// ID of the first wire (output wire / left operand).
334 pub id_0: T,
335 /// Value of the first wire (quadratic extension field element).
336 pub v_0: QuadFeltExpr<T>,
337 /// ID of the second wire (first input / left operand).
338 pub id_1: T,
339 /// Value of the second wire (quadratic extension field element).
340 pub v_1: QuadFeltExpr<T>,
341 /// Mode-dependent columns (interpretation depends on `s_block`; see table above).
342 mode: [T; 4],
343}
344
345impl<T> AceCols<T> {
346 /// Returns a READ-mode overlay of the mode-dependent columns.
347 pub fn read(&self) -> &AceReadCols<T> {
348 self.mode.as_slice().borrow()
349 }
350
351 /// Returns an EVAL-mode overlay of the mode-dependent columns.
352 pub fn eval(&self) -> &AceEvalCols<T> {
353 self.mode.as_slice().borrow()
354 }
355
356 /// Returns a mutable READ-mode overlay of the mode-dependent columns.
357 pub fn read_mut(&mut self) -> &mut AceReadCols<T> {
358 self.mode.as_mut_slice().borrow_mut()
359 }
360
361 /// Returns a mutable EVAL-mode overlay of the mode-dependent columns.
362 pub fn eval_mut(&mut self) -> &mut AceEvalCols<T> {
363 self.mode.as_mut_slice().borrow_mut()
364 }
365}
366
367impl<T: Copy> AceCols<T> {
368 /// ACE read flag: `1 - s_block`.
369 ///
370 /// Active on ACE rows in READ mode (memory word reads for circuit inputs).
371 pub fn f_read<E: PrimeCharacteristicRing>(&self) -> E
372 where
373 T: Into<E>,
374 {
375 E::ONE - self.s_block.into()
376 }
377
378 /// ACE eval flag: `s_block`.
379 ///
380 /// Active on ACE rows in EVAL mode (circuit gate evaluation).
381 pub fn f_eval<E: PrimeCharacteristicRing>(&self) -> E
382 where
383 T: Into<E>,
384 {
385 self.s_block.into()
386 }
387}
388
389/// READ mode overlay for ACE mode-dependent columns (4 columns).
390///
391/// In READ mode, the chiplet loads wire values from memory. The multiplicity columns
392/// (`m_0`, `m_1`) track how many times each wire participates in circuit gates, used
393/// by the wiring bus to verify correct wire connections.
394#[repr(C)]
395#[derive(Clone, Debug)]
396pub struct AceReadCols<T> {
397 /// Number of EVAL rows that follow this READ block.
398 pub num_eval: T,
399 /// Unused column (padding for layout alignment with EVAL overlay).
400 pub unused: T,
401 /// Multiplicity of the second wire (wire 1).
402 pub m_1: T,
403 /// Multiplicity of the first wire (wire 0).
404 pub m_0: T,
405}
406
407/// EVAL mode overlay for ACE mode-dependent columns (4 columns).
408///
409/// In EVAL mode, the chiplet evaluates an arithmetic gate on three wires: two inputs
410/// (`id_1`, `id_2`) and one output (`id_0`). The third wire's ID and value occupy the
411/// same physical columns as `num_eval`/`unused`/`m_1` in READ mode.
412#[repr(C)]
413#[derive(Clone, Debug)]
414pub struct AceEvalCols<T> {
415 /// ID of the third wire (second input / right operand).
416 pub id_2: T,
417 /// Value of the third wire.
418 pub v_2: QuadFeltExpr<T>,
419 /// Multiplicity of the first wire (wire 0).
420 pub m_0: T,
421}
422
423// ACE COLUMN INDEX MAPS
424// ================================================================================================
425
426/// Compile-time index map for the top-level ACE chiplet columns (16 columns).
427#[allow(dead_code)]
428pub const ACE_COL_MAP: AceCols<usize> = {
429 assert!(size_of::<AceCols<u8>>() == 16);
430 unsafe { core::mem::transmute(indices_arr::<{ size_of::<AceCols<u8>>() }>()) }
431};
432
433/// Compile-time index map for the READ overlay (relative to `mode`).
434pub const ACE_READ_COL_MAP: AceReadCols<usize> = {
435 assert!(size_of::<AceReadCols<u8>>() == 4);
436 unsafe { core::mem::transmute(indices_arr::<{ size_of::<AceReadCols<u8>>() }>()) }
437};
438
439/// Compile-time index map for the EVAL overlay (relative to `mode`).
440pub const ACE_EVAL_COL_MAP: AceEvalCols<usize> = {
441 assert!(size_of::<AceEvalCols<u8>>() == 4);
442 unsafe { core::mem::transmute(indices_arr::<{ size_of::<AceEvalCols<u8>>() }>()) }
443};
444
445/// Offset of the `mode` array within the ACE chiplet columns.
446#[allow(dead_code)]
447pub const MODE_OFFSET: usize = ACE_COL_MAP.mode[0];
448
449const _: () = {
450 assert!(size_of::<AceCols<u8>>() == 16);
451 assert!(size_of::<AceReadCols<u8>>() == 4);
452 assert!(size_of::<AceEvalCols<u8>>() == 4);
453
454 // m_0 is at the same position in both overlays.
455 assert!(ACE_READ_COL_MAP.m_0 == ACE_EVAL_COL_MAP.m_0);
456
457 // READ-only and EVAL-only columns overlap at the expected positions.
458 assert!(ACE_READ_COL_MAP.num_eval == ACE_EVAL_COL_MAP.id_2);
459 assert!(ACE_READ_COL_MAP.m_1 == ACE_EVAL_COL_MAP.v_2.1);
460};
461
462// KERNEL ROM COLUMNS
463// ================================================================================================
464
465/// Kernel ROM chiplet columns (5 columns), viewed from `chiplets[4..9]`.
466#[repr(C)]
467#[derive(Clone, Debug)]
468pub struct KernelRomCols<T> {
469 /// Number of SYSCALLs to this procedure (CALL-label multiplicity).
470 pub multiplicity: T,
471 /// Kernel procedure root digest.
472 pub root: [T; WORD_SIZE],
473}
474
475// PERIODIC COLUMNS
476// ================================================================================================
477
478/// All chiplet periodic columns (20 columns).
479///
480/// Aggregates hasher (18 columns) and bitwise (2 columns) periodic values into a single
481/// typed view. Use `builder.periodic_values().borrow()` to obtain a `&PeriodicCols<_>`.
482#[derive(Clone, Copy)]
483#[repr(C)]
484pub struct PeriodicCols<T> {
485 /// Hasher periodic columns (cycle markers, step selectors, round constants).
486 pub hasher: HasherPeriodicCols<T>,
487 /// Bitwise periodic columns.
488 pub bitwise: BitwisePeriodicCols<T>,
489}
490
491/// Hasher chiplet periodic columns (16 columns, period = 16 rows).
492///
493/// Provides step-type selectors and Poseidon2 round constants for the hasher chiplet.
494/// The hasher operates on a 16-row cycle (15 transitions + 1 boundary row).
495///
496/// ## Layout
497///
498/// | Index | Name | Description |
499/// |-------|----------------|-------------|
500/// | 0 | is_init_ext | 1 on row 0 (init linear + first external round) |
501/// | 1 | is_ext | 1 on rows 1-3, 12-14 (single external round) |
502/// | 2 | is_packed_int | 1 on rows 4-10 (3 packed internal rounds) |
503/// | 3 | is_int_ext | 1 on row 11 (int22 + ext5 merged) |
504/// | 4-15 | ark[0..12] | Shared round constants |
505#[derive(Clone, Copy)]
506#[repr(C)]
507pub struct HasherPeriodicCols<T> {
508 /// 1 on row 0 (init linear + first external round).
509 pub is_init_ext: T,
510 /// 1 on rows 1-3, 12-14 (single external round).
511 pub is_ext: T,
512 /// 1 on rows 4-10 (3 packed internal rounds).
513 pub is_packed_int: T,
514 /// 1 on row 11 (int22 + ext5 merged).
515 pub is_int_ext: T,
516 /// Shared round constants (12 lanes). Carry external round constants on external
517 /// rows, and internal round constants in ark[0..2] on packed-internal rows.
518 pub ark: [T; STATE_WIDTH],
519}
520
521/// Bitwise chiplet periodic columns (2 columns, period = 8 rows).
522#[derive(Clone, Copy)]
523#[repr(C)]
524pub struct BitwisePeriodicCols<T> {
525 /// Marks first row of 8-row cycle: `[1, 0, 0, 0, 0, 0, 0, 0]`.
526 pub k_first: T,
527 /// Marks non-last rows of 8-row cycle: `[1, 1, 1, 1, 1, 1, 1, 0]`.
528 pub k_transition: T,
529}
530
531// PERIODIC COLUMN GENERATION
532// ================================================================================================
533
534impl Default for HasherPeriodicCols<Vec<Felt>> {
535 fn default() -> Self {
536 Self::new()
537 }
538}
539
540impl HasherPeriodicCols<Vec<Felt>> {
541 /// Generate periodic columns for the Poseidon2 hasher chiplet.
542 ///
543 /// All columns repeat every 16 rows, matching one permutation cycle.
544 ///
545 /// The 4 selector columns identify the row type. The 12 ark columns carry either
546 /// external round constants (on external rows) or internal round constants in
547 /// `ark[0..2]` (on packed-internal rows).
548 ///
549 /// ## 16-Row Schedule
550 ///
551 /// ```text
552 /// Row Transition Selector
553 /// 0 init + ext1 is_init_ext
554 /// 1-3 ext2-ext4 is_ext
555 /// 4-10 3x packed internal is_packed_int
556 /// 11 int22 + ext5 is_int_ext
557 /// 12-14 ext6-ext8 is_ext
558 /// 15 boundary (none)
559 /// ```
560 #[expect(
561 clippy::needless_range_loop,
562 reason = "index-based assignments mirror the documented 16-row schedule"
563 )]
564 pub fn new() -> Self {
565 // -------------------------------------------------------------------------
566 // Selectors
567 // -------------------------------------------------------------------------
568 let mut is_init_ext = vec![Felt::ZERO; HASH_CYCLE_LEN];
569 let mut is_ext = vec![Felt::ZERO; HASH_CYCLE_LEN];
570 let mut is_packed_int = vec![Felt::ZERO; HASH_CYCLE_LEN];
571 let mut is_int_ext = vec![Felt::ZERO; HASH_CYCLE_LEN];
572
573 is_init_ext[0] = Felt::ONE;
574
575 for r in [1, 2, 3, 12, 13, 14] {
576 is_ext[r] = Felt::ONE;
577 }
578
579 for r in 4..=10 {
580 is_packed_int[r] = Felt::ONE;
581 }
582
583 is_int_ext[11] = Felt::ONE;
584
585 // -------------------------------------------------------------------------
586 // Shared round constants (12 columns)
587 // -------------------------------------------------------------------------
588 // On external rows (0-3, 11-14): hold per-lane external round constants.
589 // On packed-internal rows (4-10): ark[0..2] hold 3 internal round constants,
590 // ark[3..12] are zero.
591 // On boundary (row 15): all zero.
592 let ark = core::array::from_fn(|lane| {
593 let mut col = vec![Felt::ZERO; HASH_CYCLE_LEN];
594
595 // Row 0 (init+ext1): first initial external round constants
596 col[0] = Hasher::ARK_EXT_INITIAL[0][lane];
597
598 // Rows 1-3 (ext2, ext3, ext4): remaining initial external round constants
599 for r in 1..=3 {
600 col[r] = Hasher::ARK_EXT_INITIAL[r][lane];
601 }
602
603 // Rows 4-10 (packed internal): internal constants in lanes 0-2 only
604 if lane < 3 {
605 for triple in 0..7_usize {
606 let row = 4 + triple;
607 let ark_idx = triple * 3 + lane;
608 col[row] = Hasher::ARK_INT[ark_idx];
609 }
610 }
611
612 // Row 11 (int22+ext5): terminal external round 0 constants
613 // (internal constant ARK_INT[21] is hardcoded in the constraint)
614 col[11] = Hasher::ARK_EXT_TERMINAL[0][lane];
615
616 // Rows 12-14 (ext6, ext7, ext8): remaining terminal external round constants
617 for r in 12..=14 {
618 col[r] = Hasher::ARK_EXT_TERMINAL[r - 11][lane];
619 }
620
621 col
622 });
623
624 Self {
625 is_init_ext,
626 is_ext,
627 is_packed_int,
628 is_int_ext,
629 ark,
630 }
631 }
632}
633
634impl Default for BitwisePeriodicCols<Vec<Felt>> {
635 fn default() -> Self {
636 Self::new()
637 }
638}
639
640impl BitwisePeriodicCols<Vec<Felt>> {
641 /// Generate periodic columns for the bitwise chiplet.
642 pub fn new() -> Self {
643 let k_first = vec![
644 Felt::ONE,
645 Felt::ZERO,
646 Felt::ZERO,
647 Felt::ZERO,
648 Felt::ZERO,
649 Felt::ZERO,
650 Felt::ZERO,
651 Felt::ZERO,
652 ];
653
654 let k_transition = vec![
655 Felt::ONE,
656 Felt::ONE,
657 Felt::ONE,
658 Felt::ONE,
659 Felt::ONE,
660 Felt::ONE,
661 Felt::ONE,
662 Felt::ZERO,
663 ];
664
665 Self { k_first, k_transition }
666 }
667}
668
669impl PeriodicCols<Vec<Felt>> {
670 /// Generate all chiplet periodic columns as a flat `Vec<Vec<Felt>>`.
671 pub fn periodic_columns() -> Vec<Vec<Felt>> {
672 let HasherPeriodicCols {
673 is_init_ext,
674 is_ext,
675 is_packed_int,
676 is_int_ext,
677 ark: [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11],
678 } = HasherPeriodicCols::new();
679
680 let BitwisePeriodicCols { k_first, k_transition } = BitwisePeriodicCols::new();
681
682 vec![
683 is_init_ext,
684 is_ext,
685 is_packed_int,
686 is_int_ext,
687 a0,
688 a1,
689 a2,
690 a3,
691 a4,
692 a5,
693 a6,
694 a7,
695 a8,
696 a9,
697 a10,
698 a11,
699 k_first,
700 k_transition,
701 ]
702 }
703}
704
705/// Total number of periodic columns across all chiplets.
706pub const NUM_PERIODIC_COLUMNS: usize = size_of::<PeriodicCols<u8>>();
707
708impl<T> Borrow<PeriodicCols<T>> for [T] {
709 fn borrow(&self) -> &PeriodicCols<T> {
710 debug_assert_eq!(self.len(), NUM_PERIODIC_COLUMNS);
711 let (prefix, cols, suffix) = unsafe { self.align_to::<PeriodicCols<T>>() };
712 debug_assert!(prefix.is_empty() && suffix.is_empty() && cols.len() == 1);
713 &cols[0]
714 }
715}
716
717const _: () = {
718 assert!(size_of::<PeriodicCols<u8>>() == 18);
719 assert!(size_of::<HasherPeriodicCols<u8>>() == 16);
720 assert!(size_of::<BitwisePeriodicCols<u8>>() == 2);
721
722 // PermutationCols and ControllerCols overlay chiplets[0..19] (19 columns,
723 // excluding s_00/s_01 which are consumed by the chiplet selector system).
724 assert!(size_of::<PermutationCols<u8>>() == 19);
725 assert!(size_of::<ControllerCols<u8>>() == 19);
726};
727
728// BORROW IMPLS
729// ================================================================================================
730//
731// Each chiplet column struct can be borrowed zero-copy from a `[T]` slice of the matching
732// length. Mirrors the `Borrow<CoreCols<T>>` / `Borrow<ChipletCols<T>>` impls on the parent
733// `crate::constraints::columns` module.
734
735impl_borrow_for_chiplet_cols!(PermutationCols);
736impl_borrow_for_chiplet_cols!(ControllerCols);
737impl_borrow_for_chiplet_cols!(BitwiseCols);
738impl_borrow_for_chiplet_cols!(MemoryCols);
739impl_borrow_for_chiplet_cols!(AceCols);
740impl_borrow_for_chiplet_cols!(AceReadCols);
741impl_borrow_for_chiplet_cols!(AceEvalCols);
742impl_borrow_for_chiplet_cols!(KernelRomCols);
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747
748 #[test]
749 fn periodic_columns_dimensions() {
750 let cols = PeriodicCols::periodic_columns();
751 assert_eq!(cols.len(), NUM_PERIODIC_COLUMNS);
752
753 let (hasher_cols, bitwise_cols) = cols.split_at(size_of::<HasherPeriodicCols<u8>>());
754 for col in hasher_cols {
755 assert_eq!(col.len(), HASH_CYCLE_LEN);
756 }
757 for col in bitwise_cols {
758 assert_eq!(col.len(), 8);
759 }
760 }
761
762 #[test]
763 fn hasher_step_selectors_are_exclusive() {
764 let h = HasherPeriodicCols::new();
765 for row in 0..HASH_CYCLE_LEN {
766 let init_ext = h.is_init_ext[row];
767 let ext = h.is_ext[row];
768 let packed_int = h.is_packed_int[row];
769 let int_ext = h.is_int_ext[row];
770
771 // Each selector is binary.
772 assert_eq!(init_ext * (init_ext - Felt::ONE), Felt::ZERO);
773 assert_eq!(ext * (ext - Felt::ONE), Felt::ZERO);
774 assert_eq!(packed_int * (packed_int - Felt::ONE), Felt::ZERO);
775 assert_eq!(int_ext * (int_ext - Felt::ONE), Felt::ZERO);
776
777 // At most one selector is active per row.
778 let sum = init_ext + ext + packed_int + int_ext;
779 assert!(sum == Felt::ZERO || sum == Felt::ONE, "row {row}: sum = {sum}");
780 }
781 }
782
783 #[test]
784 fn external_round_constants_correct() {
785 let h = HasherPeriodicCols::new();
786
787 // Row 0: ARK_EXT_INITIAL[0]
788 for lane in 0..STATE_WIDTH {
789 assert_eq!(h.ark[lane][0], Hasher::ARK_EXT_INITIAL[0][lane]);
790 }
791
792 // Rows 1-3: ARK_EXT_INITIAL[1..3]
793 for r in 1..=3 {
794 for lane in 0..STATE_WIDTH {
795 assert_eq!(h.ark[lane][r], Hasher::ARK_EXT_INITIAL[r][lane]);
796 }
797 }
798
799 // Row 11: ARK_EXT_TERMINAL[0]
800 for lane in 0..STATE_WIDTH {
801 assert_eq!(h.ark[lane][11], Hasher::ARK_EXT_TERMINAL[0][lane]);
802 }
803
804 // Rows 12-14: ARK_EXT_TERMINAL[1..3]
805 for r in 12..=14 {
806 for lane in 0..STATE_WIDTH {
807 assert_eq!(h.ark[lane][r], Hasher::ARK_EXT_TERMINAL[r - 11][lane]);
808 }
809 }
810 }
811
812 #[test]
813 fn internal_round_constants_correct() {
814 let h = HasherPeriodicCols::new();
815
816 // Rows 4-10: packed internal round constants in ark[0..2]
817 for triple in 0..7_usize {
818 let row = 4 + triple;
819 for k in 0..3 {
820 let ark_idx = triple * 3 + k;
821 assert_eq!(
822 h.ark[k][row],
823 Hasher::ARK_INT[ark_idx],
824 "mismatch at row {row}, int constant {k} (ARK_INT[{ark_idx}])"
825 );
826 }
827 // ark[3..12] must be zero on packed-internal rows
828 for lane in 3..STATE_WIDTH {
829 assert_eq!(
830 h.ark[lane][row],
831 Felt::ZERO,
832 "ark[{lane}] nonzero at packed-int row {row}"
833 );
834 }
835 }
836 }
837
838 #[test]
839 fn boundary_row_all_zero() {
840 let h = HasherPeriodicCols::new();
841 for (lane, col) in h.ark.iter().enumerate() {
842 assert_eq!(col[15], Felt::ZERO, "ark column {lane} nonzero at row 15");
843 }
844 }
845}