diff --git a/cairo/ethereum/cancun/vm.cairo b/cairo/ethereum/cancun/vm.cairo index 01d075bf..cc19a672 100644 --- a/cairo/ethereum/cancun/vm.cairo +++ b/cairo/ethereum/cancun/vm.cairo @@ -150,6 +150,7 @@ namespace EvmImpl { 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, diff --git a/cairo/ethereum/cancun/vm/gas.cairo b/cairo/ethereum/cancun/vm/gas.cairo index 2ad23863..d6ac28bb 100644 --- a/cairo/ethereum/cancun/vm/gas.cairo +++ b/cairo/ethereum/cancun/vm/gas.cairo @@ -1,20 +1,79 @@ -from ethereum_types.numeric import U256, Uint, U64 +from ethereum_types.numeric import U256, Uint, U64, U256Struct from ethereum.utils.numeric import is_zero, divmod, taylor_exponential, min, ceil32 from ethereum_types.bytes import BytesStruct from ethereum.cancun.blocks import Header from ethereum.cancun.transactions import Transaction +from ethereum_types.others import ListTupleU256U256, TupleU256U256 from ethereum.cancun.vm import Evm, EvmStruct, EvmImpl from ethereum.cancun.vm.exceptions import ExceptionalHalt, OutOfGasError +from ethereum.cancun.vm.memory import Memory from starkware.cairo.common.math_cmp import is_le, is_not_zero, RC_BOUND from starkware.cairo.common.math import assert_le_felt +from starkware.cairo.common.uint256 import ALL_ONES, uint256_eq, uint256_le -const TARGET_BLOB_GAS_PER_BLOCK = 393216; -const GAS_INIT_CODE_WORD_COST = 2; -const GAS_MEMORY = 3; -const GAS_PER_BLOB = 2 ** 17; -const MIN_BLOB_GASPRICE = 1; -const BLOB_GASPRICE_UPDATE_FRACTION = 3338477; +from src.utils.uint256 import uint256_add +from src.constants import Constants + +namespace GasConstants { + const GAS_JUMPDEST = 1; + const GAS_BASE = 2; + const GAS_VERY_LOW = 3; + const GAS_STORAGE_SET = 20000; + const GAS_STORAGE_UPDATE = 5000; + const GAS_STORAGE_CLEAR_REFUND = 4800; + const GAS_LOW = 5; + const GAS_MID = 8; + const GAS_HIGH = 10; + const GAS_EXPONENTIATION = 10; + const GAS_EXPONENTIATION_PER_BYTE = 50; + const GAS_MEMORY = 3; + const GAS_KECCAK256 = 30; + const GAS_KECCAK256_WORD = 6; + const GAS_COPY = 3; + const GAS_BLOCK_HASH = 20; + const GAS_LOG = 375; + const GAS_LOG_DATA = 8; + const GAS_LOG_TOPIC = 375; + const GAS_CREATE = 32000; + const GAS_CODE_DEPOSIT = 200; + const GAS_ZERO = 0; + const GAS_NEW_ACCOUNT = 25000; + const GAS_CALL_VALUE = 9000; + const GAS_CALL_STIPEND = 2300; + const GAS_SELF_DESTRUCT = 5000; + const GAS_SELF_DESTRUCT_NEW_ACCOUNT = 25000; + const GAS_ECRECOVER = 3000; + const GAS_SHA256 = 60; + const GAS_SHA256_WORD = 12; + const GAS_RIPEMD160 = 600; + const GAS_RIPEMD160_WORD = 120; + const GAS_IDENTITY = 15; + const GAS_IDENTITY_WORD = 3; + const GAS_RETURN_DATA_COPY = 3; + const GAS_FAST_STEP = 5; + const GAS_BLAKE2_PER_ROUND = 1; + const GAS_COLD_SLOAD = 2100; + const GAS_COLD_ACCOUNT_ACCESS = 2600; + const GAS_WARM_ACCESS = 100; + const GAS_INIT_CODE_WORD_COST = 2; + const GAS_BLOBHASH_OPCODE = 3; + const GAS_POINT_EVALUATION = 50000; + + const TARGET_BLOB_GAS_PER_BLOCK = 393216; + const GAS_PER_BLOB = 2 ** 17; + const MIN_BLOB_GASPRICE = 1; + const BLOB_GASPRICE_UPDATE_FRACTION = 3338477; +} + +struct ExtendMemory { + value: ExtendMemoryStruct*, +} + +struct ExtendMemoryStruct { + cost: Uint, + expand_by: Uint, +} struct MessageCallGasStruct { cost: Uint, @@ -75,16 +134,73 @@ func charge_gas{range_check_ptr, evm: Evm}(amount: Uint) -> ExceptionalHalt* { return err; } +const MAX_MEMORY_COST = 0x20000000000017f7fffffffffffd; +const MAX_MEMORY_SIZE = 2 ** 64 - 32; + +// @dev: assumption: not called with size_in_bytes >= 2**64 +// only used by calculate_gas_extend_memory which saturates at 2**64-32 +// @dev: max output value given this saturation is MAX_MEMORY_COST func calculate_memory_gas_cost{range_check_ptr}(size_in_bytes: Uint) -> Uint { let size = ceil32(size_in_bytes); let (size_in_words, _) = divmod(size.value, 32); - let linear_cost = size_in_words * GAS_MEMORY; + let linear_cost = size_in_words * GasConstants.GAS_MEMORY; let quadratic_cost = size_in_words * size_in_words; let (quadratic_cost, _) = divmod(quadratic_cost, 512); let total_gas_cost = Uint(linear_cost + quadratic_cost); return total_gas_cost; } +// @dev: saturates extensions at (MAX_MEMORY_SIZE, MAX_MEMORY_COST) +func calculate_gas_extend_memory{range_check_ptr}( + memory: Memory, extensions: ListTupleU256U256 +) -> ExtendMemory { + alloc_locals; + let max_memory_offset = _max_offset(Uint(memory.value.len), extensions, 0); + let size_to_extend = Uint(max_memory_offset.value - memory.value.len); + let already_paid = calculate_memory_gas_cost(Uint(memory.value.len)); + let total_cost = calculate_memory_gas_cost(Uint(max_memory_offset.value)); + let to_be_paid = Uint(total_cost.value - already_paid.value); + tempvar res = ExtendMemory(new ExtendMemoryStruct(to_be_paid, size_to_extend)); + return res; +} + +// @dev saturates at 2**64 (Uint size) +func _max_offset{range_check_ptr}( + before_size: Uint, extensions: ListTupleU256U256, idx: felt +) -> Uint { + alloc_locals; + let extensions_len = extensions.value.len - idx; + if (extensions_len == 0) { + return before_size; + } + + let offset = extensions.value.data[idx].value.val_1; + let size = extensions.value.data[idx].value.val_2; + let (is_zero) = uint256_eq([size.value], U256Struct(0, 0)); + if (is_zero != 0) { + return _max_offset(before_size, extensions, idx + 1); + } + + let (max_offset, carry) = uint256_add([offset.value], [size.value]); + if (carry != 0) { + tempvar res = Uint(MAX_MEMORY_SIZE); + return _max_offset(res, extensions, idx + 1); + } + let (is_saturated) = uint256_le(U256Struct(MAX_MEMORY_SIZE + 1, 0), max_offset); + if (is_saturated != 0) { + tempvar res = Uint(MAX_MEMORY_SIZE); + return _max_offset(res, extensions, idx + 1); + } + + let after_size = ceil32(Uint(max_offset.low)); + let is_smaller = is_le(after_size.value, before_size.value); + if (is_smaller == 1) { + return _max_offset(before_size, extensions, idx + 1); + } + + return _max_offset(after_size, extensions, idx + 1); +} + func calculate_message_call_gas{range_check_ptr}( value: U256, gas: Uint, gas_left: Uint, memory_cost: Uint, extra_gas: Uint, call_stipend: Uint ) -> MessageCallGas { @@ -123,26 +239,27 @@ func max_message_call_gas{range_check_ptr}(gas: Uint) -> Uint { func init_code_cost{range_check_ptr}(init_code_length: Uint) -> Uint { let length = ceil32(init_code_length); let (words, _) = divmod(length.value, 32); - let cost = Uint(GAS_INIT_CODE_WORD_COST * words); + let cost = Uint(GasConstants.GAS_INIT_CODE_WORD_COST * words); return cost; } func calculate_excess_blob_gas{range_check_ptr}(parent_header: Header) -> U64 { let parent_blob_gas = parent_header.value.excess_blob_gas.value + parent_header.value.blob_gas_used.value; - let cond = is_le(parent_blob_gas, TARGET_BLOB_GAS_PER_BLOCK - 1); + let cond = is_le(parent_blob_gas, GasConstants.TARGET_BLOB_GAS_PER_BLOCK - 1); if (cond == 1) { let excess_blob_gas = U64(0); return excess_blob_gas; } - let excess_blob_gas = U64(parent_blob_gas - TARGET_BLOB_GAS_PER_BLOCK); + let excess_blob_gas = U64(parent_blob_gas - GasConstants.TARGET_BLOB_GAS_PER_BLOCK); return excess_blob_gas; } func calculate_total_blob_gas{range_check_ptr}(tx: Transaction) -> Uint { if (tx.value.blob_transaction.value != 0) { let total_blob_gas = Uint( - GAS_PER_BLOB * tx.value.blob_transaction.value.blob_versioned_hashes.value.len + GasConstants.GAS_PER_BLOB * + tx.value.blob_transaction.value.blob_versioned_hashes.value.len, ); return total_blob_gas; } @@ -152,7 +269,9 @@ func calculate_total_blob_gas{range_check_ptr}(tx: Transaction) -> Uint { func calculate_blob_gas_price{range_check_ptr}(excess_blob_gas: U64) -> Uint { let blob_gas_price = taylor_exponential( - Uint(MIN_BLOB_GASPRICE), Uint(excess_blob_gas.value), Uint(BLOB_GASPRICE_UPDATE_FRACTION) + Uint(GasConstants.MIN_BLOB_GASPRICE), + Uint(excess_blob_gas.value), + Uint(GasConstants.BLOB_GASPRICE_UPDATE_FRACTION), ); return blob_gas_price; } @@ -164,54 +283,3 @@ func calculate_data_fee{range_check_ptr}(excess_blob_gas: U64, tx: Transaction) let data_fee = Uint(total_blob_gas.value * blob_gas_price.value); return data_fee; } - -namespace GasConstants { - const GAS_JUMPDEST = 1; - const GAS_BASE = 2; - const GAS_VERY_LOW = 3; - const GAS_STORAGE_SET = 20000; - const GAS_STORAGE_UPDATE = 5000; - const GAS_STORAGE_CLEAR_REFUND = 4800; - const GAS_LOW = 5; - const GAS_MID = 8; - const GAS_HIGH = 10; - const GAS_EXPONENTIATION = 10; - const GAS_EXPONENTIATION_PER_BYTE = 50; - const GAS_MEMORY = 3; - const GAS_KECCAK256 = 30; - const GAS_KECCAK256_WORD = 6; - const GAS_COPY = 3; - const GAS_BLOCK_HASH = 20; - const GAS_LOG = 375; - const GAS_LOG_DATA = 8; - const GAS_LOG_TOPIC = 375; - const GAS_CREATE = 32000; - const GAS_CODE_DEPOSIT = 200; - const GAS_ZERO = 0; - const GAS_NEW_ACCOUNT = 25000; - const GAS_CALL_VALUE = 9000; - const GAS_CALL_STIPEND = 2300; - const GAS_SELF_DESTRUCT = 5000; - const GAS_SELF_DESTRUCT_NEW_ACCOUNT = 25000; - const GAS_ECRECOVER = 3000; - const GAS_SHA256 = 60; - const GAS_SHA256_WORD = 12; - const GAS_RIPEMD160 = 600; - const GAS_RIPEMD160_WORD = 120; - const GAS_IDENTITY = 15; - const GAS_IDENTITY_WORD = 3; - const GAS_RETURN_DATA_COPY = 3; - const GAS_FAST_STEP = 5; - const GAS_BLAKE2_PER_ROUND = 1; - const GAS_COLD_SLOAD = 2100; - const GAS_COLD_ACCOUNT_ACCESS = 2600; - const GAS_WARM_ACCESS = 100; - const GAS_INIT_CODE_WORD_COST = 2; - const GAS_BLOBHASH_OPCODE = 3; - const GAS_POINT_EVALUATION = 50000; - - const TARGET_BLOB_GAS_PER_BLOCK = 393216; - const GAS_PER_BLOB = 2 ** 17; - const MIN_BLOB_GASPRICE = 1; - const BLOB_GASPRICE_UPDATE_FRACTION = 3338477; -} diff --git a/cairo/ethereum_types/others.cairo b/cairo/ethereum_types/others.cairo index 1d7ea917..689ac0b8 100644 --- a/cairo/ethereum_types/others.cairo +++ b/cairo/ethereum_types/others.cairo @@ -12,6 +12,26 @@ // None values are just null pointers generally speaking (i.e. cast(my_var, felt) == 0) // but we need to explicitly define None to be able to serialize/deserialize None +from ethereum_types.numeric import U256 + struct None { value: felt*, } + +struct TupleU256U256Struct { + val_1: U256, + val_2: U256, +} + +struct TupleU256U256 { + value: TupleU256U256Struct*, +} + +struct ListTupleU256U256Struct { + data: TupleU256U256*, + len: felt, +} + +struct ListTupleU256U256 { + value: ListTupleU256U256Struct*, +} diff --git a/cairo/src/constants.cairo b/cairo/src/constants.cairo index e02d54f2..b31ce8c7 100644 --- a/cairo/src/constants.cairo +++ b/cairo/src/constants.cairo @@ -6,6 +6,7 @@ from src.gas import Gas // @notice This file contains global constants. namespace Constants { const UINT128_MAX = 0xffffffffffffffffffffffffffffffff; + const UINT64_MAX = 2 ** 64 - 1; // STACK const STACK_MAX_DEPTH = 1024; diff --git a/cairo/tests/conftest.py b/cairo/tests/conftest.py index b24cb335..9289e37a 100644 --- a/cairo/tests/conftest.py +++ b/cairo/tests/conftest.py @@ -93,6 +93,7 @@ def pytest_addoption(parser): max_examples=30, phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], derandomize=True, + print_blob=True, ) settings.register_profile( "debug", diff --git a/cairo/tests/ethereum/cancun/vm/test_gas.py b/cairo/tests/ethereum/cancun/vm/test_gas.py index ce7d9fd8..a77a2ac3 100644 --- a/cairo/tests/ethereum/cancun/vm/test_gas.py +++ b/cairo/tests/ethereum/cancun/vm/test_gas.py @@ -1,15 +1,20 @@ +from typing import List, Tuple + import pytest from ethereum_types.numeric import U256, Uint from hypothesis import assume, given from hypothesis import strategies as st +from hypothesis.strategies import composite from ethereum.cancun.blocks import Header from ethereum.cancun.transactions import BlobTransaction +from ethereum.cancun.vm.exceptions import ExceptionalHalt from ethereum.cancun.vm.gas import ( GAS_CALL_STIPEND, calculate_blob_gas_price, calculate_data_fee, calculate_excess_blob_gas, + calculate_gas_extend_memory, calculate_memory_gas_cost, calculate_message_call_gas, calculate_total_blob_gas, @@ -17,7 +22,15 @@ init_code_cost, max_message_call_gas, ) -from tests.utils.args_gen import Evm +from tests.utils.args_gen import Evm, Memory + + +@composite +def extensions_strategy(draw): + offset = draw(st.integers(min_value=0, max_value=2**64 - 32)) + max_size = (2**64 - 32) - offset + size = draw(st.integers(min_value=0, max_value=max_size)) + return (U256(offset), U256(size)) class TestGas: @@ -39,6 +52,20 @@ def test_calculate_memory_gas_cost(self, cairo_run, size_in_bytes: Uint): "calculate_memory_gas_cost", size_in_bytes ) + # We saturate the memory (offsets + size) at 2**64-32 + @given(memory=..., extensions=st.lists(extensions_strategy())) + def test_calculate_gas_extend_memory( + self, cairo_run, memory: Memory, extensions: List[Tuple[U256, U256]] + ): + try: + cairo_result = cairo_run("calculate_gas_extend_memory", memory, extensions) + except ExceptionalHalt as cairo_error: + with pytest.raises(type(cairo_error)): + calculate_gas_extend_memory(memory, extensions) + return + + assert calculate_gas_extend_memory(memory, extensions) == cairo_result + @given( value=..., gas=..., diff --git a/cairo/tests/test_serde.py b/cairo/tests/test_serde.py index 1e1c7767..d3634d06 100644 --- a/cairo/tests/test_serde.py +++ b/cairo/tests/test_serde.py @@ -32,7 +32,7 @@ StackOverflowError, StackUnderflowError, ) -from ethereum.cancun.vm.gas import MessageCallGas +from ethereum.cancun.vm.gas import ExtendMemory, MessageCallGas from ethereum.crypto.hash import Hash32 from ethereum.exceptions import EthereumException from tests.utils.args_gen import ( @@ -46,6 +46,7 @@ from tests.utils.args_gen import gen_arg as _gen_arg from tests.utils.args_gen import to_cairo_type as _to_cairo_type from tests.utils.serde import Serde +from tests.utils.strategies import TypedTuple @pytest.fixture(scope="module") @@ -95,6 +96,10 @@ def get_type(instance: Any) -> Type: key_type, value_type = instance.__orig_class__.__args__ return Trie[key_type, value_type] + if isinstance(instance, TypedTuple): + args = instance.__orig_class__.__args__ + return Tuple[args] + if not isinstance(instance, (tuple, list)): return type(instance) @@ -245,6 +250,8 @@ def test_type( Memory, Evm, Message, + List[Tuple[U256, U256]], + ExtendMemory, ], ): assume(no_empty_sequence(b)) diff --git a/cairo/tests/utils/args_gen.py b/cairo/tests/utils/args_gen.py index 108b4bbe..996a0366 100644 --- a/cairo/tests/utils/args_gen.py +++ b/cairo/tests/utils/args_gen.py @@ -122,7 +122,7 @@ from ethereum.cancun.vm import Evm as EvmBase from ethereum.cancun.vm import Message as MessageBase from ethereum.cancun.vm.exceptions import ExceptionalHalt -from ethereum.cancun.vm.gas import MessageCallGas +from ethereum.cancun.vm.gas import ExtendMemory, MessageCallGas from ethereum.crypto.hash import Hash32 from ethereum.exceptions import EthereumException from ethereum.rlp import Extended, Simple @@ -277,6 +277,8 @@ def __eq__(self, other): ("ethereum", "cancun", "fork_types", "SetTupleAddressBytes32"): Set[ Tuple[Address, Bytes32] ], + ("ethereum_types", "others", "TupleU256U256"): Tuple[U256, U256], + ("ethereum_types", "others", "ListTupleU256U256"): List[Tuple[U256, U256]], ("ethereum", "cancun", "transactions", "LegacyTransaction"): LegacyTransaction, ( "ethereum", @@ -348,6 +350,7 @@ def __eq__(self, other): ("ethereum", "cancun", "vm", "Evm"): Evm, ("ethereum", "cancun", "vm", "Memory"): Memory, ("ethereum", "cancun", "vm", "Stack"): Stack[U256], + ("ethereum", "cancun", "vm", "gas", "ExtendMemory"): ExtendMemory, **vm_exception_mappings, } diff --git a/cairo/tests/utils/strategies.py b/cairo/tests/utils/strategies.py index fb4796da..47aff521 100644 --- a/cairo/tests/utils/strategies.py +++ b/cairo/tests/utils/strategies.py @@ -92,10 +92,42 @@ def stack_strategy(thing): ) +from typing import Generic, TypeVar + +T1 = TypeVar("T1") +T2 = TypeVar("T2") + + +class TypedTuple(tuple, Generic[T1, T2]): + """A tuple that maintains its type information.""" + + def __new__(cls, values): + return super(TypedTuple, cls).__new__(cls, values) + + +def tuple_strategy(thing): + types = thing.__args__ + + # Handle ellipsis tuples + if len(types) == 2 and types[1] == Ellipsis: + return st.tuples(st.from_type(types[0]), st.from_type(types[0])).map( + lambda x: TypedTuple[types[0], Ellipsis](x) + ) + + return st.tuples(*(st.from_type(t) for t in types)).map( + lambda x: TypedTuple[tuple(types)](x) + ) + + # Generating up to 2**13 bytes of memory is enough for most tests as more would take too long # in the test runner. # 2**32 bytes would be the value at which the memory expansion would trigger an OOG -memory = st.binary(min_size=0, max_size=2**13).map(Memory) +# memory size must be a multiple of 32 +memory = ( + st.binary(min_size=0, max_size=2**13) + .map(lambda x: x + b"\x00" * ((32 - len(x) % 32) % 32)) + .map(Memory) +) evm = st.fixed_dictionaries( { @@ -144,7 +176,11 @@ def stack_strategy(thing): # Versions strategies with less data in collections -memory_lite = st.binary(min_size=0, max_size=128).map(Memory) +memory_lite = ( + st.binary(min_size=0, max_size=128) + .map(lambda x: x + b"\x00" * ((32 - len(x) % 32) % 32)) + .map(Memory) +) message_lite = st.fixed_dictionaries( { @@ -282,3 +318,4 @@ def register_type_strategies(): st.register_type_strategy(Stack, stack_strategy) st.register_type_strategy(Memory, memory) st.register_type_strategy(Evm, evm) + st.register_type_strategy(tuple, tuple_strategy)