use alloc::vec;
use miden_air::trace::RowIndex;
use miden_core::{Felt, Word, operations::opcodes, program::StackOutputs};
use miden_processor::{DefaultHost, FastProcessor, StackInputs};
use crate::{
Prover,
repro_harness::{ReproTrace, core_row_mut},
};
const ADDR: u64 = 40;
struct Fixture {
trace: ReproTrace,
height: usize,
dyncall_row: usize,
control_row: usize,
}
fn build_fixture() -> Fixture {
let source = "
proc foo
nop
end
begin
call.foo
mem_storew_le.40
movup.4
dyncall
end
";
let program = miden_assembly::Assembler::default()
.assemble_program("program", source)
.unwrap()
.unwrap_program();
let root = program.hash();
let foo_digest: Word = program
.mast_forest()
.procedure_digests()
.find(|d| *d != root)
.expect("foo must survive as its own procedure");
let mut stack_values = vec![Felt::ZERO; 16];
for (i, limb) in foo_digest.as_elements().iter().enumerate() {
stack_values[i] = *limb;
}
stack_values[4] = Felt::new_unchecked(ADDR);
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 dyncall_row = (0..height)
.find(|&row| op_at(row) == Felt::from_u8(opcodes::DYNCALL))
.expect("program must contain a DYNCALL");
assert_eq!(
main.stack_depth(RowIndex::from(dyncall_row)),
Felt::new_unchecked(16),
"the DYNCALL must execute at depth 16 for the overflow-gated branch to vanish"
);
assert_eq!(main.stack_element(15, RowIndex::from(dyncall_row + 1)), Felt::ZERO);
let control_row = ((dyncall_row + 1)..height)
.find(|&row| op_at(row) == Felt::from_u8(opcodes::NOOP))
.expect("program must contain a NOOP after the DYNCALL (the callee body)");
Fixture {
trace: ReproTrace::new(&trace),
height,
dyncall_row,
control_row,
}
}
#[test]
fn honest_dyncall_trace_verifies() {
let fixture = build_fixture();
assert!(fixture.trace.prove_and_verify_current().is_ok(), "honest trace must verify");
}
#[test]
fn forged_s15_after_dyncall_is_rejected() {
let fixture = build_fixture();
let forged = Felt::new_unchecked(12345);
let mut core_matrix = fixture.trace.core.clone();
for row in (fixture.dyncall_row + 1)..fixture.height {
core_row_mut(&mut core_matrix, row).stack.top[15] = forged;
}
let mut elements: [Felt; 16] =
core::array::from_fn(|i| fixture.trace.outputs().get_element(i).unwrap());
assert_eq!(elements[15], Felt::ZERO);
elements[15] = forged;
let forged_outputs = StackOutputs::from(elements);
assert_ne!(forged_outputs, fixture.trace.outputs());
let result = fixture.trace.prove_and_verify_with_outputs(core_matrix, forged_outputs);
assert!(
result.is_err(),
"the verifier must reject a forged bottom stack slot across DYNCALL: the s15 zeroing \
now covers DYNCALL when the overflow table is empty: {result:?}"
);
}
#[test]
fn forged_s15_after_noshift_row_is_rejected() {
let fixture = build_fixture();
let forged = Felt::new_unchecked(12345);
let mut core_matrix = fixture.trace.core.clone();
for row in (fixture.control_row + 1)..fixture.height {
core_row_mut(&mut core_matrix, row).stack.top[15] = forged;
}
let mut elements: [Felt; 16] =
core::array::from_fn(|i| fixture.trace.outputs().get_element(i).unwrap());
elements[15] = forged;
let forged_outputs = StackOutputs::from(elements);
let result = fixture.trace.prove_and_verify_with_outputs(core_matrix, forged_outputs);
assert!(
result.is_err(),
"the verifier must reject a forged bottom slot across an ordinary no-shift row -- \
`no_shift_at(15)` pins it there: {result:?}"
);
}