diff --git a/cairo/ethereum/cancun/vm.cairo b/cairo/ethereum/cancun/vm.cairo index 4c1d7295c..9dca121cb 100644 --- a/cairo/ethereum/cancun/vm.cairo +++ b/cairo/ethereum/cancun/vm.cairo @@ -1,10 +1,15 @@ -from ethereum.cancun.blocks import Log, TupleLog +from starkware.cairo.common.cairo_builtins import PoseidonBuiltin +from ethereum.cancun.blocks import Log, TupleLog, TupleLogStruct from ethereum.cancun.fork_types import ( Address, ListHash32, SetAddress, + SetAddressStruct, + SetAddressDictAccess, TupleAddressBytes32, SetTupleAddressBytes32, + SetTupleAddressBytes32Struct, + SetTupleAddressBytes32DictAccess, TupleVersionedHash, VersionedHash, ) @@ -14,7 +19,19 @@ from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint, bool, SetUint from ethereum.cancun.transactions import To from ethereum.cancun.vm.stack import Stack +from ethereum.cancun.state import account_exists_and_is_empty from ethereum.cancun.vm.memory import Memory +from ethereum.utils.numeric import is_zero +from starkware.cairo.common.memcpy import memcpy +from src.utils.dict import ( + hashdict_write, + hashdict_read, + dict_update, + squash_and_update, + dict_squash, +) +from starkware.cairo.common.registers import get_fp_and_pc +from starkware.cairo.common.dict import DictAccess using OptionalEthereumException = EthereumException*; using OptionalEvm = Evm; @@ -88,6 +105,313 @@ struct MessageStruct { parent_evm: OptionalEvm, } +func incorporate_child_on_success{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, evm: Evm}( + child_evm: Evm +) { + alloc_locals; + + // TODO: unless we want to retro-prove all blocks, we could remove this logic. + // In block 2675119, the empty account at 0x3 (the RIPEMD160 precompile) was + // cleared despite running out of gas. This is an obscure edge case that can + // only happen to a precompile. + // According to the general rules governing clearing of empty accounts, the + // touch should have been reverted. Due to client bugs, this event went + // unnoticed and 0x3 has been exempted from the rule that touches are + // reverted in order to preserve this historical behaviour. + + let fp_and_pc = get_fp_and_pc(); + local __fp__: felt* = fp_and_pc.fp_val; + + let new_gas_left = Uint(evm.value.gas_left.value + child_evm.value.gas_left.value); + + let dst = evm.value.logs.value.data + evm.value.logs.value.len; + let src = child_evm.value.logs.value.data; + let len = child_evm.value.logs.value.len; + memcpy(dst, src, len); + tempvar new_logs = TupleLog( + new TupleLogStruct(data=evm.value.logs.value.data, len=evm.value.logs.value.len + len) + ); + + let new_refund_counter = evm.value.refund_counter + child_evm.value.refund_counter; + + // Squash & update accounts_to_delete into parent + let accounts_to_delete = evm.value.accounts_to_delete; + let accounts_to_delete_start = accounts_to_delete.value.dict_ptr_start; + let accounts_to_delete_end = accounts_to_delete.value.dict_ptr; + let new_accounts_to_delete_end = squash_and_update( + cast(child_evm.value.accounts_to_delete.value.dict_ptr_start, DictAccess*), + cast(child_evm.value.accounts_to_delete.value.dict_ptr, DictAccess*), + cast(accounts_to_delete_end, DictAccess*), + ); + tempvar new_accounts_to_delete = SetAddress( + new SetAddressStruct( + dict_ptr_start=cast(accounts_to_delete_start, SetAddressDictAccess*), + dict_ptr=cast(new_accounts_to_delete_end, SetAddressDictAccess*), + ), + ); + + // Squash & update touched_accounts into parent + let child_touched_accounts_start = child_evm.value.touched_accounts.value.dict_ptr_start; + let child_touched_accounts_end = child_evm.value.touched_accounts.value.dict_ptr; + let touched_accounts = evm.value.touched_accounts; + let touched_accounts_end = touched_accounts.value.dict_ptr; + let new_touched_accounts_end = squash_and_update( + cast(child_touched_accounts_start, DictAccess*), + cast(child_touched_accounts_end, DictAccess*), + cast(touched_accounts_end, DictAccess*), + ); + + // Check if child message target account exists and is empty + let env = evm.value.env; + let state = env.value.state; + let exists_and_is_empty = account_exists_and_is_empty{state=state}( + child_evm.value.message.value.current_target + ); + if (exists_and_is_empty.value != 0) { + hashdict_write{dict_ptr=new_touched_accounts_end}( + 1, &child_evm.value.message.value.current_target.value, 1 + ); + } else { + tempvar poseidon_ptr = poseidon_ptr; + tempvar new_touched_accounts_end = new_touched_accounts_end; + } + let poseidon_ptr = cast([ap - 2], PoseidonBuiltin*); + let new_touched_accounts_end = cast([ap - 1], DictAccess*); + EnvImpl.set_state{env=env}(state); + + tempvar new_touched_accounts = SetAddress( + new SetAddressStruct( + dict_ptr_start=cast(touched_accounts.value.dict_ptr_start, SetAddressDictAccess*), + dict_ptr=cast(new_touched_accounts_end, SetAddressDictAccess*), + ), + ); + + // Squash & update accessed_addresses into parent + let accessed_addresses = evm.value.accessed_addresses; + let accessed_addresses_start = accessed_addresses.value.dict_ptr_start; + let accessed_addresses_end = accessed_addresses.value.dict_ptr; + let new_accessed_addresses_end = squash_and_update( + cast(child_evm.value.accessed_addresses.value.dict_ptr_start, DictAccess*), + cast(child_evm.value.accessed_addresses.value.dict_ptr, DictAccess*), + cast(accessed_addresses_end, DictAccess*), + ); + + tempvar new_accessed_addresses = SetAddress( + new SetAddressStruct( + dict_ptr_start=cast(accessed_addresses_start, SetAddressDictAccess*), + dict_ptr=cast(new_accessed_addresses_end, SetAddressDictAccess*), + ), + ); + + // Squash & update accessed_storage_keys into parent + let accessed_storage_keys = evm.value.accessed_storage_keys; + let accessed_storage_keys_start = accessed_storage_keys.value.dict_ptr_start; + let accessed_storage_keys_end = accessed_storage_keys.value.dict_ptr; + let new_accessed_storage_keys_end = squash_and_update( + cast(child_evm.value.accessed_storage_keys.value.dict_ptr_start, DictAccess*), + cast(child_evm.value.accessed_storage_keys.value.dict_ptr, DictAccess*), + cast(accessed_storage_keys_end, DictAccess*), + ); + + tempvar new_accessed_storage_keys = SetTupleAddressBytes32( + new SetTupleAddressBytes32Struct( + dict_ptr_start=cast(accessed_storage_keys_start, SetTupleAddressBytes32DictAccess*), + dict_ptr=cast(new_accessed_storage_keys_end, SetTupleAddressBytes32DictAccess*), + ), + ); + + // Squash dropped dicts + dict_squash( + cast(child_evm.value.stack.value.dict_ptr_start, DictAccess*), + cast(child_evm.value.stack.value.dict_ptr, DictAccess*), + ); + + dict_squash( + cast(child_evm.value.memory.value.dict_ptr_start, DictAccess*), + cast(child_evm.value.memory.value.dict_ptr, DictAccess*), + ); + + dict_squash( + cast(child_evm.value.valid_jump_destinations.value.dict_ptr_start, DictAccess*), + cast(child_evm.value.valid_jump_destinations.value.dict_ptr, DictAccess*), + ); + + tempvar evm = Evm( + new EvmStruct( + pc=evm.value.pc, + stack=evm.value.stack, + memory=evm.value.memory, + code=evm.value.code, + gas_left=new_gas_left, + env=env, + valid_jump_destinations=evm.value.valid_jump_destinations, + logs=new_logs, + refund_counter=new_refund_counter, + running=evm.value.running, + message=evm.value.message, + output=evm.value.output, + accounts_to_delete=new_accounts_to_delete, + touched_accounts=new_touched_accounts, + return_data=evm.value.return_data, + error=evm.value.error, + accessed_addresses=new_accessed_addresses, + accessed_storage_keys=new_accessed_storage_keys, + ), + ); + + return (); +} + +// @notice Incorporates the child EVM in its parent in case of an error. +// @dev This function is responsible for correctly squashing all the child's dicts that are dropped. +func incorporate_child_on_error{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, evm: Evm}( + child_evm: Evm +) { + alloc_locals; + + let fp_and_pc = get_fp_and_pc(); + local __fp__: felt* = fp_and_pc.fp_val; + + // Special handling for RIPEMD160 precompile address (0x3) + // TODO: Move this to precompiled_contracts.cairo + const RIPEMD160_ADDRESS = 0x0300000000000000000000000000000000000000; + + // Check if RIPEMD160 address is in child's touched accounts + let child_touched_accounts = child_evm.value.touched_accounts; + let child_touched_accounts_start = child_touched_accounts.value.dict_ptr_start; + let child_touched_accounts_end = cast(child_touched_accounts.value.dict_ptr, DictAccess*); + let (ripemd_touched) = hashdict_read{dict_ptr=child_touched_accounts_end}( + 1, new RIPEMD160_ADDRESS + ); + + // Soundness requirement: squash all child dicts - including ones from message and env. + + // EVM // + dict_squash( + cast(child_touched_accounts_start, DictAccess*), + cast(child_touched_accounts_end, DictAccess*), + ); + + let child_accessed_addresses = child_evm.value.accessed_addresses; + let child_accessed_addresses_start = child_accessed_addresses.value.dict_ptr_start; + let child_accessed_addresses_end = cast(child_accessed_addresses.value.dict_ptr, DictAccess*); + dict_squash( + cast(child_accessed_addresses_start, DictAccess*), + cast(child_accessed_addresses_end, DictAccess*), + ); + + let child_accessed_storage_keys = child_evm.value.accessed_storage_keys; + let child_accessed_storage_keys_start = child_accessed_storage_keys.value.dict_ptr_start; + let child_accessed_storage_keys_end = cast( + child_accessed_storage_keys.value.dict_ptr, DictAccess* + ); + dict_squash( + cast(child_accessed_storage_keys_start, DictAccess*), + cast(child_accessed_storage_keys_end, DictAccess*), + ); + + let accounts_to_delete = child_evm.value.accounts_to_delete; + let accounts_to_delete_start = accounts_to_delete.value.dict_ptr_start; + let accounts_to_delete_end = cast(accounts_to_delete.value.dict_ptr, DictAccess*); + dict_squash( + cast(accounts_to_delete_start, DictAccess*), cast(accounts_to_delete_end, DictAccess*) + ); + + let valid_jump_destinations = child_evm.value.valid_jump_destinations; + let valid_jump_destinations_start = valid_jump_destinations.value.dict_ptr_start; + let valid_jump_destinations_end = cast(valid_jump_destinations.value.dict_ptr, DictAccess*); + dict_squash( + cast(valid_jump_destinations_start, DictAccess*), + cast(valid_jump_destinations_end, DictAccess*), + ); + + let stack = child_evm.value.stack; + let stack_start = stack.value.dict_ptr_start; + let stack_end = cast(stack.value.dict_ptr, DictAccess*); + dict_squash(cast(stack_start, DictAccess*), cast(stack_end, DictAccess*)); + + let memory = child_evm.value.memory; + let memory_start = memory.value.dict_ptr_start; + let memory_end = cast(memory.value.dict_ptr, DictAccess*); + dict_squash(cast(memory_start, DictAccess*), cast(memory_end, DictAccess*)); + + // No need to squash the message's `accessed_addresses` and `accessed_storage_keys` because + // they were moved into the Evm, which we just squashed. + + // No need to squash the env's `state` and `transient_storage` because it was handled in `rollback_transaction`, + // when we squashed and appended the prev keys to the parent's state and transient storage segments. + + // Check if child message target is RIPEMD160 address + let is_ripemd_target = is_zero( + child_evm.value.message.value.current_target.value - RIPEMD160_ADDRESS + ); + if (is_ripemd_target != 0) { + let env = evm.value.env; + let state = env.value.state; + let exists_and_is_empty = account_exists_and_is_empty{state=state}( + child_evm.value.message.value.current_target + ); + EnvImpl.set_state{env=env}(state); + EvmImpl.set_env(env); + tempvar evm = evm; + tempvar poseidon_ptr = poseidon_ptr; + tempvar write_ripemd = exists_and_is_empty.value + ripemd_touched; + } else { + tempvar evm = evm; + tempvar poseidon_ptr = poseidon_ptr; + tempvar write_ripemd = ripemd_touched; + } + let evm_ = cast([ap - 3], EvmStruct*); + let poseidon_ptr = cast([ap - 2], PoseidonBuiltin*); + let write_ripemd = [ap - 1]; + tempvar evm = Evm(evm_); + + let touched_accounts = evm.value.touched_accounts; + let touched_accounts_end = cast(touched_accounts.value.dict_ptr, DictAccess*); + + if (write_ripemd != 0) { + hashdict_write{dict_ptr=touched_accounts_end}(1, new RIPEMD160_ADDRESS, 1); + } else { + tempvar poseidon_ptr = poseidon_ptr; + tempvar touched_accounts_end = touched_accounts_end; + } + let poseidon_ptr = cast([ap - 2], PoseidonBuiltin*); + let new_touched_accounts_end = cast([ap - 1], DictAccess*); + + tempvar new_touched_accounts = SetAddress( + new SetAddressStruct( + dict_ptr_start=cast(touched_accounts.value.dict_ptr_start, SetAddressDictAccess*), + dict_ptr=cast(new_touched_accounts_end, SetAddressDictAccess*), + ), + ); + let new_gas_left = Uint(evm.value.gas_left.value + child_evm.value.gas_left.value); + + tempvar evm = Evm( + new EvmStruct( + pc=evm.value.pc, + stack=evm.value.stack, + memory=evm.value.memory, + code=evm.value.code, + gas_left=new_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=new_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 (); +} + namespace EvmImpl { func set_pc{evm: Evm}(new_pc: Uint) { tempvar evm = Evm( diff --git a/cairo/src/utils/dict.cairo b/cairo/src/utils/dict.cairo index d45113673..03781a62e 100644 --- a/cairo/src/utils/dict.cairo +++ b/cairo/src/utils/dict.cairo @@ -8,7 +8,7 @@ from starkware.cairo.common.squash_dict import squash_dict from starkware.cairo.common.uint256 import Uint256 from ethereum_types.numeric import U256, U256Struct from ethereum_types.bytes import Bytes32 -from ethereum.utils.numeric import U256__eq__, is_not_zero +from ethereum.utils.numeric import U256__eq__, is_not_zero, is_zero from ethereum.cancun.fork_types import ( Address, Account, @@ -255,3 +255,54 @@ func get_keys_for_address_prefix{dict_ptr: DictAccess*}( tempvar res = ListTupleAddressBytes32(new ListTupleAddressBytes32Struct(keys, keys_len)); return res; } + +// @notice squashes the `src` dict and writes all its values to the `dst` dict. +// @dev If the `dst` dict is not empty, the values are added to the existing values. +// @param src: The source dict to squash. +// @param dst: The end pointer of the destination dict to write the values to. +// @returns The new end pointer of the destination dict +func squash_and_update{range_check_ptr}( + src_start: DictAccess*, src_end: DictAccess*, dst: DictAccess* +) -> DictAccess* { + alloc_locals; + + let (squashed_src_start, local squashed_src_end) = dict_squash(src_start, src_end); + let len = squashed_src_end - squashed_src_start; + local range_check_ptr = range_check_ptr; + + if (len == 0) { + return dst; + } + + let dict_ptr = squashed_src_end; + let parent_dict_end = dst; + %{ merge_dict_tracker_with_parent %} + + // Loop on all keys and write the new_value to the dst dict. + tempvar squashed_src = squashed_src_start; + tempvar dst_end = dst; + + loop: + let squashed_src = cast([ap - 2], DictAccess*); + let dst_end = cast([ap - 1], DictAccess*); + let key = squashed_src.key; + let new_value = squashed_src.new_value; + + let is_done = is_zero(squashed_src_end - squashed_src); + static_assert dst_end == [ap - 5]; + jmp done if is_done != 0; + + assert dst_end.key = key; + assert dst_end.new_value = new_value; + + tempvar squashed_src = squashed_src + DictAccess.SIZE; + tempvar dst_end = dst_end + DictAccess.SIZE; + jmp loop; + + done: + let current_tracker_ptr = dst; + let new_tracker_ptr = cast([ap - 5], DictAccess*); + %{ update_dict_tracker %} + + return new_tracker_ptr; +} diff --git a/cairo/tests/ethereum/cancun/test_vm.py b/cairo/tests/ethereum/cancun/test_vm.py new file mode 100644 index 000000000..e3f8b3ecc --- /dev/null +++ b/cairo/tests/ethereum/cancun/test_vm.py @@ -0,0 +1,44 @@ +from hypothesis import given + +from ethereum.cancun.vm import incorporate_child_on_error, incorporate_child_on_success +from tests.utils.args_gen import Evm +from tests.utils.errors import strict_raises +from tests.utils.evm_builder import EvmBuilder + +local_strategy = ( + EvmBuilder() + .with_gas_left() + .with_logs() + .with_accessed_addresses() + .with_accessed_storage_keys() + .with_accounts_to_delete() + .with_touched_accounts() + .with_refund_counter() + .build() +) + + +class TestVm: + @given(evm=local_strategy, child_evm=local_strategy) + def test_incorporate_child_on_success(self, cairo_run, evm: Evm, child_evm: Evm): + try: + evm_cairo = cairo_run("incorporate_child_on_success", evm, child_evm) + except Exception as e: + with strict_raises(type(e)): + incorporate_child_on_success(evm, child_evm) + return + + incorporate_child_on_success(evm, child_evm) + assert evm_cairo == evm + + @given(evm=local_strategy, child_evm=local_strategy) + def test_incorporate_child_on_error(self, cairo_run, evm: Evm, child_evm: Evm): + try: + evm_cairo = cairo_run("incorporate_child_on_error", evm, child_evm) + except Exception as e: + with strict_raises(type(e)): + incorporate_child_on_error(evm, child_evm) + return + + incorporate_child_on_error(evm, child_evm) + assert evm_cairo == evm diff --git a/cairo/tests/src/utils/test_dict.cairo b/cairo/tests/src/utils/test_dict.cairo index d4c8382ae..9a75879ff 100644 --- a/cairo/tests/src/utils/test_dict.cairo +++ b/cairo/tests/src/utils/test_dict.cairo @@ -8,10 +8,12 @@ from ethereum_types.numeric import Uint from ethereum.cancun.fork_types import Address, TupleAddressBytes32 from ethereum.cancun.state import ( MappingTupleAddressBytes32U256, + MappingTupleAddressBytes32U256Struct, + TupleAddressBytes32U256DictAccess, ListTupleAddressBytes32, ListTupleAddressBytes32Struct, ) -from src.utils.dict import prev_values, dict_update, get_keys_for_address_prefix +from src.utils.dict import prev_values, dict_update, get_keys_for_address_prefix, squash_and_update func test_prev_values{range_check_ptr}() -> (prev_values_start_ptr: felt*) { alloc_locals; @@ -82,3 +84,25 @@ func test_get_keys_for_address_prefix{range_check_ptr}( let res = get_keys_for_address_prefix{dict_ptr=dict_ptr}(prefix_len, prefix); return res; } + +func test_squash_and_update{range_check_ptr}( + src_dict: MappingTupleAddressBytes32U256, dst_dict: MappingTupleAddressBytes32U256 +) -> MappingTupleAddressBytes32U256 { + alloc_locals; + + let src_start = src_dict.value.dict_ptr_start; + let src_end = src_dict.value.dict_ptr; + let dst = dst_dict.value.dict_ptr; + let new_dst_end = squash_and_update( + cast(src_start, DictAccess*), cast(src_end, DictAccess*), cast(dst, DictAccess*) + ); + + tempvar new_dst_dict = MappingTupleAddressBytes32U256( + new MappingTupleAddressBytes32U256Struct( + dict_ptr_start=cast(dst_dict.value.dict_ptr_start, TupleAddressBytes32U256DictAccess*), + dict_ptr=cast(new_dst_end, TupleAddressBytes32U256DictAccess*), + parent_dict=dst_dict.value.parent_dict, + ), + ); + return new_dst_dict; +} diff --git a/cairo/tests/src/utils/test_dict.py b/cairo/tests/src/utils/test_dict.py index b04d3f1d8..91f01dd4c 100644 --- a/cairo/tests/src/utils/test_dict.py +++ b/cairo/tests/src/utils/test_dict.py @@ -114,3 +114,14 @@ def test_get_keys_for_address_prefix(cairo_run_py, dict_with_prefix): keys = cairo_run_py("test_get_keys_for_address_prefix", prefix, dict_entries) keys = [keys] if not isinstance(keys, list) else keys assert keys == [key for key in dict_entries.keys() if key[0] == prefix] + + +@given(src_dict=..., dst_dict=...) +def test_squash_and_update( + cairo_run_py, + src_dict: Mapping[Tuple[Address, Bytes32], U256], + dst_dict: Mapping[Tuple[Address, Bytes32], U256], +): + new_dst_dict = cairo_run_py("test_squash_and_update", src_dict, dst_dict) + dst_dict.update(src_dict) + assert new_dst_dict == dst_dict diff --git a/cairo/tests/utils/evm_builder.py b/cairo/tests/utils/evm_builder.py index 9630d9611..270d966e4 100644 --- a/cairo/tests/utils/evm_builder.py +++ b/cairo/tests/utils/evm_builder.py @@ -3,12 +3,15 @@ from ethereum_types.numeric import U64, U256, Bytes32, Uint from hypothesis import strategies as st +from ethereum.cancun.blocks import Log from ethereum.cancun.fork_types import Address from ethereum.cancun.state import TransientStorage from ethereum.exceptions import EthereumException from tests.utils.args_gen import Environment, Evm, Stack from tests.utils.message_builder import MessageBuilder from tests.utils.strategies import ( + MAX_ACCOUNTS_TO_DELETE_SIZE, + MAX_TOUCHED_ACCOUNTS_SIZE, Memory, address_zero, code, @@ -116,7 +119,7 @@ def with_accessed_addresses( return self def with_accessed_storage_keys( - self, strategy=st.sets(st.from_type(Tuple[Address, U256]), max_size=10) + self, strategy=st.sets(st.from_type(Tuple[Address, Bytes32]), max_size=10) ): self._accessed_storage_keys = strategy return self @@ -125,6 +128,30 @@ def with_return_data(self, strategy=st.binary(min_size=0, max_size=1024)): self._return_data = strategy return self + def with_logs(self, strategy=st.tuples(st.from_type(Log))): + self._logs = strategy + return self + + def with_accounts_to_delete( + self, + strategy=st.sets(st.from_type(Address), max_size=MAX_ACCOUNTS_TO_DELETE_SIZE), + ): + self._accounts_to_delete = strategy + return self + + def with_touched_accounts( + self, + strategy=st.sets(st.from_type(Address), max_size=MAX_TOUCHED_ACCOUNTS_SIZE), + ): + self._touched_accounts = strategy + return self + + def with_refund_counter( + self, strategy=st.integers(min_value=-(2**64) - 1, max_value=2**64) + ): + self._refund_counter = strategy + return self + def build(self): return st.builds( Evm, diff --git a/cairo/tests/utils/strategies.py b/cairo/tests/utils/strategies.py index 8e22f7878..0e30c7801 100644 --- a/cairo/tests/utils/strategies.py +++ b/cairo/tests/utils/strategies.py @@ -104,6 +104,10 @@ MAX_TRANSIENT_STORAGE_SNAPSHOTS_SIZE = int( os.getenv("HYPOTHESIS_MAX_TRANSIENT_STORAGE_SNAPSHOTS_SIZE", 10) ) +MAX_ACCOUNTS_TO_DELETE_SIZE = int( + os.getenv("HYPOTHESIS_MAX_ACCOUNTS_TO_DELETE_SIZE", 10) +) +MAX_TOUCHED_ACCOUNTS_SIZE = int(os.getenv("HYPOTHESIS_MAX_TOUCHED_ACCOUNTS_SIZE", 10)) small_bytes = st.binary(min_size=0, max_size=256) diff --git a/crates/cairo-addons/src/vm/hint_definitions/dict.rs b/crates/cairo-addons/src/vm/hint_definitions/dict.rs index 43c2e87e0..dbc42cb1d 100644 --- a/crates/cairo-addons/src/vm/hint_definitions/dict.rs +++ b/crates/cairo-addons/src/vm/hint_definitions/dict.rs @@ -15,8 +15,13 @@ use cairo_vm::{ use crate::vm::hints::Hint; -pub const HINTS: &[fn() -> Hint] = - &[dict_new_empty, copy_dict_segment, merge_dict_tracker_with_parent, update_dict_tracker]; +pub const HINTS: &[fn() -> Hint] = &[ + dict_new_empty, + dict_squash, + copy_dict_segment, + merge_dict_tracker_with_parent, + update_dict_tracker, +]; pub fn dict_new_empty() -> Hint { Hint::new( @@ -34,6 +39,34 @@ pub fn dict_new_empty() -> Hint { ) } +pub fn dict_squash() -> Hint { + Hint::new( + String::from("dict_squash"), + |vm: &mut VirtualMachine, + exec_scopes: &mut ExecutionScopes, + ids_data: &HashMap, + ap_tracking: &ApTracking, + _constants: &HashMap| + -> Result<(), HintError> { + // Get the dict_accesses_end pointer + let dict_accesses_end = + get_ptr_from_var_name("dict_accesses_end", vm, ids_data, ap_tracking)?; + + // Get dict manager and copy data from the source dictionary + let dict_manager_ref = exec_scopes.get_dict_manager()?; + let mut dict_manager = dict_manager_ref.borrow_mut(); + let tracker = dict_manager.get_tracker(dict_accesses_end)?; + let copied_data = tracker.get_dictionary_copy(); + + // Create new dict with copied data + let base = dict_manager.new_dict(vm, copied_data)?; + + // Insert the new dictionary's base pointer into ap + insert_value_into_ap(vm, base) + }, + ) +} + pub fn copy_dict_segment() -> Hint { Hint::new( String::from("copy_dict_segment"),