use miden_air::trace::RowIndex;
use miden_core::{Felt, field::Field, operations::opcodes, utils::RowMajorMatrix};
use miden_processor::{DefaultHost, FastProcessor, StackInputs};
use crate::{
Prover,
repro_harness::{ReproTrace, core_row_mut},
};
struct Fixture {
trace: ReproTrace,
continuation_end_row: usize,
call_end_row: usize,
honest_ctx: Felt,
honest_fn_hash: [Felt; 4],
honest_b0: Felt,
}
fn build_fixture() -> Fixture {
let program = miden_assembly::Assembler::default()
.assemble_program("program", "proc inner nop nop end begin call.inner end")
.unwrap()
.unwrap_program();
let mut host = DefaultHost::default();
let (trace, precompile_witness) = FastProcessor::new(StackInputs::default())
.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 call_end_row = (0..height)
.find(|&row| {
op_at(row) == Felt::from_u8(opcodes::END)
&& main.restores_caller_frame_flag(RowIndex::from(row)) == Felt::ONE
})
.expect("program must have a caller-frame END row");
let continuation_end_row = call_end_row - 1;
assert_eq!(op_at(continuation_end_row), Felt::from_u8(opcodes::END));
assert_eq!(
main.restores_caller_frame_flag(RowIndex::from(continuation_end_row)),
Felt::ZERO
);
let honest_ctx = main.ctx(RowIndex::from(call_end_row));
let honest_fn_hash = main.fn_hash(RowIndex::from(call_end_row));
let honest_b0 = main.stack_depth(RowIndex::from(call_end_row));
assert_ne!(honest_ctx, Felt::ZERO, "the callee must run in a non-root context");
assert_ne!(honest_fn_hash, [Felt::ZERO; 4], "the callee must have a non-zero fn_hash");
assert_eq!(main.ctx(RowIndex::from(call_end_row + 1)), Felt::ZERO);
assert_eq!(main.stack_depth(RowIndex::from(call_end_row + 1)), Felt::new_unchecked(16));
Fixture {
trace: ReproTrace::new(&trace),
continuation_end_row,
call_end_row,
honest_ctx,
honest_fn_hash,
honest_b0,
}
}
fn zero_caller_frame_payload(f: &Fixture, core: &mut RowMajorMatrix<Felt>) {
let row = core_row_mut(core, f.call_end_row);
row.system.ctx = Felt::ZERO;
row.system.fn_hash = [Felt::ZERO; 4];
row.stack.b0 = Felt::ZERO;
row.stack.b1 = Felt::ZERO;
row.stack.h0 = (Felt::ZERO - Felt::new_unchecked(16)).inverse();
}
#[test]
fn honest_trace_verifies() {
let f = build_fixture();
assert!(f.trace.prove_and_verify_current().is_ok(), "honest trace must verify");
}
#[test]
fn forged_caller_frame_flag_on_continuation_end_is_rejected() {
let f = build_fixture();
let mut core = f.trace.core.clone();
core_row_mut(&mut core, f.continuation_end_row).decoder.hasher_state[6] = Felt::ONE;
zero_caller_frame_payload(&f, &mut core);
let result = f.trace.prove_and_verify_allowing_lookup_rejection(core);
assert!(
result.is_err(),
"the proof pipeline must reject a caller-frame flag forged on a continuation END: the entry \
kind tag prevents a zero-payload caller-frame removal from matching the pending \
continuation addition: {result:?}"
);
}
#[test]
fn zeroed_payload_without_forged_caller_frame_flag_is_rejected() {
let f = build_fixture();
let mut core = f.trace.core.clone();
zero_caller_frame_payload(&f, &mut core);
let result = f.trace.prove_and_verify_allowing_lookup_rejection(core);
assert!(
result.is_err(),
"without the forged caller-frame flag the proof pipeline must reject this: \
{result:?}"
);
}
#[test]
fn forged_caller_frame_flag_without_zeroed_payload_is_rejected() {
let f = build_fixture();
let mut core = f.trace.core.clone();
core_row_mut(&mut core, f.continuation_end_row).decoder.hasher_state[6] = Felt::ONE;
assert_ne!(f.honest_ctx, Felt::ZERO);
assert_ne!(f.honest_fn_hash, [Felt::ZERO; 4]);
assert_eq!(f.honest_b0, Felt::new_unchecked(16));
let result = f.trace.prove_and_verify_allowing_lookup_rejection(core);
assert!(
result.is_err(),
"a caller-frame removal with a non-zero payload matches no addition and must unbalance \
the block-stack bus: {result:?}"
);
}