use miden_air::lookup::Challenges;
use miden_core::{
Felt,
field::QuadFelt,
utils::{Matrix, RowMajorMatrix},
};
use miden_precompiles::{K1_BASE_BOUND_PTR, K1_SCALAR_BOUND_PTR, UintDomain};
use rand::{Rng, RngExt, SeedableRng, rngs::StdRng};
use super::uint::random_uint_below;
use crate::{
math::{U256, add_reduce, from_limbs32, mac_reduce},
relations::{MAX_MESSAGE_WIDTH, NUM_BUS_IDS},
session::{Session, SessionTraces, statements::horner_sign_paths},
tests::{SessionTracesTestExt, bus_balance::session_stack_residual, verify_deferred},
transcript::{
eval::{
COL_IS_ADD, COL_IS_MUL, COL_IS_SUB, COL_OUT_MULT, COL_PTR, COL_TAG_ARG0,
NUM_MAIN_COLS as EVAL_NUM_MAIN_COLS, TranscriptEvalAir,
},
nodes::UintOpId,
},
};
const FP: u32 = K1_BASE_BOUND_PTR;
const FQ: u32 = K1_SCALAR_BOUND_PTR;
fn domain_bound(domain: UintDomain) -> U256 {
from_limbs32(&domain.minus_one())
}
fn fp_bound() -> U256 {
domain_bound(UintDomain::K1Base)
}
fn fq_bound() -> U256 {
domain_bound(UintDomain::K1Scalar)
}
fn random_challenges(rng: &mut impl Rng) -> [QuadFelt; 2] {
core::array::from_fn(|_| {
QuadFelt::new([Felt::new(rng.random()).unwrap(), Felt::new(rng.random()).unwrap()])
})
}
fn assert_balanced(traces: &SessionTraces, rng: &mut impl Rng) {
let [alpha, beta] = random_challenges(rng);
let challenges = Challenges::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let mains = traces.mains();
let residual = session_stack_residual(&mains, &[], &challenges);
assert!(
residual.is_empty(),
"uint-DAG stack imbalance: {} unmatched denom(s); e.g. net {:?} on {}",
residual.len(),
residual.first().map(|(m, _)| *m),
residual.first().map(|(_, s)| s.as_str()).unwrap_or(""),
);
}
fn find_op_row(eval: &RowMajorMatrix<Felt>, flag_col: usize) -> usize {
(0..eval.height())
.find(|r| eval.values[r * EVAL_NUM_MAIN_COLS + flag_col] == Felt::ONE)
.expect("expected an op row in the eval trace")
}
#[test]
fn horner_sign_alternation_full_stack() {
let mut rng = StdRng::seed_from_u64(0x0da6_4011);
let bound = fp_bound();
let x_v = random_uint_below(&mut rng, bound);
let c_v: [U256; 4] = core::array::from_fn(|_| random_uint_below(&mut rng, bound));
let mut session = Session::new();
let (acc_a, acc_b) = horner_sign_paths(&mut session, x_v, &c_v, FP);
assert_eq!(acc_a.ptr, acc_b.ptr, "canonical interning must converge");
assert_ne!(acc_a.hash(), acc_b.hash(), "but the DAG shapes differ");
let claim = session.uint_is(&acc_a, &acc_b);
let root = session.assert_and_fold([claim]);
let traces = session.finish(root);
let mains = traces.mains();
assert_eq!(mains[4].height(), 32, "eval: 22 rows pad to 32");
assert_eq!(mains[6].height(), 16, "uint-add: 7 two-row blocks pad to 8");
traces.check();
assert_balanced(&traces, &mut rng);
}
#[test]
fn op_dedup_collapses_repeated_nodes() {
let mut rng = StdRng::seed_from_u64(0x0ded_0001);
let bound = fp_bound();
let x_v = random_uint_below(&mut rng, bound);
let y_v = random_uint_below(&mut rng, bound);
let mut session = Session::new();
let x = session.uint_leaf(x_v, FP);
let y = session.uint_leaf(y_v, FP);
let r1 = session.uint_add(&x, &y);
let r2 = session.uint_add(&x, &y);
assert_eq!(r1.id, r2.id, "identical ops must collapse onto one node");
let w = session.uint_add(&r1, &r2);
let sum = add_reduce(x_v, y_v, bound);
let expected = session.uint_leaf(add_reduce(sum, sum, bound), FP);
assert_eq!(w.ptr, expected.ptr, "the expected leaf dedups onto w");
let claim = session.uint_is(&w, &expected);
let root = session.assert_and_fold([claim]);
let traces = session.finish(root);
let mains = traces.mains();
assert_eq!(mains[6].height(), 4, "uint-add: exactly two two-row blocks");
let eval = mains[4];
let r_row = (0..eval.height())
.find(|row| {
eval.values[row * EVAL_NUM_MAIN_COLS + COL_IS_ADD] == Felt::ONE
&& eval.values[row * EVAL_NUM_MAIN_COLS + COL_PTR] == Felt::from(r1.ptr.addr())
})
.expect("r's op row");
assert_eq!(
eval.values[r_row * EVAL_NUM_MAIN_COLS + COL_OUT_MULT],
Felt::from(2u32),
"the deduped node is provided at its consumer count",
);
traces.check();
assert_balanced(&traces, &mut rng);
}
#[test]
#[should_panic(expected = "stray uint value node")]
fn stray_value_node_panics_at_finish() {
let mut rng = StdRng::seed_from_u64(0x057a_0001);
let bound = fp_bound();
let v = random_uint_below(&mut rng, bound);
let mut session = Session::new();
let _dangling = session.uint_leaf(v, FP);
let root = session.assert_and_fold(core::iter::empty());
let _ = session.finish(root);
}
#[test]
#[should_panic(expected = "unprovable")]
fn unequal_is_panics() {
let mut rng = StdRng::seed_from_u64(0x057a_0002);
let bound = fp_bound();
let v = random_uint_below(&mut rng, bound);
let w = v ^ U256::ONE;
let mut session = Session::new();
let a = session.uint_leaf(v, FP);
let b = session.uint_leaf(w, FP);
let _ = session.uint_is(&a, &b);
}
#[test]
#[should_panic(expected = "share a modulus")]
fn cross_modulus_op_panics() {
let mut rng = StdRng::seed_from_u64(0x057a_0003);
let bound_p = fp_bound();
let bound_q = fq_bound();
let mut session = Session::new();
let a = session.uint_leaf(random_uint_below(&mut rng, bound_p), FP);
let b = session.uint_leaf(random_uint_below(&mut rng, bound_q), FQ);
let _ = session.uint_add(&a, &b);
}
#[test]
#[should_panic(expected = "exceeds its modulus bound")]
fn out_of_range_leaf_panics() {
let bound = fp_bound();
let v = bound + U256::ONE;
let mut session = Session::new();
let _ = session.uint_leaf(v, FP);
}
fn mul_statement(rng: &mut impl Rng) -> SessionTraces {
let bound = fp_bound();
let x_v = random_uint_below(rng, bound);
let y_v = random_uint_below(rng, bound);
let mut session = Session::new();
let x = session.uint_leaf(x_v, FP);
let y = session.uint_leaf(y_v, FP);
let r = session.uint_mul(&x, &y);
let expected = session.uint_leaf(mac_reduce(1, x_v, y_v, 0, U256::ZERO, bound), FP);
let claim = session.uint_is(&r, &expected);
let root = session.assert_and_fold([claim]);
session.finish(root)
}
#[test]
fn forged_result_ptr_unbalances() {
let mut rng = StdRng::seed_from_u64(0xf043_0001);
let traces = mul_statement(&mut rng);
let mut tampered = traces.mains()[4].clone();
let row = find_op_row(&tampered, COL_IS_MUL);
tampered.values[row * EVAL_NUM_MAIN_COLS + COL_PTR] += Felt::ONE;
let mut mains = traces.mains();
mains[4] = &tampered;
let [alpha, beta] = random_challenges(&mut rng);
let challenges = Challenges::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let residual = session_stack_residual(&mains, &[], &challenges);
assert!(!residual.is_empty(), "a forged r_ptr must unbalance the bus");
}
#[test]
fn reencoded_op_id_passes_constraints_but_unbalances() {
let mut rng = StdRng::seed_from_u64(0xf043_0002);
let bound = fp_bound();
let x_v = random_uint_below(&mut rng, bound);
let y_v = random_uint_below(&mut rng, bound);
let mut session = Session::new();
let x = session.uint_leaf(x_v, FP);
let y = session.uint_leaf(y_v, FP);
let r = session.uint_add(&x, &y);
let expected = session.uint_leaf(add_reduce(x_v, y_v, bound), FP);
let claim = session.uint_is(&r, &expected);
let root = session.assert_and_fold([claim]);
let traces = session.finish(root);
let mut tampered = traces.mains()[4].clone();
let row = find_op_row(&tampered, COL_IS_ADD);
tampered.values[row * EVAL_NUM_MAIN_COLS + COL_IS_ADD] = Felt::ZERO;
tampered.values[row * EVAL_NUM_MAIN_COLS + COL_IS_SUB] = Felt::ONE;
tampered.values[row * EVAL_NUM_MAIN_COLS + COL_TAG_ARG0] = Felt::from(UintOpId::Sub as u8);
crate::tests::check_local_inputs(TranscriptEvalAir, &tampered, traces.air_inputs());
let mut mains = traces.mains();
mains[4] = &tampered;
let [alpha, beta] = random_challenges(&mut rng);
let challenges = Challenges::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let residual = session_stack_residual(&mains, &[], &challenges);
assert!(!residual.is_empty(), "a re-encoded op id must unbalance");
}
#[test]
fn eval_chip_stays_at_lqd_1() {
assert_eq!(crate::tests::log_quotient_degree(&TranscriptEvalAir), 1);
}
#[test]
#[ignore = "full prove/verify round-trip; run explicitly"]
fn horner_sign_alternation_proves() {
let mut rng = StdRng::seed_from_u64(0x0da6_4012);
let bound = fp_bound();
let x_v = random_uint_below(&mut rng, bound);
let c_v: [U256; 4] = core::array::from_fn(|_| random_uint_below(&mut rng, bound));
let mut session = Session::new();
let (acc_a, acc_b) = horner_sign_paths(&mut session, x_v, &c_v, FP);
let claim = session.uint_is(&acc_a, &acc_b);
let root = session.assert_and_fold([claim]);
let traces = session.finish(root);
verify_deferred(&traces.prove()).expect("the uint-DAG stack must verify");
}