use alloc::vec::Vec;
use miden_air::trace::chiplets::hasher::{
DIRECTION_BIT_COL_IDX, HASH_CYCLE_LEN, IS_BOUNDARY_COL_IDX, MRUPDATE_ID_COL_IDX,
NODE_INDEX_COL_IDX, S_PERM_COL_IDX, STATE_COL_RANGE, TRACE_WIDTH,
};
use miden_core::{
ONE, ZERO,
chiplets::hasher,
crypto::merkle::{MerkleTree, NodeIndex},
mast::OpBatch,
};
use miden_utils_testing::rand::rand_array;
use super::{
Digest, Felt, Hasher, HasherState, LINEAR_HASH, MP_VERIFY, MR_UPDATE_NEW, MR_UPDATE_OLD,
RETURN_HASH, RETURN_STATE, Selectors, TraceFragment, absorb_into_state, get_digest, init_state,
init_state_from_words,
};
#[test]
fn hasher_permute() {
let mut hasher = Hasher::default();
let init_state: HasherState = rand_array();
let (addr, final_state) = hasher.permute(init_state);
assert_eq!(ONE, addr);
let expected_state = apply_permutation(init_state);
assert_eq!(expected_state, final_state);
let trace = build_trace(hasher);
assert_eq!(trace[0].len(), 2 * HASH_CYCLE_LEN);
check_controller_input(&trace, 0, LINEAR_HASH, &init_state, ZERO, ONE, ZERO, ZERO);
check_controller_output(&trace, 1, RETURN_STATE, &expected_state, ZERO, ONE, ZERO);
check_perm_segment(&trace, HASH_CYCLE_LEN, &init_state, ONE);
}
#[test]
fn hasher_permute_two() {
let mut hasher = Hasher::default();
let init_state1: HasherState = rand_array();
let init_state2: HasherState = rand_array();
let (addr1, final_state1) = hasher.permute(init_state1);
let (addr2, final_state2) = hasher.permute(init_state2);
assert_eq!(ONE, addr1);
assert_eq!(Felt::from_u8(3), addr2);
assert_eq!(apply_permutation(init_state1), final_state1);
assert_eq!(apply_permutation(init_state2), final_state2);
let trace = build_trace(hasher);
assert_eq!(trace[0].len(), HASH_CYCLE_LEN + 2 * HASH_CYCLE_LEN);
check_controller_input(&trace, 0, LINEAR_HASH, &init_state1, ZERO, ONE, ZERO, ZERO);
check_controller_output(&trace, 1, RETURN_STATE, &final_state1, ZERO, ONE, ZERO);
check_controller_input(&trace, 2, LINEAR_HASH, &init_state2, ZERO, ONE, ZERO, ZERO);
check_controller_output(&trace, 3, RETURN_STATE, &final_state2, ZERO, ONE, ZERO);
}
#[test]
fn hasher_build_merkle_root_depth_1() {
let leaves = init_leaves(&[1, 2]);
let tree = MerkleTree::new(&leaves).unwrap();
let mut hasher = Hasher::default();
let path0 = tree.get_path(NodeIndex::new(1, 0).unwrap()).unwrap();
let (_, root) = hasher.build_merkle_root(leaves[0], &path0, ZERO);
assert_eq!(root, tree.root());
let trace = build_trace(hasher);
let init_state = init_state_from_words(&leaves[0], &path0[0]);
check_controller_input(&trace, 0, MP_VERIFY, &init_state, ZERO, ONE, ZERO, ZERO);
check_controller_output(
&trace,
1,
RETURN_HASH,
&apply_permutation(init_state),
ZERO,
ONE,
ZERO,
);
}
#[test]
fn hasher_build_merkle_root_depth_3() {
let leaves = init_leaves(&[1, 2, 3, 4, 5, 6, 7, 8]);
let tree = MerkleTree::new(&leaves).unwrap();
let mut hasher = Hasher::default();
let path = tree.get_path(NodeIndex::new(3, 5).unwrap()).unwrap();
let (_, root) = hasher.build_merkle_root(leaves[5], &path, Felt::from_u8(5));
assert_eq!(root, tree.root());
let trace = build_trace(hasher);
check_merkle_controller_pair(&trace, 0, MP_VERIFY, 5, true, false, ZERO, ONE, ZERO);
check_merkle_controller_pair(&trace, 2, MP_VERIFY, 2, false, false, ZERO, ZERO, ONE);
check_merkle_controller_pair(&trace, 4, MP_VERIFY, 1, false, true, ZERO, ONE, ZERO);
for row in [0, 2, 4] {
for cap_col in 11..15 {
assert_eq!(
trace[cap_col][row], ZERO,
"capacity should be zero on tree input row {row}, col {cap_col}"
);
}
}
}
#[test]
fn hasher_update_merkle_root() {
let leaves = init_leaves(&[1, 2, 3, 4]);
let tree = MerkleTree::new(&leaves).unwrap();
let mut hasher = Hasher::default();
let index = 1u64;
let path = tree.get_path(NodeIndex::new(2, index).unwrap()).unwrap();
let new_leaf: Digest = [Felt::from_u8(100), ZERO, ZERO, ZERO].into();
let update = hasher.update_merkle_root(
leaves[index as usize],
new_leaf,
&path,
Felt::new_unchecked(index),
);
assert_eq!(update.get_old_root(), tree.root());
let trace = build_trace(hasher);
check_merkle_controller_pair(&trace, 0, MR_UPDATE_OLD, 1, true, false, ONE, ONE, ZERO);
check_merkle_controller_pair(&trace, 2, MR_UPDATE_OLD, 0, false, true, ONE, ZERO, ZERO);
check_merkle_controller_pair(&trace, 4, MR_UPDATE_NEW, 1, true, false, ONE, ONE, ZERO);
check_merkle_controller_pair(&trace, 6, MR_UPDATE_NEW, 0, false, true, ONE, ZERO, ZERO);
}
#[test]
fn perm_segment_structure() {
let mut hasher = Hasher::default();
let init_state: HasherState = rand_array();
let (addr, result) = hasher.permute(init_state);
assert_eq!(addr, ONE, "first permutation should start at address 1");
assert_eq!(result, apply_permutation(init_state), "permuted state should match");
let trace = build_trace(hasher);
let perm_start = HASH_CYCLE_LEN;
for row in perm_start..perm_start + HASH_CYCLE_LEN {
assert_eq!(trace[S_PERM_COL_IDX][row], ONE, "s_perm should be 1 at row {row}");
}
for offset in [0, 1, 2, 3, 12, 13, 14, 15] {
let row = perm_start + offset;
assert_eq!(trace[0][row], ZERO, "perm row {row}: s0 should be zero");
assert_eq!(trace[1][row], ZERO, "perm row {row}: s1 should be zero");
assert_eq!(trace[2][row], ZERO, "perm row {row}: s2 should be zero");
}
let row_11 = perm_start + 11;
assert_eq!(trace[1][row_11], ZERO, "perm row {row_11}: s1 should be zero on int+ext row");
assert_eq!(trace[2][row_11], ZERO, "perm row {row_11}: s2 should be zero on int+ext row");
assert_eq!(trace[NODE_INDEX_COL_IDX][perm_start], ONE);
for row in perm_start..perm_start + HASH_CYCLE_LEN {
assert_eq!(trace[IS_BOUNDARY_COL_IDX][row], ZERO);
assert_eq!(trace[DIRECTION_BIT_COL_IDX][row], ZERO);
assert_eq!(trace[MRUPDATE_ID_COL_IDX][row], ZERO);
}
}
#[test]
fn perm_deduplication() {
let mut hasher = Hasher::default();
let init_state: HasherState = rand_array();
let (addr1, result1) = hasher.permute(init_state);
let (addr2, result2) = hasher.permute(init_state);
assert_eq!(result1, result2, "same input should produce same output");
assert_ne!(addr1, addr2, "second call should have a different address");
let trace = build_trace(hasher);
assert_eq!(trace[0].len(), 2 * HASH_CYCLE_LEN);
let perm_start = HASH_CYCLE_LEN;
assert_eq!(trace[NODE_INDEX_COL_IDX][perm_start], Felt::from_u8(2));
}
#[test]
fn hash_memoization_control_blocks() {
let h1: Digest = rand_array::<Felt, 4>().into();
let h2: Digest = rand_array::<Felt, 4>().into();
let domain = Felt::from_u8(7);
let state = super::init_state_from_words_with_domain(&h1, &h2, domain);
let permuted = apply_permutation(state);
let expected_hash: Digest = get_digest(&permuted);
let mut hasher = Hasher::default();
let (addr1, digest1) = hasher.hash_control_block(h1, h2, domain, expected_hash);
let (addr2, digest2) = hasher.hash_control_block(h1, h2, domain, expected_hash);
assert_eq!(digest1, digest2);
assert_eq!(digest1, expected_hash);
assert_ne!(addr1, addr2);
let trace = build_trace(hasher);
assert_eq!(trace[0].len(), 2 * HASH_CYCLE_LEN);
let perm_start = HASH_CYCLE_LEN;
assert_eq!(trace[NODE_INDEX_COL_IDX][perm_start], Felt::from_u8(2));
}
#[test]
fn hash_memoization_basic_blocks_single_batch() {
let mut hasher = Hasher::default();
let batches = make_single_batch();
let expected_hash = compute_basic_block_hash(&batches);
let (addr1, digest1) = hasher.hash_basic_block(&batches, expected_hash);
let (addr2, digest2) = hasher.hash_basic_block(&batches, expected_hash);
assert_eq!(digest1, digest2, "memoized digest should match original");
assert_eq!(digest1, expected_hash);
assert_ne!(addr1, addr2, "memoized call should have a different address");
let trace = build_trace(hasher);
assert_eq!(trace[0].len(), 2 * HASH_CYCLE_LEN);
check_controller_input(
&trace,
0,
LINEAR_HASH,
&init_state(batches[0].groups(), ZERO),
ZERO,
ONE,
ZERO,
ZERO,
);
check_controller_output(
&trace,
1,
RETURN_HASH,
&apply_permutation(init_state(batches[0].groups(), ZERO)),
ZERO,
ONE,
ZERO,
);
check_memoized_trace(&trace, 0..2, 2..4);
let perm_start = HASH_CYCLE_LEN;
assert_eq!(trace[NODE_INDEX_COL_IDX][perm_start], Felt::from_u8(2));
}
#[test]
fn hash_memoization_basic_blocks_multi_batch() {
let mut hasher = Hasher::default();
let batches = make_multi_batch(3);
let expected_hash = compute_basic_block_hash(&batches);
let (addr1, digest1) = hasher.hash_basic_block(&batches, expected_hash);
let (addr2, digest2) = hasher.hash_basic_block(&batches, expected_hash);
assert_eq!(digest1, digest2);
assert_eq!(digest1, expected_hash);
assert_ne!(addr1, addr2);
let trace = build_trace(hasher);
assert_eq!(trace[0].len(), HASH_CYCLE_LEN + 3 * HASH_CYCLE_LEN);
assert_eq!(trace[IS_BOUNDARY_COL_IDX][0], ONE);
assert_eq!(trace[DIRECTION_BIT_COL_IDX][0], ZERO);
assert_eq!(trace[IS_BOUNDARY_COL_IDX][1], ZERO);
assert_eq!(trace[DIRECTION_BIT_COL_IDX][1], ZERO);
assert_eq!(trace[IS_BOUNDARY_COL_IDX][2], ZERO);
assert_eq!(trace[IS_BOUNDARY_COL_IDX][4], ZERO);
assert_eq!(trace[IS_BOUNDARY_COL_IDX][5], ONE);
check_memoized_trace(&trace, 0..6, 6..12);
let perm_start = HASH_CYCLE_LEN;
for i in 0..3 {
let cycle_start = perm_start + i * HASH_CYCLE_LEN;
assert_eq!(
trace[NODE_INDEX_COL_IDX][cycle_start],
Felt::from_u8(2),
"perm cycle {i} should have multiplicity 2"
);
}
}
#[test]
fn hash_memoization_basic_blocks_check() {
let mut hasher = Hasher::default();
let batches = make_multi_batch(2);
let bb_hash = compute_basic_block_hash(&batches);
let loop_body_batches = make_single_batch();
let loop_body_hash = compute_basic_block_hash(&loop_body_batches);
let (bb1_addr, bb1_digest) = hasher.hash_basic_block(&batches, bb_hash);
assert_eq!(bb1_digest, bb_hash);
let (_loop_addr, loop_digest) = hasher.hash_basic_block(&loop_body_batches, loop_body_hash);
assert_eq!(loop_digest, loop_body_hash);
let join2_state =
super::init_state_from_words_with_domain(&bb1_digest, &loop_digest, Felt::from_u8(7));
let join2_permuted = apply_permutation(join2_state);
let join2_hash = get_digest(&join2_permuted);
let (_join2_addr, join2_digest) =
hasher.hash_control_block(bb1_digest, loop_digest, Felt::from_u8(7), join2_hash);
assert_eq!(join2_digest, join2_hash);
let (bb2_addr, bb2_digest) = hasher.hash_basic_block(&batches, bb_hash);
assert_eq!(bb2_digest, bb_hash);
assert_ne!(bb1_addr, bb2_addr, "memoized BB2 should have a different address");
let join1_state =
super::init_state_from_words_with_domain(&join2_digest, &bb2_digest, Felt::from_u8(7));
let join1_permuted = apply_permutation(join1_state);
let join1_hash = get_digest(&join1_permuted);
let (_join1_addr, join1_digest) =
hasher.hash_control_block(join2_digest, bb2_digest, Felt::from_u8(7), join1_hash);
assert_eq!(join1_digest, join1_hash);
let trace = build_trace(hasher);
let bb1_start = bb1_addr.as_canonical_u64() as usize - 1;
let bb2_start = bb2_addr.as_canonical_u64() as usize - 1;
check_memoized_trace(&trace, bb1_start..bb1_start + 4, bb2_start..bb2_start + 4);
let controller_rows: usize = 14; let controller_padded_len = controller_rows.next_multiple_of(HASH_CYCLE_LEN);
let perm_start = controller_padded_len;
let total_len = trace[0].len();
let num_perm_cycles = (total_len - perm_start) / HASH_CYCLE_LEN;
assert!(num_perm_cycles >= 5, "expected at least 5 perm cycles, got {num_perm_cycles}");
let mut mult_2_count = 0;
let mut mult_1_count = 0;
for i in 0..num_perm_cycles {
let cycle_start = perm_start + i * HASH_CYCLE_LEN;
let mult = trace[NODE_INDEX_COL_IDX][cycle_start];
if mult == Felt::from_u8(2) {
mult_2_count += 1;
} else if mult == ONE {
mult_1_count += 1;
}
}
assert_eq!(mult_2_count, 2, "expected 2 perm cycles with multiplicity 2 (BB1's states)");
assert_eq!(mult_1_count, 3, "expected 3 perm cycles with multiplicity 1");
}
fn build_trace(hasher: Hasher) -> Vec<Vec<Felt>> {
let trace_len = hasher.trace_len();
let mut trace = (0..TRACE_WIDTH).map(|_| vec![ZERO; trace_len]).collect::<Vec<_>>();
let mut fragment = TraceFragment::trace_to_fragment(&mut trace);
hasher.fill_trace(&mut fragment);
trace
}
fn check_controller_input(
trace: &[Vec<Felt>],
row: usize,
selectors: Selectors,
state: &HasherState,
node_index: Felt,
is_boundary: Felt,
mrupdate_id: Felt,
direction_bit: Felt,
) {
assert_eq!(trace[0][row], selectors[0], "s0 at row {row}");
assert_eq!(trace[1][row], selectors[1], "s1 at row {row}");
assert_eq!(trace[2][row], selectors[2], "s2 at row {row}");
for (i, &val) in state.iter().enumerate() {
assert_eq!(trace[STATE_COL_RANGE.start + i][row], val, "state[{i}] at row {row}");
}
assert_eq!(trace[NODE_INDEX_COL_IDX][row], node_index, "node_index at row {row}");
assert_eq!(trace[IS_BOUNDARY_COL_IDX][row], is_boundary, "is_boundary at row {row}");
assert_eq!(trace[DIRECTION_BIT_COL_IDX][row], direction_bit, "direction_bit at row {row}");
assert_eq!(trace[S_PERM_COL_IDX][row], ZERO, "s_perm should be 0 on controller row {row}");
assert_eq!(trace[MRUPDATE_ID_COL_IDX][row], mrupdate_id, "mrupdate_id at row {row}");
}
fn check_controller_output(
trace: &[Vec<Felt>],
row: usize,
selectors: Selectors,
state: &HasherState,
node_index: Felt,
is_boundary: Felt,
direction_bit: Felt,
) {
assert_eq!(trace[0][row], selectors[0], "s0 at row {row}");
assert_eq!(trace[1][row], selectors[1], "s1 at row {row}");
assert_eq!(trace[2][row], selectors[2], "s2 at row {row}");
for (i, &val) in state.iter().enumerate() {
assert_eq!(trace[STATE_COL_RANGE.start + i][row], val, "state[{i}] at row {row}");
}
assert_eq!(trace[NODE_INDEX_COL_IDX][row], node_index, "node_index at row {row}");
assert_eq!(trace[IS_BOUNDARY_COL_IDX][row], is_boundary, "is_boundary at row {row}");
assert_eq!(trace[DIRECTION_BIT_COL_IDX][row], direction_bit, "direction_bit at row {row}");
assert_eq!(trace[S_PERM_COL_IDX][row], ZERO, "s_perm should be 0 on controller row {row}");
}
fn check_merkle_controller_pair(
trace: &[Vec<Felt>],
input_row: usize,
input_selectors: Selectors,
node_index: u64,
is_boundary_input: bool,
is_boundary_output: bool,
mrupdate_id: Felt,
input_direction_bit: Felt,
output_direction_bit: Felt,
) {
let output_row = input_row + 1;
let is_boundary_input_felt = if is_boundary_input { ONE } else { ZERO };
let is_boundary_output_felt = if is_boundary_output { ONE } else { ZERO };
assert_eq!(trace[0][input_row], input_selectors[0], "s0 at input row {input_row}");
assert_eq!(trace[1][input_row], input_selectors[1], "s1 at input row {input_row}");
assert_eq!(trace[2][input_row], input_selectors[2], "s2 at input row {input_row}");
assert_eq!(
trace[NODE_INDEX_COL_IDX][input_row],
Felt::new_unchecked(node_index),
"node_index at input row {input_row}"
);
assert_eq!(
trace[IS_BOUNDARY_COL_IDX][input_row], is_boundary_input_felt,
"is_boundary at input row {input_row}"
);
assert_eq!(
trace[DIRECTION_BIT_COL_IDX][input_row], input_direction_bit,
"direction_bit at input row {input_row}"
);
assert_eq!(trace[S_PERM_COL_IDX][input_row], ZERO, "s_perm at input row {input_row}");
assert_eq!(
trace[MRUPDATE_ID_COL_IDX][input_row], mrupdate_id,
"mrupdate_id at input row {input_row}"
);
assert_eq!(
trace[NODE_INDEX_COL_IDX][output_row],
Felt::new_unchecked(node_index >> 1),
"node_index at output row {output_row}"
);
assert_eq!(
trace[IS_BOUNDARY_COL_IDX][output_row], is_boundary_output_felt,
"is_boundary at output row {output_row}"
);
assert_eq!(
trace[DIRECTION_BIT_COL_IDX][output_row], output_direction_bit,
"direction_bit at output row {output_row}"
);
assert_eq!(trace[S_PERM_COL_IDX][output_row], ZERO, "s_perm at output row {output_row}");
assert_eq!(
trace[MRUPDATE_ID_COL_IDX][output_row], mrupdate_id,
"mrupdate_id at output row {output_row}"
);
}
fn check_perm_segment(
trace: &[Vec<Felt>],
start_row: usize,
init_state: &HasherState,
expected_multiplicity: Felt,
) {
use miden_core::chiplets::hasher::Hasher;
let mut state = *init_state;
for (i, &val) in state.iter().enumerate() {
assert_eq!(
trace[STATE_COL_RANGE.start + i][start_row],
val,
"state[{i}] at perm row 0 (row {start_row})"
);
}
assert_eq!(trace[NODE_INDEX_COL_IDX][start_row], expected_multiplicity);
assert_eq!(trace[S_PERM_COL_IDX][start_row], ONE);
Hasher::apply_matmul_external(&mut state);
Hasher::add_rc(&mut state, &Hasher::ARK_EXT_INITIAL[0]);
Hasher::apply_sbox(&mut state);
Hasher::apply_matmul_external(&mut state);
check_state_at_row(trace, start_row + 1, &state, "after init+ext1");
for r in 1..=3 {
Hasher::add_rc(&mut state, &Hasher::ARK_EXT_INITIAL[r]);
Hasher::apply_sbox(&mut state);
Hasher::apply_matmul_external(&mut state);
check_state_at_row(trace, start_row + 1 + r, &state, &alloc::format!("after ext{}", r + 1));
}
for triple in 0..7_usize {
let base = triple * 3;
for k in 0..3 {
state[0] += Hasher::ARK_INT[base + k];
state[0] = state[0].exp_const_u64::<7>();
Hasher::matmul_internal(&mut state, Hasher::MAT_DIAG);
}
check_state_at_row(
trace,
start_row + 5 + triple,
&state,
&alloc::format!("after int triple {triple}"),
);
}
state[0] += Hasher::ARK_INT[21];
state[0] = state[0].exp_const_u64::<7>();
Hasher::matmul_internal(&mut state, Hasher::MAT_DIAG);
Hasher::add_rc(&mut state, &Hasher::ARK_EXT_TERMINAL[0]);
Hasher::apply_sbox(&mut state);
Hasher::apply_matmul_external(&mut state);
check_state_at_row(trace, start_row + 12, &state, "after int22+ext5");
for r in 1..=3 {
Hasher::add_rc(&mut state, &Hasher::ARK_EXT_TERMINAL[r]);
Hasher::apply_sbox(&mut state);
Hasher::apply_matmul_external(&mut state);
check_state_at_row(
trace,
start_row + 12 + r,
&state,
&alloc::format!("after ext{}", r + 5),
);
}
}
fn check_state_at_row(trace: &[Vec<Felt>], row: usize, state: &HasherState, label: &str) {
for (i, &val) in state.iter().enumerate() {
assert_eq!(trace[STATE_COL_RANGE.start + i][row], val, "state[{i}] at row {row} ({label})");
}
}
fn apply_permutation(mut state: HasherState) -> HasherState {
hasher::apply_permutation(&mut state);
state
}
fn init_leaves(values: &[u64]) -> Vec<Digest> {
values.iter().map(|&v| init_leaf(v)).collect()
}
fn init_leaf(value: u64) -> Digest {
[Felt::new_unchecked(value), ZERO, ZERO, ZERO].into()
}
fn check_memoized_trace(
trace: &[Vec<Felt>],
original: core::ops::Range<usize>,
copied: core::ops::Range<usize>,
) {
assert_eq!(
original.len(),
copied.len(),
"original and copied ranges must have the same length"
);
for (orig_row, copy_row) in original.zip(copied) {
for col in 0..3 {
assert_eq!(
trace[col][orig_row], trace[col][copy_row],
"selector col {col} mismatch: original row {orig_row} vs copied row {copy_row}"
);
}
for col in STATE_COL_RANGE {
assert_eq!(
trace[col][orig_row], trace[col][copy_row],
"state col {col} mismatch: original row {orig_row} vs copied row {copy_row}"
);
}
assert_eq!(
trace[NODE_INDEX_COL_IDX][orig_row], trace[NODE_INDEX_COL_IDX][copy_row],
"node_index mismatch: original row {orig_row} vs copied row {copy_row}"
);
assert_eq!(
trace[IS_BOUNDARY_COL_IDX][orig_row], trace[IS_BOUNDARY_COL_IDX][copy_row],
"is_boundary mismatch: original row {orig_row} vs copied row {copy_row}"
);
assert_eq!(
trace[DIRECTION_BIT_COL_IDX][orig_row], trace[DIRECTION_BIT_COL_IDX][copy_row],
"direction_bit mismatch: original row {orig_row} vs copied row {copy_row}"
);
assert_eq!(
trace[S_PERM_COL_IDX][copy_row], ZERO,
"s_perm should be 0 on copied controller row {copy_row}"
);
}
}
fn make_basic_block_batches(ops: Vec<miden_core::operations::Operation>) -> Vec<OpBatch> {
use miden_core::mast::BasicBlockNodeBuilder;
let node = BasicBlockNodeBuilder::new(ops, Vec::new())
.build()
.expect("failed to build basic block");
node.op_batches().to_vec()
}
fn make_single_batch() -> Vec<OpBatch> {
use miden_core::operations::Operation;
make_basic_block_batches(vec![Operation::Pad])
}
fn make_multi_batch(n: usize) -> Vec<OpBatch> {
use miden_core::operations::Operation;
assert!(n >= 2, "use make_single_batch for n=1");
let num_ops = 72 * (n - 1) + 1;
let ops = vec![Operation::Noop; num_ops];
let batches = make_basic_block_batches(ops);
assert_eq!(batches.len(), n, "expected exactly {n} batches, got {}", batches.len());
batches
}
fn compute_basic_block_hash(batches: &[OpBatch]) -> Digest {
assert!(!batches.is_empty());
let mut state = init_state(batches[0].groups(), ZERO);
hasher::apply_permutation(&mut state);
for batch in batches.iter().skip(1) {
absorb_into_state(&mut state, batch.groups());
hasher::apply_permutation(&mut state);
}
get_digest(&state)
}