Skip to content

Commit

Permalink
feat: sload and hashed sets
Browse files Browse the repository at this point in the history
  • Loading branch information
enitrat committed Jan 15, 2025
1 parent 1436c75 commit 20bc893
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 18 deletions.
6 changes: 5 additions & 1 deletion cairo/ethereum/cancun/fork_types.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ struct TupleAddressBytes32 {
value: TupleAddressBytes32Struct*,
}

struct HashedTupleAddressBytes32 {
value: felt,
}

struct SetTupleAddressBytes32DictAccess {
key: TupleAddressBytes32,
key: HashedTupleAddressBytes32,
prev_value: bool,
new_value: bool,
}
Expand Down
26 changes: 26 additions & 0 deletions cairo/ethereum/cancun/vm.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 ();
}
}
95 changes: 95 additions & 0 deletions cairo/ethereum/cancun/vm/instructions/storage.cairo
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 22 additions & 0 deletions cairo/tests/ethereum/cancun/vm/instructions/test_storage.py
Original file line number Diff line number Diff line change
@@ -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
28 changes: 25 additions & 3 deletions cairo/tests/utils/args_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)])
Expand Down
25 changes: 16 additions & 9 deletions cairo/tests/utils/serde.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 20bc893

Please sign in to comment.