From 7fa1396c80093bad953b20a3a323bf27869c309d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Mon, 30 Dec 2024 15:03:52 +0100 Subject: [PATCH] Run eth block (#287) --- .vscode/settings.json | 2 + cairo/ethereum/cancun/trie.cairo | 70 ++++-- cairo/ethereum/exceptions.cairo | 6 +- cairo/ethereum/utils/numeric.cairo | 20 +- cairo/pyproject.toml | 2 +- cairo/src/evm.cairo | 9 +- .../src/instructions/memory_operations.cairo | 5 +- cairo/src/interpreter.cairo | 147 ++++++------ cairo/src/state.cairo | 22 +- cairo/src/utils/compiler.py | 3 + cairo/src/utils/constants.py | 2 +- cairo/src/utils/utils.cairo | 214 +++++++++++++----- cairo/tests/conftest.py | 13 ++ cairo/tests/fixtures/runner.py | 1 - cairo/tests/programs/test_os.py | 2 +- cairo/tests/src/test_state.cairo | 27 --- cairo/tests/src/test_state.py | 4 - cairo/tests/src/utils/test_utils.cairo | 56 ++++- cairo/tests/src/utils/test_utils.py | 53 +++-- cairo/tests/test_serde.cairo | 3 +- cairo/tests/test_serde.py | 21 +- cairo/tests/utils/args_gen.py | 12 +- cairo/tests/utils/serde.py | 28 ++- cairo/tests/utils/strategies.py | 2 +- uv.lock | 4 +- 25 files changed, 460 insertions(+), 268 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 43cd232b..4efff23f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "exitstatus", "fibonacci", "fixturenames", + "frozendict", "hookwrapper", "intdigest", "isort", @@ -42,6 +43,7 @@ "sessionstart", "slackapi", "testrunfinished", + "usort", "workerid", "workerinput" ], diff --git a/cairo/ethereum/cancun/trie.cairo b/cairo/ethereum/cancun/trie.cairo index 09cd7635..7bdeb585 100644 --- a/cairo/ethereum/cancun/trie.cairo +++ b/cairo/ethereum/cancun/trie.cairo @@ -58,8 +58,31 @@ struct ExtensionNode { value: ExtensionNodeStruct*, } +struct SubnodesStruct { + branch_0: Extended, + branch_1: Extended, + branch_2: Extended, + branch_3: Extended, + branch_4: Extended, + branch_5: Extended, + branch_6: Extended, + branch_7: Extended, + branch_8: Extended, + branch_9: Extended, + branch_10: Extended, + branch_11: Extended, + branch_12: Extended, + branch_13: Extended, + branch_14: Extended, + branch_15: Extended, +} + +struct Subnodes { + value: SubnodesStruct*, +} + struct BranchNodeStruct { - subnodes: SequenceExtended, + subnodes: Subnodes, value: Extended, } @@ -187,9 +210,9 @@ func encode_internal_node{ branch_node: let (value: Extended*) = alloc(); - let len = node.value.branch_node.value.subnodes.value.len; + let len = 16; // TOD0: check if we really need to copy of if we can just use the pointer - memcpy(value, node.value.branch_node.value.subnodes.value.data, len); + memcpy(value, node.value.branch_node.value.subnodes.value, len); assert [value + len] = node.value.branch_node.value.value; tempvar sequence = SequenceExtended(new SequenceExtendedStruct(value, len + 1)); let unencoded_ = ExtendedImpl.sequence(sequence); @@ -858,27 +881,28 @@ func patricialize{range_check_ptr, bitwise_ptr: BitwiseBuiltin*, keccak_ptr: Kec let patricialized_15 = patricialize(branches.value.data[15], next_level); let encoded_15 = encode_internal_node(patricialized_15); - let (sequence: Extended*) = alloc(); - assert sequence[0] = encoded_0; - assert sequence[1] = encoded_1; - assert sequence[2] = encoded_2; - assert sequence[3] = encoded_3; - assert sequence[4] = encoded_4; - assert sequence[5] = encoded_5; - assert sequence[6] = encoded_6; - assert sequence[7] = encoded_7; - assert sequence[8] = encoded_8; - assert sequence[9] = encoded_9; - assert sequence[10] = encoded_10; - assert sequence[11] = encoded_11; - assert sequence[12] = encoded_12; - assert sequence[13] = encoded_13; - assert sequence[14] = encoded_14; - assert sequence[15] = encoded_15; - - tempvar sequence_extended = SequenceExtended(new SequenceExtendedStruct(sequence, 16)); + tempvar subnodes = Subnodes( + new SubnodesStruct( + encoded_0, + encoded_1, + encoded_2, + encoded_3, + encoded_4, + encoded_5, + encoded_6, + encoded_7, + encoded_8, + encoded_9, + encoded_10, + encoded_11, + encoded_12, + encoded_13, + encoded_14, + encoded_15, + ), + ); let value_extended = ExtendedImpl.bytes(value); - tempvar branch_node = BranchNode(new BranchNodeStruct(sequence_extended, value_extended)); + tempvar branch_node = BranchNode(new BranchNodeStruct(subnodes, value_extended)); let internal_node = InternalNodeImpl.branch_node(branch_node); return internal_node; diff --git a/cairo/ethereum/exceptions.cairo b/cairo/ethereum/exceptions.cairo index c77f5c76..caaf7f5c 100644 --- a/cairo/ethereum/exceptions.cairo +++ b/cairo/ethereum/exceptions.cairo @@ -1,7 +1,7 @@ // Error types common across all Ethereum forks. // // When raising an exception, the exception is a valid pointer. Otherwise, the pointer is `0`. When -// checking for an exceptino, a simple cast(maybe_exception, felt) != 0 is enough to check if the +// checking for an exception, a simple cast(maybe_exception, felt) != 0 is enough to check if the // function raised. // // Example: @@ -16,10 +16,10 @@ // tempvar no_error = EthereumException(cast(0, BytesStruct*)); // ``` -from ethereum_types.bytes import BytesStruct +from ethereum_types.bytes import Bytes // @notice Base type for all exceptions _expected_ to be thrown during normal // operation. struct EthereumException { - value: BytesStruct*, + value: Bytes, } diff --git a/cairo/ethereum/utils/numeric.cairo b/cairo/ethereum/utils/numeric.cairo index 05133ea4..84aa46c0 100644 --- a/cairo/ethereum/utils/numeric.cairo +++ b/cairo/ethereum/utils/numeric.cairo @@ -2,14 +2,20 @@ from starkware.cairo.common.math_cmp import is_le, is_not_zero from ethereum_types.numeric import Uint func min{range_check_ptr}(a: felt, b: felt) -> felt { - if (a == b) { - return a; - } + alloc_locals; - let res = is_le(a, b); - if (res == 1) { - return a; - } + tempvar is_min_b; + %{ memory[ap - 1] = 1 if ids.b <= ids.a else 0 %} + jmp min_is_b if is_min_b != 0; + + min_is_a: + assert [range_check_ptr] = b - a; + let range_check_ptr = range_check_ptr + 1; + return a; + + min_is_b: + assert [range_check_ptr] = a - b; + let range_check_ptr = range_check_ptr + 1; return b; } diff --git a/cairo/pyproject.toml b/cairo/pyproject.toml index 952eb2ba..3cc42647 100644 --- a/cairo/pyproject.toml +++ b/cairo/pyproject.toml @@ -153,7 +153,7 @@ profile = "black" src_paths = ["src", "tests"] [tool.uv.sources] -ethereum = { git = "https://github.com/kkrt-labs/execution-specs.git", branch = "dev/change-type-branch-nodes" } +ethereum = { git = "https://github.com/kkrt-labs/execution-specs.git", rev = "b255036441d64437bd4fc9f9068bc64c45470e93" } [build-system] requires = ["hatchling"] diff --git a/cairo/src/evm.cairo b/cairo/src/evm.cairo index 629f94f0..2695890c 100644 --- a/cairo/src/evm.cairo +++ b/cairo/src/evm.cairo @@ -11,9 +11,10 @@ from starkware.cairo.common.default_dict import default_dict_finalize from src.errors import Errors from src.model import model -from src.account import Account from src.stack import Stack from src.state import State +from src.utils.dict import dict_squash +from src.utils.utils import Helpers // @title EVM related functions. // @notice This file contains functions related to the execution context. @@ -38,9 +39,11 @@ namespace EVM { } func finalize{range_check_ptr, evm: model.EVM*}() { - let (squashed_start, squashed_end) = default_dict_finalize( - evm.message.valid_jumpdests_start, evm.message.valid_jumpdests, 0 + alloc_locals; + let (local squashed_start, local squashed_end) = dict_squash( + evm.message.valid_jumpdests_start, evm.message.valid_jumpdests ); + Helpers.finalize_jumpdests(0, squashed_start, squashed_end, evm.message.bytecode); tempvar message = new model.Message( bytecode=evm.message.bytecode, bytecode_len=evm.message.bytecode_len, diff --git a/cairo/src/instructions/memory_operations.cairo b/cairo/src/instructions/memory_operations.cairo index 943763e0..ea520fe0 100644 --- a/cairo/src/instructions/memory_operations.cairo +++ b/cairo/src/instructions/memory_operations.cairo @@ -338,7 +338,10 @@ namespace MemoryOperations { let current_value = State.read_storage(evm.message.address, key); let initial_state = evm.message.initial_state; - let original_value = State.read_storage{state=initial_state}(evm.message.address, key); + // TODO: This raises with wrong dict pointer as the State.copy() only copies visited accounts + // so unvisited accounts are actually the same in the initial state and the current state. + // let original_value = State.read_storage{state=initial_state}(evm.message.address, key); + tempvar original_value = new Uint256(0, 0); tempvar message = new model.Message( bytecode=evm.message.bytecode, diff --git a/cairo/src/interpreter.cairo b/cairo/src/interpreter.cairo index 8a870d57..2953096b 100644 --- a/cairo/src/interpreter.cairo +++ b/cairo/src/interpreter.cairo @@ -866,7 +866,49 @@ namespace Interpreter { let valid_jumpdests_start = cast([ap - 2], DictAccess*); let valid_jumpdests = cast([ap - 1], DictAccess*); - let initial_state = State.copy(); + let initial_state = state; + let state = State.copy(); + let stack = Stack.init(); + let memory = Memory.init(); + + // Cache the coinbase, precompiles, caller, and target, making them warm + State.get_account(env.coinbase); + State.cache_precompiles(); + State.get_account(address); + let access_list_cost = State.cache_access_list(access_list_len, access_list); + + let intrinsic_gas = intrinsic_gas + access_list_cost; + assert [range_check_ptr] = gas_limit - intrinsic_gas; + let range_check_ptr = range_check_ptr + 1; + assert [range_check_ptr] = (2 * Constants.MAX_CODE_SIZE + 1 - bytecode_len) * is_deploy_tx; + let range_check_ptr = range_check_ptr + 1; + + // Charge the gas fee to the user without setting up a transfer. + // Transfers with the exact amounts will be performed post-execution. + // Note: balance > effective_fee was verified in eth_send_raw_unsigned_tx() + let max_fee = gas_limit * env.gas_price; + let (fee_high, fee_low) = split_felt(max_fee); + let max_fee_u256 = Uint256(low=fee_low, high=fee_high); + + let sender = State.get_account(env.origin); + let (local new_balance) = uint256_sub([sender.balance], max_fee_u256); + let sender = Account.set_balance(sender, &new_balance); + let sender = Account.set_nonce(sender, sender.nonce + 1); + State.update_account(env.origin, sender); + + let transfer = model.Transfer(env.origin, address, [value]); + let success = State.add_transfer(transfer); + + // Check collision + let account = State.get_account(address); + let code_or_nonce = Account.has_code_or_nonce(account); + let is_collision = code_or_nonce * is_deploy_tx; + // Nonce is set to 1 in case of deploy_tx and account is marked as created + let nonce = account.nonce * (1 - is_deploy_tx) + is_deploy_tx; + let account = Account.set_nonce(account, nonce); + let account = Account.set_created(account, is_deploy_tx); + State.update_account(address, account); + tempvar message = new model.Message( bytecode=bytecode, bytecode_len=bytecode_len, @@ -885,65 +927,7 @@ namespace Interpreter { env=env, initial_state=initial_state, ); - - let stack = Stack.init(); - let memory = Memory.init(); - - // Cache the coinbase, precompiles, caller, and target, making them warm - with state { - let coinbase = State.get_account(env.coinbase); - State.cache_precompiles(); - State.get_account(address); - let access_list_cost = State.cache_access_list(access_list_len, access_list); - } - - let intrinsic_gas = intrinsic_gas + access_list_cost; let evm = EVM.init(message, gas_limit - intrinsic_gas); - - let is_gas_limit_enough = is_le_felt(intrinsic_gas, gas_limit); - if (is_gas_limit_enough == FALSE) { - let evm = EVM.halt_validation_failed(evm); - State.finalize{state=state}(); - return (); - } - - tempvar is_initcode_invalid = is_deploy_tx * is_nn( - bytecode_len - (2 * Constants.MAX_CODE_SIZE + 1) - ); - if (is_initcode_invalid != FALSE) { - let evm = EVM.halt_validation_failed(evm); - State.finalize{state=state}(); - return (); - } - - // Charge the gas fee to the user without setting up a transfer. - // Transfers with the exact amounts will be performed post-execution. - // Note: balance > effective_fee was verified in eth_send_raw_unsigned_tx() - let max_fee = gas_limit * env.gas_price; - let (fee_high, fee_low) = split_felt(max_fee); - let max_fee_u256 = Uint256(low=fee_low, high=fee_high); - - with state { - let sender = State.get_account(env.origin); - let (local new_balance) = uint256_sub([sender.balance], max_fee_u256); - let sender = Account.set_balance(sender, &new_balance); - let sender = Account.set_nonce(sender, sender.nonce + 1); - State.update_account(env.origin, sender); - - let transfer = model.Transfer(env.origin, address, [value]); - let success = State.add_transfer(transfer); - - // Check collision - let account = State.get_account(address); - let code_or_nonce = Account.has_code_or_nonce(account); - let is_collision = code_or_nonce * is_deploy_tx; - // Nonce is set to 1 in case of deploy_tx and account is marked as created - let nonce = account.nonce * (1 - is_deploy_tx) + is_deploy_tx; - let account = Account.set_nonce(account, nonce); - let account = Account.set_created(account, is_deploy_tx); - State.update_account(address, account); - } - if (is_collision != 0) { let (revert_reason_len, revert_reason) = Errors.addressCollision(); tempvar evm = EVM.stop(evm, revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT); @@ -958,25 +942,15 @@ namespace Interpreter { tempvar evm = evm; } - with stack, memory, state { + with stack, memory { let evm = run(evm); } - let required_gas = gas_limit - evm.gas_left; - let (max_refund, _) = unsigned_div_rem(required_gas, 5); - let is_max_refund_le_gas_refund = is_nn(evm.gas_refund - max_refund); - tempvar gas_refund = is_max_refund_le_gas_refund * max_refund + ( - 1 - is_max_refund_le_gas_refund - ) * evm.gas_refund; - - let total_gas_used = required_gas - gas_refund; + let initial_state = evm.message.initial_state; + State.finalize{state=initial_state}(); // Reset the state if the execution has failed. // Only the gas fee paid will be committed. - State.finalize{state=state}(); - tempvar initial_state = evm.message.initial_state; - State.finalize{state=initial_state}(); - if (evm.reverted != 0) { tempvar state = initial_state; } else { @@ -986,25 +960,30 @@ namespace Interpreter { let success = 1 - is_reverted; let paid_fee_u256 = Uint256(max_fee_u256.low * success, max_fee_u256.high * success); - with state { - let sender = State.get_account(env.origin); - uint256_add([sender.balance], paid_fee_u256); - let (ap_val) = get_ap(); - let sender = Account.set_balance(sender, cast(ap_val - 3, Uint256*)); - let sender = Account.set_nonce(sender, sender.nonce + is_reverted); - State.update_account(env.origin, sender); - } + let sender = State.get_account(env.origin); + uint256_add([sender.balance], paid_fee_u256); + let (ap_val) = get_ap(); + let sender = Account.set_balance(sender, cast(ap_val - 3, Uint256*)); + let sender = Account.set_nonce(sender, sender.nonce + is_reverted); + State.update_account(env.origin, sender); // So as to not burn the base_fee_per gas, we send it to the coinbase. + let required_gas = gas_limit - evm.gas_left; + let (max_refund, _) = unsigned_div_rem(required_gas, 5); + let is_max_refund_le_gas_refund = is_nn(evm.gas_refund - max_refund); + tempvar gas_refund = is_max_refund_le_gas_refund * max_refund + ( + 1 - is_max_refund_le_gas_refund + ) * evm.gas_refund; + + let total_gas_used = required_gas - gas_refund; let actual_fee = total_gas_used * env.gas_price; let (fee_high, fee_low) = split_felt(actual_fee); let actual_fee_u256 = Uint256(low=fee_low, high=fee_high); let transfer = model.Transfer(env.origin, env.coinbase, actual_fee_u256); - with state { - State.add_transfer(transfer); - State.finalize(); - } + // TODO: This should be burnt + State.add_transfer(transfer); + State.finalize(); return (); } diff --git a/cairo/src/state.cairo b/cairo/src/state.cairo index f88396d5..83a396c5 100644 --- a/cairo/src/state.cairo +++ b/cairo/src/state.cairo @@ -30,9 +30,9 @@ namespace State { // @param self The pointer to the State func copy{range_check_ptr, state: model.State*}() -> model.State* { alloc_locals; - // accounts are a new memory segment + // accounts are a new memory segment with the same underlying data let (accounts_start, accounts) = dict_copy(state.accounts_start, state.accounts); - // for each account, storage is a new memory segment + // for each account, storage is a new memory segment with the same underlying data Internals._copy_accounts{accounts=accounts}(accounts_start, accounts); let (local events: felt*) = alloc(); @@ -388,13 +388,6 @@ namespace Internals { return (); } - if (accounts_start.new_value == 0) { - // If we do a dict_read on an unexisting account, `prev_value` and `new_value` are set to 0. - // However we expected pointers to model.Account, and casting 0 to model.Account* will - // cause a "Memory address must be relocatable" error. - return _copy_accounts(accounts_start + DictAccess.SIZE, accounts_end); - } - let account = cast(accounts_start.new_value, model.Account*); let account = Account.copy(account); dict_write{dict_ptr=accounts}(key=accounts_start.key, new_value=cast(account, felt)); @@ -412,13 +405,6 @@ namespace Internals { return (); } - if (accounts_start.new_value == 0) { - // If we do a dict_read on an unexisting account, `prev_value` and `new_value` are set to 0. - // However we expected pointers to model.Account, and casting 0 to model.Account* will - // cause a "Memory address must be relocatable" error. - return _finalize_accounts(accounts_start + DictAccess.SIZE, accounts_end); - } - let account = cast(accounts_start.new_value, model.Account*); let account = Account.finalize(account); dict_write{dict_ptr=accounts}(key=accounts_start.key, new_value=cast(account, felt)); @@ -461,10 +447,6 @@ namespace Internals { let (account_ptr) = dict_read{dict_ptr=accounts_ptr}(key=address); tempvar account = cast(account_ptr, model.Account*); - let pedersen_ptr = cast([ap - 3], HashBuiltin*); - let range_check_ptr = [ap - 2]; - let account = cast([ap - 1], model.Account*); - let account = Account.cache_storage_keys( account, storage_keys_len, cast(access_list + 2, Uint256*) ); diff --git a/cairo/src/utils/compiler.py b/cairo/src/utils/compiler.py index 41029fc4..19f563f0 100644 --- a/cairo/src/utils/compiler.py +++ b/cairo/src/utils/compiler.py @@ -18,6 +18,9 @@ dict_copy = """ from starkware.cairo.common.dict import DictTracker +if ids.new_start.address_.segment_index in __dict_manager.trackers: + raise ValueError(f"Segment {ids.new_start.address_.segment_index} already exists in __dict_manager.trackers") + data = __dict_manager.trackers[ids.dict_start.address_.segment_index].data.copy() __dict_manager.trackers[ids.new_start.address_.segment_index] = DictTracker( data=data, diff --git a/cairo/src/utils/constants.py b/cairo/src/utils/constants.py index 6376b3e6..956924fd 100644 --- a/cairo/src/utils/constants.py +++ b/cairo/src/utils/constants.py @@ -13,4 +13,4 @@ BUILD_DIR = Path("build") BUILD_DIR.mkdir(exist_ok=True, parents=True) -CHAIN_ID = int.from_bytes(b"keth", "big") +CHAIN_ID = 1 diff --git a/cairo/src/utils/utils.cairo b/cairo/src/utils/utils.cairo index b8ef8eaa..b9d95000 100644 --- a/cairo/src/utils/utils.cairo +++ b/cairo/src/utils/utils.cairo @@ -1,6 +1,6 @@ from starkware.cairo.common.alloc import alloc from starkware.cairo.common.math import assert_le, split_felt, assert_nn_le -from starkware.cairo.common.math_cmp import is_nn, is_not_zero +from starkware.cairo.common.math_cmp import is_nn, is_not_zero, is_in_range from starkware.cairo.common.memcpy import memcpy from starkware.cairo.common.dict_access import DictAccess from starkware.cairo.common.bool import TRUE, FALSE @@ -788,73 +788,179 @@ namespace Helpers { // @param bytecode The EVM bytecode to analyze. // @return (valid_jumpdests_start, valid_jumpdests) The starting and ending pointers of the valid jump destinations. // - // @dev This function iterates over the bytecode from the current index 'i'. - // If the opcode at the current index is between 0x5f and 0x7f (PUSHN opcodes) (inclusive), - // it skips the next 'n_args' opcodes, where 'n_args' is the opcode minus 0x5f. - // If the opcode is 0x5b (JUMPDEST), it marks the current index as a valid jump destination. - // It continues by jumping back to the body flag until it has processed the entire bytecode. + // @dev This function is an oracle and doesn't enforce anything. During the EVM execution, the prover + // commits to the valid or invalid jumpdest responses, and the verifier checks the response in the + // finalize_jumpdests function. func initialize_jumpdests{range_check_ptr}(bytecode_len: felt, bytecode: felt*) -> ( valid_jumpdests_start: DictAccess*, valid_jumpdests: DictAccess* ) { alloc_locals; - let (local valid_jumpdests_start: DictAccess*) = default_dict_new(0); - tempvar range_check_ptr = range_check_ptr; - tempvar valid_jumpdests = valid_jumpdests_start; - tempvar i = 0; - jmp body if bytecode_len != 0; - - static_assert range_check_ptr == [ap - 3]; - jmp end; + %{ + from ethereum.cancun.vm.runtime import get_valid_jump_destinations + from starkware.cairo.common.dict import DictTracker + from collections import defaultdict + + bytecode = bytes([memory[ids.bytecode + i] for i in range(ids.bytecode_len)]) + valid_jumpdest = get_valid_jump_destinations(bytecode) + + data = defaultdict(int, {int(dest): 1 for dest in valid_jumpdest}) + base = segments.add() + assert base.segment_index not in __dict_manager.trackers + __dict_manager.trackers[base.segment_index] = DictTracker( + data=data, current_ptr=base + ) + memory[ap] = base + %} + ap += 1; + let valid_jumpdests_start = cast([ap - 1], DictAccess*); + return (valid_jumpdests_start, valid_jumpdests_start); + } - body: - let bytecode_len = [fp - 4]; - let bytecode = cast([fp - 3], felt*); - let range_check_ptr = [ap - 3]; - let valid_jumpdests = cast([ap - 2], DictAccess*); - let i = [ap - 1]; + // @notice Assert that the dictionary of valid jump destinations in EVM bytecode is valid. + // @dev Iterate over the list of DictAccesses and assert that + // - the prev_value is equal to the new_value (no dict_writes) + // - if the prev_value is TRUE + // - assert that the bytecode at the key is 0x5b (JUMPDEST) + // - assert that no PUSH are right before the JUMPDEST + // - if the prev_value is FALSE, assert that the bytecode at the key is not 0x5b (JUMPDEST) + // @dev The keys are supposed to be sorted in ascending order, it's not a soundness issue if it's + // not the case as the final assert will fail. + func finalize_jumpdests{range_check_ptr}( + index: felt, + valid_jumpdests_start: DictAccess*, + valid_jumpdests: DictAccess*, + bytecode: felt*, + ) { + alloc_locals; - with_attr error_message("Reading out of bounds bytecode") { - assert [range_check_ptr] = bytecode_len - 1 - i; + if (valid_jumpdests_start == valid_jumpdests) { + return (); } + + // Assert that the jumpdests are sorted in ascending order + assert [range_check_ptr] = valid_jumpdests_start.key - index; let range_check_ptr = range_check_ptr + 1; + assert_valid_jumpdest(index, bytecode, valid_jumpdests_start); + + return finalize_jumpdests( + index=valid_jumpdests_start.key + 1, + valid_jumpdests_start=valid_jumpdests_start + DictAccess.SIZE, + valid_jumpdests=valid_jumpdests, + bytecode=bytecode, + ); + } + + // @notice Assert that a single valid_jumpdest is valid. + // @dev Use a hint to determine if the easy case (no PUSHes before the JUMPDEST) is true + // Otherwise, starts back at the given start_index, ie analyse the whole bytecode[start_index:key] segment. + func assert_valid_jumpdest{range_check_ptr}( + start_index: felt, bytecode: felt*, valid_jumpdest: DictAccess* + ) { + alloc_locals; + // Assert that the dict access is only read (same prev and new value) + assert valid_jumpdest.prev_value = valid_jumpdest.new_value; + let bytecode_at_jumpdest = [bytecode + valid_jumpdest.key]; + + if (bytecode_at_jumpdest != 0x5b) { + with_attr error_message("assert_valid_jumpdest: invalid jumpdest") { + assert valid_jumpdest.prev_value = 0; + } + return (); + } + + // At this stage, we know that the jumpdest is a JUMPDEST byte. We still need to check if there is a PUSH + // before or if it's actually a JUMPDEST opcode. There are two cases: + // 1. Optimized case: We can just assert that there is no PUSH in the 32 bytes before the JUMPDEST + // 2. General case: We incrementally update the PC from start_index and check if we eventually reach the JUMPDEST. + // This is generally speaking more step consuming and will be used if the optimized case is not possible. Note that + // the start_index needs to point to a real opcode and is not checked to be itself a PUSH argument. 0 will always + // be sound; if some previous JUMPDEST are already checked, they can be used to shorten the range of the general case. + if (valid_jumpdest.key == 0) { + with_attr error_message("assert_valid_jumpdest: invalid jumpdest") { + assert valid_jumpdest.prev_value = 1; + } + return (); + } + + tempvar is_no_push_case; + %{ + # Get the 32 previous bytes + bytecode = [memory[ids.bytecode + ids.valid_jumpdest.key - i - 1] for i in range(min(ids.valid_jumpdest.key, 32))][::-1] + # Check if any PUSH may prevent this to be a JUMPDEST + memory[ap - 1] = int(not any([0x60 + i <= byte <= 0x7f for i, byte in enumerate(bytecode[::-1])])) + %} + jmp no_push_case if is_no_push_case != 0; + + general_case: + tempvar range_check_ptr = range_check_ptr; + tempvar i = start_index; + + body_general_case: + let bytecode = cast([fp - 4], felt*); + let range_check_ptr = [ap - 2]; + let i = [ap - 1]; + tempvar opcode = [bytecode + i]; - let is_opcode_ge_0x5f = Helpers.is_le_unchecked(0x5f, opcode); - let is_opcode_le_0x7f = Helpers.is_le_unchecked(opcode, 0x7f); - let is_push_opcode = is_opcode_ge_0x5f * is_opcode_le_0x7f; - let next_i = i + 1 + is_push_opcode * (opcode - 0x5f); // 0x5f is the first PUSHN opcode, opcode - 0x5f is the number of arguments. - - if (opcode == 0x5b) { - dict_write{dict_ptr=valid_jumpdests}(i, TRUE); - tempvar valid_jumpdests = valid_jumpdests; - tempvar next_i = next_i; - tempvar range_check_ptr = range_check_ptr; - } else { - tempvar valid_jumpdests = valid_jumpdests; - tempvar next_i = next_i; - tempvar range_check_ptr = range_check_ptr; - } - - // continue_loop != 0 => next_i - bytecode_len < 0 <=> next_i < bytecode_len - tempvar a = next_i - bytecode_len; - %{ memory[ap] = 0 if 0 <= (ids.a % PRIME) < range_check_builtin.bound else 1 %} - ap += 1; - let continue_loop = [ap - 1]; + let is_push_opcode = is_in_range(opcode, 0x60, 0x80); + tempvar next_i = i + 1 + is_push_opcode * (opcode - 0x5f); + + tempvar cond; tempvar range_check_ptr = range_check_ptr; - tempvar valid_jumpdests = valid_jumpdests; tempvar i = next_i; - static_assert range_check_ptr == [ap - 3]; - static_assert valid_jumpdests == [ap - 2]; - static_assert i == [ap - 1]; - jmp body if continue_loop != 0; + %{ ids.cond = 1 if ids.i < ids.valid_jumpdest.key else 0 %} + jmp body_general_case if cond != 0; - end: - let range_check_ptr = [ap - 3]; + let range_check_ptr = [ap - 2]; let i = [ap - 1]; - // Verify that i >= bytecode_len to ensure loop terminated correctly. - let check = Helpers.is_le_unchecked(bytecode_len, i); - assert check = 1; - return (valid_jumpdests_start, valid_jumpdests); + + // We stop the loop when i >= valid_jumpdest.key + assert [range_check_ptr] = i - valid_jumpdest.key; + let range_check_ptr = range_check_ptr + 1; + + // Either the jumpdest is valid and we've reached it, or it's not and we've overpassed it + with_attr error_message("assert_valid_jumpdest: invalid jumpdest") { + assert (i - valid_jumpdest.key) * valid_jumpdest.prev_value = 0; + } + + return (); + + no_push_case: + tempvar offset = 1; + tempvar range_check_ptr = range_check_ptr; + + body_no_push_case: + let offset = [ap - 2]; + let range_check_ptr = [ap - 1]; + let bytecode = cast([fp - 4], felt*); + let valid_jumpdest = cast([fp - 3], DictAccess*); + + let opcode = [bytecode + valid_jumpdest.key - offset]; + // offset is the distance from the JUMPDEST, so offset = i means that any PUSH_i + // with i > offset may prevent this to be a JUMPDEST + let is_push_opcode = is_in_range(opcode, 0x5f + offset, 0x80); + assert is_push_opcode = 0; + tempvar cond; + tempvar offset = offset + 1; + tempvar range_check_ptr = range_check_ptr; + + static_assert offset == [ap - 2]; + static_assert range_check_ptr == [ap - 1]; + %{ ids.cond = 0 if ids.offset > 32 or ids.valid_jumpdest.key < ids.offset else 1 %} + jmp body_no_push_case if cond != 0; + + let offset = [ap - 2]; + let range_check_ptr = [ap - 1]; + let valid_jumpdest = cast([fp - 3], DictAccess*); + + // Offset should be either 33 or key + 1, meaning we've been up until the beginning of the + // bytecode, or up to 32 bytes earlier + assert (32 + 1 - offset) * (valid_jumpdest.key + 1 - offset) = 0; + with_attr error_message("assert_valid_jumpdest: invalid jumpdest") { + assert valid_jumpdest.prev_value = 1; + } + + return (); } const BYTES_PER_FELT = 31; diff --git a/cairo/tests/conftest.py b/cairo/tests/conftest.py index d09347fb..456bb4f1 100644 --- a/cairo/tests/conftest.py +++ b/cairo/tests/conftest.py @@ -46,6 +46,12 @@ def pytest_addoption(parser): dest="skip_cached_tests", help="run all tests regardless of cache", ) + parser.addoption( + "--no-skip-mark", + action="store_true", + default=False, + help="Do not skip tests by marked with @pytest.mark.skip", + ) parser.addoption( "--layout", choices=dir(LAYOUTS), @@ -191,9 +197,16 @@ def pytest_collection_modifyitems(session, config, items): + file_hash(item.fspath) + item.nodeid.encode() + file_hash(Path(__file__).parent / "fixtures" / "runner.py") + + file_hash(Path(__file__).parent / "utils" / "serde.py") + + file_hash(Path(__file__).parent / "utils" / "args_gen.py") ).hexdigest() session.test_hashes[item.nodeid] = test_hash + if config.getoption("no_skip_mark"): + item.own_markers = [ + mark for mark in item.own_markers if mark.name != "skip" + ] + if test_hash in tests_to_skip and config.getoption("skip_cached_tests"): item.add_marker(pytest.mark.skip(reason="Cached results")) diff --git a/cairo/tests/fixtures/runner.py b/cairo/tests/fixtures/runner.py index 14ef2e21..397dde1b 100644 --- a/cairo/tests/fixtures/runner.py +++ b/cairo/tests/fixtures/runner.py @@ -344,7 +344,6 @@ def _factory(entrypoint, *args, **kwargs): if add_output: final_output = serde.serialize_list(output_ptr) - cumulative_retdata_offsets = serde.get_offsets(return_data_types) unfiltered_output = [ serde.serialize(return_data_type, runner.vm.run_context.ap, offset) for offset, return_data_type in zip( diff --git a/cairo/tests/programs/test_os.py b/cairo/tests/programs/test_os.py index 89f60dfb..49e77ccc 100644 --- a/cairo/tests/programs/test_os.py +++ b/cairo/tests/programs/test_os.py @@ -92,7 +92,7 @@ def test_erc20_transfer(self, cairo_run): @pytest.mark.skip("Only for debugging") @pytest.mark.slow - @pytest.mark.parametrize("block_number", [21389405]) + @pytest.mark.parametrize("block_number", [21421739]) def test_eth_block(self, cairo_run, block_number): prover_input_path = Path(f"cache/{block_number}_long.json") with open(prover_input_path, "r") as f: diff --git a/cairo/tests/src/test_state.cairo b/cairo/tests/src/test_state.cairo index af3ee5ad..c43d3ea9 100644 --- a/cairo/tests/src/test_state.cairo +++ b/cairo/tests/src/test_state.cairo @@ -106,33 +106,6 @@ func test__is_account_alive__account_not_alive_not_in_state{ return (); } -func test___copy_accounts__should_handle_null_pointers{range_check_ptr}() { - alloc_locals; - let (accounts) = default_dict_new(0); - tempvar accounts_start = accounts; - tempvar address = 2; - tempvar balance = new Uint256(1, 0); - let (code) = alloc(); - tempvar code_hash = new Uint256( - 304396909071904405792975023732328604784, 262949717399590921288928019264691438528 - ); - let account = Account.init(0, code, code_hash, 1, balance); - dict_write{dict_ptr=accounts}(address, cast(account, felt)); - let empty_address = 'empty address'; - dict_read{dict_ptr=accounts}(empty_address); - let (local accounts_copy: DictAccess*) = default_dict_new(0); - Internals._copy_accounts{accounts=accounts_copy}(accounts_start, accounts); - - let (pointer) = dict_read{dict_ptr=accounts_copy}(address); - tempvar existing_account = cast(pointer, model.Account*); - - assert existing_account.balance.low = 1; - assert existing_account.balance.high = 0; - assert existing_account.code_len = 0; - - return (); -} - func test__is_account_warm__account_in_state{pedersen_ptr: HashBuiltin*, range_check_ptr}() { let evm_address = 'alive'; tempvar address = evm_address; diff --git a/cairo/tests/src/test_state.py b/cairo/tests/src/test_state.py index d0c33b10..67234db1 100644 --- a/cairo/tests/src/test_state.py +++ b/cairo/tests/src/test_state.py @@ -15,10 +15,6 @@ class TestIsAccountWarm: def test_should_return_true_when_account_in_state(self, cairo_run): cairo_run("test__is_account_warm__account_in_state") - class TestCopyAccounts: - def test_should_handle_null_pointers(self, cairo_run): - cairo_run("test___copy_accounts__should_handle_null_pointers") - class TestAddTransfer: def test_should_return_false_when_overflowing_recipient_balance( self, cairo_run diff --git a/cairo/tests/src/utils/test_utils.cairo b/cairo/tests/src/utils/test_utils.cairo index 18371d6d..8b01d540 100644 --- a/cairo/tests/src/utils/test_utils.cairo +++ b/cairo/tests/src/utils/test_utils.cairo @@ -5,7 +5,9 @@ from starkware.cairo.common.alloc import alloc from starkware.cairo.common.uint256 import Uint256, assert_uint256_eq from starkware.cairo.common.memset import memset from starkware.cairo.common.memcpy import memcpy +from starkware.cairo.common.dict_access import DictAccess +from src.utils.dict import dict_squash from src.utils.utils import Helpers from src.constants import Constants from tests.utils.dict import dict_keys @@ -122,9 +124,59 @@ func test__initialize_jumpdests{range_check_ptr}(output_ptr: felt*) { let (valid_jumpdests_start, valid_jumpdests) = Helpers.initialize_jumpdests( bytecode_len, bytecode ); - let (keys_len, keys) = dict_keys(valid_jumpdests_start, valid_jumpdests); - memcpy(output_ptr, keys, keys_len); + %{ segments.write_arg(ids.output_ptr, __dict_manager.get_dict(ids.valid_jumpdests)) %} + + return (); +} + +func test__finalize_jumpdests{range_check_ptr}() { + alloc_locals; + + local bytecode: felt*; + local valid_jumpdests_start: DictAccess*; + local valid_jumpdests: DictAccess*; + %{ + from starkware.cairo.common.dict import DictTracker + from tests.utils.helpers import flatten + from ethereum.cancun.vm.runtime import get_valid_jump_destinations + + memory[fp] = segments.add() + segments.write_arg(memory[fp], program_input["bytecode"]) + + data = {k: 1 for k in get_valid_jump_destinations(program_input["bytecode"])} + + base = segments.add() + segments.load_data( + base, + flatten([[int(k), 1, 1] for k in data.keys()]) + ) + __dict_manager.trackers[base.segment_index] = DictTracker( + data=data, + current_ptr=(base + len(data) * 3), + ) + memory[fp + 1] = base + memory[fp + 2] = base + len(data) * 3 + %} + + let (sorted_keys_start, sorted_keys_end) = dict_squash(valid_jumpdests_start, valid_jumpdests); + + Helpers.finalize_jumpdests(0, sorted_keys_start, sorted_keys_end, bytecode); + + return (); +} + +func test__assert_valid_jumpdest{range_check_ptr}() { + alloc_locals; + tempvar bytecode: felt*; + tempvar valid_jumpdest: DictAccess*; + %{ + ids.bytecode = segments.add() + segments.write_arg(ids.bytecode, program_input["bytecode"]) + ids.valid_jumpdest = segments.add() + segments.write_arg(ids.valid_jumpdest.address_, program_input["valid_jumpdest"]) + %} + Helpers.assert_valid_jumpdest(0, bytecode, valid_jumpdest); return (); } diff --git a/cairo/tests/src/utils/test_utils.py b/cairo/tests/src/utils/test_utils.py index c6d093ef..50a8a760 100644 --- a/cairo/tests/src/utils/test_utils.py +++ b/cairo/tests/src/utils/test_utils.py @@ -8,7 +8,6 @@ from ethereum.cancun.vm.runtime import get_valid_jump_destinations from tests.utils.errors import cairo_error -from tests.utils.hints import patch_hint from tests.utils.solidity import get_contract @@ -111,22 +110,46 @@ def test_should_return_same_as_execution_specs(self, cairo_run, bytecode: Bytes) map(Uint, output if isinstance(output, list) else [output]) ) == get_valid_jump_destinations(bytecode) + +class TestFinalizeJumpdests: @given(bytecode=...) @example(bytecode=get_contract("Counter", "Counter").bytecode_runtime) - def test_should_err_on_malicious_prover( - self, cairo_program, cairo_run, bytecode: Bytes - ): - with ( - patch_hint( - cairo_program, - "memory[ap] = 0 if 0 <= (ids.a % PRIME) < range_check_builtin.bound else 1", - "memory[ap] = 1", - "initialize_jumpdests", - ), - cairo_error(message="Reading out of bounds bytecode"), - ): - bytecode = get_contract("Counter", "Counter").bytecode_runtime - cairo_run("test__initialize_jumpdests", bytecode=bytecode) + def test_should_pass(self, cairo_run, bytecode: Bytes): + cairo_run( + "test__finalize_jumpdests", + bytecode=list(bytecode), + valid_jumpdests=get_valid_jump_destinations(bytecode), + ) + + +class TestAssertValidJumpdest: + @pytest.mark.parametrize( + "jumpdest", + get_valid_jump_destinations( + get_contract("Counter", "Counter").bytecode_runtime + ), + ) + def test_should_pass_on_valid_jumpdest(self, cairo_run, jumpdest): + cairo_run( + "test__assert_valid_jumpdest", + bytecode=list(get_contract("Counter", "Counter").bytecode_runtime), + valid_jumpdest=[int(jumpdest), 1, 1], + ) + + def test_should_raise_if_jumpdest_but_false(self, cairo_run): + with cairo_error("assert_valid_jumpdest: invalid jumpdest"): + cairo_run( + "test__assert_valid_jumpdest", bytecode=[0x5B], valid_jumpdest=[0, 0, 0] + ) + + @pytest.mark.parametrize("push", list(range(0x60, 0x80))) + def test_should_raise_if_jumpdest_is_push_arg(self, cairo_run, push): + with cairo_error("assert_valid_jumpdest: invalid jumpdest"): + cairo_run( + "test__assert_valid_jumpdest", + bytecode=[push] + (push - 0x5F) * [0x5B], + valid_jumpdest=[push - 0x5F, 1, 1], + ) class TestSplitWord: diff --git a/cairo/tests/test_serde.cairo b/cairo/tests/test_serde.cairo index 1b1febc7..18a1b5d1 100644 --- a/cairo/tests/test_serde.cairo +++ b/cairo/tests/test_serde.cairo @@ -35,5 +35,6 @@ from ethereum.cancun.transactions import ( from ethereum.cancun.vm.gas import MessageCallGas -from ethereum.cancun.trie import BranchNode, ExtensionNode, InternalNode, LeafNode, Node +from ethereum.cancun.trie import BranchNode, ExtensionNode, InternalNode, LeafNode, Node, Subnodes from ethereum.exceptions import EthereumException +from ethereum.cancun.vm.exceptions import StackOverflowError, StackUnderflowError diff --git a/cairo/tests/test_serde.py b/cairo/tests/test_serde.py index 140da893..1b55d1cd 100644 --- a/cairo/tests/test_serde.py +++ b/cairo/tests/test_serde.py @@ -19,6 +19,7 @@ Transaction, ) from ethereum.cancun.trie import BranchNode, ExtensionNode, InternalNode, LeafNode, Node +from ethereum.cancun.vm.exceptions import StackOverflowError, StackUnderflowError from ethereum.cancun.vm.gas import MessageCallGas from ethereum.exceptions import EthereumException from tests.utils.args_gen import _cairo_struct_to_python_type @@ -183,16 +184,24 @@ def test_type( @given(err=...) def test_exception( - self, to_cairo_type, segments, serde, gen_arg, err: Union[EthereumException] + self, + to_cairo_type, + segments, + serde, + gen_arg, + err: Union[EthereumException, StackOverflowError, StackUnderflowError], ): - base = segments.gen_arg([gen_arg(EthereumException, err)]) + base = segments.gen_arg([gen_arg(type(err), err)]) with pytest.raises(type(err)) as exception: - serde.serialize(to_cairo_type(EthereumException), base, shift=0) + serde.serialize(to_cairo_type(type(err)), base, shift=0) assert str(exception.value) == str(err) - def test_none_exception(self, to_cairo_type, serde, gen_arg): - base = gen_arg(EthereumException, None) - result = serde.serialize(to_cairo_type(EthereumException), base, shift=0) + @pytest.mark.parametrize( + "error_type", [EthereumException, StackOverflowError, StackUnderflowError] + ) + def test_none_exception(self, to_cairo_type, serde, gen_arg, error_type): + base = gen_arg(error_type, None) + result = serde.serialize(to_cairo_type(error_type), base, shift=0) assert result is None diff --git a/cairo/tests/utils/args_gen.py b/cairo/tests/utils/args_gen.py index 852d955c..6226c461 100644 --- a/cairo/tests/utils/args_gen.py +++ b/cairo/tests/utils/args_gen.py @@ -199,6 +199,7 @@ "exceptions", "StackOverflowError", ): StackOverflowError, + ("ethereum", "cancun", "trie", "Subnodes"): Annotated[Tuple[Extended, ...], 16], } # In the EELS, some functions are annotated with Sequence while it's actually just Bytes. @@ -302,9 +303,14 @@ def _gen_arg( # These are represented as a pointer to a struct with a pointer to each element. element_types = get_args(arg_type) - # Handle fixed-size tuples with size annotation (e.g. Annotated[Tuple[T], N]) - if annotations and len(annotations) == 1 and len(element_types) == 1: - element_types = element_types * annotations[0] + # Handle fixed-size tuples with size annotation (e.g. Annotated[Tuple[T, ...], N]) + if ( + annotations + and len(annotations) == 1 + and len(element_types) == 2 + and element_types[1] == Ellipsis + ): + element_types = [element_types[0]] * annotations[0] elif annotations: raise ValueError( f"Invalid tuple size annotation for {arg_type} with annotations {annotations}" diff --git a/cairo/tests/utils/serde.py b/cairo/tests/utils/serde.py index 2d10f0c4..8be705a4 100644 --- a/cairo/tests/utils/serde.py +++ b/cairo/tests/utils/serde.py @@ -124,9 +124,10 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: if "__main__" in full_path: full_path = self.main_part + full_path[full_path.index("__main__") + 1 :] python_cls = to_python_type(full_path) + annotations = [] if get_origin(python_cls) is Annotated: - python_cls, _ = get_args(python_cls) + python_cls, *annotations = get_args(python_cls) if get_origin(python_cls) is Union: value_ptr = self.serialize_pointers(path, ptr)["value"] @@ -184,8 +185,8 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: } return [dict_repr[i] for i in range(list_len)] - if get_origin(python_cls) in (tuple, list, Sequence, abc.Sequence): - # Tuple and list are represented as structs with a pointer to the first element and the length. + if get_origin(python_cls) in (tuple, Sequence, abc.Sequence): + # Tuples are represented as structs with a pointer to the first element and the length. # The value field is a list of Relocatable (pointers to each element) or Felt (tuple of felts). # In usual cairo, a pointer to a struct, (e.g. Uint256*) is actually a pointer to one single # memory segment, where values need to be read from consecutive memory cells (e.g. data[i: i + 2]). @@ -197,12 +198,24 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: .cairo_type.pointee.scope.path ) members = get_struct_definition(self.program, tuple_struct_path).members - if get_origin(python_cls) is tuple and Ellipsis not in get_args(python_cls): + if get_origin(python_cls) is tuple and ( + (Ellipsis not in get_args(python_cls)) + or (Ellipsis in get_args(python_cls) and len(annotations) == 1) + ): # These are regular tuples with a given size. - return tuple( + result = tuple( self._serialize(member.cairo_type, tuple_struct_ptr + member.offset) for member in members.values() ) + if ( + annotations + and len(annotations) == 1 + and annotations[0] != len(result) + ): + raise ValueError( + f"Expected tuple of size {annotations[0]}, got {len(result)}" + ) + return result else: # These are tuples with a variable size (or list or sequences). raw = self.serialize_pointers(tuple_struct_path, tuple_struct_ptr) @@ -270,9 +283,8 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: value_type = ( get_struct_definition(self.program, path).members["value"].cairo_type ) - error_bytes = self._serialize(value_type, tuple_struct_ptr) - error_message = error_bytes.decode() or "" - raise python_cls(error_message) + error_bytes = self._serialize(value_type, ptr) + raise python_cls(error_bytes.decode()) if python_cls == Bytes256: base_ptr = self.memory.get(ptr) diff --git a/cairo/tests/utils/strategies.py b/cairo/tests/utils/strategies.py index 6a3d9d70..7355b2a8 100644 --- a/cairo/tests/utils/strategies.py +++ b/cairo/tests/utils/strategies.py @@ -150,7 +150,7 @@ def register_type_strategies(): # 16 subnodes of 32 bytes each "subnodes": st.lists( st.binary(min_size=32, max_size=32), min_size=16, max_size=16 - ), + ).map(tuple), # Value in branch nodes is always empty "value": st.just(b""), } diff --git a/uv.lock b/uv.lock index 96dcc100..2d6f970d 100644 --- a/uv.lock +++ b/uv.lock @@ -389,7 +389,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "cairo-lang", specifier = ">=0.13.2" }, - { name = "ethereum", git = "https://github.com/kkrt-labs/execution-specs.git?branch=dev%2Fchange-type-branch-nodes" }, + { name = "ethereum", git = "https://github.com/kkrt-labs/execution-specs.git?rev=b255036441d64437bd4fc9f9068bc64c45470e93" }, { name = "marshmallow-dataclass", specifier = ">=8.6.1" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "toml", specifier = ">=0.10.2" }, @@ -975,7 +975,7 @@ wheels = [ [[package]] name = "ethereum" version = "0.1.0" -source = { git = "https://github.com/kkrt-labs/execution-specs.git?branch=dev%2Fchange-type-branch-nodes#678325284038d9aafca8a7594e97f8544d698947" } +source = { git = "https://github.com/kkrt-labs/execution-specs.git?rev=b255036441d64437bd4fc9f9068bc64c45470e93#b255036441d64437bd4fc9f9068bc64c45470e93" } dependencies = [ { name = "coincurve" }, { name = "ethereum-types" },