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