diff --git a/cairo/ethereum/cancun/vm/instructions/environment.cairo b/cairo/ethereum/cancun/vm/instructions/environment.cairo index fc6b8f4d..36e1e218 100644 --- a/cairo/ethereum/cancun/vm/instructions/environment.cairo +++ b/cairo/ethereum/cancun/vm/instructions/environment.cairo @@ -3,6 +3,7 @@ from starkware.cairo.common.registers import get_fp_and_pc from starkware.cairo.common.dict_access import DictAccess from starkware.cairo.common.math import split_felt from starkware.cairo.common.uint256 import uint256_lt +from starkware.cairo.common.math_cmp import is_le from ethereum_types.bytes import Bytes32, Bytes32Struct from ethereum_types.numeric import U256, U256Struct, Uint, UnionUintU256, UnionUintU256Enum @@ -14,7 +15,7 @@ from ethereum_types.others import ( ) from ethereum.cancun.fork_types import Address, SetAddress, SetAddressStruct, SetAddressDictAccess from ethereum.cancun.vm import Evm, EvmImpl, EnvImpl -from ethereum.cancun.vm.exceptions import ExceptionalHalt, OutOfGasError +from ethereum.cancun.vm.exceptions import ExceptionalHalt, OutOfGasError, OutOfBoundsRead from ethereum.cancun.vm.gas import charge_gas, GasConstants, calculate_gas_extend_memory from ethereum.cancun.vm.memory import buffer_read, memory_write, expand_by from ethereum.cancun.vm.stack import Stack, push, pop @@ -278,6 +279,75 @@ func returndatasize{range_check_ptr, evm: Evm}() -> ExceptionalHalt* { return ok; } +func returndatacopy{range_check_ptr: felt, evm: Evm}() -> ExceptionalHalt* { + alloc_locals; + // STACK + let stack = evm.value.stack; + with stack { + let (memory_start_index, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + let (returndata_start_position, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + let (size, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + } + if (size.value.high != 0) { + tempvar err = new ExceptionalHalt(OutOfGasError); + return err; + } + + let ceil32_size = ceil32(Uint(size.value.low)); + let (words, _) = divmod(ceil32_size.value, 32); + let return_data_copy_gas_cost = GasConstants.GAS_RETURN_DATA_COPY * words; + // Calculate memory expansion cost + tempvar extensions_tuple = new TupleU256U256(new TupleU256U256Struct(memory_start_index, size)); + tempvar extensions_list = ListTupleU256U256(new ListTupleU256U256Struct(extensions_tuple, 1)); + let extend_memory = calculate_gas_extend_memory(evm.value.memory, extensions_list); + let err = charge_gas( + Uint(GasConstants.GAS_VERY_LOW + return_data_copy_gas_cost + extend_memory.value.cost.value) + ); + if (cast(err, felt) != 0) { + return err; + } + + let memory = evm.value.memory; + // Check if the read on return_data is in bounds + // If the start position is greater than 2 ** 128, then it is almost surely out of bounds + if (returndata_start_position.value.high != 0) { + tempvar err = new ExceptionalHalt(OutOfBoundsRead); + return err; + } + // Check if returndata_start_position and size are each less than 2**128, so that their + // sum is less than 2**129, which fits into a felt. We can then be sure that + // size.value.low + returndata_start_position.value.low won't wrap around the PRIME. + assert [range_check_ptr] = returndata_start_position.value.low; + let range_check_ptr = range_check_ptr + 1; + assert [range_check_ptr] = size.value.low; + let range_check_ptr = range_check_ptr + 1; + let is_in_bounds = is_le( + size.value.low + returndata_start_position.value.low, evm.value.return_data.value.len + ); + if (is_in_bounds == 0) { + tempvar err = new ExceptionalHalt(OutOfBoundsRead); + return err; + } + + with memory { + expand_by(extend_memory.value.expand_by); + let value = buffer_read(evm.value.return_data, returndata_start_position, size); + memory_write(memory_start_index, value); + } + EvmImpl.set_pc_stack_memory(Uint(evm.value.pc.value + 1), stack, memory); + let ok = cast(0, ExceptionalHalt*); + return ok; +} + // @notice Push the balance of the current address to the stack func self_balance{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, evm: Evm}() -> ExceptionalHalt* { alloc_locals; diff --git a/cairo/tests/ethereum/cancun/vm/instructions/test_environment.py b/cairo/tests/ethereum/cancun/vm/instructions/test_environment.py index de8b77b6..23138533 100644 --- a/cairo/tests/ethereum/cancun/vm/instructions/test_environment.py +++ b/cairo/tests/ethereum/cancun/vm/instructions/test_environment.py @@ -16,6 +16,7 @@ codesize, gasprice, origin, + returndatacopy, returndatasize, self_balance, ) @@ -56,6 +57,16 @@ EvmBuilder().with_gas_left().with_env(environment_empty_state).build() ) +evm_environment_strategy_with_return_data = ( + EvmBuilder() + .with_memory() + .with_gas_left() + .with_env(environment_empty_state) + .with_return_data() + .with_capped_values_stack() + .build() +) + code_access_size_strategy = st.integers(min_value=0, max_value=MAX_CODE_SIZE).map(U256) code_start_index_strategy = code_access_size_strategy @@ -194,6 +205,18 @@ def test_returndatasize(self, cairo_run, evm: Evm): returndatasize(evm) assert evm == cairo_result + @given(evm=evm_environment_strategy_with_return_data) + def test_returndatacopy(self, cairo_run, evm: Evm): + try: + cairo_result = cairo_run("returndatacopy", evm) + except Exception as cairo_error: + with strict_raises(type(cairo_error)): + returndatacopy(evm) + return + + returndatacopy(evm) + assert evm == cairo_result + @given(evm=evm_environment_strategy) def test_self_balance(self, cairo_run, evm: Evm): try: diff --git a/cairo/tests/utils/evm_builder.py b/cairo/tests/utils/evm_builder.py index 9e045498..a0464072 100644 --- a/cairo/tests/utils/evm_builder.py +++ b/cairo/tests/utils/evm_builder.py @@ -92,6 +92,14 @@ def with_stack(self, strategy=stack_strategy(Stack[U256])): self._stack = strategy return self + def with_capped_values_stack(self, max_value=2**8 - 1): + self._stack = st.lists( + st.integers(min_value=0, max_value=max_value).map(U256), + min_size=0, + max_size=1024, + ).map(lambda x: Stack[U256](x)) + return self + def with_memory(self, strategy=memory_lite): self._memory = strategy return self @@ -132,6 +140,10 @@ def with_accessed_storage_keys( self._accessed_storage_keys = strategy return self + def with_return_data(self, strategy=st.binary(min_size=0, max_size=1024)): + self._return_data = strategy + return self + def build(self): return st.builds( Evm,