From 4542e0b5123d7fe25ab9a6d3d2e234be13436465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 24 Oct 2024 18:43:30 +0200 Subject: [PATCH 1/5] Add ceil32 and diff tests for math --- cairo/src/utils/maths.cairo | 8 ++++ cairo/tests/fixtures/data.py | 30 --------------- cairo/tests/src/utils/test_math.cairo | 22 +++++++++++ cairo/tests/src/utils/test_math.py | 18 +++++++++ cairo/tests/utils/strategies.py | 55 +++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 cairo/tests/src/utils/test_math.cairo create mode 100644 cairo/tests/src/utils/test_math.py create mode 100644 cairo/tests/utils/strategies.py diff --git a/cairo/src/utils/maths.cairo b/cairo/src/utils/maths.cairo index 3de054c9a..307f85d2d 100644 --- a/cairo/src/utils/maths.cairo +++ b/cairo/src/utils/maths.cairo @@ -32,3 +32,11 @@ func unsigned_div_rem{range_check_ptr}(value, div) -> (q: felt, r: felt) { assert value = q * div + r; return (q, r); } + +func ceil32{range_check_ptr}(value: felt) -> felt { + if (value == 0) { + return 0; + } + let (q, r) = unsigned_div_rem(value + 31, 32); + return q * 32; +} diff --git a/cairo/tests/fixtures/data.py b/cairo/tests/fixtures/data.py index 485917030..095d9d58f 100644 --- a/cairo/tests/fixtures/data.py +++ b/cairo/tests/fixtures/data.py @@ -1,37 +1,7 @@ import pytest -from hypothesis import strategies as st from tests.utils.models import Account, Block, State -block_header_strategy = st.fixed_dictionaries( - { - "parent_hash": st.binary(min_size=32, max_size=32), - "ommers_hash": st.just( - bytes.fromhex( - "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - ) - ), - "coinbase": st.binary(min_size=20, max_size=20), - "state_root": st.binary(min_size=32, max_size=32), - "transactions_root": st.binary(min_size=32, max_size=32), - "receipt_root": st.binary(min_size=32, max_size=32), - "bloom": st.binary(min_size=256, max_size=256), - "difficulty": st.just(0x00), - "number": st.integers(min_value=0, max_value=2**64 - 1), - "gas_limit": st.integers(min_value=0, max_value=2**64 - 1), - "gas_used": st.integers(min_value=0, max_value=2**64 - 1), - "timestamp": st.integers(min_value=0, max_value=2**64 - 1), - "extra_data": st.binary(max_size=32), - "prev_randao": st.binary(min_size=32, max_size=32), - "nonce": st.just("0x0000000000000000"), - "base_fee_per_gas": st.integers(min_value=0, max_value=2**64 - 1), - "withdrawals_root": st.binary(min_size=32, max_size=32), - "blob_gas_used": st.integers(min_value=0, max_value=2**64 - 1), - "excess_blob_gas": st.integers(min_value=0, max_value=2**64 - 1), - "parent_beacon_block_root": st.binary(min_size=32, max_size=32), - } -) - @pytest.fixture def block(): diff --git a/cairo/tests/src/utils/test_math.cairo b/cairo/tests/src/utils/test_math.cairo new file mode 100644 index 000000000..3d10d7194 --- /dev/null +++ b/cairo/tests/src/utils/test_math.cairo @@ -0,0 +1,22 @@ +%builtins range_check + +from src.utils.maths import unsigned_div_rem, ceil32 + +func test__unsigned_div_rem{range_check_ptr}() -> (felt, felt) { + alloc_locals; + + tempvar value; + tempvar div; + %{ + ids.value = program_input["value"] + ids.div = program_input["div"] + %} + return unsigned_div_rem(value, div); +} + +func test__ceil32{range_check_ptr}() -> felt { + alloc_locals; + tempvar value; + %{ ids.value = program_input["value"] %} + return ceil32(value); +} diff --git a/cairo/tests/src/utils/test_math.py b/cairo/tests/src/utils/test_math.py new file mode 100644 index 000000000..9f2d5ae06 --- /dev/null +++ b/cairo/tests/src/utils/test_math.py @@ -0,0 +1,18 @@ +from ethereum.utils.numeric import ceil32 +from hypothesis import given +from hypothesis import strategies as st +from starkware.cairo.lang.cairo_constants import DEFAULT_PRIME + +from tests.utils.strategies import uint128 + + +class TestMaths: + @given(uint128, st.integers(min_value=1, max_value=DEFAULT_PRIME // 2**128)) + def test_should_unsigned_div_rem(self, cairo_run, value, div): + assert list(divmod(value, div)) == cairo_run( + "test__unsigned_div_rem", value=value, div=div + ) + + @given(uint128) + def test_should_ceil32(self, cairo_run, value): + assert ceil32(value) == cairo_run("test__ceil32", value=value) diff --git a/cairo/tests/utils/strategies.py b/cairo/tests/utils/strategies.py new file mode 100644 index 000000000..382ad9263 --- /dev/null +++ b/cairo/tests/utils/strategies.py @@ -0,0 +1,55 @@ +from hypothesis import strategies as st + +uint64 = st.integers(min_value=0, max_value=2**64 - 1) +uint128 = st.integers(min_value=0, max_value=2**128 - 1) +uint256 = st.integers(min_value=0, max_value=2**256 - 1) + +bytes20 = st.binary(min_size=20, max_size=20) +bytes32 = st.binary(min_size=32, max_size=32) + +block_header = st.fixed_dictionaries( + { + "parent_hash": bytes32, + "ommers_hash": st.just( + bytes.fromhex( + "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + ) + ), + "coinbase": bytes20, + "state_root": bytes32, + "transactions_root": bytes32, + "receipt_root": bytes32, + "bloom": st.binary(min_size=256, max_size=256), + "difficulty": st.just(0x00), + "number": uint64, + "gas_limit": uint64, + "gas_used": uint64, + "timestamp": uint64, + "extra_data": st.binary(max_size=32), + "prev_randao": bytes32, + "nonce": st.just("0x0000000000000000"), + "base_fee_per_gas": uint64, + "withdrawals_root": bytes32, + "blob_gas_used": uint64, + "excess_blob_gas": uint64, + "parent_beacon_block_root": bytes32, + } +) + +access_list_transaction = st.fixed_dictionaries( + { + "chain_id": uint64, + "nonce": uint64, + "gas_price": uint64, + "gas": uint64, + "to": bytes20, + "value": uint64, + "data": st.binary(), + "access_list": st.lists( + st.tuples(bytes20, st.lists(bytes32)), min_size=1, max_size=10 + ), + "y_parity": uint64, + "r": bytes32, + "s": bytes32, + } +) From 532a8de26c6bb1bfb7a29b842a78687c66156bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 24 Oct 2024 18:58:28 +0200 Subject: [PATCH 2/5] Add init_code_cost --- cairo/src/gas.cairo | 12 +++++- .../src/instructions/system_operations.cairo | 5 +-- cairo/src/interpreter.cairo | 4 +- cairo/tests/src/test_gas.cairo | 6 +++ cairo/tests/src/test_gas.py | 39 +++++++------------ cairo/tests/utils/strategies.py | 2 + 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/cairo/src/gas.cairo b/cairo/src/gas.cairo index 5343202a4..60d089a67 100644 --- a/cairo/src/gas.cairo +++ b/cairo/src/gas.cairo @@ -6,7 +6,9 @@ from starkware.cairo.common.uint256 import Uint256, uint256_lt from src.model import model from src.utils.uint256 import uint256_eq from src.utils.utils import Helpers -from src.utils.maths import unsigned_div_rem +from src.utils.maths import unsigned_div_rem, ceil32 + +const GAS_INIT_CODE_WORD_COST = 2; namespace Gas { const JUMPDEST = 1; @@ -49,13 +51,19 @@ namespace Gas { const COLD_SLOAD = 2100; const COLD_ACCOUNT_ACCESS = 2600; const WARM_ACCESS = 100; - const INIT_CODE_WORD_COST = 2; const TX_BASE_COST = 21000; const TX_ACCESS_LIST_ADDRESS_COST = 2400; const TX_ACCESS_LIST_STORAGE_KEY_COST = 1900; const BLOBHASH = 3; const MEMORY_COST_U32 = 0x200018000000; + // @notive See https://github.com/ethereum/execution-specs/blob/master/src/ethereum/cancun/vm/gas.py#L253-L269 + func init_code_cost{range_check_ptr}(init_code_length: felt) -> felt { + let init_code_bytes = ceil32(init_code_length); + let (init_code_words, _) = unsigned_div_rem(init_code_bytes, 32); + return GAS_INIT_CODE_WORD_COST * init_code_words; + } + // @notice Compute the cost of the memory for a given words length. // @dev To avoid range_check overflow, we compute words_len / 512 // instead of words_len * words_len / 512. Then we recompute the diff --git a/cairo/src/instructions/system_operations.cairo b/cairo/src/instructions/system_operations.cairo index 5776c2266..53ecd0bc0 100644 --- a/cairo/src/instructions/system_operations.cairo +++ b/cairo/src/instructions/system_operations.cairo @@ -4,7 +4,6 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin from starkware.cairo.common.math import split_felt from starkware.cairo.common.math_cmp import is_nn, is_not_zero from starkware.cairo.common.uint256 import Uint256, uint256_lt, uint256_le -from starkware.cairo.common.default_dict import default_dict_new from starkware.cairo.common.dict_access import DictAccess from src.account import Account @@ -12,7 +11,7 @@ from src.interfaces.interfaces import ICairo1Helpers from src.constants import Constants from src.errors import Errors from src.evm import EVM -from src.gas import Gas +from src.gas import Gas, GAS_INIT_CODE_WORD_COST from src.memory import Memory from src.model import model from src.stack import Stack @@ -57,7 +56,7 @@ namespace SystemOperations { return evm; } let (calldata_words, _) = unsigned_div_rem(size.low + 31, 32); - let init_code_gas_low = Gas.INIT_CODE_WORD_COST * calldata_words; + let init_code_gas_low = GAS_INIT_CODE_WORD_COST * calldata_words; tempvar init_code_gas_high = is_not_zero(size.high) * 2 ** 128; let calldata_word_gas = is_create2 * Gas.KECCAK256_WORD * calldata_words; let evm = EVM.charge_gas( diff --git a/cairo/src/interpreter.cairo b/cairo/src/interpreter.cairo index 4e7f634a5..a7f8a62ab 100644 --- a/cairo/src/interpreter.cairo +++ b/cairo/src/interpreter.cairo @@ -28,7 +28,7 @@ from src.precompiles.precompiles import Precompiles from src.precompiles.precompiles_helpers import PrecompilesHelpers from src.stack import Stack from src.state import State -from src.gas import Gas +from src.gas import Gas, GAS_INIT_CODE_WORD_COST from src.utils.utils import Helpers from src.utils.array import count_not_zero from src.utils.uint256 import uint256_sub, uint256_add @@ -851,7 +851,7 @@ namespace Interpreter { if (is_deploy_tx != FALSE) { let (empty: felt*) = alloc(); let (init_code_words, _) = unsigned_div_rem(bytecode_len + 31, 32); - let init_code_gas = Gas.INIT_CODE_WORD_COST * init_code_words; + let init_code_gas = GAS_INIT_CODE_WORD_COST * init_code_words; assert bytecode = tmp_calldata; assert calldata = empty; assert intrinsic_gas = tmp_intrinsic_gas + Gas.CREATE + init_code_gas; diff --git a/cairo/tests/src/test_gas.cairo b/cairo/tests/src/test_gas.cairo index e3032f8cf..53c152323 100644 --- a/cairo/tests/src/test_gas.cairo +++ b/cairo/tests/src/test_gas.cairo @@ -83,3 +83,9 @@ func test__compute_message_call_gas{range_check_ptr}() -> felt { return gas; } + +func test__init_code_cost{range_check_ptr}() -> felt { + tempvar init_code_len: felt; + %{ ids.init_code_len = program_input["init_code_len"]; %} + return Gas.init_code_cost(init_code_len); +} diff --git a/cairo/tests/src/test_gas.py b/cairo/tests/src/test_gas.py index 7cc96310a..3c001e59e 100644 --- a/cairo/tests/src/test_gas.py +++ b/cairo/tests/src/test_gas.py @@ -2,29 +2,22 @@ from ethereum.shanghai.vm.gas import ( calculate_gas_extend_memory, calculate_memory_gas_cost, + init_code_cost, ) from hypothesis import given -from hypothesis.strategies import integers -from src.utils.uint256 import int_to_uint256 - -int_to_uint256(0) # (0, 0) +from tests.utils.strategies import uint20, uint24, uint64, uint128, uint256 class TestGas: class TestCost: - @given(max_offset=integers(min_value=0, max_value=0xFFFFFF)) - def test_should_return_same_as_execution_specs(self, cairo_run, max_offset): + @given(max_offset=uint24) + def test_memory_cost(self, cairo_run, max_offset): output = cairo_run("test__memory_cost", words_len=(max_offset + 31) // 32) assert calculate_memory_gas_cost(max_offset) == output - @given( - bytes_len=integers(min_value=0, max_value=2**128 - 1), - added_offset=integers(min_value=0, max_value=2**128 - 1), - ) - def test_should_return_correct_expansion_cost( - self, cairo_run, bytes_len, added_offset - ): + @given(bytes_len=uint128, added_offset=uint128) + def test_memory_expansion_cost(self, cairo_run, bytes_len, added_offset): max_offset = bytes_len + added_offset output = cairo_run( "test__memory_expansion_cost", @@ -36,13 +29,8 @@ def test_should_return_correct_expansion_cost( diff = cost_after - cost_before assert diff == output - @given( - offset_1=integers(min_value=0, max_value=0xFFFFF), - size_1=integers(min_value=0, max_value=0xFFFFF), - offset_2=integers(min_value=0, max_value=0xFFFFF), - size_2=integers(min_value=0, max_value=0xFFFFF), - ) - def test_should_return_max_expansion_cost( + @given(offset_1=uint20, size_1=uint20, offset_2=uint20, size_2=uint20) + def test_max_memory_expansion_cost( self, cairo_run, offset_1, size_1, offset_2, size_2 ): output = cairo_run( @@ -64,10 +52,7 @@ def test_should_return_max_expansion_cost( ).cost ) - @given( - offset=integers(min_value=0, max_value=2**256 - 1), - size=integers(min_value=0, max_value=2**256 - 1), - ) + @given(offset=uint256, size=uint256) def test_memory_expansion_cost_saturated(self, cairo_run, offset, size): output = cairo_run( "test__memory_expansion_cost_saturated", @@ -84,6 +69,12 @@ def test_memory_expansion_cost_saturated(self, cairo_run, offset, size): assert cost == output + @given(init_code_len=uint64) + def test_init_code_cost(self, cairo_run, init_code_len): + assert init_code_cost(init_code_len) == cairo_run( + "test__init_code_cost", init_code_len=init_code_len + ) + class TestMessageGas: @pytest.mark.parametrize( "gas_param, gas_left, expected", diff --git a/cairo/tests/utils/strategies.py b/cairo/tests/utils/strategies.py index 382ad9263..9702a797c 100644 --- a/cairo/tests/utils/strategies.py +++ b/cairo/tests/utils/strategies.py @@ -1,5 +1,7 @@ from hypothesis import strategies as st +uint20 = st.integers(min_value=0, max_value=2**20 - 1) +uint24 = st.integers(min_value=0, max_value=2**24 - 1) uint64 = st.integers(min_value=0, max_value=2**64 - 1) uint128 = st.integers(min_value=0, max_value=2**128 - 1) uint256 = st.integers(min_value=0, max_value=2**256 - 1) From 023f222a7d468991c1e6628dcd8875ee9ab954f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 24 Oct 2024 19:01:57 +0200 Subject: [PATCH 3/5] Remove useless import --- cairo/src/state.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cairo/src/state.cairo b/cairo/src/state.cairo index 115281d22..57310e0ad 100644 --- a/cairo/src/state.cairo +++ b/cairo/src/state.cairo @@ -1,6 +1,6 @@ from starkware.cairo.common.alloc import alloc from starkware.cairo.common.cairo_builtins import HashBuiltin -from starkware.cairo.common.default_dict import default_dict_new, default_dict_finalize +from starkware.cairo.common.default_dict import default_dict_new from starkware.cairo.common.dict import dict_read, dict_write from starkware.cairo.common.dict_access import DictAccess from starkware.cairo.common.memcpy import memcpy From 87efb23a24c774ff63bfb311fd505c93ddbdd077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 24 Oct 2024 19:07:19 +0200 Subject: [PATCH 4/5] gen_arg for dataclasses as list --- cairo/tests/utils/hints.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cairo/tests/utils/hints.py b/cairo/tests/utils/hints.py index 314bf4448..47fbd7e89 100644 --- a/cairo/tests/utils/hints.py +++ b/cairo/tests/utils/hints.py @@ -1,5 +1,6 @@ from collections import defaultdict from contextlib import contextmanager +from dataclasses import asdict, is_dataclass from typing import Dict, Iterable, Tuple, Union from unittest.mock import patch @@ -43,6 +44,11 @@ def gen_arg( segments.load_data(base, arg) return base + if is_dataclass(arg): + return gen_arg( + dict_manager, segments, asdict(arg).values(), apply_modulo_to_args + ) + if apply_modulo_to_args and isinstance(arg, int): return arg % segments.prime From 8c7ca95266625ddf0248a9edacf9ade736607346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Sun, 27 Oct 2024 15:30:31 +0100 Subject: [PATCH 5/5] Add calculate_intrinsic_cost diff testing --- cairo/programs/fork.cairo | 54 +++++++++++++++++ cairo/src/utils/transaction.cairo | 7 +++ cairo/tests/programs/test_fork.cairo | 23 +++++++- cairo/tests/programs/test_fork.py | 32 +++++----- cairo/tests/utils/models.py | 88 +++++++++++++++++++++++++++- cairo/tests/utils/parsers.py | 50 +++++++++++++++- 6 files changed, 236 insertions(+), 18 deletions(-) diff --git a/cairo/programs/fork.cairo b/cairo/programs/fork.cairo index cdf955f59..58cc499fe 100644 --- a/cairo/programs/fork.cairo +++ b/cairo/programs/fork.cairo @@ -6,6 +6,16 @@ from starkware.cairo.common.math_cmp import is_nn from starkware.cairo.common.bool import FALSE from src.model import model +from src.utils.array import count_not_zero +from src.gas import Gas +from src.utils.transaction import ( + TX_DATA_COST_PER_ZERO, + TX_DATA_COST_PER_NON_ZERO, + TX_CREATE_COST, + TX_BASE_COST, + TX_ACCESS_LIST_ADDRESS_COST, + TX_ACCESS_LIST_STORAGE_KEY_COST, +) using Uint128 = felt; using Uint64 = felt; @@ -217,3 +227,47 @@ func validate_header{range_check_ptr}(header: model.BlockHeader, parent_header: // raise InvalidBlock return (); } + +// @notice See https://github.com/ethereum/execution-specs/blob/master/src/ethereum/cancun/fork.py#L818-L862 +func calculate_intrinsic_cost{range_check_ptr}(tx: model.Transaction*) -> felt { + alloc_locals; + + let count = count_not_zero(tx.payload_len, tx.payload); + let zeroes = tx.payload_len - count; + local data_cost = zeroes * TX_DATA_COST_PER_ZERO + count * TX_DATA_COST_PER_NON_ZERO; + + if (tx.destination.is_some == FALSE) { + let init_code_cost = Gas.init_code_cost(tx.payload_len); + tempvar range_check_ptr = range_check_ptr; + tempvar create_cost = TX_CREATE_COST + init_code_cost; + static_assert range_check_ptr == [ap - 2]; + } else { + tempvar range_check_ptr = range_check_ptr; + tempvar create_cost = 0; + static_assert range_check_ptr == [ap - 2]; + } + local range_check_ptr = [ap - 2]; + let create_cost = [ap - 1]; + + let access_list_cost = Internals.access_list_cost(tx.access_list_len, tx.access_list); + return TX_BASE_COST + data_cost + create_cost + access_list_cost; +} + +// A namespace for functions not part of the execution specs file but required to ease the cairo code +namespace Internals { + func access_list_cost(access_list_len: felt, access_list: felt*) -> felt { + alloc_locals; + + if (access_list_len == 0) { + return 0; + } + + let address = [access_list]; + let storage_keys_len = [access_list + 1]; + let item_len = 2 + storage_keys_len * Uint256.SIZE; + let cum_gas_cost = access_list_cost(access_list_len - item_len, access_list + item_len); + let current_cost = TX_ACCESS_LIST_ADDRESS_COST + storage_keys_len * + TX_ACCESS_LIST_STORAGE_KEY_COST; + return cum_gas_cost + current_cost; + } +} diff --git a/cairo/src/utils/transaction.cairo b/cairo/src/utils/transaction.cairo index 4071309d1..680780708 100644 --- a/cairo/src/utils/transaction.cairo +++ b/cairo/src/utils/transaction.cairo @@ -13,6 +13,13 @@ from src.utils.utils import Helpers from src.utils.bytes import keccak from src.utils.signature import Signature +const TX_BASE_COST = 21000; +const TX_DATA_COST_PER_NON_ZERO = 16; +const TX_DATA_COST_PER_ZERO = 4; +const TX_CREATE_COST = 32000; +const TX_ACCESS_LIST_ADDRESS_COST = 2400; +const TX_ACCESS_LIST_STORAGE_KEY_COST = 1900; + // @title Transaction utils // @notice This file contains utils for decoding eth transactions // @custom:namespace Transaction diff --git a/cairo/tests/programs/test_fork.cairo b/cairo/tests/programs/test_fork.cairo index 9b8145fd1..c40827231 100644 --- a/cairo/tests/programs/test_fork.cairo +++ b/cairo/tests/programs/test_fork.cairo @@ -1,4 +1,10 @@ -from programs.fork import check_gas_limit, calculate_base_fee_per_gas, validate_header, Uint128 +from programs.fork import ( + check_gas_limit, + calculate_base_fee_per_gas, + validate_header, + Uint128, + calculate_intrinsic_cost, +) from src.model import model func test_check_gas_limit{range_check_ptr}() { @@ -46,3 +52,18 @@ func test_validate_header{range_check_ptr}() { validate_header([header], [parent_header]); return (); } + +func test_calculate_intrinsic_cost{range_check_ptr}() -> felt { + tempvar tx: model.Transaction*; + %{ + if '__dict_manager' not in globals(): + from starkware.cairo.common.dict import DictManager + __dict_manager = DictManager() + + from tests.utils.hints import gen_arg + + ids.tx = gen_arg(__dict_manager, segments, program_input["tx"]) + %} + + return calculate_intrinsic_cost(tx); +} diff --git a/cairo/tests/programs/test_fork.py b/cairo/tests/programs/test_fork.py index bea4bf58f..7ad161bbd 100644 --- a/cairo/tests/programs/test_fork.py +++ b/cairo/tests/programs/test_fork.py @@ -1,23 +1,26 @@ from ethereum.cancun.blocks import Header from ethereum.cancun.fork import ( calculate_base_fee_per_gas, + calculate_intrinsic_cost, check_gas_limit, validate_header, ) +from ethereum.cancun.transactions import AccessListTransaction from ethereum.exceptions import InvalidBlock from hypothesis import given -from hypothesis.strategies import integers -from tests.fixtures.data import block_header_strategy from tests.utils.errors import cairo_error -from tests.utils.models import BlockHeader +from tests.utils.models import BlockHeader, Transaction +from tests.utils.strategies import ( + access_list_transaction, + block_header, + uint64, + uint128, +) class TestFork: - @given( - integers(min_value=0, max_value=2**128 - 1), - integers(min_value=0, max_value=2**128 - 1), - ) + @given(uint128, uint128) def test_check_gas_limit(self, cairo_run, gas_limit, parent_gas_limit): error = check_gas_limit(gas_limit, parent_gas_limit) if not error: @@ -34,12 +37,7 @@ def test_check_gas_limit(self, cairo_run, gas_limit, parent_gas_limit): parent_gas_limit=parent_gas_limit, ) - @given( - integers(min_value=0, max_value=2**64 - 1), - integers(min_value=0, max_value=2**64 - 1), - integers(min_value=0, max_value=2**64 - 1), - integers(min_value=0, max_value=2**64 - 1), - ) + @given(uint64, uint64, uint64, uint64) def test_calculate_base_fee_per_gas( self, cairo_run, @@ -76,7 +74,7 @@ def test_calculate_base_fee_per_gas( parent_base_fee_per_gas=parent_base_fee_per_gas, ) - @given(header=block_header_strategy, parent_header=block_header_strategy) + @given(header=block_header, parent_header=block_header) def test_validate_header(self, cairo_run, header, parent_header): error = None try: @@ -97,3 +95,9 @@ def test_validate_header(self, cairo_run, header, parent_header): header=BlockHeader.model_validate(header), parent_header=BlockHeader.model_validate(parent_header), ) + + @given(tx=access_list_transaction) + def test_calculate_intrinsic_cost(self, cairo_run, tx): + assert calculate_intrinsic_cost(AccessListTransaction(**tx)) == cairo_run( + "test_calculate_intrinsic_cost", tx=Transaction.model_validate(tx) + ) diff --git a/cairo/tests/utils/models.py b/cairo/tests/utils/models.py index b4a7c02ec..3419d9541 100644 --- a/cairo/tests/utils/models.py +++ b/cairo/tests/utils/models.py @@ -18,8 +18,8 @@ from starkware.cairo.lang.vm.crypto import pedersen_hash from src.utils.uint256 import int_to_uint256 -from tests.utils.helpers import rlp_encode_signed_data -from tests.utils.parsers import to_bytes, to_int +from tests.utils.helpers import flatten, rlp_encode_signed_data +from tests.utils.parsers import address, bytes_, to_bytes, to_int, uint, uint64, uint128 class BaseModelIterValuesOnly(BaseModel): @@ -327,3 +327,87 @@ def parse_addresses(cls, values): values = values.copy() values["accounts"] = defaultdict(int, {to_int(k): v for k, v in values.items()}) return values + + +class Transaction(BaseModelIterValuesOnly): + + @model_validator(mode="before") + def split_uint256(cls, values): + values = values.copy() + for key in ["amount", "value"]: + if key not in values: + key = to_camel(key) + if key not in values: + continue + + values[key], values[to_camel(key) + "High"] = int_to_uint256( + to_int(values[key]) + ) + return values + + @model_validator(mode="before") + def split_option(cls, values): + values = values.copy() + for key in ["destination", "chain_id"]: + if key not in values: + key = to_camel(key) + if key not in values and "to" in values: + key = "to" + is_some = key in values and values[key] is not None + # it's possible that values[key] exists and is None, that why we can't use get default value + value = to_int(values.get(key) or 0) + values[to_camel(key) + "IsSome"] = is_some + values[to_camel(key) + "Value"] = value + return values + + @model_validator(mode="before") + def parse_access_list(cls, values): + values = values.copy() + if "access_list" not in values: + return values + + value = flatten( + [ + ( + int.from_bytes(key, "big"), + len(addresses), + *[ + int_to_uint256(int.from_bytes(address, "big")) + for address in addresses + ], + ) + for key, addresses in values["access_list"] + ] + ) + if value is None: + return values + values["access_list"] = value + values["access_list_len"] = len(value) + return values + + @model_validator(mode="before") + def add_len(cls, values): + values = values.copy() + for key in ["payload", "data"]: + if key not in values: + key = to_camel(key) + if key not in values: + continue + + values[to_camel(key) + "Len"] = len(values[key]) + return values + + signer_nonce: uint64 = Field(validation_alias="nonce") + gas_limit: uint = Field(validation_alias="gas") + max_priority_fee_per_gas: uint = Field(validation_alias="gas_price") + max_fee_per_gas: uint = Field(validation_alias="gas_price") + destination_is_some: uint = Field(validation_alias="toIsSome") + destination_value: address = Field(validation_alias="toValue") + amount_low: uint128 = Field(validation_alias="value") + amount_high: uint128 = Field(validation_alias="valueHigh") + payload_len: uint = Field(validation_alias="dataLen") + payload: bytes_ = Field(validation_alias="data") + access_list_len: uint + access_list: list[uint] + chain_id_is_some: uint + chain_id_value: uint64 diff --git a/cairo/tests/utils/parsers.py b/cairo/tests/utils/parsers.py index f19c1023e..adf6b7533 100644 --- a/cairo/tests/utils/parsers.py +++ b/cairo/tests/utils/parsers.py @@ -1,5 +1,9 @@ import re -from typing import Optional, Union +from typing import Annotated, Optional, Union + +from ethereum.base_types import U64, U256, Bytes, Bytes0, Bytes32, Uint +from ethereum.cancun.fork_types import Address +from pydantic import BeforeValidator hex_pattern = re.compile(r"^(0x)?[0-9a-fA-F]+$") @@ -27,3 +31,47 @@ def to_bytes(v: Optional[Union[str, bytes, list[int]]]) -> Optional[bytes]: return v.encode() else: return bytes(v) + + +def to_fixed_bytes(length: int): + def _parser(v: Union[str, bytes, int, list[int]]): + if isinstance(v, int): + return v.to_bytes(length, byteorder="big") + + res = to_bytes(v) + if res is None or len(res) > length: + raise ValueError(f"Value {v} is too big for a {length} bytes fixed bytes") + return res.ljust(length, b"\x00") + + return _parser + + +int_parser = BeforeValidator(to_int) +bytes_parser = BeforeValidator(to_bytes) +bytes0_parser = BeforeValidator(to_fixed_bytes(0)) +bytes32_parser = BeforeValidator(to_fixed_bytes(32)) +bytes20_parser = BeforeValidator(to_fixed_bytes(20)) + +u256_validator = BeforeValidator(U256) +u64_validator = BeforeValidator(U64) +u128_validator = BeforeValidator( + lambda v: v if v < 2**128 else ValueError("Value is too big for a 128 bit uint") +) +uint_validator = BeforeValidator(Uint) +bytes0_validator = BeforeValidator(lambda b: Bytes0(b)) +address_validator = BeforeValidator(lambda b: Address(b)) +destination_validator = BeforeValidator( + lambda b: Bytes0() if len(b) == 0 else Address(to_fixed_bytes(20)(b)) +) +bytes32_validator = BeforeValidator(lambda b: Bytes32(b)) +bytes_validator = BeforeValidator(lambda b: Bytes(b)) + +uint256 = Annotated[int, u256_validator, int_parser] +uint128 = Annotated[int, u128_validator, int_parser] +uint64 = Annotated[int, u64_validator, int_parser] +uint = Annotated[int, uint_validator, int_parser] +bytes32 = Annotated[bytes, bytes32_validator, bytes32_parser] +bytes0 = Annotated[bytes, bytes0_validator, bytes0_parser] +bytes_ = Annotated[list, BeforeValidator(list), bytes_parser] +address = Annotated[bytes, address_validator, bytes20_parser] +destination = Annotated[Union[Address, Bytes0], destination_validator, bytes_parser]