From c270f04f7e6a40cf754c279b973435b20e5d2c3e Mon Sep 17 00:00:00 2001 From: Mathieu <60658558+enitrat@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:29:21 +0700 Subject: [PATCH] feat: memory opcodes (#365) Resolves #351 --------- Co-authored-by: Oba --- cairo/ethereum/cancun/vm.cairo | 53 ++++ .../vm/instructions/memory_instructions.cairo | 257 ++++++++++++++++++ cairo/ethereum/cancun/vm/memory.cairo | 57 +++- .../instructions/test_memory_instructions.py | 69 +++++ cairo/tests/utils/constants.py | 1 + cairo/tests/utils/serde.py | 3 +- cairo/tests/utils/strategies.py | 3 +- 7 files changed, 433 insertions(+), 10 deletions(-) create mode 100644 cairo/ethereum/cancun/vm/instructions/memory_instructions.cairo create mode 100644 cairo/tests/ethereum/cancun/vm/instructions/test_memory_instructions.py diff --git a/cairo/ethereum/cancun/vm.cairo b/cairo/ethereum/cancun/vm.cairo index cc19a672..d4cfd633 100644 --- a/cairo/ethereum/cancun/vm.cairo +++ b/cairo/ethereum/cancun/vm.cairo @@ -176,6 +176,7 @@ namespace EvmImpl { code=new_code, gas_left=evm.value.gas_left, env=evm.value.env, + valid_jump_destinations=evm.value.valid_jump_destinations, logs=evm.value.logs, refund_counter=evm.value.refund_counter, running=evm.value.running, @@ -553,4 +554,56 @@ namespace EvmImpl { ); return (); } + + func set_pc_stack{evm: Evm}(new_pc: Uint, new_stack: Stack) { + tempvar evm = Evm( + new EvmStruct( + pc=new_pc, + stack=new_stack, + memory=evm.value.memory, + code=evm.value.code, + gas_left=evm.value.gas_left, + env=evm.value.env, + valid_jump_destinations=evm.value.valid_jump_destinations, + logs=evm.value.logs, + refund_counter=evm.value.refund_counter, + running=evm.value.running, + message=evm.value.message, + output=evm.value.output, + accounts_to_delete=evm.value.accounts_to_delete, + touched_accounts=evm.value.touched_accounts, + return_data=evm.value.return_data, + error=evm.value.error, + accessed_addresses=evm.value.accessed_addresses, + accessed_storage_keys=evm.value.accessed_storage_keys, + ), + ); + return (); + } + + func set_pc_stack_memory{evm: Evm}(new_pc: Uint, new_stack: Stack, new_memory: Memory) { + tempvar evm = Evm( + new EvmStruct( + pc=new_pc, + stack=new_stack, + memory=new_memory, + code=evm.value.code, + gas_left=evm.value.gas_left, + env=evm.value.env, + valid_jump_destinations=evm.value.valid_jump_destinations, + logs=evm.value.logs, + refund_counter=evm.value.refund_counter, + running=evm.value.running, + message=evm.value.message, + output=evm.value.output, + accounts_to_delete=evm.value.accounts_to_delete, + touched_accounts=evm.value.touched_accounts, + return_data=evm.value.return_data, + error=evm.value.error, + accessed_addresses=evm.value.accessed_addresses, + accessed_storage_keys=evm.value.accessed_storage_keys, + ), + ); + return (); + } } diff --git a/cairo/ethereum/cancun/vm/instructions/memory_instructions.cairo b/cairo/ethereum/cancun/vm/instructions/memory_instructions.cairo new file mode 100644 index 00000000..41306e4a --- /dev/null +++ b/cairo/ethereum/cancun/vm/instructions/memory_instructions.cairo @@ -0,0 +1,257 @@ +from starkware.cairo.common.cairo_builtins import BitwiseBuiltin +from starkware.cairo.common.uint256 import uint256_eq, Uint256, uint256_and +from starkware.cairo.common.math_cmp import is_le_felt + +from ethereum.cancun.vm.stack import Stack, pop, push +from ethereum.cancun.vm import Evm, EvmImpl, EvmStruct +from ethereum.cancun.vm.exceptions import ExceptionalHalt, OutOfGasError +from ethereum_types.numeric import U256, U256Struct, Uint +from ethereum.cancun.vm.gas import ( + charge_gas, + GasConstants, + ExtendMemory, + calculate_gas_extend_memory, +) +from ethereum.cancun.vm.memory import Memory, memory_read_bytes, memory_write, expand_by +from ethereum.utils.numeric import ceil32, divmod +from ethereum_types.bytes import Bytes, BytesStruct +from starkware.cairo.common.alloc import alloc +from ethereum_types.others import ( + ListTupleU256U256, + ListTupleU256U256Struct, + TupleU256U256, + TupleU256U256Struct, +) +from src.utils.bytes import uint256_to_bytes32 +from src.utils.utils import Helpers + +// @notice Stores a word to memory +func mstore{range_check_ptr, evm: Evm}() -> ExceptionalHalt* { + alloc_locals; + // STACK + let stack = evm.value.stack; + with stack { + let (start_position, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + let (value, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + } + + // GAS + tempvar mem_access_tuple = new TupleU256U256( + new TupleU256U256Struct(start_position, U256(new U256Struct(32, 0))) + ); + tempvar mem_access_list = ListTupleU256U256(new ListTupleU256U256Struct(mem_access_tuple, 1)); + let extend_memory = calculate_gas_extend_memory(evm.value.memory, mem_access_list); + + // assumed that cost < 2**110 (see calculate_memory_gas_cost) + let err = charge_gas(Uint(GasConstants.GAS_VERY_LOW + extend_memory.value.cost.value)); + if (cast(err, felt) != 0) { + return err; + } + + // OPERATION + let memory = evm.value.memory; + let (value_data: felt*) = alloc(); + uint256_to_bytes32(value_data, [value.value]); + tempvar value_bytes = Bytes(new BytesStruct(value_data, 32)); + + let memory = evm.value.memory; + with memory { + expand_by(extend_memory.value.expand_by); + memory_write(start_position, value_bytes); + } + + // PROGRAM COUNTER + EvmImpl.set_pc_stack_memory(Uint(evm.value.pc.value + 1), stack, memory); + let ok = cast(0, ExceptionalHalt*); + return ok; +} + +// @notice Stores a byte to memory +func mstore8{range_check_ptr, bitwise_ptr: BitwiseBuiltin*, evm: Evm}() -> ExceptionalHalt* { + alloc_locals; + // STACK + let stack = evm.value.stack; + with stack { + let (start_position, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + let (value, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + } + + // GAS + tempvar mem_access_tuple = new TupleU256U256( + new TupleU256U256Struct(start_position, U256(new U256Struct(1, 0))) + ); + tempvar mem_access_list = ListTupleU256U256(new ListTupleU256U256Struct(mem_access_tuple, 1)); + let extend_memory = calculate_gas_extend_memory(evm.value.memory, mem_access_list); + + // assumed that cost < 2**110 (see calculate_memory_gas_cost) + let err = charge_gas(Uint(GasConstants.GAS_VERY_LOW + extend_memory.value.cost.value)); + if (cast(err, felt) != 0) { + return err; + } + + // OPERATION + let (data) = alloc(); + assert bitwise_ptr.x = value.value.low; + assert bitwise_ptr.y = 0xFF; + assert [data] = bitwise_ptr.x_and_y; + let bitwise_ptr = bitwise_ptr + BitwiseBuiltin.SIZE; + tempvar value_bytes = Bytes(new BytesStruct(data, 1)); + + let memory = evm.value.memory; + with memory { + expand_by(extend_memory.value.expand_by); + memory_write(start_position, value_bytes); + } + + // PROGRAM COUNTER + EvmImpl.set_pc_stack_memory(Uint(evm.value.pc.value + 1), stack, memory); + let ok = cast(0, ExceptionalHalt*); + return ok; +} + +// @notice Load word from memory +func mload{range_check_ptr, evm: Evm}() -> ExceptionalHalt* { + alloc_locals; + // STACK + let stack = evm.value.stack; + with stack { + let (start_position, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + } + + // GAS + tempvar mem_access_tuple = new TupleU256U256( + new TupleU256U256Struct(start_position, U256(new U256Struct(32, 0))) + ); + tempvar mem_access_list = ListTupleU256U256(new ListTupleU256U256Struct(mem_access_tuple, 1)); + let extend_memory = calculate_gas_extend_memory(evm.value.memory, mem_access_list); + + // assumed that cost < 2**110 (see calculate_memory_gas_cost) + let err = charge_gas(Uint(GasConstants.GAS_VERY_LOW + extend_memory.value.cost.value)); + if (cast(err, felt) != 0) { + return err; + } + + // OPERATION + let memory = evm.value.memory; + with memory { + let value_bytes = memory_read_bytes(start_position, U256(new U256Struct(32, 0))); + expand_by(extend_memory.value.expand_by); + } + let value = Helpers.bytes32_to_uint256(value_bytes.value.data); + with stack { + let err = push(U256(new U256Struct(value.low, value.high))); + if (cast(err, felt) != 0) { + return err; + } + } + + // PROGRAM COUNTER + EvmImpl.set_pc_stack_memory(Uint(evm.value.pc.value + 1), stack, memory); + let ok = cast(0, ExceptionalHalt*); + return ok; +} + +// @notice Push the size of active memory in bytes onto the stack +func msize{range_check_ptr, evm: Evm}() -> ExceptionalHalt* { + alloc_locals; + // STACK + + // GAS + let err = charge_gas(Uint(GasConstants.GAS_BASE)); + if (cast(err, felt) != 0) { + return err; + } + + // OPERATION + let stack = evm.value.stack; + with stack { + let err = push(U256(new U256Struct(evm.value.memory.value.len, 0))); + if (cast(err, felt) != 0) { + return err; + } + } + + // PROGRAM COUNTER + EvmImpl.set_pc_stack(Uint(evm.value.pc.value + 1), stack); + let ok = cast(0, ExceptionalHalt*); + return ok; +} + +// @notice Copy the bytes in memory from one location to another +func mcopy{range_check_ptr, evm: Evm}() -> ExceptionalHalt* { + alloc_locals; + // STACK + let stack = evm.value.stack; + with stack { + let (destination, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + let (source, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + let (length, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + } + + // GAS + // OutOfGasError if length > 2**128 + if (length.value.high != 0) { + tempvar err = new ExceptionalHalt(OutOfGasError); + return err; + } + + let length_ceil32 = ceil32(Uint(length.value.low)); + let (words, _) = divmod(length_ceil32.value, 32); + let copy_gas_cost = GasConstants.GAS_COPY * words; + + let (mem_access_tuples: TupleU256U256*) = alloc(); + assert mem_access_tuples[0] = TupleU256U256( + new TupleU256U256Struct(source, U256(new U256Struct(length.value.low, 0))) + ); + assert mem_access_tuples[1] = TupleU256U256( + new TupleU256U256Struct(destination, U256(new U256Struct(length.value.low, 0))) + ); + tempvar mem_access_list = ListTupleU256U256(new ListTupleU256U256Struct(mem_access_tuples, 2)); + let extend_memory = calculate_gas_extend_memory(evm.value.memory, mem_access_list); + + // copy_gas_cost in [0, 3 * 2**120) + // extend_memory.value.cost.value in [0, 2**110) + // -> sum < felt_size, no overflow + let err = charge_gas( + Uint(GasConstants.GAS_VERY_LOW + copy_gas_cost + extend_memory.value.cost.value) + ); + if (cast(err, felt) != 0) { + return err; + } + + // OPERATION + let memory = evm.value.memory; + with memory { + expand_by(extend_memory.value.expand_by); + let value = memory_read_bytes(source, length); + memory_write(destination, value); + } + + EvmImpl.set_pc_stack_memory(Uint(evm.value.pc.value + 1), stack, memory); + let ok = cast(0, ExceptionalHalt*); + return ok; +} diff --git a/cairo/ethereum/cancun/vm/memory.cairo b/cairo/ethereum/cancun/vm/memory.cairo index 3166eaa5..6d2a4cb7 100644 --- a/cairo/ethereum/cancun/vm/memory.cairo +++ b/cairo/ethereum/cancun/vm/memory.cairo @@ -11,8 +11,9 @@ from starkware.cairo.common.math import assert_le, assert_lt from starkware.cairo.common.math_cmp import is_le, is_not_zero from ethereum_types.bytes import Bytes, BytesStruct, Bytes1DictAccess -from ethereum_types.numeric import U256 +from ethereum_types.numeric import U256, Uint from ethereum.utils.numeric import max +from src.utils.bytes import uint256_to_bytes32 struct MemoryStruct { dict_ptr_start: Bytes1DictAccess*, @@ -25,6 +26,7 @@ struct Memory { } // @notice Write bytes to memory at a given position. +// @dev assumption: memory is resized by the calling opcode // @param memory The pointer to the bytearray. // @param start_position Starting position to write at. // @param value Bytes to write. @@ -32,7 +34,13 @@ func memory_write{range_check_ptr, memory: Memory}(start_position: U256, value: alloc_locals; let bytes_len = value.value.len; let start_position_felt = start_position.value.low; - with_attr error_message("memory_write: start_position > 2**128 || value.len > 2**128") { + + // Early return if nothing to write + if (value.value.len == 0) { + return (); + } + + with_attr error_message("memory_write: start_position > 2**128") { assert start_position.value.high = 0; } @@ -43,8 +51,10 @@ func memory_write{range_check_ptr, memory: Memory}(start_position: U256, value: } let new_dict_ptr = cast(dict_ptr, Bytes1DictAccess*); - let len = max(memory.value.len, start_position.value.low + value.value.len); - tempvar memory = Memory(new MemoryStruct(memory.value.dict_ptr_start, new_dict_ptr, len)); + // we do not resize the memory here as it is done by the calling opcode + tempvar memory = Memory( + new MemoryStruct(memory.value.dict_ptr_start, new_dict_ptr, memory.value.len) + ); return (); } @@ -56,11 +66,20 @@ func memory_write{range_check_ptr, memory: Memory}(start_position: U256, value: func memory_read_bytes{memory: Memory}(start_position: U256, size: U256) -> Bytes { alloc_locals; - with_attr error_message("memory_read_bytes: start_position > 2**128 || size > 2**128") { - assert start_position.value.high = 0; + with_attr error_message("memory_read_bytes: size > 2**128") { assert size.value.high = 0; } + // Early return if nothing to read + if (size.value.low == 0) { + tempvar result = Bytes(new BytesStruct(cast(0, felt*), 0)); + return result; + } + + with_attr error_message("memory_read_bytes: start_position > 2**128") { + assert start_position.value.high = 0; + } + let (local output: felt*) = alloc(); let dict_ptr = cast(memory.value.dict_ptr, DictAccess*); let start_position_felt = start_position.value.low; @@ -92,16 +111,38 @@ func buffer_read{range_check_ptr}(buffer: Bytes, start_position: U256, size: U25 let buffer_data = buffer.value.data; let start_position_felt = start_position.value.low; let size_felt = size.value.low; - with_attr error_message("buffer_read: start_position > 2**128 || size > 2**128") { - assert start_position.value.high = 0; + + with_attr error_message("buffer_read: size > 2**128") { assert size.value.high = 0; } + // Early return if nothing to read + if (size_felt == 0) { + tempvar result = Bytes(new BytesStruct(cast(0, felt*), 0)); + return result; + } + + with_attr error_message("buffer_read: start_position > 2**128") { + assert start_position.value.high = 0; + } + _buffer_read(buffer_len, buffer_data, start_position_felt, size_felt, output); tempvar result = Bytes(new BytesStruct(output, size_felt)); return result; } +// @notice Internal function to expand memory by a given amount. +// @param memory The pointer to the bytearray. +// @param expansion The amount to expand by. +func expand_by{memory: Memory}(expansion: Uint) { + tempvar memory = Memory( + new MemoryStruct( + memory.value.dict_ptr_start, memory.value.dict_ptr, memory.value.len + expansion.value + ), + ); + return (); +} + // @notice Internal function to write bytes to memory. // @param start_position Starting position to write at. // @param data Pointer to the bytes data. diff --git a/cairo/tests/ethereum/cancun/vm/instructions/test_memory_instructions.py b/cairo/tests/ethereum/cancun/vm/instructions/test_memory_instructions.py new file mode 100644 index 00000000..fcbf509f --- /dev/null +++ b/cairo/tests/ethereum/cancun/vm/instructions/test_memory_instructions.py @@ -0,0 +1,69 @@ +import pytest +from hypothesis import given + +from ethereum.cancun.vm.exceptions import ExceptionalHalt +from ethereum.cancun.vm.instructions.memory import mcopy, mload, msize, mstore, mstore8 +from tests.utils.args_gen import Evm +from tests.utils.strategies import evm_lite + + +class TestMemory: + @given(evm=evm_lite) + def test_mstore(self, cairo_run, evm: Evm): + try: + cairo_result = cairo_run("mstore", evm) + except ExceptionalHalt as cairo_error: + with pytest.raises(type(cairo_error)): + mstore(evm) + return + + mstore(evm) + assert evm == cairo_result + + @given(evm=evm_lite) + def test_mstore8(self, cairo_run, evm: Evm): + try: + cairo_result = cairo_run("mstore8", evm) + except ExceptionalHalt as cairo_error: + with pytest.raises(type(cairo_error)): + mstore8(evm) + return + + mstore8(evm) + assert evm == cairo_result + + @given(evm=evm_lite) + def test_mload(self, cairo_run, evm: Evm): + try: + cairo_result = cairo_run("mload", evm) + except ExceptionalHalt as cairo_error: + with pytest.raises(type(cairo_error)): + mload(evm) + return + + mload(evm) + assert evm == cairo_result + + @given(evm=evm_lite) + def test_msize(self, cairo_run, evm: Evm): + try: + cairo_result = cairo_run("msize", evm) + except ExceptionalHalt as cairo_error: + with pytest.raises(type(cairo_error)): + msize(evm) + return + + msize(evm) + assert evm == cairo_result + + @given(evm=evm_lite) + def test_mcopy(self, cairo_run, evm: Evm): + try: + cairo_result = cairo_run("mcopy", evm) + except ExceptionalHalt as cairo_error: + with pytest.raises(type(cairo_error)): + mcopy(evm) + return + + mcopy(evm) + assert evm == cairo_result diff --git a/cairo/tests/utils/constants.py b/cairo/tests/utils/constants.py index ad54c469..6d6b6b2e 100644 --- a/cairo/tests/utils/constants.py +++ b/cairo/tests/utils/constants.py @@ -21,6 +21,7 @@ CHAIN_ID = 1 TRANSACTION_GAS_LIMIT = 10_000_000 +BLOCK_GAS_LIMIT = 30_000_000 COINBASE = "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba" BASE_FEE_PER_GAS = 1_000 signers = { diff --git a/cairo/tests/utils/serde.py b/cairo/tests/utils/serde.py index 16c029f2..0883389f 100644 --- a/cairo/tests/utils/serde.py +++ b/cairo/tests/utils/serde.py @@ -200,7 +200,8 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: if python_cls is Memory: # For bytearray, convert Bytes1 objects to integers return Memory( - int.from_bytes(dict_repr[i], "little") for i in range(data_len) + int.from_bytes(dict_repr.get(i, b"\x00"), "little") + for i in range(data_len) ) return [dict_repr[i] for i in range(data_len)] diff --git a/cairo/tests/utils/strategies.py b/cairo/tests/utils/strategies.py index 47aff521..60af5f9b 100644 --- a/cairo/tests/utils/strategies.py +++ b/cairo/tests/utils/strategies.py @@ -13,6 +13,7 @@ from ethereum.crypto.elliptic_curve import SECP256K1N from ethereum.exceptions import EthereumException from tests.utils.args_gen import Environment, Evm, Memory, Message, Stack +from tests.utils.constants import BLOCK_GAS_LIMIT # Mock the Extended type because hypothesis cannot handle the RLP Protocol # Needs to be done before importing the types from ethereum.cancun.trie @@ -208,7 +209,7 @@ def tuple_strategy(thing): "stack": stack_strategy(Stack[U256]), "memory": memory_lite, "code": st.just(b""), - "gas_left": uint, + "gas_left": st.integers(min_value=0, max_value=BLOCK_GAS_LIMIT).map(Uint), "env": st.from_type(Environment), "valid_jump_destinations": st.just(set()), "logs": st.just(()),