use alloc::{boxed::Box, vec::Vec};
use core::{
borrow::{Borrow, BorrowMut},
mem::size_of,
};
use miden_air::{
BaseAir, ChipletCols, ControllerCols, LiftedAir, MidenAir, MidenMultiAir, ProverStatement,
StarkConfig, Statement, config, debug,
logup::{BusId, HasherPermLinkMsg, MIDEN_MAX_MESSAGE_WIDTH},
lookup::{
Challenges, LookupMessage, accumulate, build_lookup_fractions,
debug::collect_column_oracle_folds,
},
trace::CHIPLET_CONTROLLER_OFFSET,
};
use miden_core::{
field::{Field, QuadFelt},
utils::{Matrix, RowMajorMatrix},
};
use super::{ExecutionTrace, Felt, build_trace_from_ops, rand_array};
use crate::operation::Operation;
const CONTROLLER_OFFSET: usize = CHIPLET_CONTROLLER_OFFSET;
const CONTROLLER_WIDTH: usize = size_of::<ControllerCols<u8>>();
static PANIC_HOOK_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn tiny_span() -> Vec<Operation> {
vec![
Operation::Pad,
Operation::Pad,
Operation::Add,
Operation::Pad,
Operation::Mul,
Operation::Drop,
]
}
fn assert_prover_matches_oracle(
label: &str,
aux: &RowMajorMatrix<QuadFelt>,
sigma_prime: QuadFelt,
oracle_folds: &[Vec<(QuadFelt, QuadFelt)>],
aux_width: usize,
) {
let num_rows = oracle_folds.len();
assert_eq!(aux.width(), aux_width, "{label}: aux width mismatch");
assert_eq!(aux.height(), num_rows, "{label}: aux height mismatch");
let aux_values = &aux.values;
for (r, row_folds) in oracle_folds.iter().enumerate() {
assert_eq!(row_folds.len(), aux_width, "{label} row {r}: fold width mismatch");
let per_row_values: Vec<QuadFelt> = row_folds
.iter()
.enumerate()
.map(|(col, &(v_col, u_col))| {
let u_inv = u_col.try_inverse().unwrap_or_else(|| {
panic!(
"{label} row {r} col {col}: oracle U_col is zero — bus has a \
zero-denominator product, indicating a bug in the emitter or \
message encoding",
)
});
v_col * u_inv
})
.collect();
let expected_delta: QuadFelt = per_row_values.iter().copied().sum();
let next = (r + 1) % num_rows;
let actual_delta = aux_values[next * aux_width] - aux_values[r * aux_width] + sigma_prime;
assert_eq!(
actual_delta, expected_delta,
"{label} row {r} col 0 (accumulator): prover vs constraint path mismatch",
);
for col in 1..aux_width {
let actual_value = aux_values[r * aux_width + col];
assert_eq!(
actual_value, per_row_values[col],
"{label} row {r} col {col} (fraction): prover vs constraint path mismatch",
);
}
}
}
fn perm_link_fractions(
chip_matrix: &RowMajorMatrix<Felt>,
poseidon2_matrix: &RowMajorMatrix<Felt>,
challenges: &Challenges<QuadFelt>,
) -> Vec<(Felt, QuadFelt)> {
let chip_periodic = MidenAir::Chiplets.periodic_columns();
let poseidon2_periodic = MidenAir::Poseidon2Permutation.periodic_columns();
let chip_fractions =
build_lookup_fractions(&MidenAir::Chiplets, chip_matrix, &chip_periodic, challenges);
let poseidon2_fractions = build_lookup_fractions(
&MidenAir::Poseidon2Permutation,
poseidon2_matrix,
&poseidon2_periodic,
challenges,
);
chip_fractions
.fractions()
.iter()
.chain(poseidon2_fractions.fractions())
.copied()
.collect()
}
fn net_multiplicity(fractions: &[(Felt, QuadFelt)], denom: QuadFelt) -> Felt {
let mut net = Felt::ZERO;
for &(multiplicity, encoded) in fractions {
if encoded == denom {
net += multiplicity;
}
}
net
}
fn chiplet_row(matrix: &RowMajorMatrix<Felt>, row: usize) -> &ChipletCols<Felt> {
let width = matrix.width();
matrix.values[row * width..(row + 1) * width].borrow()
}
fn controller_row(matrix: &RowMajorMatrix<Felt>, row: usize) -> &ControllerCols<Felt> {
chiplet_row(matrix, row).controller()
}
fn controller_row_mut(matrix: &mut RowMajorMatrix<Felt>, row: usize) -> &mut ControllerCols<Felt> {
let width = matrix.width();
let start = row * width + CONTROLLER_OFFSET;
matrix.values[start..start + CONTROLLER_WIDTH].borrow_mut()
}
fn assert_trace_constraints_reject(
trace: &ExecutionTrace,
core_matrix: RowMajorMatrix<Felt>,
chip_matrix: RowMajorMatrix<Felt>,
poseidon2_matrix: RowMajorMatrix<Felt>,
) {
let (public_values, aux_inputs) = trace.public_inputs().to_air_inputs();
let statement =
Statement::<Felt, QuadFelt, _>::new(MidenMultiAir::new(), public_values, aux_inputs)
.expect("valid statement inputs");
let prover_statement =
ProverStatement::new(statement, vec![core_matrix, chip_matrix, poseidon2_matrix])
.expect("valid trace shapes");
let config = config::poseidon2_config(config::pcs_params(), config::RELATION_DIGEST);
let _guard = PANIC_HOOK_LOCK.lock().expect("panic hook lock poisoned");
let panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
debug::check_constraints(&prover_statement, config.challenger());
}));
std::panic::set_hook(panic_hook);
assert!(result.is_err(), "mutated trace should violate AIR constraints");
}
#[test]
fn build_lookup_fractions_matches_constraint_path_oracle() {
let trace = build_trace_from_ops(tiny_span(), &[]);
let (core_matrix, chip_matrix, poseidon2_matrix) = trace.main_trace().to_air_matrices();
let public_vals = trace.to_public_values();
let chip_periodic = MidenAir::Chiplets.periodic_columns();
let poseidon2_periodic = MidenAir::Poseidon2Permutation.periodic_columns();
let raw = rand_array::<Felt, 4>();
let alpha = QuadFelt::new([raw[0], raw[1]]);
let beta = QuadFelt::new([raw[2], raw[3]]);
let challenges =
Challenges::<QuadFelt>::new(alpha, beta, MIDEN_MAX_MESSAGE_WIDTH, BusId::COUNT);
let core_fractions = build_lookup_fractions(&MidenAir::Core, &core_matrix, &[], &challenges);
assert!(
!core_fractions.fractions().is_empty(),
"no Core fractions collected — trace is degenerate or emitters are broken",
);
let (core_aux, core_sigma_prime) = accumulate(&core_fractions);
let core_folds =
collect_column_oracle_folds(&MidenAir::Core, &core_matrix, &[], &public_vals, &challenges);
assert_prover_matches_oracle(
"Core",
&core_aux,
core_sigma_prime,
&core_folds,
LiftedAir::<Felt, QuadFelt>::aux_width(&MidenAir::Core),
);
let chip_fractions =
build_lookup_fractions(&MidenAir::Chiplets, &chip_matrix, &chip_periodic, &challenges);
assert!(
!chip_fractions.fractions().is_empty(),
"no Chiplets fractions collected — trace is degenerate or emitters are broken",
);
let (chip_aux, chip_sigma_prime) = accumulate(&chip_fractions);
let chip_folds = collect_column_oracle_folds(
&MidenAir::Chiplets,
&chip_matrix,
&chip_periodic,
&public_vals,
&challenges,
);
assert_prover_matches_oracle(
"Chiplets",
&chip_aux,
chip_sigma_prime,
&chip_folds,
LiftedAir::<Felt, QuadFelt>::aux_width(&MidenAir::Chiplets),
);
let poseidon2_fractions = build_lookup_fractions(
&MidenAir::Poseidon2Permutation,
&poseidon2_matrix,
&poseidon2_periodic,
&challenges,
);
assert!(
!poseidon2_fractions.fractions().is_empty(),
"no Poseidon2 fractions collected; trace is degenerate or emitters are broken",
);
let (poseidon2_aux, poseidon2_sigma_prime) = accumulate(&poseidon2_fractions);
let poseidon2_folds = collect_column_oracle_folds(
&MidenAir::Poseidon2Permutation,
&poseidon2_matrix,
&poseidon2_periodic,
&public_vals,
&challenges,
);
assert_prover_matches_oracle(
"Poseidon2Permutation",
&poseidon2_aux,
poseidon2_sigma_prime,
&poseidon2_folds,
LiftedAir::<Felt, QuadFelt>::aux_width(&MidenAir::Poseidon2Permutation),
);
}
#[test]
fn perm_link_rejects_swapped_controller_outputs() {
let trace =
build_trace_from_ops(vec![Operation::HPerm, Operation::HPerm], &[8, 7, 6, 5, 4, 3, 2, 1]);
let (core_matrix, chip_matrix, poseidon2_matrix) = trace.main_trace().to_air_matrices();
let output_rows: Vec<_> = (0..chip_matrix.height())
.filter(|&row| {
let chiplet = chiplet_row(&chip_matrix, row);
let ctrl = chiplet.controller();
chiplet.chiplet_selectors()[0] == Felt::ZERO
&& ctrl.s0 == Felt::ZERO
&& ctrl.s1 == Felt::ZERO
&& ctrl.s2 == Felt::ONE
})
.take(2)
.collect();
assert_eq!(output_rows.len(), 2, "expected two controller output rows");
let ctrl_a = controller_row(&chip_matrix, output_rows[0]);
let ctrl_b = controller_row(&chip_matrix, output_rows[1]);
let state_a = ctrl_a.state;
let state_b = ctrl_b.state;
assert_ne!(state_a, state_b, "test needs two distinct permutation outputs");
let perm_id_a = ctrl_a.perm_id;
let perm_id_b = ctrl_b.perm_id;
assert_ne!(perm_id_a, perm_id_b, "test needs two distinct permutation ids");
let challenges = Challenges::<QuadFelt>::new(
QuadFelt::new([Felt::new_unchecked(7), Felt::ZERO]),
QuadFelt::new([Felt::new_unchecked(11), Felt::ZERO]),
MIDEN_MAX_MESSAGE_WIDTH,
BusId::COUNT,
);
let honest_fractions = perm_link_fractions(&chip_matrix, &poseidon2_matrix, &challenges);
for msg in [
HasherPermLinkMsg::Output { perm_id: perm_id_a, state: state_a },
HasherPermLinkMsg::Output { perm_id: perm_id_b, state: state_b },
] {
assert_eq!(
net_multiplicity(&honest_fractions, msg.encode(&challenges)),
Felt::ZERO,
"honest controller output link is balanced"
);
}
let mut state_swapped_chip_matrix = chip_matrix.clone();
controller_row_mut(&mut state_swapped_chip_matrix, output_rows[0]).state = state_b;
controller_row_mut(&mut state_swapped_chip_matrix, output_rows[1]).state = state_a;
let swapped_fractions =
perm_link_fractions(&state_swapped_chip_matrix, &poseidon2_matrix, &challenges);
for msg in [
HasherPermLinkMsg::Output { perm_id: perm_id_a, state: state_b },
HasherPermLinkMsg::Output { perm_id: perm_id_b, state: state_a },
] {
assert_eq!(
net_multiplicity(&swapped_fractions, msg.encode(&challenges)),
Felt::ONE,
"swapped controller output leaves an unmatched perm-link addition"
);
}
let mut tuple_swapped_chip_matrix = chip_matrix;
{
let row = controller_row_mut(&mut tuple_swapped_chip_matrix, output_rows[0]);
row.state = state_b;
row.perm_id = perm_id_b;
}
{
let row = controller_row_mut(&mut tuple_swapped_chip_matrix, output_rows[1]);
row.state = state_a;
row.perm_id = perm_id_a;
}
let balanced_fractions =
perm_link_fractions(&tuple_swapped_chip_matrix, &poseidon2_matrix, &challenges);
for msg in [
HasherPermLinkMsg::Output { perm_id: perm_id_a, state: state_a },
HasherPermLinkMsg::Output { perm_id: perm_id_b, state: state_b },
] {
assert_eq!(
net_multiplicity(&balanced_fractions, msg.encode(&challenges)),
Felt::ZERO,
"swapping output tuples keeps the perm-link bus balanced"
);
}
assert_trace_constraints_reject(
&trace,
core_matrix,
tuple_swapped_chip_matrix,
poseidon2_matrix,
);
}