miden_air/ace.rs
1//! ACE circuit integration for ProcessorAir.
2//!
3//! This module extends the constraint-evaluation DAG produced by
4//! `build_ace_dag_for_air` with the LogUp auxiliary-trace boundary check:
5//!
6//! ```text
7//! 0 = Σ aux_bound[0..NUM_LOGUP_COMMITTED_FINALS]
8//! + c_block_hash
9//! + c_log_precompile
10//! + c_kernel_rom
11//! ```
12//!
13//! Two of the three corrections depend only on fixed-length public inputs
14//! (`c_bh`, `c_lp`), so they are rebuilt directly inside the DAG as rational
15//! fractions `(n, d)` and folded into a running rational `(N, D)` without any
16//! in-circuit inversion. The kernel-ROM correction depends on the variable-
17//! length kernel digest list which the circuit can't walk, so MASM computes
18//! it (one final `ext2inv`) and hands it in as a single scalar via the
19//! existing `VlpiReduction(0)` input. The final boundary check is the
20//! quadratic identity `(Σ aux_bound + c_kr) · D + N = 0`.
21
22use alloc::{vec, vec::Vec};
23
24use miden_ace_codegen::{
25 AceCircuit, AceConfig, AceDag, AceError, DagBuilder, InputKey, NodeId, build_ace_dag_for_air,
26};
27use miden_core::{Felt, field::ExtensionField};
28use miden_crypto::{
29 field::Algebra,
30 stark::air::{LiftedAir, symbolic::SymbolicExpressionExt},
31};
32
33use crate::{PV_PROGRAM_HASH, PV_TRANSCRIPT_STATE};
34
35// BATCHING TYPES
36// ================================================================================================
37
38/// An element in a bus message encoding.
39///
40/// Bus messages are encoded as `bus_prefix + sum(beta^i * elements[i])` using the
41/// aux randomness challenges. Each element is either a constant base-field value
42/// or a reference to a fixed-length public input.
43#[derive(Debug, Clone)]
44pub enum MessageElement {
45 /// A constant base-field value.
46 Constant(Felt),
47 /// A fixed-length public input value, indexed into the public values array.
48 PublicInput(usize),
49}
50
51/// Sign applied to a `BusFraction` numerator.
52#[derive(Debug, Clone, Copy)]
53pub enum Sign {
54 Plus,
55 Minus,
56}
57
58/// A rational `±1 / bus_message` contribution to the LogUp boundary sum.
59///
60/// The bus message denominator is rebuilt inside the ACE DAG from public inputs
61/// and aux randomness, so no external input is needed for this term.
62#[derive(Debug, Clone)]
63pub struct BusFraction {
64 pub sign: Sign,
65 pub bus: usize,
66 pub message: Vec<MessageElement>,
67}
68
69/// Configuration for the LogUp auxiliary-trace boundary batching.
70///
71/// Consumed by [`batch_logup_boundary`] to extend the constraint-check DAG with
72/// the boundary identity checked by `ProcessorAir::reduced_aux_values`.
73#[derive(Debug, Clone)]
74pub struct LogUpBoundaryConfig {
75 /// Aux-bus-boundary column indices summed as `Σ aux_bound[col]`.
76 pub sum_columns: Vec<usize>,
77 /// Aux-bus-boundary columns that must individually equal zero. Absorbed at
78 /// an independent γ power from the accumulator sum so a malicious prover
79 /// cannot cancel them against `sum_columns` or the rational corrections.
80 /// Mirrors the native runtime check in `ProcessorAir::reduced_aux_values`.
81 pub zero_columns: Vec<usize>,
82 /// Rational `(±1, d_i)` fractions folded into the running rational `(N, D)`.
83 pub fractions: Vec<BusFraction>,
84 /// Scalar EF inputs added directly to the aux-boundary sum. Trusted from
85 /// MASM (the VM's own constraint system covers the procedure that produced
86 /// them). Typically one entry pointing at `VlpiReduction(0)` for `c_kr`.
87 pub scalar_corrections: Vec<InputKey>,
88}
89
90// BATCHING FUNCTIONS
91// ================================================================================================
92
93/// Extend a constraint DAG with the LogUp auxiliary-trace boundary check.
94///
95/// Builds:
96///
97/// `sum_aux = Σ AuxBusBoundary(col) + Σ scalar_corrections`
98/// `(N, D) = fold((0, 1), fractions)` via `(N', D') = (N·d_i + D·n_i, D·d_i)`
99/// `boundary = sum_aux · D + N`
100/// `zero_sum = Σ AuxBusBoundary(col)` for `col` in `zero_columns`
101/// `root = constraint_check + γ · boundary + γ² · zero_sum`
102///
103/// Returns the new DAG with the batched root. The zero-columns identity lives at
104/// γ² so a nonzero padding slot cannot cancel against the γ¹-batched boundary
105/// accumulator.
106pub fn batch_logup_boundary<EF>(
107 constraint_dag: AceDag<EF>,
108 config: &LogUpBoundaryConfig,
109) -> AceDag<EF>
110where
111 EF: ExtensionField<Felt>,
112{
113 let constraint_root = constraint_dag.root;
114 let mut builder = DagBuilder::from_dag(constraint_dag);
115
116 // sum_aux = Σ aux_bound[col] + Σ scalar_corrections
117 let mut sum_aux = builder.constant(EF::ZERO);
118 for &col in &config.sum_columns {
119 let node = builder.input(InputKey::AuxBusBoundary(col));
120 sum_aux = builder.add(sum_aux, node);
121 }
122 for &scalar in &config.scalar_corrections {
123 let node = builder.input(scalar);
124 sum_aux = builder.add(sum_aux, node);
125 }
126
127 // Fold all rational fractions into a single (N, D) running rational.
128 let mut num = builder.constant(EF::ZERO);
129 let mut den = builder.constant(EF::ONE);
130 for fraction in &config.fractions {
131 let d_i = encode_bus_message(&mut builder, fraction.bus, &fraction.message);
132 let sign_value = match fraction.sign {
133 Sign::Plus => EF::ONE,
134 Sign::Minus => -EF::ONE,
135 };
136 let n_i = builder.constant(sign_value);
137
138 // (num', den') = (num * d_i + den * n_i, den * d_i)
139 let num_d = builder.mul(num, d_i);
140 let den_n = builder.mul(den, n_i);
141 num = builder.add(num_d, den_n);
142 den = builder.mul(den, d_i);
143 }
144
145 // boundary = sum_aux · D + N
146 let sum_times_den = builder.mul(sum_aux, den);
147 let boundary = builder.add(sum_times_den, num);
148
149 // zero_sum = Σ aux_bound[col] for col in zero_columns. Each column's identity
150 // `aux_bound[col] = 0` is batched at γ² so it cannot cancel against boundary
151 // terms at γ¹ or against other zero columns in the same sum (the whole sum is
152 // the coefficient of a single γ power, which forces the sum to zero — and since
153 // the boundary check is linear in each slot, each slot is independently zero with
154 // high probability over γ).
155 let mut zero_sum = builder.constant(EF::ZERO);
156 for &col in &config.zero_columns {
157 let node = builder.input(InputKey::AuxBusBoundary(col));
158 zero_sum = builder.add(zero_sum, node);
159 }
160
161 // Batch with the constraint root using gamma.
162 let gamma = builder.input(InputKey::Gamma);
163 let gamma_boundary = builder.mul(gamma, boundary);
164 let constraint_plus_boundary = builder.add(constraint_root, gamma_boundary);
165 let gamma_sq = builder.mul(gamma, gamma);
166 let gamma_sq_zero = builder.mul(gamma_sq, zero_sum);
167 let root = builder.add(constraint_plus_boundary, gamma_sq_zero);
168
169 builder.build(root)
170}
171
172/// Encode a bus message as `bus_prefix[bus] + sum(beta^i * elements[i])`.
173///
174/// The bus prefix provides domain separation: `bus_prefix[bus] = alpha + (bus+1) * gamma_bus`
175/// where `gamma_bus = beta^MIDEN_MAX_MESSAGE_WIDTH`. This matches [`lookup::Challenges::encode`].
176fn encode_bus_message<EF>(
177 builder: &mut DagBuilder<EF>,
178 bus: usize,
179 elements: &[MessageElement],
180) -> NodeId
181where
182 EF: ExtensionField<Felt>,
183{
184 use crate::constraints::lookup::messages::MIDEN_MAX_MESSAGE_WIDTH;
185
186 let alpha = builder.input(InputKey::AuxRandAlpha);
187 let beta = builder.input(InputKey::AuxRandBeta);
188
189 // gamma_bus = beta^MIDEN_MAX_MESSAGE_WIDTH.
190 let mut gamma_bus = builder.constant(EF::ONE);
191 for _ in 0..MIDEN_MAX_MESSAGE_WIDTH {
192 gamma_bus = builder.mul(gamma_bus, beta);
193 }
194
195 // bus_prefix = alpha + (bus + 1) * gamma_bus
196 let scale = builder.constant(EF::from(Felt::from_u32((bus as u32) + 1)));
197 let offset = builder.mul(gamma_bus, scale);
198 let bus_prefix = builder.add(alpha, offset);
199
200 // acc = bus_prefix + sum(beta^i * elem_i)
201 //
202 // Beta powers are built incrementally; the DagBuilder is hash-consed, so
203 // identical beta^i nodes across multiple message encodings are shared
204 // automatically.
205 let mut acc = bus_prefix;
206 let mut beta_power = builder.constant(EF::ONE);
207 for elem in elements {
208 let node = match elem {
209 MessageElement::Constant(f) => builder.constant(EF::from(*f)),
210 MessageElement::PublicInput(idx) => builder.input(InputKey::Public(*idx)),
211 };
212 let term = builder.mul(beta_power, node);
213 acc = builder.add(acc, term);
214 beta_power = builder.mul(beta_power, beta);
215 }
216 acc
217}
218
219// AIR-SPECIFIC CONFIG
220// ================================================================================================
221
222/// Build the [`LogUpBoundaryConfig`] for the Miden VM ProcessorAir.
223///
224/// This mirrors `ProcessorAir::reduced_aux_values` in `air/src/lib.rs`: it sums
225/// the sole accumulator column's committed final value, adds the scalar kernel-ROM
226/// correction supplied by MASM via `VlpiReduction(0)`, and folds the two open-
227/// bus corrections `c_block_hash` and `c_log_precompile` as rational fractions
228/// whose denominators are rebuilt from public inputs inside the DAG.
229///
230/// The three fractions are:
231/// 1. `c_bh = +1 / encode(BLOCK_HASH_TABLE, [ph[0..4], 0, 0, 0])`
232/// 2. `c_lp_init = +1 / encode(LOG_PRECOMPILE, [0, 0, 0, 0])`
233/// 3. `c_lp_final = −1 / encode(LOG_PRECOMPILE, ts[0..4])`
234///
235/// `c_lp_init − c_lp_final` matches `transcript_messages` in `lib.rs` (initial
236/// minus final contribution). Splitting it into two rationals keeps the code
237/// uniform at the cost of two extra mul gates.
238pub fn logup_boundary_config() -> LogUpBoundaryConfig {
239 use MessageElement::{Constant, PublicInput};
240
241 use crate::constraints::lookup::messages::BusId;
242
243 // ph_msg = encode([ph[0], ph[1], ph[2], ph[3], 0, 0, 0])
244 // Matches `program_hash_message` in lib.rs.
245 let ph_msg = vec![
246 PublicInput(PV_PROGRAM_HASH),
247 PublicInput(PV_PROGRAM_HASH + 1),
248 PublicInput(PV_PROGRAM_HASH + 2),
249 PublicInput(PV_PROGRAM_HASH + 3),
250 Constant(Felt::ZERO), // parent_id = 0 (root block)
251 Constant(Felt::ZERO), // is_first_child = false
252 Constant(Felt::ZERO), // is_loop_body = false
253 ];
254
255 // default_lp_msg = encode([0, 0, 0, 0])
256 // Matches `transcript_messages(..).0` (initial default state).
257 let default_lp_msg = vec![
258 Constant(Felt::ZERO),
259 Constant(Felt::ZERO),
260 Constant(Felt::ZERO),
261 Constant(Felt::ZERO),
262 ];
263
264 // final_lp_msg = encode(ts[0..4])
265 // Matches `transcript_messages(..).1` (final public-input state).
266 let final_lp_msg = vec![
267 PublicInput(PV_TRANSCRIPT_STATE),
268 PublicInput(PV_TRANSCRIPT_STATE + 1),
269 PublicInput(PV_TRANSCRIPT_STATE + 2),
270 PublicInput(PV_TRANSCRIPT_STATE + 3),
271 ];
272
273 // TODO(#3032): only col 0 carries a real final accumulator; slot 1 is the
274 // placeholder — see `NUM_LOGUP_COMMITTED_FINALS`. Once trace splitting lands,
275 // move slot 1 from `zero_columns` to `sum_columns`.
276 //
277 // Slot 1 is in `zero_columns` so the recursive verifier independently rejects
278 // any nonzero padding value. The native verifier applies the matching runtime
279 // check in `ProcessorAir::reduced_aux_values` (see `lib.rs`).
280 LogUpBoundaryConfig {
281 sum_columns: vec![0],
282 zero_columns: vec![1],
283 fractions: vec![
284 BusFraction {
285 sign: Sign::Plus,
286 bus: BusId::BlockHashTable as usize,
287 message: ph_msg,
288 },
289 BusFraction {
290 sign: Sign::Plus,
291 bus: BusId::LogPrecompileTranscript as usize,
292 message: default_lp_msg,
293 },
294 BusFraction {
295 sign: Sign::Minus,
296 bus: BusId::LogPrecompileTranscript as usize,
297 message: final_lp_msg,
298 },
299 ],
300 scalar_corrections: vec![InputKey::VlpiReduction(0)],
301 }
302}
303
304// CONVENIENCE FUNCTION
305// ================================================================================================
306
307/// Build a batched ACE circuit for the provided AIR.
308///
309/// Builds the constraint-evaluation DAG, extends it with the LogUp auxiliary
310/// trace boundary check via [`batch_logup_boundary`], and emits the off-VM
311/// circuit representation.
312///
313/// The output circuit checks `constraint_check + γ · boundary = 0`, where
314/// `boundary = (Σ aux_bound + c_kr) · D + N` and `(N, D)` is the rational sum
315/// of the in-DAG open-bus corrections.
316pub fn build_batched_ace_circuit<A, EF>(
317 air: &A,
318 config: AceConfig,
319 boundary_config: &LogUpBoundaryConfig,
320) -> Result<AceCircuit<EF>, AceError>
321where
322 A: LiftedAir<Felt, EF>,
323 EF: ExtensionField<Felt>,
324 SymbolicExpressionExt<Felt, EF>: Algebra<EF>,
325{
326 let artifacts = build_ace_dag_for_air::<A, Felt, EF>(air, config)?;
327 let batched_dag = batch_logup_boundary(artifacts.dag, boundary_config);
328 miden_ace_codegen::emit_circuit(&batched_dag, artifacts.layout)
329}