use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use std::{fs, io, println};
use miden_ace_codegen::{AceCircuit, AceConfig, LayoutKind};
use miden_air::ProcessorAir;
use miden_core::{Felt, crypto::hash::Poseidon2, field::QuadFelt};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Mode {
Check,
Write,
}
const MASM_CONFIG: AceConfig = AceConfig {
num_quotient_chunks: 8,
num_vlpi_groups: 1,
layout: LayoutKind::Masm,
};
pub const RELATION_DIGEST_PATHS: (&str, &str) =
("asm/sys/vm/mod.masm", "asm/sys/vm/constraints_eval.masm");
const PROTOCOL_ID: u64 = 0;
const AIR_CONFIG_PATH: &str = "../../../air/src/config.rs";
const CONSTRAINTS_EVAL_PATH: &str = "asm/sys/vm/constraints_eval.masm";
const RELATION_DIGEST_PATH: &str = RELATION_DIGEST_PATHS.0;
pub fn build_batched_circuit(config: AceConfig) -> AceCircuit<QuadFelt> {
let air = ProcessorAir;
let batch_config = miden_air::ace::logup_boundary_config();
miden_air::ace::build_batched_ace_circuit::<_, QuadFelt>(&air, config, &batch_config).unwrap()
}
pub fn compute_relation_digest(circuit_commitment: &[Felt; 4]) -> [Felt; 4] {
let input: Vec<Felt> = core::iter::once(Felt::new_unchecked(PROTOCOL_ID))
.chain(circuit_commitment.iter().copied())
.collect();
let digest = Poseidon2::hash_elements(&input);
let elems = digest.as_elements();
[elems[0], elems[1], elems[2], elems[3]]
}
pub fn run(mode: Mode) -> Result<(), String> {
match mode {
Mode::Check => check(),
Mode::Write => write().map_err(|e| format!("{e}")),
}
}
fn write() -> io::Result<()> {
let artifact = compute_artifacts()?;
write_artifacts(&artifact)
}
fn check() -> Result<(), String> {
constraints_eval_masm_matches_air()?;
relation_digest_matches_air()?;
Ok(())
}
fn compute_artifacts() -> io::Result<ComputedArtifacts> {
let circuit = build_batched_circuit(MASM_CONFIG);
let encoded = circuit.to_ace().unwrap();
let num_inputs = encoded.num_vars();
let num_eval_gates = encoded.num_eval_rows();
let instructions = encoded.instructions();
let size_in_felt = encoded.size_in_felt();
if !size_in_felt.is_multiple_of(8) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"circuit size_in_felt ({size_in_felt}) is not 8-aligned; adv_pipe requires 8-element chunks"
),
));
}
let adv_pipe_rows = size_in_felt / 8;
let circuit_commitment: [Felt; 4] = encoded.circuit_hash().into();
let relation_digest = compute_relation_digest(&circuit_commitment);
let mut constraints_eval = read_file(CONSTRAINTS_EVAL_PATH);
{
replace_masm_const(&mut constraints_eval, "NUM_INPUTS_CIRCUIT", &num_inputs.to_string());
replace_masm_const(
&mut constraints_eval,
"NUM_EVAL_GATES_CIRCUIT",
&num_eval_gates.to_string(),
);
let proc_start =
constraints_eval.find("proc load_ace_circuit_description").ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"proc load_ace_circuit_description not found",
)
})?;
if let Some(repeat_offset) = constraints_eval[proc_start..].find("repeat.") {
let abs = proc_start + repeat_offset;
let end = constraints_eval[abs..]
.find('\n')
.map(|i| abs + i)
.unwrap_or(constraints_eval.len());
constraints_eval.replace_range(abs..end, &format!("repeat.{adv_pipe_rows}"));
}
let section_marker = "# CONSTRAINT EVALUATION CIRCUIT DESCRIPTION";
let section_start = constraints_eval.find(section_marker).ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "constraints section marker not found")
})?;
constraints_eval.truncate(section_start);
let trimmed = constraints_eval.trim_end().len();
constraints_eval.truncate(trimmed);
constraints_eval.push_str("\n\n# CONSTRAINT EVALUATION CIRCUIT DESCRIPTION\n");
constraints_eval
.push_str("# =================================================================================================\n\n");
constraints_eval.push_str("adv_map CIRCUIT_COMMITMENT = [\n");
for (i, chunk) in instructions.chunks(8).enumerate() {
let vals: Vec<String> =
chunk.iter().map(|f| f.as_canonical_u64().to_string()).collect();
let line = vals.join(",");
if i < adv_pipe_rows - 1 {
constraints_eval.push_str(&format!(" {line},\n"));
} else {
constraints_eval.push_str(&format!(" {line}\n"));
}
}
constraints_eval.push_str("]\n");
}
let mut relation_mod = read_file(RELATION_DIGEST_PATH);
for (i, elem) in relation_digest.iter().enumerate() {
replace_masm_const(
&mut relation_mod,
&format!("RELATION_DIGEST_{i}"),
&elem.as_canonical_u64().to_string(),
);
}
let mut air_config = read_file(AIR_CONFIG_PATH);
let marker = "pub const RELATION_DIGEST: [Felt; 4] = [";
let start = air_config.find(marker).ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "RELATION_DIGEST not found in config.rs")
})?;
let block_start = start + marker.len();
let block_end =
air_config[block_start..]
.find("];")
.map(|idx| idx + block_start)
.ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "RELATION_DIGEST terminator not found")
})?;
let mut new_block: String = relation_digest
.iter()
.map(|f| format!("\n Felt::new_unchecked({}),", f.as_canonical_u64()))
.collect();
new_block.push('\n');
air_config.replace_range(block_start..block_end, &new_block);
Ok(ComputedArtifacts {
num_inputs,
num_eval_gates,
adv_pipe_rows,
circuit_commitment,
relation_digest,
constraints_eval,
relation_mod,
air_config,
})
}
fn write_artifacts(artifact: &ComputedArtifacts) -> io::Result<()> {
write_file(CONSTRAINTS_EVAL_PATH, &artifact.constraints_eval)?;
write_file(RELATION_DIGEST_PATH, &artifact.relation_mod)?;
write_file(AIR_CONFIG_PATH, &artifact.air_config)?;
println!(
"wrote asm/sys/vm/constraints_eval.masm ({} inputs, {} eval gates, repeat.{})",
artifact.num_inputs, artifact.num_eval_gates, artifact.adv_pipe_rows
);
println!("wrote asm/sys/vm/mod.masm (RELATION_DIGEST)");
println!("wrote air/src/config.rs (RELATION_DIGEST)");
println!("done — run `cargo test -p miden-air --lib` to update the insta snapshot");
Ok(())
}
pub fn constraints_eval_masm_matches_air() -> Result<(), String> {
let artifact = compute_artifacts().map_err(|e| e.to_string())?;
let masm = read_file(RELATION_DIGEST_PATHS.1);
let actual_num_inputs: usize =
parse_masm_const(&masm, "NUM_INPUTS_CIRCUIT", "constraints_eval.masm")?;
let actual_num_eval: usize =
parse_masm_const(&masm, "NUM_EVAL_GATES_CIRCUIT", "constraints_eval.masm")?;
let proc_start = masm
.find("proc load_ace_circuit_description")
.ok_or_else(|| "load_ace_circuit_description proc not found".to_string())?;
let actual_adv_pipe: usize = masm[proc_start..]
.lines()
.find_map(|line| line.trim().strip_prefix("repeat.").and_then(|v| v.parse::<usize>().ok()))
.ok_or_else(|| "repeat.N not found in load_ace_circuit_description".to_string())?;
let adv_start = masm
.find("adv_map CIRCUIT_COMMITMENT = [")
.ok_or_else(|| "adv_map CIRCUIT_COMMITMENT not found".to_string())?;
let adv_end = masm[adv_start..]
.find(']')
.map(|idx| idx + adv_start)
.ok_or_else(|| "adv_map data block terminator not found".to_string())?;
let data_str = &masm[masm[..adv_start].len() + "adv_map CIRCUIT_COMMITMENT = [".len()..adv_end];
let actual_data: Vec<Felt> = data_str
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|s| {
s.parse::<u64>()
.map(Felt::new_unchecked)
.map_err(|_| "invalid u64 in adv_map".to_string())
})
.collect::<Result<_, _>>()?;
let actual_hash = Poseidon2::hash_elements(&actual_data);
let actual_hash_u64: Vec<u64> =
actual_hash.as_elements().iter().map(Felt::as_canonical_u64).collect();
let expected_hash_u64: Vec<u64> =
artifact.circuit_commitment.iter().map(Felt::as_canonical_u64).collect();
if actual_num_inputs != artifact.num_inputs {
return Err(format!(
"NUM_INPUTS_CIRCUIT is stale ({actual_num_inputs} != {})",
artifact.num_inputs,
));
}
if actual_num_eval != artifact.num_eval_gates {
return Err(format!(
"NUM_EVAL_GATES_CIRCUIT is stale ({actual_num_eval} != {})",
artifact.num_eval_gates,
));
}
if actual_adv_pipe != artifact.adv_pipe_rows {
return Err(format!(
"repeat.N in load_ace_circuit_description is stale ({actual_adv_pipe} != {})",
artifact.adv_pipe_rows,
));
}
if actual_hash_u64 != expected_hash_u64 {
return Err("Circuit data in adv_map is stale (hash mismatch)".into());
}
Ok(())
}
pub fn relation_digest_matches_air() -> Result<(), String> {
let artifact = compute_artifacts().map_err(|e| e.to_string())?;
let expected = artifact.relation_digest;
if miden_air::config::RELATION_DIGEST != expected {
return Err("RELATION_DIGEST in air/src/config.rs is stale".into());
}
let masm = read_file(RELATION_DIGEST_PATH);
let mut masm_digest: [Felt; 4] = [Felt::ZERO; 4];
for (i, slot) in masm_digest.iter_mut().enumerate() {
let name = format!("RELATION_DIGEST_{i}");
*slot =
parse_masm_const::<u64>(&masm, &name, "sys/vm/mod.masm").map(Felt::new_unchecked)?;
}
if masm_digest != expected {
return Err("RELATION_DIGEST in sys/vm/mod.masm is stale".into());
}
Ok(())
}
fn parse_masm_const<T: core::str::FromStr>(
masm: &str,
name: &str,
file_label: &str,
) -> Result<T, String>
where
T::Err: core::fmt::Debug,
{
let prefix = format!("const {name} = ");
masm.lines()
.find_map(|line| line.trim().strip_prefix(&prefix).and_then(|v| v.parse::<T>().ok()))
.ok_or_else(|| format!("constant {name} not found in {file_label}"))
}
fn replace_masm_const(content: &mut String, name: &str, new_value: &str) {
let prefix = format!("const {name} = ");
let line_start = content.find(&prefix).unwrap_or_else(|| panic!("const {name} not found"));
let line_end = content[line_start..]
.find('\n')
.map(|i| line_start + i)
.unwrap_or(content.len());
content.replace_range(line_start..line_end, &format!("{prefix}{new_value}"));
}
fn read_file(rel_path: &str) -> String {
let path = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), rel_path);
fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"))
}
fn write_file(rel_path: &str, contents: &str) -> io::Result<()> {
let path = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), rel_path);
fs::write(&path, contents)
.map_err(|e| io::Error::new(e.kind(), format!("failed to write {path}: {e}")))
}
struct ComputedArtifacts {
num_inputs: usize,
num_eval_gates: usize,
adv_pipe_rows: usize,
circuit_commitment: [Felt; 4],
relation_digest: [Felt; 4],
constraints_eval: String,
relation_mod: String,
air_config: String,
}