Skip to main content

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}