pub mod columns;
use miden_crypto::stark::air::AirBuilder;
use p3_field::Dup;
use crate::{
CoreCols, Felt, MidenAirBuilder,
constraints::{
constants::{F_1, F_128},
decoder::columns::DecoderCols,
op_flags::OpFlags,
utils::{BoolNot, horner_eval_bits},
},
trace::chiplets::hasher::CONTROLLER_ROWS_PER_PERM_FELT,
};
pub fn enforce_main<AB>(
builder: &mut AB,
local: &CoreCols<AB::Var>,
next: &CoreCols<AB::Var>,
op_flags: &OpFlags<AB::Expr>,
) where
AB: MidenAirBuilder,
{
let DecoderCols {
addr,
op_bits,
hasher_state,
in_span,
group_count,
op_index,
batch_flags,
extra,
} = local.decoder;
let [b0, b1, b2, b3, b4, b5, b6] = op_bits;
let [bc0, bc1, bc2] = batch_flags;
let [e0, e1] = extra;
let h0 = hasher_state[0];
let end_flags = local.decoder.end_block_flags();
let is_loop_body = end_flags.is_loop_body;
let is_loop = end_flags.is_loop;
let DecoderCols {
addr: addr_next,
op_bits: op_bits_next,
hasher_state: hasher_state_next,
in_span: in_span_next,
group_count: group_count_next,
op_index: op_index_next,
..
} = next.decoder;
let h0_next = hasher_state_next[0];
let delta_group_count: AB::Expr = group_count - group_count_next;
let is_push = op_flags.push();
builder.when_first_row().assert_zero(in_span);
builder.assert_bool(in_span);
let enters_span = op_flags.span() + op_flags.respan();
let exits_span_next = op_flags.end_next() + op_flags.respan_next();
let continues_span = in_span * exits_span_next.not();
builder.when_transition().assert_eq(in_span_next, enters_span + continues_span);
builder.assert_bools(op_bits);
let e0_expected = b6 * b5.into().not() * b4;
builder.assert_eq(e0, e0_expected);
let e1_expected = b6 * b5;
builder.assert_eq(e1, e1_expected);
let prefix_100_or_11 = b6 - e0;
let prefix_101_with_low_bits_111 = e0 * b3 * b2 * b1;
builder.when(prefix_100_or_11 + prefix_101_with_low_bits_111).assert_zero(b0);
builder.when(e1).assert_zero(b1);
let branch_condition = local.stack.get(0);
builder.when(op_flags.split()).assert_bool(branch_condition);
{
let builder = &mut builder.when(op_flags.dyn_op());
let hasher_zeros = [hasher_state[4], hasher_state[5], hasher_state[6], hasher_state[7]];
builder.assert_zeros(hasher_zeros)
}
{
let overflow = op_flags.overflow();
let builder = &mut builder.when(op_flags.dyncall());
builder.assert_eq(hasher_state[4], local.stack.b0 - overflow.dup());
builder.when(overflow.not()).assert_zero(hasher_state[5]);
}
builder.when(op_flags.repeat()).assert_one(local.stack.get(0));
let loop_condition = local.stack.get(0);
builder.when(op_flags.end()).when(is_loop).assert_zero(loop_condition);
{
let restores_caller_frame = end_flags.restores_caller_frame;
let builder = &mut builder.when(op_flags.end());
builder.assert_bool(is_loop);
builder.assert_bool(restores_caller_frame);
builder.assert_zero(hasher_state[7]);
builder.assert_zero(is_loop * restores_caller_frame);
}
builder
.when_transition()
.when(op_flags.repeat_next())
.assert_one(op_flags.end() * is_loop_body);
builder
.when_transition()
.when(op_flags.loop_op())
.assert_zero(op_flags.end_next());
builder.when_first_row().assert_zero(op_flags.repeat());
builder.when_transition().when(op_flags.halt()).assert_one(op_flags.halt_next());
{
let gate = builder.is_transition() * in_span;
let builder = &mut builder.when(gate);
builder.assert_bool(delta_group_count.clone());
builder.when(delta_group_count.clone()).when(is_push.not()).assert_zero(h0);
}
builder
.when(op_flags.span() + op_flags.respan() + is_push.clone())
.assert_one(delta_group_count.clone());
builder
.when_transition()
.when(delta_group_count.clone())
.assert_zero(op_flags.end_next() + op_flags.respan_next());
builder.when(op_flags.end()).assert_zero(group_count);
{
let f_span = op_flags.span();
let f_respan = op_flags.respan();
let same_group_count: AB::Expr = in_span * in_span_next * delta_group_count.not();
let op_next: AB::Expr = horner_eval_bits(&op_bits_next);
let h0_shift = h0 - h0_next * F_128 - op_next;
let h0_active = f_span + f_respan + is_push.clone() + same_group_count;
builder.when_transition().when(h0_active).assert_zero(h0_shift);
let end_or_respan_next = op_flags.end_next() + op_flags.respan_next();
builder.when_transition().when(in_span).when(end_or_respan_next).assert_zero(h0);
}
{
let new_group: AB::Expr = delta_group_count - is_push;
builder.when(op_flags.span() + op_flags.respan()).assert_zero(op_index_next);
builder
.when_transition()
.when(in_span)
.when(new_group.clone())
.assert_zero(op_index_next);
builder
.when_transition()
.when(in_span)
.when(in_span_next)
.when(new_group.not())
.assert_eq(op_index_next, op_index + F_1);
let mut range_check: AB::Expr = op_index.into();
for i in 1..=8u64 {
range_check *= op_index - Felt::new_unchecked(i);
}
builder.assert_zero(range_check);
}
{
builder.assert_bools([bc0, bc1, bc2]);
let groups_8 = bc0;
let not_bc0 = bc0.into().not();
let groups_4 = not_bc0.clone() * bc1 * bc2.into().not();
let groups_2 = not_bc0.clone() * bc1.into().not() * bc2;
let groups_1 = not_bc0 * bc1 * bc2;
let groups_1_or_2 = groups_1.clone() + groups_2;
let groups_1_or_2_or_4 = groups_1_or_2.clone() + groups_4;
let span_or_respan = op_flags.span() + op_flags.respan();
builder.assert_eq(span_or_respan.clone(), groups_1_or_2_or_4.clone() + groups_8);
builder.when(span_or_respan.not()).assert_zero(bc0 + bc1 + bc2);
{
let builder = &mut builder.when(groups_1_or_2_or_4);
for i in 0..4 {
builder.assert_zero(hasher_state[4 + i]);
}
}
{
let builder = &mut builder.when(groups_1_or_2);
for i in 0..2 {
builder.assert_zero(hasher_state[2 + i]);
}
}
builder.when(groups_1).assert_zero(hasher_state[1]);
}
builder
.when_transition()
.when(in_span + op_flags.repeat())
.assert_eq(addr_next, addr);
builder
.when(op_flags.respan())
.assert_eq(addr_next, addr + CONTROLLER_ROWS_PER_PERM_FELT);
builder.when(op_flags.halt()).assert_zero(addr);
builder.assert_one(in_span + op_flags.control_flow());
builder.when_last_row().assert_one(op_flags.halt());
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use miden_core::{
Felt, ONE,
field::{Field, PrimeCharacteristicRing, QuadFelt},
operations::opcodes,
};
use super::{CONTROLLER_ROWS_PER_PERM_FELT, enforce_main};
use crate::{
CoreCols,
constraints::{
op_flags::{OpFlags, generate_test_row},
stack::test_utils::ConstraintEvalBuilder,
},
};
fn eval_decoder(local: &CoreCols<Felt>, next: &CoreCols<Felt>) -> Vec<QuadFelt> {
let op_flags = OpFlags::new(&local.decoder, &local.stack, &next.decoder);
let mut builder = ConstraintEvalBuilder::new();
enforce_main(&mut builder, local, next, &op_flags);
builder.evaluations
}
fn decoder_accepts(local: &CoreCols<Felt>, next: &CoreCols<Felt>) -> bool {
eval_decoder(local, next).iter().all(|v| *v == QuadFelt::ZERO)
}
#[test]
fn decoder_rejects_exit_from_span_without_end_or_respan() {
let mut local = generate_test_row(opcodes::EQ.into());
local.decoder.in_span = ONE;
local.decoder.addr = ONE;
local.decoder.group_count = Felt::from_u8(3);
local.decoder.hasher_state[0] = Felt::from_u8(opcodes::ASSERT);
let mut next = generate_test_row(opcodes::SPAN.into());
next.decoder.addr = ONE;
next.decoder.group_count = Felt::from_u8(3);
assert!(
!decoder_accepts(&local, &next),
"leaving a span without END or RESPAN must violate the decoder AIR",
);
}
#[test]
fn decoder_rejects_entry_into_span_without_span_or_respan() {
let mut local = generate_test_row(opcodes::REPEAT.into());
local.stack.top[0] = ONE;
local.decoder.hasher_state[4] = ONE;
let mut next = generate_test_row(opcodes::NOOP.into());
next.decoder.in_span = ONE;
next.decoder.group_count = Felt::from_u8(3);
assert!(
!decoder_accepts(&local, &next),
"entering a span without SPAN or RESPAN must violate the decoder AIR",
);
}
fn honest_in_span_pair() -> (CoreCols<Felt>, CoreCols<Felt>) {
let mut local = generate_test_row(opcodes::NOOP.into());
let mut next = generate_test_row(opcodes::NOOP.into());
local.decoder.addr = Felt::new_unchecked(17);
next.decoder.addr = local.decoder.addr;
local.decoder.in_span = Felt::ONE;
next.decoder.in_span = Felt::ONE;
local.decoder.group_count = Felt::ONE;
next.decoder.group_count = Felt::ONE;
local.decoder.op_index = Felt::ZERO;
next.decoder.op_index = Felt::ONE;
(local, next)
}
fn span_row_with_single_group() -> CoreCols<Felt> {
let mut row = generate_test_row(opcodes::SPAN.into());
row.decoder.addr = Felt::new_unchecked(17);
row.decoder.group_count = Felt::new_unchecked(2);
row.decoder.batch_flags[1] = Felt::ONE;
row.decoder.batch_flags[2] = Felt::ONE;
row
}
fn repeat_row_with_true_condition() -> CoreCols<Felt> {
let mut row = generate_test_row(opcodes::REPEAT.into());
row.stack.top[0] = Felt::ONE;
row
}
fn set_opcode(row: &mut CoreCols<Felt>, opcode: usize) {
let opcode_row = generate_test_row(opcode);
row.decoder.op_bits = opcode_row.decoder.op_bits;
row.decoder.extra = opcode_row.decoder.extra;
}
#[test]
fn permitted_decoder_adjacency_pairs_are_accepted() {
let (in_span_local, in_span_next) = honest_in_span_pair();
assert!(
decoder_accepts(&in_span_local, &in_span_next),
"an ordinary in-span transition must be accepted"
);
let span = span_row_with_single_group();
let mut first_op = generate_test_row(opcodes::NOOP.into());
first_op.decoder.addr = span.decoder.addr;
first_op.decoder.in_span = Felt::ONE;
first_op.decoder.group_count = Felt::ONE;
assert!(decoder_accepts(&span, &first_op), "SPAN must be allowed to enter a basic block");
let mut respan = span_row_with_single_group();
set_opcode(&mut respan, opcodes::RESPAN.into());
first_op.decoder.addr = respan.decoder.addr + CONTROLLER_ROWS_PER_PERM_FELT;
assert!(
decoder_accepts(&respan, &first_op),
"RESPAN must be allowed to enter the next batch"
);
for exit_opcode in [opcodes::END, opcodes::RESPAN] {
let mut in_span = generate_test_row(opcodes::NOOP.into());
let mut exit = generate_test_row(exit_opcode.into());
in_span.decoder.addr = Felt::new_unchecked(17);
exit.decoder.addr = in_span.decoder.addr;
in_span.decoder.in_span = Felt::ONE;
assert!(
decoder_accepts(&in_span, &exit),
"an in-span row must be allowed to exit via opcode {exit_opcode}"
);
}
let mut end = generate_test_row(opcodes::END.into());
let mut repeat = repeat_row_with_true_condition();
end.stack.top[0] = Felt::ONE;
end.decoder.hasher_state[4] = Felt::ONE;
repeat.decoder.hasher_state[0] = Felt::new_unchecked(7);
assert!(
decoder_accepts(&end, &repeat),
"a loop-body END may precede REPEAT without copying its helper lanes"
);
end.stack.top[0] = Felt::ZERO;
end.decoder.hasher_state[5] = Felt::ONE;
assert!(
decoder_accepts(&end, &repeat),
"the completed loop body may itself be a nested LOOP"
);
}
#[test]
fn malformed_decoder_adjacency_pairs_are_rejected() {
let (mut in_span, _) = honest_in_span_pair();
let mut repeat = repeat_row_with_true_condition();
repeat.decoder.addr = in_span.decoder.addr;
assert!(
!decoder_accepts(&in_span, &repeat),
"an in-span row cannot exit directly to REPEAT"
);
let loop_row = generate_test_row(opcodes::LOOP.into());
assert!(!decoder_accepts(&loop_row, &repeat), "REPEAT's predecessor must be END");
let non_loop_body_end = generate_test_row(opcodes::END.into());
assert!(
!decoder_accepts(&non_loop_body_end, &repeat_row_with_true_condition()),
"REPEAT must follow an END marked as a loop body"
);
let end = generate_test_row(opcodes::END.into());
let mut illegal_entry = generate_test_row(opcodes::NOOP.into());
illegal_entry.decoder.in_span = Felt::ONE;
assert!(
!decoder_accepts(&end, &illegal_entry),
"an in-span successor must be entered by SPAN/RESPAN or another in-span row"
);
in_span.decoder.in_span = Felt::ONE;
let split = generate_test_row(opcodes::SPLIT.into());
assert!(
!decoder_accepts(&in_span, &split),
"a basic-block row cannot exit via arbitrary control flow"
);
}
#[test]
fn loop_cannot_jump_directly_to_its_end() {
let loop_row = generate_test_row(opcodes::LOOP.into());
let span = span_row_with_single_group();
assert!(decoder_accepts(&loop_row, &span), "LOOP must be allowed to enter its body");
let end = generate_test_row(opcodes::END.into());
assert!(
!decoder_accepts(&loop_row, &end),
"LOOP must not skip its do-while body by jumping directly to END"
);
}
#[test]
fn opcode_prefix_boundaries() {
let (mut local, next) = honest_in_span_pair();
for opcode in [opcodes::U32ADD, opcodes::LOGDEFERRED, opcodes::MRUPDATE] {
set_opcode(&mut local, opcode.into());
assert!(decoder_accepts(&local, &next), "opcode {opcode} must be accepted");
}
for opcode in [65, 95, 97, 98] {
set_opcode(&mut local, opcode);
assert!(!decoder_accepts(&local, &next), "unused opcode {opcode} must be rejected");
}
}
#[test]
fn block_stack_entry_kind_selectors_must_encode_a_valid_kind() {
let accepts = |is_loop: Felt, restores_caller_frame: Felt, h7: Felt| {
let mut local = generate_test_row(opcodes::END.into());
local.decoder.hasher_state[5] = is_loop;
local.decoder.hasher_state[6] = restores_caller_frame;
local.decoder.hasher_state[7] = h7;
let next = generate_test_row(0);
eval_decoder(&local, &next).iter().all(|v| *v == QuadFelt::ZERO)
};
for (is_loop, restores_caller_frame, h7) in [
(Felt::ZERO, Felt::ZERO, Felt::ZERO),
(Felt::ONE, Felt::ZERO, Felt::ZERO),
(Felt::ZERO, Felt::ONE, Felt::ZERO),
] {
assert!(
accepts(is_loop, restores_caller_frame, h7),
"a valid END entry kind must be permitted"
);
}
assert!(
!accepts(Felt::new_unchecked(3), Felt::ZERO, Felt::ZERO),
"a non-boolean LOOP selector must be rejected"
);
assert!(
!accepts(Felt::ONE, Felt::ONE, Felt::ZERO),
"an END cannot be both a LOOP continuation and a caller frame"
);
assert!(
!accepts(Felt::ZERO, Felt::ONE, Felt::ONE),
"the unused h7 lane must be zero on an END row"
);
assert!(
!accepts(Felt::ZERO, Felt::new_unchecked(3), Felt::ZERO),
"a non-boolean caller-frame restoration selector must be rejected"
);
}
#[test]
fn helper_lanes_are_unconstrained_off_end_rows() {
let mut local = generate_test_row(opcodes::NOOP.into());
local.decoder.hasher_state[5] = Felt::new_unchecked(3);
local.decoder.hasher_state[6] = Felt::new_unchecked(5);
local.decoder.hasher_state[7] = Felt::new_unchecked(7);
let next = generate_test_row(0);
let with_values = eval_decoder(&local, &next);
let mut cleared = local;
cleared.decoder.hasher_state[5] = Felt::ZERO;
cleared.decoder.hasher_state[6] = Felt::ZERO;
cleared.decoder.hasher_state[7] = Felt::ZERO;
let with_zeros = eval_decoder(&cleared, &next);
assert_eq!(
with_values, with_zeros,
"the END-gated booleanity constraint must not reach helper lanes on a NOOP row"
);
}
#[test]
fn dyncall_saved_frame_cells_follow_the_post_pop_state() {
let accepts = |depth: Felt, overflow_helper: Felt, h4: Felt, h5: Felt| {
let mut local = generate_test_row(opcodes::DYNCALL.into());
local.stack.b0 = depth;
local.stack.h0 = overflow_helper;
local.decoder.hasher_state[4] = h4;
local.decoder.hasher_state[5] = h5;
let next = generate_test_row(0);
eval_decoder(&local, &next).iter().all(|v| *v == QuadFelt::ZERO)
};
assert!(accepts(Felt::from_u8(16), Felt::ZERO, Felt::from_u8(16), Felt::ZERO));
assert!(!accepts(Felt::from_u8(16), Felt::ZERO, Felt::from_u8(15), Felt::ZERO));
assert!(!accepts(Felt::from_u8(16), Felt::ZERO, Felt::from_u8(16), Felt::ONE));
let inverse_two = Felt::from_u8(2).inverse();
let relation_owned_h5 = Felt::new_unchecked(123);
assert!(accepts(Felt::from_u8(18), inverse_two, Felt::from_u8(17), relation_owned_h5));
assert!(!accepts(Felt::from_u8(18), inverse_two, Felt::from_u8(18), relation_owned_h5));
}
}