From 2417b96bcdc45b64521f43aed31f1e6428a0fff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Sat, 21 Jan 2023 23:02:23 +0100 Subject: [PATCH] feat: add VM hooks as rust conditional feature doc: improve doc support for optional features --- Cargo.toml | 7 +- Makefile | 2 +- .../builtin_hint_processor/mod.rs | 2 + .../skip_next_instruction.rs | 12 +- src/hint_processor/hint_processor_utils.rs | 4 +- src/lib.rs | 7 + src/vm/hooks.rs | 199 ++++++++++++++++++ src/vm/mod.rs | 4 + src/vm/runners/cairo_runner.rs | 42 +++- src/vm/vm_core.rs | 25 ++- 10 files changed, 283 insertions(+), 21 deletions(-) create mode 100644 src/vm/hooks.rs diff --git a/Cargo.toml b/Cargo.toml index 93edba707d..e1cc94871a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,11 @@ description = "Blazing fast Cairo interpreter" [features] default = ["with_mimalloc"] with_mimalloc = ["mimalloc"] -skip_next_instruction_hint = [] # This feature will reference every test-oriented feature. # Note that these features are not retro-compatible with the cairo Python VM. -test_utils = ["skip_next_instruction_hint"] +test_utils = ["skip_next_instruction_hint", "hooks"] +skip_next_instruction_hint = [] +hooks = [] [dependencies] mimalloc = { version = "0.1.29", default-features = false, optional = true } @@ -34,7 +35,7 @@ sha3 = "0.10.1" rand_core = "0.6.4" lazy_static = "1.4.0" nom = "7" -sha2 = {version="0.10.2", features=["compress"]} +sha2 = { version = "0.10.2", features = ["compress"] } thiserror = "1.0.32" generic-array = "0.14.6" keccak = "0.1.2" diff --git a/Makefile b/Makefile index 08f9f0a509..5d8285871a 100644 --- a/Makefile +++ b/Makefile @@ -148,7 +148,7 @@ compare_memory_proof: $(CAIRO_RS_MEM_PROOF) $(CAIRO_MEM_PROOF) cd tests; ./compare_vm_state.sh memory proof_mode docs: - cargo doc --verbose --release --locked --no-deps + RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --verbose --release --locked --no-deps --all-features --open clean: rm -f $(TEST_DIR)/*.json diff --git a/src/hint_processor/builtin_hint_processor/mod.rs b/src/hint_processor/builtin_hint_processor/mod.rs index 8143abcd05..32ff08b4f4 100644 --- a/src/hint_processor/builtin_hint_processor/mod.rs +++ b/src/hint_processor/builtin_hint_processor/mod.rs @@ -17,6 +17,8 @@ pub mod segments; pub mod set; pub mod sha256_utils; pub mod signature; +#[cfg(feature = "skip_next_instruction_hint")] +#[cfg_attr(docsrs, doc(cfg(feature = "skip_next_instruction_hint")))] pub mod skip_next_instruction; pub mod squash_dict_utils; pub mod uint256_utils; diff --git a/src/hint_processor/builtin_hint_processor/skip_next_instruction.rs b/src/hint_processor/builtin_hint_processor/skip_next_instruction.rs index f60fa3eab0..62ab998697 100644 --- a/src/hint_processor/builtin_hint_processor/skip_next_instruction.rs +++ b/src/hint_processor/builtin_hint_processor/skip_next_instruction.rs @@ -1,14 +1,10 @@ -#[cfg(feature = "skip_next_instruction_hint")] use crate::vm::errors::hint_errors::HintError; -#[cfg(feature = "skip_next_instruction_hint")] use crate::vm::vm_core::VirtualMachine; -/* -This hint doesn't belong to the Cairo common library -It's only added for testing proposes -*/ - -#[cfg(feature = "skip_next_instruction_hint")] +/// Prevent the execution of the next instruction +/// +/// This hint doesn't belong to the Cairo common library +/// It's only added for testing proposes pub fn skip_next_instruction(vm: &mut VirtualMachine) -> Result<(), HintError> { vm.skip_next_instruction_execution(); Ok(()) diff --git a/src/hint_processor/hint_processor_utils.rs b/src/hint_processor/hint_processor_utils.rs index 407796a7af..799ce1f1a7 100644 --- a/src/hint_processor/hint_processor_utils.rs +++ b/src/hint_processor/hint_processor_utils.rs @@ -58,7 +58,7 @@ pub fn get_ptr_from_reference( } } -//Returns the value given by a reference as an Option +///Returns the value given by a reference as [MaybeRelocatable] pub fn get_maybe_relocatable_from_reference( vm: &VirtualMachine, hint_reference: &HintReference, @@ -80,7 +80,7 @@ pub fn get_maybe_relocatable_from_reference( value.ok_or(HintError::FailedToGetIds) } -///Computes the memory address of the ids variable indicated by the HintReference as a Relocatable +///Computes the memory address of the ids variable indicated by the HintReference as a [Relocatable] pub fn compute_addr_from_reference( //Reference data of the ids variable hint_reference: &HintReference, diff --git a/src/lib.rs b/src/lib.rs index 3176ec2055..576c150300 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,10 @@ +//! An implementation of the Cairo virtual machine +//! +//! # Feature Flags +//! - `skip_next_instruction_hint`: Enable the `skip_next_instruction()` hint. Not enabled by default. +//! - `hooks`: Enable [Hooks](vm::hooks) support for the [VirtualMachine](vm::vm_core::VirtualMachine). Not enabled by default. + +#![cfg_attr(docsrs, feature(doc_cfg))] #![deny(warnings)] pub mod cairo_run; pub mod hint_processor; diff --git a/src/vm/hooks.rs b/src/vm/hooks.rs new file mode 100644 index 0000000000..7cae20cc1d --- /dev/null +++ b/src/vm/hooks.rs @@ -0,0 +1,199 @@ +//! VM hooks +//! +//! Make it possible to execute custom arbitrary code at different stages of the VM execution +//! +//! If added to the VM, hooks function will be called during the VM execution at specific stages. +//! +//! Available hooks: +//! - before_first_step, executed before entering the execution loop in [run_until_pc](CairoRunner::run_until_pc) +//! - pre_step_instruction, executed before each instruction_step in [step](VirtualMachine::step) +//! - post_step_instruction, executed after each instruction_step in [step](VirtualMachine::step) + +use std::{any::Any, collections::HashMap, sync::Arc}; + +use felt::Felt; + +use crate::{ + hint_processor::hint_processor_definition::HintProcessor, types::exec_scope::ExecutionScopes, +}; + +use super::{ + errors::vm_errors::VirtualMachineError, runners::cairo_runner::CairoRunner, + vm_core::VirtualMachine, +}; + +type BeforeFirstStepHookFunc = Arc< + dyn Fn( + &mut VirtualMachine, + &mut CairoRunner, + &HashMap>>, + ) -> Result<(), VirtualMachineError> + + Sync + + Send, +>; + +type StepHookFunc = Arc< + dyn Fn( + &mut VirtualMachine, + &mut dyn HintProcessor, + &mut ExecutionScopes, + &HashMap>>, + &HashMap, + ) -> Result<(), VirtualMachineError> + + Sync + + Send, +>; + +/// The hooks to be executed during the VM run +/// +/// They can be individually ignored by setting them to [None] +#[derive(Clone, Default)] +pub struct Hooks { + before_first_step: Option, + pre_step_instruction: Option, + post_step_instruction: Option, +} + +impl Hooks { + pub fn new( + before_first_step: Option, + pre_step_instruction: Option, + post_step_instruction: Option, + ) -> Self { + Hooks { + before_first_step, + pre_step_instruction, + post_step_instruction, + } + } +} + +impl VirtualMachine { + pub fn execute_before_first_step( + &mut self, + runner: &mut CairoRunner, + hint_data_dictionary: &HashMap>>, + ) -> Result<(), VirtualMachineError> { + if let Some(hook_func) = self.hooks.clone().before_first_step { + (hook_func)(self, runner, hint_data_dictionary)?; + } + + Ok(()) + } + + pub fn execute_pre_step_instruction( + &mut self, + hint_executor: &mut dyn HintProcessor, + exec_scope: &mut ExecutionScopes, + hint_data_dictionary: &HashMap>>, + constants: &HashMap, + ) -> Result<(), VirtualMachineError> { + if let Some(hook_func) = self.hooks.clone().pre_step_instruction { + (hook_func)( + self, + hint_executor, + exec_scope, + hint_data_dictionary, + constants, + )?; + } + + Ok(()) + } + + pub fn execute_post_step_instruction( + &mut self, + hint_executor: &mut dyn HintProcessor, + exec_scope: &mut ExecutionScopes, + hint_data_dictionary: &HashMap>>, + constants: &HashMap, + ) -> Result<(), VirtualMachineError> { + if let Some(hook_func) = self.hooks.clone().post_step_instruction { + (hook_func)( + self, + hint_executor, + exec_scope, + hint_data_dictionary, + constants, + )?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::*; + use crate::{ + hint_processor::builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor, + types::program::Program, + utils::test_utils::{cairo_runner, vm}, + }; + + #[test] + fn empty_hooks() { + let program = Program::from_file(Path::new("cairo_programs/sqrt.json"), Some("main")) + .expect("Call to `Program::from_file()` failed."); + + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let mut cairo_runner = cairo_runner!(program); + let mut vm = vm!(); + vm.hooks = Hooks::new(None, None, None); + + let end = cairo_runner.initialize(&mut vm).unwrap(); + assert!(cairo_runner + .run_until_pc(end, &mut vm, &mut hint_processor) + .is_ok()); + } + + #[test] + fn hook_failure() { + let program = Program::from_file(Path::new("cairo_programs/sqrt.json"), Some("main")) + .expect("Call to `Program::from_file()` failed."); + + fn before_first_step_hook( + _vm: &mut VirtualMachine, + _runner: &mut CairoRunner, + _hint_data: &HashMap>>, + ) -> Result<(), VirtualMachineError> { + Err(VirtualMachineError::Unexpected) + } + + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let mut cairo_runner = cairo_runner!(program); + let mut vm = vm!(); + vm.hooks = Hooks::new(Some(Arc::new(before_first_step_hook)), None, None); + + let end = cairo_runner.initialize(&mut vm).unwrap(); + assert!(cairo_runner + .run_until_pc(end, &mut vm, &mut hint_processor) + .is_err()); + } + + #[test] + fn hook_success() { + let program = Program::from_file(Path::new("cairo_programs/sqrt.json"), Some("main")) + .expect("Call to `Program::from_file()` failed."); + + fn before_first_step_hook( + _vm: &mut VirtualMachine, + _runner: &mut CairoRunner, + _hint_data: &HashMap>>, + ) -> Result<(), VirtualMachineError> { + Ok(()) + } + + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let mut cairo_runner = cairo_runner!(program); + let mut vm = vm!(); + vm.hooks = Hooks::new(Some(Arc::new(before_first_step_hook)), None, None); + + let end = cairo_runner.initialize(&mut vm).unwrap(); + assert!(cairo_runner + .run_until_pc(end, &mut vm, &mut hint_processor) + .is_ok()); + } +} diff --git a/src/vm/mod.rs b/src/vm/mod.rs index c04a7f53c4..4fb3779f25 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -6,3 +6,7 @@ pub mod security; pub mod trace; pub mod vm_core; pub mod vm_memory; + +#[cfg(any(feature = "hooks"))] +#[cfg_attr(docsrs, doc(cfg(feature = "hooks")))] +pub mod hooks; diff --git a/src/vm/runners/cairo_runner.rs b/src/vm/runners/cairo_runner.rs index 58ea71085f..834ac3acd0 100644 --- a/src/vm/runners/cairo_runner.rs +++ b/src/vm/runners/cairo_runner.rs @@ -523,6 +523,8 @@ impl CairoRunner { ) -> Result<(), VirtualMachineError> { let references = self.get_reference_list(); let hint_data_dictionary = self.get_hint_data_dictionary(&references, hint_processor)?; + #[cfg(feature = "hooks")] + vm.execute_before_first_step(self, &hint_data_dictionary)?; while vm.run_context.pc != address { vm.step( hint_processor, @@ -714,7 +716,12 @@ impl CairoRunner { vm.segments.compute_effective_sizes(&vm.memory); if self.proof_mode && !disable_trace_padding { - self.run_until_next_power_of_2(vm, hint_processor)?; + self.run_until_next_power_of_2( + vm, + hint_processor, + #[cfg(hooks)] + hooks, + )?; loop { match self.check_used_cells(vm) { Ok(_) => break, @@ -726,8 +733,19 @@ impl CairoRunner { }, } - self.run_for_steps(1, vm, hint_processor)?; - self.run_until_next_power_of_2(vm, hint_processor)?; + self.run_for_steps( + 1, + vm, + hint_processor, + #[cfg(hooks)] + hooks, + )?; + self.run_until_next_power_of_2( + vm, + hint_processor, + #[cfg(hooks)] + hooks, + )?; } } @@ -958,6 +976,7 @@ impl CairoRunner { verify_secure: bool, vm: &mut VirtualMachine, hint_processor: &mut dyn HintProcessor, + #[cfg(hooks)] hooks: crate::vm::hooks::Hooks, ) -> Result<(), VirtualMachineError> { let stack = args .iter() @@ -968,8 +987,21 @@ impl CairoRunner { self.initialize_vm(vm)?; - self.run_until_pc(end, vm, hint_processor)?; - self.end_run(true, false, vm, hint_processor)?; + self.run_until_pc( + end, + vm, + hint_processor, + #[cfg(hooks)] + hooks, + )?; + self.end_run( + true, + false, + vm, + hint_processor, + #[cfg(hooks)] + hooks, + )?; if verify_secure { verify_secure_runner(self, false, vm)?; diff --git a/src/vm/vm_core.rs b/src/vm/vm_core.rs index e6e2a545ea..818bd82249 100644 --- a/src/vm/vm_core.rs +++ b/src/vm/vm_core.rs @@ -85,6 +85,8 @@ pub struct VirtualMachine { pub(crate) current_step: usize, skip_instruction_execution: bool, run_finished: bool, + #[cfg(feature = "hooks")] + pub(crate) hooks: crate::vm::hooks::Hooks, } impl HintData { @@ -128,6 +130,8 @@ impl VirtualMachine { skip_instruction_execution: false, segments: MemorySegmentManager::new(), run_finished: false, + #[cfg(feature = "hooks")] + hooks: Default::default(), } } @@ -524,7 +528,24 @@ impl VirtualMachine { constants: &HashMap, ) -> Result<(), VirtualMachineError> { self.step_hint(hint_executor, exec_scopes, hint_data_dictionary, constants)?; - self.step_instruction() + + #[cfg(feature = "hooks")] + self.execute_pre_step_instruction( + hint_executor, + exec_scopes, + hint_data_dictionary, + constants, + )?; + self.step_instruction()?; + #[cfg(feature = "hooks")] + self.execute_post_step_instruction( + hint_executor, + exec_scopes, + hint_data_dictionary, + constants, + )?; + + Ok(()) } fn compute_op0_deductions( @@ -2470,7 +2491,7 @@ mod tests { &mut hint_processor, exec_scopes_ref!(), &HashMap::new(), - &HashMap::new() + &HashMap::new(), ), Ok(()) );