cairo_vm/
cairo_run.rs

1use crate::{
2    hint_processor::hint_processor_definition::HintProcessor,
3    types::{
4        builtin_name::BuiltinName, layout::CairoLayoutParams, layout_name::LayoutName,
5        program::Program,
6    },
7    vm::{
8        errors::{
9            cairo_run_errors::CairoRunError, runner_errors::RunnerError, vm_exception::VmException,
10        },
11        runners::{cairo_pie::CairoPie, cairo_runner::CairoRunner},
12        security::verify_secure_runner,
13    },
14};
15
16use crate::Felt252;
17use bincode::enc::write::Writer;
18
19use thiserror::Error;
20
21use crate::types::exec_scope::ExecutionScopes;
22#[cfg(feature = "test_utils")]
23use arbitrary::{self, Arbitrary};
24
25#[cfg_attr(feature = "test_utils", derive(Arbitrary))]
26pub struct CairoRunConfig<'a> {
27    #[cfg_attr(feature = "test_utils", arbitrary(value = "main"))]
28    pub entrypoint: &'a str,
29    pub trace_enabled: bool,
30    pub relocate_mem: bool,
31    pub layout: LayoutName,
32    /// The `dynamic_layout_params` argument should only be used with dynamic layout.
33    /// It is ignored otherwise.
34    pub dynamic_layout_params: Option<CairoLayoutParams>,
35    pub proof_mode: bool,
36    pub secure_run: Option<bool>,
37    /// Disable padding of the trace.
38    /// By default, the trace is padded to accommodate the expected builtins-n_steps relationships
39    /// according to the layout.
40    /// When the padding is disabled:
41    /// - It doesn't modify/pad n_steps.
42    /// - It still pads each builtin segment to the next power of 2 (w.r.t the number of used
43    ///   instances of the builtin) compared to their sizes at the end of the execution.
44    pub disable_trace_padding: bool,
45    pub allow_missing_builtins: Option<bool>,
46}
47
48impl Default for CairoRunConfig<'_> {
49    fn default() -> Self {
50        CairoRunConfig {
51            entrypoint: "main",
52            trace_enabled: false,
53            relocate_mem: false,
54            layout: LayoutName::plain,
55            proof_mode: false,
56            secure_run: None,
57            disable_trace_padding: false,
58            allow_missing_builtins: None,
59            dynamic_layout_params: None,
60        }
61    }
62}
63
64#[allow(clippy::result_large_err)]
65/// Runs a program with a customized execution scope.
66pub fn cairo_run_program_with_initial_scope(
67    program: &Program,
68    cairo_run_config: &CairoRunConfig,
69    hint_processor: &mut dyn HintProcessor,
70    exec_scopes: ExecutionScopes,
71) -> Result<CairoRunner, CairoRunError> {
72    let secure_run = cairo_run_config
73        .secure_run
74        .unwrap_or(!cairo_run_config.proof_mode);
75
76    let allow_missing_builtins = cairo_run_config
77        .allow_missing_builtins
78        .unwrap_or(cairo_run_config.proof_mode);
79
80    let mut cairo_runner = CairoRunner::new(
81        program,
82        cairo_run_config.layout,
83        cairo_run_config.dynamic_layout_params.clone(),
84        cairo_run_config.proof_mode,
85        cairo_run_config.trace_enabled,
86        cairo_run_config.disable_trace_padding,
87    )?;
88
89    cairo_runner.exec_scopes = exec_scopes;
90
91    let end = cairo_runner.initialize(allow_missing_builtins)?;
92    // check step calculation
93
94    cairo_runner
95        .run_until_pc(end, hint_processor)
96        .map_err(|err| VmException::from_vm_error(&cairo_runner, err))?;
97
98    if cairo_run_config.proof_mode {
99        // we run an additional step to ensure that `end` is the last step execute,
100        // rather than the one after it.
101        cairo_runner.run_for_steps(1, hint_processor)?;
102    }
103    cairo_runner.end_run(
104        cairo_run_config.disable_trace_padding,
105        false,
106        hint_processor,
107    )?;
108
109    cairo_runner.vm.verify_auto_deductions()?;
110    cairo_runner.read_return_values(allow_missing_builtins)?;
111    if cairo_run_config.proof_mode {
112        cairo_runner.finalize_segments()?;
113    }
114    if secure_run {
115        verify_secure_runner(&cairo_runner, true, None)?;
116    }
117    cairo_runner.relocate(cairo_run_config.relocate_mem)?;
118
119    Ok(cairo_runner)
120}
121
122#[allow(clippy::result_large_err)]
123pub fn cairo_run_program(
124    program: &Program,
125    cairo_run_config: &CairoRunConfig,
126    hint_processor: &mut dyn HintProcessor,
127) -> Result<CairoRunner, CairoRunError> {
128    cairo_run_program_with_initial_scope(
129        program,
130        cairo_run_config,
131        hint_processor,
132        ExecutionScopes::new(),
133    )
134}
135
136#[allow(clippy::result_large_err)]
137pub fn cairo_run(
138    program_content: &[u8],
139    cairo_run_config: &CairoRunConfig,
140    hint_processor: &mut dyn HintProcessor,
141) -> Result<CairoRunner, CairoRunError> {
142    let program = Program::from_bytes(program_content, Some(cairo_run_config.entrypoint))?;
143
144    cairo_run_program(&program, cairo_run_config, hint_processor)
145}
146
147#[allow(clippy::result_large_err)]
148/// Runs a Cairo PIE generated by a previous cairo execution
149/// To generate a cairo pie use the runner's method `get_cairo_pie`
150/// Note: Cairo PIEs cannot be ran in proof_mode
151/// WARNING: As the RunResources are part of the HintProcessor trait, the caller should make sure that
152/// the number of steps in the `RunResources` matches that of the `ExecutionResources` in the `CairoPie`.
153/// An error will be returned if this doesn't hold.
154pub fn cairo_run_pie(
155    pie: &CairoPie,
156    cairo_run_config: &CairoRunConfig,
157    hint_processor: &mut dyn HintProcessor,
158) -> Result<CairoRunner, CairoRunError> {
159    if cairo_run_config.proof_mode {
160        return Err(RunnerError::CairoPieProofMode.into());
161    }
162    if hint_processor
163        .get_n_steps()
164        .is_none_or(|steps| steps != pie.execution_resources.n_steps)
165    {
166        return Err(RunnerError::PieNStepsVsRunResourcesNStepsMismatch.into());
167    }
168    pie.run_validity_checks()?;
169    let secure_run = cairo_run_config.secure_run.unwrap_or(true);
170
171    let allow_missing_builtins = cairo_run_config.allow_missing_builtins.unwrap_or_default();
172
173    let program = Program::from_stripped_program(&pie.metadata.program);
174    let mut cairo_runner = CairoRunner::new(
175        &program,
176        cairo_run_config.layout,
177        cairo_run_config.dynamic_layout_params.clone(),
178        false,
179        cairo_run_config.trace_enabled,
180        cairo_run_config.disable_trace_padding,
181    )?;
182
183    let end = cairo_runner.initialize(allow_missing_builtins)?;
184    cairo_runner.vm.finalize_segments_by_cairo_pie(pie);
185    // Load builtin additional data
186    for (name, data) in pie.additional_data.0.iter() {
187        // Data is not trusted in secure_run, therefore we skip extending the hash builtin's data
188        if matches!(name, BuiltinName::pedersen) && secure_run {
189            continue;
190        }
191        if let Some(builtin) = cairo_runner
192            .vm
193            .builtin_runners
194            .iter_mut()
195            .find(|b| b.name() == *name)
196        {
197            builtin.extend_additional_data(data)?;
198        }
199    }
200    // Load previous execution memory
201    let has_zero_segment = cairo_runner.vm.segments.has_zero_segment() as usize;
202    let n_extra_segments = pie.metadata.extra_segments.len() - has_zero_segment;
203    cairo_runner
204        .vm
205        .segments
206        .load_pie_memory(&pie.memory, n_extra_segments)?;
207
208    cairo_runner
209        .run_until_pc(end, hint_processor)
210        .map_err(|err| VmException::from_vm_error(&cairo_runner, err))?;
211
212    cairo_runner.end_run(
213        cairo_run_config.disable_trace_padding,
214        false,
215        hint_processor,
216    )?;
217
218    cairo_runner.vm.verify_auto_deductions()?;
219    cairo_runner.read_return_values(allow_missing_builtins)?;
220
221    if secure_run {
222        verify_secure_runner(&cairo_runner, true, None)?;
223        // Check that the Cairo PIE produced by this run is compatible with the Cairo PIE received
224        cairo_runner.get_cairo_pie()?.check_pie_compatibility(pie)?;
225    }
226    cairo_runner.relocate(cairo_run_config.relocate_mem)?;
227
228    Ok(cairo_runner)
229}
230
231#[cfg(feature = "test_utils")]
232#[allow(clippy::result_large_err)]
233pub fn cairo_run_fuzzed_program(
234    program: Program,
235    cairo_run_config: &CairoRunConfig,
236    hint_processor: &mut dyn HintProcessor,
237    steps_limit: usize,
238) -> Result<CairoRunner, CairoRunError> {
239    use crate::vm::errors::vm_errors::VirtualMachineError;
240
241    let secure_run = cairo_run_config
242        .secure_run
243        .unwrap_or(!cairo_run_config.proof_mode);
244
245    let allow_missing_builtins = cairo_run_config
246        .allow_missing_builtins
247        .unwrap_or(cairo_run_config.proof_mode);
248
249    let mut cairo_runner = CairoRunner::new(
250        &program,
251        cairo_run_config.layout,
252        cairo_run_config.dynamic_layout_params.clone(),
253        cairo_run_config.proof_mode,
254        cairo_run_config.trace_enabled,
255        cairo_run_config.disable_trace_padding,
256    )?;
257
258    let _end = cairo_runner.initialize(allow_missing_builtins)?;
259
260    let res = match cairo_runner.run_until_steps(steps_limit, hint_processor) {
261        Err(VirtualMachineError::EndOfProgram(_remaining)) => Ok(()), // program ran OK but ended before steps limit
262        res => res,
263    };
264
265    res.map_err(|err| VmException::from_vm_error(&cairo_runner, err))?;
266
267    cairo_runner.end_run(false, false, hint_processor)?;
268
269    cairo_runner.vm.verify_auto_deductions()?;
270    cairo_runner.read_return_values(allow_missing_builtins)?;
271    if cairo_run_config.proof_mode {
272        cairo_runner.finalize_segments()?;
273    }
274    if secure_run {
275        verify_secure_runner(&cairo_runner, true, None)?;
276    }
277    cairo_runner.relocate(cairo_run_config.relocate_mem)?;
278
279    Ok(cairo_runner)
280}
281
282#[derive(Debug, Error)]
283#[error("Failed to encode trace at position {0}, serialize error: {1}")]
284pub struct EncodeTraceError(usize, bincode::error::EncodeError);
285
286/// Writes the trace binary representation.
287///
288/// Bincode encodes to little endian by default and each trace entry is composed of
289/// 3 usize values that are padded to always reach 64 bit size.
290pub fn write_encoded_trace(
291    relocated_trace: &[crate::vm::trace::trace_entry::RelocatedTraceEntry],
292    dest: &mut impl Writer,
293) -> Result<(), EncodeTraceError> {
294    for (i, entry) in relocated_trace.iter().enumerate() {
295        dest.write(&((entry.ap as u64).to_le_bytes()))
296            .map_err(|e| EncodeTraceError(i, e))?;
297        dest.write(&((entry.fp as u64).to_le_bytes()))
298            .map_err(|e| EncodeTraceError(i, e))?;
299        dest.write(&((entry.pc as u64).to_le_bytes()))
300            .map_err(|e| EncodeTraceError(i, e))?;
301    }
302
303    Ok(())
304}
305
306/// Writes a binary representation of the relocated memory.
307///
308/// The memory pairs (address, value) are encoded and concatenated:
309/// * address -> 8-byte encoded
310/// * value -> 32-byte encoded
311pub fn write_encoded_memory(
312    relocated_memory: &[Option<Felt252>],
313    dest: &mut impl Writer,
314) -> Result<(), EncodeTraceError> {
315    for (i, memory_cell) in relocated_memory.iter().enumerate() {
316        match memory_cell {
317            None => continue,
318            Some(unwrapped_memory_cell) => {
319                dest.write(&(i as u64).to_le_bytes())
320                    .map_err(|e| EncodeTraceError(i, e))?;
321                dest.write(&unwrapped_memory_cell.to_bytes_le())
322                    .map_err(|e| EncodeTraceError(i, e))?;
323            }
324        }
325    }
326
327    Ok(())
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::stdlib::prelude::*;
334    use crate::vm::runners::cairo_runner::RunResources;
335    use crate::Felt252;
336    use crate::{
337        hint_processor::{
338            builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor,
339            hint_processor_definition::HintProcessor,
340        },
341        utils::test_utils::*,
342    };
343    use bincode::enc::write::SliceWriter;
344
345    use rstest::rstest;
346    #[cfg(target_arch = "wasm32")]
347    use wasm_bindgen_test::*;
348
349    #[allow(clippy::result_large_err)]
350    fn run_test_program(
351        program_content: &[u8],
352        hint_processor: &mut dyn HintProcessor,
353    ) -> Result<CairoRunner, CairoRunError> {
354        let program = Program::from_bytes(program_content, Some("main")).unwrap();
355        let mut cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true);
356        let end = cairo_runner
357            .initialize(false)
358            .map_err(CairoRunError::Runner)?;
359
360        assert!(cairo_runner.run_until_pc(end, hint_processor).is_ok());
361
362        Ok(cairo_runner)
363    }
364
365    #[test]
366    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
367    fn cairo_run_custom_entry_point() {
368        let program = Program::from_bytes(
369            include_bytes!("../../cairo_programs/not_main.json"),
370            Some("not_main"),
371        )
372        .unwrap();
373        let mut hint_processor = BuiltinHintProcessor::new_empty();
374        let mut cairo_runner = cairo_runner!(program);
375
376        let end = cairo_runner.initialize(false).unwrap();
377        assert!(cairo_runner.run_until_pc(end, &mut hint_processor).is_ok());
378        assert!(cairo_runner.relocate(true).is_ok());
379        // `main` returns without doing nothing, but `not_main` sets `[ap]` to `1`
380        // Memory location was found empirically and simply hardcoded
381        assert_eq!(cairo_runner.relocated_memory[2], Some(Felt252::from(123)));
382    }
383
384    #[test]
385    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
386    fn cairo_run_with_no_data_program() {
387        // a compiled program with no `data` key.
388        // it should fail when the program is loaded.
389        let mut hint_processor = BuiltinHintProcessor::new_empty();
390        let no_data_program_path =
391            include_bytes!("../../cairo_programs/manually_compiled/no_data_program.json");
392        let cairo_run_config = CairoRunConfig::default();
393        assert!(cairo_run(no_data_program_path, &cairo_run_config, &mut hint_processor,).is_err());
394    }
395
396    #[test]
397    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
398    fn cairo_run_with_no_main_program() {
399        // a compiled program with no main scope
400        // it should fail when trying to run initialize_main_entrypoint.
401        let mut hint_processor = BuiltinHintProcessor::new_empty();
402        let no_main_program =
403            include_bytes!("../../cairo_programs/manually_compiled/no_main_program.json");
404        let cairo_run_config = CairoRunConfig::default();
405        assert!(cairo_run(no_main_program, &cairo_run_config, &mut hint_processor,).is_err());
406    }
407
408    #[test]
409    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
410    fn cairo_run_with_invalid_memory() {
411        // the program invalid_memory.json has an invalid memory cell and errors when trying to
412        // decode the instruction.
413        let mut hint_processor = BuiltinHintProcessor::new_empty();
414        let invalid_memory =
415            include_bytes!("../../cairo_programs/manually_compiled/invalid_memory.json");
416        let cairo_run_config = CairoRunConfig::default();
417        assert!(cairo_run(invalid_memory, &cairo_run_config, &mut hint_processor,).is_err());
418    }
419
420    #[test]
421    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
422    fn write_output_program() {
423        let program_content = include_bytes!("../../cairo_programs/bitwise_output.json");
424        let mut hint_processor = BuiltinHintProcessor::new_empty();
425        let mut runner = run_test_program(program_content, &mut hint_processor)
426            .expect("Couldn't initialize cairo runner");
427
428        let mut output_buffer = String::new();
429        runner.vm.write_output(&mut output_buffer).unwrap();
430        assert_eq!(&output_buffer, "0\n");
431    }
432
433    #[test]
434    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
435    fn write_binary_trace_file() {
436        let program_content = include_bytes!("../../cairo_programs/struct.json");
437        let expected_encoded_trace =
438            include_bytes!("../../cairo_programs/trace_memory/cairo_trace_struct");
439
440        // run test program until the end
441        let mut hint_processor = BuiltinHintProcessor::new_empty();
442        let mut cairo_runner = run_test_program(program_content, &mut hint_processor).unwrap();
443
444        assert!(cairo_runner.relocate(false).is_ok());
445
446        let trace_entries = cairo_runner.relocated_trace.unwrap();
447        let mut buffer = [0; 24];
448        let mut buff_writer = SliceWriter::new(&mut buffer);
449        // write cairo_rs vm trace file
450        write_encoded_trace(&trace_entries, &mut buff_writer).unwrap();
451
452        // compare that the original cairo vm trace file and cairo_rs vm trace files are equal
453        assert_eq!(buffer, *expected_encoded_trace);
454    }
455
456    #[test]
457    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
458    fn write_binary_memory_file() {
459        let program_content = include_bytes!("../../cairo_programs/struct.json");
460        let expected_encoded_memory =
461            include_bytes!("../../cairo_programs/trace_memory/cairo_memory_struct");
462
463        // run test program until the end
464        let mut hint_processor = BuiltinHintProcessor::new_empty();
465        let mut cairo_runner = run_test_program(program_content, &mut hint_processor).unwrap();
466
467        // relocate memory so we can dump it to file
468        assert!(cairo_runner.relocate(true).is_ok());
469
470        let mut buffer = [0; 120];
471        let mut buff_writer = SliceWriter::new(&mut buffer);
472        // write cairo_rs vm memory file
473        write_encoded_memory(&cairo_runner.relocated_memory, &mut buff_writer).unwrap();
474
475        // compare that the original cairo vm memory file and cairo_rs vm memory files are equal
476        assert_eq!(*expected_encoded_memory, buffer);
477    }
478
479    #[test]
480    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
481    fn run_with_no_trace() {
482        let program = Program::from_bytes(
483            include_bytes!("../../cairo_programs/struct.json"),
484            Some("main"),
485        )
486        .unwrap();
487
488        let mut hint_processor = BuiltinHintProcessor::new_empty();
489        let mut cairo_runner = cairo_runner!(program);
490        let end = cairo_runner.initialize(false).unwrap();
491        assert!(cairo_runner.run_until_pc(end, &mut hint_processor).is_ok());
492        assert!(cairo_runner.relocate(false).is_ok());
493        assert!(cairo_runner.relocated_trace.is_none());
494    }
495
496    #[rstest]
497    #[case(include_bytes!("../../cairo_programs/fibonacci.json"))]
498    #[case(include_bytes!("../../cairo_programs/integration.json"))]
499    #[case(include_bytes!("../../cairo_programs/common_signature.json"))]
500    #[case(include_bytes!("../../cairo_programs/relocate_segments.json"))]
501    #[case(include_bytes!("../../cairo_programs/ec_op.json"))]
502    #[case(include_bytes!("../../cairo_programs/bitwise_output.json"))]
503    #[case(include_bytes!("../../cairo_programs/value_beyond_segment.json"))]
504    fn get_and_run_cairo_pie(#[case] program_content: &[u8]) {
505        let cairo_run_config = CairoRunConfig {
506            layout: LayoutName::starknet_with_keccak,
507            ..Default::default()
508        };
509        // First run program to get Cairo PIE
510        let cairo_pie = {
511            let runner = cairo_run(
512                program_content,
513                &cairo_run_config,
514                &mut BuiltinHintProcessor::new_empty(),
515            )
516            .unwrap();
517            runner.get_cairo_pie().unwrap()
518        };
519        let mut hint_processor = BuiltinHintProcessor::new(
520            Default::default(),
521            RunResources::new(cairo_pie.execution_resources.n_steps),
522        );
523        // Default config runs with secure_run, which checks that the Cairo PIE produced by this run is compatible with the one received
524        assert!(cairo_run_pie(&cairo_pie, &cairo_run_config, &mut hint_processor).is_ok());
525    }
526
527    #[test]
528    fn cairo_run_pie_n_steps_not_set() {
529        // First run program to get Cairo PIE
530        let cairo_pie = {
531            let runner = cairo_run(
532                include_bytes!("../../cairo_programs/fibonacci.json"),
533                &CairoRunConfig::default(),
534                &mut BuiltinHintProcessor::new_empty(),
535            )
536            .unwrap();
537            runner.get_cairo_pie().unwrap()
538        };
539        // Run Cairo PIE
540        let res = cairo_run_pie(
541            &cairo_pie,
542            &CairoRunConfig::default(),
543            &mut BuiltinHintProcessor::new_empty(),
544        );
545        assert!(res.is_err_and(|err| matches!(
546            err,
547            CairoRunError::Runner(RunnerError::PieNStepsVsRunResourcesNStepsMismatch)
548        )));
549    }
550}