use alloc::vec;
use miden_air::trace::RowIndex;
use miden_core::{Felt, Word, operations::opcodes, utils::RowMajorMatrix};
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,
dyncall_end: usize,
callee_ctx: Felt,
callee_fn_hash: [Felt; 4],
}
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 = |r: usize| main.get_op_code(RowIndex::from(r)).as_canonical_u64() as u8;
let dyncall_row = (0..height).find(|&r| op(r) == opcodes::DYNCALL).expect("has DYNCALL");
let dyncall_end = (dyncall_row..height)
.find(|&r| {
op(r) == opcodes::END && main.restores_caller_frame_flag(RowIndex::from(r)) == Felt::ONE
})
.expect("DYNCALL block has a caller-frame END");
assert_eq!(main.stack_depth(RowIndex::from(dyncall_row)), Felt::new_unchecked(16));
assert_eq!(main.ctx(RowIndex::from(dyncall_row)), Felt::ZERO);
assert_eq!(main.fn_hash(RowIndex::from(dyncall_row)), [Felt::ZERO; 4]);
assert_eq!(
main.decoder_hasher_state_element(4, RowIndex::from(dyncall_row)),
Felt::new_unchecked(16),
"h4 must honestly hold the caller depth 16 (the sole nonzero saved-state slot)"
);
let callee_ctx = main.ctx(RowIndex::from(dyncall_end));
let callee_fn_hash = main.fn_hash(RowIndex::from(dyncall_end));
assert_ne!(callee_ctx, Felt::ZERO, "the callee runs in a non-root context");
assert_ne!(callee_fn_hash, [Felt::ZERO; 4]);
assert_eq!(main.ctx(RowIndex::from(dyncall_end + 1)), Felt::ZERO, "honestly restored");
assert_eq!(main.decoder_hasher_state_element(6, RowIndex::from(dyncall_end)), Felt::ONE);
Fixture {
trace: ReproTrace::new(&trace),
height,
dyncall_row,
dyncall_end,
callee_ctx,
callee_fn_hash,
}
}
fn carry_callee_context_forward(f: &Fixture, core: &mut RowMajorMatrix<Felt>) {
for row in (f.dyncall_end + 1)..f.height {
let r = core_row_mut(core, row);
r.system.ctx = f.callee_ctx;
r.system.fn_hash = f.callee_fn_hash;
}
}
#[test]
fn honest_trace_verifies() {
let f = build_fixture();
assert!(f.trace.prove_and_verify_current().is_ok(), "honest trace must verify");
}
#[test]
fn relabelled_dyncall_end_is_rejected() {
let f = build_fixture();
let mut core = f.trace.core.clone();
core_row_mut(&mut core, f.dyncall_row).decoder.hasher_state[4] = Felt::ZERO;
core_row_mut(&mut core, f.dyncall_end).decoder.hasher_state[6] = Felt::ZERO;
carry_callee_context_forward(&f, &mut core);
let result = f.trace.prove_and_verify_allowing_lookup_rejection(core);
assert!(
result.is_err(),
"the proof pipeline must reject a relabelled DYNCALL END: {result:?}"
);
}
#[test]
fn relabel_without_degenerating_the_addition_is_rejected() {
let f = build_fixture();
let mut core = f.trace.core.clone();
core_row_mut(&mut core, f.dyncall_end).decoder.hasher_state[6] = Felt::ZERO;
carry_callee_context_forward(&f, &mut core);
let result = f.trace.prove_and_verify_allowing_lookup_rejection(core);
assert!(
result.is_err(),
"with h4 = 16, relabelling the caller-frame END and carrying the callee context forward \
must be rejected by the block-stack relation: {result:?}"
);
}
#[test]
fn degenerate_addition_without_relabelling_is_rejected() {
let f = build_fixture();
let mut core = f.trace.core.clone();
core_row_mut(&mut core, f.dyncall_row).decoder.hasher_state[4] = Felt::ZERO;
carry_callee_context_forward(&f, &mut core);
let result = f.trace.prove_and_verify_allowing_lookup_rejection(core);
assert!(
result.is_err(),
"degenerating h4 while leaving the caller-frame END honest must be rejected by the direct \
h4 binding and the mismatched caller-frame payload: {result:?}"
);
}