diff --git a/cairo/ethereum/cancun/fork_types.cairo b/cairo/ethereum/cancun/fork_types.cairo index 22609747..f1aefe05 100644 --- a/cairo/ethereum/cancun/fork_types.cairo +++ b/cairo/ethereum/cancun/fork_types.cairo @@ -43,8 +43,12 @@ struct TupleAddressBytes32 { value: TupleAddressBytes32Struct*, } +struct HashedTupleAddressBytes32 { + value: felt, +} + struct SetTupleAddressBytes32DictAccess { - key: TupleAddressBytes32, + key: HashedTupleAddressBytes32, prev_value: bool, new_value: bool, } diff --git a/cairo/ethereum/cancun/vm.cairo b/cairo/ethereum/cancun/vm.cairo index 0f77e5a7..09b774e6 100644 --- a/cairo/ethereum/cancun/vm.cairo +++ b/cairo/ethereum/cancun/vm.cairo @@ -228,6 +228,7 @@ namespace EvmImpl { code=evm.value.code, gas_left=evm.value.gas_left, env=new_env, + valid_jump_destinations=evm.value.valid_jump_destinations, logs=evm.value.logs, refund_counter=evm.value.refund_counter, running=evm.value.running, @@ -608,3 +609,28 @@ namespace EvmImpl { return (); } } + +namespace EnvImpl { + func set_state{env: Environment}(new_state: State) { + tempvar env = Environment( + new EnvironmentStruct( + caller=env.value.caller, + block_hashes=env.value.block_hashes, + origin=env.value.origin, + coinbase=env.value.coinbase, + number=env.value.number, + base_fee_per_gas=env.value.base_fee_per_gas, + gas_limit=env.value.gas_limit, + gas_price=env.value.gas_price, + time=env.value.time, + prev_randao=env.value.prev_randao, + state=new_state, + chain_id=env.value.chain_id, + excess_blob_gas=env.value.excess_blob_gas, + blob_versioned_hashes=env.value.blob_versioned_hashes, + transient_storage=env.value.transient_storage, + ), + ); + return (); + } +} diff --git a/cairo/ethereum/cancun/vm/instructions/storage.cairo b/cairo/ethereum/cancun/vm/instructions/storage.cairo new file mode 100644 index 00000000..b0c29919 --- /dev/null +++ b/cairo/ethereum/cancun/vm/instructions/storage.cairo @@ -0,0 +1,95 @@ +from ethereum.cancun.vm.stack import pop, push +from ethereum.cancun.vm import Evm, EvmImpl, Environment, EnvImpl +from ethereum.cancun.vm.exceptions import ExceptionalHalt +from ethereum.cancun.vm.gas import charge_gas, GasConstants +from ethereum.cancun.state import get_storage +from ethereum.cancun.fork_types import ( + SetTupleAddressBytes32, + SetTupleAddressBytes32DictAccess, + SetTupleAddressBytes32Struct, + TupleAddressBytes32, + TupleAddressBytes32Struct, + Address, +) +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import Uint, U256, U256Struct +from ethereum.utils.numeric import U256_to_be_bytes +from src.utils.dict import hashdict_read, hashdict_write +from src.utils.utils import Helpers + +from starkware.cairo.common.cairo_builtins import PoseidonBuiltin, BitwiseBuiltin +from starkware.cairo.lang.compiler.lib.registers import get_fp_and_pc +from starkware.cairo.common.alloc import alloc +from starkware.cairo.common.dict_access import DictAccess + +// @notice Loads to the stack, the value corresponding to a certain key from the +// storage of the current account. +func sload{range_check_ptr, bitwise_ptr: BitwiseBuiltin*, poseidon_ptr: PoseidonBuiltin*, evm: Evm}( + ) -> ExceptionalHalt* { + alloc_locals; + // STACK + let stack = evm.value.stack; + with stack { + let (key, err) = pop(); + if (cast(err, felt) != 0) { + return err; + } + } + // Gas + // Get the entry from the accessed storage keys + let key_bytes32 = U256_to_be_bytes(key); + tempvar accessed_tuple = TupleAddressBytes32( + new TupleAddressBytes32Struct(evm.value.message.value.current_target, key_bytes32) + ); + let (serialized_keys: felt*) = alloc(); + assert serialized_keys[0] = accessed_tuple.value.address.value; + assert serialized_keys[1] = accessed_tuple.value.bytes32.value.low; + assert serialized_keys[2] = accessed_tuple.value.bytes32.value.high; + let dict_ptr = cast(evm.value.accessed_storage_keys.value.dict_ptr, DictAccess*); + with dict_ptr { + let (is_present) = hashdict_read(3, serialized_keys); + if (is_present == 0) { + // If the entry is not in the accessed storage keys, add it + hashdict_write(3, serialized_keys, 1); + tempvar poseidon_ptr = poseidon_ptr; + tempvar dict_ptr = dict_ptr; + } else { + tempvar poseidon_ptr = poseidon_ptr; + tempvar dict_ptr = dict_ptr; + } + let poseidon_ptr = cast([ap - 2], PoseidonBuiltin*); + let dict_ptr = cast([ap - 1], DictAccess*); + + let access_gas_cost = (is_present * GasConstants.GAS_WARM_ACCESS) + (1 - is_present) * + GasConstants.GAS_COLD_SLOAD; + let err = charge_gas(Uint(access_gas_cost)); + if (cast(err, felt) != 0) { + return err; + } + } + let new_dict_ptr = cast(dict_ptr, SetTupleAddressBytes32DictAccess*); + tempvar new_accessed_storage_keys = SetTupleAddressBytes32( + new SetTupleAddressBytes32Struct( + evm.value.accessed_storage_keys.value.dict_ptr_start, new_dict_ptr + ), + ); + + // OPERATION + let state = evm.value.env.value.state; + with state, stack { + let value = get_storage(evm.value.message.value.current_target, Bytes32(key.value)); + let err = push(value); + if (cast(err, felt) != 0) { + return err; + } + } + + // PROGRAM COUNTER + let env = evm.value.env; + EnvImpl.set_state{env=env}(state); + EvmImpl.set_env(env); + EvmImpl.set_pc_stack(Uint(evm.value.pc.value + 1), stack); + EvmImpl.set_accessed_storage_keys(new_accessed_storage_keys); + let ok = cast(0, ExceptionalHalt*); + return ok; +} diff --git a/cairo/tests/ethereum/cancun/vm/instructions/test_storage.py b/cairo/tests/ethereum/cancun/vm/instructions/test_storage.py new file mode 100644 index 00000000..5062a7a3 --- /dev/null +++ b/cairo/tests/ethereum/cancun/vm/instructions/test_storage.py @@ -0,0 +1,22 @@ +import pytest +from hypothesis import given + +from ethereum.cancun.vm.instructions.storage import sload +from tests.utils.args_gen import Evm +from tests.utils.evm_builder import EvmBuilder + +pytestmark = pytest.mark.python_vm + + +class TestStorage: + @given(evm=EvmBuilder().with_stack().with_env().with_gas_left().build()) + def test_sload(self, cairo_run, evm: Evm): + try: + cairo_evm = cairo_run("sload", evm) + except Exception as cairo_error: + with pytest.raises(type(cairo_error)): + sload(evm) + return + + sload(evm) + assert evm == cairo_evm diff --git a/cairo/tests/utils/args_gen.py b/cairo/tests/utils/args_gen.py index 44e518ca..f668a9e9 100644 --- a/cairo/tests/utils/args_gen.py +++ b/cairo/tests/utils/args_gen.py @@ -132,7 +132,18 @@ from ethereum.rlp import Extended, Simple from tests.utils.helpers import flatten -HASHED_TYPES = [Bytes, bytes, bytearray, str, U256, Hash32, Bytes32, Bytes256] +HASHED_TYPES = [ + Bytes, + bytes, + bytearray, + str, + U256, + Hash32, + Bytes32, + Bytes256, + Tuple[Bytes20, Bytes32], + tuple[Bytes20, Bytes32], +] class Memory(bytearray): @@ -487,15 +498,26 @@ def _gen_arg( ) struct_ptr = segments.add() data = [ - _gen_arg(dict_manager, segments, element_type, value) + _gen_arg( + dict_manager, segments, element_type, value, hash_mode=hash_mode + ) for element_type, value in zip(element_types, arg) ] + if hash_mode: + return tuple(flatten(data)) segments.load_data(struct_ptr, data) return struct_ptr # Case list, which is represented as a pointer to a struct with a pointer to the elements and the size. instances_ptr = segments.add() - data = [_gen_arg(dict_manager, segments, get_args(arg_type)[0], x) for x in arg] + data = [ + _gen_arg( + dict_manager, segments, get_args(arg_type)[0], x, hash_mode=hash_mode + ) + for x in arg + ] + if hash_mode: + return tuple(flatten(data)) segments.load_data(instances_ptr, data) struct_ptr = segments.add() segments.load_data(struct_ptr, [instances_ptr, len(arg)]) diff --git a/cairo/tests/utils/serde.py b/cairo/tests/utils/serde.py index 0ea8dc6a..757c5542 100644 --- a/cairo/tests/utils/serde.py +++ b/cairo/tests/utils/serde.py @@ -544,16 +544,23 @@ def _serialize_mapping_struct( ) if value is not None: serialized_dict[preimage] = value + + elif get_origin(python_key_type) is tuple: + # If the key is a tuple, we're in the case of a Set[Tuple[Address, Bytes32]]] + # Where the key is the hashed tuple.] + hashed_key = poseidon_hash_many(key) + preimage_address = key[0].to_bytes(20, "little") + preimage_bytes32 = b"".join( + felt.to_bytes(16, "little") for felt in key[1:] + ) + preimage = (preimage_address, preimage_bytes32) + value = dict_segment_data.get( + hashed_key, serialized_original.get(preimage) + ) + if value is not None: + serialized_dict[preimage] = value else: raise ValueError(f"Unsupported key type: {python_key_type}") - - elif get_origin(python_key_type) is tuple: - # If the key is a tuple, we're in the case of a Set[Tuple[Address, Bytes32]]] - # In that case, the keys are not hashed __yet__, the dict simply registers the - # Cases where they key is not hashed, and the key is a pointer. - # In that case, we can just return the dict as is. - # TODO: we need to hash the keys here. - serialized_dict = {**dict_segment_data, **serialized_original} else: # Even if the dict is not hashed, we need to use the tracker # to differentiate between default-values _read_ and explicit writes. @@ -586,7 +593,7 @@ def key_transform(k): serialized_dict[preimage] = value if origin_cls is set: - return set(serialized_dict.keys()) + return set(k for k, v in serialized_dict.items() if v is True) return serialized_dict diff --git a/pyproject.toml b/pyproject.toml index 7fde4506..ef699118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dev-dependencies = [ "eth-account>=0.13.3", "eth-keys>=0.5.1", "eth-utils>=5.0.0", - "hypothesis>=6.112.1", + "hypothesis>=6.123.17", "ipykernel>=6.29.5", "pytest-xdist>=3.6.1", "pytest>=8.3.3", diff --git a/uv.lock b/uv.lock index 8c27b5b6..5629866d 100644 --- a/uv.lock +++ b/uv.lock @@ -1125,16 +1125,16 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.122.3" +version = "6.123.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/21/c4c755ad5763f4c882a855b9966ac019c2314e5578b5f5eb39d9fe9fe64d/hypothesis-6.122.3.tar.gz", hash = "sha256:f4c927ce0ec739fa6266e4572949d0b54e24a14601a2bc5fec8f78e16af57918", size = 414395 } +sdist = { url = "https://files.pythonhosted.org/packages/15/a7/695b2bcb4e8438e1d4683efa6877fc95be293a11251471d4552d6dd08259/hypothesis-6.123.17.tar.gz", hash = "sha256:5850893975b4f08e893ddc10f1d468bc7e011d59703f70fe06a10161e426e602", size = 418572 } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/cb/44fe7e78c3cfbcb01f905b3b252eff6396e2f2e8e88b2d27b5140a6ac474/hypothesis-6.122.3-py3-none-any.whl", hash = "sha256:f0f57036d3b95b979491602b32c95b6725c3af678cccb6165d8de330857f3c83", size = 475651 }, + { url = "https://files.pythonhosted.org/packages/11/8a/f1c166f048df4b314d0d38e9530b7af516a16160873d724bb416084d6990/hypothesis-6.123.17-py3-none-any.whl", hash = "sha256:5c949fb44935e32c61c64abfcc3975eec41f8205ade2223073ba074c1e078ead", size = 480880 }, ] [[package]] @@ -1562,7 +1562,7 @@ dev = [ { name = "eth-keys", specifier = ">=0.5.1" }, { name = "eth-utils", specifier = ">=5.0.0" }, { name = "gprof2dot", specifier = ">=2024.6.6" }, - { name = "hypothesis", specifier = ">=6.112.1" }, + { name = "hypothesis", specifier = ">=6.123.17" }, { name = "ipykernel", specifier = ">=6.29.5" }, { name = "jupyter", specifier = ">=1.1.1" }, { name = "polars", specifier = ">=1.18.0" },