diff --git a/crates/evm/src/context.cairo b/crates/evm/src/context.cairo index 52cf14460..70ba05dca 100644 --- a/crates/evm/src/context.cairo +++ b/crates/evm/src/context.cairo @@ -315,4 +315,9 @@ impl ExecutionContextImpl of ExecutionContextTrait { fn child_return_data(self: @ExecutionContext) -> Option> { *self.child_return_data } + + #[inline(always)] + fn append_event(ref self: ExecutionContext, event: Event) { + self.events.append(event); + } } diff --git a/crates/evm/src/errors.cairo b/crates/evm/src/errors.cairo index 2b7fa20d0..00d13775e 100644 --- a/crates/evm/src/errors.cairo +++ b/crates/evm/src/errors.cairo @@ -14,6 +14,9 @@ const RETURNDATA_OUT_OF_BOUNDS_ERROR: felt252 = 'KKT: ReturnDataOutOfBounds'; // JUMP const INVALID_DESTINATION: felt252 = 'KKT: invalid JUMP destination'; +// EVM STATE +const WRITE_IN_STATIC_CONTEXT: felt252 = 'KKT: WriteInStaticContext'; + #[derive(Drop, Copy, PartialEq)] enum EVMError { StackError: felt252, @@ -22,7 +25,8 @@ enum EVMError { ReturnDataError: felt252, JumpError: felt252, NotImplemented, - UnknownOpcode: u8 + UnknownOpcode: u8, + WriteInStaticContext: felt252 } @@ -36,7 +40,8 @@ impl EVMErrorIntoU256 of Into { EVMError::JumpError(error_message) => error_message.into(), EVMError::NotImplemented => 'NotImplemented'.into(), // TODO: refactor with dynamic strings once supported - EVMError::UnknownOpcode => 'UnknownOpcode'.into() + EVMError::UnknownOpcode => 'UnknownOpcode'.into(), + EVMError::WriteInStaticContext(error_message) => error_message.into(), } } } diff --git a/crates/evm/src/instructions.cairo b/crates/evm/src/instructions.cairo index c4fd50788..a6b0192f9 100644 --- a/crates/evm/src/instructions.cairo +++ b/crates/evm/src/instructions.cairo @@ -17,6 +17,7 @@ use comparison_operations::ComparisonAndBitwiseOperationsTrait; use duplication_operations::DuplicationOperationsTrait; use environmental_information::EnvironmentInformationTrait; use exchange_operations::ExchangeOperationsTrait; +use logging_operations::LoggingOperationsTrait; use memory_operations::MemoryOperationTrait; use push_operations::PushOperationsTrait; use sha3::Sha3Trait; diff --git a/crates/evm/src/instructions/logging_operations.cairo b/crates/evm/src/instructions/logging_operations.cairo index b804ade15..700b5eaca 100644 --- a/crates/evm/src/instructions/logging_operations.cairo +++ b/crates/evm/src/instructions/logging_operations.cairo @@ -1,48 +1,77 @@ +use evm::errors::EVMError; //! Logging Operations. // Internal imports use evm::machine::Machine; -mod internal { - use evm::machine::Machine; +#[generate_trait] +impl LoggingOperations of LoggingOperationsTrait { + /// 0xA0 - LOG0 operation + /// Append log record with no topic. + /// # Specification: https://www.evm.codes/#a0?fork=shanghai + fn exec_log0(ref self: Machine) -> Result<(), EVMError> { + internal::exec_log_i(ref self, 0) + } - /// Generic logging operation. - /// Append log record with n topics. - fn exec_log_i(ref machine: Machine, topics_len: u8) {} -} + /// 0xA1 - LOG1 + /// Append log record with one topic. + /// # Specification: https://www.evm.codes/#a1?fork=shanghai + fn exec_log1(ref self: Machine) -> Result<(), EVMError> { + internal::exec_log_i(ref self, 1) + } -/// 0xA0 - LOG0 operation -/// Append log record with no topic. -/// # Specification: https://www.evm.codes/#a0?fork=shanghai -fn exec_log0(ref machine: Machine) { - internal::exec_log_i(ref machine, 0); -} + /// 0xA2 - LOG2 + /// Append log record with two topics. + /// # Specification: https://www.evm.codes/#a2?fork=shanghai + fn exec_log2(ref self: Machine) -> Result<(), EVMError> { + internal::exec_log_i(ref self, 2) + } + /// 0xA3 - LOG3 + /// Append log record with three topics. + /// # Specification: https://www.evm.codes/#a3?fork=shanghai + fn exec_log3(ref self: Machine) -> Result<(), EVMError> { + internal::exec_log_i(ref self, 3) + } -/// 0xA1 - LOG1 -/// Append log record with one topic. -/// # Specification: https://www.evm.codes/#a1?fork=shanghai -fn exec_log1(ref machine: Machine) { - internal::exec_log_i(ref machine, 1); + /// 0xA4 - LOG4 + /// Append log record with four topics. + /// # Specification: https://www.evm.codes/#a4?fork=shanghai + fn exec_log4(ref self: Machine) -> Result<(), EVMError> { + internal::exec_log_i(ref self, 4) + } } -/// 0xA2 - LOG2 -/// Append log record with two topics. -/// # Specification: https://www.evm.codes/#a2?fork=shanghai -fn exec_log2(ref machine: Machine) { - internal::exec_log_i(ref machine, 2); -} +mod internal { + use evm::errors::{EVMError, WRITE_IN_STATIC_CONTEXT}; + use evm::machine::{Machine, MachineCurrentContextTrait}; + use evm::memory::MemoryTrait; + use evm::model::Event; + use evm::stack::StackTrait; -/// 0xA3 - LOG3 -/// Append log record with three topics. -/// # Specification: https://www.evm.codes/#a3?fork=shanghai -fn exec_log3(ref machine: Machine) { - internal::exec_log_i(ref machine, 3); -} + /// Store a new event in the dynamic context using topics + /// popped from the stack and data from the memory. + /// + /// # Arguments + /// + /// * `self` - The context to which the event will be added + /// * `topics_len` - The amount of topics to pop from the stack + fn exec_log_i(ref self: Machine, topics_len: u8) -> Result<(), EVMError> { + // Revert if the transaction is in a read only context + if self.read_only() { + return Result::Err(EVMError::WriteInStaticContext(WRITE_IN_STATIC_CONTEXT)); + } + + let offset = self.stack.pop_usize()?; + let size = self.stack.pop_usize()?; + let topics: Array = self.stack.pop_n(topics_len.into())?; + + let mut data: Array = Default::default(); + self.memory.load_n(size, ref data, offset); + + let event: Event = Event { keys: topics, data }; + self.append_event(event); -/// 0xA4 - LOG4 -/// Append log record with 4 topics. -/// # Specification: https://www.evm.codes/#a4?fork=shanghai -fn exec_log4(ref machine: Machine) { - internal::exec_log_i(ref machine, 4); + Result::Ok(()) + } } diff --git a/crates/evm/src/interpreter.cairo b/crates/evm/src/interpreter.cairo index 447df2394..a6d72accd 100644 --- a/crates/evm/src/interpreter.cairo +++ b/crates/evm/src/interpreter.cairo @@ -4,9 +4,11 @@ use evm::context::{CallContextTrait,}; use evm::errors::{EVMError, PC_OUT_OF_BOUNDS}; use evm::instructions::{ - ExchangeOperationsTrait, StopAndArithmeticOperationsTrait, ComparisonAndBitwiseOperationsTrait, - SystemOperationsTrait, BlockInformationTrait, DuplicationOperationsTrait, - EnvironmentInformationTrait, PushOperationsTrait, MemoryOperationTrait, logging_operations + duplication_operations, environmental_information, ExchangeOperationsTrait, logging_operations, + LoggingOperationsTrait, memory_operations, sha3, StopAndArithmeticOperationsTrait, + ComparisonAndBitwiseOperationsTrait, SystemOperationsTrait, BlockInformationTrait, + DuplicationOperationsTrait, EnvironmentInformationTrait, PushOperationsTrait, + MemoryOperationTrait }; use evm::machine::{Machine, MachineCurrentContextTrait}; use utils::{helpers::u256_to_bytes_array}; @@ -586,23 +588,23 @@ impl EVMInterpreterImpl of EVMInterpreterTrait { } if opcode == 160 { // LOG0 - logging_operations::exec_log0(ref machine); + return machine.exec_log0(); } if opcode == 161 { // LOG1 - logging_operations::exec_log1(ref machine); + return machine.exec_log1(); } if opcode == 162 { // LOG2 - logging_operations::exec_log2(ref machine); + return machine.exec_log2(); } if opcode == 163 { // LOG3 - logging_operations::exec_log3(ref machine); + return machine.exec_log3(); } if opcode == 164 { // LOG4 - logging_operations::exec_log4(ref machine); + return machine.exec_log4(); } if opcode == 240 { // CREATE diff --git a/crates/evm/src/machine.cairo b/crates/evm/src/machine.cairo index 68ecbb385..01fb23cbb 100644 --- a/crates/evm/src/machine.cairo +++ b/crates/evm/src/machine.cairo @@ -189,6 +189,13 @@ impl MachineCurrentContextImpl of MachineCurrentContextTrait { current_call_ctx.read_only() } + #[inline(always)] + fn append_event(ref self: Machine, event: Event) { + let mut current_execution_ctx = self.current_ctx.unbox(); + current_execution_ctx.append_event(event); + self.current_ctx = BoxTrait::new(current_execution_ctx); + } + #[inline(always)] fn gas_limit(ref self: Machine) -> u64 { let current_call_ctx = self.call_ctx(); diff --git a/crates/evm/src/model.cairo b/crates/evm/src/model.cairo index 8ac93588b..26441b251 100644 --- a/crates/evm/src/model.cairo +++ b/crates/evm/src/model.cairo @@ -1,5 +1,5 @@ #[derive(Drop)] struct Event { keys: Array, - data: Array, + data: Array, } diff --git a/crates/evm/src/tests/test_instructions.cairo b/crates/evm/src/tests/test_instructions.cairo index d435460a1..e4ef87860 100644 --- a/crates/evm/src/tests/test_instructions.cairo +++ b/crates/evm/src/tests/test_instructions.cairo @@ -3,6 +3,7 @@ mod test_comparison_operations; mod test_duplication_operations; mod test_environment_information; mod test_exchange_operations; +mod test_logging_operations; mod test_memory_operations; mod test_push_operations; mod test_sha3; diff --git a/crates/evm/src/tests/test_instructions/test_logging_operations.cairo b/crates/evm/src/tests/test_instructions/test_logging_operations.cairo new file mode 100644 index 000000000..14a78d6dd --- /dev/null +++ b/crates/evm/src/tests/test_instructions/test_logging_operations.cairo @@ -0,0 +1,331 @@ +use evm::errors::{EVMError, WRITE_IN_STATIC_CONTEXT, TYPE_CONVERSION_ERROR}; +use evm::instructions::LoggingOperationsTrait; +use evm::machine::{Machine, MachineCurrentContextTrait}; +use evm::memory::MemoryTrait; +use evm::stack::StackTrait; +use evm::tests::test_utils::{setup_machine, setup_machine_with_read_only}; +use integer::BoundedInt; +use utils::helpers::u256_to_bytes_array; + +#[test] +#[available_gas(20000000)] +fn test_exec_log0() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + + machine.stack.push(0x1F); + machine.stack.push(0x00); + + // When + let result = machine.exec_log0(); + + // Then + assert(result.is_ok(), 'should have succeeded'); + assert(machine.stack.len() == 0, 'stack should be empty'); + + let mut events = machine.events(); + assert(events.len() == 1, 'context should have one event'); + + let event = events.pop_front().unwrap(); + assert(event.keys.len() == 0, 'event should not have keys'); + + assert(event.data.len() == 31, 'event should have 31 bytes'); + let data_expected = u256_to_bytes_array(BoundedInt::::max()).span().slice(0, 31); + assert(event.data.span() == data_expected, 'event data are incorrect'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log1() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x20); + machine.stack.push(0x00); + + // When + let result = machine.exec_log1(); + + // Then + assert(result.is_ok(), 'should have succeeded'); + assert(machine.stack.len() == 0, 'stack should be empty'); + + let mut events = machine.events(); + assert(events.len() == 1, 'context should have one event'); + + let event = events.pop_front().unwrap(); + assert(event.keys.len() == 1, 'event should have one key'); + assert(*event.keys[0] == 0x0123456789ABCDEF, 'event key is not correct'); + + assert(event.data.len() == 32, 'event should have 32 bytes'); + let data_expected = u256_to_bytes_array(BoundedInt::::max()).span().slice(0, 32); + assert(event.data.span() == data_expected, 'event data are incorrect'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log2() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + + machine.stack.push(BoundedInt::::max()); + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x05); + machine.stack.push(0x05); + + // When + let result = machine.exec_log2(); + + // Then + assert(result.is_ok(), 'should have succeeded'); + assert(machine.stack.len() == 0, 'stack should be empty'); + + let mut events = machine.events(); + assert(events.len() == 1, 'context should have one event'); + + let event = events.pop_front().unwrap(); + assert(event.keys.len() == 2, 'event should have two keys'); + assert(*event.keys[0] == 0x0123456789ABCDEF, 'event key is not correct'); + assert(*event.keys[1] == BoundedInt::::max(), 'event key is not correct'); + + assert(event.data.len() == 5, 'event should have 5 bytes'); + let data_expected = u256_to_bytes_array(BoundedInt::::max()).span().slice(0, 5); + assert(event.data.span() == data_expected, 'event data are incorrect'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log3() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + machine.memory.store(0x0123456789ABCDEF000000000000000000000000000000000000000000000000, 0x20); + + machine.stack.push(0x00); + machine.stack.push(BoundedInt::::max()); + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x28); + machine.stack.push(0x00); + + // When + let result = machine.exec_log3(); + + // Then + assert(result.is_ok(), 'should have succeeded'); + assert(machine.stack.len() == 0, 'stack should be empty'); + + let mut events = machine.events(); + assert(events.len() == 1, 'context should have one event'); + + let event = events.pop_front().unwrap(); + assert(event.keys.len() == 3, 'event should have 3 keys'); + assert(*event.keys[0] == 0x0123456789ABCDEF, 'event key is not correct'); + assert(*event.keys[1] == BoundedInt::::max(), 'event key is not correct'); + assert(*event.keys[2] == 0x00, 'event key is not correct'); + + assert(event.data.len() == 40, 'event should have 40 bytes'); + let data_expected = u256_to_bytes_array(BoundedInt::::max()).span(); + assert(event.data.span().slice(0, 32) == data_expected, 'event data are incorrect'); + let data_expected = array![0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF].span(); + assert(event.data.span().slice(32, 8) == data_expected, 'event data are incorrect'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log4() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + machine.memory.store(0x0123456789ABCDEF000000000000000000000000000000000000000000000000, 0x20); + + machine.stack.push(BoundedInt::::max()); + machine.stack.push(0x00); + machine.stack.push(BoundedInt::::max()); + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x0A); + machine.stack.push(0x20); + + // When + let result = machine.exec_log4(); + + // Then + assert(result.is_ok(), 'should have succeeded'); + assert(machine.stack.len() == 0, 'stack should be empty'); + + let mut events = machine.events(); + assert(events.len() == 1, 'context should have one event'); + + let event = events.pop_front().unwrap(); + assert(event.keys.len() == 4, 'event should have 4 keys'); + assert(*event.keys[0] == 0x0123456789ABCDEF, 'event key is not correct'); + assert(*event.keys[1] == BoundedInt::::max(), 'event key is not correct'); + assert(*event.keys[2] == 0x00, 'event key is not correct'); + assert(*event.keys[3] == BoundedInt::::max(), 'event key is not correct'); + + assert(event.data.len() == 10, 'event should have 10 bytes'); + let data_expected = array![0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x00, 0x00].span(); + assert(event.data.span() == data_expected, 'event data are incorrect'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log1_read_only_context() { + // Given + let mut machine = setup_machine_with_read_only(); + + machine.memory.store(BoundedInt::::max(), 0); + + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x20); + machine.stack.push(0x00); + + // When + let result = machine.exec_log1(); + + // Then + assert(result.is_err(), 'should have returned an error'); + assert( + result.unwrap_err() == EVMError::WriteInStaticContext(WRITE_IN_STATIC_CONTEXT), + 'err != WriteInStaticContext' + ); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log1_size_0_offset_0() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x00); + machine.stack.push(0x00); + + // When + let result = machine.exec_log1(); + + // Then + assert(result.is_ok(), 'should have succeeded'); + assert(machine.stack.len() == 0, 'stack should be empty'); + + let mut events = machine.events(); + assert(events.len() == 1, 'context should have one event'); + + let event = events.pop_front().unwrap(); + assert(event.keys.len() == 1, 'event should have one key'); + assert(*event.keys[0] == 0x0123456789ABCDEF, 'event key is not correct'); + + assert(event.data.len() == 0, 'event data should be empty'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log1_size_too_big() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(BoundedInt::::max()); + machine.stack.push(0x00); + + // When + let result = machine.exec_log1(); + + // Then + assert(result.is_err(), 'should return an error'); + assert( + result.unwrap_err() == EVMError::TypeConversionError(TYPE_CONVERSION_ERROR), + 'err != TypeConversionError' + ); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log1_offset_too_big() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x20); + machine.stack.push(BoundedInt::::max()); + + // When + let result = machine.exec_log1(); + + // Then + assert(result.is_err(), 'should return an error'); + assert( + result.unwrap_err() == EVMError::TypeConversionError(TYPE_CONVERSION_ERROR), + 'err != TypeConversionError' + ); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_log_multiple_events() { + // Given + let mut machine = setup_machine(); + + machine.memory.store(BoundedInt::::max(), 0); + machine.memory.store(0x0123456789ABCDEF000000000000000000000000000000000000000000000000, 0x20); + + machine.stack.push(BoundedInt::::max()); + machine.stack.push(0x00); + machine.stack.push(BoundedInt::::max()); + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x0A); + machine.stack.push(0x20); + machine.stack.push(0x00); + machine.stack.push(BoundedInt::::max()); + machine.stack.push(0x0123456789ABCDEF); + machine.stack.push(0x28); + machine.stack.push(0x00); + + // When + let result = machine.exec_log3(); + let result = machine.exec_log4(); + + // Then + assert(result.is_ok(), 'should have succeeded'); + assert(machine.stack.len() == 0, 'stack size should be 0'); + + let mut events = machine.events(); + assert(events.len() == 2, 'context should have 2 events'); + + let event1 = events.pop_front().unwrap(); + assert(event1.keys.len() == 3, 'event1 should have 3 keys'); + assert(*event1.keys[0] == 0x0123456789ABCDEF, 'event1 key is not correct'); + assert(*event1.keys[1] == BoundedInt::::max(), 'event1 key is not correct'); + assert(*event1.keys[2] == 0x00, 'event1 key is not correct'); + + assert(event1.data.len() == 40, 'event1 should have 40 bytes'); + let data_expected = u256_to_bytes_array(BoundedInt::::max()).span(); + assert(event1.data.span().slice(0, 32) == data_expected, 'event1 data are incorrect'); + let data_expected = array![0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF].span(); + assert(event1.data.span().slice(32, 8) == data_expected, 'event1 data are incorrect'); + + let event2 = events.pop_front().unwrap(); + assert(event2.keys.len() == 4, 'event2 should have 4 keys'); + assert(*event2.keys[0] == 0x0123456789ABCDEF, 'event2 key is not correct'); + assert(*event2.keys[1] == BoundedInt::::max(), 'event2 key is not correct'); + assert(*event2.keys[2] == 0x00, 'event2 key is not correct'); + assert(*event2.keys[3] == BoundedInt::::max(), 'event2 key is not correct'); + + assert(event2.data.len() == 10, 'event2 should have 10 bytes'); + let data_expected = array![0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x00, 0x00].span(); + assert(event2.data.span() == data_expected, 'event2 data are incorrect'); +} diff --git a/crates/evm/src/tests/test_utils.cairo b/crates/evm/src/tests/test_utils.cairo index c6f11de7d..1bc97d047 100644 --- a/crates/evm/src/tests/test_utils.cairo +++ b/crates/evm/src/tests/test_utils.cairo @@ -151,3 +151,13 @@ fn setup_machine_with_calldata(calldata: Span) -> Machine { storage_journal: Default::default(), } } + +fn setup_machine_with_read_only() -> Machine { + let mut machine = setup_machine(); + let mut current_ctx = machine.current_ctx.unbox(); + let mut current_call_ctx = current_ctx.call_ctx.unbox(); + current_call_ctx.read_only = true; + current_ctx.call_ctx = BoxTrait::new(current_call_ctx); + machine.current_ctx = BoxTrait::new(current_ctx); + machine +}