Skip to main content

miden_processor/trace/parallel/
mod.rs

1use alloc::{boxed::Box, collections::BTreeMap, sync::Arc, vec::Vec};
2use core::borrow::{Borrow, BorrowMut};
3
4use itertools::Itertools;
5use miden_air::{
6    AIRS, CoreCols, Felt, MIDEN_AIR_COUNT, MidenAir, StackCols, SystemCols, config, memory,
7    trace::{
8        DECODER_TRACE_WIDTH, MIN_TRACE_LEN, MainTrace, RANGE_CHECK_TRACE_WIDTH, RowIndex,
9        STACK_TRACE_WIDTH, SYS_TRACE_WIDTH, chiplets::bitwise::OP_CYCLE_LEN, decoder::NUM_OP_BITS,
10    },
11};
12use miden_core::{
13    ONE, Word, ZERO,
14    field::{PrimeCharacteristicRing, batch_inversion_allow_zeros},
15    mast::{MastForestId, OpBatch, SparseMastForest},
16    operations::opcodes,
17    program::{KernelDescriptor, MIN_STACK_DEPTH},
18    utils::Idx,
19};
20use rayon::prelude::*;
21use tracing::{info_span, instrument};
22
23use super::{
24    chiplets::Chiplets,
25    execution_tracer::TraceReplay,
26    trace_state::{
27        AceReplay, BitwiseOp, BitwiseReplay, CoreTraceFragmentContext, CoreTraceState,
28        ExecutionReplay, HasherRequestReplay, KernelReplay, MemoryWritesReplay, RangeCheckerReplay,
29        ResolvedBasicBlockGroups, ResolvedHasherOp,
30    },
31};
32use crate::{
33    ContextId, ExecutionError,
34    continuation_stack::{Continuation, ContinuationStack},
35    errors::MapExecErrNoCtx,
36    trace::{
37        ChipletsLengths, TraceLenSummary, VmTrace, VmWitness,
38        chiplets::{Ace, Bitwise, Hasher, KernelRom, Memory},
39        parallel::{processor::ReplayProcessor, tracer::CoreTraceGenerationTracer},
40        range::RangeChecker,
41        utils::RowMajorTraceWriter,
42    },
43};
44
45/// Per-row payload written by the core tracer (system + decoder + stack).
46pub const CORE_TRACE_WIDTH: usize = SYS_TRACE_WIDTH + DECODER_TRACE_WIDTH + STACK_TRACE_WIDTH;
47
48/// Physical row width of the core buffer: the [`CORE_TRACE_WIDTH`] payload plus the two
49/// trailing range-checker columns, which together form the per-AIR Core matrix
50/// (`NUM_CORE_COLS`) consumed directly by proving. The range columns are filled in-place
51/// after padding (see `write_range_into_core`).
52pub const CORE_STORAGE_WIDTH: usize = CORE_TRACE_WIDTH + RANGE_CHECK_TRACE_WIDTH;
53
54/// `build_trace()` uses this as a hard cap on trace rows, independent of the memory budget.
55///
56/// The code checks `core_trace_contexts.len() * fragment_size` before allocation. It checks the
57/// same cap again while replaying chiplet activity, and once more against each padded per-AIR
58/// height once they're known (see [`validate_heights_within_max_trace_len`]). Row indices are
59/// `u32`-backed, so this bound must hold regardless of budget; the actual memory bound is the
60/// tiered check in `build_trace_inner` (see [`memory::max_any_height_for_budget`] and
61/// [`memory::prover_peak_bytes`]).
62pub(crate) const MAX_TRACE_LEN: usize = 1 << 29;
63
64/// Default maximum memory, in bytes, [`build_trace`] assumes when no budget is given explicitly.
65/// Set to 64 GiB, comfortably above every workload in-repo and far below what the previous
66/// row-count cap admitted; callers that own actual proving policy (e.g. `miden-prover`'s
67/// `Prover`) are expected to set their own via [`build_trace_with_budget`].
68pub const DEFAULT_MAX_PROVER_MEMORY_BYTES: u64 = 64 << 30;
69
70pub(crate) mod core_trace_fragment;
71
72mod processor;
73mod tracer;
74
75#[cfg(test)]
76mod tests;
77
78// BUILD TRACE
79// ================================================================================================
80
81/// Builds the main trace from the provided trace states in parallel.
82///
83/// # Example
84/// ```
85/// use miden_assembly::Assembler;
86/// use miden_processor::{DefaultHost, FastProcessor, StackInputs};
87///
88/// let program = Assembler::default()
89///     .assemble_program("prg", "begin push.1 drop end")
90///     .unwrap()
91///     .unwrap_program();
92/// let mut host = DefaultHost::default();
93///
94/// let execution_witness = FastProcessor::new(StackInputs::default())
95///     .execute_for_proving_sync(&program, &mut host)
96///     .unwrap();
97/// let (vm_witness, _) = execution_witness.into_parts();
98/// let trace = miden_processor::trace::build_trace(vm_witness).unwrap();
99///
100/// assert_eq!(*trace.program_hash(), program.hash());
101/// ```
102#[instrument(name = "build_trace", skip_all)]
103pub fn build_trace(witness: VmWitness) -> Result<VmTrace, ExecutionError> {
104    build_trace_inner(witness, None, DEFAULT_MAX_PROVER_MEMORY_BYTES)
105}
106
107/// Same as [`build_trace`], but with an explicit memory budget instead of the default.
108pub fn build_trace_with_budget(
109    witness: VmWitness,
110    max_prover_memory_bytes: u64,
111) -> Result<VmTrace, ExecutionError> {
112    build_trace_inner(witness, None, max_prover_memory_bytes)
113}
114
115/// Same as [`build_trace_with_budget`], but with a hasher chiplet that was already built — used
116/// by the streaming path, where the hasher builder runs concurrently with program execution
117/// (`FastProcessor::execute_and_build_trace_sync`, std-only).
118#[cfg(feature = "std")]
119pub(crate) fn build_trace_with_prebuilt_hasher(
120    witness: VmWitness,
121    prebuilt_hasher: Hasher,
122    max_prover_memory_bytes: u64,
123) -> Result<VmTrace, ExecutionError> {
124    build_trace_inner(witness, Some(prebuilt_hasher), max_prover_memory_bytes)
125}
126
127fn build_trace_inner(
128    witness: VmWitness,
129    prebuilt_hasher: Option<Hasher>,
130    max_prover_memory_bytes: u64,
131) -> Result<VmTrace, ExecutionError> {
132    let VmWitness {
133        program_info,
134        stack_inputs,
135        stack_outputs,
136        trace,
137        precompile_root,
138    } = witness;
139
140    let TraceReplay {
141        core_trace_contexts,
142        mast_forest_store,
143        range_checker_replay,
144        memory_writes,
145        bitwise_replay: bitwise,
146        kernel_replay,
147        hasher_for_chiplet,
148        ace_replay,
149        fragment_size,
150        max_stack_depth,
151    } = trace;
152
153    let pcs_params = config::pcs_params();
154
155    // Tier 1: a permissive per-AIR row cap derived from the cheapest AIR's marginal cost, so it
156    // never rejects a shape that the exact budget check below would accept. `MAX_TRACE_LEN` is a
157    // hard ceiling independent of the budget (row indices are `u32`-backed).
158    let max_trace_len =
159        MAX_TRACE_LEN.min(memory::max_any_height_for_budget(max_prover_memory_bytes, &pcs_params));
160
161    // Before any trace generation, check that the core trace buffer `generate_core_trace_row_major`
162    // is about to allocate (`core_trace_contexts.len() * fragment_size` rows, before any padding or
163    // proving-time blowup) doesn't itself exceed the byte budget. This is deliberately a separate,
164    // more permissive cap than `max_trace_len`: that one prices in the full proving pipeline
165    // (blowup, quotient, Merkle trees) that this raw buffer hasn't incurred yet, so reusing it here
166    // would reject buffer allocations the exact budget check below would happily accept once the
167    // real (usually much smaller) padded core height is known.
168    //
169    // Note that we add 1 to the total core trace rows to account for the additional HALT opcode row
170    // that is pushed at the end of the last fragment.
171    let max_core_alloc_len = MAX_TRACE_LEN.min(max_core_alloc_rows(max_prover_memory_bytes));
172    let total_core_trace_rows = core_trace_contexts
173        .len()
174        .checked_mul(fragment_size)
175        .and_then(|n| n.checked_add(1))
176        .ok_or(ExecutionError::TraceLenExceeded(max_core_alloc_len))?;
177    if total_core_trace_rows > max_core_alloc_len {
178        return Err(ExecutionError::TraceLenExceeded(max_core_alloc_len));
179    }
180
181    if core_trace_contexts.is_empty() {
182        return Err(ExecutionError::Internal("no trace fragments provided in the trace witness"));
183    }
184
185    let chiplets = info_span!("initialize_chiplets").in_scope(|| {
186        initialize_chiplets(
187            program_info.kernel().clone(),
188            &core_trace_contexts,
189            memory_writes,
190            bitwise,
191            kernel_replay,
192            hasher_for_chiplet,
193            prebuilt_hasher,
194            ace_replay,
195            &mast_forest_store,
196            max_trace_len,
197        )
198    })?;
199
200    let range_checker = info_span!("initialize_range_checker")
201        .in_scope(|| initialize_range_checker(range_checker_replay, &chiplets));
202
203    let mut core_trace_data = info_span!("generate_core_trace").in_scope(|| {
204        generate_core_trace_row_major(
205            core_trace_contexts,
206            program_info.kernel().clone(),
207            fragment_size,
208            &mast_forest_store,
209            max_stack_depth,
210        )
211    })?;
212
213    let core_trace_len = core_trace_data.len() / CORE_STORAGE_WIDTH;
214
215    // Get the number of rows for the range checker
216    let range_table_len = range_checker.get_number_range_checker_rows();
217
218    let core_height = pad_to_trace_length(core_trace_len.max(range_table_len));
219    let chiplets_height = pad_to_trace_length(chiplets.trace_len());
220    let poseidon2_permutation_trace_len = chiplets.poseidon2_permutation_trace_len();
221    let poseidon2_permutation_height = pad_to_trace_length(poseidon2_permutation_trace_len);
222
223    // Exact check against modelled peak prover memory: pad-up can push usage over budget even
224    // when the permissive tier-1 row cap above passed.
225    debug_assert_eq!(
226        AIRS,
227        [MidenAir::Core, MidenAir::Chiplets, MidenAir::Poseidon2Permutation],
228        "heights below must be listed in AIRS order",
229    );
230    let heights: [usize; MIDEN_AIR_COUNT] =
231        [core_height, chiplets_height, poseidon2_permutation_height];
232    validate_heights_within_max_trace_len(&heights)?;
233    let estimated_bytes = memory::prover_peak_bytes(&heights, &pcs_params).ok_or(
234        ExecutionError::ProverMemoryExceeded {
235            estimated_bytes: u64::MAX,
236            budget_bytes: max_prover_memory_bytes,
237        },
238    )?;
239    if estimated_bytes > max_prover_memory_bytes {
240        return Err(ExecutionError::ProverMemoryExceeded {
241            estimated_bytes,
242            budget_bytes: max_prover_memory_bytes,
243        });
244    }
245
246    let trace_len_summary = TraceLenSummary::new_with_padded(
247        core_trace_len,
248        range_table_len,
249        ChipletsLengths::new(&chiplets),
250        poseidon2_permutation_trace_len,
251        heights,
252    );
253
254    // Each segment is built at its own per-AIR height (no cross-padding to the unified max).
255    let ((chiplets_trace, poseidon2_permutation_trace), ()) = info_span!("chiplet_traces_core_pad")
256        .in_scope(|| {
257            rayon::join(
258                || chiplets.into_traces(chiplets_height, poseidon2_permutation_height),
259                || pad_core_row_major(&mut core_trace_data, core_height),
260            )
261        });
262
263    // The range checker occupies the two trailing columns of the core buffer.
264    info_span!("write_range_checker_columns").in_scope(|| {
265        range_checker.write_range_into_core(
266            &mut core_trace_data,
267            CORE_STORAGE_WIDTH,
268            CORE_TRACE_WIDTH,
269            CORE_TRACE_WIDTH + 1,
270            range_table_len,
271            core_height,
272        )
273    });
274
275    // Create the MainTrace
276    let main_trace = {
277        let last_program_row = RowIndex::from((core_trace_len as u32).saturating_sub(1));
278        MainTrace::from_parts(
279            core_trace_data,
280            chiplets_trace.trace,
281            poseidon2_permutation_trace.trace,
282            last_program_row,
283        )
284    };
285
286    Ok(VmTrace::new_from_parts(
287        program_info,
288        stack_inputs,
289        stack_outputs,
290        precompile_root,
291        main_trace,
292        trace_len_summary,
293    ))
294}
295
296// HELPERS
297// ================================================================================================
298
299/// Pad a logical row count to a valid trace length: next power of two, clamped to `MIN_TRACE_LEN`.
300fn pad_to_trace_length(logical_len: usize) -> usize {
301    logical_len.next_power_of_two().max(MIN_TRACE_LEN)
302}
303
304/// The largest number of core-trace rows that may be allocated for
305/// `generate_core_trace_row_major`'s raw buffer while staying within `max_prover_memory_bytes`.
306///
307/// The buffer is `rows * CORE_STORAGE_WIDTH` [`Felt`]s at its 1x size, with no blowup, quotient, or
308/// Merkle-tree overhead yet, so this is priced directly off the buffer's own byte size rather than
309/// through [`memory::max_any_height_for_budget`]'s full-pipeline model.
310fn max_core_alloc_rows(max_prover_memory_bytes: u64) -> usize {
311    let bytes_per_row = (CORE_STORAGE_WIDTH * size_of::<Felt>()) as u64;
312    usize::try_from(max_prover_memory_bytes / bytes_per_row).unwrap_or(usize::MAX)
313}
314
315/// Rejects any padded per-AIR height above the hard [`MAX_TRACE_LEN`] row cap, independent of the
316/// memory budget: row indices are `u32`-backed, so a height must stay well under `2^32` regardless
317/// of how permissive the budget is.
318fn validate_heights_within_max_trace_len(
319    heights: &[usize; MIDEN_AIR_COUNT],
320) -> Result<(), ExecutionError> {
321    if heights.iter().any(|&height| height > MAX_TRACE_LEN) {
322        return Err(ExecutionError::TraceLenExceeded(MAX_TRACE_LEN));
323    }
324    Ok(())
325}
326
327/// Generates row-major core trace in parallel from the provided trace fragment contexts.
328fn generate_core_trace_row_major(
329    core_trace_contexts: Vec<CoreTraceFragmentContext>,
330    kernel: KernelDescriptor,
331    fragment_size: usize,
332    mast_forest_store: &[Arc<SparseMastForest>],
333    max_stack_depth: usize,
334) -> Result<Vec<Felt>, ExecutionError> {
335    let num_fragments = core_trace_contexts.len();
336    let total_allocated_rows = num_fragments * fragment_size;
337
338    let mut core_trace_data = Felt::zero_vec(total_allocated_rows * CORE_STORAGE_WIDTH);
339
340    // Save the first stack top for initialization
341    let first_stack_top = if let Some(first_context) = core_trace_contexts.first() {
342        first_context.state.stack.stack_top.to_vec()
343    } else {
344        vec![ZERO; MIN_STACK_DEPTH]
345    };
346
347    let writers: Vec<RowMajorTraceWriter<'_, Felt>> = core_trace_data
348        .chunks_exact_mut(fragment_size * CORE_STORAGE_WIDTH)
349        .map(|chunk| {
350            RowMajorTraceWriter::with_stride(chunk, CORE_STORAGE_WIDTH, CORE_STORAGE_WIDTH)
351        })
352        .collect();
353
354    // Build the core trace fragments in parallel
355    let fragment_results: Result<Vec<_>, ExecutionError> = core_trace_contexts
356        .into_par_iter()
357        .zip(writers.into_par_iter())
358        .map(|(trace_state, writer)| {
359            let (mut processor, mut tracer, mut continuation_stack, mut current_forest) =
360                split_trace_fragment_context(
361                    trace_state,
362                    writer,
363                    fragment_size,
364                    mast_forest_store,
365                    max_stack_depth,
366                )?;
367
368            processor.execute(
369                &mut continuation_stack,
370                &mut current_forest,
371                &kernel,
372                &mut tracer,
373            )?;
374
375            tracer.into_final_state()
376        })
377        .collect();
378    let fragment_results = fragment_results?;
379
380    let mut stack_rows = Vec::new();
381    let mut system_rows = Vec::new();
382    let mut total_core_trace_rows = 0;
383
384    for final_state in fragment_results {
385        stack_rows.push(final_state.last_stack_cols);
386        system_rows.push(final_state.last_system_cols);
387        total_core_trace_rows += final_state.num_rows_written;
388    }
389
390    // Fix up stack and system rows
391    fixup_stack_and_system_rows(
392        &mut core_trace_data,
393        fragment_size,
394        &stack_rows,
395        &system_rows,
396        &first_stack_top,
397    );
398    set_loop_body_multiplicities(&mut core_trace_data, total_core_trace_rows)?;
399
400    // Run batch inversion on stack's H0 helper column, processing each fragment in parallel.
401    // This must be done after fixup_stack_and_system_rows since that function overwrites the first
402    // row of each fragment with non-inverted values.
403    {
404        let w = CORE_STORAGE_WIDTH;
405        core_trace_data[..total_core_trace_rows * w]
406            .par_chunks_mut(fragment_size * w)
407            .for_each(|fragment_chunk| {
408                let num_rows = fragment_chunk.len() / w;
409                let mut h0_vals: Vec<Felt> = (0..num_rows)
410                    .map(|r| {
411                        let row: &CoreCols<Felt> = fragment_chunk[r * w..(r + 1) * w].borrow();
412                        row.stack.h0
413                    })
414                    .collect();
415                batch_inversion_allow_zeros(&mut h0_vals);
416                for (r, &val) in h0_vals.iter().enumerate() {
417                    let row: &mut CoreCols<Felt> = fragment_chunk[r * w..(r + 1) * w].borrow_mut();
418                    row.stack.h0 = val;
419                }
420            });
421    }
422
423    // Truncate the core trace columns to the actual number of rows written.
424    core_trace_data.truncate(total_core_trace_rows * CORE_STORAGE_WIDTH);
425
426    push_halt_opcode_row(
427        &mut core_trace_data,
428        total_core_trace_rows,
429        system_rows
430            .last()
431            .ok_or(ExecutionError::Internal("no trace fragments provided in the trace witness"))?,
432        stack_rows
433            .last()
434            .ok_or(ExecutionError::Internal("no trace fragments provided in the trace witness"))?,
435    );
436
437    Ok(core_trace_data)
438}
439
440/// Fill each LOOP row's `group_count` with the number of loop-body END rows that return to that
441/// dynamic loop address.
442///
443/// The block-hash lookup uses this value as the multiplicity of the LOOP-committed body digest.
444/// REPEAT rows intentionally do not add loop-body entries, so the original LOOP row must carry
445/// the multiplicity for every iteration of the dynamic loop instance. This is the honest aggregate
446/// count; AIR soundness also relies on the decoder/address/block-stack provenance constraints to
447/// prevent forged same-key END rows or dynamic-address reuse.
448fn set_loop_body_multiplicities(
449    core_trace_data: &mut [Felt],
450    num_rows: usize,
451) -> Result<(), ExecutionError> {
452    if num_rows < 2 {
453        return Ok(());
454    }
455
456    let mut loop_body_counts = BTreeMap::<u64, u64>::new();
457    let width = CORE_STORAGE_WIDTH;
458
459    for row_idx in 0..num_rows - 1 {
460        let row: &CoreCols<Felt> = core_trace_data[row_idx * width..(row_idx + 1) * width].borrow();
461
462        if decode_opcode(&row.decoder.op_bits) != opcodes::END
463            || row.decoder.end_block_flags().is_loop_body != ONE
464        {
465            continue;
466        }
467
468        let next: &CoreCols<Felt> =
469            core_trace_data[(row_idx + 1) * width..(row_idx + 2) * width].borrow();
470        let loop_addr = next.decoder.addr.as_canonical_u64();
471        *loop_body_counts.entry(loop_addr).or_insert(0) += 1;
472    }
473
474    for row_idx in 0..num_rows - 1 {
475        let is_loop = {
476            let row: &CoreCols<Felt> =
477                core_trace_data[row_idx * width..(row_idx + 1) * width].borrow();
478            decode_opcode(&row.decoder.op_bits) == opcodes::LOOP
479        };
480        if !is_loop {
481            continue;
482        }
483
484        let loop_addr = {
485            let next: &CoreCols<Felt> =
486                core_trace_data[(row_idx + 1) * width..(row_idx + 2) * width].borrow();
487            next.decoder.addr.as_canonical_u64()
488        };
489        let body_count = loop_body_counts
490            .get(&loop_addr)
491            .copied()
492            .ok_or(ExecutionError::Internal("dynamic LOOP has no matching body END"))?;
493
494        let row: &mut CoreCols<Felt> =
495            core_trace_data[row_idx * width..(row_idx + 1) * width].borrow_mut();
496        row.decoder.group_count = Felt::new_unchecked(body_count);
497    }
498
499    Ok(())
500}
501
502fn decode_opcode(op_bits: &[Felt; NUM_OP_BITS]) -> u8 {
503    let mut opcode = 0u8;
504    for (idx, bit) in op_bits.iter().enumerate() {
505        opcode |= ((bit.as_canonical_u64() & 1) as u8) << idx;
506    }
507    opcode
508}
509
510/// Initializing the first row of each fragment with the appropriate stack and system state.
511///
512/// This needs to be done as a separate pass after all fragments have been generated, because the
513/// system and stack rows write the state at clk `i` to the row at index `i+1`. Hence, the state of
514/// the last row of any given fragment cannot be written in parallel, since any given fragment
515/// filler doesn't have access to the next fragment's first row.
516fn fixup_stack_and_system_rows(
517    core_trace_data: &mut [Felt],
518    fragment_size: usize,
519    stack_rows: &[StackCols<Felt>],
520    system_rows: &[SystemCols<Felt>],
521    first_stack_top: &[Felt],
522) {
523    const MIN_STACK_DEPTH_FELT: Felt = Felt::new_unchecked(MIN_STACK_DEPTH as u64);
524    let w = CORE_STORAGE_WIDTH;
525
526    {
527        let row: &mut CoreCols<Felt> = core_trace_data[..w].borrow_mut();
528
529        // Stack order in the trace is reversed vs `first_stack_top`.
530        for (stack_col_idx, &value) in first_stack_top.iter().rev().enumerate() {
531            row.stack.top[stack_col_idx] = value;
532        }
533
534        row.stack.b0 = MIN_STACK_DEPTH_FELT;
535        row.stack.b1 = ZERO;
536        row.stack.h0 = ZERO;
537    }
538
539    let total_rows = core_trace_data.len() / w;
540    let num_fragments = total_rows / fragment_size;
541
542    for frag_idx in 1..num_fragments {
543        let row_idx = frag_idx * fragment_size;
544        let row_start = row_idx * w;
545        let row: &mut CoreCols<Felt> = core_trace_data[row_start..row_start + w].borrow_mut();
546        row.system = system_rows[frag_idx - 1].clone();
547        row.stack = stack_rows[frag_idx - 1].clone();
548    }
549}
550
551/// Appends a HALT row (`num_rows_before` is the row count before append).
552///
553/// This ensures that the trace ends with at least one HALT operation, which is necessary to satisfy
554/// the constraints.
555fn push_halt_opcode_row(
556    core_trace_data: &mut Vec<Felt>,
557    num_rows_before: usize,
558    last_system_state: &SystemCols<Felt>,
559    last_stack_state: &StackCols<Felt>,
560) {
561    let w = CORE_STORAGE_WIDTH;
562    let mut row_data = [ZERO; CORE_STORAGE_WIDTH];
563
564    // Read the previous row's hasher state first half before we take a mutable borrow on
565    // `row_data` (propagates the program hash into the HALT padding).
566    let prev_hasher_state_first_half: [Felt; 4] = if num_rows_before > 0 {
567        let last_row_start = (num_rows_before - 1) * w;
568        let prev: &CoreCols<Felt> = core_trace_data[last_row_start..last_row_start + w].borrow();
569        let hs = &prev.decoder.hasher_state;
570        [hs[0], hs[1], hs[2], hs[3]]
571    } else {
572        [ZERO; 4]
573    };
574
575    {
576        let row: &mut CoreCols<Felt> = row_data.as_mut_slice().borrow_mut();
577
578        row.system = last_system_state.clone();
579        row.stack = last_stack_state.clone();
580
581        // Pad op_bits columns with HALT opcode bits
582        let halt_opcode = opcodes::HALT;
583        for bit_idx in 0..NUM_OP_BITS {
584            row.decoder.op_bits[bit_idx] = Felt::from_u8((halt_opcode >> bit_idx) & 1);
585        }
586
587        // Pad hasher state columns (8 columns)
588        // - First 4 columns: copy the last value (to propagate program hash)
589        // - Remaining 4 columns: fill with ZEROs
590        row.decoder.hasher_state[..4].copy_from_slice(&prev_hasher_state_first_half);
591
592        // Pad op_bit_extra columns (2 columns)
593        // - First column: do nothing (pre-filled with ZEROs, HALT doesn't use this)
594        // - Second column: fill with ONEs (product of two most significant HALT bits, both are 1)
595        row.decoder.extra[1] = ONE;
596    }
597
598    core_trace_data.extend_from_slice(&row_data);
599}
600
601/// Initializes the ranger checker from the recorded range checks during execution and returns it.
602///
603/// Note that the maximum number of rows that the range checker can produce is 2^16, which is less
604/// than the maximum trace length (2^29). Hence, we can safely generate the entire range checker
605/// trace and then pad it to the final trace length, without worrying about hitting memory limits.
606fn initialize_range_checker(
607    range_checker_replay: RangeCheckerReplay,
608    chiplets: &Chiplets,
609) -> RangeChecker {
610    let mut range_checker = RangeChecker::new();
611
612    // Add all range checks recorded during execution.
613    for values in range_checker_replay {
614        range_checker.add_range_checks(values.as_ref());
615    }
616
617    // Add all hasher- and memory-related range checks.
618    chiplets.append_range_checks(&mut range_checker);
619
620    range_checker
621}
622
623/// Replays recorded operations to populate chiplet traces. Results were already used during
624/// execution; this pass only needs the trace-recording side effects.
625///
626/// The five chiplets are populated from disjoint replays, so they build in parallel. Their
627/// non-hasher lengths are known from the replay metadata; checking those up front and giving the
628/// hasher only the remaining rows preserves the hard cap before any builder materializes its
629/// trace on the buffered path. A prebuilt (streamed) hasher was already built during execution
630/// under the full `max_trace_len` budget and is instead validated against the remaining rows
631/// after the fact.
632fn initialize_chiplets(
633    kernel: KernelDescriptor,
634    core_trace_contexts: &[CoreTraceFragmentContext],
635    memory_writes: MemoryWritesReplay,
636    bitwise: BitwiseReplay,
637    kernel_replay: KernelReplay,
638    hasher_for_chiplet: HasherRequestReplay,
639    prebuilt_hasher: Option<Hasher>,
640    ace_replay: AceReplay,
641    mast_forest_store: &[Arc<SparseMastForest>],
642    max_trace_len: usize,
643) -> Result<Chiplets, ExecutionError> {
644    let non_hasher_trace_len = non_hasher_trace_len(
645        &kernel,
646        core_trace_contexts,
647        &memory_writes,
648        &bitwise,
649        &ace_replay,
650        max_trace_len,
651    )?;
652    let max_hasher_trace_len = max_trace_len
653        .checked_sub(non_hasher_trace_len)
654        .ok_or(ExecutionError::TraceLenExceeded(max_trace_len))?;
655
656    if prebuilt_hasher
657        .as_ref()
658        .is_some_and(|hasher| hasher.trace_len() > max_hasher_trace_len)
659    {
660        return Err(ExecutionError::TraceLenExceeded(max_trace_len));
661    }
662
663    let (hasher, (bitwise, (memory, (ace, kernel_rom)))) = rayon::join(
664        || match prebuilt_hasher {
665            Some(hasher) => Ok(hasher),
666            None => build_hasher_chiplet(
667                hasher_for_chiplet.into_resolved_ops(mast_forest_store),
668                max_hasher_trace_len,
669            )
670            .map_err(|err| match err {
671                // The builder reports its internal remainder budget; surface the
672                // configured cap instead, like every other rejection site.
673                ExecutionError::TraceLenExceeded(_) => {
674                    ExecutionError::TraceLenExceeded(max_trace_len)
675                },
676                other => other,
677            }),
678        },
679        || {
680            rayon::join(
681                || build_bitwise_chiplet(bitwise, max_trace_len),
682                || {
683                    rayon::join(
684                        || build_memory_chiplet(memory_writes, core_trace_contexts, max_trace_len),
685                        || {
686                            rayon::join(
687                                || build_ace_chiplet(ace_replay, max_trace_len),
688                                || build_kernel_rom_chiplet(kernel, kernel_replay, max_trace_len),
689                            )
690                        },
691                    )
692                },
693            )
694        },
695    );
696
697    let chiplets = Chiplets {
698        hasher: hasher?,
699        bitwise: bitwise?,
700        memory: memory?,
701        ace: ace?,
702        kernel_rom: kernel_rom?,
703    };
704    debug_assert_eq!(
705        non_hasher_trace_len,
706        chiplets.trace_len() - chiplets.hasher.trace_len(),
707        "chiplet preflight length differs from the materialized trace",
708    );
709    // Release-only insurance: in debug builds a preflight undercount trips the
710    // assert above before this check can fire.
711    if chiplets.trace_len() > max_trace_len {
712        return Err(ExecutionError::TraceLenExceeded(max_trace_len));
713    }
714    Ok(chiplets)
715}
716
717fn non_hasher_trace_len(
718    kernel: &KernelDescriptor,
719    core_trace_contexts: &[CoreTraceFragmentContext],
720    memory_writes: &MemoryWritesReplay,
721    bitwise: &BitwiseReplay,
722    ace: &AceReplay,
723    max_trace_len: usize,
724) -> Result<usize, ExecutionError> {
725    let overflow = || ExecutionError::TraceLenExceeded(max_trace_len);
726    let bitwise_len = bitwise.num_operations().checked_mul(OP_CYCLE_LEN).ok_or_else(overflow)?;
727    let memory_reads_len = core_trace_contexts.iter().try_fold(0usize, |len, context| {
728        len.checked_add(context.replay.memory_reads.num_accesses()?)
729    });
730    let memory_len = memory_writes
731        .num_accesses()
732        .and_then(|writes| memory_reads_len.and_then(|reads| writes.checked_add(reads)))
733        .ok_or_else(overflow)?;
734    let ace_len = ace.trace_len().ok_or_else(overflow)?;
735
736    [1, kernel.proc_hashes().len(), bitwise_len, memory_len, ace_len]
737        .into_iter()
738        .try_fold(0usize, usize::checked_add)
739        .filter(|&total| total <= max_trace_len)
740        .ok_or_else(overflow)
741}
742
743/// Builds the hasher chiplet by replaying resolved requests in order.
744///
745/// The iterator abstracts over the two delivery modes: the buffered replay drained against the
746/// finalized forest store, or a live channel fed by a concurrently executing processor (see
747/// `FastProcessor::execute_and_build_trace_sync`).
748pub(crate) fn build_hasher_chiplet<'a>(
749    ops: impl IntoIterator<Item = Result<ResolvedHasherOp<'a>, ExecutionError>>,
750    max_trace_len: usize,
751) -> Result<Hasher, ExecutionError> {
752    let mut hasher = Hasher::default();
753    for hasher_op in ops {
754        match hasher_op? {
755            ResolvedHasherOp::Permute(input_state) => {
756                let _ = hasher.permute(input_state);
757            },
758            ResolvedHasherOp::HashControlBlock((h1, h2, domain, expected_hash)) => {
759                let _ = hasher.hash_control_block(h1, h2, domain, expected_hash);
760            },
761            ResolvedHasherOp::HashBasicBlock((batch_groups, expected_hash)) => match batch_groups {
762                ResolvedBasicBlockGroups::Borrowed(op_batches) => {
763                    let _ = hasher
764                        .hash_basic_block(op_batches.iter().map(OpBatch::groups), expected_hash);
765                },
766                ResolvedBasicBlockGroups::Owned(batch_groups) => {
767                    let _ = hasher.hash_basic_block(batch_groups.iter(), expected_hash);
768                },
769            },
770            ResolvedHasherOp::BuildMerkleRoot((value, path, index)) => {
771                let _ = hasher.build_merkle_root(value, &path, index);
772            },
773            ResolvedHasherOp::UpdateMerkleRoot((old_value, new_value, path, index)) => {
774                hasher.update_merkle_root(old_value, new_value, &path, index);
775            },
776        }
777        if hasher.trace_len() > max_trace_len {
778            return Err(ExecutionError::TraceLenExceeded(max_trace_len));
779        }
780    }
781    Ok(hasher)
782}
783
784/// Builds the bitwise chiplet by replaying recorded `u32and`/`u32xor` requests in order.
785fn build_bitwise_chiplet(
786    bitwise_replay: BitwiseReplay,
787    max_trace_len: usize,
788) -> Result<Bitwise, ExecutionError> {
789    let mut bitwise = Bitwise::default();
790    for (bitwise_op, a, b) in bitwise_replay {
791        match bitwise_op {
792            BitwiseOp::U32And => {
793                bitwise.u32and(a, b).map_exec_err_no_ctx()?;
794            },
795            BitwiseOp::U32Xor => {
796                bitwise.u32xor(a, b).map_exec_err_no_ctx()?;
797            },
798        }
799        if bitwise.trace_len() > max_trace_len {
800            return Err(ExecutionError::TraceLenExceeded(max_trace_len));
801        }
802    }
803    Ok(bitwise)
804}
805
806/// Builds the memory chiplet by replaying recorded accesses merged in clock-cycle order.
807fn build_memory_chiplet(
808    memory_writes: MemoryWritesReplay,
809    core_trace_contexts: &[CoreTraceFragmentContext],
810    max_trace_len: usize,
811) -> Result<Memory, ExecutionError> {
812    enum MemoryAccess {
813        ReadElement(Felt, ContextId, RowIndex),
814        WriteElement(Felt, Felt, ContextId, RowIndex),
815        ReadWord(Felt, ContextId, RowIndex),
816        WriteWord(Felt, Word, ContextId, RowIndex),
817    }
818
819    impl MemoryAccess {
820        fn clk(&self) -> RowIndex {
821            match self {
822                MemoryAccess::ReadElement(_, _, clk) => *clk,
823                MemoryAccess::WriteElement(_, _, _, clk) => *clk,
824                MemoryAccess::ReadWord(_, _, clk) => *clk,
825                MemoryAccess::WriteWord(_, _, _, clk) => *clk,
826            }
827        }
828    }
829
830    let mut memory = Memory::default();
831
832    // Note: care is taken to order all the accesses by clock cycle, since the memory chiplet
833    // currently assumes that all memory accesses are issued in the same order as they appear in
834    // the trace.
835    let elements_written: Box<dyn Iterator<Item = MemoryAccess>> =
836        Box::new(memory_writes.iter_elements_written().map(|(element, addr, ctx, clk)| {
837            MemoryAccess::WriteElement(*addr, *element, *ctx, *clk)
838        }));
839    let words_written: Box<dyn Iterator<Item = MemoryAccess>> = Box::new(
840        memory_writes
841            .iter_words_written()
842            .map(|(word, addr, ctx, clk)| MemoryAccess::WriteWord(*addr, *word, *ctx, *clk)),
843    );
844    let elements_read: Box<dyn Iterator<Item = MemoryAccess>> =
845        Box::new(core_trace_contexts.iter().flat_map(|ctx| {
846            ctx.replay
847                .memory_reads
848                .iter_read_elements()
849                .map(|(_, addr, ctx, clk)| MemoryAccess::ReadElement(addr, ctx, clk))
850        }));
851    let words_read: Box<dyn Iterator<Item = MemoryAccess>> =
852        Box::new(core_trace_contexts.iter().flat_map(|ctx| {
853            ctx.replay
854                .memory_reads
855                .iter_read_words()
856                .map(|(_, addr, ctx, clk)| MemoryAccess::ReadWord(addr, ctx, clk))
857        }));
858
859    [elements_written, words_written, elements_read, words_read]
860        .into_iter()
861        .kmerge_by(|a, b| a.clk() < b.clk())
862        .try_for_each(|mem_access| {
863            match mem_access {
864                MemoryAccess::ReadElement(addr, ctx, clk) => memory
865                    .read(ctx, addr, clk)
866                    .map(|_| ())
867                    .map_err(ExecutionError::MemoryErrorNoCtx)?,
868                MemoryAccess::WriteElement(addr, element, ctx, clk) => memory
869                    .write(ctx, addr, clk, element)
870                    .map_err(ExecutionError::MemoryErrorNoCtx)?,
871                MemoryAccess::ReadWord(addr, ctx, clk) => memory
872                    .read_word(ctx, addr, clk)
873                    .map(|_| ())
874                    .map_err(ExecutionError::MemoryErrorNoCtx)?,
875                MemoryAccess::WriteWord(addr, word, ctx, clk) => memory
876                    .write_word(ctx, addr, clk, word)
877                    .map_err(ExecutionError::MemoryErrorNoCtx)?,
878            }
879            if memory.trace_len() > max_trace_len {
880                return Err(ExecutionError::TraceLenExceeded(max_trace_len));
881            }
882            Ok(())
883        })?;
884
885    Ok(memory)
886}
887
888/// Builds the ACE chiplet by replaying recorded circuit evaluations in order.
889fn build_ace_chiplet(ace_replay: AceReplay, max_trace_len: usize) -> Result<Ace, ExecutionError> {
890    let mut ace = Ace::default();
891    for (clk, circuit_eval) in ace_replay.into_iter() {
892        ace.add_circuit_evaluation(clk, circuit_eval);
893        if ace.trace_len() > max_trace_len {
894            return Err(ExecutionError::TraceLenExceeded(max_trace_len));
895        }
896    }
897    Ok(ace)
898}
899
900/// Builds the kernel ROM chiplet by replaying recorded kernel procedure accesses in order.
901fn build_kernel_rom_chiplet(
902    kernel: KernelDescriptor,
903    kernel_replay: KernelReplay,
904    max_trace_len: usize,
905) -> Result<KernelRom, ExecutionError> {
906    let mut kernel_rom = KernelRom::new(kernel);
907    for proc_hash in kernel_replay.into_iter() {
908        kernel_rom.access_proc(proc_hash).map_exec_err_no_ctx()?;
909        if kernel_rom.trace_len() > max_trace_len {
910            return Err(ExecutionError::TraceLenExceeded(max_trace_len));
911        }
912    }
913    Ok(kernel_rom)
914}
915
916/// Pads the core trace to `core_height` rows (HALT template, CLK incremented per row).
917fn pad_core_row_major(core_trace_data: &mut Vec<Felt>, core_height: usize) {
918    let w = CORE_STORAGE_WIDTH;
919    let total_program_rows = core_trace_data.len() / w;
920    assert!(total_program_rows <= core_height);
921    assert!(total_program_rows > 0);
922
923    let num_padding_rows = core_height - total_program_rows;
924    if num_padding_rows == 0 {
925        return;
926    }
927    let last_row_start = (total_program_rows - 1) * w;
928
929    // Safety: per our documented safety guarantees, we know that `total_program_rows > 0`,
930    // and row `total_program_rows - 1` is initialized.
931    let (last_hasher_first_half, last_stack): ([Felt; 4], StackCols<Felt>) = {
932        let last: &CoreCols<Felt> = core_trace_data[last_row_start..last_row_start + w].borrow();
933        let hs = &last.decoder.hasher_state;
934        let last_hasher: [Felt; 4] = [hs[0], hs[1], hs[2], hs[3]];
935        (last_hasher, last.stack.clone())
936    };
937
938    let mut template_data = [ZERO; CORE_STORAGE_WIDTH];
939    {
940        let template: &mut CoreCols<Felt> = template_data.as_mut_slice().borrow_mut();
941
942        // Decoder columns
943        // ------------------------
944
945        // Pad op_bits columns with HALT opcode bits
946        let halt_opcode = opcodes::HALT;
947        for i in 0..NUM_OP_BITS {
948            template.decoder.op_bits[i] = Felt::from_u8((halt_opcode >> i) & 1);
949        }
950        // Pad hasher state columns (8 columns)
951        // - First 4 columns: copy the last value (to propagate program hash)
952        // - Remaining 4 columns: fill with ZEROs
953        template.decoder.hasher_state[..4].copy_from_slice(&last_hasher_first_half);
954
955        // Pad op_bit_extra columns (2 columns)
956        // - First column: do nothing (filled with ZEROs, HALT doesn't use this)
957        // - Second column: fill with ONEs (product of two most significant HALT bits, both are 1)
958        template.decoder.extra[1] = ONE;
959
960        // Stack columns
961        // ------------------------
962
963        // Pad stack columns with the last value in each column (analogous to Stack::into_trace())
964        template.stack = last_stack;
965    }
966
967    // System columns
968    // ------------------------
969
970    // Pad CLK trace - fill with index values
971
972    let pad_start = total_program_rows * w;
973    core_trace_data.resize(pad_start + num_padding_rows * w, ZERO);
974    core_trace_data[pad_start..]
975        .par_chunks_mut(w)
976        .enumerate()
977        .for_each(|(idx, row_buf)| {
978            row_buf.copy_from_slice(&template_data);
979            let row: &mut CoreCols<Felt> = row_buf.borrow_mut();
980            row.system.clk = Felt::from_u32((total_program_rows + idx) as u32);
981        });
982}
983
984type SplitFragmentContext<'a> = (
985    ReplayProcessor,
986    CoreTraceGenerationTracer<'a>,
987    ContinuationStack<Arc<SparseMastForest>>,
988    Arc<SparseMastForest>,
989);
990
991/// Uses the provided `CoreTraceFragmentContext` to build and return a `ReplayProcessor` and
992/// `CoreTraceGenerationTracer` that can be used to execute the fragment.
993///
994/// `mast_forest_store` provides the [`SparseMastForest`]s that the indices stored in the fragment
995/// (the initial forest index and the `EnterForest` continuations) refer to.
996///
997/// # Errors
998///
999/// Returns [`ExecutionError::Internal`] if any [`MastForestId`] referenced by the fragment
1000/// (either `initial_mast_forest_id` or an `EnterForest` continuation) is out of range of
1001/// `mast_forest_store`. Because [`CoreTraceFragmentContext`] is attacker-controllable when fed in
1002/// from outside, we validate these indices rather than indexing-and-panicking.
1003fn split_trace_fragment_context<'a>(
1004    fragment_context: CoreTraceFragmentContext,
1005    writer: RowMajorTraceWriter<'a, Felt>,
1006    fragment_size: usize,
1007    mast_forest_store: &[Arc<SparseMastForest>],
1008    max_stack_depth: usize,
1009) -> Result<SplitFragmentContext<'a>, ExecutionError> {
1010    let CoreTraceFragmentContext {
1011        state: CoreTraceState { system, decoder, stack },
1012        replay:
1013            ExecutionReplay {
1014                block_stack: block_stack_replay,
1015                execution_context: execution_context_replay,
1016                stack_overflow: stack_overflow_replay,
1017                memory_reads: memory_reads_replay,
1018                advice: advice_replay,
1019                hasher: hasher_response_replay,
1020                block_address: block_address_replay,
1021                mast_forest_resolution: mast_forest_resolution_replay,
1022            },
1023        continuation,
1024        initial_mast_forest_id,
1025    } = fragment_context;
1026
1027    let translated_continuation =
1028        translate_snapshot_continuation_stack(continuation, mast_forest_store)?;
1029
1030    let initial_mast_forest =
1031        lookup_mast_forest(mast_forest_store, initial_mast_forest_id)?.clone();
1032
1033    let processor = ReplayProcessor::new(
1034        system,
1035        stack,
1036        stack_overflow_replay,
1037        execution_context_replay,
1038        advice_replay,
1039        memory_reads_replay,
1040        hasher_response_replay,
1041        mast_forest_resolution_replay,
1042        mast_forest_store.to_vec(),
1043        max_stack_depth,
1044        fragment_size.into(),
1045    );
1046    let tracer =
1047        CoreTraceGenerationTracer::new(writer, decoder, block_address_replay, block_stack_replay);
1048
1049    Ok((processor, tracer, translated_continuation, initial_mast_forest))
1050}
1051
1052/// Translates a snapshotted `ContinuationStack<MastForestId>` into one carrying actual
1053/// [`Arc<SparseMastForest>`] handles, ready to drive `execute_impl`.
1054///
1055/// Returns [`ExecutionError::Internal`] if any `EnterForest` continuation carries a
1056/// [`MastForestId`] that is out of range of `mast_forest_store`.
1057fn translate_snapshot_continuation_stack(
1058    snapshot: ContinuationStack<MastForestId>,
1059    mast_forest_store: &[Arc<SparseMastForest>],
1060) -> Result<ContinuationStack<Arc<SparseMastForest>>, ExecutionError> {
1061    let mut out: ContinuationStack<Arc<SparseMastForest>> = ContinuationStack::default();
1062    for cont in snapshot.into_inner() {
1063        let translated = match cont {
1064            Continuation::EnterForest {
1065                forest: id,
1066                package_debug_info,
1067                inline_context_depth,
1068            } => Continuation::EnterForest {
1069                forest: lookup_mast_forest(mast_forest_store, id)?.clone(),
1070                package_debug_info,
1071                inline_context_depth,
1072            },
1073            Continuation::StartNode(id) => Continuation::StartNode(id),
1074            Continuation::FinishJoin(id) => Continuation::FinishJoin(id),
1075            Continuation::FinishSplit(id) => Continuation::FinishSplit(id),
1076            Continuation::FinishLoop(node_id) => Continuation::FinishLoop(node_id),
1077            Continuation::FinishCall(id) => Continuation::FinishCall(id),
1078            Continuation::FinishDyn(id) => Continuation::FinishDyn(id),
1079            Continuation::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch } => {
1080                Continuation::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch }
1081            },
1082            Continuation::Respan { node_id, batch_index } => {
1083                Continuation::Respan { node_id, batch_index }
1084            },
1085            Continuation::FinishBasicBlock(id) => Continuation::FinishBasicBlock(id),
1086        };
1087        out.push_continuation(translated);
1088    }
1089    Ok(out)
1090}
1091
1092/// Looks up `id` in `mast_forest_store`, returning [`ExecutionError::Internal`] if it is out of
1093/// range.
1094pub(super) fn lookup_mast_forest(
1095    mast_forest_store: &[Arc<SparseMastForest>],
1096    id: MastForestId,
1097) -> Result<&Arc<SparseMastForest>, ExecutionError> {
1098    mast_forest_store
1099        .get(id.to_usize())
1100        .ok_or(ExecutionError::Internal("MastForestId out of range of mast_forest_store"))
1101}