use alloc::vec;
use miden_air::trace::RowIndex;
use miden_core::{
Felt,
operations::opcodes,
program::StackOutputs,
utils::{Matrix, RowMajorMatrix},
};
use miden_processor::{DefaultHost, FastProcessor, StackInputs};
use crate::{
Prover,
repro_harness::{ReproTrace, core_row_mut},
};
const RANGE_WIDTH: usize = 2;
const OPS_ALLOWED_IN_TAIL: [u8; 4] = [opcodes::SPAN, opcodes::NOOP, opcodes::END, opcodes::HALT];
struct Fixture {
repro: ReproTrace,
height: usize,
splice_row: usize,
honest_outputs: StackOutputs,
}
fn op_bits(opcode: u8) -> [Felt; 7] {
core::array::from_fn(|i| Felt::from_u8((opcode >> i) & 1))
}
fn build_fixture() -> Fixture {
let source = "
begin
if.true
nop
end
nop
end
";
let program = miden_assembly::Assembler::default()
.assemble_program("program", source)
.unwrap()
.unwrap_program();
let mut stack_values = vec![Felt::ZERO; 16];
stack_values[0] = Felt::ONE;
stack_values[1] = Felt::new_unchecked(7);
stack_values[2] = Felt::new_unchecked(3);
let stack_inputs = StackInputs::new(&stack_values).unwrap();
let mut host = DefaultHost::default();
let (trace, precompile_witness) = FastProcessor::new(stack_inputs)
.execute_and_build_trace_sync(&program, &mut host, Prover::DEFAULT_MAX_PROVER_MEMORY_BYTES)
.unwrap();
assert!(precompile_witness.is_none());
let main = trace.main_trace();
let height = main.core_height();
let op_at = |row: usize| main.get_op_code(RowIndex::from(row));
let is = |row: usize, op: u8| op_at(row) == Felt::from_u8(op);
let splice_row = (2..height)
.find(|&row| {
is(row - 1, opcodes::END)
&& is(row, opcodes::SPAN)
&& is(row + 1, opcodes::NOOP)
&& (row..height).all(|r| OPS_ALLOWED_IN_TAIL.iter().any(|&op| is(r, op)))
})
.expect("trace must contain a first-child END -> SPAN -> NOOP window with an inert tail");
for op in [opcodes::END, opcodes::REPEAT, opcodes::RESPAN, opcodes::HALT] {
assert!(!is(splice_row, op), "predecessor END must already encode is_first_child = 1");
}
for row in splice_row..height {
for slot in 0..16 {
assert_eq!(
main.stack_element(slot, RowIndex::from(row)),
main.stack_element(slot, RowIndex::from(splice_row)),
"stack must be constant from the splice point on (row {row}, slot {slot})"
);
}
}
for row in 0..height {
assert_eq!(
main.core_row(RowIndex::from(row)).system.clk,
Felt::new_unchecked(row as u64),
"clk must equal the row index at row {row}"
);
}
let honest_outputs = *trace.stack_outputs();
let repro = ReproTrace::new(&trace);
Fixture {
repro,
height,
splice_row,
honest_outputs,
}
}
fn splice_op(fixture: &Fixture, opcode: u8) -> RowMajorMatrix<Felt> {
let mut core = fixture.repro.core.clone();
let width = core.width();
let splice = fixture.splice_row;
let height = fixture.height;
let prefix = width - RANGE_WIDTH;
for row in (splice + 1..height).rev() {
let (src, dst) = ((row - 1) * width, row * width);
core.values.copy_within(src..src + prefix, dst);
}
for row in 0..height {
assert_eq!(
core.values[row * width + prefix..(row + 1) * width],
fixture.repro.core.values[row * width + prefix..(row + 1) * width],
"the splice must leave the range-check table bit-identical (row {row})"
);
}
{
let decoder = &mut core_row_mut(&mut core, splice).decoder;
let bits = op_bits(opcode);
decoder.op_bits = bits;
decoder.hasher_state = [Felt::ZERO; 8];
decoder.in_span = Felt::ONE;
decoder.op_index = Felt::ZERO;
decoder.batch_flags = [Felt::ZERO; 3];
let [_, _, _, _, b4, b5, b6] = bits;
decoder.extra = [b6 * (Felt::ONE - b5) * b4, b6 * b5];
}
for row in 0..height {
core_row_mut(&mut core, row).system.clk = Felt::new_unchecked(row as u64);
}
core
}
fn apply_swap_effect(fixture: &Fixture, core: &mut RowMajorMatrix<Felt>) {
for row in (fixture.splice_row + 1)..fixture.height {
core_row_mut(core, row).stack.top.swap(0, 1);
}
}
fn swapped_outputs(fixture: &Fixture) -> StackOutputs {
let mut elements: [Felt; 16] =
core::array::from_fn(|i| fixture.honest_outputs.get_element(i).unwrap());
elements.swap(0, 1);
assert_ne!(elements[0], elements[1], "the swap must be observable");
StackOutputs::from(elements)
}
#[test]
fn honest_trace_verifies() {
let fixture = build_fixture();
let result = fixture.repro.prove_and_verify_current();
assert!(result.is_ok(), "honest trace must verify: {result:?}");
}
#[test]
fn uncommitted_noop_between_end_and_span_is_rejected() {
let fixture = build_fixture();
let core = splice_op(&fixture, opcodes::NOOP);
let result = fixture.repro.prove_and_verify_with_outputs(core, fixture.honest_outputs);
assert!(
result.is_err(),
"an in-span row cannot be entered without SPAN/RESPAN or an in-span predecessor: {result:?}"
);
}
#[test]
fn uncommitted_swap_between_end_and_span_is_rejected() {
let fixture = build_fixture();
let mut core = splice_op(&fixture, opcodes::SWAP);
apply_swap_effect(&fixture, &mut core);
let result = fixture.repro.prove_and_verify_with_outputs(core, swapped_outputs(&fixture));
assert!(
result.is_err(),
"an uncommitted SWAP must not verify against forged public outputs: {result:?}"
);
}
#[test]
fn spliced_noop_with_swapped_stack_is_rejected() {
let fixture = build_fixture();
let mut core = splice_op(&fixture, opcodes::NOOP);
apply_swap_effect(&fixture, &mut core);
let result = fixture.repro.prove_and_verify_with_outputs(core, swapped_outputs(&fixture));
assert!(result.is_err(), "a NOOP must not swap the stack: {result:?}");
}
#[test]
fn swapped_stack_without_splice_is_rejected() {
let fixture = build_fixture();
let mut core = fixture.repro.core.clone();
apply_swap_effect(&fixture, &mut core);
let result = fixture.repro.prove_and_verify_with_outputs(core, swapped_outputs(&fixture));
assert!(result.is_err(), "the stack forgery alone must be rejected: {result:?}");
}