use miden_air::BaseAir;
use miden_core::{
Felt,
field::{PrimeCharacteristicRing, QuadFelt},
};
use miden_lifted_air::LiftedAir;
use rand::{RngExt, SeedableRng, rngs::StdRng};
use crate::{
hash::{
chunk::{
COL_ACT, COL_CHUNK_SEQ_ID, COL_F_BEGIN, COL_F_END, COL_IS_HEAD, COL_PERM_SEQ_ID,
ChunkAir, ChunkChainMsg, NUM_AUX_COLS, NUM_MAIN_COLS,
trace::{ChunkRequires, Invocation, generate_trace},
},
memory64::Memory64Msg,
},
logup::{Challenges, LookupMessage, NUM_PUBLIC_VALUES, NUM_RANDOMNESS, NUM_SIGMA_VALUES},
relations::{BusId, MAX_MESSAGE_WIDTH, NUM_BUS_IDS},
transcript::poseidon2::trace::Poseidon2Requires,
};
fn build_chunk_requires(invocations: &[Invocation]) -> (ChunkRequires, Poseidon2Requires) {
let mut p2 = Poseidon2Requires::new();
let mut chunk = ChunkRequires::new();
for inv in invocations {
chunk.require(inv, &mut p2);
}
(chunk, p2)
}
fn check_with_seed(_seed: u64, invocations: &[Invocation]) {
let (chunk_req, _p2) = build_chunk_requires(invocations);
let main = generate_trace(chunk_req);
crate::tests::check_local(ChunkAir, &main);
}
fn inv(len: usize, seed: u64) -> Invocation {
let mut rng = StdRng::seed_from_u64(seed);
Invocation {
input: (0..len).map(|_| rng.random()).collect(),
}
}
#[test]
fn chunk_chain_msg_encodes_with_chunk_chain_bus_prefix() {
let alpha = QuadFelt::from_u64(31);
let beta = QuadFelt::from_u64(37);
let challenges = Challenges::<QuadFelt>::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let chunk_seq_id_head = Felt::from(13u32);
let perm_seq_id_head = Felt::from(17u32);
let msg = ChunkChainMsg { chunk_seq_id_head, perm_seq_id_head };
let enc = msg.encode(&challenges);
let bus_prefix = alpha
+ beta.exp_u64(MAX_MESSAGE_WIDTH as u64)
* QuadFelt::from_u64((BusId::ChunkChain as u64) + 1);
let expected =
bus_prefix + QuadFelt::from(chunk_seq_id_head) + beta * QuadFelt::from(perm_seq_id_head);
assert_eq!(enc, expected);
}
#[test]
fn chunk_chain_bus_has_disjoint_prefix() {
let alpha = QuadFelt::from_u64(2);
let beta = QuadFelt::from_u64(3);
let challenges = Challenges::<QuadFelt>::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let a = Felt::from(5u32);
let b = Felt::from(11u32);
let enc_chunk_chain = ChunkChainMsg {
chunk_seq_id_head: a,
perm_seq_id_head: b,
}
.encode(&challenges);
let enc_memory64 = Memory64Msg { addr: a, lo: b, hi: Felt::ZERO }.encode(&challenges);
assert_ne!(enc_chunk_chain, enc_memory64);
}
#[test]
fn main_column_layout_partitions_12_indices() {
assert_eq!(COL_CHUNK_SEQ_ID, 0);
assert_eq!(COL_PERM_SEQ_ID, 1);
assert_eq!(COL_ACT, 2);
assert_eq!(COL_IS_HEAD, 3);
assert_eq!(COL_F_BEGIN, 4);
assert_eq!(COL_F_END, NUM_MAIN_COLS);
assert_eq!(NUM_MAIN_COLS, 12);
assert_eq!(<ChunkAir as BaseAir<Felt>>::width(&ChunkAir), NUM_MAIN_COLS);
}
#[test]
fn lifted_air_validates_and_layout_matches_spec() {
let air = ChunkAir;
let layout = <ChunkAir as LiftedAir<Felt, QuadFelt>>::air_layout(&air);
assert_eq!(layout.preprocessed_width, 0);
assert_eq!(layout.main_width, NUM_MAIN_COLS);
assert_eq!(layout.num_public_values, NUM_PUBLIC_VALUES);
assert_eq!(layout.permutation_width, NUM_AUX_COLS);
assert_eq!(layout.num_permutation_challenges, NUM_RANDOMNESS);
assert_eq!(layout.num_permutation_values, NUM_SIGMA_VALUES);
assert_eq!(layout.num_periodic_columns, 0);
}
#[test]
fn log_quotient_degree_matches_design_target() {
let air = ChunkAir;
assert_eq!(crate::tests::log_quotient_degree(&air), 1);
}
#[test]
fn constraints_hold_on_single_chunk() {
check_with_seed(0x01, &[inv(1, 0x11)]);
check_with_seed(0x07, &[inv(7, 0x77)]);
check_with_seed(0x08, &[inv(8, 0x88)]);
check_with_seed(0x20, &[inv(32, 0x20)]);
}
#[test]
fn constraints_hold_on_two_chunks() {
check_with_seed(0x21, &[inv(33, 0x21)]);
}
#[test]
fn constraints_hold_on_block_aligned() {
check_with_seed(0x80, &[inv(128, 0x80)]);
}
#[test]
fn constraints_hold_on_five_chunks() {
check_with_seed(0x81, &[inv(129, 0x81)]);
check_with_seed(0x87, &[inv(135, 0x87)]);
}
#[test]
fn constraints_hold_on_pad_block_tail() {
check_with_seed(0x88, &[inv(136, 0x88)]);
check_with_seed(0xc8, &[inv(200, 0xc8)]);
}
#[test]
fn constraints_hold_on_multiple_invocations() {
check_with_seed(0x5ea, &[inv(33, 0xa1), inv(40, 0xb2), inv(129, 0xc3)]);
}
#[test]
fn constraints_hold_with_perm_seq_id_jump_at_head() {
let (chunk_req, _p2) = build_chunk_requires(&[inv(33, 0xa1), inv(40, 0xb2)]);
let mut main = generate_trace(chunk_req);
let gap = Felt::from(7u8);
for row in 2..4 {
let cell = &mut main.values[row * NUM_MAIN_COLS + COL_PERM_SEQ_ID];
*cell += gap;
}
crate::tests::check_local(ChunkAir, &main);
}
fn corrupt_and_check(
_seed: u64,
invocations: &[Invocation],
corruption: impl FnOnce(&mut miden_core::utils::RowMajorMatrix<Felt>),
) {
let (chunk_req, _p2) = build_chunk_requires(invocations);
let mut main = generate_trace(chunk_req);
corruption(&mut main);
crate::tests::check_local(ChunkAir, &main);
}
#[test]
#[should_panic(expected = "constraint not satisfied")]
fn corruption_non_binary_act() {
corrupt_and_check(0xc0, &[inv(33, 0x21)], |main| {
main.values[COL_ACT] = Felt::from(2u8);
});
}
#[test]
#[should_panic(expected = "constraint not satisfied")]
fn corruption_non_binary_is_head() {
corrupt_and_check(0xc1, &[inv(33, 0x21)], |main| {
main.values[COL_IS_HEAD] = Felt::from(2u8);
});
}
#[test]
#[should_panic(expected = "constraint not satisfied")]
fn corruption_chunk_seq_id_breaks_chain() {
corrupt_and_check(0xc3, &[inv(200, 0xc8)], |main| {
main.values[NUM_MAIN_COLS + COL_CHUNK_SEQ_ID] = Felt::from(7u8);
});
}
#[test]
#[should_panic(expected = "constraint not satisfied")]
fn corruption_perm_seq_id_jump_mid_chain() {
corrupt_and_check(0xc4, &[inv(200, 0xc8)], |main| {
let cell = &mut main.values[3 * NUM_MAIN_COLS + COL_PERM_SEQ_ID];
*cell += Felt::from(5u8);
});
}
#[test]
#[should_panic(expected = "constraint not satisfied")]
fn corruption_is_head_on_dead_row() {
corrupt_and_check(0xc5, &[inv(200, 0xc8)], |main| {
main.values[7 * NUM_MAIN_COLS + COL_IS_HEAD] = Felt::ONE;
});
}