use std::sync::Arc;
use miden_assembly::{Assembler, Linkage};
use miden_core::{Felt, deferred::DeferredState, utils::bytes_to_packed_u32_elements};
use miden_core_lib::CoreLibrary;
use miden_crypto::hash::keccak::Keccak256;
use miden_processor::{
DefaultHost, ExecutionError, ExecutionOptions, ExecutionOutput, FastProcessor, StackInputs,
advice::AdviceInputs,
};
use crate::helpers::{masm_push_felts, masm_store_felts};
const IN_PTR: u32 = 128;
const TRUNCATE_STACK_TO_1_PROC: &str = r#"
@locals(1)
proc truncate_stack_to_1
loc_store.0
sdepth neq.16
while.true
drop
sdepth neq.16
end
loc_load.0 swap drop
end
"#;
const TRUNCATE_STACK_TO_8_PROC: &str = r#"
@locals(8)
proc truncate_stack_to_8
loc_storew_le.0 dropw
loc_storew_le.4 dropw
sdepth neq.16
while.true
drop
sdepth neq.16
end
loc_loadw_le.4
swapw
loc_loadw_le.0
end
"#;
#[test]
fn core_keccak256_hash_bytes_returns_expected_digest() {
let input = b"core hash compatibility";
let output =
run_core_hash_bytes("keccak256", input).expect("keccak256::hash_bytes must execute");
assert_deferred_state_round_trips(&output);
assert_eq!(read_stack_felts(&output, 8), pack_digest(&Keccak256::hash(input)));
}
#[test]
fn core_keccak256_hash_returns_expected_digest() {
let input: Vec<u8> = (0u8..32).collect();
let output = run_core_fixed_hash("keccak256", &input).expect("keccak256::hash must execute");
assert_deferred_state_round_trips(&output);
assert_eq!(read_stack_felts(&output, 8), pack_digest(&Keccak256::hash(&input)));
}
#[test]
fn core_keccak256_merge_returns_expected_digest() {
let left: Vec<u8> = (0u8..32).collect();
let right: Vec<u8> = (32u8..64).collect();
let mut preimage = left.clone();
preimage.extend_from_slice(&right);
let output = run_core_merge("keccak256", &left, &right).expect("keccak256::merge must execute");
assert_deferred_state_round_trips(&output);
assert_eq!(read_stack_felts(&output, 8), pack_digest(&Keccak256::hash(&preimage)));
}
#[test]
fn core_hash_wrapper_cycle_baselines() {
let input: Vec<u8> = (0u8..32).collect();
let left: Vec<u8> = (0u8..32).collect();
let right: Vec<u8> = (32u8..64).collect();
let short = b"core hash compatibility";
let mut mismatches = Vec::new();
for (name, source, expected) in [
("core_keccak_hash", cycle_fixed_hash_source("keccak256", &input), 221),
("core_keccak_merge", cycle_merge_source("keccak256", &left, &right), 238),
("core_keccak_hash_bytes_short", cycle_hash_bytes_source("keccak256", short), 248),
] {
let output =
run_core_program(&source).unwrap_or_else(|err| panic!("{name} failed: {err:?}"));
let cycles = read_stack_felts(&output, 1)[0].as_canonical_u64();
if cycles != expected {
mismatches.push(format!("{name}: expected {expected}, got {cycles}"));
}
}
assert!(mismatches.is_empty(), "cycle count changed:\n{}", mismatches.join("\n"));
}
fn run_core_hash_bytes(module: &str, input: &[u8]) -> Result<ExecutionOutput, ExecutionError> {
let input_felts = bytes_to_packed_u32_elements(input);
let stores = masm_store_felts(&input_felts, IN_PTR);
let truncate = truncate_proc_for_module(module);
let truncate_call = truncate_call_for_module(module);
let source = format!(
r#"
{truncate}
begin
{stores}
push.{len_bytes}
push.{IN_PTR}
exec.::miden::core::crypto::hashes::{module}::hash_bytes
exec.{truncate_call}
end
"#,
len_bytes = input.len(),
);
run_core_program(&source)
}
fn run_core_fixed_hash(module: &str, input: &[u8]) -> Result<ExecutionOutput, ExecutionError> {
assert_eq!(input.len(), 32, "fixed hash wrapper takes a 256-bit input");
let input = masm_push_felts(&bytes_to_packed_u32_elements(input));
let truncate = truncate_proc_for_module(module);
let truncate_call = truncate_call_for_module(module);
let source = format!(
r#"
{truncate}
begin
{input}
exec.::miden::core::crypto::hashes::{module}::hash
exec.{truncate_call}
end
"#,
);
run_core_program(&source)
}
fn run_core_merge(
module: &str,
left: &[u8],
right: &[u8],
) -> Result<ExecutionOutput, ExecutionError> {
assert_eq!(left.len(), 32, "merge left input must be 256 bits");
assert_eq!(right.len(), 32, "merge right input must be 256 bits");
let left = masm_push_felts(&bytes_to_packed_u32_elements(left));
let right = masm_push_felts(&bytes_to_packed_u32_elements(right));
let truncate = truncate_proc_for_module(module);
let truncate_call = truncate_call_for_module(module);
let source = format!(
r#"
{truncate}
begin
{right}
{left}
exec.::miden::core::crypto::hashes::{module}::merge
exec.{truncate_call}
end
"#,
);
run_core_program(&source)
}
fn cycle_fixed_hash_source(module: &str, input: &[u8]) -> String {
assert_eq!(input.len(), 32, "fixed hash wrapper takes a 256-bit input");
let input = masm_push_felts(&bytes_to_packed_u32_elements(input));
format!(
r#"
{TRUNCATE_STACK_TO_1_PROC}
begin
{input}
clk push.512 mem_store
exec.::miden::core::crypto::hashes::{module}::hash
clk push.512 mem_load sub
exec.truncate_stack_to_1
end
"#,
)
}
fn cycle_merge_source(module: &str, left: &[u8], right: &[u8]) -> String {
assert_eq!(left.len(), 32, "merge left input must be 256 bits");
assert_eq!(right.len(), 32, "merge right input must be 256 bits");
let left = masm_push_felts(&bytes_to_packed_u32_elements(left));
let right = masm_push_felts(&bytes_to_packed_u32_elements(right));
format!(
r#"
{TRUNCATE_STACK_TO_1_PROC}
begin
{right}
{left}
clk push.512 mem_store
exec.::miden::core::crypto::hashes::{module}::merge
clk push.512 mem_load sub
exec.truncate_stack_to_1
end
"#,
)
}
fn cycle_hash_bytes_source(module: &str, input: &[u8]) -> String {
let input_felts = bytes_to_packed_u32_elements(input);
let stores = masm_store_felts(&input_felts, IN_PTR);
format!(
r#"
{TRUNCATE_STACK_TO_1_PROC}
begin
{stores}
push.{len_bytes}
push.{IN_PTR}
clk push.512 mem_store
exec.::miden::core::crypto::hashes::{module}::hash_bytes
clk push.512 mem_load sub
exec.truncate_stack_to_1
end
"#,
len_bytes = input.len(),
)
}
fn truncate_proc_for_module(module: &str) -> &'static str {
match module {
"keccak256" => TRUNCATE_STACK_TO_8_PROC,
_ => panic!("unsupported hash module {module}"),
}
}
fn truncate_call_for_module(module: &str) -> &'static str {
match module {
"keccak256" => "truncate_stack_to_8",
_ => panic!("unsupported hash module {module}"),
}
}
fn run_core_program(source: &str) -> Result<ExecutionOutput, ExecutionError> {
let core_lib = CoreLibrary::default();
let program = Assembler::default()
.with_package(core_lib.package(), Linkage::Dynamic)
.expect("failed to link core library")
.assemble_program("core_hash_compat_test", source)
.expect("failed to assemble core hash compatibility test program")
.unwrap_program();
let mut host = DefaultHost::default()
.with_library(&core_lib)
.expect("failed to load CoreLibrary into the host");
let processor = FastProcessor::new_with_options(
StackInputs::default(),
AdviceInputs::default(),
ExecutionOptions::default(),
)
.expect("processor construction");
let output = processor.execute_sync(&program, &mut host);
if let Ok(output) = &output {
assert!(output.advice.stack().is_empty(), "core hash wrappers must consume advice");
}
output
}
fn read_stack_felts(output: &ExecutionOutput, len: usize) -> Vec<Felt> {
(0..len).map(|i| output.stack.get_element(i).expect("stack element")).collect()
}
fn pack_digest(bytes: &[u8]) -> Vec<Felt> {
bytes_to_packed_u32_elements(bytes)
}
fn assert_deferred_state_round_trips(output: &ExecutionOutput) {
let registry = Arc::new(miden_precompiles::registry());
let wire = output.deferred_state.to_wire().expect("deferred state must encode to wire");
let rehydrated = DeferredState::from_wire(Arc::clone(®istry), &wire, usize::MAX)
.expect("deferred wire must rehydrate under miden-precompiles registry");
assert_eq!(
rehydrated.root(),
output.deferred_state.root(),
"wire round-trip must preserve the deferred root",
);
}