diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index d51e7e0144..227779994f 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -23,5 +23,6 @@ benchmark_fast: bal: evm-type: develop - fill-params: --fork=Amsterdam ./tests/amsterdam/eip7928_block_level_access_lists + # TODO: Turn on block rlp limit tests after making filling them more flexible. + fill-params: --fork=Amsterdam -k "not eip7934" feature_only: true diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cdc6168306..6aeefed9f4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -48,6 +48,7 @@ Test fixtures for use by clients are available for each release on the [Github r ### πŸ“‹ Misc - 🐞 WELDed the EEST tox environments relevant to producing documentation into EELS, and added a tool to cleanly add codespell whitelist entries. ([#1695](https://github.com/ethereum/execution-specs/pull/1659)). +- 🐞 Fix duplicate storage write issues for block access lists EIP-7928 implementation ([#1743](https://github.com/ethereum/execution-specs/pull/1743)). ### πŸ§ͺ Test Cases diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/helpers/ruleset.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/helpers/ruleset.py index 0561727b0c..15ab56bc9c 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/helpers/ruleset.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/helpers/ruleset.py @@ -296,7 +296,6 @@ def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]: "HIVE_CANCUN_TIMESTAMP": 0, "HIVE_PRAGUE_TIMESTAMP": 0, "HIVE_OSAKA_TIMESTAMP": 0, - **get_blob_schedule_entries(Osaka), }, PragueToOsakaAtTime15k: { "HIVE_FORK_HOMESTEAD": 0, @@ -314,7 +313,6 @@ def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]: "HIVE_CANCUN_TIMESTAMP": 0, "HIVE_PRAGUE_TIMESTAMP": 0, "HIVE_OSAKA_TIMESTAMP": 15000, - **get_blob_schedule_entries(Osaka), }, BPO1: { "HIVE_FORK_HOMESTEAD": 0, @@ -496,11 +494,10 @@ def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]: "HIVE_CANCUN_TIMESTAMP": 0, "HIVE_PRAGUE_TIMESTAMP": 0, "HIVE_OSAKA_TIMESTAMP": 0, - "HIVE_BPO1_TIMESTAMP": 0, - "HIVE_BPO2_TIMESTAMP": 0, - "HIVE_BPO3_TIMESTAMP": 0, - "HIVE_BPO4_TIMESTAMP": 0, + # TODO: While we are still reworking BPO interaction with T8N, + # turn off BPO timestamps for now. + # "HIVE_BPO1_TIMESTAMP": 0, + # "HIVE_BPO2_TIMESTAMP": 0, "HIVE_AMSTERDAM_TIMESTAMP": 0, - **get_blob_schedule_entries(Amsterdam), }, } diff --git a/packages/testing/src/execution_testing/client_clis/clis/erigon.py b/packages/testing/src/execution_testing/client_clis/clis/erigon.py index 3ac873f1d1..9d25027121 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/erigon.py +++ b/packages/testing/src/execution_testing/client_clis/clis/erigon.py @@ -56,6 +56,7 @@ class ErigonExceptionMapper(ExceptionMapper): BlockException.INVALID_LOG_BLOOM: "invalid bloom", } mapping_regex = { + BlockException.INVALID_BLOCK_ACCESS_LIST: r"invalid block access list|block access list mismatch", TransactionException.GAS_LIMIT_EXCEEDS_MAXIMUM: ( r"invalid block, txnIdx=\d+,.*gas limit too high" ), diff --git a/packages/testing/src/execution_testing/fixtures/blockchain.py b/packages/testing/src/execution_testing/fixtures/blockchain.py index d5476034ab..bb8ecd943a 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -199,6 +199,9 @@ class FixtureHeader(CamelModel): requests_hash: ( Annotated[Hash, HeaderForkRequirement("requests")] | None ) = Field(None) + block_access_list_hash: ( + Annotated[Hash, HeaderForkRequirement("bal_hash")] | None + ) = Field(None, alias="blockAccessListHash") fork: Fork | None = Field(None, exclude=True) @@ -283,6 +286,11 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self: "requests_hash": Requests() if fork.header_requests_required(block_number=0, timestamp=0) else None, + "block_access_list_hash": ( + BlockAccessList().rlp_hash + if fork.header_bal_hash_required(block_number=0, timestamp=0) + else None + ), "fork": fork, } return cls(**environment_values, **extras) @@ -408,6 +416,14 @@ def from_fixture_header( "Invalid header for engine_newPayload" ) + if fork.engine_execution_payload_block_access_list( + block_number=header.number, timestamp=header.timestamp + ): + if block_access_list is None: + raise ValueError( + f"`block_access_list` is required in engine `ExecutionPayload` for >={fork}." + ) + execution_payload = FixtureExecutionPayload.from_fixture_header( header=header, transactions=transactions, diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 34979e8c49..7d4e13eb92 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -345,6 +345,14 @@ def header_requests_required( """Return true if the header must contain beacon chain requests.""" pass + @classmethod + @abstractmethod + def header_bal_hash_required( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """Return true if the header must contain block access list hash.""" + pass + # Gas related abstract methods @classmethod @@ -710,6 +718,17 @@ def engine_new_payload_target_blobs_per_block( """ pass + @classmethod + @abstractmethod + def engine_execution_payload_block_access_list( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """ + Return `True` if the engine api version requires execution payload to + include a `block_access_list`. + """ + pass + @classmethod @abstractmethod def engine_payload_attribute_target_blobs_per_block( diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 4a1af669a7..d1b2dcf505 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -443,6 +443,14 @@ def header_requests_required( del block_number, timestamp return False + @classmethod + def header_bal_hash_required( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """At genesis, header must not contain block access list hash.""" + del block_number, timestamp + return False + @classmethod def engine_new_payload_version( cls, *, block_number: int = 0, timestamp: int = 0 @@ -483,6 +491,14 @@ def engine_new_payload_requests( del block_number, timestamp return False + @classmethod + def engine_execution_payload_block_access_list( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """At genesis, payloads do not have block access list.""" + del block_number, timestamp + return False + @classmethod def engine_new_payload_target_blobs_per_block( cls, @@ -2462,6 +2478,16 @@ class BPO5(BPO4, bpo_fork=True): class Amsterdam(Osaka): """Amsterdam fork.""" + @classmethod + def header_bal_hash_required( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """ + From Amsterdam, header must contain block access list hash (EIP-7928). + """ + del block_number, timestamp + return True + @classmethod def is_deployed(cls) -> bool: """Return True if this fork is deployed.""" @@ -2475,6 +2501,17 @@ def engine_new_payload_version( del block_number, timestamp return 5 + @classmethod + def engine_execution_payload_block_access_list( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """ + From Amsterdam, engine execution payload includes `block_access_list` + as a parameter. + """ + del block_number, timestamp + return True + class EOFv1(Prague, solc_name="cancun"): """EOF fork.""" diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 1e1d4b63dc..207b4a953f 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -551,11 +551,16 @@ def make_genesis( state_root = pre_alloc.state_root() genesis = FixtureHeader.genesis(self.fork, env, state_root) + genesis_bal = None + if self.fork.header_bal_hash_required(block_number=0, timestamp=0): + genesis_bal = BlockAccessList() + return ( pre_alloc, FixtureBlockBase( header=genesis, withdrawals=None if env.withdrawals is None else [], + block_access_list=genesis_bal, ).with_rlp(txs=[]), ) @@ -687,6 +692,24 @@ def generate_block_data( ) requests_list = block.requests + if self.fork.header_bal_hash_required( + block_number=header.number, timestamp=header.timestamp + ): + assert ( + transition_tool_output.result.block_access_list is not None + ), ( + "Block access list is required for this block but was not provided " + "by the transition tool" + ) + + rlp = transition_tool_output.result.block_access_list.rlp + computed_bal_hash = Hash(rlp.keccak256()) + assert computed_bal_hash == header.block_access_list_hash, ( + "Block access list hash in header does not match the " + f"computed hash from BAL: {header.block_access_list_hash} " + f"!= {computed_bal_hash}" + ) + if block.rlp_modifier is not None: # Modify any parameter specified in the `rlp_modifier` after # transition tool processing. @@ -695,6 +718,29 @@ def generate_block_data( self.fork ) # Deleted during `apply` because `exclude=True` + # Process block access list - apply transformer if present for invalid + # tests + t8n_bal = transition_tool_output.result.block_access_list + bal = t8n_bal + + # Always validate BAL structural integrity (ordering, duplicates) if present + if t8n_bal is not None: + t8n_bal.validate_structure() + + # If expected BAL is defined, verify against it + if ( + block.expected_block_access_list is not None + and t8n_bal is not None + ): + block.expected_block_access_list.verify_against(t8n_bal) + + bal = block.expected_block_access_list.modify_if_invalid_test( + t8n_bal + ) + if bal != t8n_bal: + # If the BAL was modified, update the header hash + header.block_access_list_hash = Hash(bal.rlp.keccak256()) + built_block = BuiltBlock( header=header, alloc=transition_tool_output.alloc, @@ -708,7 +754,7 @@ def generate_block_data( expected_exception=block.exception, engine_api_error_code=block.engine_api_error_code, fork=self.fork, - block_access_list=None, + block_access_list=bal, ) try: diff --git a/packages/testing/src/execution_testing/test_types/block_access_list/expectations.py b/packages/testing/src/execution_testing/test_types/block_access_list/expectations.py index fad7017c4c..6150dfeabd 100644 --- a/packages/testing/src/execution_testing/test_types/block_access_list/expectations.py +++ b/packages/testing/src/execution_testing/test_types/block_access_list/expectations.py @@ -18,7 +18,6 @@ BalCodeChange, BalNonceChange, BalStorageSlot, - BlockAccessListChangeLists, ) from .exceptions import BlockAccessListValidationError from .t8n import BlockAccessList @@ -179,9 +178,8 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None: Verify that the actual BAL from the client matches this expected BAL. Validation steps: - 1. Validate actual BAL conforms to EIP-7928 ordering requirements - 2. Verify address expectations - presence or explicit absence - 3. Verify expected changes within accounts match actual changes + 1. Verify address expectations - presence or explicit absence + 2. Verify expected changes within accounts match actual changes Args: actual_bal: The BlockAccessList model from the client @@ -190,9 +188,6 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None: BlockAccessListValidationError: If verification fails """ - # validate the actual BAL structure follows EIP-7928 ordering - self._validate_bal_ordering(actual_bal) - actual_accounts_by_addr = {acc.address: acc for acc in actual_bal.root} for address, expectation in self.account_expectations.items(): if expectation is None: @@ -236,111 +231,6 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None: f"Account {address}: {str(e)}" ) from e - @staticmethod - def _validate_bal_ordering(bal: "BlockAccessList") -> None: - """ - Validate BAL ordering follows EIP-7928 requirements. - - Args: - bal: The BlockAccessList to validate - - Raises: - BlockAccessListValidationError: If ordering is invalid - - """ - # Check address ordering (ascending) - for i in range(1, len(bal.root)): - if bal.root[i - 1].address >= bal.root[i].address: - raise BlockAccessListValidationError( - f"BAL addresses are not in lexicographic order: " - f"{bal.root[i - 1].address} >= {bal.root[i].address}" - ) - - # Check transaction index ordering and uniqueness within accounts - for account in bal.root: - changes_to_check: List[tuple[str, BlockAccessListChangeLists]] = [ - ("nonce_changes", account.nonce_changes), - ("balance_changes", account.balance_changes), - ("code_changes", account.code_changes), - ] - - for field_name, change_list in changes_to_check: - if not change_list: - continue - - tx_indices = [c.tx_index for c in change_list] - - # Check both ordering and duplicates - if tx_indices != sorted(tx_indices): - raise BlockAccessListValidationError( - f"Transaction indices not in ascending order in {field_name} of account " - f"{account.address}. Got: {tx_indices}, Expected: {sorted(tx_indices)}" - ) - - if len(tx_indices) != len(set(tx_indices)): - duplicates = sorted( - { - idx - for idx in tx_indices - if tx_indices.count(idx) > 1 - } - ) - raise BlockAccessListValidationError( - f"Duplicate transaction indices in {field_name} of account " - f"{account.address}. Duplicates: {duplicates}" - ) - - # Check storage slot ordering - for i in range(1, len(account.storage_changes)): - if ( - account.storage_changes[i - 1].slot - >= account.storage_changes[i].slot - ): - raise BlockAccessListValidationError( - f"Storage slots not in ascending order in account " - f"{account.address}: {account.storage_changes[i - 1].slot} >= " - f"{account.storage_changes[i].slot}" - ) - - # Check transaction index ordering and uniqueness within storage - # slots - for storage_slot in account.storage_changes: - if not storage_slot.slot_changes: - continue - - tx_indices = [c.tx_index for c in storage_slot.slot_changes] - - # Check both ordering and duplicates - if tx_indices != sorted(tx_indices): - raise BlockAccessListValidationError( - f"Transaction indices not in ascending order in storage slot " - f"{storage_slot.slot} of account {account.address}. " - f"Got: {tx_indices}, Expected: {sorted(tx_indices)}" - ) - - if len(tx_indices) != len(set(tx_indices)): - duplicates = sorted( - { - idx - for idx in tx_indices - if tx_indices.count(idx) > 1 - } - ) - raise BlockAccessListValidationError( - f"Duplicate transaction indices in storage slot " - f"{storage_slot.slot} of account {account.address}. " - f"Duplicates: {duplicates}" - ) - - # Check storage reads ordering - for i in range(1, len(account.storage_reads)): - if account.storage_reads[i - 1] >= account.storage_reads[i]: - raise BlockAccessListValidationError( - f"Storage reads not in ascending order in account " - f"{account.address}: {account.storage_reads[i - 1]} >= " - f"{account.storage_reads[i]}" - ) - @staticmethod def _compare_account_expectations( expected: BalAccountExpectation, actual: BalAccountChange diff --git a/packages/testing/src/execution_testing/test_types/block_access_list/t8n.py b/packages/testing/src/execution_testing/test_types/block_access_list/t8n.py index 9a9ba84508..03b8224bbf 100644 --- a/packages/testing/src/execution_testing/test_types/block_access_list/t8n.py +++ b/packages/testing/src/execution_testing/test_types/block_access_list/t8n.py @@ -12,6 +12,7 @@ ) from .account_changes import BalAccountChange +from .exceptions import BlockAccessListValidationError class BlockAccessList(EthereumTestRootModel[List[BalAccountChange]]): @@ -49,3 +50,108 @@ def rlp(self) -> Bytes: def rlp_hash(self) -> Bytes: """Return the hash of the RLP encoded block access list.""" return self.rlp.keccak256() + + def validate_structure(self) -> None: + """ + Validate BAL structure follows EIP-7928 requirements. + + Checks: + - Addresses are in lexicographic (ascending) order + - Transaction indices are sorted and unique within each change list + - Storage slots are in ascending order + - Storage reads are in ascending order + + Raises: + BlockAccessListValidationError: If validation fails + """ + # Check address ordering (ascending) + for i in range(1, len(self.root)): + if self.root[i - 1].address >= self.root[i].address: + raise BlockAccessListValidationError( + f"BAL addresses are not in lexicographic order: " + f"{self.root[i - 1].address} >= {self.root[i].address}" + ) + + # Check transaction index ordering and uniqueness within accounts + for account in self.root: + changes_to_check: List[tuple[str, List[Any]]] = [ + ("nonce_changes", account.nonce_changes), + ("balance_changes", account.balance_changes), + ("code_changes", account.code_changes), + ] + + for field_name, change_list in changes_to_check: + if not change_list: + continue + + tx_indices = [c.tx_index for c in change_list] + + # Check both ordering and duplicates + if tx_indices != sorted(tx_indices): + raise BlockAccessListValidationError( + f"Transaction indices not in ascending order in {field_name} of account " + f"{account.address}. Got: {tx_indices}, Expected: {sorted(tx_indices)}" + ) + + if len(tx_indices) != len(set(tx_indices)): + duplicates = sorted( + { + idx + for idx in tx_indices + if tx_indices.count(idx) > 1 + } + ) + raise BlockAccessListValidationError( + f"Duplicate transaction indices in {field_name} of account " + f"{account.address}. Duplicates: {duplicates}" + ) + + # Check storage slot ordering + for i in range(1, len(account.storage_changes)): + if ( + account.storage_changes[i - 1].slot + >= account.storage_changes[i].slot + ): + raise BlockAccessListValidationError( + f"Storage slots not in ascending order in account " + f"{account.address}: {account.storage_changes[i - 1].slot} >= " + f"{account.storage_changes[i].slot}" + ) + + # Check transaction index ordering and uniqueness within storage slots + for storage_slot in account.storage_changes: + if not storage_slot.slot_changes: + continue + + tx_indices = [c.tx_index for c in storage_slot.slot_changes] + + # Check both ordering and duplicates + if tx_indices != sorted(tx_indices): + raise BlockAccessListValidationError( + f"Transaction indices not in ascending order in storage slot " + f"{storage_slot.slot} of account {account.address}. " + f"Got: {tx_indices}, Expected: {sorted(tx_indices)}" + ) + + if len(tx_indices) != len(set(tx_indices)): + duplicates = sorted( + { + idx + for idx in tx_indices + if tx_indices.count(idx) > 1 + } + ) + raise BlockAccessListValidationError( + f"Duplicate transaction indices in storage slot " + f"{storage_slot.slot} of account {account.address}. " + f"Duplicates: {duplicates}" + ) + + # Check storage reads ordering + for i in range(1, len(account.storage_reads)): + if account.storage_reads[i - 1] >= account.storage_reads[i]: + raise BlockAccessListValidationError( + f"Storage reads not in ascending order in account " + f"{account.address}: {account.storage_reads[i - 1]} >= " + f"{account.storage_reads[i]}" + ) diff --git a/packages/testing/src/execution_testing/test_types/tests/test_block_access_lists.py b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_expectation.py similarity index 82% rename from packages/testing/src/execution_testing/test_types/tests/test_block_access_lists.py rename to packages/testing/src/execution_testing/test_types/tests/test_block_access_list_expectation.py index 8effee3688..899d9647e4 100644 --- a/packages/testing/src/execution_testing/test_types/tests/test_block_access_lists.py +++ b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_expectation.py @@ -312,231 +312,6 @@ def test_missing_expected_address() -> None: expectation.verify_against(actual_bal) -@pytest.mark.parametrize( - "addresses,error_message", - [ - ( - [ - Address(0xB), - Address(0xA), # should come first - ], - "BAL addresses are not in lexicographic order", - ), - ( - [ - Address(0x1), - Address(0x3), - Address(0x2), - ], - "BAL addresses are not in lexicographic order", - ), - ], -) -def test_actual_bal_address_ordering_validation( - addresses: Any, error_message: str -) -> None: - """Test that actual BAL must have addresses in lexicographic order.""" - # Create BAL with addresses in the given order - actual_bal = BlockAccessList( - [ - BalAccountChange(address=addr, nonce_changes=[]) - for addr in addresses - ] - ) - - expectation = BlockAccessListExpectation(account_expectations={}) - - with pytest.raises(BlockAccessListValidationError, match=error_message): - expectation.verify_against(actual_bal) - - -@pytest.mark.parametrize( - "storage_slots,error_message", - [ - ( - [StorageKey(0x02), StorageKey(0x01)], # 0x02 before 0x01 - "Storage slots not in ascending order", - ), - ( - [StorageKey(0x01), StorageKey(0x03), StorageKey(0x02)], - "Storage slots not in ascending order", - ), - ], -) -def test_actual_bal_storage_slot_ordering( - storage_slots: Any, error_message: str -) -> None: - """Test that actual BAL must have storage slots in lexicographic order.""" - addr = Address(0xA) - - actual_bal = BlockAccessList( - [ - BalAccountChange( - address=addr, - storage_changes=[ - BalStorageSlot(slot=slot, slot_changes=[]) - for slot in storage_slots - ], - ) - ] - ) - - expectation = BlockAccessListExpectation(account_expectations={}) - - with pytest.raises(BlockAccessListValidationError, match=error_message): - expectation.verify_against(actual_bal) - - -@pytest.mark.parametrize( - "storage_reads,error_message", - [ - ( - [StorageKey(0x02), StorageKey(0x01)], - "Storage reads not in ascending order", - ), - ( - [StorageKey(0x01), StorageKey(0x03), StorageKey(0x02)], - "Storage reads not in ascending order", - ), - ], -) -def test_actual_bal_storage_reads_ordering( - storage_reads: Any, error_message: str -) -> None: - """Test that actual BAL must have storage reads in lexicographic order.""" - addr = Address(0xA) - - actual_bal = BlockAccessList( - [BalAccountChange(address=addr, storage_reads=storage_reads)] - ) - - expectation = BlockAccessListExpectation(account_expectations={}) - - with pytest.raises(BlockAccessListValidationError, match=error_message): - expectation.verify_against(actual_bal) - - -@pytest.mark.parametrize( - "field_name", - ["nonce_changes", "balance_changes", "code_changes"], -) -def test_actual_bal_tx_indices_ordering(field_name: str) -> None: - """Test that actual BAL must have tx indices in ascending order.""" - addr = Address(0xA) - - tx_indices = [2, 3, 1] # out of order - - changes: Any = [] - if field_name == "nonce_changes": - changes = [ - BalNonceChange(tx_index=idx, post_nonce=1) for idx in tx_indices - ] - elif field_name == "balance_changes": - changes = [ - BalBalanceChange(tx_index=idx, post_balance=100) - for idx in tx_indices - ] - elif field_name == "code_changes": - changes = [ - BalCodeChange(tx_index=idx, new_code=b"code") for idx in tx_indices - ] - - actual_bal = BlockAccessList( - [BalAccountChange(address=addr, **{field_name: changes})] - ) - - expectation = BlockAccessListExpectation(account_expectations={}) - - with pytest.raises( - BlockAccessListValidationError, - match="Transaction indices not in ascending order", - ): - expectation.verify_against(actual_bal) - - -@pytest.mark.parametrize( - "field_name", - ["nonce_changes", "balance_changes", "code_changes"], -) -def test_actual_bal_duplicate_tx_indices(field_name: str) -> None: - """ - Test that actual BAL must not have duplicate tx indices in change lists. - """ - addr = Address(0xA) - - # Duplicate tx_index=1 - changes: Any = [] - if field_name == "nonce_changes": - changes = [ - BalNonceChange(tx_index=1, post_nonce=1), - BalNonceChange(tx_index=1, post_nonce=2), # duplicate tx_index - BalNonceChange(tx_index=2, post_nonce=3), - ] - elif field_name == "balance_changes": - changes = [ - BalBalanceChange(tx_index=1, post_balance=100), - BalBalanceChange( - tx_index=1, post_balance=200 - ), # duplicate tx_index - BalBalanceChange(tx_index=2, post_balance=300), - ] - elif field_name == "code_changes": - changes = [ - BalCodeChange(tx_index=1, new_code=b"code1"), - BalCodeChange(tx_index=1, new_code=b""), # duplicate tx_index - BalCodeChange(tx_index=2, new_code=b"code2"), - ] - - actual_bal = BlockAccessList( - [BalAccountChange(address=addr, **{field_name: changes})] - ) - - expectation = BlockAccessListExpectation(account_expectations={}) - - with pytest.raises( - BlockAccessListValidationError, - match=f"Duplicate transaction indices in {field_name}.*Duplicates: \\[1\\]", - ): - expectation.verify_against(actual_bal) - - -def test_actual_bal_storage_duplicate_tx_indices() -> None: - """ - Test that storage changes must not have duplicate tx indices within same - slot. - """ - addr = Address(0xA) - - # Create storage changes with duplicate tx_index within the same slot - actual_bal = BlockAccessList( - [ - BalAccountChange( - address=addr, - storage_changes=[ - BalStorageSlot( - slot=0x01, - slot_changes=[ - BalStorageChange(tx_index=1, post_value=0x100), - BalStorageChange( - tx_index=1, post_value=0x200 - ), # duplicate tx_index - BalStorageChange(tx_index=2, post_value=0x300), - ], - ) - ], - ) - ] - ) - - expectation = BlockAccessListExpectation(account_expectations={}) - - with pytest.raises( - BlockAccessListValidationError, - match="Duplicate transaction indices in storage slot.*Duplicates: \\[1\\]", - ): - expectation.verify_against(actual_bal) - - def test_expected_addresses_auto_sorted() -> None: """ Test that expected addresses are automatically sorted before comparison. diff --git a/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_t8n.py b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_t8n.py new file mode 100644 index 0000000000..3c884cf2f4 --- /dev/null +++ b/packages/testing/src/execution_testing/test_types/tests/test_block_access_list_t8n.py @@ -0,0 +1,351 @@ +""" +Tests for BlockAccessList.validate_structure() method. + +These tests verify that the BAL structural validation correctly enforces +EIP-7928 requirements for ordering and uniqueness. +""" + +from typing import List, Union + +import pytest + +from execution_testing.base_types import Address, HexNumber, StorageKey +from execution_testing.test_types.block_access_list import ( + BalAccountChange, + BalBalanceChange, + BalCodeChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessList, + BlockAccessListValidationError, +) + + +def test_bal_address_ordering_validation() -> None: + """Test that BAL addresses must be in lexicographic order.""" + alice = Address(0xAA) + bob = Address(0xBB) + + # Correct order: alice < bob + bal_valid = BlockAccessList( + [ + BalAccountChange(address=alice), + BalAccountChange(address=bob), + ] + ) + bal_valid.validate_structure() # Should not raise + + # Incorrect order: bob before alice + bal_invalid = BlockAccessList( + [ + BalAccountChange(address=bob), + BalAccountChange(address=alice), + ] + ) + + with pytest.raises( + BlockAccessListValidationError, + match="addresses are not in lexicographic order", + ): + bal_invalid.validate_structure() + + +def test_bal_storage_slot_ordering() -> None: + """Test that storage slots must be in ascending order.""" + addr = Address(0xA) + + # Correct order + bal_valid = BlockAccessList( + [ + BalAccountChange( + address=addr, + storage_changes=[ + BalStorageSlot(slot=StorageKey(0), slot_changes=[]), + BalStorageSlot(slot=StorageKey(1), slot_changes=[]), + BalStorageSlot(slot=StorageKey(2), slot_changes=[]), + ], + ) + ] + ) + bal_valid.validate_structure() # Should not raise + + # Incorrect order: slot 2 before slot 1 + bal_invalid = BlockAccessList( + [ + BalAccountChange( + address=addr, + storage_changes=[ + BalStorageSlot(slot=StorageKey(0), slot_changes=[]), + BalStorageSlot(slot=StorageKey(2), slot_changes=[]), + BalStorageSlot(slot=StorageKey(1), slot_changes=[]), + ], + ) + ] + ) + + with pytest.raises( + BlockAccessListValidationError, + match="Storage slots not in ascending order", + ): + bal_invalid.validate_structure() + + +def test_bal_storage_reads_ordering() -> None: + """Test that storage reads must be in ascending order.""" + addr = Address(0xA) + + # Correct order + bal_valid = BlockAccessList( + [ + BalAccountChange( + address=addr, + storage_reads=[StorageKey(0), StorageKey(1), StorageKey(2)], + ) + ] + ) + bal_valid.validate_structure() # Should not raise + + # Incorrect order + bal_invalid = BlockAccessList( + [ + BalAccountChange( + address=addr, + storage_reads=[StorageKey(0), StorageKey(2), StorageKey(1)], + ) + ] + ) + + with pytest.raises( + BlockAccessListValidationError, + match="Storage reads not in ascending order", + ): + bal_invalid.validate_structure() + + +@pytest.mark.parametrize( + "field_name", + ["nonce_changes", "balance_changes", "code_changes"], +) +def test_bal_tx_indices_ordering(field_name: str) -> None: + """ + Test that transaction indices must be in ascending order within change lists. + """ + addr = Address(0xA) + + changes_valid: List[Union[BalNonceChange, BalBalanceChange, BalCodeChange]] + changes_invalid: List[ + Union[BalNonceChange, BalBalanceChange, BalCodeChange] + ] + + # Correct order: tx_index 1, 2, 3 + if field_name == "nonce_changes": + changes_valid = [ + BalNonceChange(tx_index=HexNumber(1), post_nonce=HexNumber(1)), + BalNonceChange(tx_index=HexNumber(2), post_nonce=HexNumber(2)), + BalNonceChange(tx_index=HexNumber(3), post_nonce=HexNumber(3)), + ] + changes_invalid = [ + BalNonceChange(tx_index=HexNumber(1), post_nonce=HexNumber(1)), + BalNonceChange(tx_index=HexNumber(3), post_nonce=HexNumber(3)), + BalNonceChange(tx_index=HexNumber(2), post_nonce=HexNumber(2)), + ] + elif field_name == "balance_changes": + changes_valid = [ + BalBalanceChange( + tx_index=HexNumber(1), post_balance=HexNumber(100) + ), + BalBalanceChange( + tx_index=HexNumber(2), post_balance=HexNumber(200) + ), + BalBalanceChange( + tx_index=HexNumber(3), post_balance=HexNumber(300) + ), + ] + changes_invalid = [ + BalBalanceChange( + tx_index=HexNumber(1), post_balance=HexNumber(100) + ), + BalBalanceChange( + tx_index=HexNumber(3), post_balance=HexNumber(300) + ), + BalBalanceChange( + tx_index=HexNumber(2), post_balance=HexNumber(200) + ), + ] + elif field_name == "code_changes": + changes_valid = [ + BalCodeChange(tx_index=HexNumber(1), new_code=b"code1"), + BalCodeChange(tx_index=HexNumber(2), new_code=b"code2"), + BalCodeChange(tx_index=HexNumber(3), new_code=b"code3"), + ] + changes_invalid = [ + BalCodeChange(tx_index=HexNumber(1), new_code=b"code1"), + BalCodeChange(tx_index=HexNumber(3), new_code=b"code3"), + BalCodeChange(tx_index=HexNumber(2), new_code=b"code2"), + ] + + bal_valid = BlockAccessList( + [BalAccountChange(address=addr, **{field_name: changes_valid})] + ) + bal_valid.validate_structure() # Should not raise + + bal_invalid = BlockAccessList( + [BalAccountChange(address=addr, **{field_name: changes_invalid})] + ) + + with pytest.raises( + BlockAccessListValidationError, + match=f"Transaction indices not in ascending order in {field_name}", + ): + bal_invalid.validate_structure() + + +@pytest.mark.parametrize( + "field_name", + ["nonce_changes", "balance_changes", "code_changes"], +) +def test_bal_duplicate_tx_indices(field_name: str) -> None: + """ + Test that BAL must not have duplicate tx indices in change lists. + """ + addr = Address(0xA) + + changes: List[Union[BalNonceChange, BalBalanceChange, BalCodeChange]] + + # Duplicate tx_index=1 + if field_name == "nonce_changes": + changes = [ + BalNonceChange(tx_index=HexNumber(1), post_nonce=HexNumber(1)), + BalNonceChange( + tx_index=HexNumber(1), post_nonce=HexNumber(2) + ), # duplicate tx_index + BalNonceChange(tx_index=HexNumber(2), post_nonce=HexNumber(3)), + ] + elif field_name == "balance_changes": + changes = [ + BalBalanceChange( + tx_index=HexNumber(1), post_balance=HexNumber(100) + ), + BalBalanceChange( + tx_index=HexNumber(1), post_balance=HexNumber(200) + ), # duplicate tx_index + BalBalanceChange( + tx_index=HexNumber(2), post_balance=HexNumber(300) + ), + ] + elif field_name == "code_changes": + changes = [ + BalCodeChange(tx_index=HexNumber(1), new_code=b"code1"), + BalCodeChange( + tx_index=HexNumber(1), new_code=b"" + ), # duplicate tx_index + BalCodeChange(tx_index=HexNumber(2), new_code=b"code2"), + ] + + bal = BlockAccessList( + [BalAccountChange(address=addr, **{field_name: changes})] + ) + + with pytest.raises( + BlockAccessListValidationError, + match=f"Duplicate transaction indices in {field_name}.*Duplicates: \\[1\\]", + ): + bal.validate_structure() + + +def test_bal_storage_duplicate_tx_indices() -> None: + """ + Test that storage changes must not have duplicate tx indices within same slot. + """ + addr = Address(0xA) + + # Create storage changes with duplicate tx_index within the same slot + bal = BlockAccessList( + [ + BalAccountChange( + address=addr, + storage_changes=[ + BalStorageSlot( + slot=StorageKey(0), + slot_changes=[ + BalStorageChange( + tx_index=HexNumber(1), + post_value=StorageKey(100), + ), + BalStorageChange( + tx_index=HexNumber(1), + post_value=StorageKey(200), + ), # duplicate tx_index + BalStorageChange( + tx_index=HexNumber(2), + post_value=StorageKey(300), + ), + ], + ) + ], + ) + ] + ) + + with pytest.raises( + BlockAccessListValidationError, + match="Duplicate transaction indices in storage slot.*Duplicates: \\[1\\]", + ): + bal.validate_structure() + + +def test_bal_multiple_violations() -> None: + """ + Test that validation catches the first violation when multiple exist. + """ + alice = Address(0xAA) + bob = Address(0xBB) + + # Wrong address order AND duplicate tx indices + bal = BlockAccessList( + [ + BalAccountChange( + address=bob, # Should come after alice + nonce_changes=[ + BalNonceChange( + tx_index=HexNumber(1), post_nonce=HexNumber(1) + ), + BalNonceChange( + tx_index=HexNumber(1), post_nonce=HexNumber(2) + ), # duplicate + ], + ), + BalAccountChange(address=alice), + ] + ) + + # Should catch the first error (address ordering) + with pytest.raises( + BlockAccessListValidationError, + match="addresses are not in lexicographic order", + ): + bal.validate_structure() + + +def test_bal_empty_list_valid() -> None: + """Test that an empty BAL is valid.""" + bal = BlockAccessList([]) + bal.validate_structure() # Should not raise + + +def test_bal_single_account_valid() -> None: + """Test that a BAL with a single account is valid.""" + bal = BlockAccessList( + [ + BalAccountChange( + address=Address(0xA), + nonce_changes=[ + BalNonceChange( + tx_index=HexNumber(1), post_nonce=HexNumber(1) + ) + ], + ) + ] + ) + bal.validate_structure() # Should not raise diff --git a/pyproject.toml b/pyproject.toml index 4bf3928f95..61b5665e9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,6 +142,13 @@ packages = [ "ethereum.forks.osaka.vm.instructions", "ethereum.forks.osaka.vm.precompiled_contracts", "ethereum.forks.osaka.vm.precompiled_contracts.bls12_381", + "ethereum.forks.amsterdam", + "ethereum.forks.amsterdam.block_access_lists", + "ethereum.forks.amsterdam.utils", + "ethereum.forks.amsterdam.vm", + "ethereum.forks.amsterdam.vm.instructions", + "ethereum.forks.amsterdam.vm.precompiled_contracts", + "ethereum.forks.amsterdam.vm.precompiled_contracts.bls12_381", ] [tool.setuptools.package-data] @@ -372,6 +379,15 @@ ignore = [ "src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py" = [ "N815" # The traces must use camel case in JSON property names ] +"src/ethereum/forks/amsterdam/blocks.py" = [ + "E501" # Line too long - needed for long ref links +] + "src/ethereum/forks/amsterdam/block_access_lists/builder.py" = [ + "E501" # Line too long - needed for long ref links + ] +"src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py" = [ + "E501" # Line too long - needed for long ref links + ] "tests/*" = ["ARG001"] "vulture_whitelist.py" = [ "B018", # Useless expression (intentional for Vulture whitelisting) @@ -379,6 +395,10 @@ ignore = [ "F405", # Undefined names from star imports ] +[tool.ruff.lint.mccabe] +# Set the maximum allowed cyclomatic complexity. C901 default is 10. +max-complexity = 7 + [tool.codespell] builtin = "clear,code,usage" # Built-in dictionaries to use skip = [ # Don't check these files/folders diff --git a/src/ethereum/forks/amsterdam/__init__.py b/src/ethereum/forks/amsterdam/__init__.py new file mode 100644 index 0000000000..1e653dbb9d --- /dev/null +++ b/src/ethereum/forks/amsterdam/__init__.py @@ -0,0 +1,15 @@ +""" +The Amsterdam fork ([EIP-7773]) includes block-level access lists. + +### Changes + +- [EIP-7928: Block-Level Access Lists][EIP-7928] + +### Releases + +[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 +""" # noqa: E501 + +from ethereum.fork_criteria import ForkCriteria, Unscheduled + +FORK_CRITERIA: ForkCriteria = Unscheduled(order_index=0) diff --git a/src/ethereum/forks/amsterdam/block_access_lists/__init__.py b/src/ethereum/forks/amsterdam/block_access_lists/__init__.py new file mode 100644 index 0000000000..a83523861a --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/__init__.py @@ -0,0 +1,33 @@ +""" +Block Access Lists (EIP-7928) implementation for Ethereum Amsterdam fork. +""" + +from .builder import ( + BlockAccessListBuilder, + add_balance_change, + add_code_change, + add_nonce_change, + add_storage_read, + add_storage_write, + add_touched_account, + build_block_access_list, +) +from .rlp_utils import ( + compute_block_access_list_hash, + rlp_encode_block_access_list, + validate_block_access_list_against_execution, +) + +__all__ = [ + "BlockAccessListBuilder", + "add_balance_change", + "add_code_change", + "add_nonce_change", + "add_storage_read", + "add_storage_write", + "add_touched_account", + "build_block_access_list", + "compute_block_access_list_hash", + "rlp_encode_block_access_list", + "validate_block_access_list_against_execution", +] diff --git a/src/ethereum/forks/amsterdam/block_access_lists/builder.py b/src/ethereum/forks/amsterdam/block_access_lists/builder.py new file mode 100644 index 0000000000..e860c84068 --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/builder.py @@ -0,0 +1,520 @@ +""" +Implements the Block Access List builder that tracks all account +and storage accesses during block execution and constructs the final +[`BlockAccessList`]. + +The builder follows a two-phase approach: + +1. **Collection Phase**: During transaction execution, all state accesses are + recorded via the tracking functions. +2. **Build Phase**: After block execution, the accumulated data is sorted + and encoded into the final deterministic format. + +[`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 +""" + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Dict, List, Set + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64, U256 + +from ..fork_types import Address +from .rlp_types import ( + AccountChanges, + BalanceChange, + BlockAccessIndex, + BlockAccessList, + CodeChange, + NonceChange, + SlotChanges, + StorageChange, +) + +if TYPE_CHECKING: + from ..state_tracker import StateChanges + + +@dataclass +class AccountData: + """ + Account data stored in the builder during block execution. + + This dataclass tracks all changes made to a single account throughout + the execution of a block, organized by the type of change and the + transaction index where it occurred. + """ + + storage_changes: Dict[Bytes32, List[StorageChange]] = field( + default_factory=dict + ) + """ + Mapping from storage slot to list of changes made to that slot. + Each change includes the transaction index and new value. + """ + + storage_reads: Set[Bytes32] = field(default_factory=set) + """ + Set of storage slots that were read but not modified. + """ + + balance_changes: List[BalanceChange] = field(default_factory=list) + """ + List of balance changes for this account, ordered by transaction index. + """ + + nonce_changes: List[NonceChange] = field(default_factory=list) + """ + List of nonce changes for this account, ordered by transaction index. + """ + + code_changes: List[CodeChange] = field(default_factory=list) + """ + List of code changes (contract deployments) for this account, + ordered by transaction index. + """ + + +@dataclass +class BlockAccessListBuilder: + """ + Builder for constructing [`BlockAccessList`] efficiently during transaction + execution. + + The builder accumulates all account and storage accesses during block + execution and constructs a deterministic access list. Changes are tracked + by address, field type, and transaction index to enable efficient + reconstruction of state changes. + + [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 + """ + + accounts: Dict[Address, AccountData] = field(default_factory=dict) + """ + Mapping from account address to its tracked changes during block execution. + """ + + +def ensure_account(builder: BlockAccessListBuilder, address: Address) -> None: + """ + Ensure an account exists in the builder's tracking structure. + + Creates an empty [`AccountData`] entry for the given address if it + doesn't already exist. This function is idempotent and safe to call + multiple times for the same address. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address to ensure exists. + + [`AccountData`] : + ref:ethereum.forks.amsterdam.block_access_lists.builder.AccountData + + """ + if address not in builder.accounts: + builder.accounts[address] = AccountData() + + +def add_storage_write( + builder: BlockAccessListBuilder, + address: Address, + slot: Bytes32, + block_access_index: BlockAccessIndex, + new_value: Bytes32, +) -> None: + """ + Add a storage write operation to the block access list. + + Records a storage slot modification for a given address at a specific + transaction index. If multiple writes occur to the same slot within the + same transaction (same block_access_index), only the final value is kept. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address whose storage is being modified. + slot : + The storage slot being written to. + block_access_index : + The block access index for this change (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). + new_value : + The new value being written to the storage slot. + + """ + ensure_account(builder, address) + + if slot not in builder.accounts[address].storage_changes: + builder.accounts[address].storage_changes[slot] = [] + + # Check if there's already an entry with the same block_access_index + # If so, update it with the new value, keeping only the final write + changes = builder.accounts[address].storage_changes[slot] + for i, existing_change in enumerate(changes): + if existing_change.block_access_index == block_access_index: + # Update the existing entry with the new value + changes[i] = StorageChange( + block_access_index=block_access_index, new_value=new_value + ) + return + + # No existing entry found, append new change + change = StorageChange( + block_access_index=block_access_index, new_value=new_value + ) + builder.accounts[address].storage_changes[slot].append(change) + + +def add_storage_read( + builder: BlockAccessListBuilder, address: Address, slot: Bytes32 +) -> None: + """ + Add a storage read operation to the block access list. + + Records that a storage slot was read during execution. Storage slots + that are both read and written will only appear in the storage changes + list, not in the storage reads list, as per [EIP-7928]. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address whose storage is being read. + slot : + The storage slot being read. + + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + + """ + ensure_account(builder, address) + builder.accounts[address].storage_reads.add(slot) + + +def add_balance_change( + builder: BlockAccessListBuilder, + address: Address, + block_access_index: BlockAccessIndex, + post_balance: U256, +) -> None: + """ + Add a balance change to the block access list. + + Records the post-transaction balance for an account after it has been + modified. This includes changes from transfers, gas fees, block rewards, + and any other balance-affecting operations. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address whose balance changed. + block_access_index : + The block access index for this change (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). + post_balance : + The account balance after the change as U256. + + """ + ensure_account(builder, address) + + # Balance value is already U256 + balance_value = post_balance + + # Check if we already have a balance change for this tx_index and update it + # This ensures we only track the final balance per transaction + existing_changes = builder.accounts[address].balance_changes + for i, existing in enumerate(existing_changes): + if existing.block_access_index == block_access_index: + # Update the existing balance change with the new balance + existing_changes[i] = BalanceChange( + block_access_index=block_access_index, + post_balance=balance_value, + ) + return + + # No existing change for this tx_index, add a new one + change = BalanceChange( + block_access_index=block_access_index, post_balance=balance_value + ) + builder.accounts[address].balance_changes.append(change) + + +def add_nonce_change( + builder: BlockAccessListBuilder, + address: Address, + block_access_index: BlockAccessIndex, + new_nonce: U64, +) -> None: + """ + Add a nonce change to the block access list. + + Records a nonce increment for an account. This occurs when an EOA sends + a transaction or when a contract performs [`CREATE`] or [`CREATE2`] + operations. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address whose nonce changed. + block_access_index : + The block access index for this change (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). + new_nonce : + The new nonce value after the change. + + [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 + + """ + ensure_account(builder, address) + + # Check if we already have a nonce change for this tx_index and update it + # This ensures we only track the final (highest) nonce per transaction + existing_changes = builder.accounts[address].nonce_changes + for i, existing in enumerate(existing_changes): + if existing.block_access_index == block_access_index: + # Keep the highest nonce value + if new_nonce > existing.new_nonce: + existing_changes[i] = NonceChange( + block_access_index=block_access_index, new_nonce=new_nonce + ) + return + + # No existing change for this tx_index, add a new one + change = NonceChange( + block_access_index=block_access_index, new_nonce=new_nonce + ) + builder.accounts[address].nonce_changes.append(change) + + +def add_code_change( + builder: BlockAccessListBuilder, + address: Address, + block_access_index: BlockAccessIndex, + new_code: Bytes, +) -> None: + """ + Add a code change to the block access list. + + Records contract code deployment or modification. This typically occurs + during contract creation via [`CREATE`], [`CREATE2`], or [`SETCODE`] + operations. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address receiving new code. + block_access_index : + The block access index for this change (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). + new_code : + The deployed contract bytecode. + + [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 + + """ + ensure_account(builder, address) + + # Check if we already have a code change for this block_access_index + # This handles the case of in-transaction selfdestructs where code is + # first deployed and then cleared in the same transaction + existing_changes = builder.accounts[address].code_changes + for i, existing in enumerate(existing_changes): + if existing.block_access_index == block_access_index: + # Replace the existing code change with the new one + # For selfdestructs, this ensures we only record the final state (empty code) + existing_changes[i] = CodeChange( + block_access_index=block_access_index, new_code=new_code + ) + return + + # No existing change for this block_access_index, add a new one + change = CodeChange( + block_access_index=block_access_index, new_code=new_code + ) + builder.accounts[address].code_changes.append(change) + + +def add_touched_account( + builder: BlockAccessListBuilder, address: Address +) -> None: + """ + Add an account that was accessed but not modified. + + Records that an account was accessed during execution without any state + changes. This is used for operations like [`EXTCODEHASH`], [`BALANCE`], + [`EXTCODESIZE`], and [`EXTCODECOPY`] that read account data without + modifying it. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address that was accessed. + + [`EXTCODEHASH`] : + ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodehash + [`BALANCE`] : + ref:ethereum.forks.amsterdam.vm.instructions.environment.balance + [`EXTCODESIZE`] : + ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodesize + [`EXTCODECOPY`] : + ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodecopy + + """ + ensure_account(builder, address) + + +def _build_from_builder( + builder: BlockAccessListBuilder, +) -> BlockAccessList: + """ + Build the final [`BlockAccessList`] from a builder (internal helper). + + Constructs a deterministic block access list by sorting all accumulated + changes. The resulting list is ordered by: + + 1. Account addresses (lexicographically) + 2. Within each account: + - Storage slots (lexicographically) + - Transaction indices (numerically) for each change type + + Parameters + ---------- + builder : + The block access list builder containing all tracked changes. + + Returns + ------- + block_access_list : + The final sorted and encoded block access list. + + [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 + + """ + block_access_list: BlockAccessList = [] + + for address, changes in builder.accounts.items(): + storage_changes = [] + for slot, slot_changes in changes.storage_changes.items(): + sorted_changes = tuple( + sorted(slot_changes, key=lambda x: x.block_access_index) + ) + storage_changes.append( + SlotChanges(slot=slot, changes=sorted_changes) + ) + + storage_reads = [] + for slot in changes.storage_reads: + if slot not in changes.storage_changes: + storage_reads.append(slot) + + balance_changes = tuple( + sorted(changes.balance_changes, key=lambda x: x.block_access_index) + ) + nonce_changes = tuple( + sorted(changes.nonce_changes, key=lambda x: x.block_access_index) + ) + code_changes = tuple( + sorted(changes.code_changes, key=lambda x: x.block_access_index) + ) + + storage_changes.sort(key=lambda x: x.slot) + storage_reads.sort() + + account_change = AccountChanges( + address=address, + storage_changes=tuple(storage_changes), + storage_reads=tuple(storage_reads), + balance_changes=balance_changes, + nonce_changes=nonce_changes, + code_changes=code_changes, + ) + + block_access_list.append(account_change) + + block_access_list.sort(key=lambda x: x.address) + + return block_access_list + + +def build_block_access_list( + state_changes: "StateChanges", +) -> BlockAccessList: + """ + Build a [`BlockAccessList`] from a StateChanges frame. + + Converts the accumulated state changes from the frame-based architecture + into the final deterministic BlockAccessList format. + + Parameters + ---------- + state_changes : + The block-level StateChanges frame containing all changes from the block. + + Returns + ------- + block_access_list : + The final sorted and encoded block access list. + + [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 + [`StateChanges`]: ref:ethereum.forks.amsterdam.state_tracker.StateChanges + + """ + builder = BlockAccessListBuilder() + + # Add all touched addresses + for address in state_changes.touched_addresses: + add_touched_account(builder, address) + + # Add all storage reads + for address, slot in state_changes.storage_reads: + add_storage_read(builder, address, slot) + + # Add all storage writes + # Net-zero filtering happens at transaction commit time, not here. + # At block level, we track ALL writes at their respective indices. + for ( + address, + slot, + block_access_index, + ), value in state_changes.storage_writes.items(): + # Convert U256 to Bytes32 for storage + value_bytes = Bytes32(value.to_bytes(U256(32), "big")) + add_storage_write( + builder, address, slot, block_access_index, value_bytes + ) + + # Add all balance changes (balance_changes is keyed by (address, index)) + for ( + address, + block_access_index, + ), new_balance in state_changes.balance_changes.items(): + add_balance_change(builder, address, block_access_index, new_balance) + + # Add all nonce changes + for address, block_access_index, new_nonce in state_changes.nonce_changes: + add_nonce_change(builder, address, block_access_index, new_nonce) + + # Add all code changes + # Filtering happens at transaction level in eoa_delegation.py + for ( + address, + block_access_index, + ), new_code in state_changes.code_changes.items(): + add_code_change(builder, address, block_access_index, new_code) + + return _build_from_builder(builder) diff --git a/src/ethereum/forks/amsterdam/block_access_lists/rlp_types.py b/src/ethereum/forks/amsterdam/block_access_lists/rlp_types.py new file mode 100644 index 0000000000..c4f49ff4aa --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/rlp_types.py @@ -0,0 +1,121 @@ +""" +Defines the RLP data structures for Block-Level Access Lists +as specified in EIP-7928. These structures enable efficient encoding and +decoding of all accounts and storage locations accessed during block execution. + +The encoding follows the pattern: +address -> field -> block_access_index -> change. +""" + +from dataclasses import dataclass +from typing import List, Tuple + +from ethereum_types.bytes import Bytes, Bytes20, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint + +# Type aliases for clarity (matching EIP-7928 specification) +Address = Bytes20 +StorageKey = Bytes32 +StorageValue = Bytes32 +CodeData = Bytes +BlockAccessIndex = Uint # uint16 in the spec, but using Uint for compatibility +Balance = U256 # Post-transaction balance in wei +Nonce = U64 + +# Constants chosen to support a 630m block gas limit +MAX_TXS = 30_000 +# MAX_SLOTS = 300_000 +# MAX_ACCOUNTS = 300_000 +MAX_CODE_SIZE = 24_576 +MAX_CODE_CHANGES = 1 + + +@slotted_freezable +@dataclass +class StorageChange: + """ + Storage change: [block_access_index, new_value]. + RLP encoded as a list. + """ + + block_access_index: BlockAccessIndex + new_value: StorageValue + + +@slotted_freezable +@dataclass +class BalanceChange: + """ + Balance change: [block_access_index, post_balance]. + RLP encoded as a list. + """ + + block_access_index: BlockAccessIndex + post_balance: Balance + + +@slotted_freezable +@dataclass +class NonceChange: + """ + Nonce change: [block_access_index, new_nonce]. + RLP encoded as a list. + """ + + block_access_index: BlockAccessIndex + new_nonce: Nonce + + +@slotted_freezable +@dataclass +class CodeChange: + """ + Code change: [block_access_index, new_code]. + RLP encoded as a list. + """ + + block_access_index: BlockAccessIndex + new_code: CodeData + + +@slotted_freezable +@dataclass +class SlotChanges: + """ + All changes to a single storage slot: [slot, [changes]]. + RLP encoded as a list. + """ + + slot: StorageKey + changes: Tuple[StorageChange, ...] + + +@slotted_freezable +@dataclass +class AccountChanges: + """ + All changes for a single account, grouped by field type. + RLP encoded as: [address, storage_changes, storage_reads, + balance_changes, nonce_changes, code_changes]. + """ + + address: Address + + # slot -> [block_access_index -> new_value] + storage_changes: Tuple[SlotChanges, ...] + + # read-only storage keys + storage_reads: Tuple[StorageKey, ...] + + # [block_access_index -> post_balance] + balance_changes: Tuple[BalanceChange, ...] + + # [block_access_index -> new_nonce] + nonce_changes: Tuple[NonceChange, ...] + + # [block_access_index -> new_code] + code_changes: Tuple[CodeChange, ...] + + +BlockAccessList = List[AccountChanges] diff --git a/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py new file mode 100644 index 0000000000..2cd5b827f3 --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py @@ -0,0 +1,230 @@ +""" +Utilities for working with Block Access Lists using RLP encoding, +as specified in EIP-7928. + +This module provides: + +- RLP encoding functions for all Block Access List types +- Hash computation using [`keccak256`] +- Validation logic to ensure structural correctness + +The encoding follows the RLP specification used throughout Ethereum. + +[`keccak256`]: ref:ethereum.crypto.hash.keccak256 +""" + +from typing import cast + +from ethereum_rlp import Extended, rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import Hash32, keccak256 + +from .builder import BlockAccessListBuilder +from .rlp_types import MAX_CODE_SIZE, MAX_TXS, BlockAccessList + + +def compute_block_access_list_hash( + block_access_list: BlockAccessList, +) -> Hash32: + """ + Compute the hash of a Block Access List. + + The Block Access List is RLP-encoded and then hashed with keccak256. + + Parameters + ---------- + block_access_list : + The Block Access List to hash. + + Returns + ------- + hash : + The keccak256 hash of the RLP-encoded Block Access List. + + """ + block_access_list_bytes = rlp_encode_block_access_list(block_access_list) + return keccak256(block_access_list_bytes) + + +def rlp_encode_block_access_list(block_access_list: BlockAccessList) -> Bytes: + """ + Encode a [`BlockAccessList`] to RLP bytes. + + This is the top-level encoding function that produces the final RLP + representation of a block's access list, following the updated EIP-7928 + specification. + + Parameters + ---------- + block_access_list : + The block access list to encode. + + Returns + ------- + encoded : + The complete RLP-encoded block access list. + + [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 + + """ + # Encode as a list of AccountChanges directly (not wrapped) + account_changes_list = [] + for account in block_access_list: + # Each account is encoded as: + # [address, storage_changes, storage_reads, + # balance_changes, nonce_changes, code_changes] + storage_changes_list = [ + [ + slot_changes.slot, + [ + [Uint(c.block_access_index), c.new_value] + for c in slot_changes.changes + ], + ] + for slot_changes in account.storage_changes + ] + + storage_reads_list = list(account.storage_reads) + + balance_changes_list = [ + [Uint(bc.block_access_index), Uint(bc.post_balance)] + for bc in account.balance_changes + ] + + nonce_changes_list = [ + [Uint(nc.block_access_index), Uint(nc.new_nonce)] + for nc in account.nonce_changes + ] + + code_changes_list = [ + [Uint(cc.block_access_index), cc.new_code] + for cc in account.code_changes + ] + + account_changes_list.append( + [ + account.address, + storage_changes_list, + storage_reads_list, + balance_changes_list, + nonce_changes_list, + code_changes_list, + ] + ) + + encoded = rlp.encode(cast(Extended, account_changes_list)) + return Bytes(encoded) + + +def validate_block_access_list_against_execution( + block_access_list: BlockAccessList, + block_access_list_builder: BlockAccessListBuilder | None = None, +) -> bool: + """ + Validate that a Block Access List is structurally correct and + optionally matches a builder's state. + + Parameters + ---------- + block_access_list : + The Block Access List to validate. + block_access_list_builder : + Optional Block Access List builder to validate against. + If provided, checks that the + Block Access List hash matches what would be built from + the builder's current state. + + Returns + ------- + valid : + True if the Block Access List is structurally valid and + matches the builder (if provided). + + """ + # 1. Validate structural constraints + + # Check that storage changes and reads don't overlap for the same slot + for account in block_access_list: + changed_slots = {sc.slot for sc in account.storage_changes} + read_slots = set(account.storage_reads) + + # A slot should not be in both changes and reads (per EIP-7928) + if changed_slots & read_slots: + return False + + # 2. Validate ordering (addresses should be sorted lexicographically) + addresses = [account.address for account in block_access_list] + if addresses != sorted(addresses): + return False + + # 3. Validate all data is within bounds + max_block_access_index = ( + MAX_TXS + 1 + ) # 0 for pre-exec, 1..MAX_TXS for txs, MAX_TXS+1 for post-exec + for account in block_access_list: + # Validate storage slots are sorted within each account + storage_slots = [sc.slot for sc in account.storage_changes] + if storage_slots != sorted(storage_slots): + return False + + # Check storage changes + for slot_changes in account.storage_changes: + # Check changes are sorted by block_access_index + indices = [c.block_access_index for c in slot_changes.changes] + if indices != sorted(indices): + return False + + for change in slot_changes.changes: + if int(change.block_access_index) > max_block_access_index: + return False + + # Check balance changes are sorted by block_access_index + balance_indices = [ + bc.block_access_index for bc in account.balance_changes + ] + if balance_indices != sorted(balance_indices): + return False + + for balance_change in account.balance_changes: + if int(balance_change.block_access_index) > max_block_access_index: + return False + + # Check nonce changes are sorted by block_access_index + nonce_indices = [nc.block_access_index for nc in account.nonce_changes] + if nonce_indices != sorted(nonce_indices): + return False + + for nonce_change in account.nonce_changes: + if int(nonce_change.block_access_index) > max_block_access_index: + return False + + # Check code changes are sorted by block_access_index + code_indices = [cc.block_access_index for cc in account.code_changes] + if code_indices != sorted(code_indices): + return False + + for code_change in account.code_changes: + if int(code_change.block_access_index) > max_block_access_index: + return False + if len(code_change.new_code) > MAX_CODE_SIZE: + return False + + # 4. If Block Access List builder provided, validate against it + # by comparing hashes + if block_access_list_builder is not None: + from .builder import _build_from_builder + + # Build a Block Access List from the builder + expected_block_access_list = _build_from_builder( + block_access_list_builder + ) + + # Compare hashes + if compute_block_access_list_hash( + block_access_list + ) != compute_block_access_list_hash(expected_block_access_list): + return False + + return True diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py new file mode 100644 index 0000000000..0d14066f47 --- /dev/null +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -0,0 +1,423 @@ +""" +A `Block` is a single link in the chain that is Ethereum. Each `Block` contains +a `Header` and zero or more transactions. Each `Header` contains associated +metadata like the block number, parent block hash, and how much gas was +consumed by its transactions. + +Together, these blocks form a cryptographically secure journal recording the +history of all state transitions that have happened since the genesis of the +chain. +""" + +from dataclasses import dataclass +from typing import Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes8, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32 + +from .block_access_lists.rlp_types import BlockAccessList +from .fork_types import Address, Bloom, Root +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, +) + + +@slotted_freezable +@dataclass +class Withdrawal: + """ + Withdrawals represent a transfer of ETH from the consensus layer (beacon + chain) to the execution layer, as validated by the consensus layer. Each + withdrawal is listed in the block's list of withdrawals. See [`block`]. + + [`block`]: ref:ethereum.forks.amsterdam.blocks.Block.withdrawals + """ + + index: U64 + """ + The unique index of the withdrawal, incremented for each withdrawal + processed. + """ + + validator_index: U64 + """ + The index of the validator on the consensus layer that is withdrawing. + """ + + address: Address + """ + The execution-layer address receiving the withdrawn ETH. + """ + + amount: U256 + """ + The amount of ETH being withdrawn. + """ + + +@slotted_freezable +@dataclass +class Header: + """ + Header portion of a block on the chain, containing metadata and + cryptographic commitments to the block's contents. + """ + + parent_hash: Hash32 + """ + Hash ([`keccak256`]) of the parent block's header, encoded with [RLP]. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [RLP]: https://ethereum.github.io/ethereum-rlp/src/ethereum_rlp/rlp.py.html + """ + + ommers_hash: Hash32 + """ + Hash ([`keccak256`]) of the ommers (uncle blocks) in this block, encoded + with [RLP]. However, in post merge forks `ommers_hash` is always + [`EMPTY_OMMER_HASH`]. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [RLP]: https://ethereum.github.io/ethereum-rlp/src/ethereum_rlp/rlp.py.html + [`EMPTY_OMMER_HASH`]: ref:ethereum.forks.amsterdam.fork.EMPTY_OMMER_HASH + """ + + coinbase: Address + """ + Address of the miner (or validator) who mined this block. + + The coinbase address receives the block reward and the priority fees (tips) + from included transactions. Base fees (introduced in [EIP-1559]) are burned + and do not go to the coinbase. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + state_root: Root + """ + Root hash ([`keccak256`]) of the state trie after executing all + transactions in this block. It represents the state of the Ethereum Virtual + Machine (EVM) after all transactions in this block have been processed. It + is computed using the [`state_root()`] function, which computes the root + of the Merkle-Patricia [Trie] representing the Ethereum world state. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [`state_root()`]: ref:ethereum.forks.amsterdam.state.state_root + [Trie]: ref:ethereum.forks.amsterdam.trie.Trie + """ + + transactions_root: Root + """ + Root hash ([`keccak256`]) of the transactions trie, which contains all + transactions included in this block in their original order. It is computed + using the [`root()`] function over the Merkle-Patricia [trie] of + transactions as the parameter. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [`root()`]: ref:ethereum.forks.amsterdam.trie.root + [Trie]: ref:ethereum.forks.amsterdam.trie.Trie + """ + + receipt_root: Root + """ + Root hash ([`keccak256`]) of the receipts trie, which contains all receipts + for transactions in this block. It is computed using the [`root()`] + function over the Merkle-Patricia [trie] constructed from the receipts. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [`root()`]: ref:ethereum.forks.amsterdam.trie.root + [Trie]: ref:ethereum.forks.amsterdam.trie.Trie + """ + + bloom: Bloom + """ + Bloom filter for logs generated by transactions in this block. + Constructed from all logs in the block using the [logs bloom] mechanism. + + [logs bloom]: ref:ethereum.forks.amsterdam.bloom.logs_bloom + """ + + difficulty: Uint + """ + Difficulty of the block (pre-PoS), or a constant in PoS. + """ + + number: Uint + """ + Block number, (height) in the chain. + """ + + gas_limit: Uint + """ + Maximum gas allowed in this block. Pre [EIP-1559], this was the maximum + gas that could be consumed by all transactions in the block. Post + [EIP-1559], this is still the maximum gas limit, but the base fee per gas + is also considered when calculating the effective gas limit. This can be + [adjusted by a factor of 1/1024] from the previous block's gas limit, up + until a maximum of 30 million gas. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + [adjusted by a factor of 1/1024]: + https://ethereum.org/en/developers/docs/blocks/ + """ + + gas_used: Uint + """ + Total gas used by all transactions in this block. + """ + + timestamp: U256 + """ + Timestamp of when the block was mined, in seconds since the unix epoch. + """ + + extra_data: Bytes + """ + Arbitrary data included by the miner. + """ + + prev_randao: Bytes32 + """ + Output of the RANDAO beacon for random validator selection. + """ + + nonce: Bytes8 + """ + Nonce used in the mining process (pre-PoS), set to zero in PoS. + """ + + base_fee_per_gas: Uint + """ + Base fee per gas for transactions in this block, introduced in + [EIP-1559]. This is the minimum fee per gas that must be paid for a + transaction to be included in this block. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + withdrawals_root: Root + """ + Root hash of the withdrawals trie, which contains all withdrawals in this + block. + """ + + blob_gas_used: U64 + """ + Total blob gas consumed by the transactions within this block. Introduced + in [EIP-4844]. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + excess_blob_gas: U64 + """ + Running total of blob gas consumed in excess of the target, prior to this + block. Blocks with above-target blob gas consumption increase this value, + while blocks with below-target blob gas consumption decrease it (to a + minimum of zero). Introduced in [EIP-4844]. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + parent_beacon_block_root: Root + """ + Root hash of the corresponding beacon chain block. + """ + + requests_hash: Hash32 + """ + [SHA2-256] hash of all the collected requests in this block. Introduced in + [EIP-7685]. See [`compute_requests_hash`][crh] for more details. + + [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 + [crh]: ref:ethereum.forks.amsterdam.requests.compute_requests_hash + [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 + """ + + block_access_list_hash: Hash32 + """ + [SHA2-256] hash of the Block Access List containing all accounts and + storage locations accessed during block execution. Introduced in + [EIP-7928]. See [`compute_block_access_list_hash`][cbalh] for more + details. + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + [cbalh]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_utils.compute_block_access_list_hash # noqa: E501 + """ + + +@slotted_freezable +@dataclass +class Block: + """ + A complete block on Ethereum, which is composed of a block [`header`], + a list of transactions, a list of ommers (deprecated), and a list of + validator [withdrawals]. + + The block [`header`] includes fields relevant to the Proof-of-Stake + consensus, with deprecated Proof-of-Work fields such as `difficulty`, + `nonce`, and `ommersHash` set to constants. The `coinbase` field + denotes the address receiving priority fees from the block. + + The header also contains commitments to the current state (`stateRoot`), + the transactions (`transactionsRoot`), the transaction receipts + (`receiptsRoot`), and `withdrawalsRoot` committing to the validator + withdrawals included in this block. It also includes a bloom filter which + summarizes log data from the transactions. + + Withdrawals represent ETH transfers from validators to their recipients, + introduced by the consensus layer. Ommers remain deprecated and empty. + + [`header`]: ref:ethereum.forks.amsterdam.blocks.Header + [withdrawals]: ref:ethereum.forks.amsterdam.blocks.Withdrawal + """ + + header: Header + """ + The block header containing metadata and cryptographic commitments. Refer + [headers] for more details on the fields included in the header. + + [headers]: ref:ethereum.forks.amsterdam.blocks.Header + """ + + transactions: Tuple[Bytes | LegacyTransaction, ...] + """ + A tuple of transactions included in this block. Each transaction can be + any of a legacy transaction, an access list transaction, a fee market + transaction, a blob transaction, or a set code transaction. + """ + + ommers: Tuple[Header, ...] + """ + A tuple of ommers (uncle blocks) included in this block. Always empty in + Proof-of-Stake forks. + """ + + withdrawals: Tuple[Withdrawal, ...] + """ + A tuple of withdrawals processed in this block. + """ + + block_access_list: BlockAccessList + """ + Block Access List containing all accounts and storage locations accessed + during block execution. Introduced in [EIP-7928]. + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + """ + + +@slotted_freezable +@dataclass +class Log: + """ + Data record produced during the execution of a transaction. Logs are used + by smart contracts to emit events (using the EVM log opcodes ([`LOG0`], + [`LOG1`], [`LOG2`], [`LOG3`] and [`LOG4`]), which can be efficiently + searched using the bloom filter in the block header. + + [`LOG0`]: ref:ethereum.forks.amsterdam.vm.instructions.log.log0 + [`LOG1`]: ref:ethereum.forks.amsterdam.vm.instructions.log.log1 + [`LOG2`]: ref:ethereum.forks.amsterdam.vm.instructions.log.log2 + [`LOG3`]: ref:ethereum.forks.amsterdam.vm.instructions.log.log3 + [`LOG4`]: ref:ethereum.forks.amsterdam.vm.instructions.log.log4 + """ + + address: Address + """ + The address of the contract that emitted the log. + """ + + topics: Tuple[Hash32, ...] + """ + A tuple of up to four topics associated with the log, used for filtering. + """ + + data: Bytes + """ + The data payload of the log, which can contain any arbitrary data. + """ + + +@slotted_freezable +@dataclass +class Receipt: + """ + Result of a transaction execution. Receipts are included in the receipts + trie. + """ + + succeeded: bool + """ + Whether the transaction execution was successful. + """ + + cumulative_gas_used: Uint + """ + Total gas used in the block up to and including this transaction. + """ + + bloom: Bloom + """ + Bloom filter for logs generated by this transaction. This is a 2048-byte + bit array that allows for efficient filtering of logs. + """ + + logs: Tuple[Log, ...] + """ + A tuple of logs generated by this transaction. Each log contains the + address of the contract that emitted it, a tuple of topics, and the data + payload. + """ + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Bytes | Receipt: + r""" + Encodes a transaction receipt based on the transaction type. + + The encoding follows the same format as transactions encoding, where: + - AccessListTransaction receipts are prefixed with `b"\x01"`. + - FeeMarketTransaction receipts are prefixed with `b"\x02"`. + - BlobTransaction receipts are prefixed with `b"\x03"`. + - SetCodeTransaction receipts are prefixed with `b"\x04"`. + - LegacyTransaction receipts are returned as is. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(receipt) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Bytes | Receipt) -> Receipt: + r""" + Decodes a receipt from its serialized form. + + The decoding follows the same format as transactions decoding, where: + - Receipts prefixed with `b"\x01"` are decoded as AccessListTransaction + receipts. + - Receipts prefixed with `b"\x02"` are decoded as FeeMarketTransaction + receipts. + - Receipts prefixed with `b"\x03"` are decoded as BlobTransaction + receipts. + - Receipts prefixed with `b"\x04"` are decoded as SetCodeTransaction + receipts. + - LegacyTransaction receipts are returned as is. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2, 3, 4) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/forks/amsterdam/bloom.py b/src/ethereum/forks/amsterdam/bloom.py new file mode 100644 index 0000000000..8a12ec081d --- /dev/null +++ b/src/ethereum/forks/amsterdam/bloom.py @@ -0,0 +1,87 @@ +""" +Ethereum Logs Bloom. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +This modules defines functions for calculating bloom filters of logs. For the +general theory of bloom filters see e.g. `Wikipedia +`_. Bloom filters are used to allow +for efficient searching of logs by address and/or topic, by rapidly +eliminating blocks and receipts from their search. +""" + +from typing import Tuple + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import keccak256 + +from .blocks import Log +from .fork_types import Bloom + + +def add_to_bloom(bloom: bytearray, bloom_entry: Bytes) -> None: + """ + Add a bloom entry to the bloom filter (`bloom`). + + The number of hash functions used is 3. They are calculated by taking the + least significant 11 bits from the first 3 16-bit words of the + `keccak_256()` hash of `bloom_entry`. + + Parameters + ---------- + bloom : + The bloom filter. + bloom_entry : + An entry which is to be added to bloom filter. + + """ + hashed = keccak256(bloom_entry) + + for idx in (0, 2, 4): + # Obtain the least significant 11 bits from the pair of bytes + # (16 bits), and set this bit in bloom bytearray. + # The obtained bit is 0-indexed in the bloom filter from the least + # significant bit to the most significant bit. + bit_to_set = Uint.from_be_bytes(hashed[idx : idx + 2]) & Uint(0x07FF) + # Below is the index of the bit in the bytearray (where 0-indexed + # byte is the most significant byte) + bit_index = 0x07FF - int(bit_to_set) + + byte_index = bit_index // 8 + bit_value = 1 << (7 - (bit_index % 8)) + bloom[byte_index] = bloom[byte_index] | bit_value + + +def logs_bloom(logs: Tuple[Log, ...]) -> Bloom: + """ + Obtain the logs bloom from a list of log entries. + + The address and each topic of a log are added to the bloom filter. + + Parameters + ---------- + logs : + List of logs for which the logs bloom is to be obtained. + + Returns + ------- + logs_bloom : `Bloom` + The logs bloom obtained which is 256 bytes with some bits set as per + the caller address and the log topics. + + """ + bloom: bytearray = bytearray(b"\x00" * 256) + + for log in logs: + add_to_bloom(bloom, log.address) + for topic in log.topics: + add_to_bloom(bloom, topic) + + return Bloom(bloom) diff --git a/src/ethereum/forks/amsterdam/exceptions.py b/src/ethereum/forks/amsterdam/exceptions.py new file mode 100644 index 0000000000..3074a1f738 --- /dev/null +++ b/src/ethereum/forks/amsterdam/exceptions.py @@ -0,0 +1,131 @@ +""" +Exceptions specific to this fork. +""" + +from typing import TYPE_CHECKING, Final + +from ethereum_types.numeric import Uint + +from ethereum.exceptions import InvalidTransaction + +if TYPE_CHECKING: + from .transactions import Transaction + + +class TransactionTypeError(InvalidTransaction): + """ + Unknown [EIP-2718] transaction type byte. + + [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + """ + + transaction_type: Final[int] + """ + The type byte of the transaction that caused the error. + """ + + def __init__(self, transaction_type: int): + super().__init__(f"unknown transaction type `{transaction_type}`") + self.transaction_type = transaction_type + + +class TransactionTypeContractCreationError(InvalidTransaction): + """ + Contract creation is not allowed for a transaction type. + """ + + transaction: "Transaction" + """ + The transaction that caused the error. + """ + + def __init__(self, transaction: "Transaction"): + super().__init__( + f"transaction type `{type(transaction).__name__}` not allowed to " + "create contracts" + ) + self.transaction = transaction + + +class BlobGasLimitExceededError(InvalidTransaction): + """ + The blob gas limit for the transaction exceeds the maximum allowed. + """ + + +class InsufficientMaxFeePerBlobGasError(InvalidTransaction): + """ + The maximum fee per blob gas is insufficient for the transaction. + """ + + +class InsufficientMaxFeePerGasError(InvalidTransaction): + """ + The maximum fee per gas is insufficient for the transaction. + """ + + transaction_max_fee_per_gas: Final[Uint] + """ + The maximum fee per gas specified in the transaction. + """ + + block_base_fee_per_gas: Final[Uint] + """ + The base fee per gas of the block in which the transaction is included. + """ + + def __init__( + self, transaction_max_fee_per_gas: Uint, block_base_fee_per_gas: Uint + ): + super().__init__( + f"Insufficient max fee per gas " + f"({transaction_max_fee_per_gas} < {block_base_fee_per_gas})" + ) + self.transaction_max_fee_per_gas = transaction_max_fee_per_gas + self.block_base_fee_per_gas = block_base_fee_per_gas + + +class InvalidBlobVersionedHashError(InvalidTransaction): + """ + The versioned hash of the blob is invalid. + """ + + +class NoBlobDataError(InvalidTransaction): + """ + The transaction does not contain any blob data. + """ + + +class BlobCountExceededError(InvalidTransaction): + """ + The transaction has more blobs than the limit. + """ + + +class PriorityFeeGreaterThanMaxFeeError(InvalidTransaction): + """ + The priority fee is greater than the maximum fee per gas. + """ + + +class EmptyAuthorizationListError(InvalidTransaction): + """ + The authorization list in the transaction is empty. + """ + + +class InitCodeTooLargeError(InvalidTransaction): + """ + The init code of the transaction is too large. + """ + + +class TransactionGasLimitExceededError(InvalidTransaction): + """ + The transaction has specified a gas limit that is greater than the allowed + maximum. + + Note that this is _not_ the exception thrown when bytecode execution runs + out of gas. + """ diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py new file mode 100644 index 0000000000..697086e2b4 --- /dev/null +++ b/src/ethereum/forks/amsterdam/fork.py @@ -0,0 +1,1185 @@ +""" +Ethereum Specification. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Entry point for the Ethereum specification. +""" + +from dataclasses import dataclass +from typing import List, Optional, Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import ( + EthereumException, + GasUsedExceedsLimitError, + InsufficientBalanceError, + InvalidBlock, + InvalidSenderError, + NonceMismatchError, +) + +from . import vm +from .block_access_lists.builder import build_block_access_list +from .block_access_lists.rlp_utils import compute_block_access_list_hash +from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt +from .bloom import logs_bloom +from .exceptions import ( + BlobCountExceededError, + BlobGasLimitExceededError, + EmptyAuthorizationListError, + InsufficientMaxFeePerBlobGasError, + InsufficientMaxFeePerGasError, + InvalidBlobVersionedHashError, + NoBlobDataError, + PriorityFeeGreaterThanMaxFeeError, + TransactionTypeContractCreationError, +) +from .fork_types import Account, Address, Authorization, VersionedHash +from .requests import ( + CONSOLIDATION_REQUEST_TYPE, + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + compute_requests_hash, + parse_deposit_requests, +) +from .state import ( + State, + TransientStorage, + account_exists_and_is_empty, + destroy_account, + get_account, + increment_nonce, + modify_state, + set_account_balance, + state_root, +) +from .state_tracker import ( + StateChanges, + capture_pre_balance, + commit_transaction_frame, + create_child_frame, + filter_net_zero_frame_changes, + increment_block_access_index, + track_address, + track_balance_change, + track_nonce_change, + track_selfdestruct, +) +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, + decode_transaction, + encode_transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set +from .utils.hexadecimal import hex_to_address +from .utils.message import prepare_message +from .vm import Message +from .vm.eoa_delegation import is_valid_delegation +from .vm.gas import ( + BLOB_SCHEDULE_MAX, + GAS_PER_BLOB, + calculate_blob_gas_price, + calculate_data_fee, + calculate_excess_blob_gas, + calculate_total_blob_gas, +) +from .vm.interpreter import MessageCallOutput, process_message_call + +BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) +ELASTICITY_MULTIPLIER = Uint(2) +GAS_LIMIT_ADJUSTMENT_FACTOR = Uint(1024) +GAS_LIMIT_MINIMUM = Uint(5000) +EMPTY_OMMER_HASH = keccak256(rlp.encode([])) +SYSTEM_ADDRESS = hex_to_address("0xfffffffffffffffffffffffffffffffffffffffe") +BEACON_ROOTS_ADDRESS = hex_to_address( + "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" +) +SYSTEM_TRANSACTION_GAS = Uint(30000000) +MAX_BLOB_GAS_PER_BLOCK = BLOB_SCHEDULE_MAX * GAS_PER_BLOB +VERSIONED_HASH_VERSION_KZG = b"\x01" + +WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x00000961Ef480Eb55e80D19ad83579A64c007002" +) +CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x0000BBdDc7CE488642fb579F8B00f3a590007251" +) +HISTORY_STORAGE_ADDRESS = hex_to_address( + "0x0000F90827F1C53a10cb7A02335B175320002935" +) +MAX_BLOCK_SIZE = 10_485_760 +SAFETY_MARGIN = 2_097_152 +MAX_RLP_BLOCK_SIZE = MAX_BLOCK_SIZE - SAFETY_MARGIN +BLOB_COUNT_LIMIT = 6 + + +@dataclass +class BlockChain: + """ + History and current state of the block chain. + """ + + blocks: List[Block] + state: State + chain_id: U64 + + +def apply_fork(old: BlockChain) -> BlockChain: + """ + Transforms the state from the previous hard fork (`old`) into the block + chain object for this hard fork and returns it. + + When forks need to implement an irregular state transition, this function + is used to handle the irregularity. See the :ref:`DAO Fork ` for + an example. + + Parameters + ---------- + old : + Previous block chain object. + + Returns + ------- + new : `BlockChain` + Upgraded block chain object for this hard fork. + + """ + return old + + +def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: + """ + Obtain the list of hashes of the previous 256 blocks in order of + increasing block number. + + This function will return less hashes for the first 256 blocks. + + The ``BLOCKHASH`` opcode needs to access the latest hashes on the chain, + therefore this function retrieves them. + + Parameters + ---------- + chain : + History and current state. + + Returns + ------- + recent_block_hashes : `List[Hash32]` + Hashes of the recent 256 blocks in order of increasing block number. + + """ + recent_blocks = chain.blocks[-255:] + # TODO: This function has not been tested rigorously + if len(recent_blocks) == 0: + return [] + + recent_block_hashes = [] + + for block in recent_blocks: + prev_block_hash = block.header.parent_hash + recent_block_hashes.append(prev_block_hash) + + # We are computing the hash only for the most recent block and not for + # the rest of the blocks as they have successors which have the hash of + # the current block as parent hash. + most_recent_block_hash = keccak256(rlp.encode(recent_blocks[-1].header)) + recent_block_hashes.append(most_recent_block_hash) + + return recent_block_hashes + + +def state_transition(chain: BlockChain, block: Block) -> None: + """ + Attempts to apply a block to an existing block chain. + + All parts of the block's contents need to be verified before being added + to the chain. Blocks are verified by ensuring that the contents of the + block make logical sense with the contents of the parent block. The + information in the block's header must also match the corresponding + information in the block. + + To implement Ethereum, in theory clients are only required to store the + most recent 255 blocks of the chain since as far as execution is + concerned, only those blocks are accessed. Practically, however, clients + should store more blocks to handle reorgs. + + Parameters + ---------- + chain : + History and current state. + block : + Block to apply to `chain`. + + """ + if len(rlp.encode(block)) > MAX_RLP_BLOCK_SIZE: + raise InvalidBlock("Block rlp size exceeds MAX_RLP_BLOCK_SIZE") + + validate_header(chain, block.header) + if block.ommers != (): + raise InvalidBlock + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + prev_randao=block.header.prev_randao, + excess_blob_gas=block.header.excess_blob_gas, + parent_beacon_block_root=block.header.parent_beacon_block_root, + state_changes=StateChanges(), + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + withdrawals=block.withdrawals, + ) + block_state_root = state_root(block_env.state) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + withdrawals_root = root(block_output.withdrawals_trie) + requests_hash = compute_requests_hash(block_output.requests) + computed_block_access_list_hash = compute_block_access_list_hash( + block_output.block_access_list + ) + + if block_output.block_gas_used != block.header.gas_used: + raise InvalidBlock( + f"{block_output.block_gas_used} != {block.header.gas_used}" + ) + if transactions_root != block.header.transactions_root: + raise InvalidBlock + if block_state_root != block.header.state_root: + raise InvalidBlock + if receipt_root != block.header.receipt_root: + raise InvalidBlock + if block_logs_bloom != block.header.bloom: + raise InvalidBlock + if withdrawals_root != block.header.withdrawals_root: + raise InvalidBlock + if block_output.blob_gas_used != block.header.blob_gas_used: + raise InvalidBlock + if requests_hash != block.header.requests_hash: + raise InvalidBlock + if computed_block_access_list_hash != block.header.block_access_list_hash: + raise InvalidBlock("Invalid block access list hash") + + chain.blocks.append(block) + if len(chain.blocks) > 255: + # Real clients have to store more blocks to deal with reorgs, but the + # protocol only requires the last 255 + chain.blocks = chain.blocks[-255:] + + +def calculate_base_fee_per_gas( + block_gas_limit: Uint, + parent_gas_limit: Uint, + parent_gas_used: Uint, + parent_base_fee_per_gas: Uint, +) -> Uint: + """ + Calculates the base fee per gas for the block. + + Parameters + ---------- + block_gas_limit : + Gas limit of the block for which the base fee is being calculated. + parent_gas_limit : + Gas limit of the parent block. + parent_gas_used : + Gas used in the parent block. + parent_base_fee_per_gas : + Base fee per gas of the parent block. + + Returns + ------- + base_fee_per_gas : `Uint` + Base fee per gas for the block. + + """ + parent_gas_target = parent_gas_limit // ELASTICITY_MULTIPLIER + if not check_gas_limit(block_gas_limit, parent_gas_limit): + raise InvalidBlock + + if parent_gas_used == parent_gas_target: + expected_base_fee_per_gas = parent_base_fee_per_gas + elif parent_gas_used > parent_gas_target: + gas_used_delta = parent_gas_used - parent_gas_target + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = max( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR, + Uint(1), + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas + base_fee_per_gas_delta + ) + else: + gas_used_delta = parent_gas_target - parent_gas_used + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = ( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas - base_fee_per_gas_delta + ) + + return Uint(expected_base_fee_per_gas) + + +def validate_header(chain: BlockChain, header: Header) -> None: + """ + Verifies a block header. + + In order to consider a block's header valid, the logic for the + quantities in the header should match the logic for the block itself. + For example the header timestamp should be greater than the block's parent + timestamp because the block was created *after* the parent block. + Additionally, the block's number should be directly following the parent + block's number since it is the next block in the sequence. + + Parameters + ---------- + chain : + History and current state. + header : + Header to check for correctness. + + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_header = chain.blocks[-1].header + + excess_blob_gas = calculate_excess_blob_gas(parent_header) + if header.excess_blob_gas != excess_blob_gas: + raise InvalidBlock + + if header.gas_used > header.gas_limit: + raise InvalidBlock + + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_header.base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: + raise InvalidBlock + if header.number != parent_header.number + Uint(1): + raise InvalidBlock + if len(header.extra_data) > 32: + raise InvalidBlock + if header.difficulty != 0: + raise InvalidBlock + if header.nonce != b"\x00\x00\x00\x00\x00\x00\x00\x00": + raise InvalidBlock + if header.ommers_hash != EMPTY_OMMER_HASH: + raise InvalidBlock + + block_parent_hash = keccak256(rlp.encode(parent_header)) + if header.parent_hash != block_parent_hash: + raise InvalidBlock + + +def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, +) -> Tuple[Address, Uint, Tuple[VersionedHash, ...], U64]: + """ + Check if the transaction is includable in the block. + + Parameters + ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. + tx : + The transaction. + + Returns + ------- + sender_address : + The sender of the transaction. + effective_gas_price : + The price to charge for gas when the transaction is executed. + blob_versioned_hashes : + The blob versioned hashes of the transaction. + tx_blob_gas_used: + The blob gas used by the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not includable. + GasUsedExceedsLimitError : + If the gas used by the transaction exceeds the block's gas limit. + NonceMismatchError : + If the nonce of the transaction is not equal to the sender's nonce. + InsufficientBalanceError : + If the sender's balance is not enough to pay for the transaction. + InvalidSenderError : + If the transaction is from an address that does not exist anymore. + PriorityFeeGreaterThanMaxFeeError : + If the priority fee is greater than the maximum fee per gas. + InsufficientMaxFeePerGasError : + If the maximum fee per gas is insufficient for the transaction. + InsufficientMaxFeePerBlobGasError : + If the maximum fee per blob gas is insufficient for the transaction. + BlobGasLimitExceededError : + If the blob gas used by the transaction exceeds the block's blob gas + limit. + InvalidBlobVersionedHashError : + If the transaction contains a blob versioned hash with an invalid + version. + NoBlobDataError : + If the transaction is a type 3 but has no blobs. + BlobCountExceededError : + If the transaction is a type 3 and has more blobs than the limit. + TransactionTypeContractCreationError: + If the transaction type is not allowed to create contracts. + EmptyAuthorizationListError : + If the transaction is a SetCodeTransaction and the authorization list + is empty. + + """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used + blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used + + if tx.gas > gas_available: + raise GasUsedExceedsLimitError("gas used exceeds limit") + + tx_blob_gas_used = calculate_total_blob_gas(tx) + if tx_blob_gas_used > blob_gas_available: + raise BlobGasLimitExceededError("blob gas limit exceeded") + + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) + + if isinstance( + tx, (FeeMarketTransaction, BlobTransaction, SetCodeTransaction) + ): + if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: + raise PriorityFeeGreaterThanMaxFeeError( + "priority fee greater than max fee" + ) + if tx.max_fee_per_gas < block_env.base_fee_per_gas: + raise InsufficientMaxFeePerGasError( + tx.max_fee_per_gas, block_env.base_fee_per_gas + ) + + priority_fee_per_gas = min( + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, + ) + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas + else: + if tx.gas_price < block_env.base_fee_per_gas: + raise InvalidBlock + effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if isinstance(tx, BlobTransaction): + blob_count = len(tx.blob_versioned_hashes) + if blob_count == 0: + raise NoBlobDataError("no blob data in transaction") + if blob_count > BLOB_COUNT_LIMIT: + raise BlobCountExceededError( + f"Tx has {blob_count} blobs. Max allowed: {BLOB_COUNT_LIMIT}" + ) + for blob_versioned_hash in tx.blob_versioned_hashes: + if blob_versioned_hash[0:1] != VERSIONED_HASH_VERSION_KZG: + raise InvalidBlobVersionedHashError( + "invalid blob versioned hash" + ) + + blob_gas_price = calculate_blob_gas_price(block_env.excess_blob_gas) + if Uint(tx.max_fee_per_blob_gas) < blob_gas_price: + raise InsufficientMaxFeePerBlobGasError( + "insufficient max fee per blob gas" + ) + + max_gas_fee += Uint(calculate_total_blob_gas(tx)) * Uint( + tx.max_fee_per_blob_gas + ) + blob_versioned_hashes = tx.blob_versioned_hashes + else: + blob_versioned_hashes = () + + if isinstance(tx, (BlobTransaction, SetCodeTransaction)): + if not isinstance(tx.to, Address): + raise TransactionTypeContractCreationError(tx) + + if isinstance(tx, SetCodeTransaction): + if not any(tx.authorizations): + raise EmptyAuthorizationListError("empty authorization list") + + if sender_account.nonce > Uint(tx.nonce): + raise NonceMismatchError("nonce too low") + elif sender_account.nonce < Uint(tx.nonce): + raise NonceMismatchError("nonce too high") + + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InsufficientBalanceError("insufficient sender balance") + if sender_account.code and not is_valid_delegation(sender_account.code): + raise InvalidSenderError("not EOA") + + return ( + sender_address, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) + + +def make_receipt( + tx: Transaction, + error: Optional[EthereumException], + cumulative_gas_used: Uint, + logs: Tuple[Log, ...], +) -> Bytes | Receipt: + """ + Make the receipt for a transaction that was executed. + + Parameters + ---------- + tx : + The executed transaction. + error : + Error in the top level frame of the transaction, if any. + cumulative_gas_used : + The total gas used so far in the block after the transaction was + executed. + logs : + The logs produced by the transaction. + + Returns + ------- + receipt : + The receipt for the transaction. + + """ + receipt = Receipt( + succeeded=error is None, + cumulative_gas_used=cumulative_gas_used, + bloom=logs_bloom(logs), + logs=logs, + ) + + return encode_receipt(tx, receipt) + + +def process_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + system_contract_code: Bytes, + data: Bytes, +) -> MessageCallOutput: + """ + Process a system transaction with the given code. + + Prefer calling `process_checked_system_transaction` or + `process_unchecked_system_transaction` depending on whether missing code or + an execution error should cause the block to be rejected. + + Parameters + ---------- + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + system_contract_code : + Code of the contract to call. + data : + Data to pass to the contract. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + + """ + # EIP-7928: Create a child frame for system transaction + # This allows proper pre-state capture for net-zero filtering + system_tx_state_changes = create_child_frame(block_env.state_changes) + + tx_env = vm.TransactionEnvironment( + origin=SYSTEM_ADDRESS, + gas_price=block_env.base_fee_per_gas, + gas=SYSTEM_TRANSACTION_GAS, + access_list_addresses=set(), + access_list_storage_keys=set(), + transient_storage=TransientStorage(), + blob_versioned_hashes=(), + authorizations=(), + index_in_block=None, + tx_hash=None, + state_changes=system_tx_state_changes, + ) + + # Create call frame as child of tx frame + call_frame = create_child_frame(tx_env.state_changes) + + system_tx_message = Message( + block_env=block_env, + tx_env=tx_env, + caller=SYSTEM_ADDRESS, + target=target_address, + gas=SYSTEM_TRANSACTION_GAS, + value=U256(0), + data=data, + code=system_contract_code, + depth=Uint(0), + current_target=target_address, + code_address=target_address, + should_transfer_value=False, + is_static=False, + accessed_addresses=set(), + accessed_storage_keys=set(), + disable_precompiles=False, + parent_evm=None, + is_create=False, + state_changes=call_frame, + ) + + system_tx_output = process_message_call(system_tx_message) + + # Commit system transaction changes to block frame + # System transactions always succeed (or block is invalid) + commit_transaction_frame(tx_env.state_changes) + + return system_tx_output + + +def process_checked_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + data: Bytes, +) -> MessageCallOutput: + """ + Process a system transaction and raise an error if the contract does not + contain code or if the transaction fails. + + Parameters + ---------- + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + data : + Data to pass to the contract. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + + """ + system_contract_code = get_account(block_env.state, target_address).code + + if len(system_contract_code) == 0: + raise InvalidBlock( + f"System contract address {target_address.hex()} does not " + "contain code" + ) + + system_tx_output = process_system_transaction( + block_env, + target_address, + system_contract_code, + data, + ) + + if system_tx_output.error: + raise InvalidBlock( + f"System contract ({target_address.hex()}) call failed: " + f"{system_tx_output.error}" + ) + + return system_tx_output + + +def process_unchecked_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + data: Bytes, +) -> MessageCallOutput: + """ + Process a system transaction without checking if the contract contains code + or if the transaction fails. + + Parameters + ---------- + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + data : + Data to pass to the contract. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + + """ + system_contract_code = get_account(block_env.state, target_address).code + return process_system_transaction( + block_env, + target_address, + system_contract_code, + data, + ) + + +def apply_body( + block_env: vm.BlockEnvironment, + transactions: Tuple[LegacyTransaction | Bytes, ...], + withdrawals: Tuple[Withdrawal, ...], +) -> vm.BlockOutput: + """ + Executes a block. + + Many of the contents of a block are stored in data structures called + tries. There is a transactions trie which is similar to a ledger of the + transactions stored in the current block. There is also a receipts trie + which stores the results of executing a transaction, like the post state + and gas used. This function creates and executes the block that is to be + added to the chain. + + Parameters + ---------- + block_env : + The block scoped environment. + transactions : + Transactions included in the block. + withdrawals : + Withdrawals to be processed in the current block. + + Returns + ------- + block_output : + The block output for the current block. + + """ + block_output = vm.BlockOutput() + + # EIP-7928: System contracts use block_access_index 0 + # The block frame already starts at index 0, so system transactions + # naturally use that index through the block frame + + process_unchecked_system_transaction( + block_env=block_env, + target_address=BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, + ) + + process_unchecked_system_transaction( + block_env=block_env, + target_address=HISTORY_STORAGE_ADDRESS, + data=block_env.block_hashes[-1], # The parent hash + ) + + for i, tx in enumerate(map(decode_transaction, transactions)): + process_transaction(block_env, block_output, tx, Uint(i)) + + # EIP-7928: Increment block frame to post-execution index + # After N transactions, block frame is at index N + # Post-execution operations (withdrawals, etc.) use index N+1 + increment_block_access_index(block_env.state_changes) + + process_withdrawals(block_env, block_output, withdrawals) + + process_general_purpose_requests( + block_env=block_env, + block_output=block_output, + ) + # Build block access list from block_env.state_changes + block_output.block_access_list = build_block_access_list( + block_env.state_changes + ) + + return block_output + + +def process_general_purpose_requests( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, +) -> None: + """ + Process all the requests in the block. + + Parameters + ---------- + block_env : + The execution environment for the Block. + block_output : + The block output for the current block. + + """ + # Requests are to be in ascending order of request type + deposit_requests = parse_deposit_requests(block_output) + requests_from_execution = block_output.requests + if len(deposit_requests) > 0: + requests_from_execution.append(DEPOSIT_REQUEST_TYPE + deposit_requests) + + system_withdrawal_tx_output = process_checked_system_transaction( + block_env=block_env, + target_address=WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + data=b"", + ) + + if len(system_withdrawal_tx_output.return_data) > 0: + requests_from_execution.append( + WITHDRAWAL_REQUEST_TYPE + system_withdrawal_tx_output.return_data + ) + + system_consolidation_tx_output = process_checked_system_transaction( + block_env=block_env, + target_address=CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + data=b"", + ) + + if len(system_consolidation_tx_output.return_data) > 0: + requests_from_execution.append( + CONSOLIDATION_REQUEST_TYPE + + system_consolidation_tx_output.return_data + ) + + +def process_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: + """ + Execute a transaction against the provided environment. + + This function processes the actions needed to execute a transaction. + It decrements the sender's account balance after calculating the gas fee + and refunds them the proper amount after execution. Calling contracts, + deploying code, and incrementing nonces are all examples of actions that + happen within this function or from a call made within this function. + + Accounts that are marked for deletion are processed and destroyed after + execution. + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. + tx : + Transaction to execute. + index: + Index of the transaction in the block. + + """ + # EIP-7928: Create a transaction-level StateChanges frame + # The frame will read the current block_access_index from the block frame + increment_block_access_index(block_env.state_changes) + tx_state_changes = create_child_frame(block_env.state_changes) + + # Capture coinbase pre-balance for net-zero filtering + coinbase_pre_balance = get_account( + block_env.state, block_env.coinbase + ).balance + track_address(tx_state_changes, block_env.coinbase) + capture_pre_balance( + tx_state_changes, block_env.coinbase, coinbase_pre_balance + ) + + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + + intrinsic_gas, calldata_floor_gas_cost = validate_transaction(tx) + + ( + sender, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.state, sender) + + if isinstance(tx, BlobTransaction): + blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) + else: + blob_gas_fee = Uint(0) + + effective_gas_fee = tx.gas * effective_gas_price + + gas = tx.gas - intrinsic_gas + + # Track sender nonce increment + increment_nonce(block_env.state, sender) + sender_nonce_after = get_account(block_env.state, sender).nonce + track_nonce_change(tx_state_changes, sender, U64(sender_nonce_after)) + + # Track sender balance deduction for gas fee + sender_balance_before = get_account(block_env.state, sender).balance + track_address(tx_state_changes, sender) + capture_pre_balance(tx_state_changes, sender, sender_balance_before) + + sender_balance_after_gas_fee = ( + Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee + ) + set_account_balance( + block_env.state, sender, U256(sender_balance_after_gas_fee) + ) + track_balance_change( + tx_state_changes, + sender, + U256(sender_balance_after_gas_fee), + ) + + access_list_addresses = set() + access_list_storage_keys = set() + access_list_addresses.add(block_env.coinbase) + if isinstance( + tx, + ( + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + ), + ): + for access in tx.access_list: + access_list_addresses.add(access.account) + for slot in access.slots: + access_list_storage_keys.add((access.account, slot)) + + authorizations: Tuple[Authorization, ...] = () + if isinstance(tx, SetCodeTransaction): + authorizations = tx.authorizations + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + transient_storage=TransientStorage(), + blob_versioned_hashes=blob_versioned_hashes, + authorizations=authorizations, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + state_changes=tx_state_changes, + ) + + message = prepare_message( + block_env, + tx_env, + tx, + ) + + tx_output = process_message_call(message) + + # For EIP-7623 we first calculate the execution_gas_used, which includes + # the execution gas refund. + tx_gas_used_before_refund = tx.gas - tx_output.gas_left + tx_gas_refund = min( + tx_gas_used_before_refund // Uint(5), Uint(tx_output.refund_counter) + ) + tx_gas_used_after_refund = tx_gas_used_before_refund - tx_gas_refund + + # Transactions with less execution_gas_used than the floor pay at the + # floor cost. + tx_gas_used_after_refund = max( + tx_gas_used_after_refund, calldata_floor_gas_cost + ) + + tx_gas_left = tx.gas - tx_gas_used_after_refund + gas_refund_amount = tx_gas_left * effective_gas_price + + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used_after_refund * priority_fee_per_gas + + # refund gas + sender_balance_after_refund = get_account( + block_env.state, sender + ).balance + U256(gas_refund_amount) + set_account_balance(block_env.state, sender, sender_balance_after_refund) + track_balance_change( + tx_env.state_changes, + sender, + sender_balance_after_refund, + ) + + coinbase_balance_after_mining_fee = get_account( + block_env.state, block_env.coinbase + ).balance + U256(transaction_fee) + + set_account_balance( + block_env.state, block_env.coinbase, coinbase_balance_after_mining_fee + ) + track_balance_change( + tx_env.state_changes, + block_env.coinbase, + coinbase_balance_after_mining_fee, + ) + + if coinbase_balance_after_mining_fee == 0 and account_exists_and_is_empty( + block_env.state, block_env.coinbase + ): + destroy_account(block_env.state, block_env.coinbase) + + block_output.block_gas_used += tx_gas_used_after_refund + block_output.blob_gas_used += tx_blob_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + receipt_key = rlp.encode(Uint(index)) + block_output.receipt_keys += (receipt_key,) + + trie_set( + block_output.receipts_trie, + receipt_key, + receipt, + ) + + block_output.block_logs += tx_output.logs + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + # EIP-7928: Filter net-zero changes before committing to block frame. + # Must happen AFTER destroy_account so filtering sees correct state. + filter_net_zero_frame_changes(tx_env.state_changes, block_env.state) + + commit_transaction_frame(tx_env.state_changes) + + # EIP-7928: Track in-transaction self-destruct normalization AFTER merge + # Convert storage writes to reads and remove nonce/code changes + for address in tx_output.accounts_to_delete: + track_selfdestruct(block_env.state_changes, address) + + +def process_withdrawals( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + withdrawals: Tuple[Withdrawal, ...], +) -> None: + """ + Increase the balance of the withdrawing account. + """ + # Capture pre-state for withdrawal balance filtering + withdrawal_addresses = {wd.address for wd in withdrawals} + for address in withdrawal_addresses: + pre_balance = get_account(block_env.state, address).balance + track_address(block_env.state_changes, address) + capture_pre_balance(block_env.state_changes, address, pre_balance) + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += wd.amount * U256(10**9) + + for i, wd in enumerate(withdrawals): + trie_set( + block_output.withdrawals_trie, + rlp.encode(Uint(i)), + rlp.encode(wd), + ) + + modify_state(block_env.state, wd.address, increase_recipient_balance) + + new_balance = get_account(block_env.state, wd.address).balance + track_balance_change( + block_env.state_changes, + wd.address, + new_balance, + ) + + if account_exists_and_is_empty(block_env.state, wd.address): + destroy_account(block_env.state, wd.address) + + # EIP-7928: Filter net-zero balance changes for withdrawals + filter_net_zero_frame_changes(block_env.state_changes, block_env.state) + + +def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: + """ + Validates the gas limit for a block. + + The bounds of the gas limit, ``max_adjustment_delta``, is set as the + quotient of the parent block's gas limit and the + ``GAS_LIMIT_ADJUSTMENT_FACTOR``. Therefore, if the gas limit that is + passed through as a parameter is greater than or equal to the *sum* of + the parent's gas and the adjustment delta then the limit for gas is too + high and fails this function's check. Similarly, if the limit is less + than or equal to the *difference* of the parent's gas and the adjustment + delta *or* the predefined ``GAS_LIMIT_MINIMUM`` then this function's + check fails because the gas limit doesn't allow for a sufficient or + reasonable amount of gas to be used on a block. + + Parameters + ---------- + gas_limit : + Gas limit to validate. + + parent_gas_limit : + Gas limit of the parent block. + + Returns + ------- + check : `bool` + True if gas limit constraints are satisfied, False otherwise. + + """ + max_adjustment_delta = parent_gas_limit // GAS_LIMIT_ADJUSTMENT_FACTOR + if gas_limit >= parent_gas_limit + max_adjustment_delta: + return False + if gas_limit <= parent_gas_limit - max_adjustment_delta: + return False + if gas_limit < GAS_LIMIT_MINIMUM: + return False + + return True diff --git a/src/ethereum/forks/amsterdam/fork_types.py b/src/ethereum/forks/amsterdam/fork_types.py new file mode 100644 index 0000000000..24fbf08baf --- /dev/null +++ b/src/ethereum/forks/amsterdam/fork_types.py @@ -0,0 +1,78 @@ +""" +Ethereum Types. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Types reused throughout the specification, which are specific to Ethereum. +""" + +from dataclasses import dataclass + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes20, Bytes256 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U8, U64, U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 + +Address = Bytes20 +Root = Hash32 +VersionedHash = Hash32 + +Bloom = Bytes256 + + +@slotted_freezable +@dataclass +class Account: + """ + State associated with an address. + """ + + nonce: Uint + balance: U256 + code: Bytes + + +EMPTY_ACCOUNT = Account( + nonce=Uint(0), + balance=U256(0), + code=b"", +) + + +def encode_account(raw_account_data: Account, storage_root: Bytes) -> Bytes: + """ + Encode `Account` dataclass. + + Storage is not stored in the `Account` dataclass, so `Accounts` cannot be + encoded without providing a storage root. + """ + return rlp.encode( + ( + raw_account_data.nonce, + raw_account_data.balance, + storage_root, + keccak256(raw_account_data.code), + ) + ) + + +@slotted_freezable +@dataclass +class Authorization: + """ + The authorization for a set code transaction. + """ + + chain_id: U256 + address: Address + nonce: U64 + y_parity: U8 + r: U256 + s: U256 diff --git a/src/ethereum/forks/amsterdam/requests.py b/src/ethereum/forks/amsterdam/requests.py new file mode 100644 index 0000000000..929e973e58 --- /dev/null +++ b/src/ethereum/forks/amsterdam/requests.py @@ -0,0 +1,191 @@ +""" +Requests were introduced in EIP-7685 as a a general purpose framework for +storing contract-triggered requests. It extends the execution header and +body with a single field each to store the request information. +This inherently exposes the requests to the consensus layer, which can +then process each one. + +[EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 +""" + +from hashlib import sha256 +from typing import List + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint, ulen + +from ethereum.exceptions import InvalidBlock +from ethereum.utils.hexadecimal import hex_to_bytes32 + +from .blocks import decode_receipt +from .trie import trie_get +from .utils.hexadecimal import hex_to_address +from .vm import BlockOutput + +DEPOSIT_CONTRACT_ADDRESS = hex_to_address( + "0x00000000219ab540356cbb839cbe05303d7705fa" +) +DEPOSIT_EVENT_SIGNATURE_HASH = hex_to_bytes32( + "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5" +) +DEPOSIT_REQUEST_TYPE = b"\x00" +WITHDRAWAL_REQUEST_TYPE = b"\x01" +CONSOLIDATION_REQUEST_TYPE = b"\x02" + + +DEPOSIT_EVENT_LENGTH = Uint(576) + +PUBKEY_OFFSET = Uint(160) +WITHDRAWAL_CREDENTIALS_OFFSET = Uint(256) +AMOUNT_OFFSET = Uint(320) +SIGNATURE_OFFSET = Uint(384) +INDEX_OFFSET = Uint(512) + +PUBKEY_SIZE = Uint(48) +WITHDRAWAL_CREDENTIALS_SIZE = Uint(32) +AMOUNT_SIZE = Uint(8) +SIGNATURE_SIZE = Uint(96) +INDEX_SIZE = Uint(8) + + +def extract_deposit_data(data: Bytes) -> Bytes: + """ + Extracts Deposit Request from the DepositContract.DepositEvent data. + + Raises + ------ + InvalidBlock : + If the deposit contract did not produce a valid log. + + """ + if ulen(data) != DEPOSIT_EVENT_LENGTH: + raise InvalidBlock("Invalid deposit event data length") + + # Check that all the offsets are in order + pubkey_offset = Uint.from_be_bytes(data[0:32]) + if pubkey_offset != PUBKEY_OFFSET: + raise InvalidBlock("Invalid pubkey offset in deposit log") + + withdrawal_credentials_offset = Uint.from_be_bytes(data[32:64]) + if withdrawal_credentials_offset != WITHDRAWAL_CREDENTIALS_OFFSET: + raise InvalidBlock( + "Invalid withdrawal credentials offset in deposit log" + ) + + amount_offset = Uint.from_be_bytes(data[64:96]) + if amount_offset != AMOUNT_OFFSET: + raise InvalidBlock("Invalid amount offset in deposit log") + + signature_offset = Uint.from_be_bytes(data[96:128]) + if signature_offset != SIGNATURE_OFFSET: + raise InvalidBlock("Invalid signature offset in deposit log") + + index_offset = Uint.from_be_bytes(data[128:160]) + if index_offset != INDEX_OFFSET: + raise InvalidBlock("Invalid index offset in deposit log") + + # Check that all the sizes are in order + pubkey_size = Uint.from_be_bytes( + data[pubkey_offset : pubkey_offset + Uint(32)] + ) + if pubkey_size != PUBKEY_SIZE: + raise InvalidBlock("Invalid pubkey size in deposit log") + + pubkey = data[ + pubkey_offset + Uint(32) : pubkey_offset + Uint(32) + PUBKEY_SIZE + ] + + withdrawal_credentials_size = Uint.from_be_bytes( + data[ + withdrawal_credentials_offset : withdrawal_credentials_offset + + Uint(32) + ], + ) + if withdrawal_credentials_size != WITHDRAWAL_CREDENTIALS_SIZE: + raise InvalidBlock( + "Invalid withdrawal credentials size in deposit log" + ) + + withdrawal_credentials = data[ + withdrawal_credentials_offset + + Uint(32) : withdrawal_credentials_offset + + Uint(32) + + WITHDRAWAL_CREDENTIALS_SIZE + ] + + amount_size = Uint.from_be_bytes( + data[amount_offset : amount_offset + Uint(32)] + ) + if amount_size != AMOUNT_SIZE: + raise InvalidBlock("Invalid amount size in deposit log") + + amount = data[ + amount_offset + Uint(32) : amount_offset + Uint(32) + AMOUNT_SIZE + ] + + signature_size = Uint.from_be_bytes( + data[signature_offset : signature_offset + Uint(32)] + ) + if signature_size != SIGNATURE_SIZE: + raise InvalidBlock("Invalid signature size in deposit log") + + signature = data[ + signature_offset + Uint(32) : signature_offset + + Uint(32) + + SIGNATURE_SIZE + ] + + index_size = Uint.from_be_bytes( + data[index_offset : index_offset + Uint(32)] + ) + if index_size != INDEX_SIZE: + raise InvalidBlock("Invalid index size in deposit log") + + index = data[ + index_offset + Uint(32) : index_offset + Uint(32) + INDEX_SIZE + ] + + return pubkey + withdrawal_credentials + amount + signature + index + + +def parse_deposit_requests(block_output: BlockOutput) -> Bytes: + """ + Parse deposit requests from the block output. + """ + deposit_requests: Bytes = b"" + for key in block_output.receipt_keys: + receipt = trie_get(block_output.receipts_trie, key) + assert receipt is not None + decoded_receipt = decode_receipt(receipt) + for log in decoded_receipt.logs: + if log.address == DEPOSIT_CONTRACT_ADDRESS: + if ( + len(log.topics) > 0 + and log.topics[0] == DEPOSIT_EVENT_SIGNATURE_HASH + ): + request = extract_deposit_data(log.data) + deposit_requests += request + + return deposit_requests + + +def compute_requests_hash(requests: List[Bytes]) -> Bytes: + """ + Get the hash of the requests using the SHA2-256 algorithm. + + Parameters + ---------- + requests : Bytes + The requests to hash. + + Returns + ------- + requests_hash : Bytes + The hash of the requests. + + """ + m = sha256() + for request in requests: + m.update(sha256(request).digest()) + + return m.digest() diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py new file mode 100644 index 0000000000..fcf12e971b --- /dev/null +++ b/src/ethereum/forks/amsterdam/state.py @@ -0,0 +1,727 @@ +""" +State. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state contains all information that is preserved between transactions. + +It consists of a main account trie and storage tries for each contract. + +There is a distinction between an account that does not exist and +`EMPTY_ACCOUNT`. +""" + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set, Tuple + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.frozen import modify +from ethereum_types.numeric import U256, Uint + +from .fork_types import EMPTY_ACCOUNT, Account, Address, Root +from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set + +if TYPE_CHECKING: + from .vm import BlockEnvironment # noqa: F401 + + +@dataclass +class State: + """ + Contains all information that is preserved between transactions. + """ + + _main_trie: Trie[Address, Optional[Account]] = field( + default_factory=lambda: Trie(secured=True, default=None) + ) + _storage_tries: Dict[Address, Trie[Bytes32, U256]] = field( + default_factory=dict + ) + _snapshots: List[ + Tuple[ + Trie[Address, Optional[Account]], + Dict[Address, Trie[Bytes32, U256]], + ] + ] = field(default_factory=list) + created_accounts: Set[Address] = field(default_factory=set) + + +@dataclass +class TransientStorage: + """ + Contains all information that is preserved between message calls + within a transaction. + """ + + _tries: Dict[Address, Trie[Bytes32, U256]] = field(default_factory=dict) + _snapshots: List[Dict[Address, Trie[Bytes32, U256]]] = field( + default_factory=list + ) + + +def close_state(state: State) -> None: + """ + Free resources held by the state. Used by optimized implementations to + release file descriptors. + """ + del state._main_trie + del state._storage_tries + del state._snapshots + del state.created_accounts + + +def begin_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Start a state transaction. + + Transactions are entirely implicit and can be nested. It is not possible to + calculate the state root during a transaction. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + + """ + state._snapshots.append( + ( + copy_trie(state._main_trie), + {k: copy_trie(t) for (k, t) in state._storage_tries.items()}, + ) + ) + transient_storage._snapshots.append( + {k: copy_trie(t) for (k, t) in transient_storage._tries.items()} + ) + + +def commit_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Commit a state transaction. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + + """ + state._snapshots.pop() + if not state._snapshots: + state.created_accounts.clear() + + transient_storage._snapshots.pop() + + +def rollback_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Rollback a state transaction, resetting the state to the point when the + corresponding `begin_transaction()` call was made. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + + """ + state._main_trie, state._storage_tries = state._snapshots.pop() + if not state._snapshots: + state.created_accounts.clear() + + transient_storage._tries = transient_storage._snapshots.pop() + + +def get_account(state: State, address: Address) -> Account: + """ + Get the `Account` object at an address. Returns `EMPTY_ACCOUNT` if there + is no account at the address. + + Use `get_account_optional()` if you care about the difference between a + non-existent account and `EMPTY_ACCOUNT`. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to lookup. + + Returns + ------- + account : `Account` + Account at address. + + """ + account = get_account_optional(state, address) + if isinstance(account, Account): + return account + else: + return EMPTY_ACCOUNT + + +def get_account_optional(state: State, address: Address) -> Optional[Account]: + """ + Get the `Account` object at an address. Returns `None` (rather than + `EMPTY_ACCOUNT`) if there is no account at the address. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to lookup. + + Returns + ------- + account : `Account` + Account at address. + + """ + account = trie_get(state._main_trie, address) + return account + + +def set_account( + state: State, address: Address, account: Optional[Account] +) -> None: + """ + Set the `Account` object at an address. Setting to `None` deletes + the account (but not its storage, see `destroy_account()`). + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to set. + account : `Account` + Account to set at address. + + """ + trie_set(state._main_trie, address, account) + + +def destroy_account(state: State, address: Address) -> None: + """ + Completely remove the account at `address` and all of its storage. + + This function is made available exclusively for the `SELFDESTRUCT` + opcode. It is expected that `SELFDESTRUCT` will be disabled in a future + hardfork and this function will be removed. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of account to destroy. + + """ + destroy_storage(state, address) + set_account(state, address, None) + + +def destroy_storage(state: State, address: Address) -> None: + """ + Completely remove the storage at `address`. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of account whose storage is to be deleted. + + """ + if address in state._storage_tries: + del state._storage_tries[address] + + +def mark_account_created(state: State, address: Address) -> None: + """ + Mark an account as having been created in the current transaction. + This information is used by `get_storage_original()` to handle an obscure + edgecase, and to respect the constraints added to SELFDESTRUCT by + EIP-6780. + + The marker is not removed even if the account creation reverts. Since the + account cannot have had code prior to its creation and can't call + `get_storage_original()`, this is harmless. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account that has been created. + + """ + state.created_accounts.add(address) + + +def get_storage(state: State, address: Address, key: Bytes32) -> U256: + """ + Get a value at a storage key on an account. Returns `U256(0)` if the + storage key has not been set previously. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account. + key : `Bytes` + Key to lookup. + + Returns + ------- + value : `U256` + Value at the key. + + """ + trie = state._storage_tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + +def set_storage( + state: State, address: Address, key: Bytes32, value: U256 +) -> None: + """ + Set a value at a storage key on an account. Setting to `U256(0)` deletes + the key. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account. + key : `Bytes` + Key to set. + value : `U256` + Value to set at the key. + + """ + assert trie_get(state._main_trie, address) is not None + + trie = state._storage_tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + state._storage_tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del state._storage_tries[address] + + +def storage_root(state: State, address: Address) -> Root: + """ + Calculate the storage root of an account. + + Parameters + ---------- + state: + The state + address : + Address of the account. + + Returns + ------- + root : `Root` + Storage root of the account. + + """ + assert not state._snapshots + if address in state._storage_tries: + return root(state._storage_tries[address]) + else: + return EMPTY_TRIE_ROOT + + +def state_root(state: State) -> Root: + """ + Calculate the state root. + + Parameters + ---------- + state: + The current state. + + Returns + ------- + root : `Root` + The state root. + + """ + assert not state._snapshots + + def get_storage_root(address: Address) -> Root: + return storage_root(state, address) + + return root(state._main_trie, get_storage_root=get_storage_root) + + +def account_exists(state: State, address: Address) -> bool: + """ + Checks if an account exists in the state trie. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + account_exists : `bool` + True if account exists in the state trie, False otherwise + + """ + return get_account_optional(state, address) is not None + + +def account_has_code_or_nonce(state: State, address: Address) -> bool: + """ + Checks if an account has non zero nonce or non empty code. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + has_code_or_nonce : `bool` + True if the account has non zero nonce or non empty code, + False otherwise. + + """ + account = get_account(state, address) + return account.nonce != Uint(0) or account.code != b"" + + +def account_has_storage(state: State, address: Address) -> bool: + """ + Checks if an account has storage. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + has_storage : `bool` + True if the account has storage, False otherwise. + + """ + return address in state._storage_tries + + +def account_exists_and_is_empty(state: State, address: Address) -> bool: + """ + Checks if an account exists and has zero nonce, empty code and zero + balance. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + exists_and_is_empty : `bool` + True if an account exists and has zero nonce, empty code and zero + balance, False otherwise. + + """ + account = get_account_optional(state, address) + return ( + account is not None + and account.nonce == Uint(0) + and account.code == b"" + and account.balance == 0 + ) + + +def is_account_alive(state: State, address: Address) -> bool: + """ + Check whether an account is both in the state and non-empty. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + is_alive : `bool` + True if the account is alive. + + """ + account = get_account_optional(state, address) + return account is not None and account != EMPTY_ACCOUNT + + +def modify_state( + state: State, address: Address, f: Callable[[Account], None] +) -> None: + """ + Modify an `Account` in the `State`. If, after modification, the account + exists and has zero nonce, empty code, and zero balance, it is destroyed. + """ + set_account(state, address, modify(get_account(state, address), f)) + if account_exists_and_is_empty(state, address): + destroy_account(state, address) + + +def move_ether( + state: State, + sender_address: Address, + recipient_address: Address, + amount: U256, +) -> None: + """ + Move funds between accounts. + + Parameters + ---------- + state: + The current state. + sender_address: + Address of the sender. + recipient_address: + Address of the recipient. + amount: + The amount to transfer. + + """ + + def reduce_sender_balance(sender: Account) -> None: + if sender.balance < amount: + raise AssertionError + sender.balance -= amount + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += amount + + modify_state(state, sender_address, reduce_sender_balance) + modify_state(state, recipient_address, increase_recipient_balance) + + +def set_account_balance(state: State, address: Address, amount: U256) -> None: + """ + Sets the balance of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose balance needs to be set. + + amount: + The amount that needs to set in balance. + + """ + + def set_balance(account: Account) -> None: + account.balance = amount + + modify_state(state, address, set_balance) + + +def increment_nonce(state: State, address: Address) -> None: + """ + Increments the nonce of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + + """ + + def increase_nonce(sender: Account) -> None: + sender.nonce += Uint(1) + + modify_state(state, address, increase_nonce) + + +def set_code(state: State, address: Address, code: Bytes) -> None: + """ + Sets Account code. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose code needs to be update. + + code: + The bytecode that needs to be set. + + """ + + def write_code(sender: Account) -> None: + sender.code = code + + modify_state(state, address, write_code) + + +def set_authority_code(state: State, address: Address, code: Bytes) -> None: + """ + Sets authority account code for EIP-7702 delegation. + + This function is used specifically for setting authority code within + EIP-7702 Set Code Transactions. + + Parameters + ---------- + state: + The current state. + + address: + Address of the authority account whose code needs to be set. + + code: + The delegation designation bytecode to set. + + """ + + def write_code(sender: Account) -> None: + sender.code = code + + modify_state(state, address, write_code) + + +def get_storage_original(state: State, address: Address, key: Bytes32) -> U256: + """ + Get the original value in a storage slot i.e. the value before the current + transaction began. This function reads the value from the snapshots taken + before executing the transaction. + + Parameters + ---------- + state: + The current state. + address: + Address of the account to read the value from. + key: + Key of the storage slot. + + """ + # In the transaction where an account is created, its preexisting storage + # is ignored. + if address in state.created_accounts: + return U256(0) + + _, original_trie = state._snapshots[0] + original_account_trie = original_trie.get(address) + + if original_account_trie is None: + original_value = U256(0) + else: + original_value = trie_get(original_account_trie, key) + + assert isinstance(original_value, U256) + + return original_value + + +def get_transient_storage( + transient_storage: TransientStorage, address: Address, key: Bytes32 +) -> U256: + """ + Get a value at a storage key on an account from transient storage. + Returns `U256(0)` if the storage key has not been set previously. + + Parameters + ---------- + transient_storage: `TransientStorage` + The transient storage + address : `Address` + Address of the account. + key : `Bytes` + Key to lookup. + + Returns + ------- + value : `U256` + Value at the key. + + """ + trie = transient_storage._tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + +def set_transient_storage( + transient_storage: TransientStorage, + address: Address, + key: Bytes32, + value: U256, +) -> None: + """ + Set a value at a storage key on an account. Setting to `U256(0)` deletes + the key. + + Parameters + ---------- + transient_storage: `TransientStorage` + The transient storage + address : `Address` + Address of the account. + key : `Bytes` + Key to set. + value : `U256` + Value to set at the key. + + """ + trie = transient_storage._tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + transient_storage._tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del transient_storage._tries[address] diff --git a/src/ethereum/forks/amsterdam/state_tracker.py b/src/ethereum/forks/amsterdam/state_tracker.py new file mode 100644 index 0000000000..19a929d0dd --- /dev/null +++ b/src/ethereum/forks/amsterdam/state_tracker.py @@ -0,0 +1,556 @@ +""" +EIP-7928 Block Access Lists: Hierarchical State Change Tracking. + +Frame hierarchy mirrors EVM execution: Block -> Transaction -> Call frames. +Each frame tracks state accesses and merges to parent on completion. + +On success, changes merge upward with net-zero filtering (pre-state vs final). +On failure, only reads merge (writes discarded). Pre-state captures use +first-write-wins semantics and are stored at the transaction frame level. + +[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 +""" + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from .block_access_lists.rlp_types import BlockAccessIndex +from .fork_types import Address + +if TYPE_CHECKING: + from .state import State + + +@dataclass +class StateChanges: + """ + Tracks state changes within a single execution frame. + + Frames form a hierarchy (Block -> Transaction -> Call) linked by parent + references. The block_access_index is stored at the root frame. Pre-state + captures (pre_balances, etc.) are only populated at the transaction level. + """ + + parent: Optional["StateChanges"] = None + block_access_index: BlockAccessIndex = BlockAccessIndex(0) + + touched_addresses: Set[Address] = field(default_factory=set) + storage_reads: Set[Tuple[Address, Bytes32]] = field(default_factory=set) + storage_writes: Dict[Tuple[Address, Bytes32, BlockAccessIndex], U256] = ( + field(default_factory=dict) + ) + + balance_changes: Dict[Tuple[Address, BlockAccessIndex], U256] = field( + default_factory=dict + ) + nonce_changes: Set[Tuple[Address, BlockAccessIndex, U64]] = field( + default_factory=set + ) + code_changes: Dict[Tuple[Address, BlockAccessIndex], Bytes] = field( + default_factory=dict + ) + + # Pre-state captures (transaction-scoped, only populated at tx frame) + pre_balances: Dict[Address, U256] = field(default_factory=dict) + pre_nonces: Dict[Address, U64] = field(default_factory=dict) + pre_storage: Dict[Tuple[Address, Bytes32], U256] = field( + default_factory=dict + ) + pre_code: Dict[Address, Bytes] = field(default_factory=dict) + + +def get_block_frame(state_changes: StateChanges) -> StateChanges: + """ + Walk to the root (block-level) frame. + + Parameters + ---------- + state_changes : + Any frame in the hierarchy. + + Returns + ------- + block_frame : StateChanges + The root block-level frame. + + """ + block_frame = state_changes + while block_frame.parent is not None: + block_frame = block_frame.parent + return block_frame + + +def increment_block_access_index(root_frame: StateChanges) -> None: + """ + Increment the block access index in the root frame. + + Parameters + ---------- + root_frame : + The root block-level frame. + + """ + root_frame.block_access_index = BlockAccessIndex( + root_frame.block_access_index + Uint(1) + ) + + +def get_transaction_frame(state_changes: StateChanges) -> StateChanges: + """ + Walk to the transaction-level frame (child of block frame). + + Parameters + ---------- + state_changes : + Any frame in the hierarchy. + + Returns + ------- + tx_frame : StateChanges + The transaction-level frame. + + """ + tx_frame = state_changes + while tx_frame.parent is not None and tx_frame.parent.parent is not None: + tx_frame = tx_frame.parent + return tx_frame + + +def capture_pre_balance( + tx_frame: StateChanges, address: Address, balance: U256 +) -> None: + """ + Capture pre-balance if not already captured (first-write-wins). + + Parameters + ---------- + tx_frame : + The transaction-level frame. + address : + The address whose balance to capture. + balance : + The current balance value. + + """ + if address not in tx_frame.pre_balances: + tx_frame.pre_balances[address] = balance + + +def capture_pre_nonce( + tx_frame: StateChanges, address: Address, nonce: U64 +) -> None: + """ + Capture pre-nonce if not already captured (first-write-wins). + + Parameters + ---------- + tx_frame : + The transaction-level frame. + address : + The address whose nonce to capture. + nonce : + The current nonce value. + + """ + if address not in tx_frame.pre_nonces: + tx_frame.pre_nonces[address] = nonce + + +def capture_pre_storage( + tx_frame: StateChanges, address: Address, key: Bytes32, value: U256 +) -> None: + """ + Capture pre-storage value if not already captured (first-write-wins). + + Parameters + ---------- + tx_frame : + The transaction-level frame. + address : + The address whose storage to capture. + key : + The storage key. + value : + The current storage value. + + """ + slot = (address, key) + if slot not in tx_frame.pre_storage: + tx_frame.pre_storage[slot] = value + + +def capture_pre_code( + tx_frame: StateChanges, address: Address, code: Bytes +) -> None: + """ + Capture pre-code if not already captured (first-write-wins). + + Parameters + ---------- + tx_frame : + The transaction-level frame. + address : + The address whose code to capture. + code : + The current code value. + + """ + if address not in tx_frame.pre_code: + tx_frame.pre_code[address] = code + + +def track_address(state_changes: StateChanges, address: Address) -> None: + """ + Record that an address was accessed. + + Parameters + ---------- + state_changes : + The state changes frame. + address : + The address that was accessed. + + """ + state_changes.touched_addresses.add(address) + + +def track_storage_read( + state_changes: StateChanges, address: Address, key: Bytes32 +) -> None: + """ + Record a storage read operation. + + Parameters + ---------- + state_changes : + The state changes frame. + address : + The address whose storage was read. + key : + The storage key that was read. + + """ + state_changes.storage_reads.add((address, key)) + + +def track_storage_write( + state_changes: StateChanges, + address: Address, + key: Bytes32, + value: U256, +) -> None: + """ + Record a storage write keyed by (address, key, block_access_index). + + Parameters + ---------- + state_changes : + The state changes frame. + address : + The address whose storage was written. + key : + The storage key that was written. + value : + The new storage value. + + """ + idx = state_changes.block_access_index + state_changes.storage_writes[(address, key, idx)] = value + + +def track_balance_change( + state_changes: StateChanges, + address: Address, + new_balance: U256, +) -> None: + """ + Record a balance change keyed by (address, block_access_index). + + Parameters + ---------- + state_changes : + The state changes frame. + address : + The address whose balance changed. + new_balance : + The new balance value. + + """ + idx = state_changes.block_access_index + state_changes.balance_changes[(address, idx)] = new_balance + + +def track_nonce_change( + state_changes: StateChanges, + address: Address, + new_nonce: U64, +) -> None: + """ + Record a nonce change as (address, block_access_index, new_nonce). + + Parameters + ---------- + state_changes : + The state changes frame. + address : + The address whose nonce changed. + new_nonce : + The new nonce value. + + """ + idx = state_changes.block_access_index + state_changes.nonce_changes.add((address, idx, new_nonce)) + + +def track_code_change( + state_changes: StateChanges, + address: Address, + new_code: Bytes, +) -> None: + """ + Record a code change keyed by (address, block_access_index). + + Parameters + ---------- + state_changes : + The state changes frame. + address : + The address whose code changed. + new_code : + The new code value. + + """ + idx = state_changes.block_access_index + state_changes.code_changes[(address, idx)] = new_code + + +def track_selfdestruct( + state_changes: StateChanges, + address: Address, +) -> None: + """ + Handle selfdestruct of account created in same transaction. + + Per EIP-7928/EIP-6780: removes nonce/code changes, converts storage + writes to reads. Balance changes handled by net-zero filtering. + + Parameters + ---------- + state_changes : + The state changes tracker. + address : + The address that self-destructed. + + """ + idx = state_changes.block_access_index + + # Remove nonce changes from current transaction + state_changes.nonce_changes = { + (addr, i, nonce) + for addr, i, nonce in state_changes.nonce_changes + if not (addr == address and i == idx) + } + + # Remove code changes from current transaction + if (address, idx) in state_changes.code_changes: + del state_changes.code_changes[(address, idx)] + + # Convert storage writes from current transaction to reads + for addr, key, i in list(state_changes.storage_writes.keys()): + if addr == address and i == idx: + del state_changes.storage_writes[(addr, key, i)] + state_changes.storage_reads.add((addr, key)) + + +def merge_on_success(child_frame: StateChanges) -> None: + """ + Merge child frame into parent on success. + + Child values overwrite parent values (most recent wins). No net-zero + filtering here - that happens once at transaction commit via + normalize_transaction(). + + Parameters + ---------- + child_frame : + The child frame being merged. + + """ + assert child_frame.parent is not None + parent_frame = child_frame.parent + + # Merge address accesses + parent_frame.touched_addresses.update(child_frame.touched_addresses) + + # Merge storage: reads union, writes overwrite (child supersedes parent) + parent_frame.storage_reads.update(child_frame.storage_reads) + for storage_key, storage_value in child_frame.storage_writes.items(): + parent_frame.storage_writes[storage_key] = storage_value + + # Merge balance changes: child overwrites parent for same key + for balance_key, balance_value in child_frame.balance_changes.items(): + parent_frame.balance_changes[balance_key] = balance_value + + # Merge nonce changes: keep highest nonce per address + address_final_nonces: Dict[Address, Tuple[BlockAccessIndex, U64]] = {} + for addr, idx, nonce in child_frame.nonce_changes: + if ( + addr not in address_final_nonces + or nonce > address_final_nonces[addr][1] + ): + address_final_nonces[addr] = (idx, nonce) + for addr, (idx, final_nonce) in address_final_nonces.items(): + parent_frame.nonce_changes.add((addr, idx, final_nonce)) + + # Merge code changes: child overwrites parent for same key + for code_key, code_value in child_frame.code_changes.items(): + parent_frame.code_changes[code_key] = code_value + + +def merge_on_failure(child_frame: StateChanges) -> None: + """ + Merge child frame into parent on failure/revert. + + Only reads merge; writes are discarded (converted to reads). + + Parameters + ---------- + child_frame : + The failed child frame. + + """ + assert child_frame.parent is not None + parent_frame = child_frame.parent + # Only merge reads and address accesses on failure + parent_frame.touched_addresses.update(child_frame.touched_addresses) + parent_frame.storage_reads.update(child_frame.storage_reads) + + # Convert writes to reads (failed writes still accessed the slots) + for address, key, _idx in child_frame.storage_writes.keys(): + parent_frame.storage_reads.add((address, key)) + + # Note: balance_changes, nonce_changes, and code_changes are NOT + # merged on failure - they are discarded + + +def commit_transaction_frame(tx_frame: StateChanges) -> None: + """ + Commit transaction frame to block frame. + + Unlike merge_on_success(), this merges ALL changes without net-zero + filtering (each tx's changes recorded at their respective index). + + Parameters + ---------- + tx_frame : + The transaction frame to commit. + + """ + assert tx_frame.parent is not None + block_frame = tx_frame.parent + + # Merge address accesses + block_frame.touched_addresses.update(tx_frame.touched_addresses) + + # Merge storage operations + block_frame.storage_reads.update(tx_frame.storage_reads) + for (addr, key, idx), value in tx_frame.storage_writes.items(): + block_frame.storage_writes[(addr, key, idx)] = value + + # Merge balance changes + for (addr, idx), final_balance in tx_frame.balance_changes.items(): + block_frame.balance_changes[(addr, idx)] = final_balance + + # Merge nonce changes + for addr, idx, nonce in tx_frame.nonce_changes: + block_frame.nonce_changes.add((addr, idx, nonce)) + + # Merge code changes (net-zero filtering done in normalize_transaction) + for (addr, idx), final_code in tx_frame.code_changes.items(): + block_frame.code_changes[(addr, idx)] = final_code + + +def create_child_frame(parent: StateChanges) -> StateChanges: + """ + Create a child frame linked to the given parent. + + Inherits block_access_index from parent so track functions can + access it directly without walking up the frame hierarchy. + + Parameters + ---------- + parent : + The parent frame. + + Returns + ------- + child : StateChanges + A new child frame with parent reference and inherited + block_access_index. + + """ + return StateChanges( + parent=parent, + block_access_index=parent.block_access_index, + ) + + +def filter_net_zero_frame_changes( + tx_frame: StateChanges, + state: "State", +) -> None: + """ + Filter net-zero changes from transaction frame before commit. + + Compares final values against pre-tx state for storage, balance, and code. + Net-zero storage writes are converted to reads. Net-zero balance/code + changes are removed entirely. Nonces are not filtered (only increment). + + Parameters + ---------- + tx_frame : + The transaction-level state changes frame. + state : + The current state to read final values from. + + """ + # Import locally to avoid circular import + from .state import get_account + + idx = tx_frame.block_access_index + + # Filter storage: compare against pre_storage, convert net-zero to reads + for addr, key, i in list(tx_frame.storage_writes.keys()): + if i != idx: + continue + final_value = tx_frame.storage_writes[(addr, key, i)] + if (addr, key) in tx_frame.pre_storage: + if tx_frame.pre_storage[(addr, key)] == final_value: + # Net-zero write - convert to read + del tx_frame.storage_writes[(addr, key, i)] + tx_frame.storage_reads.add((addr, key)) + + # Filter balance: compare pre vs post, remove if equal + addresses_to_check = [ + addr for (addr, i) in tx_frame.balance_changes.keys() if i == idx + ] + for addr in addresses_to_check: + if addr in tx_frame.pre_balances: + pre_balance = tx_frame.pre_balances[addr] + post_balance = get_account(state, addr).balance + if pre_balance == post_balance: + del tx_frame.balance_changes[(addr, idx)] + + # Filter code: compare pre vs post, remove if equal + for addr, i in list(tx_frame.code_changes.keys()): + if i != idx: + continue + final_code = tx_frame.code_changes[(addr, i)] + pre_code = tx_frame.pre_code.get(addr, b"") + if pre_code == final_code: + del tx_frame.code_changes[(addr, i)] + + # Nonces: no filtering needed (nonces only increment, never net-zero) diff --git a/src/ethereum/forks/amsterdam/transactions.py b/src/ethereum/forks/amsterdam/transactions.py new file mode 100644 index 0000000000..bea74819ef --- /dev/null +++ b/src/ethereum/forks/amsterdam/transactions.py @@ -0,0 +1,887 @@ +""" +Transactions are atomic units of work created externally to Ethereum and +submitted to be executed. If Ethereum is viewed as a state machine, +transactions are the events that move between states. +""" + +from dataclasses import dataclass +from typing import Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes0, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint, ulen + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import ( + InsufficientTransactionGasError, + InvalidSignatureError, + NonceOverflowError, +) + +from .exceptions import ( + InitCodeTooLargeError, + TransactionGasLimitExceededError, + TransactionTypeError, +) +from .fork_types import Address, Authorization, VersionedHash + +TX_BASE_COST = Uint(21000) +""" +Base cost of a transaction in gas units. This is the minimum amount of gas +required to execute a transaction. +""" + +FLOOR_CALLDATA_COST = Uint(10) +""" +Minimum gas cost per byte of calldata as per [EIP-7623]. Used to calculate +the minimum gas cost for transactions that include calldata. + +[EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 +""" + +STANDARD_CALLDATA_TOKEN_COST = Uint(4) +""" +Gas cost per byte of calldata as per [EIP-7623]. Used to calculate the +gas cost for transactions that include calldata. + +[EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 +""" + +TX_CREATE_COST = Uint(32000) +""" +Additional gas cost for creating a new contract. +""" + +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +""" +Gas cost for including an address in the access list of a transaction. +""" + +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) +""" +Gas cost for including a storage key in the access list of a transaction. +""" + +TX_MAX_GAS_LIMIT = Uint(16_777_216) + + +@slotted_freezable +@dataclass +class LegacyTransaction: + """ + Atomic operation performed on the block chain. This represents the original + transaction format used before [EIP-1559], [EIP-2930], [EIP-4844], + and [EIP-7702]. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + gas_price: Uint + """ + The price of gas for this transaction, in wei. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + v: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@slotted_freezable +@dataclass +class Access: + """ + A mapping from account address to storage slots that are pre-warmed as part + of a transaction. + """ + + account: Address + """ + The address of the account that is accessed. + """ + + slots: Tuple[Bytes32, ...] + """ + A tuple of storage slots that are accessed in the account. + """ + + +@slotted_freezable +@dataclass +class AccessListTransaction: + """ + The transaction type added in [EIP-2930] to support access lists. + + This transaction type extends the legacy transaction with an access list + and chain ID. The access list specifies which addresses and storage slots + the transaction will access. + + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + gas_price: Uint + """ + The price of gas for this transaction. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@slotted_freezable +@dataclass +class FeeMarketTransaction: + """ + The transaction type added in [EIP-1559]. + + This transaction type introduces a new fee market mechanism with two gas + price parameters: max_priority_fee_per_gas and max_fee_per_gas. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@slotted_freezable +@dataclass +class BlobTransaction: + """ + The transaction type added in [EIP-4844]. + + This transaction type extends the fee market transaction to support + blob-carrying transactions. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + max_fee_per_blob_gas: U256 + """ + The maximum fee per blob gas that the sender is willing to pay. + """ + + blob_versioned_hashes: Tuple[VersionedHash, ...] + """ + A tuple of objects that represent the versioned hashes of the blobs + included in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@slotted_freezable +@dataclass +class SetCodeTransaction: + """ + The transaction type added in [EIP-7702]. + + This transaction type allows Ethereum Externally Owned Accounts (EOAs) + to set code on their account, enabling them to act as smart contracts. + + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U64 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + authorizations: Tuple[Authorization, ...] + """ + A tuple of `Authorization` objects that specify what code the signer + desires to execute in the context of their EOA. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +Transaction = ( + LegacyTransaction + | AccessListTransaction + | FeeMarketTransaction + | BlobTransaction + | SetCodeTransaction +) +""" +Union type representing any valid transaction type. +""" + + +def encode_transaction(tx: Transaction) -> LegacyTransaction | Bytes: + """ + Encode a transaction into its RLP or typed transaction format. + Needed because non-legacy transactions aren't RLP. + + Legacy transactions are returned as-is, while other transaction types + are prefixed with their type identifier and RLP encoded. + """ + if isinstance(tx, LegacyTransaction): + return tx + elif isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(tx) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(tx) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(tx) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + rlp.encode(tx) + else: + raise Exception(f"Unable to encode transaction of type {type(tx)}") + + +def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: + """ + Decode a transaction from its RLP or typed transaction format. + Needed because non-legacy transactions aren't RLP. + + Legacy transactions are returned as-is, while other transaction types + are decoded based on their type identifier prefix. + """ + if isinstance(tx, Bytes): + if tx[0] == 1: + return rlp.decode_to(AccessListTransaction, tx[1:]) + elif tx[0] == 2: + return rlp.decode_to(FeeMarketTransaction, tx[1:]) + elif tx[0] == 3: + return rlp.decode_to(BlobTransaction, tx[1:]) + elif tx[0] == 4: + return rlp.decode_to(SetCodeTransaction, tx[1:]) + else: + raise TransactionTypeError(tx[0]) + else: + return tx + + +def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: + """ + Verifies a transaction. + + The gas in a transaction gets used to pay for the intrinsic cost of + operations, therefore if there is insufficient gas then it would not + be possible to execute a transaction and it will be declared invalid. + + Additionally, the nonce of a transaction must not equal or exceed the + limit defined in [EIP-2681]. + In practice, defining the limit as ``2**64-1`` has no impact because + sending ``2**64-1`` transactions is improbable. It's not strictly + impossible though, ``2**64-1`` transactions is the entire capacity of the + Ethereum blockchain at 2022 gas limits for a little over 22 years. + + Also, the code size of a contract creation transaction must be within + limits of the protocol. + + This function takes a transaction as a parameter and returns the intrinsic + gas cost and the minimum calldata gas cost for the transaction after + validation. It throws an `InsufficientTransactionGasError` exception if + the transaction does not provide enough gas to cover the intrinsic cost, + and a `NonceOverflowError` exception if the nonce is greater than + `2**64 - 2`. It also raises an `InitCodeTooLargeError` if the code size of + a contract creation transaction exceeds the maximum allowed size. + + [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 + [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 + """ + from .vm.interpreter import MAX_INIT_CODE_SIZE + + intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) + if max(intrinsic_gas, calldata_floor_gas_cost) > tx.gas: + raise InsufficientTransactionGasError("Insufficient gas") + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise NonceOverflowError("Nonce too high") + if tx.to == Bytes0(b"") and len(tx.data) > MAX_INIT_CODE_SIZE: + raise InitCodeTooLargeError("Code size too large") + if tx.gas > TX_MAX_GAS_LIMIT: + raise TransactionGasLimitExceededError("Gas limit too high") + + return intrinsic_gas, calldata_floor_gas_cost + + +def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: + """ + Calculates the gas that is charged before execution is started. + + The intrinsic cost of the transaction is charged before execution has + begun. Functions/operations in the EVM cost money to execute so this + intrinsic cost is for the operations that need to be paid for as part of + the transaction. Data transfer, for example, is part of this intrinsic + cost. It costs ether to send data over the wire and that ether is + accounted for in the intrinsic cost calculated in this function. This + intrinsic cost must be calculated and paid for before execution in order + for all operations to be implemented. + + The intrinsic cost includes: + 1. Base cost (`TX_BASE_COST`) + 2. Cost for data (zero and non-zero bytes) + 3. Cost for contract creation (if applicable) + 4. Cost for access list entries (if applicable) + 5. Cost for authorizations (if applicable) + + + This function takes a transaction as a parameter and returns the intrinsic + gas cost of the transaction and the minimum gas cost used by the + transaction based on the calldata size. + """ + from .vm.eoa_delegation import PER_EMPTY_ACCOUNT_COST + from .vm.gas import init_code_cost + + zero_bytes = 0 + for byte in tx.data: + if byte == 0: + zero_bytes += 1 + + tokens_in_calldata = Uint(zero_bytes + (len(tx.data) - zero_bytes) * 4) + # EIP-7623 floor price (note: no EVM costs) + calldata_floor_gas_cost = ( + tokens_in_calldata * FLOOR_CALLDATA_COST + TX_BASE_COST + ) + + data_cost = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST + + if tx.to == Bytes0(b""): + create_cost = TX_CREATE_COST + init_code_cost(ulen(tx.data)) + else: + create_cost = Uint(0) + + access_list_cost = Uint(0) + if isinstance( + tx, + ( + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + ), + ): + for access in tx.access_list: + access_list_cost += TX_ACCESS_LIST_ADDRESS_COST + access_list_cost += ( + ulen(access.slots) * TX_ACCESS_LIST_STORAGE_KEY_COST + ) + + auth_cost = Uint(0) + if isinstance(tx, SetCodeTransaction): + auth_cost += Uint(PER_EMPTY_ACCOUNT_COST * len(tx.authorizations)) + + return ( + Uint( + TX_BASE_COST + + data_cost + + create_cost + + access_list_cost + + auth_cost + ), + calldata_floor_gas_cost, + ) + + +def recover_sender(chain_id: U64, tx: Transaction) -> Address: + """ + Extracts the sender address from a transaction. + + The v, r, and s values are the three parts that make up the signature + of a transaction. In order to recover the sender of a transaction the two + components needed are the signature (``v``, ``r``, and ``s``) and the + signing hash of the transaction. The sender's public key can be obtained + with these two values and therefore the sender address can be retrieved. + + This function takes chain_id and a transaction as parameters and returns + the address of the sender of the transaction. It raises an + `InvalidSignatureError` if the signature values (r, s, v) are invalid. + """ + r, s = tx.r, tx.s + if U256(0) >= r or r >= SECP256K1N: + raise InvalidSignatureError("bad r") + if U256(0) >= s or s > SECP256K1N // U256(2): + raise InvalidSignatureError("bad s") + + if isinstance(tx, LegacyTransaction): + v = tx.v + if v == 27 or v == 28: + public_key = secp256k1_recover( + r, s, v - U256(27), signing_hash_pre155(tx) + ) + else: + chain_id_x2 = U256(chain_id) * U256(2) + if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: + raise InvalidSignatureError("bad v") + public_key = secp256k1_recover( + r, + s, + v - U256(35) - chain_id_x2, + signing_hash_155(tx, chain_id), + ) + elif isinstance(tx, AccessListTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_2930(tx) + ) + elif isinstance(tx, FeeMarketTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_1559(tx) + ) + elif isinstance(tx, BlobTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_4844(tx) + ) + elif isinstance(tx, SetCodeTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_7702(tx) + ) + + return Address(keccak256(public_key)[12:32]) + + +def signing_hash_pre155(tx: LegacyTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a legacy (pre [EIP-155]) + signature. + + This function takes a legacy transaction as a parameter and returns the + signing hash of the transaction. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + ) + ) + ) + + +def signing_hash_155(tx: LegacyTransaction, chain_id: U64) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-155] signature. + + This function takes a legacy transaction and a chain ID as parameters + and returns the hash of the transaction used in an [EIP-155] signature. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + chain_id, + Uint(0), + Uint(0), + ) + ) + ) + + +def signing_hash_2930(tx: AccessListTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-2930] signature. + + This function takes an access list transaction as a parameter + and returns the hash of the transaction used in an [EIP-2930] signature. + + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + """ + return keccak256( + b"\x01" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: + """ + Compute the hash of a transaction used in an [EIP-1559] signature. + + This function takes a fee market transaction as a parameter + and returns the hash of the transaction used in an [EIP-1559] signature. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + return keccak256( + b"\x02" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_4844(tx: BlobTransaction) -> Hash32: + """ + Compute the hash of a transaction used in an [EIP-4844] signature. + + This function takes a transaction as a parameter and returns the + signing hash of the transaction used in an [EIP-4844] signature. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + return keccak256( + b"\x03" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.max_fee_per_blob_gas, + tx.blob_versioned_hashes, + ) + ) + ) + + +def signing_hash_7702(tx: SetCodeTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-7702] signature. + + This function takes a transaction as a parameter and returns the + signing hash of the transaction used in a [EIP-7702] signature. + + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + return keccak256( + b"\x04" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.authorizations, + ) + ) + ) + + +def get_transaction_hash(tx: Bytes | LegacyTransaction) -> Hash32: + """ + Compute the hash of a transaction. + + This function takes a transaction as a parameter and returns the + keccak256 hash of the transaction. It can handle both legacy transactions + and typed transactions (`AccessListTransaction`, `FeeMarketTransaction`, + etc.). + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) diff --git a/src/ethereum/forks/amsterdam/trie.py b/src/ethereum/forks/amsterdam/trie.py new file mode 100644 index 0000000000..11a2e035ab --- /dev/null +++ b/src/ethereum/forks/amsterdam/trie.py @@ -0,0 +1,508 @@ +""" +State Trie. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state trie is the structure responsible for storing +`.fork_types.Account` objects. +""" + +import copy +from dataclasses import dataclass, field +from typing import ( + Callable, + Dict, + Generic, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + Tuple, + TypeVar, + cast, +) + +from ethereum_rlp import Extended, rlp +from ethereum_types.bytes import Bytes +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U256, Uint +from typing_extensions import assert_type + +from ethereum.crypto.hash import keccak256 +from ethereum.forks.osaka import trie as previous_trie +from ethereum.utils.hexadecimal import hex_to_bytes + +from .blocks import Receipt, Withdrawal +from .fork_types import Account, Address, Root, encode_account +from .transactions import LegacyTransaction + +# note: an empty trie (regardless of whether it is secured) has root: +# +# keccak256(RLP(b'')) +# == +# 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 # noqa: E501 +# +# also: +# +# keccak256(RLP(())) +# == +# 1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 # noqa: E501 +# +# which is the sha3Uncles hash in block header with no uncles +EMPTY_TRIE_ROOT = Root( + hex_to_bytes( + "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + ) +) + +Node = ( + Account + | Bytes + | LegacyTransaction + | Receipt + | Uint + | U256 + | Withdrawal + | None +) +K = TypeVar("K", bound=Bytes) +V = TypeVar( + "V", + Optional[Account], + Optional[Bytes], + Bytes, + Optional[LegacyTransaction | Bytes], + Optional[Receipt | Bytes], + Optional[Withdrawal | Bytes], + Uint, + U256, +) + + +@slotted_freezable +@dataclass +class LeafNode: + """Leaf node in the Merkle Trie.""" + + rest_of_key: Bytes + value: Extended + + +@slotted_freezable +@dataclass +class ExtensionNode: + """Extension node in the Merkle Trie.""" + + key_segment: Bytes + subnode: Extended + + +BranchSubnodes = Tuple[ + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, +] + + +@slotted_freezable +@dataclass +class BranchNode: + """Branch node in the Merkle Trie.""" + + subnodes: BranchSubnodes + value: Extended + + +InternalNode = LeafNode | ExtensionNode | BranchNode + + +def encode_internal_node(node: Optional[InternalNode]) -> Extended: + """ + Encodes a Merkle Trie node into its RLP form. The RLP will then be + serialized into a `Bytes` and hashed unless it is less that 32 bytes + when serialized. + + This function also accepts `None`, representing the absence of a node, + which is encoded to `b""`. + + Parameters + ---------- + node : Optional[InternalNode] + The node to encode. + + Returns + ------- + encoded : `Extended` + The node encoded as RLP. + + """ + unencoded: Extended + if node is None: + unencoded = b"" + elif isinstance(node, LeafNode): + unencoded = ( + nibble_list_to_compact(node.rest_of_key, True), + node.value, + ) + elif isinstance(node, ExtensionNode): + unencoded = ( + nibble_list_to_compact(node.key_segment, False), + node.subnode, + ) + elif isinstance(node, BranchNode): + unencoded = list(node.subnodes) + [node.value] + else: + raise AssertionError(f"Invalid internal node type {type(node)}!") + + encoded = rlp.encode(unencoded) + if len(encoded) < 32: + return unencoded + else: + return keccak256(encoded) + + +def encode_node(node: Node, storage_root: Optional[Bytes] = None) -> Bytes: + """ + Encode a Node for storage in the Merkle Trie. + + Currently mostly an unimplemented stub. + """ + if isinstance(node, Account): + assert storage_root is not None + return encode_account(node, storage_root) + elif isinstance(node, (LegacyTransaction, Receipt, Withdrawal, U256)): + return rlp.encode(node) + elif isinstance(node, Bytes): + return node + else: + return previous_trie.encode_node(node, storage_root) + + +@dataclass +class Trie(Generic[K, V]): + """ + The Merkle Trie. + """ + + secured: bool + default: V + _data: Dict[K, V] = field(default_factory=dict) + + +def copy_trie(trie: Trie[K, V]) -> Trie[K, V]: + """ + Create a copy of `trie`. Since only frozen objects may be stored in tries, + the contents are reused. + + Parameters + ---------- + trie: `Trie` + Trie to copy. + + Returns + ------- + new_trie : `Trie[K, V]` + A copy of the trie. + + """ + return Trie(trie.secured, trie.default, copy.copy(trie._data)) + + +def trie_set(trie: Trie[K, V], key: K, value: V) -> None: + """ + Stores an item in a Merkle Trie. + + This method deletes the key if `value == trie.default`, because the Merkle + Trie represents the default value by omitting it from the trie. + + Parameters + ---------- + trie: `Trie` + Trie to store in. + key : `Bytes` + Key to lookup. + value : `V` + Node to insert at `key`. + + """ + if value == trie.default: + if key in trie._data: + del trie._data[key] + else: + trie._data[key] = value + + +def trie_get(trie: Trie[K, V], key: K) -> V: + """ + Gets an item from the Merkle Trie. + + This method returns `trie.default` if the key is missing. + + Parameters + ---------- + trie: + Trie to lookup in. + key : + Key to lookup. + + Returns + ------- + node : `V` + Node at `key` in the trie. + + """ + return trie._data.get(key, trie.default) + + +def common_prefix_length(a: Sequence, b: Sequence) -> int: + """ + Find the longest common prefix of two sequences. + """ + for i in range(len(a)): + if i >= len(b) or a[i] != b[i]: + return i + return len(a) + + +def nibble_list_to_compact(x: Bytes, is_leaf: bool) -> Bytes: + """ + Compresses nibble-list into a standard byte array with a flag. + + A nibble-list is a list of byte values no greater than `15`. The flag is + encoded in high nibble of the highest byte. The flag nibble can be broken + down into two two-bit flags. + + Highest nibble:: + + +---+---+----------+--------+ + | _ | _ | is_leaf | parity | + +---+---+----------+--------+ + 3 2 1 0 + + + The lowest bit of the nibble encodes the parity of the length of the + remaining nibbles -- `0` when even and `1` when odd. The second lowest bit + is used to distinguish leaf and extension nodes. The other two bits are not + used. + + Parameters + ---------- + x : + Array of nibbles. + is_leaf : + True if this is part of a leaf node, or false if it is an extension + node. + + Returns + ------- + compressed : `bytearray` + Compact byte array. + + """ + compact = bytearray() + + if len(x) % 2 == 0: # ie even length + compact.append(16 * (2 * is_leaf)) + for i in range(0, len(x), 2): + compact.append(16 * x[i] + x[i + 1]) + else: + compact.append(16 * ((2 * is_leaf) + 1) + x[0]) + for i in range(1, len(x), 2): + compact.append(16 * x[i] + x[i + 1]) + + return Bytes(compact) + + +def bytes_to_nibble_list(bytes_: Bytes) -> Bytes: + """ + Converts a `Bytes` into to a sequence of nibbles (bytes with value < 16). + + Parameters + ---------- + bytes_: + The `Bytes` to convert. + + Returns + ------- + nibble_list : `Bytes` + The `Bytes` in nibble-list format. + + """ + nibble_list = bytearray(2 * len(bytes_)) + for byte_index, byte in enumerate(bytes_): + nibble_list[byte_index * 2] = (byte & 0xF0) >> 4 + nibble_list[byte_index * 2 + 1] = byte & 0x0F + return Bytes(nibble_list) + + +def _prepare_trie( + trie: Trie[K, V], + get_storage_root: Optional[Callable[[Address], Root]] = None, +) -> Mapping[Bytes, Bytes]: + """ + Prepares the trie for root calculation. Removes values that are empty, + hashes the keys (if `secured == True`) and encodes all the nodes. + + Parameters + ---------- + trie : + The `Trie` to prepare. + get_storage_root : + Function to get the storage root of an account. Needed to encode + `Account` objects. + + Returns + ------- + out : `Mapping[ethereum.base_types.Bytes, Node]` + Object with keys mapped to nibble-byte form. + + """ + mapped: MutableMapping[Bytes, Bytes] = {} + + for preimage, value in trie._data.items(): + if isinstance(value, Account): + assert get_storage_root is not None + address = Address(preimage) + encoded_value = encode_node(value, get_storage_root(address)) + else: + encoded_value = encode_node(value) + if encoded_value == b"": + raise AssertionError + key: Bytes + if trie.secured: + # "secure" tries hash keys once before construction + key = keccak256(preimage) + else: + key = preimage + mapped[bytes_to_nibble_list(key)] = encoded_value + + return mapped + + +def root( + trie: Trie[K, V], + get_storage_root: Optional[Callable[[Address], Root]] = None, +) -> Root: + """ + Computes the root of a modified merkle patricia trie (MPT). + + Parameters + ---------- + trie : + `Trie` to get the root of. + get_storage_root : + Function to get the storage root of an account. Needed to encode + `Account` objects. + + + Returns + ------- + root : `.fork_types.Root` + MPT root of the underlying key-value pairs. + + """ + obj = _prepare_trie(trie, get_storage_root) + + root_node = encode_internal_node(patricialize(obj, Uint(0))) + if len(rlp.encode(root_node)) < 32: + return keccak256(rlp.encode(root_node)) + else: + assert isinstance(root_node, Bytes) + return Root(root_node) + + +def patricialize( + obj: Mapping[Bytes, Bytes], level: Uint +) -> Optional[InternalNode]: + """ + Structural composition function. + + Used to recursively patricialize and merkleize a dictionary. Includes + memoization of the tree structure and hashes. + + Parameters + ---------- + obj : + Underlying trie key-value pairs, with keys in nibble-list format. + level : + Current trie level. + + Returns + ------- + node : `ethereum.base_types.Bytes` + Root node of `obj`. + + """ + if len(obj) == 0: + return None + + arbitrary_key = next(iter(obj)) + + # if leaf node + if len(obj) == 1: + leaf = LeafNode(arbitrary_key[level:], obj[arbitrary_key]) + return leaf + + # prepare for extension node check by finding max j such that all keys in + # obj have the same key[i:j] + substring = arbitrary_key[level:] + prefix_length = len(substring) + for key in obj: + prefix_length = min( + prefix_length, common_prefix_length(substring, key[level:]) + ) + + # finished searching, found another key at the current level + if prefix_length == 0: + break + + # if extension node + if prefix_length > 0: + prefix = arbitrary_key[int(level) : int(level) + prefix_length] + return ExtensionNode( + prefix, + encode_internal_node( + patricialize(obj, level + Uint(prefix_length)) + ), + ) + + branches: List[MutableMapping[Bytes, Bytes]] = [] + for _ in range(16): + branches.append({}) + value = b"" + for key in obj: + if len(key) == level: + # shouldn't ever have an account or receipt in an internal node + if isinstance(obj[key], (Account, Receipt, Uint)): + raise AssertionError + value = obj[key] + else: + branches[key[level]][key] = obj[key] + + subnodes = tuple( + encode_internal_node(patricialize(branches[k], level + Uint(1))) + for k in range(16) + ) + return BranchNode( + cast(BranchSubnodes, assert_type(subnodes, Tuple[Extended, ...])), + value, + ) diff --git a/src/ethereum/forks/amsterdam/utils/__init__.py b/src/ethereum/forks/amsterdam/utils/__init__.py new file mode 100644 index 0000000000..224a4d269b --- /dev/null +++ b/src/ethereum/forks/amsterdam/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Utility functions unique to this particular fork. +""" diff --git a/src/ethereum/forks/amsterdam/utils/address.py b/src/ethereum/forks/amsterdam/utils/address.py new file mode 100644 index 0000000000..270d562ca3 --- /dev/null +++ b/src/ethereum/forks/amsterdam/utils/address.py @@ -0,0 +1,94 @@ +""" +Hardfork Utility Functions For Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Address specific functions used in this amsterdam version of +specification. +""" + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.utils.byte import left_pad_zero_bytes + +from ..fork_types import Address + + +def to_address_masked(data: Uint | U256) -> Address: + """ + Convert a Uint or U256 value to a valid address (20 bytes). + + Parameters + ---------- + data : + The numeric value to be converted to address. + + Returns + ------- + address : `Address` + The obtained address. + + """ + return Address(data.to_be_bytes32()[-20:]) + + +def compute_contract_address(address: Address, nonce: Uint) -> Address: + """ + Computes address of the new account that needs to be created. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + nonce : + The transaction count of the account that wants to create the new + account. + + Returns + ------- + address: `Address` + The computed address of the new account. + + """ + computed_address = keccak256(rlp.encode([address, nonce])) + canonical_address = computed_address[-20:] + padded_address = left_pad_zero_bytes(canonical_address, 20) + return Address(padded_address) + + +def compute_create2_contract_address( + address: Address, salt: Bytes32, call_data: Bytes +) -> Address: + """ + Computes address of the new account that needs to be created, which is + based on the sender address, salt and the call data as well. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + salt : + Address generation salt. + call_data : + The code of the new account which is to be created. + + Returns + ------- + address: `ethereum.forks.amsterdam.fork_types.Address` + The computed address of the new account. + + """ + preimage = b"\xff" + address + salt + keccak256(call_data) + computed_address = keccak256(preimage) + canonical_address = computed_address[-20:] + padded_address = left_pad_zero_bytes(canonical_address, 20) + + return Address(padded_address) diff --git a/src/ethereum/forks/amsterdam/utils/hexadecimal.py b/src/ethereum/forks/amsterdam/utils/hexadecimal.py new file mode 100644 index 0000000000..23401e5d4f --- /dev/null +++ b/src/ethereum/forks/amsterdam/utils/hexadecimal.py @@ -0,0 +1,55 @@ +""" +Utility Functions For Hexadecimal Strings. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Hexadecimal utility functions used in this specification, specific to +Amsterdam types. +""" + +from ethereum_types.bytes import Bytes + +from ethereum.utils.hexadecimal import remove_hex_prefix + +from ..fork_types import Address, Root + + +def hex_to_root(hex_string: str) -> Root: + """ + Convert hex string to trie root. + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to trie root. + + Returns + ------- + root : `Root` + Trie root obtained from the given hexadecimal string. + + """ + return Root(Bytes.fromhex(remove_hex_prefix(hex_string))) + + +def hex_to_address(hex_string: str) -> Address: + """ + Convert hex string to Address (20 bytes). + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to Address. + + Returns + ------- + address : `Address` + The address obtained from the given hexadecimal string. + + """ + return Address(Bytes.fromhex(remove_hex_prefix(hex_string).rjust(40, "0"))) diff --git a/src/ethereum/forks/amsterdam/utils/message.py b/src/ethereum/forks/amsterdam/utils/message.py new file mode 100644 index 0000000000..130532fef6 --- /dev/null +++ b/src/ethereum/forks/amsterdam/utils/message.py @@ -0,0 +1,96 @@ +""" +Hardfork Utility Functions For The Message Data-structure. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Message specific functions used in this amsterdam version of +specification. +""" + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint + +from ..fork_types import Address +from ..state import get_account +from ..state_tracker import create_child_frame +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from .address import compute_contract_address + + +def prepare_message( + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, +) -> Message: + """ + Execute a transaction against the provided environment. + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. + + Returns + ------- + message: `ethereum.forks.amsterdam.vm.Message` + Items containing contract creation or message call specific data. + + """ + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): + current_target = compute_contract_address( + tx_env.origin, + get_account(block_env.state, tx_env.origin).nonce - Uint(1), + ) + msg_data = Bytes(b"") + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_account(block_env.state, tx.to).code + code_address = tx.to + else: + raise AssertionError("Target must be address or empty bytes") + + accessed_addresses.add(current_target) + + # Create call frame as child of transaction frame + call_frame = create_child_frame(tx_env.state_changes) + + return Message( + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, + data=msg_data, + code=code, + depth=Uint(0), + current_target=current_target, + code_address=code_address, + should_transfer_value=True, + is_static=False, + accessed_addresses=accessed_addresses, + accessed_storage_keys=set(tx_env.access_list_storage_keys), + disable_precompiles=False, + parent_evm=None, + is_create=isinstance(tx.to, Bytes0), + state_changes=call_frame, + ) diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py new file mode 100644 index 0000000000..6c47b50acf --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -0,0 +1,205 @@ +""" +Ethereum Virtual Machine (EVM). + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The abstract computer which runs the code stored in an +`.fork_types.Account`. +""" + +from dataclasses import dataclass, field +from typing import List, Optional, Set, Tuple + +from ethereum_types.bytes import Bytes, Bytes0, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException + +from ..block_access_lists.rlp_types import BlockAccessList +from ..blocks import Log, Receipt, Withdrawal +from ..fork_types import Address, Authorization, VersionedHash +from ..state import State, TransientStorage +from ..state_tracker import StateChanges, merge_on_failure, merge_on_success +from ..transactions import LegacyTransaction +from ..trie import Trie + +__all__ = ("Environment", "Evm", "Message") + + +@dataclass +class BlockEnvironment: + """ + Items external to the virtual machine itself, provided by the environment. + """ + + chain_id: U64 + state: State + block_gas_limit: Uint + block_hashes: List[Hash32] + coinbase: Address + number: Uint + base_fee_per_gas: Uint + time: U256 + prev_randao: Bytes32 + excess_blob_gas: U64 + parent_beacon_block_root: Hash32 + state_changes: StateChanges + + +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + receipt_keys : + Key of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + withdrawals_trie : `ethereum.fork_types.Root` + Trie root of all the withdrawals in the block. + blob_gas_used : `ethereum.base_types.U64` + Total blob gas used in the block. + requests : `Bytes` + Hash of all the requests in the block. + block_access_list: `BlockAccessList` + The block access list for the block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Bytes | LegacyTransaction]] = ( + field(default_factory=lambda: Trie(secured=False, default=None)) + ) + receipts_trie: Trie[Bytes, Optional[Bytes | Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipt_keys: Tuple[Bytes, ...] = field(default_factory=tuple) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + withdrawals_trie: Trie[Bytes, Optional[Bytes | Withdrawal]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + blob_gas_used: U64 = U64(0) + requests: List[Bytes] = field(default_factory=list) + block_access_list: BlockAccessList = field(default_factory=list) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + transient_storage: TransientStorage + blob_versioned_hashes: Tuple[VersionedHash, ...] + authorizations: Tuple[Authorization, ...] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] + state_changes: "StateChanges" = field(default_factory=StateChanges) + + +@dataclass +class Message: + """ + Items that are used by contract creation or message call. + """ + + block_env: BlockEnvironment + tx_env: TransactionEnvironment + caller: Address + target: Bytes0 | Address + current_target: Address + gas: Uint + value: U256 + data: Bytes + code_address: Optional[Address] + code: Bytes + depth: Uint + should_transfer_value: bool + is_static: bool + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + disable_precompiles: bool + parent_evm: Optional["Evm"] + is_create: bool + state_changes: "StateChanges" = field(default_factory=StateChanges) + + +@dataclass +class Evm: + """The internal state of the virtual machine.""" + + pc: Uint + stack: List[U256] + memory: bytearray + code: Bytes + gas_left: Uint + valid_jump_destinations: Set[Uint] + logs: Tuple[Log, ...] + refund_counter: int + running: bool + message: Message + output: Bytes + accounts_to_delete: Set[Address] + return_data: Bytes + error: Optional[EthereumException] + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + state_changes: StateChanges + + +def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of a successful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + + """ + evm.gas_left += child_evm.gas_left + evm.logs += child_evm.logs + evm.refund_counter += child_evm.refund_counter + evm.accounts_to_delete.update(child_evm.accounts_to_delete) + evm.accessed_addresses.update(child_evm.accessed_addresses) + evm.accessed_storage_keys.update(child_evm.accessed_storage_keys) + + merge_on_success(child_evm.state_changes) + + +def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of an unsuccessful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + + """ + evm.gas_left += child_evm.gas_left + + merge_on_failure(child_evm.state_changes) diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py new file mode 100644 index 0000000000..1f1aac9d97 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -0,0 +1,277 @@ +""" +Set EOA account code. +""" + +from typing import Optional, Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import keccak256 +from ethereum.exceptions import InvalidBlock, InvalidSignatureError + +from ..fork_types import Address, Authorization +from ..state import ( + account_exists, + get_account, + increment_nonce, + set_authority_code, +) +from ..state_tracker import ( + capture_pre_code, + track_address, + track_code_change, + track_nonce_change, +) +from ..utils.hexadecimal import hex_to_address +from ..vm.gas import GAS_COLD_ACCOUNT_ACCESS, GAS_WARM_ACCESS +from . import Evm, Message + +SET_CODE_TX_MAGIC = b"\x05" +EOA_DELEGATION_MARKER = b"\xef\x01\x00" +EOA_DELEGATION_MARKER_LENGTH = len(EOA_DELEGATION_MARKER) +EOA_DELEGATED_CODE_LENGTH = 23 +PER_EMPTY_ACCOUNT_COST = 25000 +PER_AUTH_BASE_COST = 12500 +NULL_ADDRESS = hex_to_address("0x0000000000000000000000000000000000000000") + + +def is_valid_delegation(code: bytes) -> bool: + """ + Whether the code is a valid delegation designation. + + Parameters + ---------- + code: `bytes` + The code to check. + + Returns + ------- + valid : `bool` + True if the code is a valid delegation designation, + False otherwise. + + """ + if ( + len(code) == EOA_DELEGATED_CODE_LENGTH + and code[:EOA_DELEGATION_MARKER_LENGTH] == EOA_DELEGATION_MARKER + ): + return True + return False + + +def get_delegated_code_address(code: bytes) -> Optional[Address]: + """ + Get the address to which the code delegates. + + Parameters + ---------- + code: `bytes` + The code to get the address from. + + Returns + ------- + address : `Optional[Address]` + The address of the delegated code. + + """ + if is_valid_delegation(code): + return Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + return None + + +def recover_authority(authorization: Authorization) -> Address: + """ + Recover the authority address from the authorization. + + Parameters + ---------- + authorization + The authorization to recover the authority from. + + Raises + ------ + InvalidSignatureError + If the signature is invalid. + + Returns + ------- + authority : `Address` + The recovered authority address. + + """ + y_parity, r, s = authorization.y_parity, authorization.r, authorization.s + if y_parity not in (0, 1): + raise InvalidSignatureError("Invalid y_parity in authorization") + if U256(0) >= r or r >= SECP256K1N: + raise InvalidSignatureError("Invalid r value in authorization") + if U256(0) >= s or s > SECP256K1N // U256(2): + raise InvalidSignatureError("Invalid s value in authorization") + + signing_hash = keccak256( + SET_CODE_TX_MAGIC + + rlp.encode( + ( + authorization.chain_id, + authorization.address, + authorization.nonce, + ) + ) + ) + + public_key = secp256k1_recover(r, s, U256(y_parity), signing_hash) + return Address(keccak256(public_key)[12:32]) + + +def calculate_delegation_cost( + evm: Evm, address: Address +) -> Tuple[bool, Address, Optional[Address], Uint]: + """ + Check if address has delegation and calculate delegation target gas cost. + + This function reads the original account's code to check for delegation + and tracks it in state_changes. It calculates the delegation target's + gas cost but does NOT read the delegation target yet. + + Parameters + ---------- + evm : `Evm` + The execution frame. + address : `Address` + The address to check for delegation. + + Returns + ------- + delegation_info : `Tuple[bool, Address, Optional[Address], Uint]` + (is_delegated, original_address, delegated_address_or_none, + delegation_gas_cost) + + """ + state = evm.message.block_env.state + + code = get_account(state, address).code + track_address(evm.state_changes, address) + + if not is_valid_delegation(code): + return False, address, None, Uint(0) + + delegated_address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + + # Calculate gas cost for delegation target access + if delegated_address in evm.accessed_addresses: + delegation_gas_cost = GAS_WARM_ACCESS + else: + delegation_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + return True, address, delegated_address, delegation_gas_cost + + +def read_delegation_target(evm: Evm, delegated_address: Address) -> Bytes: + """ + Read the delegation target's code and track the access. + + Should ONLY be called AFTER verifying we have gas for the access. + + This function: + 1. Reads the delegation target's code from state + 2. Adds it to accessed_addresses (if not already there) + 3. Tracks it in state_changes for BAL + + Parameters + ---------- + evm : `Evm` + The execution frame. + delegated_address : `Address` + The delegation target address. + + Returns + ------- + code : `Bytes` + The delegation target's code. + + """ + state = evm.message.block_env.state + + if delegated_address not in evm.accessed_addresses: + evm.accessed_addresses.add(delegated_address) + + track_address(evm.state_changes, delegated_address) + + return get_account(state, delegated_address).code + + +def set_delegation(message: Message) -> U256: + """ + Set the delegation code for the authorities in the message. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + refund_counter: `U256` + Refund from authority which already exists in state. + + """ + state = message.block_env.state + refund_counter = U256(0) + for auth in message.tx_env.authorizations: + if auth.chain_id not in (message.block_env.chain_id, U256(0)): + continue + + if auth.nonce >= U64.MAX_VALUE: + continue + + try: + authority = recover_authority(auth) + except InvalidSignatureError: + continue + + message.accessed_addresses.add(authority) + + authority_account = get_account(state, authority) + authority_code = authority_account.code + + track_address(message.tx_env.state_changes, authority) + + if authority_code and not is_valid_delegation(authority_code): + continue + + authority_nonce = authority_account.nonce + if authority_nonce != auth.nonce: + continue + + if account_exists(state, authority): + refund_counter += U256(PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST) + + if auth.address == NULL_ADDRESS: + code_to_set = b"" + else: + code_to_set = EOA_DELEGATION_MARKER + auth.address + + tx_frame = message.tx_env.state_changes + # EIP-7928: Capture pre-code before any changes + capture_pre_code(tx_frame, authority, authority_code) + + set_authority_code(state, authority, code_to_set) + + if authority_code != code_to_set: + # Track code change if different from current + track_code_change(tx_frame, authority, code_to_set) + + increment_nonce(state, authority) + nonce_after = get_account(state, authority).nonce + track_nonce_change(tx_frame, authority, U64(nonce_after)) + + if message.code_address is None: + raise InvalidBlock("Invalid type 4 transaction: no target") + + message.code = get_account(state, message.code_address).code + + return refund_counter diff --git a/src/ethereum/forks/amsterdam/vm/exceptions.py b/src/ethereum/forks/amsterdam/vm/exceptions.py new file mode 100644 index 0000000000..4bf3cee405 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/exceptions.py @@ -0,0 +1,139 @@ +""" +Ethereum Virtual Machine (EVM) Exceptions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Exceptions which cause the EVM to halt exceptionally. +""" + +from ethereum.exceptions import EthereumException + + +class ExceptionalHalt(EthereumException): + """ + Indicates that the EVM has experienced an exceptional halt. This causes + execution to immediately end with all gas being consumed. + """ + + +class Revert(EthereumException): + """ + Raised by the `REVERT` opcode. + + Unlike other EVM exceptions this does not result in the consumption of all + gas. + """ + + pass + + +class StackUnderflowError(ExceptionalHalt): + """ + Occurs when a pop is executed on an empty stack. + """ + + pass + + +class StackOverflowError(ExceptionalHalt): + """ + Occurs when a push is executed on a stack at max capacity. + """ + + pass + + +class OutOfGasError(ExceptionalHalt): + """ + Occurs when an operation costs more than the amount of gas left in the + frame. + """ + + pass + + +class InvalidOpcode(ExceptionalHalt): + """ + Raised when an invalid opcode is encountered. + """ + + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code + + +class InvalidJumpDestError(ExceptionalHalt): + """ + Occurs when the destination of a jump operation doesn't meet any of the + following criteria. + + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + """ + + +class StackDepthLimitError(ExceptionalHalt): + """ + Raised when the message depth is greater than `1024`. + """ + + pass + + +class WriteInStaticContext(ExceptionalHalt): + """ + Raised when an attempt is made to modify the state while operating inside + of a STATICCALL context. + """ + + pass + + +class OutOfBoundsRead(ExceptionalHalt): + """ + Raised when an attempt was made to read data beyond the + boundaries of the buffer. + """ + + pass + + +class InvalidParameter(ExceptionalHalt): + """ + Raised when invalid parameters are passed. + """ + + pass + + +class InvalidContractPrefix(ExceptionalHalt): + """ + Raised when the new contract code starts with 0xEF. + """ + + pass + + +class AddressCollision(ExceptionalHalt): + """ + Raised when the new contract address has a collision. + """ + + pass + + +class KZGProofError(ExceptionalHalt): + """ + Raised when the point evaluation precompile can't verify a proof. + """ + + pass diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py new file mode 100644 index 0000000000..22a022a289 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -0,0 +1,413 @@ +""" +Ethereum Virtual Machine (EVM) Gas. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM gas constants and calculators. +""" + +from dataclasses import dataclass +from typing import List, Tuple + +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.numeric import ceil32, taylor_exponential + +from ..blocks import Header +from ..transactions import BlobTransaction, Transaction +from . import Evm +from .exceptions import OutOfGasError + +GAS_JUMPDEST = Uint(1) +GAS_BASE = Uint(2) +GAS_VERY_LOW = Uint(3) +GAS_STORAGE_SET = Uint(20000) +GAS_STORAGE_UPDATE = Uint(5000) +GAS_STORAGE_CLEAR_REFUND = Uint(4800) +GAS_LOW = Uint(5) +GAS_MID = Uint(8) +GAS_HIGH = Uint(10) +GAS_EXPONENTIATION = Uint(10) +GAS_EXPONENTIATION_PER_BYTE = Uint(50) +GAS_MEMORY = Uint(3) +GAS_KECCAK256 = Uint(30) +GAS_KECCAK256_WORD = Uint(6) +GAS_COPY = Uint(3) +GAS_BLOCK_HASH = Uint(20) +GAS_LOG = Uint(375) +GAS_LOG_DATA = Uint(8) +GAS_LOG_TOPIC = Uint(375) +GAS_CREATE = Uint(32000) +GAS_CODE_DEPOSIT = Uint(200) +GAS_ZERO = Uint(0) +GAS_NEW_ACCOUNT = Uint(25000) +GAS_CALL_VALUE = Uint(9000) +GAS_CALL_STIPEND = Uint(2300) +GAS_SELF_DESTRUCT = Uint(5000) +GAS_SELF_DESTRUCT_NEW_ACCOUNT = Uint(25000) +GAS_ECRECOVER = Uint(3000) +GAS_P256VERIFY = Uint(6900) +GAS_SHA256 = Uint(60) +GAS_SHA256_WORD = Uint(12) +GAS_RIPEMD160 = Uint(600) +GAS_RIPEMD160_WORD = Uint(120) +GAS_IDENTITY = Uint(15) +GAS_IDENTITY_WORD = Uint(3) +GAS_RETURN_DATA_COPY = Uint(3) +GAS_FAST_STEP = Uint(5) +GAS_BLAKE2_PER_ROUND = Uint(1) +GAS_COLD_SLOAD = Uint(2100) +GAS_COLD_ACCOUNT_ACCESS = Uint(2600) +GAS_WARM_ACCESS = Uint(100) +GAS_INIT_CODE_WORD_COST = Uint(2) +GAS_BLOBHASH_OPCODE = Uint(3) +GAS_POINT_EVALUATION = Uint(50000) + +GAS_PER_BLOB = U64(2**17) +BLOB_SCHEDULE_TARGET = U64(6) +TARGET_BLOB_GAS_PER_BLOCK = GAS_PER_BLOB * BLOB_SCHEDULE_TARGET +BLOB_BASE_COST = Uint(2**13) +BLOB_SCHEDULE_MAX = U64(9) +MIN_BLOB_GASPRICE = Uint(1) +BLOB_BASE_FEE_UPDATE_FRACTION = Uint(5007716) + +GAS_BLS_G1_ADD = Uint(375) +GAS_BLS_G1_MUL = Uint(12000) +GAS_BLS_G1_MAP = Uint(5500) +GAS_BLS_G2_ADD = Uint(600) +GAS_BLS_G2_MUL = Uint(22500) +GAS_BLS_G2_MAP = Uint(23800) + + +@dataclass +class ExtendMemory: + """ + Define the parameters for memory extension in opcodes. + + `cost`: `ethereum.base_types.Uint` + The gas required to perform the extension + `expand_by`: `ethereum.base_types.Uint` + The size by which the memory will be extended + """ + + cost: Uint + expand_by: Uint + + +@dataclass +class MessageCallGas: + """ + Define the gas cost and gas given to the sub-call for + executing the call opcodes. + + `cost`: `ethereum.base_types.Uint` + The gas required to execute the call opcode, excludes + memory expansion costs. + `sub_call`: `ethereum.base_types.Uint` + The portion of gas available to sub-calls that is refundable + if not consumed. + """ + + cost: Uint + sub_call: Uint + + +def check_gas(evm: Evm, amount: Uint) -> None: + """ + Checks if `amount` gas is available without charging it. + Raises OutOfGasError if insufficient gas. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of gas to check. + + """ + if evm.gas_left < amount: + raise OutOfGasError + + +def charge_gas(evm: Evm, amount: Uint) -> None: + """ + Subtracts `amount` from `evm.gas_left`. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of gas the current operation requires. + + """ + evm_trace(evm, GasAndRefund(int(amount))) + + if evm.gas_left < amount: + raise OutOfGasError + else: + evm.gas_left -= amount + + +def calculate_memory_gas_cost(size_in_bytes: Uint) -> Uint: + """ + Calculates the gas cost for allocating memory + to the smallest multiple of 32 bytes, + such that the allocated size is at least as big as the given size. + + Parameters + ---------- + size_in_bytes : + The size of the data in bytes. + + Returns + ------- + total_gas_cost : `ethereum.base_types.Uint` + The gas cost for storing data in memory. + + """ + size_in_words = ceil32(size_in_bytes) // Uint(32) + linear_cost = size_in_words * GAS_MEMORY + quadratic_cost = size_in_words ** Uint(2) // Uint(512) + total_gas_cost = linear_cost + quadratic_cost + try: + return total_gas_cost + except ValueError as e: + raise OutOfGasError from e + + +def calculate_gas_extend_memory( + memory: bytearray, extensions: List[Tuple[U256, U256]] +) -> ExtendMemory: + """ + Calculates the gas amount to extend memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + extensions: + List of extensions to be made to the memory. + Consists of a tuple of start position and size. + + Returns + ------- + extend_memory: `ExtendMemory` + + """ + size_to_extend = Uint(0) + to_be_paid = Uint(0) + current_size = Uint(len(memory)) + for start_position, size in extensions: + if size == 0: + continue + before_size = ceil32(current_size) + after_size = ceil32(Uint(start_position) + Uint(size)) + if after_size <= before_size: + continue + + size_to_extend += after_size - before_size + already_paid = calculate_memory_gas_cost(before_size) + total_cost = calculate_memory_gas_cost(after_size) + to_be_paid += total_cost - already_paid + + current_size = after_size + + return ExtendMemory(to_be_paid, size_to_extend) + + +def calculate_message_call_gas( + value: U256, + gas: Uint, + gas_left: Uint, + memory_cost: Uint, + extra_gas: Uint, + call_stipend: Uint = GAS_CALL_STIPEND, +) -> MessageCallGas: + """ + Calculates the MessageCallGas (cost and gas made available to the sub-call) + for executing call Opcodes. + + Parameters + ---------- + value: + The amount of `ETH` that needs to be transferred. + gas : + The amount of gas provided to the message-call. + gas_left : + The amount of gas left in the current frame. + memory_cost : + The amount needed to extend the memory in the current frame. + extra_gas : + The amount of gas needed for transferring value + creating a new + account inside a message call. + call_stipend : + The amount of stipend provided to a message call to execute code while + transferring value(ETH). + + Returns + ------- + message_call_gas: `MessageCallGas` + + """ + call_stipend = Uint(0) if value == 0 else call_stipend + if gas_left < extra_gas + memory_cost: + return MessageCallGas(gas + extra_gas, gas + call_stipend) + + gas = min(gas, max_message_call_gas(gas_left - memory_cost - extra_gas)) + + return MessageCallGas(gas + extra_gas, gas + call_stipend) + + +def max_message_call_gas(gas: Uint) -> Uint: + """ + Calculates the maximum gas that is allowed for making a message call. + + Parameters + ---------- + gas : + The amount of gas provided to the message-call. + + Returns + ------- + max_allowed_message_call_gas: `ethereum.base_types.Uint` + The maximum gas allowed for making the message-call. + + """ + return gas - (gas // Uint(64)) + + +def init_code_cost(init_code_length: Uint) -> Uint: + """ + Calculates the gas to be charged for the init code in CREATE* + opcodes as well as create transactions. + + Parameters + ---------- + init_code_length : + The length of the init code provided to the opcode + or a create transaction + + Returns + ------- + init_code_gas: `ethereum.base_types.Uint` + The gas to be charged for the init code. + + """ + return GAS_INIT_CODE_WORD_COST * ceil32(init_code_length) // Uint(32) + + +def calculate_excess_blob_gas(parent_header: Header) -> U64: + """ + Calculated the excess blob gas for the current block based + on the gas used in the parent block. + + Parameters + ---------- + parent_header : + The parent block of the current block. + + Returns + ------- + excess_blob_gas: `ethereum.base_types.U64` + The excess blob gas for the current block. + + """ + # At the fork block, these are defined as zero. + excess_blob_gas = U64(0) + blob_gas_used = U64(0) + base_fee_per_gas = Uint(0) + + if isinstance(parent_header, Header): + # After the fork block, read them from the parent header. + excess_blob_gas = parent_header.excess_blob_gas + blob_gas_used = parent_header.blob_gas_used + base_fee_per_gas = parent_header.base_fee_per_gas + + parent_blob_gas = excess_blob_gas + blob_gas_used + if parent_blob_gas < TARGET_BLOB_GAS_PER_BLOCK: + return U64(0) + + target_blob_gas_price = Uint(GAS_PER_BLOB) + target_blob_gas_price *= calculate_blob_gas_price(excess_blob_gas) + + base_blob_tx_price = BLOB_BASE_COST * base_fee_per_gas + if base_blob_tx_price > target_blob_gas_price: + blob_schedule_delta = BLOB_SCHEDULE_MAX - BLOB_SCHEDULE_TARGET + return ( + excess_blob_gas + + blob_gas_used * blob_schedule_delta // BLOB_SCHEDULE_MAX + ) + + return parent_blob_gas - TARGET_BLOB_GAS_PER_BLOCK + + +def calculate_total_blob_gas(tx: Transaction) -> U64: + """ + Calculate the total blob gas for a transaction. + + Parameters + ---------- + tx : + The transaction for which the blob gas is to be calculated. + + Returns + ------- + total_blob_gas: `ethereum.base_types.Uint` + The total blob gas for the transaction. + + """ + if isinstance(tx, BlobTransaction): + return GAS_PER_BLOB * U64(len(tx.blob_versioned_hashes)) + else: + return U64(0) + + +def calculate_blob_gas_price(excess_blob_gas: U64) -> Uint: + """ + Calculate the blob gasprice for a block. + + Parameters + ---------- + excess_blob_gas : + The excess blob gas for the block. + + Returns + ------- + blob_gasprice: `Uint` + The blob gasprice. + + """ + return taylor_exponential( + MIN_BLOB_GASPRICE, + Uint(excess_blob_gas), + BLOB_BASE_FEE_UPDATE_FRACTION, + ) + + +def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: + """ + Calculate the blob data fee for a transaction. + + Parameters + ---------- + excess_blob_gas : + The excess_blob_gas for the execution. + tx : + The transaction for which the blob data fee is to be calculated. + + Returns + ------- + data_fee: `Uint` + The blob data fee. + + """ + return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( + excess_blob_gas + ) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/__init__.py b/src/ethereum/forks/amsterdam/vm/instructions/__init__.py new file mode 100644 index 0000000000..0da72c8ea5 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/__init__.py @@ -0,0 +1,367 @@ +""" +EVM Instruction Encoding (Opcodes). + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Machine readable representations of EVM instructions, and a mapping to their +implementations. +""" + +import enum +from typing import Callable, Dict + +from . import arithmetic as arithmetic_instructions +from . import bitwise as bitwise_instructions +from . import block as block_instructions +from . import comparison as comparison_instructions +from . import control_flow as control_flow_instructions +from . import environment as environment_instructions +from . import keccak as keccak_instructions +from . import log as log_instructions +from . import memory as memory_instructions +from . import stack as stack_instructions +from . import storage as storage_instructions +from . import system as system_instructions + + +class Ops(enum.Enum): + """ + Enum for EVM Opcodes. + """ + + # Arithmetic Ops + ADD = 0x01 + MUL = 0x02 + SUB = 0x03 + DIV = 0x04 + SDIV = 0x05 + MOD = 0x06 + SMOD = 0x07 + ADDMOD = 0x08 + MULMOD = 0x09 + EXP = 0x0A + SIGNEXTEND = 0x0B + + # Comparison Ops + LT = 0x10 + GT = 0x11 + SLT = 0x12 + SGT = 0x13 + EQ = 0x14 + ISZERO = 0x15 + + # Bitwise Ops + AND = 0x16 + OR = 0x17 + XOR = 0x18 + NOT = 0x19 + BYTE = 0x1A + SHL = 0x1B + SHR = 0x1C + SAR = 0x1D + CLZ = 0x1E + + # Keccak Op + KECCAK = 0x20 + + # Environmental Ops + ADDRESS = 0x30 + BALANCE = 0x31 + ORIGIN = 0x32 + CALLER = 0x33 + CALLVALUE = 0x34 + CALLDATALOAD = 0x35 + CALLDATASIZE = 0x36 + CALLDATACOPY = 0x37 + CODESIZE = 0x38 + CODECOPY = 0x39 + GASPRICE = 0x3A + EXTCODESIZE = 0x3B + EXTCODECOPY = 0x3C + RETURNDATASIZE = 0x3D + RETURNDATACOPY = 0x3E + EXTCODEHASH = 0x3F + + # Block Ops + BLOCKHASH = 0x40 + COINBASE = 0x41 + TIMESTAMP = 0x42 + NUMBER = 0x43 + PREVRANDAO = 0x44 + GASLIMIT = 0x45 + CHAINID = 0x46 + SELFBALANCE = 0x47 + BASEFEE = 0x48 + BLOBHASH = 0x49 + BLOBBASEFEE = 0x4A + + # Control Flow Ops + STOP = 0x00 + JUMP = 0x56 + JUMPI = 0x57 + PC = 0x58 + GAS = 0x5A + JUMPDEST = 0x5B + + # Storage Ops + SLOAD = 0x54 + SSTORE = 0x55 + TLOAD = 0x5C + TSTORE = 0x5D + + # Pop Operation + POP = 0x50 + + # Push Operations + PUSH0 = 0x5F + PUSH1 = 0x60 + PUSH2 = 0x61 + PUSH3 = 0x62 + PUSH4 = 0x63 + PUSH5 = 0x64 + PUSH6 = 0x65 + PUSH7 = 0x66 + PUSH8 = 0x67 + PUSH9 = 0x68 + PUSH10 = 0x69 + PUSH11 = 0x6A + PUSH12 = 0x6B + PUSH13 = 0x6C + PUSH14 = 0x6D + PUSH15 = 0x6E + PUSH16 = 0x6F + PUSH17 = 0x70 + PUSH18 = 0x71 + PUSH19 = 0x72 + PUSH20 = 0x73 + PUSH21 = 0x74 + PUSH22 = 0x75 + PUSH23 = 0x76 + PUSH24 = 0x77 + PUSH25 = 0x78 + PUSH26 = 0x79 + PUSH27 = 0x7A + PUSH28 = 0x7B + PUSH29 = 0x7C + PUSH30 = 0x7D + PUSH31 = 0x7E + PUSH32 = 0x7F + + # Dup operations + DUP1 = 0x80 + DUP2 = 0x81 + DUP3 = 0x82 + DUP4 = 0x83 + DUP5 = 0x84 + DUP6 = 0x85 + DUP7 = 0x86 + DUP8 = 0x87 + DUP9 = 0x88 + DUP10 = 0x89 + DUP11 = 0x8A + DUP12 = 0x8B + DUP13 = 0x8C + DUP14 = 0x8D + DUP15 = 0x8E + DUP16 = 0x8F + + # Swap operations + SWAP1 = 0x90 + SWAP2 = 0x91 + SWAP3 = 0x92 + SWAP4 = 0x93 + SWAP5 = 0x94 + SWAP6 = 0x95 + SWAP7 = 0x96 + SWAP8 = 0x97 + SWAP9 = 0x98 + SWAP10 = 0x99 + SWAP11 = 0x9A + SWAP12 = 0x9B + SWAP13 = 0x9C + SWAP14 = 0x9D + SWAP15 = 0x9E + SWAP16 = 0x9F + + # Memory Operations + MLOAD = 0x51 + MSTORE = 0x52 + MSTORE8 = 0x53 + MSIZE = 0x59 + MCOPY = 0x5E + + # Log Operations + LOG0 = 0xA0 + LOG1 = 0xA1 + LOG2 = 0xA2 + LOG3 = 0xA3 + LOG4 = 0xA4 + + # System Operations + CREATE = 0xF0 + CALL = 0xF1 + CALLCODE = 0xF2 + RETURN = 0xF3 + DELEGATECALL = 0xF4 + CREATE2 = 0xF5 + STATICCALL = 0xFA + REVERT = 0xFD + SELFDESTRUCT = 0xFF + + +op_implementation: Dict[Ops, Callable] = { + Ops.STOP: control_flow_instructions.stop, + Ops.ADD: arithmetic_instructions.add, + Ops.MUL: arithmetic_instructions.mul, + Ops.SUB: arithmetic_instructions.sub, + Ops.DIV: arithmetic_instructions.div, + Ops.SDIV: arithmetic_instructions.sdiv, + Ops.MOD: arithmetic_instructions.mod, + Ops.SMOD: arithmetic_instructions.smod, + Ops.ADDMOD: arithmetic_instructions.addmod, + Ops.MULMOD: arithmetic_instructions.mulmod, + Ops.EXP: arithmetic_instructions.exp, + Ops.SIGNEXTEND: arithmetic_instructions.signextend, + Ops.LT: comparison_instructions.less_than, + Ops.GT: comparison_instructions.greater_than, + Ops.SLT: comparison_instructions.signed_less_than, + Ops.SGT: comparison_instructions.signed_greater_than, + Ops.EQ: comparison_instructions.equal, + Ops.ISZERO: comparison_instructions.is_zero, + Ops.AND: bitwise_instructions.bitwise_and, + Ops.OR: bitwise_instructions.bitwise_or, + Ops.XOR: bitwise_instructions.bitwise_xor, + Ops.NOT: bitwise_instructions.bitwise_not, + Ops.BYTE: bitwise_instructions.get_byte, + Ops.SHL: bitwise_instructions.bitwise_shl, + Ops.SHR: bitwise_instructions.bitwise_shr, + Ops.SAR: bitwise_instructions.bitwise_sar, + Ops.CLZ: bitwise_instructions.count_leading_zeros, + Ops.KECCAK: keccak_instructions.keccak, + Ops.SLOAD: storage_instructions.sload, + Ops.BLOCKHASH: block_instructions.block_hash, + Ops.COINBASE: block_instructions.coinbase, + Ops.TIMESTAMP: block_instructions.timestamp, + Ops.NUMBER: block_instructions.number, + Ops.PREVRANDAO: block_instructions.prev_randao, + Ops.GASLIMIT: block_instructions.gas_limit, + Ops.CHAINID: block_instructions.chain_id, + Ops.MLOAD: memory_instructions.mload, + Ops.MSTORE: memory_instructions.mstore, + Ops.MSTORE8: memory_instructions.mstore8, + Ops.MSIZE: memory_instructions.msize, + Ops.MCOPY: memory_instructions.mcopy, + Ops.ADDRESS: environment_instructions.address, + Ops.BALANCE: environment_instructions.balance, + Ops.ORIGIN: environment_instructions.origin, + Ops.CALLER: environment_instructions.caller, + Ops.CALLVALUE: environment_instructions.callvalue, + Ops.CALLDATALOAD: environment_instructions.calldataload, + Ops.CALLDATASIZE: environment_instructions.calldatasize, + Ops.CALLDATACOPY: environment_instructions.calldatacopy, + Ops.CODESIZE: environment_instructions.codesize, + Ops.CODECOPY: environment_instructions.codecopy, + Ops.GASPRICE: environment_instructions.gasprice, + Ops.EXTCODESIZE: environment_instructions.extcodesize, + Ops.EXTCODECOPY: environment_instructions.extcodecopy, + Ops.RETURNDATASIZE: environment_instructions.returndatasize, + Ops.RETURNDATACOPY: environment_instructions.returndatacopy, + Ops.EXTCODEHASH: environment_instructions.extcodehash, + Ops.SELFBALANCE: environment_instructions.self_balance, + Ops.BASEFEE: environment_instructions.base_fee, + Ops.BLOBHASH: environment_instructions.blob_hash, + Ops.BLOBBASEFEE: environment_instructions.blob_base_fee, + Ops.SSTORE: storage_instructions.sstore, + Ops.TLOAD: storage_instructions.tload, + Ops.TSTORE: storage_instructions.tstore, + Ops.JUMP: control_flow_instructions.jump, + Ops.JUMPI: control_flow_instructions.jumpi, + Ops.PC: control_flow_instructions.pc, + Ops.GAS: control_flow_instructions.gas_left, + Ops.JUMPDEST: control_flow_instructions.jumpdest, + Ops.POP: stack_instructions.pop, + Ops.PUSH0: stack_instructions.push0, + Ops.PUSH1: stack_instructions.push1, + Ops.PUSH2: stack_instructions.push2, + Ops.PUSH3: stack_instructions.push3, + Ops.PUSH4: stack_instructions.push4, + Ops.PUSH5: stack_instructions.push5, + Ops.PUSH6: stack_instructions.push6, + Ops.PUSH7: stack_instructions.push7, + Ops.PUSH8: stack_instructions.push8, + Ops.PUSH9: stack_instructions.push9, + Ops.PUSH10: stack_instructions.push10, + Ops.PUSH11: stack_instructions.push11, + Ops.PUSH12: stack_instructions.push12, + Ops.PUSH13: stack_instructions.push13, + Ops.PUSH14: stack_instructions.push14, + Ops.PUSH15: stack_instructions.push15, + Ops.PUSH16: stack_instructions.push16, + Ops.PUSH17: stack_instructions.push17, + Ops.PUSH18: stack_instructions.push18, + Ops.PUSH19: stack_instructions.push19, + Ops.PUSH20: stack_instructions.push20, + Ops.PUSH21: stack_instructions.push21, + Ops.PUSH22: stack_instructions.push22, + Ops.PUSH23: stack_instructions.push23, + Ops.PUSH24: stack_instructions.push24, + Ops.PUSH25: stack_instructions.push25, + Ops.PUSH26: stack_instructions.push26, + Ops.PUSH27: stack_instructions.push27, + Ops.PUSH28: stack_instructions.push28, + Ops.PUSH29: stack_instructions.push29, + Ops.PUSH30: stack_instructions.push30, + Ops.PUSH31: stack_instructions.push31, + Ops.PUSH32: stack_instructions.push32, + Ops.DUP1: stack_instructions.dup1, + Ops.DUP2: stack_instructions.dup2, + Ops.DUP3: stack_instructions.dup3, + Ops.DUP4: stack_instructions.dup4, + Ops.DUP5: stack_instructions.dup5, + Ops.DUP6: stack_instructions.dup6, + Ops.DUP7: stack_instructions.dup7, + Ops.DUP8: stack_instructions.dup8, + Ops.DUP9: stack_instructions.dup9, + Ops.DUP10: stack_instructions.dup10, + Ops.DUP11: stack_instructions.dup11, + Ops.DUP12: stack_instructions.dup12, + Ops.DUP13: stack_instructions.dup13, + Ops.DUP14: stack_instructions.dup14, + Ops.DUP15: stack_instructions.dup15, + Ops.DUP16: stack_instructions.dup16, + Ops.SWAP1: stack_instructions.swap1, + Ops.SWAP2: stack_instructions.swap2, + Ops.SWAP3: stack_instructions.swap3, + Ops.SWAP4: stack_instructions.swap4, + Ops.SWAP5: stack_instructions.swap5, + Ops.SWAP6: stack_instructions.swap6, + Ops.SWAP7: stack_instructions.swap7, + Ops.SWAP8: stack_instructions.swap8, + Ops.SWAP9: stack_instructions.swap9, + Ops.SWAP10: stack_instructions.swap10, + Ops.SWAP11: stack_instructions.swap11, + Ops.SWAP12: stack_instructions.swap12, + Ops.SWAP13: stack_instructions.swap13, + Ops.SWAP14: stack_instructions.swap14, + Ops.SWAP15: stack_instructions.swap15, + Ops.SWAP16: stack_instructions.swap16, + Ops.LOG0: log_instructions.log0, + Ops.LOG1: log_instructions.log1, + Ops.LOG2: log_instructions.log2, + Ops.LOG3: log_instructions.log3, + Ops.LOG4: log_instructions.log4, + Ops.CREATE: system_instructions.create, + Ops.RETURN: system_instructions.return_, + Ops.CALL: system_instructions.call, + Ops.CALLCODE: system_instructions.callcode, + Ops.DELEGATECALL: system_instructions.delegatecall, + Ops.SELFDESTRUCT: system_instructions.selfdestruct, + Ops.STATICCALL: system_instructions.staticcall, + Ops.REVERT: system_instructions.revert, + Ops.CREATE2: system_instructions.create2, +} diff --git a/src/ethereum/forks/amsterdam/vm/instructions/arithmetic.py b/src/ethereum/forks/amsterdam/vm/instructions/arithmetic.py new file mode 100644 index 0000000000..de5d6ab43d --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/arithmetic.py @@ -0,0 +1,373 @@ +""" +Ethereum Virtual Machine (EVM) Arithmetic Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Arithmetic instructions. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import get_sign + +from .. import Evm +from ..gas import ( + GAS_EXPONENTIATION, + GAS_EXPONENTIATION_PER_BYTE, + GAS_LOW, + GAS_MID, + GAS_VERY_LOW, + charge_gas, +) +from ..stack import pop, push + + +def add(evm: Evm) -> None: + """ + Adds the top two elements of the stack together, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = x.wrapping_add(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sub(evm: Evm) -> None: + """ + Subtracts the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = x.wrapping_sub(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mul(evm: Evm) -> None: + """ + Multiply the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + result = x.wrapping_mul(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def div(evm: Evm) -> None: + """ + Integer division of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + dividend = pop(evm.stack) + divisor = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if divisor == 0: + quotient = U256(0) + else: + quotient = dividend // divisor + + push(evm.stack, quotient) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +U255_CEIL_VALUE = 2**255 + + +def sdiv(evm: Evm) -> None: + """ + Signed integer division of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + dividend = pop(evm.stack).to_signed() + divisor = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if divisor == 0: + quotient = 0 + elif dividend == -U255_CEIL_VALUE and divisor == -1: + quotient = -U255_CEIL_VALUE + else: + sign = get_sign(dividend * divisor) + quotient = sign * (abs(dividend) // abs(divisor)) + + push(evm.stack, U256.from_signed(quotient)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mod(evm: Evm) -> None: + """ + Modulo remainder of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if y == 0: + remainder = U256(0) + else: + remainder = x % y + + push(evm.stack, remainder) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def smod(evm: Evm) -> None: + """ + Signed modulo remainder of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack).to_signed() + y = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if y == 0: + remainder = 0 + else: + remainder = get_sign(x) * (abs(x) % abs(y)) + + push(evm.stack, U256.from_signed(remainder)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def addmod(evm: Evm) -> None: + """ + Modulo addition of the top 2 elements with the 3rd element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_MID) + + # OPERATION + if z == 0: + result = U256(0) + else: + result = U256((x + y) % z) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mulmod(evm: Evm) -> None: + """ + Modulo multiplication of the top 2 elements with the 3rd element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_MID) + + # OPERATION + if z == 0: + result = U256(0) + else: + result = U256((x * y) % z) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def exp(evm: Evm) -> None: + """ + Exponential operation of the top 2 elements. Pushes the result back on + the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + base = Uint(pop(evm.stack)) + exponent = Uint(pop(evm.stack)) + + # GAS + # This is equivalent to 1 + floor(log(y, 256)). But in python the log + # function is inaccurate leading to wrong results. + exponent_bits = exponent.bit_length() + exponent_bytes = (exponent_bits + Uint(7)) // Uint(8) + charge_gas( + evm, GAS_EXPONENTIATION + GAS_EXPONENTIATION_PER_BYTE * exponent_bytes + ) + + # OPERATION + result = U256(pow(base, exponent, Uint(U256.MAX_VALUE) + Uint(1))) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signextend(evm: Evm) -> None: + """ + Sign extend operation. In other words, extend a signed number which + fits in N bytes to 32 bytes. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + byte_num = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + if byte_num > U256(31): + # Can't extend any further + result = value + else: + # U256(0).to_be_bytes() gives b'' instead b'\x00'. + value_bytes = Bytes(value.to_be_bytes32()) + # Now among the obtained value bytes, consider only + # N `least significant bytes`, where N is `byte_num + 1`. + value_bytes = value_bytes[31 - int(byte_num) :] + sign_bit = value_bytes[0] >> 7 + if sign_bit == 0: + result = U256.from_be_bytes(value_bytes) + else: + num_bytes_prepend = U256(32) - (byte_num + U256(1)) + result = U256.from_be_bytes( + bytearray([0xFF] * num_bytes_prepend) + value_bytes + ) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/bitwise.py b/src/ethereum/forks/amsterdam/vm/instructions/bitwise.py new file mode 100644 index 0000000000..cc6fa2fbb2 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/bitwise.py @@ -0,0 +1,274 @@ +""" +Ethereum Virtual Machine (EVM) Bitwise Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM bitwise instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import GAS_LOW, GAS_VERY_LOW, charge_gas +from ..stack import pop, push + + +def bitwise_and(evm: Evm) -> None: + """ + Bitwise AND operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + push(evm.stack, x & y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_or(evm: Evm) -> None: + """ + Bitwise OR operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + push(evm.stack, x | y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_xor(evm: Evm) -> None: + """ + Bitwise XOR operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + push(evm.stack, x ^ y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_not(evm: Evm) -> None: + """ + Bitwise NOT operation of the top element of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + push(evm.stack, ~x) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def get_byte(evm: Evm) -> None: + """ + For a word (defined by next top element of the stack), retrieve the + Nth byte (0-indexed and defined by top element of stack) from the + left (most significant) to right (least significant). + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + byte_index = pop(evm.stack) + word = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if byte_index >= U256(32): + result = U256(0) + else: + extra_bytes_to_right = U256(31) - byte_index + # Remove the extra bytes in the right + word = word >> (extra_bytes_to_right * U256(8)) + # Remove the extra bytes in the left + word = word & U256(0xFF) + result = word + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_shl(evm: Evm) -> None: + """ + Logical shift left (SHL) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = Uint(pop(evm.stack)) + value = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if shift < Uint(256): + result = U256((value << shift) & Uint(U256.MAX_VALUE)) + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_shr(evm: Evm) -> None: + """ + Logical shift right (SHR) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if shift < U256(256): + result = value >> shift + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_sar(evm: Evm) -> None: + """ + Arithmetic shift right (SAR) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = int(pop(evm.stack)) + signed_value = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + if shift < 256: + result = U256.from_signed(signed_value >> shift) + elif signed_value >= 0: + result = U256(0) + else: + result = U256.MAX_VALUE + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def count_leading_zeros(evm: Evm) -> None: + """ + Count the number of leading zero bits in a 256-bit word. + + Pops one value from the stack and pushes the number of leading zero bits. + If the input is zero, pushes 256. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_LOW) + + # OPERATION + bit_length = U256(x.bit_length()) + result = U256(256) - bit_length + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/block.py b/src/ethereum/forks/amsterdam/vm/instructions/block.py new file mode 100644 index 0000000000..78783751dd --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/block.py @@ -0,0 +1,261 @@ +""" +Ethereum Virtual Machine (EVM) Block Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM block instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import GAS_BASE, GAS_BLOCK_HASH, charge_gas +from ..stack import pop, push + + +def block_hash(evm: Evm) -> None: + """ + Push the hash of one of the 256 most recent complete blocks onto the + stack. The block number to hash is present at the top of the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.StackUnderflowError` + If `len(stack)` is less than `1`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `20`. + + """ + # STACK + block_number = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_BLOCK_HASH) + + # OPERATION + max_block_number = block_number + Uint(256) + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): + # Default hash to 0, if the block of interest is not yet on the chain + # (including the block which has the current executing transaction), + # or if the block's age is more than 256. + current_block_hash = b"\x00" + else: + current_block_hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] + + push(evm.stack, U256.from_be_bytes(current_block_hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def coinbase(evm: Evm) -> None: + """ + Push the current block's beneficiary address (address of the block miner) + onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def timestamp(evm: Evm) -> None: + """ + Push the current block's timestamp onto the stack. Here the timestamp + being referred is actually the unix timestamp in seconds. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, evm.message.block_env.time) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def number(evm: Evm) -> None: + """ + Push the current block's number onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.number)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def prev_randao(evm: Evm) -> None: + """ + Push the `prev_randao` value onto the stack. + + The `prev_randao` value is the random output of the beacon chain's + randomness oracle for the previous block. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.block_env.prev_randao)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gas_limit(evm: Evm) -> None: + """ + Push the current block's gas limit onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def chain_id(evm: Evm) -> None: + """ + Push the chain id onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.chain_id)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/comparison.py b/src/ethereum/forks/amsterdam/vm/instructions/comparison.py new file mode 100644 index 0000000000..a6a3d99bc8 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/comparison.py @@ -0,0 +1,177 @@ +""" +Ethereum Virtual Machine (EVM) Comparison Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Comparison instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import GAS_VERY_LOW, charge_gas +from ..stack import pop, push + + +def less_than(evm: Evm) -> None: + """ + Checks if the top element is less than the next top element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left < right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signed_less_than(evm: Evm) -> None: + """ + Signed less-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left < right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def greater_than(evm: Evm) -> None: + """ + Checks if the top element is greater than the next top element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left > right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signed_greater_than(evm: Evm) -> None: + """ + Signed greater-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left > right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def equal(evm: Evm) -> None: + """ + Checks if the top element is equal to the next top element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(left == right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def is_zero(evm: Evm) -> None: + """ + Checks if the top element is equal to 0. Pushes the result back on the + stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + result = U256(x == 0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/control_flow.py b/src/ethereum/forks/amsterdam/vm/instructions/control_flow.py new file mode 100644 index 0000000000..b3b1f2316a --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/control_flow.py @@ -0,0 +1,171 @@ +""" +Ethereum Virtual Machine (EVM) Control Flow Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM control flow instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ...vm.gas import GAS_BASE, GAS_HIGH, GAS_JUMPDEST, GAS_MID, charge_gas +from .. import Evm +from ..exceptions import InvalidJumpDestError +from ..stack import pop, push + + +def stop(evm: Evm) -> None: + """ + Stop further execution of EVM code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + pass + + # OPERATION + evm.running = False + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def jump(evm: Evm) -> None: + """ + Alter the program counter to the location specified by the top of the + stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + jump_dest = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_MID) + + # OPERATION + if jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + + # PROGRAM COUNTER + evm.pc = Uint(jump_dest) + + +def jumpi(evm: Evm) -> None: + """ + Alter the program counter to the specified location if and only if a + condition is true. If the condition is not true, then the program counter + would increase only by 1. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + jump_dest = Uint(pop(evm.stack)) + conditional_value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_HIGH) + + # OPERATION + if conditional_value == 0: + destination = evm.pc + Uint(1) + elif jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + else: + destination = jump_dest + + # PROGRAM COUNTER + evm.pc = destination + + +def pc(evm: Evm) -> None: + """ + Push onto the stack the value of the program counter after reaching the + current instruction and without increasing it for the next instruction. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.pc)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gas_left(evm: Evm) -> None: + """ + Push the amount of available gas (including the corresponding reduction + for the cost of this instruction) onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.gas_left)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def jumpdest(evm: Evm) -> None: + """ + Mark a valid destination for jumps. This is a noop, present only + to be used by `JUMP` and `JUMPI` opcodes to verify that their jump is + valid. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_JUMPDEST) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/environment.py b/src/ethereum/forks/amsterdam/vm/instructions/environment.py new file mode 100644 index 0000000000..3d23b8f136 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/environment.py @@ -0,0 +1,617 @@ +""" +Ethereum Virtual Machine (EVM) Environmental Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM environment related instructions. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U256, Uint, ulen + +from ethereum.crypto.hash import keccak256 +from ethereum.utils.numeric import ceil32 + +# track_address_access removed - now using state_changes.track_address() +from ...fork_types import EMPTY_ACCOUNT +from ...state import get_account +from ...state_tracker import track_address +from ...utils.address import to_address_masked +from ...vm.memory import buffer_read, memory_write +from .. import Evm +from ..exceptions import OutOfBoundsRead +from ..gas import ( + GAS_BASE, + GAS_BLOBHASH_OPCODE, + GAS_COLD_ACCOUNT_ACCESS, + GAS_COPY, + GAS_FAST_STEP, + GAS_RETURN_DATA_COPY, + GAS_VERY_LOW, + GAS_WARM_ACCESS, + calculate_blob_gas_price, + calculate_gas_extend_memory, + charge_gas, + check_gas, +) +from ..stack import pop, push + + +def address(evm: Evm) -> None: + """ + Pushes the address of the current executing account to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.current_target)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def balance(evm: Evm) -> None: + """ + Pushes the balance of the given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + is_cold_access = address not in evm.accessed_addresses + gas_cost = GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS + check_gas(evm, gas_cost) + if is_cold_access: + evm.accessed_addresses.add(address) + track_address(evm.state_changes, address) + charge_gas(evm, gas_cost) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + state = evm.message.block_env.state + balance = get_account(state, address).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def origin(evm: Evm) -> None: + """ + Pushes the address of the original transaction sender to the stack. + The origin address can only be an EOA. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def caller(evm: Evm) -> None: + """ + Pushes the address of the caller onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.caller)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def callvalue(evm: Evm) -> None: + """ + Push the value (in wei) sent with the call onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, evm.message.value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldataload(evm: Evm) -> None: + """ + Push a word (32 bytes) of the input data belonging to the current + environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_index = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + value = buffer_read(evm.message.data, start_index, U256(32)) + + push(evm.stack, U256.from_be_bytes(value)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldatasize(evm: Evm) -> None: + """ + Push the size of input data in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.message.data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldatacopy(evm: Evm) -> None: + """ + Copy a portion of the input data in current environment to memory. + + This will also expand the memory, in case that the memory is insufficient + to store the data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + data_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + copy_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = buffer_read(evm.message.data, data_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def codesize(evm: Evm) -> None: + """ + Push the size of code running in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.code))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def codecopy(evm: Evm) -> None: + """ + Copy a portion of the code in current environment to memory. + + This will also expand the memory, in case that the memory is insufficient + to store the data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + code_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + copy_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = buffer_read(evm.code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gasprice(evm: Evm) -> None: + """ + Push the gas price used in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.message.tx_env.gas_price)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodesize(evm: Evm) -> None: + """ + Push the code size of a given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + is_cold_access = address not in evm.accessed_addresses + access_gas_cost = ( + GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS + ) + check_gas(evm, access_gas_cost) + if is_cold_access: + evm.accessed_addresses.add(address) + track_address(evm.state_changes, address) + charge_gas(evm, access_gas_cost) + + # OPERATION + state = evm.message.block_env.state + code = get_account(state, address).code + + codesize = U256(len(code)) + push(evm.stack, codesize) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodecopy(evm: Evm) -> None: + """ + Copy a portion of an account's code to memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + memory_start_index = pop(evm.stack) + code_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + is_cold_access = address not in evm.accessed_addresses + access_gas_cost = ( + GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS + ) + total_gas_cost = access_gas_cost + copy_gas_cost + extend_memory.cost + + check_gas(evm, total_gas_cost) + if is_cold_access: + evm.accessed_addresses.add(address) + track_address(evm.state_changes, address) + charge_gas(evm, total_gas_cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + state = evm.message.block_env.state + code = get_account(state, address).code + + value = buffer_read(code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def returndatasize(evm: Evm) -> None: + """ + Pushes the size of the return data buffer onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.return_data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def returndatacopy(evm: Evm) -> None: + """ + Copies data from the return data buffer code to memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + return_data_start_position = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GAS_RETURN_DATA_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + copy_gas_cost + extend_memory.cost) + if Uint(return_data_start_position) + Uint(size) > ulen(evm.return_data): + raise OutOfBoundsRead + + evm.memory += b"\x00" * extend_memory.expand_by + value = evm.return_data[ + return_data_start_position : return_data_start_position + size + ] + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodehash(evm: Evm) -> None: + """ + Returns the keccak256 hash of a contract’s bytecode. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + is_cold_access = address not in evm.accessed_addresses + access_gas_cost = ( + GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS + ) + check_gas(evm, access_gas_cost) + if is_cold_access: + evm.accessed_addresses.add(address) + track_address(evm.state_changes, address) + charge_gas(evm, access_gas_cost) + + # OPERATION + state = evm.message.block_env.state + account = get_account(state, address) + + if account == EMPTY_ACCOUNT: + codehash = U256(0) + else: + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) + + push(evm.stack, codehash) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def self_balance(evm: Evm) -> None: + """ + Pushes the balance of the current address to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_FAST_STEP) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def base_fee(evm: Evm) -> None: + """ + Pushes the base fee of the current block on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def blob_hash(evm: Evm) -> None: + """ + Pushes the versioned hash at a particular index on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + index = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_BLOBHASH_OPCODE) + + # OPERATION + if int(index) < len(evm.message.tx_env.blob_versioned_hashes): + blob_hash = evm.message.tx_env.blob_versioned_hashes[index] + else: + blob_hash = Bytes32(b"\x00" * 32) + push(evm.stack, U256.from_be_bytes(blob_hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def blob_base_fee(evm: Evm) -> None: + """ + Pushes the blob base fee on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + blob_base_fee = calculate_blob_gas_price( + evm.message.block_env.excess_blob_gas + ) + push(evm.stack, U256(blob_base_fee)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/keccak.py b/src/ethereum/forks/amsterdam/vm/instructions/keccak.py new file mode 100644 index 0000000000..44ba2eb40b --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/keccak.py @@ -0,0 +1,63 @@ +""" +Ethereum Virtual Machine (EVM) Keccak Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM keccak instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.utils.numeric import ceil32 + +from .. import Evm +from ..gas import ( + GAS_KECCAK256, + GAS_KECCAK256_WORD, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes +from ..stack import pop, push + + +def keccak(evm: Evm) -> None: + """ + Pushes to the stack the Keccak-256 hash of a region of memory. + + This also expands the memory, in case the memory is insufficient to + access the data's memory location. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + word_gas_cost = GAS_KECCAK256_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_KECCAK256 + word_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + data = memory_read_bytes(evm.memory, memory_start_index, size) + hashed = keccak256(data) + + push(evm.stack, U256.from_be_bytes(hashed)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/log.py b/src/ethereum/forks/amsterdam/vm/instructions/log.py new file mode 100644 index 0000000000..a6e95b3170 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/log.py @@ -0,0 +1,88 @@ +""" +Ethereum Virtual Machine (EVM) Logging Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM logging instructions. +""" + +from functools import partial + +from ethereum_types.numeric import Uint + +from ...blocks import Log +from .. import Evm +from ..exceptions import WriteInStaticContext +from ..gas import ( + GAS_LOG, + GAS_LOG_DATA, + GAS_LOG_TOPIC, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes +from ..stack import pop + + +def log_n(evm: Evm, num_topics: int) -> None: + """ + Appends a log entry, having `num_topics` topics, to the evm logs. + + This will also expand the memory if the data (required by the log entry) + corresponding to the memory is not accessible. + + Parameters + ---------- + evm : + The current EVM frame. + num_topics : + The number of topics to be included in the log entry. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + topics = [] + for _ in range(num_topics): + topic = pop(evm.stack).to_be_bytes32() + topics.append(topic) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GAS_LOG + + GAS_LOG_DATA * Uint(size) + + GAS_LOG_TOPIC * Uint(num_topics) + + extend_memory.cost, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + if evm.message.is_static: + raise WriteInStaticContext + log_entry = Log( + address=evm.message.current_target, + topics=tuple(topics), + data=memory_read_bytes(evm.memory, memory_start_index, size), + ) + + evm.logs = evm.logs + (log_entry,) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +log0 = partial(log_n, num_topics=0) +log1 = partial(log_n, num_topics=1) +log2 = partial(log_n, num_topics=2) +log3 = partial(log_n, num_topics=3) +log4 = partial(log_n, num_topics=4) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/memory.py b/src/ethereum/forks/amsterdam/vm/instructions/memory.py new file mode 100644 index 0000000000..631d33a7fd --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/memory.py @@ -0,0 +1,177 @@ +""" +Ethereum Virtual Machine (EVM) Memory Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Memory instructions. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import ceil32 + +from .. import Evm +from ..gas import ( + GAS_BASE, + GAS_COPY, + GAS_VERY_LOW, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def mstore(evm: Evm) -> None: + """ + Stores a word to memory. + This also expands the memory, if the memory is + insufficient to store the word. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + value = pop(evm.stack).to_be_bytes32() + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(len(value)))] + ) + + charge_gas(evm, GAS_VERY_LOW + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + memory_write(evm.memory, start_position, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mstore8(evm: Evm) -> None: + """ + Stores a byte to memory. + This also expands the memory, if the memory is + insufficient to store the word. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + value = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(1))] + ) + + charge_gas(evm, GAS_VERY_LOW + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + normalized_bytes_value = Bytes([value & U256(0xFF)]) + memory_write(evm.memory, start_position, normalized_bytes_value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mload(evm: Evm) -> None: + """ + Load word from memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(32))] + ) + charge_gas(evm, GAS_VERY_LOW + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = U256.from_be_bytes( + memory_read_bytes(evm.memory, start_position, U256(32)) + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def msize(evm: Evm) -> None: + """ + Push the size of active memory in bytes onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.memory))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mcopy(evm: Evm) -> None: + """ + Copy the bytes in memory from one location to another. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + destination = pop(evm.stack) + source = pop(evm.stack) + length = pop(evm.stack) + + # GAS + words = ceil32(Uint(length)) // Uint(32) + copy_gas_cost = GAS_COPY * words + + extend_memory = calculate_gas_extend_memory( + evm.memory, [(source, length), (destination, length)] + ) + charge_gas(evm, GAS_VERY_LOW + copy_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = memory_read_bytes(evm.memory, source, length) + memory_write(evm.memory, destination, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/stack.py b/src/ethereum/forks/amsterdam/vm/instructions/stack.py new file mode 100644 index 0000000000..e381b52c37 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/stack.py @@ -0,0 +1,208 @@ +""" +Ethereum Virtual Machine (EVM) Stack Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM stack related instructions. +""" + +from functools import partial + +from ethereum_types.numeric import U256, Uint + +from .. import Evm, stack +from ..exceptions import StackUnderflowError +from ..gas import GAS_BASE, GAS_VERY_LOW, charge_gas +from ..memory import buffer_read + + +def pop(evm: Evm) -> None: + """ + Remove item from stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + stack.pop(evm.stack) + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def push_n(evm: Evm, num_bytes: int) -> None: + """ + Pushes a N-byte immediate onto the stack. Push zero if num_bytes is zero. + + Parameters + ---------- + evm : + The current EVM frame. + + num_bytes : + The number of immediate bytes to be read from the code and pushed to + the stack. Push zero if num_bytes is zero. + + """ + # STACK + pass + + # GAS + if num_bytes == 0: + charge_gas(evm, GAS_BASE) + else: + charge_gas(evm, GAS_VERY_LOW) + + # OPERATION + data_to_push = U256.from_be_bytes( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(num_bytes)) + ) + stack.push(evm.stack, data_to_push) + + # PROGRAM COUNTER + evm.pc += Uint(1) + Uint(num_bytes) + + +def dup_n(evm: Evm, item_number: int) -> None: + """ + Duplicate the Nth stack item (from top of the stack) to the top of stack. + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be duplicated + to the top of stack. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_VERY_LOW) + if item_number >= len(evm.stack): + raise StackUnderflowError + data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] + stack.push(evm.stack, data_to_duplicate) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def swap_n(evm: Evm, item_number: int) -> None: + """ + Swap the top and the `item_number` element of the stack, where + the top of the stack is position zero. + + If `item_number` is zero, this function does nothing (which should not be + possible, since there is no `SWAP0` instruction). + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be swapped + with the top of stack element. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_VERY_LOW) + if item_number >= len(evm.stack): + raise StackUnderflowError + evm.stack[-1], evm.stack[-1 - item_number] = ( + evm.stack[-1 - item_number], + evm.stack[-1], + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +push0 = partial(push_n, num_bytes=0) +push1 = partial(push_n, num_bytes=1) +push2 = partial(push_n, num_bytes=2) +push3 = partial(push_n, num_bytes=3) +push4 = partial(push_n, num_bytes=4) +push5 = partial(push_n, num_bytes=5) +push6 = partial(push_n, num_bytes=6) +push7 = partial(push_n, num_bytes=7) +push8 = partial(push_n, num_bytes=8) +push9 = partial(push_n, num_bytes=9) +push10 = partial(push_n, num_bytes=10) +push11 = partial(push_n, num_bytes=11) +push12 = partial(push_n, num_bytes=12) +push13 = partial(push_n, num_bytes=13) +push14 = partial(push_n, num_bytes=14) +push15 = partial(push_n, num_bytes=15) +push16 = partial(push_n, num_bytes=16) +push17 = partial(push_n, num_bytes=17) +push18 = partial(push_n, num_bytes=18) +push19 = partial(push_n, num_bytes=19) +push20 = partial(push_n, num_bytes=20) +push21 = partial(push_n, num_bytes=21) +push22 = partial(push_n, num_bytes=22) +push23 = partial(push_n, num_bytes=23) +push24 = partial(push_n, num_bytes=24) +push25 = partial(push_n, num_bytes=25) +push26 = partial(push_n, num_bytes=26) +push27 = partial(push_n, num_bytes=27) +push28 = partial(push_n, num_bytes=28) +push29 = partial(push_n, num_bytes=29) +push30 = partial(push_n, num_bytes=30) +push31 = partial(push_n, num_bytes=31) +push32 = partial(push_n, num_bytes=32) + +dup1 = partial(dup_n, item_number=0) +dup2 = partial(dup_n, item_number=1) +dup3 = partial(dup_n, item_number=2) +dup4 = partial(dup_n, item_number=3) +dup5 = partial(dup_n, item_number=4) +dup6 = partial(dup_n, item_number=5) +dup7 = partial(dup_n, item_number=6) +dup8 = partial(dup_n, item_number=7) +dup9 = partial(dup_n, item_number=8) +dup10 = partial(dup_n, item_number=9) +dup11 = partial(dup_n, item_number=10) +dup12 = partial(dup_n, item_number=11) +dup13 = partial(dup_n, item_number=12) +dup14 = partial(dup_n, item_number=13) +dup15 = partial(dup_n, item_number=14) +dup16 = partial(dup_n, item_number=15) + +swap1 = partial(swap_n, item_number=1) +swap2 = partial(swap_n, item_number=2) +swap3 = partial(swap_n, item_number=3) +swap4 = partial(swap_n, item_number=4) +swap5 = partial(swap_n, item_number=5) +swap6 = partial(swap_n, item_number=6) +swap7 = partial(swap_n, item_number=7) +swap8 = partial(swap_n, item_number=8) +swap9 = partial(swap_n, item_number=9) +swap10 = partial(swap_n, item_number=10) +swap11 = partial(swap_n, item_number=11) +swap12 = partial(swap_n, item_number=12) +swap13 = partial(swap_n, item_number=13) +swap14 = partial(swap_n, item_number=14) +swap15 = partial(swap_n, item_number=15) +swap16 = partial(swap_n, item_number=16) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py new file mode 100644 index 0000000000..de7ef935f5 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -0,0 +1,233 @@ +""" +Ethereum Virtual Machine (EVM) Storage Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM storage related instructions. +""" + +from ethereum_types.numeric import Uint + +from ...state import ( + get_storage, + get_storage_original, + get_transient_storage, + set_storage, + set_transient_storage, +) +from ...state_tracker import ( + capture_pre_storage, + track_storage_read, + track_storage_write, +) +from .. import Evm +from ..exceptions import WriteInStaticContext +from ..gas import ( + GAS_CALL_STIPEND, + GAS_COLD_SLOAD, + GAS_STORAGE_CLEAR_REFUND, + GAS_STORAGE_SET, + GAS_STORAGE_UPDATE, + GAS_WARM_ACCESS, + charge_gas, + check_gas, +) +from ..stack import pop, push + + +def sload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + storage of the current account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + is_cold_access = ( + evm.message.current_target, + key, + ) not in evm.accessed_storage_keys + gas_cost = GAS_COLD_SLOAD if is_cold_access else GAS_WARM_ACCESS + + charge_gas(evm, gas_cost) + + # OPERATION + state = evm.message.block_env.state + value = get_storage(state, evm.message.current_target, key) + + if is_cold_access: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + track_storage_read( + evm.state_changes, + evm.message.current_target, + key, + ) + + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's storage. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + + # check we have at least the stipend gas + check_gas(evm, GAS_CALL_STIPEND + Uint(1)) + + # check static context before accessing storage + if evm.message.is_static: + raise WriteInStaticContext + + # GAS + gas_cost = Uint(0) + is_cold_access = ( + evm.message.current_target, + key, + ) not in evm.accessed_storage_keys + + if is_cold_access: + gas_cost += GAS_COLD_SLOAD + + state = evm.message.block_env.state + original_value = get_storage_original( + state, evm.message.current_target, key + ) + current_value = get_storage(state, evm.message.current_target, key) + + if is_cold_access: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + + capture_pre_storage( + evm.message.tx_env.state_changes, + evm.message.current_target, + key, + current_value, + ) + track_storage_read( + evm.state_changes, + evm.message.current_target, + key, + ) + + if original_value == current_value and current_value != new_value: + if original_value == 0: + gas_cost += GAS_STORAGE_SET + else: + gas_cost += GAS_STORAGE_UPDATE - GAS_COLD_SLOAD + else: + gas_cost += GAS_WARM_ACCESS + + charge_gas(evm, gas_cost) + + # REFUND COUNTER + if current_value != new_value: + if original_value != 0 and current_value != 0 and new_value == 0: + # Storage is cleared for the first time in the transaction + evm.refund_counter += int(GAS_STORAGE_CLEAR_REFUND) + + if original_value != 0 and current_value == 0: + # Gas refund issued earlier to be reversed + evm.refund_counter -= int(GAS_STORAGE_CLEAR_REFUND) + + if original_value == new_value: + # Storage slot being restored to its original value + if original_value == 0: + # Slot was originally empty and was SET earlier + evm.refund_counter += int(GAS_STORAGE_SET - GAS_WARM_ACCESS) + else: + # Slot was originally non-empty and was UPDATED earlier + evm.refund_counter += int( + GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS + ) + + # OPERATION + set_storage(state, evm.message.current_target, key, new_value) + track_storage_write( + evm.state_changes, + evm.message.current_target, + key, + new_value, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def tload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + transient storage of the current account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + charge_gas(evm, GAS_WARM_ACCESS) + + # OPERATION + value = get_transient_storage( + evm.message.tx_env.transient_storage, evm.message.current_target, key + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def tstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's transient storage. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_WARM_ACCESS) + if evm.message.is_static: + raise WriteInStaticContext + set_transient_storage( + evm.message.tx_env.transient_storage, + evm.message.current_target, + key, + new_value, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py new file mode 100644 index 0000000000..e5e2ec306f --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -0,0 +1,938 @@ +""" +Ethereum Virtual Machine (EVM) System Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM system related instructions. +""" + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.utils.numeric import ceil32 + +# track_address_access removed - now using state_changes.track_address() +from ...fork_types import Address +from ...state import ( + account_has_code_or_nonce, + account_has_storage, + get_account, + increment_nonce, + is_account_alive, + move_ether, + set_account_balance, +) +from ...state_tracker import ( + capture_pre_balance, + create_child_frame, + track_address, + track_balance_change, + track_nonce_change, +) +from ...utils.address import ( + compute_contract_address, + compute_create2_contract_address, + to_address_masked, +) +from ...vm.eoa_delegation import ( + calculate_delegation_cost, + read_delegation_target, +) +from .. import ( + Evm, + Message, + incorporate_child_on_error, + incorporate_child_on_success, +) +from ..exceptions import OutOfGasError, Revert, WriteInStaticContext +from ..gas import ( + GAS_CALL_VALUE, + GAS_COLD_ACCOUNT_ACCESS, + GAS_CREATE, + GAS_KECCAK256_WORD, + GAS_NEW_ACCOUNT, + GAS_SELF_DESTRUCT, + GAS_SELF_DESTRUCT_NEW_ACCOUNT, + GAS_WARM_ACCESS, + GAS_ZERO, + calculate_gas_extend_memory, + calculate_message_call_gas, + charge_gas, + check_gas, + init_code_cost, + max_message_call_gas, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def generic_create( + evm: Evm, + endowment: U256, + contract_address: Address, + memory_start_position: U256, + memory_size: U256, +) -> None: + """ + Core logic used by the `CREATE*` family of opcodes. + """ + # This import causes a circular import error + # if it's not moved inside this method + from ...vm.interpreter import ( + MAX_INIT_CODE_SIZE, + STACK_DEPTH_LIMIT, + process_create_message, + ) + + # Check static context first + if evm.message.is_static: + raise WriteInStaticContext + + # Check max init code size early before memory read + if memory_size > U256(MAX_INIT_CODE_SIZE): + raise OutOfGasError + + state = evm.message.block_env.state + + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) + evm.gas_left -= create_message_gas + evm.return_data = b"" + + sender_address = evm.message.current_target + sender = get_account(state, sender_address) + + if ( + sender.balance < endowment + or sender.nonce == Uint(2**64 - 1) + or evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT + ): + evm.gas_left += create_message_gas + push(evm.stack, U256(0)) + return + + evm.accessed_addresses.add(contract_address) + + track_address(evm.state_changes, contract_address) + + if account_has_code_or_nonce( + state, contract_address + ) or account_has_storage(state, contract_address): + increment_nonce(state, evm.message.current_target) + nonce_after = get_account(state, evm.message.current_target).nonce + track_nonce_change( + evm.state_changes, + evm.message.current_target, + U64(nonce_after), + ) + push(evm.stack, U256(0)) + return + + # Track nonce increment for CREATE + increment_nonce(state, evm.message.current_target) + nonce_after = get_account(state, evm.message.current_target).nonce + track_nonce_change( + evm.state_changes, + evm.message.current_target, + U64(nonce_after), + ) + + # Create call frame as child of parent EVM's frame + child_state_changes = create_child_frame(evm.state_changes) + + child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, + caller=evm.message.current_target, + target=Bytes0(), + gas=create_message_gas, + value=endowment, + data=b"", + code=call_data, + current_target=contract_address, + depth=evm.message.depth + Uint(1), + code_address=None, + should_transfer_value=True, + is_static=False, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + disable_precompiles=False, + parent_evm=evm, + is_create=True, + state_changes=child_state_changes, + ) + + child_evm = process_create_message(child_message) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = b"" + push(evm.stack, U256.from_be_bytes(child_evm.message.current_target)) + + +def create(evm: Evm) -> None: + """ + Creates a new account with associated code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + init_code_gas = init_code_cost(Uint(memory_size)) + + charge_gas(evm, GAS_CREATE + extend_memory.cost + init_code_gas) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + contract_address = compute_contract_address( + evm.message.current_target, + get_account( + evm.message.block_env.state, evm.message.current_target + ).nonce, + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def create2(evm: Evm) -> None: + """ + Creates a new account with associated code. + + It's similar to CREATE opcode except that the address of new account + depends on the init_code instead of the nonce of sender. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + salt = pop(evm.stack).to_be_bytes32() + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + call_data_words = ceil32(Uint(memory_size)) // Uint(32) + init_code_gas = init_code_cost(Uint(memory_size)) + charge_gas( + evm, + GAS_CREATE + + GAS_KECCAK256_WORD * call_data_words + + extend_memory.cost + + init_code_gas, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + contract_address = compute_create2_contract_address( + evm.message.current_target, + salt, + memory_read_bytes(evm.memory, memory_start_position, memory_size), + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def return_(evm: Evm) -> None: + """ + Halts execution returning output data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + + charge_gas(evm, GAS_ZERO + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + evm.output = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + evm.running = False + + # PROGRAM COUNTER + pass + + +def generic_call( + evm: Evm, + gas: Uint, + value: U256, + caller: Address, + to: Address, + code_address: Address, + should_transfer_value: bool, + is_staticcall: bool, + memory_input_start_position: U256, + memory_input_size: U256, + memory_output_start_position: U256, + memory_output_size: U256, + code: Bytes, + disable_precompiles: bool, +) -> None: + """ + Perform the core logic of the `CALL*` family of opcodes. + """ + from ...vm.interpreter import STACK_DEPTH_LIMIT, process_message + + evm.return_data = b"" + + if evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT: + evm.gas_left += gas + push(evm.stack, U256(0)) + return + + call_data = memory_read_bytes( + evm.memory, memory_input_start_position, memory_input_size + ) + + # Create call frame as child of parent EVM's frame + child_state_changes = create_child_frame(evm.state_changes) + + child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, + caller=caller, + target=to, + gas=gas, + value=value, + data=call_data, + code=code, + current_target=to, + depth=evm.message.depth + Uint(1), + code_address=code_address, + should_transfer_value=should_transfer_value, + is_static=True if is_staticcall else evm.message.is_static, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + disable_precompiles=disable_precompiles, + parent_evm=evm, + is_create=False, + state_changes=child_state_changes, + ) + + child_evm = process_message(child_message) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(1)) + + actual_output_size = min(memory_output_size, U256(len(child_evm.output))) + memory_write( + evm.memory, + memory_output_start_position, + child_evm.output[:actual_output_size], + ) + + +def call(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address_masked(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + is_cold_access = to not in evm.accessed_addresses + access_gas_cost = ( + GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS + ) + + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE + + check_gas( + evm, + access_gas_cost + transfer_gas_cost + extend_memory.cost, + ) + + # need to access account to check if account is alive, check gas before + create_gas_cost = GAS_NEW_ACCOUNT + if value == 0 or is_account_alive(evm.message.block_env.state, to): + create_gas_cost = Uint(0) + + if is_cold_access: + evm.accessed_addresses.add(to) + + ( + is_delegated, + original_address, + delegated_address, + delegation_gas_cost, + ) = calculate_delegation_cost(evm, to) + + if is_delegated and delegation_gas_cost > Uint(0): + assert delegated_address is not None + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + + transfer_gas_cost + + create_gas_cost + + delegation_gas_cost, + ) + check_gas(evm, message_call_gas.cost + extend_memory.cost) + code = read_delegation_target(evm, delegated_address) + final_address = delegated_address + else: + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + create_gas_cost + transfer_gas_cost, + ) + check_gas(evm, message_call_gas.cost + extend_memory.cost) + code = get_account(evm.message.block_env.state, to).code + final_address = to + + code_address = final_address + disable_precompiles = is_delegated + + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + if evm.message.is_static and value != U256(0): + raise WriteInStaticContext + evm.memory += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.sub_call + else: + generic_call( + evm, + message_call_gas.sub_call, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def callcode(evm: Evm) -> None: + """ + Message-call into this account with alternative account's code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address_masked(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + to = evm.message.current_target + + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + is_cold_access = code_address not in evm.accessed_addresses + access_gas_cost = ( + GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS + ) + if is_cold_access: + evm.accessed_addresses.add(code_address) + + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE + + check_gas( + evm, + access_gas_cost + extend_memory.cost + transfer_gas_cost, + ) + + # need to access account to get delegation code, check gas before + ( + is_delegated, + original_address, + delegated_address, + delegation_gas_cost, + ) = calculate_delegation_cost(evm, code_address) + + if is_delegated and delegation_gas_cost > Uint(0): + assert delegated_address is not None + # Recalculate with delegation cost and check gas + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + transfer_gas_cost + delegation_gas_cost, + ) + check_gas(evm, message_call_gas.cost + extend_memory.cost) + code = read_delegation_target(evm, delegated_address) + final_address = delegated_address + else: + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + transfer_gas_cost, + ) + check_gas(evm, message_call_gas.cost + extend_memory.cost) + code = get_account(evm.message.block_env.state, code_address).code + final_address = code_address + + code_address = final_address + disable_precompiles = is_delegated + + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + + # EIP-7928: For CALLCODE with value transfer, capture pre-balance + # in transaction frame. CALLCODE transfers value from/to current_target + # (same address), affecting current storage context, not child frame + if value != 0 and sender_balance >= value: + capture_pre_balance( + evm.message.tx_env.state_changes, + evm.message.current_target, + sender_balance, + ) + + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.sub_call + else: + generic_call( + evm, + message_call_gas.sub_call, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def selfdestruct(evm: Evm) -> None: + """ + Halt execution and register account for later deletion. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + if evm.message.is_static: + raise WriteInStaticContext + + # STACK + beneficiary = to_address_masked(pop(evm.stack)) + + # GAS + gas_cost = GAS_SELF_DESTRUCT + is_cold_access = beneficiary not in evm.accessed_addresses + if is_cold_access: + gas_cost += GAS_COLD_ACCOUNT_ACCESS + + check_gas(evm, gas_cost) + + # is_account_alive requires account to be accessed, check gas before + if ( + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 + ): + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + if is_cold_access: + evm.accessed_addresses.add(beneficiary) + track_address(evm.state_changes, beneficiary) + + charge_gas(evm, gas_cost) + + state = evm.message.block_env.state + originator = evm.message.current_target + originator_balance = get_account(state, originator).balance + beneficiary_balance = get_account(state, beneficiary).balance + + # Get tracking context + tx_frame = evm.message.tx_env.state_changes + + # Capture pre-balances for net-zero filtering + track_address(evm.state_changes, originator) + capture_pre_balance(tx_frame, originator, originator_balance) + capture_pre_balance(tx_frame, beneficiary, beneficiary_balance) + + # Transfer balance + move_ether(state, originator, beneficiary, originator_balance) + + # Track balance changes + originator_new_balance = get_account(state, originator).balance + beneficiary_new_balance = get_account(state, beneficiary).balance + track_balance_change( + evm.state_changes, + originator, + originator_new_balance, + ) + track_balance_change( + evm.state_changes, + beneficiary, + beneficiary_new_balance, + ) + + # register account for deletion only if it was created + # in the same transaction + if originator in state.created_accounts: + set_account_balance(state, originator, U256(0)) + track_balance_change(evm.state_changes, originator, U256(0)) + evm.accounts_to_delete.add(originator) + + # HALT the execution + evm.running = False + + # PROGRAM COUNTER + pass + + +def delegatecall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address_masked(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + is_cold_access = code_address not in evm.accessed_addresses + access_gas_cost = ( + GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS + ) + if is_cold_access: + evm.accessed_addresses.add(code_address) + + check_gas(evm, access_gas_cost + extend_memory.cost) + + # need to access account to get delegation code, check gas before + ( + is_delegated, + original_address, + delegated_address, + delegation_gas_cost, + ) = calculate_delegation_cost(evm, code_address) + + if is_delegated and delegation_gas_cost > Uint(0): + assert delegated_address is not None + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + delegation_gas_cost, + ) + check_gas(evm, message_call_gas.cost + extend_memory.cost) + code = read_delegation_target(evm, delegated_address) + final_address = delegated_address + else: + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost, + ) + check_gas(evm, message_call_gas.cost + extend_memory.cost) + code = get_account(evm.message.block_env.state, code_address).code + final_address = code_address + + code_address = final_address + disable_precompiles = is_delegated + + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.sub_call, + evm.message.value, + evm.message.caller, + evm.message.current_target, + code_address, + False, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def staticcall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address_masked(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + is_cold_access = to not in evm.accessed_addresses + access_gas_cost = ( + GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS + ) + if is_cold_access: + evm.accessed_addresses.add(to) + + check_gas(evm, access_gas_cost + extend_memory.cost) + + # need to access account to get delegation code, check gas before + ( + is_delegated, + original_address, + delegated_address, + delegation_gas_cost, + ) = calculate_delegation_cost(evm, to) + + if is_delegated and delegation_gas_cost > Uint(0): + assert delegated_address is not None + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + delegation_gas_cost, + ) + check_gas(evm, message_call_gas.cost + extend_memory.cost) + code = read_delegation_target(evm, delegated_address) + final_address = delegated_address + else: + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost, + ) + check_gas(evm, message_call_gas.cost + extend_memory.cost) + code = get_account(evm.message.block_env.state, to).code + final_address = to + + code_address = final_address + disable_precompiles = is_delegated + + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.sub_call, + U256(0), + evm.message.current_target, + to, + code_address, + True, + True, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def revert(evm: Evm) -> None: + """ + Stop execution and revert state changes, without consuming all provided gas + and also has the ability to return a reason. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + charge_gas(evm, extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + output = memory_read_bytes(evm.memory, memory_start_index, size) + evm.output = Bytes(output) + raise Revert + + # PROGRAM COUNTER + # no-op diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py new file mode 100644 index 0000000000..154c56de11 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -0,0 +1,390 @@ +""" +Ethereum Virtual Machine (EVM) Interpreter. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +A straightforward interpreter that executes EVM code. +""" + +from dataclasses import dataclass +from typing import Optional, Set, Tuple + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import U64, U256, Uint, ulen + +from ethereum.exceptions import EthereumException +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) + +from ..blocks import Log +from ..fork_types import Address +from ..state import ( + account_has_code_or_nonce, + account_has_storage, + begin_transaction, + commit_transaction, + destroy_storage, + get_account, + increment_nonce, + mark_account_created, + move_ether, + rollback_transaction, + set_code, +) +from ..state_tracker import ( + StateChanges, + capture_pre_balance, + merge_on_failure, + merge_on_success, + track_address, + track_balance_change, + track_code_change, + track_nonce_change, +) +from ..vm import Message +from ..vm.eoa_delegation import get_delegated_code_address, set_delegation +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from . import Evm +from .exceptions import ( + AddressCollision, + ExceptionalHalt, + InvalidContractPrefix, + InvalidOpcode, + OutOfGasError, + Revert, + StackDepthLimitError, +) +from .instructions import Ops, op_implementation +from .runtime import get_valid_jump_destinations + +STACK_DEPTH_LIMIT = Uint(1024) +MAX_CODE_SIZE = 0x6000 +MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE + + +@dataclass +class MessageCallOutput: + """ + Output of a particular message call. + + Contains the following: + + 1. `gas_left`: remaining gas after execution. + 2. `refund_counter`: gas to refund after execution. + 3. `logs`: list of `Log` generated during execution. + 4. `accounts_to_delete`: Contracts which have self-destructed. + 5. `error`: The error from the execution if any. + 6. `return_data`: The output of the execution. + """ + + gas_left: Uint + refund_counter: U256 + logs: Tuple[Log, ...] + accounts_to_delete: Set[Address] + error: Optional[EthereumException] + return_data: Bytes + + +def process_message_call(message: Message) -> MessageCallOutput: + """ + If `message.target` is empty then it creates a smart contract + else it executes a call from the `message.caller` to the `message.target`. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + output : `MessageCallOutput` + Output of the message call + + """ + block_env = message.block_env + refund_counter = U256(0) + if message.target == Bytes0(b""): + is_collision = account_has_code_or_nonce( + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) + track_address(message.tx_env.state_changes, message.current_target) + if is_collision: + return MessageCallOutput( + Uint(0), + U256(0), + tuple(), + set(), + AddressCollision(), + Bytes(b""), + ) + else: + evm = process_create_message(message) + else: + if message.tx_env.authorizations != (): + refund_counter += set_delegation(message) + + delegated_address = get_delegated_code_address(message.code) + if delegated_address is not None: + message.disable_precompiles = True + message.accessed_addresses.add(delegated_address) + message.code = get_account(block_env.state, delegated_address).code + message.code_address = delegated_address + track_address(message.block_env.state_changes, delegated_address) + + evm = process_message(message) + + if evm.error: + logs: Tuple[Log, ...] = () + accounts_to_delete = set() + else: + logs = evm.logs + accounts_to_delete = evm.accounts_to_delete + refund_counter += U256(evm.refund_counter) + + tx_end = TransactionEnd( + int(message.gas) - int(evm.gas_left), evm.output, evm.error + ) + evm_trace(evm, tx_end) + + return MessageCallOutput( + gas_left=evm.gas_left, + refund_counter=refund_counter, + logs=logs, + accounts_to_delete=accounts_to_delete, + error=evm.error, + return_data=evm.output, + ) + + +def process_create_message(message: Message) -> Evm: + """ + Executes a call to create a smart contract. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + evm: :py:class:`~ethereum.forks.amsterdam.vm.Evm` + Items containing execution specific objects. + + """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage + # take snapshot of state before processing the message + begin_transaction(state, transient_storage) + + # If the address where the account is being created has storage, it is + # destroyed. This can only happen in the following highly unlikely + # circumstances: + # * The address created by a `CREATE` call collides with a subsequent + # `CREATE` or `CREATE2` call. + # * The first `CREATE` happened before Spurious Dragon and left empty + # code. + destroy_storage(state, message.current_target) + + # In the previously mentioned edge case the preexisting storage is ignored + # for gas refund purposes. In order to do this we must track created + # accounts. This tracking is also needed to respect the constraints + # added to SELFDESTRUCT by EIP-6780. + mark_account_created(state, message.current_target) + + increment_nonce(state, message.current_target) + nonce_after = get_account(state, message.current_target).nonce + track_nonce_change( + message.state_changes, + message.current_target, + U64(nonce_after), + ) + + evm = process_message(message) + if not evm.error: + contract_code = evm.output + contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT + try: + if len(contract_code) > 0: + if contract_code[0] == 0xEF: + raise InvalidContractPrefix + charge_gas(evm, contract_code_gas) + if len(contract_code) > MAX_CODE_SIZE: + raise OutOfGasError + except ExceptionalHalt as error: + rollback_transaction(state, transient_storage) + merge_on_failure(message.state_changes) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + else: + # Note: No need to capture pre code since it's always b"" here + set_code(state, message.current_target, contract_code) + if contract_code != b"": + track_code_change( + message.state_changes, + message.current_target, + contract_code, + ) + commit_transaction(state, transient_storage) + merge_on_success(message.state_changes) + else: + rollback_transaction(state, transient_storage) + merge_on_failure(message.state_changes) + return evm + + +def process_message(message: Message) -> Evm: + """ + Move ether and execute the relevant code. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + evm: :py:class:`~ethereum.forks.amsterdam.vm.Evm` + Items containing execution specific objects + + """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage + if message.depth > STACK_DEPTH_LIMIT: + raise StackDepthLimitError("Stack depth limit reached") + + # take snapshot of state before processing the message + begin_transaction(state, transient_storage) + + track_address(message.state_changes, message.current_target) + + if message.should_transfer_value and message.value != 0: + # Track value transfer + sender_balance = get_account(state, message.caller).balance + recipient_balance = get_account(state, message.current_target).balance + + track_address(message.state_changes, message.caller) + capture_pre_balance( + message.tx_env.state_changes, message.caller, sender_balance + ) + capture_pre_balance( + message.tx_env.state_changes, + message.current_target, + recipient_balance, + ) + + move_ether( + state, message.caller, message.current_target, message.value + ) + + sender_new_balance = get_account(state, message.caller).balance + recipient_new_balance = get_account( + state, message.current_target + ).balance + + track_balance_change( + message.state_changes, + message.caller, + U256(sender_new_balance), + ) + track_balance_change( + message.state_changes, + message.current_target, + U256(recipient_new_balance), + ) + + evm = execute_code(message, message.state_changes) + if evm.error: + rollback_transaction(state, transient_storage) + if not message.is_create: + merge_on_failure(evm.state_changes) + else: + commit_transaction(state, transient_storage) + if not message.is_create: + merge_on_success(evm.state_changes) + return evm + + +def execute_code(message: Message, state_changes: StateChanges) -> Evm: + """ + Executes bytecode present in the `message`. + + Parameters + ---------- + message : + Transaction specific items. + state_changes : + The state changes frame to use for tracking. + + Returns + ------- + evm: `ethereum.vm.EVM` + Items containing execution specific objects + + """ + code = message.code + valid_jump_destinations = get_valid_jump_destinations(code) + + evm = Evm( + pc=Uint(0), + stack=[], + memory=bytearray(), + code=code, + gas_left=message.gas, + valid_jump_destinations=valid_jump_destinations, + logs=(), + refund_counter=0, + running=True, + message=message, + output=b"", + accounts_to_delete=set(), + return_data=b"", + error=None, + accessed_addresses=message.accessed_addresses, + accessed_storage_keys=message.accessed_storage_keys, + state_changes=state_changes, + ) + try: + if evm.message.code_address in PRE_COMPILED_CONTRACTS: + if message.disable_precompiles: + return evm + evm_trace(evm, PrecompileStart(evm.message.code_address)) + PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) + return evm + + while evm.running and evm.pc < ulen(evm.code): + try: + op = Ops(evm.code[evm.pc]) + except ValueError as e: + raise InvalidOpcode(evm.code[evm.pc]) from e + + evm_trace(evm, OpStart(op)) + op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) + + except ExceptionalHalt as error: + evm_trace(evm, OpException(error)) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + except Revert as error: + evm_trace(evm, OpException(error)) + evm.error = error + return evm diff --git a/src/ethereum/forks/amsterdam/vm/memory.py b/src/ethereum/forks/amsterdam/vm/memory.py new file mode 100644 index 0000000000..3b76b2454c --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/memory.py @@ -0,0 +1,83 @@ +""" +Ethereum Virtual Machine (EVM) Memory. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM memory operations. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.byte import right_pad_zero_bytes + + +def memory_write( + memory: bytearray, start_position: U256, value: Bytes +) -> None: + """ + Writes to memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + value : + Data to write to memory. + + """ + memory[start_position : int(start_position) + len(value)] = value + + +def memory_read_bytes( + memory: bytearray, start_position: U256, size: U256 +) -> Bytes: + """ + Read bytes from memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Size of the data that needs to be read from `start_position`. + + Returns + ------- + data_bytes : + Data read from memory. + + """ + return Bytes(memory[start_position : Uint(start_position) + Uint(size)]) + + +def buffer_read(buffer: Bytes, start_position: U256, size: U256) -> Bytes: + """ + Read bytes from a buffer. Padding with zeros if necessary. + + Parameters + ---------- + buffer : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Size of the data that needs to be read from `start_position`. + + Returns + ------- + data_bytes : + Data read from memory. + + """ + buffer_slice = buffer[start_position : Uint(start_position) + Uint(size)] + return right_pad_zero_bytes(bytes(buffer_slice), size) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/__init__.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/__init__.py new file mode 100644 index 0000000000..d32959fc93 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/__init__.py @@ -0,0 +1,55 @@ +""" +Precompiled Contract Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Addresses of precompiled contracts and mappings to their +implementations. +""" + +from ...utils.hexadecimal import hex_to_address + +__all__ = ( + "ECRECOVER_ADDRESS", + "SHA256_ADDRESS", + "RIPEMD160_ADDRESS", + "IDENTITY_ADDRESS", + "MODEXP_ADDRESS", + "ALT_BN128_ADD_ADDRESS", + "ALT_BN128_MUL_ADDRESS", + "ALT_BN128_PAIRING_CHECK_ADDRESS", + "BLAKE2F_ADDRESS", + "POINT_EVALUATION_ADDRESS", + "BLS12_G1_ADD_ADDRESS", + "BLS12_G1_MSM_ADDRESS", + "BLS12_G2_ADD_ADDRESS", + "BLS12_G2_MSM_ADDRESS", + "BLS12_PAIRING_ADDRESS", + "BLS12_MAP_FP_TO_G1_ADDRESS", + "BLS12_MAP_FP2_TO_G2_ADDRESS", + "P256VERIFY_ADDRESS", +) + +ECRECOVER_ADDRESS = hex_to_address("0x01") +SHA256_ADDRESS = hex_to_address("0x02") +RIPEMD160_ADDRESS = hex_to_address("0x03") +IDENTITY_ADDRESS = hex_to_address("0x04") +MODEXP_ADDRESS = hex_to_address("0x05") +ALT_BN128_ADD_ADDRESS = hex_to_address("0x06") +ALT_BN128_MUL_ADDRESS = hex_to_address("0x07") +ALT_BN128_PAIRING_CHECK_ADDRESS = hex_to_address("0x08") +BLAKE2F_ADDRESS = hex_to_address("0x09") +POINT_EVALUATION_ADDRESS = hex_to_address("0x0a") +BLS12_G1_ADD_ADDRESS = hex_to_address("0x0b") +BLS12_G1_MSM_ADDRESS = hex_to_address("0x0c") +BLS12_G2_ADD_ADDRESS = hex_to_address("0x0d") +BLS12_G2_MSM_ADDRESS = hex_to_address("0x0e") +BLS12_PAIRING_ADDRESS = hex_to_address("0x0f") +BLS12_MAP_FP_TO_G1_ADDRESS = hex_to_address("0x10") +BLS12_MAP_FP2_TO_G2_ADDRESS = hex_to_address("0x11") +P256VERIFY_ADDRESS = hex_to_address("0x100") diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py new file mode 100644 index 0000000000..214725d8da --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py @@ -0,0 +1,230 @@ +""" +Ethereum Virtual Machine (EVM) ALT_BN128 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ALT_BN128 precompiled contracts. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint +from py_ecc.optimized_bn128.optimized_curve import ( + FQ, + FQ2, + FQ12, + add, + b, + b2, + curve_order, + field_modulus, + is_inf, + is_on_curve, + multiply, + normalize, +) +from py_ecc.optimized_bn128.optimized_pairing import pairing +from py_ecc.typing import Optimized_Point3D as Point3D + +from ...vm import Evm +from ...vm.gas import charge_gas +from ...vm.memory import buffer_read +from ..exceptions import InvalidParameter, OutOfGasError + + +def bytes_to_g1(data: Bytes) -> Point3D[FQ]: + """ + Decode 64 bytes to a point on the curve. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Point3D + A point on the curve. + + Raises + ------ + InvalidParameter + Either a field element is invalid or the point is not on the curve. + + """ + if len(data) != 64: + raise InvalidParameter("Input should be 64 bytes long") + + x_bytes = buffer_read(data, U256(0), U256(32)) + x = int(U256.from_be_bytes(x_bytes)) + y_bytes = buffer_read(data, U256(32), U256(32)) + y = int(U256.from_be_bytes(y_bytes)) + + if x >= field_modulus: + raise InvalidParameter("Invalid field element") + if y >= field_modulus: + raise InvalidParameter("Invalid field element") + + z = 1 + if x == 0 and y == 0: + z = 0 + + point = (FQ(x), FQ(y), FQ(z)) + + # Check if the point is on the curve + if not is_on_curve(point, b): + raise InvalidParameter("Point is not on curve") + + return point + + +def bytes_to_g2(data: Bytes) -> Point3D[FQ2]: + """ + Decode 128 bytes to a G2 point. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Point2D + A point on the curve. + + Raises + ------ + InvalidParameter + Either a field element is invalid or the point is not on the curve. + + """ + if len(data) != 128: + raise InvalidParameter("G2 should be 128 bytes long") + + x0_bytes = buffer_read(data, U256(0), U256(32)) + x0 = int(U256.from_be_bytes(x0_bytes)) + x1_bytes = buffer_read(data, U256(32), U256(32)) + x1 = int(U256.from_be_bytes(x1_bytes)) + + y0_bytes = buffer_read(data, U256(64), U256(32)) + y0 = int(U256.from_be_bytes(y0_bytes)) + y1_bytes = buffer_read(data, U256(96), U256(32)) + y1 = int(U256.from_be_bytes(y1_bytes)) + + if x0 >= field_modulus or x1 >= field_modulus: + raise InvalidParameter("Invalid field element") + if y0 >= field_modulus or y1 >= field_modulus: + raise InvalidParameter("Invalid field element") + + x = FQ2((x1, x0)) + y = FQ2((y1, y0)) + + z = (1, 0) + if x == FQ2((0, 0)) and y == FQ2((0, 0)): + z = (0, 0) + + point = (x, y, FQ2(z)) + + # Check if the point is on the curve + if not is_on_curve(point, b2): + raise InvalidParameter("Point is not on curve") + + return point + + +def alt_bn128_add(evm: Evm) -> None: + """ + The ALT_BN128 addition precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, Uint(150)) + + # OPERATION + try: + p0 = bytes_to_g1(buffer_read(data, U256(0), U256(64))) + p1 = bytes_to_g1(buffer_read(data, U256(64), U256(64))) + except InvalidParameter as e: + raise OutOfGasError from e + + p = add(p0, p1) + x, y = normalize(p) + + evm.output = Uint(x).to_be_bytes32() + Uint(y).to_be_bytes32() + + +def alt_bn128_mul(evm: Evm) -> None: + """ + The ALT_BN128 multiplication precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, Uint(6000)) + + # OPERATION + try: + p0 = bytes_to_g1(buffer_read(data, U256(0), U256(64))) + except InvalidParameter as e: + raise OutOfGasError from e + n = int(U256.from_be_bytes(buffer_read(data, U256(64), U256(32)))) + + p = multiply(p0, n) + x, y = normalize(p) + + evm.output = Uint(x).to_be_bytes32() + Uint(y).to_be_bytes32() + + +def alt_bn128_pairing_check(evm: Evm) -> None: + """ + The ALT_BN128 pairing check precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, Uint(34000 * (len(data) // 192) + 45000)) + + # OPERATION + if len(data) % 192 != 0: + raise OutOfGasError + result = FQ12.one() + for i in range(len(data) // 192): + try: + p = bytes_to_g1(buffer_read(data, U256(192 * i), U256(64))) + q = bytes_to_g2(buffer_read(data, U256(192 * i + 64), U256(128))) + except InvalidParameter as e: + raise OutOfGasError from e + if not is_inf(multiply(p, curve_order)): + raise OutOfGasError + if not is_inf(multiply(q, curve_order)): + raise OutOfGasError + + result *= pairing(q, p) + + if result == FQ12.one(): + evm.output = U256(1).to_be_bytes32() + else: + evm.output = U256(0).to_be_bytes32() diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py new file mode 100644 index 0000000000..204fbcea28 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py @@ -0,0 +1,42 @@ +""" +Ethereum Virtual Machine (EVM) Blake2 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `Blake2` precompiled contract. +""" + +from ethereum.crypto.blake2 import Blake2b + +from ...vm import Evm +from ...vm.gas import GAS_BLAKE2_PER_ROUND, charge_gas +from ..exceptions import InvalidParameter + + +def blake2f(evm: Evm) -> None: + """ + Writes the Blake2 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + if len(data) != 213: + raise InvalidParameter + + blake2b = Blake2b() + rounds, h, m, t_0, t_1, f = blake2b.get_blake2_parameters(data) + + charge_gas(evm, GAS_BLAKE2_PER_ROUND * rounds) + if f not in [0, 1]: + raise InvalidParameter + + evm.output = blake2b.compress(rounds, h, m, t_0, t_1, f) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/__init__.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/__init__.py new file mode 100644 index 0000000000..c99e7573f4 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/__init__.py @@ -0,0 +1,622 @@ +""" +BLS12 381 Precompile. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Precompile for BLS12-381 curve operations. +""" + +from functools import lru_cache +from typing import Tuple + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint +from py_ecc.optimized_bls12_381.optimized_curve import ( + FQ, + FQ2, + b, + b2, + curve_order, + is_inf, + is_on_curve, + normalize, +) +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) +from py_ecc.typing import Optimized_Point3D as Point3D + +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter + +G1_K_DISCOUNT = [ + 1000, + 949, + 848, + 797, + 764, + 750, + 738, + 728, + 719, + 712, + 705, + 698, + 692, + 687, + 682, + 677, + 673, + 669, + 665, + 661, + 658, + 654, + 651, + 648, + 645, + 642, + 640, + 637, + 635, + 632, + 630, + 627, + 625, + 623, + 621, + 619, + 617, + 615, + 613, + 611, + 609, + 608, + 606, + 604, + 603, + 601, + 599, + 598, + 596, + 595, + 593, + 592, + 591, + 589, + 588, + 586, + 585, + 584, + 582, + 581, + 580, + 579, + 577, + 576, + 575, + 574, + 573, + 572, + 570, + 569, + 568, + 567, + 566, + 565, + 564, + 563, + 562, + 561, + 560, + 559, + 558, + 557, + 556, + 555, + 554, + 553, + 552, + 551, + 550, + 549, + 548, + 547, + 547, + 546, + 545, + 544, + 543, + 542, + 541, + 540, + 540, + 539, + 538, + 537, + 536, + 536, + 535, + 534, + 533, + 532, + 532, + 531, + 530, + 529, + 528, + 528, + 527, + 526, + 525, + 525, + 524, + 523, + 522, + 522, + 521, + 520, + 520, + 519, +] + +G2_K_DISCOUNT = [ + 1000, + 1000, + 923, + 884, + 855, + 832, + 812, + 796, + 782, + 770, + 759, + 749, + 740, + 732, + 724, + 717, + 711, + 704, + 699, + 693, + 688, + 683, + 679, + 674, + 670, + 666, + 663, + 659, + 655, + 652, + 649, + 646, + 643, + 640, + 637, + 634, + 632, + 629, + 627, + 624, + 622, + 620, + 618, + 615, + 613, + 611, + 609, + 607, + 606, + 604, + 602, + 600, + 598, + 597, + 595, + 593, + 592, + 590, + 589, + 587, + 586, + 584, + 583, + 582, + 580, + 579, + 578, + 576, + 575, + 574, + 573, + 571, + 570, + 569, + 568, + 567, + 566, + 565, + 563, + 562, + 561, + 560, + 559, + 558, + 557, + 556, + 555, + 554, + 553, + 552, + 552, + 551, + 550, + 549, + 548, + 547, + 546, + 545, + 545, + 544, + 543, + 542, + 541, + 541, + 540, + 539, + 538, + 537, + 537, + 536, + 535, + 535, + 534, + 533, + 532, + 532, + 531, + 530, + 530, + 529, + 528, + 528, + 527, + 526, + 526, + 525, + 524, + 524, +] + +G1_MAX_DISCOUNT = 519 +G2_MAX_DISCOUNT = 524 +MULTIPLIER = Uint(1000) + + +# Note: Caching as a way to optimize client performance can create a DoS +# attack vector for worst-case inputs that trigger only cache misses. This +# should not be relied upon for client performance optimization in +# production systems. +@lru_cache(maxsize=128) +def _bytes_to_g1_cached( + data: bytes, + subgroup_check: bool = False, +) -> Point3D[FQ]: + """ + Internal cached version of `bytes_to_g1` that works with hashable `bytes`. + """ + if len(data) != 128: + raise InvalidParameter("Input should be 128 bytes long") + + x = bytes_to_fq(data[:64]) + y = bytes_to_fq(data[64:]) + + if x >= FQ.field_modulus: + raise InvalidParameter("x >= field modulus") + if y >= FQ.field_modulus: + raise InvalidParameter("y >= field modulus") + + z = 1 + if x == 0 and y == 0: + z = 0 + point = FQ(x), FQ(y), FQ(z) + + if not is_on_curve(point, b): + raise InvalidParameter("G1 point is not on curve") + + if subgroup_check and not is_inf(bls12_multiply(point, curve_order)): + raise InvalidParameter("Subgroup check failed for G1 point.") + + return point + + +def bytes_to_g1( + data: Bytes, + subgroup_check: bool = False, +) -> Point3D[FQ]: + """ + Decode 128 bytes to a G1 point with or without subgroup check. + + Parameters + ---------- + data : + The bytes data to decode. + subgroup_check : bool + Whether to perform a subgroup check on the G1 point. + + Returns + ------- + point : Point3D[FQ] + The G1 point. + + Raises + ------ + InvalidParameter + If a field element is invalid, the point is not on the curve, or the + subgroup check fails. + + """ + # This is needed bc when we slice `Bytes` we get a `bytearray`, + # which is not hashable + return _bytes_to_g1_cached(bytes(data), subgroup_check) + + +def g1_to_bytes( + g1_point: Point3D[FQ], +) -> Bytes: + """ + Encode a G1 point to 128 bytes. + + Parameters + ---------- + g1_point : + The G1 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + g1_normalized = normalize(g1_point) + x, y = g1_normalized + return int(x).to_bytes(64, "big") + int(y).to_bytes(64, "big") + + +def decode_g1_scalar_pair( + data: Bytes, +) -> Tuple[Point3D[FQ], int]: + """ + Decode 160 bytes to a G1 point and a scalar. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Tuple[Point3D[FQ], int] + The G1 point and the scalar. + + Raises + ------ + InvalidParameter + If the subgroup check failed. + + """ + if len(data) != 160: + InvalidParameter("Input should be 160 bytes long") + + point = bytes_to_g1(data[:128], subgroup_check=True) + + m = int.from_bytes(buffer_read(data, U256(128), U256(32)), "big") + + return point, m + + +def bytes_to_fq(data: Bytes) -> FQ: + """ + Decode 64 bytes to a FQ element. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + fq : FQ + The FQ element. + + Raises + ------ + InvalidParameter + If the field element is invalid. + + """ + if len(data) != 64: + raise InvalidParameter("FQ should be 64 bytes long") + + c = int.from_bytes(data[:64], "big") + + if c >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + + return FQ(c) + + +def bytes_to_fq2(data: Bytes) -> FQ2: + """ + Decode 128 bytes to an FQ2 element. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + fq2 : FQ2 + The FQ2 element. + + Raises + ------ + InvalidParameter + If the field element is invalid. + + """ + if len(data) != 128: + raise InvalidParameter("FQ2 input should be 128 bytes long") + c_0 = int.from_bytes(data[:64], "big") + c_1 = int.from_bytes(data[64:], "big") + + if c_0 >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + if c_1 >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + + return FQ2((c_0, c_1)) + + +# Note: Caching as a way to optimize client performance can create a DoS +# attack vector for worst-case inputs that trigger only cache misses. This +# should not be relied upon for client performance optimization in +# production systems. +@lru_cache(maxsize=128) +def _bytes_to_g2_cached( + data: bytes, + subgroup_check: bool = False, +) -> Point3D[FQ2]: + """ + Internal cached version of `bytes_to_g2` that works with hashable `bytes`. + """ + if len(data) != 256: + raise InvalidParameter("G2 should be 256 bytes long") + + x = bytes_to_fq2(data[:128]) + y = bytes_to_fq2(data[128:]) + + z = (1, 0) + if x == FQ2((0, 0)) and y == FQ2((0, 0)): + z = (0, 0) + + point = x, y, FQ2(z) + + if not is_on_curve(point, b2): + raise InvalidParameter("Point is not on curve") + + if subgroup_check and not is_inf(bls12_multiply(point, curve_order)): + raise InvalidParameter("Subgroup check failed for G2 point.") + + return point + + +def bytes_to_g2( + data: Bytes, + subgroup_check: bool = False, +) -> Point3D[FQ2]: + """ + Decode 256 bytes to a G2 point with or without subgroup check. + + Parameters + ---------- + data : + The bytes data to decode. + subgroup_check : bool + Whether to perform a subgroup check on the G2 point. + + Returns + ------- + point : Point3D[FQ2] + The G2 point. + + Raises + ------ + InvalidParameter + If a field element is invalid, the point is not on the curve, or the + subgroup check fails. + + """ + # This is needed bc when we slice `Bytes` we get a `bytearray`, + # which is not hashable + return _bytes_to_g2_cached(data, subgroup_check) + + +def fq2_to_bytes(fq2: FQ2) -> Bytes: + """ + Encode a FQ2 point to 128 bytes. + + Parameters + ---------- + fq2 : + The FQ2 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + coord0, coord1 = fq2.coeffs + return int(coord0).to_bytes(64, "big") + int(coord1).to_bytes(64, "big") + + +def g2_to_bytes( + g2_point: Point3D[FQ2], +) -> Bytes: + """ + Encode a G2 point to 256 bytes. + + Parameters + ---------- + g2_point : + The G2 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + x_coords, y_coords = normalize(g2_point) + return fq2_to_bytes(x_coords) + fq2_to_bytes(y_coords) + + +def decode_g2_scalar_pair( + data: Bytes, +) -> Tuple[Point3D[FQ2], int]: + """ + Decode 288 bytes to a G2 point and a scalar. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Tuple[Point3D[FQ2], int] + The G2 point and the scalar. + + Raises + ------ + InvalidParameter + If the subgroup check failed. + + """ + if len(data) != 288: + InvalidParameter("Input should be 288 bytes long") + + point = bytes_to_g2(data[:256], subgroup_check=True) + n = int.from_bytes(data[256 : 256 + 32], "big") + + return point, n diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py new file mode 100644 index 0000000000..ae8eeba0ce --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py @@ -0,0 +1,151 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of pre-compiles in G1 (curve over base prime field). +""" + +from ethereum_types.numeric import U256, Uint +from py_ecc.bls.hash_to_curve import clear_cofactor_G1, map_to_curve_G1 +from py_ecc.optimized_bls12_381.optimized_curve import FQ +from py_ecc.optimized_bls12_381.optimized_curve import add as bls12_add +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) + +from ....vm import Evm +from ....vm.gas import ( + GAS_BLS_G1_ADD, + GAS_BLS_G1_MAP, + GAS_BLS_G1_MUL, + charge_gas, +) +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import ( + G1_K_DISCOUNT, + G1_MAX_DISCOUNT, + MULTIPLIER, + bytes_to_g1, + decode_g1_scalar_pair, + g1_to_bytes, +) + +LENGTH_PER_PAIR = 160 + + +def bls12_g1_add(evm: Evm) -> None: + """ + The bls12_381 G1 point addition precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 256: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, Uint(GAS_BLS_G1_ADD)) + + # OPERATION + p1 = bytes_to_g1(buffer_read(data, U256(0), U256(128))) + p2 = bytes_to_g1(buffer_read(data, U256(128), U256(128))) + + result = bls12_add(p1, p2) + + evm.output = g1_to_bytes(result) + + +def bls12_g1_msm(evm: Evm) -> None: + """ + The bls12_381 G1 multi-scalar multiplication precompile. + Note: This uses the naive approach to multi-scalar multiplication + which is not suitably optimized for production clients. Clients are + required to implement a more efficient algorithm such as the Pippenger + algorithm. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // LENGTH_PER_PAIR + if k <= 128: + discount = Uint(G1_K_DISCOUNT[k - 1]) + else: + discount = Uint(G1_MAX_DISCOUNT) + + gas_cost = Uint(k) * GAS_BLS_G1_MUL * discount // MULTIPLIER + charge_gas(evm, gas_cost) + + # OPERATION + for i in range(k): + start_index = i * LENGTH_PER_PAIR + end_index = start_index + LENGTH_PER_PAIR + + p, m = decode_g1_scalar_pair(data[start_index:end_index]) + product = bls12_multiply(p, m) + + if i == 0: + result = product + else: + result = bls12_add(result, product) + + evm.output = g1_to_bytes(result) + + +def bls12_map_fp_to_g1(evm: Evm) -> None: + """ + Precompile to map field element to G1. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 64: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, Uint(GAS_BLS_G1_MAP)) + + # OPERATION + fp = int.from_bytes(data, "big") + if fp >= FQ.field_modulus: + raise InvalidParameter("coordinate >= field modulus") + + g1_optimized_3d = clear_cofactor_G1(map_to_curve_G1(FQ(fp))) + evm.output = g1_to_bytes(g1_optimized_3d) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py new file mode 100644 index 0000000000..473739b78c --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py @@ -0,0 +1,153 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 G2 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of pre-compiles in G2 (curve over base prime field). +""" + +from ethereum_types.numeric import U256, Uint +from py_ecc.bls.hash_to_curve import clear_cofactor_G2, map_to_curve_G2 +from py_ecc.optimized_bls12_381.optimized_curve import FQ2 +from py_ecc.optimized_bls12_381.optimized_curve import add as bls12_add +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) + +from ....vm import Evm +from ....vm.gas import ( + GAS_BLS_G2_ADD, + GAS_BLS_G2_MAP, + GAS_BLS_G2_MUL, + charge_gas, +) +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import ( + G2_K_DISCOUNT, + G2_MAX_DISCOUNT, + MULTIPLIER, + bytes_to_fq2, + bytes_to_g2, + decode_g2_scalar_pair, + g2_to_bytes, +) + +LENGTH_PER_PAIR = 288 + + +def bls12_g2_add(evm: Evm) -> None: + """ + The bls12_381 G2 point addition precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 512: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, Uint(GAS_BLS_G2_ADD)) + + # OPERATION + p1 = bytes_to_g2(buffer_read(data, U256(0), U256(256))) + p2 = bytes_to_g2(buffer_read(data, U256(256), U256(256))) + + result = bls12_add(p1, p2) + + evm.output = g2_to_bytes(result) + + +def bls12_g2_msm(evm: Evm) -> None: + """ + The bls12_381 G2 multi-scalar multiplication precompile. + Note: This uses the naive approach to multi-scalar multiplication + which is not suitably optimized for production clients. Clients are + required to implement a more efficient algorithm such as the Pippenger + algorithm. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // LENGTH_PER_PAIR + if k <= 128: + discount = Uint(G2_K_DISCOUNT[k - 1]) + else: + discount = Uint(G2_MAX_DISCOUNT) + + gas_cost = Uint(k) * GAS_BLS_G2_MUL * discount // MULTIPLIER + charge_gas(evm, gas_cost) + + # OPERATION + for i in range(k): + start_index = i * LENGTH_PER_PAIR + end_index = start_index + LENGTH_PER_PAIR + + p, m = decode_g2_scalar_pair(data[start_index:end_index]) + product = bls12_multiply(p, m) + + if i == 0: + result = product + else: + result = bls12_add(result, product) + + evm.output = g2_to_bytes(result) + + +def bls12_map_fp2_to_g2(evm: Evm) -> None: + """ + Precompile to map field element to G2. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 128: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, Uint(GAS_BLS_G2_MAP)) + + # OPERATION + field_element = bytes_to_fq2(data) + assert isinstance(field_element, FQ2) + + fp2 = bytes_to_fq2(data) + g2_3d = clear_cofactor_G2(map_to_curve_G2(fp2)) + + evm.output = g2_to_bytes(g2_3d) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py new file mode 100644 index 0000000000..6cb29a32fd --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py @@ -0,0 +1,69 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 PAIRING PRE-COMPILE. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the BLS12 381 pairing pre-compile. +""" + +from ethereum_types.numeric import Uint +from py_ecc.optimized_bls12_381 import FQ12, curve_order, is_inf, pairing +from py_ecc.optimized_bls12_381 import multiply as bls12_multiply + +from ....vm import Evm +from ....vm.gas import charge_gas +from ...exceptions import InvalidParameter +from . import bytes_to_g1, bytes_to_g2 + + +def bls12_pairing(evm: Evm) -> None: + """ + The bls12_381 pairing precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid or if sub-group check fails. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % 384 != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // 384 + gas_cost = Uint(32600 * k + 37700) + charge_gas(evm, gas_cost) + + # OPERATION + result = FQ12.one() + for i in range(k): + g1_start = Uint(384 * i) + g2_start = Uint(384 * i + 128) + + g1_slice = data[g1_start : g1_start + Uint(128)] + g1_point = bytes_to_g1(bytes(g1_slice)) + if not is_inf(bls12_multiply(g1_point, curve_order)): + raise InvalidParameter("Sub-group check failed for G1 point.") + + g2_slice = data[g2_start : g2_start + Uint(256)] + g2_point = bytes_to_g2(bytes(g2_slice)) + if not is_inf(bls12_multiply(g2_point, curve_order)): + raise InvalidParameter("Sub-group check failed for G2 point.") + + result *= pairing(g2_point, g1_point) + + if result == FQ12.one(): + evm.output = b"\x00" * 31 + b"\x01" + else: + evm.output = b"\x00" * 32 diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py new file mode 100644 index 0000000000..d2eeaf75df --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py @@ -0,0 +1,64 @@ +""" +Ethereum Virtual Machine (EVM) ECRECOVER PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ECRECOVER precompiled contract. +""" + +from ethereum_types.numeric import U256 + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GAS_ECRECOVER, charge_gas +from ...vm.memory import buffer_read + + +def ecrecover(evm: Evm) -> None: + """ + Decrypts the address using elliptic curve DSA recovery mechanism and writes + the address to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GAS_ECRECOVER) + + # OPERATION + message_hash_bytes = buffer_read(data, U256(0), U256(32)) + message_hash = Hash32(message_hash_bytes) + v = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + r = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + s = U256.from_be_bytes(buffer_read(data, U256(96), U256(32))) + + if v != U256(27) and v != U256(28): + return + if U256(0) >= r or r >= SECP256K1N: + return + if U256(0) >= s or s >= SECP256K1N: + return + + try: + public_key = secp256k1_recover(r, s, v - U256(27), message_hash) + except InvalidSignatureError: + # unable to extract public key + return + + address = keccak256(public_key)[12:32] + padded_address = left_pad_zero_bytes(address, 32) + evm.output = padded_address diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py new file mode 100644 index 0000000000..49a79a4c4a --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py @@ -0,0 +1,39 @@ +""" +Ethereum Virtual Machine (EVM) IDENTITY PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `IDENTITY` precompiled contract. +""" + +from ethereum_types.numeric import Uint + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_IDENTITY, GAS_IDENTITY_WORD, charge_gas + + +def identity(evm: Evm) -> None: + """ + Writes the message data to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_IDENTITY + GAS_IDENTITY_WORD * word_count) + + # OPERATION + evm.output = data diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/mapping.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/mapping.py new file mode 100644 index 0000000000..a80e2b0235 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/mapping.py @@ -0,0 +1,77 @@ +""" +Precompiled Contract Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Mapping of precompiled contracts their implementations. +""" + +from typing import Callable, Dict + +from ...fork_types import Address +from . import ( + ALT_BN128_ADD_ADDRESS, + ALT_BN128_MUL_ADDRESS, + ALT_BN128_PAIRING_CHECK_ADDRESS, + BLAKE2F_ADDRESS, + BLS12_G1_ADD_ADDRESS, + BLS12_G1_MSM_ADDRESS, + BLS12_G2_ADD_ADDRESS, + BLS12_G2_MSM_ADDRESS, + BLS12_MAP_FP2_TO_G2_ADDRESS, + BLS12_MAP_FP_TO_G1_ADDRESS, + BLS12_PAIRING_ADDRESS, + ECRECOVER_ADDRESS, + IDENTITY_ADDRESS, + MODEXP_ADDRESS, + P256VERIFY_ADDRESS, + POINT_EVALUATION_ADDRESS, + RIPEMD160_ADDRESS, + SHA256_ADDRESS, +) +from .alt_bn128 import alt_bn128_add, alt_bn128_mul, alt_bn128_pairing_check +from .blake2f import blake2f +from .bls12_381.bls12_381_g1 import ( + bls12_g1_add, + bls12_g1_msm, + bls12_map_fp_to_g1, +) +from .bls12_381.bls12_381_g2 import ( + bls12_g2_add, + bls12_g2_msm, + bls12_map_fp2_to_g2, +) +from .bls12_381.bls12_381_pairing import bls12_pairing +from .ecrecover import ecrecover +from .identity import identity +from .modexp import modexp +from .p256verify import p256verify +from .point_evaluation import point_evaluation +from .ripemd160 import ripemd160 +from .sha256 import sha256 + +PRE_COMPILED_CONTRACTS: Dict[Address, Callable] = { + ECRECOVER_ADDRESS: ecrecover, + SHA256_ADDRESS: sha256, + RIPEMD160_ADDRESS: ripemd160, + IDENTITY_ADDRESS: identity, + MODEXP_ADDRESS: modexp, + ALT_BN128_ADD_ADDRESS: alt_bn128_add, + ALT_BN128_MUL_ADDRESS: alt_bn128_mul, + ALT_BN128_PAIRING_CHECK_ADDRESS: alt_bn128_pairing_check, + BLAKE2F_ADDRESS: blake2f, + POINT_EVALUATION_ADDRESS: point_evaluation, + BLS12_G1_ADD_ADDRESS: bls12_g1_add, + BLS12_G1_MSM_ADDRESS: bls12_g1_msm, + BLS12_G2_ADD_ADDRESS: bls12_g2_add, + BLS12_G2_MSM_ADDRESS: bls12_g2_msm, + BLS12_PAIRING_ADDRESS: bls12_pairing, + BLS12_MAP_FP_TO_G1_ADDRESS: bls12_map_fp_to_g1, + BLS12_MAP_FP2_TO_G2_ADDRESS: bls12_map_fp2_to_g2, + P256VERIFY_ADDRESS: p256verify, +} diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py new file mode 100644 index 0000000000..5e7e895b91 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py @@ -0,0 +1,175 @@ +""" +Ethereum Virtual Machine (EVM) MODEXP PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `MODEXP` precompiled contract. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ...vm import Evm +from ...vm.exceptions import ExceptionalHalt +from ...vm.gas import charge_gas +from ..memory import buffer_read + + +def modexp(evm: Evm) -> None: + """ + Calculates `(base**exp) % modulus` for arbitrary sized `base`, `exp` and. + `modulus`. The return value is the same length as the modulus. + """ + data = evm.message.data + + # GAS + base_length = U256.from_be_bytes(buffer_read(data, U256(0), U256(32))) + if base_length > U256(1024): + raise ExceptionalHalt("Mod-exp base length is too large") + + exp_length = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + if exp_length > U256(1024): + raise ExceptionalHalt("Mod-exp exponent length is too large") + + modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + if modulus_length > U256(1024): + raise ExceptionalHalt("Mod-exp modulus length is too large") + + exp_start = U256(96) + base_length + + exp_head = U256.from_be_bytes( + buffer_read(data, exp_start, min(U256(32), exp_length)) + ) + + charge_gas( + evm, + gas_cost(base_length, modulus_length, exp_length, exp_head), + ) + + # OPERATION + if base_length == 0 and modulus_length == 0: + evm.output = Bytes() + return + + base = Uint.from_be_bytes(buffer_read(data, U256(96), base_length)) + exp = Uint.from_be_bytes(buffer_read(data, exp_start, exp_length)) + + modulus_start = exp_start + exp_length + modulus = Uint.from_be_bytes( + buffer_read(data, modulus_start, modulus_length) + ) + + if modulus == 0: + evm.output = Bytes(b"\x00") * modulus_length + else: + evm.output = pow(base, exp, modulus).to_bytes( + Uint(modulus_length), "big" + ) + + +def complexity(base_length: U256, modulus_length: U256) -> Uint: + """ + Estimate the complexity of performing a modular exponentiation. + + Parameters + ---------- + base_length : + Length of the array representing the base integer. + + modulus_length : + Length of the array representing the modulus integer. + + Returns + ------- + complexity : `Uint` + Complexity of performing the operation. + + """ + max_length = max(Uint(base_length), Uint(modulus_length)) + words = (max_length + Uint(7)) // Uint(8) + complexity = Uint(16) + if max_length > Uint(32): + complexity = Uint(2) * words ** Uint(2) + return complexity + + +def iterations(exponent_length: U256, exponent_head: U256) -> Uint: + """ + Calculate the number of iterations required to perform a modular + exponentiation. + + Parameters + ---------- + exponent_length : + Length of the array representing the exponent integer. + + exponent_head : + First 32 bytes of the exponent (with leading zero padding if it is + shorter than 32 bytes), as a U256. + + Returns + ------- + iterations : `Uint` + Number of iterations. + + """ + if exponent_length <= U256(32) and exponent_head == U256(0): + count = Uint(0) + elif exponent_length <= U256(32): + bit_length = exponent_head.bit_length() + + if bit_length > Uint(0): + bit_length -= Uint(1) + + count = bit_length + else: + length_part = Uint(16) * (Uint(exponent_length) - Uint(32)) + bits_part = exponent_head.bit_length() + + if bits_part > Uint(0): + bits_part -= Uint(1) + + count = length_part + bits_part + + return max(count, Uint(1)) + + +def gas_cost( + base_length: U256, + modulus_length: U256, + exponent_length: U256, + exponent_head: U256, +) -> Uint: + """ + Calculate the gas cost of performing a modular exponentiation. + + Parameters + ---------- + base_length : + Length of the array representing the base integer. + + modulus_length : + Length of the array representing the modulus integer. + + exponent_length : + Length of the array representing the exponent integer. + + exponent_head : + First 32 bytes of the exponent (with leading zero padding if it is + shorter than 32 bytes), as a U256. + + Returns + ------- + gas_cost : `Uint` + Gas required for performing the operation. + + """ + multiplication_complexity = complexity(base_length, modulus_length) + iteration_count = iterations(exponent_length, exponent_head) + cost = multiplication_complexity * iteration_count + return max(Uint(500), cost) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py new file mode 100644 index 0000000000..6f6e7ff4e9 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py @@ -0,0 +1,89 @@ +""" +Ethereum Virtual Machine (EVM) P256VERIFY PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction. +------------ +Implementation of the P256VERIFY precompiled contract. +""" + +from ethereum_types.numeric import U256 + +from ethereum.crypto.elliptic_curve import ( + SECP256R1N, + SECP256R1P, + is_on_curve_secp256r1, + secp256r1_verify, +) +from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import InvalidSignatureError +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GAS_P256VERIFY, charge_gas +from ...vm.memory import buffer_read + + +def p256verify(evm: Evm) -> None: + """ + Verifies a P-256 signature. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GAS_P256VERIFY) + + if len(data) != 160: + return + + # OPERATION + message_hash_bytes = buffer_read(data, U256(0), U256(32)) + message_hash = Hash32(message_hash_bytes) + r = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + s = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + public_key_x = U256.from_be_bytes( + buffer_read(data, U256(96), U256(32)) + ) # qx + public_key_y = U256.from_be_bytes( + buffer_read(data, U256(128), U256(32)) + ) # qy + + # Signature component bounds: + # Both r and s MUST satisfy 0 < r < n and 0 < s < n + if r <= U256(0) or r >= SECP256R1N: + return + if s <= U256(0) or s >= SECP256R1N: + return + + # Public key bounds: + # Both qx and qy MUST satisfy 0 ≀ qx < p and 0 ≀ qy < p + # U256 is unsigned, so we don't need to check for < 0 + if public_key_x >= SECP256R1P: + return + if public_key_y >= SECP256R1P: + return + + # Point should not be at infinity (represented as (0, 0)) + if public_key_x == U256(0) and public_key_y == U256(0): + return + + # Point validity: The point (qx, qy) MUST satisfy the curve equation + # qy^2 ≑ qx^3 + a*qx + b (mod p) + if not is_on_curve_secp256r1(public_key_x, public_key_y): + return + + try: + secp256r1_verify(r, s, public_key_x, public_key_y, message_hash) + except InvalidSignatureError: + return + + evm.output = left_pad_zero_bytes(b"\x01", 32) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py new file mode 100644 index 0000000000..760af47736 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py @@ -0,0 +1,72 @@ +""" +Ethereum Virtual Machine (EVM) POINT EVALUATION PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the POINT EVALUATION precompiled contract. +""" + +from ethereum_types.bytes import Bytes, Bytes32, Bytes48 +from ethereum_types.numeric import U256 + +from ethereum.crypto.kzg import ( + KZGCommitment, + kzg_commitment_to_versioned_hash, + verify_kzg_proof, +) + +from ...vm import Evm +from ...vm.exceptions import KZGProofError +from ...vm.gas import GAS_POINT_EVALUATION, charge_gas + +FIELD_ELEMENTS_PER_BLOB = 4096 +BLS_MODULUS = 52435875175126190479447740508185965837690552500527637822603658699938581184513 # noqa: E501 +VERSIONED_HASH_VERSION_KZG = b"\x01" + + +def point_evaluation(evm: Evm) -> None: + """ + A pre-compile that verifies a KZG proof which claims that a blob + (represented by a commitment) evaluates to a given value at a given point. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + if len(data) != 192: + raise KZGProofError + + versioned_hash = data[:32] + z = Bytes32(data[32:64]) + y = Bytes32(data[64:96]) + commitment = KZGCommitment(data[96:144]) + proof = Bytes48(data[144:192]) + + # GAS + charge_gas(evm, GAS_POINT_EVALUATION) + if kzg_commitment_to_versioned_hash(commitment) != versioned_hash: + raise KZGProofError + + # Verify KZG proof with z and y in big endian format + try: + kzg_proof_verification = verify_kzg_proof(commitment, z, y, proof) + except Exception as e: + raise KZGProofError from e + + if not kzg_proof_verification: + raise KZGProofError + + # Return FIELD_ELEMENTS_PER_BLOB and BLS_MODULUS as padded + # 32 byte big endian values + evm.output = Bytes( + U256(FIELD_ELEMENTS_PER_BLOB).to_be_bytes32() + + U256(BLS_MODULUS).to_be_bytes32() + ) diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py new file mode 100644 index 0000000000..74cefd93af --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py @@ -0,0 +1,44 @@ +""" +Ethereum Virtual Machine (EVM) RIPEMD160 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `RIPEMD160` precompiled contract. +""" + +import hashlib + +from ethereum_types.numeric import Uint + +from ethereum.utils.byte import left_pad_zero_bytes +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_RIPEMD160, GAS_RIPEMD160_WORD, charge_gas + + +def ripemd160(evm: Evm) -> None: + """ + Writes the ripemd160 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_RIPEMD160 + GAS_RIPEMD160_WORD * word_count) + + # OPERATION + hash_bytes = hashlib.new("ripemd160", data).digest() + padded_hash = left_pad_zero_bytes(hash_bytes, 32) + evm.output = padded_hash diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py new file mode 100644 index 0000000000..b0d1517b40 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py @@ -0,0 +1,41 @@ +""" +Ethereum Virtual Machine (EVM) SHA256 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `SHA256` precompiled contract. +""" + +import hashlib + +from ethereum_types.numeric import Uint + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_SHA256, GAS_SHA256_WORD, charge_gas + + +def sha256(evm: Evm) -> None: + """ + Writes the sha256 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_SHA256 + GAS_SHA256_WORD * word_count) + + # OPERATION + evm.output = hashlib.sha256(data).digest() diff --git a/src/ethereum/forks/amsterdam/vm/runtime.py b/src/ethereum/forks/amsterdam/vm/runtime.py new file mode 100644 index 0000000000..505b3488de --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/runtime.py @@ -0,0 +1,69 @@ +""" +Ethereum Virtual Machine (EVM) Runtime Operations. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Runtime related operations used while executing EVM code. +""" + +from typing import Set + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint, ulen + +from .instructions import Ops + + +def get_valid_jump_destinations(code: Bytes) -> Set[Uint]: + """ + Analyze the evm code to obtain the set of valid jump destinations. + + Valid jump destinations are defined as follows: + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + + Note - Jump destinations are 0-indexed. + + Parameters + ---------- + code : + The EVM code which is to be executed. + + Returns + ------- + valid_jump_destinations: `Set[Uint]` + The set of valid jump destinations in the code. + + """ + valid_jump_destinations = set() + pc = Uint(0) + + while pc < ulen(code): + try: + current_opcode = Ops(code[pc]) + except ValueError: + # Skip invalid opcodes, as they don't affect the jumpdest + # analysis. Nevertheless, such invalid opcodes would be caught + # and raised when the interpreter runs. + pc += Uint(1) + continue + + if current_opcode == Ops.JUMPDEST: + valid_jump_destinations.add(pc) + elif Ops.PUSH1.value <= current_opcode.value <= Ops.PUSH32.value: + # If PUSH-N opcodes are encountered, skip the current opcode along + # with the trailing data segment corresponding to the PUSH-N + # opcodes. + push_data_size = current_opcode.value - Ops.PUSH1.value + 1 + pc += Uint(push_data_size) + + pc += Uint(1) + + return valid_jump_destinations diff --git a/src/ethereum/forks/amsterdam/vm/stack.py b/src/ethereum/forks/amsterdam/vm/stack.py new file mode 100644 index 0000000000..a87b0a4707 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/stack.py @@ -0,0 +1,58 @@ +""" +Ethereum Virtual Machine (EVM) Stack. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the stack operators for the EVM. +""" + +from typing import List + +from ethereum_types.numeric import U256 + +from .exceptions import StackOverflowError, StackUnderflowError + + +def pop(stack: List[U256]) -> U256: + """ + Pops the top item off of `stack`. + + Parameters + ---------- + stack : + EVM stack. + + Returns + ------- + value : `U256` + The top element on the stack. + + """ + if len(stack) == 0: + raise StackUnderflowError + + return stack.pop() + + +def push(stack: List[U256], value: U256) -> None: + """ + Pushes `value` onto `stack`. + + Parameters + ---------- + stack : + EVM stack. + + value : + Item to be pushed onto `stack`. + + """ + if len(stack) == 1024: + raise StackOverflowError + + return stack.append(value) diff --git a/src/ethereum/forks/osaka/vm/eoa_delegation.py b/src/ethereum/forks/osaka/vm/eoa_delegation.py index 29909b5fa5..0913fa63ff 100644 --- a/src/ethereum/forks/osaka/vm/eoa_delegation.py +++ b/src/ethereum/forks/osaka/vm/eoa_delegation.py @@ -134,6 +134,7 @@ def access_delegation( """ state = evm.message.block_env.state + code = get_account(state, address).code if not is_valid_delegation(code): return False, address, code, Uint(0) diff --git a/src/ethereum/genesis.py b/src/ethereum/genesis.py index 84519271dc..7ba79d6c26 100644 --- a/src/ethereum/genesis.py +++ b/src/ethereum/genesis.py @@ -259,6 +259,9 @@ def add_genesis_block( if has_field(hardfork.Header, "requests_hash"): fields["requests_hash"] = Hash32(b"\0" * 32) + if has_field(hardfork.Header, "block_access_list_hash"): + fields["block_access_list_hash"] = Hash32(b"\0" * 32) + genesis_header = hardfork.Header(**fields) block_fields = { @@ -273,6 +276,9 @@ def add_genesis_block( if has_field(hardfork.Block, "requests"): block_fields["requests"] = () + if has_field(hardfork.Block, "block_access_list"): + block_fields["block_access_list"] = rlp.encode([]) + genesis_block = hardfork.Block(**block_fields) chain.blocks.append(genesis_block) diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py index d01eda47c9..ab9e1b99d9 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fixture_loader.py @@ -196,4 +196,8 @@ def json_to_header(self, raw: Any) -> Any: requests_hash = hex_to_bytes32(raw.get("requestsHash")) parameters.append(requests_hash) + if "blockAccessListHash" in raw: + bal_hash = hex_to_bytes32(raw.get("blockAccessListHash")) + parameters.append(bal_hash) + return self.fork.Header(*parameters) diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py index 5e085504cd..64e920d266 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -120,6 +120,27 @@ def has_signing_hash_155(self) -> bool: """Check if the fork has a `signing_hash_155` function.""" return hasattr(self._module("transactions"), "signing_hash_155") + @property + def build_block_access_list(self) -> Any: + """Build function of the fork.""" + return self._module("block_access_lists").build_block_access_list + + @property + def compute_block_access_list_hash(self) -> Any: + """compute_block_access_list_hash function of the fork.""" + return self._module( + "block_access_lists" + ).compute_block_access_list_hash + + @property + def has_block_access_list_hash(self) -> bool: + """Check if the fork has a `block_access_list_hash` function.""" + try: + module = self._module("block_access_lists") + except ModuleNotFoundError: + return False + return hasattr(module, "compute_block_access_list_hash") + @property def signing_hash_2930(self) -> Any: """signing_hash_2930 function of the fork.""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index fc09dfd8a3..4988ef25bc 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -16,6 +16,7 @@ from ethereum import trace from ethereum.exceptions import EthereumException, InvalidBlock from ethereum.fork_criteria import ByBlockNumber, ByTimestamp, Unscheduled +from ethereum.forks.amsterdam.state_tracker import StateChanges from ethereum_spec_tools.forks import Hardfork, TemporaryHardfork from ..loaders.fixture_loader import Load @@ -308,6 +309,9 @@ def block_environment(self) -> Any: ) kw_arguments["excess_blob_gas"] = self.env.excess_blob_gas + if self.fork.has_block_access_list_hash: + kw_arguments["state_changes"] = StateChanges() + return block_environment(**kw_arguments) def backup_state(self) -> None: @@ -386,20 +390,34 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: data=block_env.parent_beacon_block_root, ) - for i, tx in zip( - self.txs.successfully_parsed, - self.txs.transactions, - strict=True, + for tx_index, (original_idx, tx) in enumerate( + zip( + self.txs.successfully_parsed, + self.txs.transactions, + strict=True, + ) ): self.backup_state() try: self.fork.process_transaction( - block_env, block_output, tx, Uint(i) + block_env, block_output, tx, Uint(tx_index) ) except EthereumException as e: - self.txs.rejected_txs[i] = f"Failed transaction: {e!r}" + self.txs.rejected_txs[original_idx] = ( + f"Failed transaction: {e!r}" + ) self.restore_state() - self.logger.warning(f"Transaction {i} failed: {e!r}") + self.logger.warning( + f"Transaction {original_idx} failed: {e!r}" + ) + + # Post-execution operations use index N+1 + if self.fork.has_block_access_list_hash: + from ethereum.forks.amsterdam.state_tracker import ( + increment_block_access_index, + ) + + increment_block_access_index(block_env.state_changes) if not self.fork.proof_of_stake: if self.options.state_reward is None: @@ -417,6 +435,12 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: if self.fork.has_compute_requests_hash: self.fork.process_general_purpose_requests(block_env, block_output) + if self.fork.has_block_access_list_hash: + # Build block access list from block_env.state_changes + block_output.block_access_list = self.fork.build_block_access_list( + block_env.state_changes + ) + def run_blockchain_test(self) -> None: """ Apply a block on the pre-state. Also includes system operations. diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 07a3071c1f..be719ba7af 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -145,6 +145,9 @@ def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: if t8n.fork.has_compute_requests_hash: arguments["requests_hash"] = Hash32(b"\0" * 32) + if t8n.fork.has_block_access_list_hash: + arguments["block_access_list_hash"] = Hash32(b"\0" * 32) + parent_header = t8n.fork.Header(**arguments) self.excess_blob_gas = t8n.fork.calculate_excess_blob_gas( diff --git a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py index c60c266965..cb13727f0d 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -268,6 +268,8 @@ class Result: requests_hash: Optional[Hash32] = None requests: Optional[List[Bytes]] = None block_exception: Optional[str] = None + block_access_list: Optional[Any] = None + block_access_list_hash: Optional[Hash32] = None def get_receipts_from_output( self, @@ -323,6 +325,81 @@ def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: self.requests = block_output.requests self.requests_hash = t8n.fork.compute_requests_hash(self.requests) + if hasattr(block_output, "block_access_list"): + self.block_access_list = block_output.block_access_list + self.block_access_list_hash = ( + t8n.fork.compute_block_access_list_hash( + block_output.block_access_list + ) + ) + + @staticmethod + def _block_access_list_to_json(account_changes: Any) -> Any: + """ + Convert BlockAccessList to JSON format matching the Pydantic models. + """ + json_account_changes = [] + for account in account_changes: + account_data: Dict[str, Any] = { + "address": "0x" + account.address.hex() + } + + if account.storage_changes: + storage_changes = [] + for slot_change in account.storage_changes: + slot_data: Dict[str, Any] = { + "slot": int.from_bytes(slot_change.slot, "big"), + "slotChanges": [], + } + for change in slot_change.changes: + slot_data["slotChanges"].append( + { + "txIndex": int(change.block_access_index), + "postValue": int.from_bytes( + change.new_value, "big" + ), + } + ) + storage_changes.append(slot_data) + account_data["storageChanges"] = storage_changes + + if account.storage_reads: + account_data["storageReads"] = [ + int.from_bytes(slot, "big") + for slot in account.storage_reads + ] + + if account.balance_changes: + account_data["balanceChanges"] = [ + { + "txIndex": int(change.block_access_index), + "postBalance": int(change.post_balance), + } + for change in account.balance_changes + ] + + if account.nonce_changes: + account_data["nonceChanges"] = [ + { + "txIndex": int(change.block_access_index), + "postNonce": int(change.new_nonce), + } + for change in account.nonce_changes + ] + + if account.code_changes: + account_data["codeChanges"] = [ + { + "txIndex": int(change.block_access_index), + "newCode": "0x" + change.new_code.hex(), + } + for change in account.code_changes + ] + + json_account_changes.append(account_data) + + return json_account_changes + def json_encode_receipts(self) -> Any: """ Encode receipts to JSON. @@ -390,4 +467,15 @@ def to_json(self) -> Any: if self.block_exception is not None: data["blockException"] = self.block_exception + if self.block_access_list is not None: + # Convert BAL to JSON format + data["blockAccessList"] = self._block_access_list_to_json( + self.block_access_list + ) + + if self.block_access_list_hash is not None: + data["blockAccessListHash"] = encode_to_hex( + self.block_access_list_hash + ) + return data diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 4f51fd946b..b9dbcf6244 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -1,6 +1,6 @@ """Tests for EIP-7928 using the consistent data class pattern.""" -from typing import Callable, Dict +from typing import Callable import pytest from execution_testing import ( @@ -8,6 +8,7 @@ Account, Address, Alloc, + AuthorizationTuple, BalAccountExpectation, BalBalanceChange, BalCodeChange, @@ -21,9 +22,9 @@ Fork, Hash, Header, - Initcode, Op, Transaction, + add_kzg_version, compute_create_address, ) @@ -217,162 +218,6 @@ def test_bal_code_changes( ) -@pytest.mark.parametrize( - "self_destruct_in_same_tx", [True, False], ids=["same_tx", "new_tx"] -) -@pytest.mark.parametrize( - "pre_funded", [True, False], ids=["pre_funded", "not_pre_funded"] -) -def test_bal_self_destruct( - pre: Alloc, - blockchain_test: BlockchainTestFiller, - self_destruct_in_same_tx: bool, - pre_funded: bool, -) -> None: - """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" - alice = pre.fund_eoa() - bob = pre.fund_eoa(amount=0) - - selfdestruct_code = ( - Op.SLOAD(0x01) # Read from storage slot 0x01 - + Op.SSTORE(0x02, 0x42) # Write to storage slot 0x02 - + Op.SELFDESTRUCT(bob) - ) - # A pre existing self-destruct contract with initial storage - kaboom = pre.deploy_contract(code=selfdestruct_code, storage={0x01: 0x123}) - - # A template for self-destruct contract - self_destruct_init_code = Initcode(deploy_code=selfdestruct_code) - template = pre.deploy_contract(code=self_destruct_init_code) - - transfer_amount = expected_recipient_balance = 100 - pre_fund_amount = 10 - - if self_destruct_in_same_tx: - # The goal is to create a self-destructing contract in the same - # transaction to trigger deletion of code as per EIP-6780. - # The factory contract below creates a new self-destructing - # contract and calls it in this transaction. - - bytecode_size = len(self_destruct_init_code) - factory_bytecode = ( - # Clone template memory - Op.EXTCODECOPY(template, 0, 0, bytecode_size) - # Fund 100 wei and deploy the clone - + Op.CREATE(transfer_amount, 0, bytecode_size) - # Call the clone, which self-destructs - + Op.CALL(100_000, Op.DUP6, 0, 0, 0, 0, 0) - + Op.STOP - ) - - factory = pre.deploy_contract(code=factory_bytecode) - kaboom_same_tx = compute_create_address(address=factory, nonce=1) - - # Determine which account will be self-destructed - self_destructed_account = ( - kaboom_same_tx if self_destruct_in_same_tx else kaboom - ) - - if pre_funded: - expected_recipient_balance += pre_fund_amount - pre.fund_address( - address=self_destructed_account, amount=pre_fund_amount - ) - - tx = Transaction( - sender=alice, - to=factory if self_destruct_in_same_tx else kaboom, - value=transfer_amount, - gas_limit=1_000_000, - gas_price=0xA, - ) - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - bob: BalAccountExpectation( - balance_changes=[ - BalBalanceChange( - tx_index=1, post_balance=expected_recipient_balance - ) - ] - ), - self_destructed_account: BalAccountExpectation( - balance_changes=[ - BalBalanceChange(tx_index=1, post_balance=0) - ] - if pre_funded - else [], - # Accessed slots for same-tx are recorded as reads (0x02) - storage_reads=[0x01, 0x02] - if self_destruct_in_same_tx - else [0x01], - # Storage changes are recorded for non-same-tx - # self-destructs - storage_changes=[ - BalStorageSlot( - slot=0x02, - slot_changes=[ - BalStorageChange(tx_index=1, post_value=0x42) - ], - ) - ] - if not self_destruct_in_same_tx - else [], - code_changes=[], # should not be present - nonce_changes=[], # should not be present - ), - } - ), - ) - - post: Dict[Address, Account] = { - alice: Account(nonce=1), - bob: Account(balance=expected_recipient_balance), - } - - # If the account was self-destructed in the same transaction, - # we expect the account to non-existent and its balance to be 0. - if self_destruct_in_same_tx: - post.update( - { - factory: Account( - nonce=2, # incremented after CREATE - balance=0, # spent on CREATE - code=factory_bytecode, - ), - kaboom_same_tx: Account.NONEXISTENT, # type: ignore - # The pre-existing contract remains unaffected - kaboom: Account( - balance=0, code=selfdestruct_code, storage={0x01: 0x123} - ), - } - ) - else: - post.update( - { - # This contract was self-destructed in a separate tx. - # From EIP 6780: `SELFDESTRUCT` does not delete any data - # (including storage keys, code, or the account itself). - kaboom: Account( - balance=0, - code=selfdestruct_code, - storage={0x01: 0x123, 0x2: 0x42}, - ), - } - ) - - blockchain_test( - pre=pre, - blocks=[block], - post=post, - ) - - @pytest.mark.parametrize( "account_access_opcode", [ @@ -1417,3 +1262,1312 @@ def test_bal_fully_unmutated_account( ) blockchain_test(pre=pre, blocks=[block], post={}) + + +def test_bal_empty_block_no_coinbase( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL correctly handles empty blocks without including coinbase. + + When a block has no transactions and no withdrawals, the coinbase/fee + recipient receives no fees and should not be included in the BAL. + """ + coinbase = pre.fund_eoa(amount=0) + + block = Block( + txs=[], + withdrawals=None, + fee_recipient=coinbase, + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + # Coinbase must NOT be included - receives no fees + coinbase: None, + } + ), + ) + + blockchain_test(pre=pre, blocks=[block], post={}) + + +def test_bal_coinbase_zero_tip( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork: Fork, +) -> None: + """Ensure BAL includes coinbase even when priority fee is zero.""" + alice_initial_balance = 1_000_000 + alice = pre.fund_eoa(amount=alice_initial_balance) + bob = pre.fund_eoa(amount=0) + coinbase = pre.fund_eoa(amount=0) # fee recipient + + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_gas_calculator( + calldata=b"", + contract_creation=False, + access_list=[], + ) + tx_gas_limit = intrinsic_gas + 1000 + + # Calculate base fee + genesis_env = Environment(base_fee_per_gas=0x7) + base_fee_per_gas = fork.base_fee_per_gas_calculator()( + parent_base_fee_per_gas=int(genesis_env.base_fee_per_gas or 0), + parent_gas_used=0, + parent_gas_limit=genesis_env.gas_limit, + ) + + # Set gas_price equal to base_fee so tip = 0 + tx = Transaction( + sender=alice, + to=bob, + value=5, + gas_limit=tx_gas_limit, + gas_price=base_fee_per_gas, + ) + + alice_final_balance = ( + alice_initial_balance - 5 - (intrinsic_gas * base_fee_per_gas) + ) + + block = Block( + txs=[tx], + fee_recipient=coinbase, + header_verify=Header(base_fee_per_gas=base_fee_per_gas), + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=alice_final_balance + ) + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=5) + ] + ), + # Coinbase must be included even with zero tip + coinbase: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1, balance=alice_final_balance), + bob: Account(balance=5), + }, + genesis_environment=genesis_env, + ) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(10**18, id="with_value"), + pytest.param(0, id="no_value"), + ], +) +@pytest.mark.with_all_precompiles +def test_bal_precompile_funded( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + precompile: Address, + value: int, +) -> None: + """ + Ensure BAL records precompile value transfer. + + Alice sends value to precompile (pure value transfer). + If value > 0: BAL must include balance_changes. + If value = 0: BAL must have empty balance_changes. + """ + alice = pre.fund_eoa() + + addr_int = int.from_bytes(precompile, "big") + + # Map precompile addresses to their required minimal input sizes + # - Most precompiles accept zero-padded input of appropriate length + # - For 0x0a (POINT_EVALUATION), use a known valid input from mainnet + if addr_int == 0x0A: + # Valid point evaluation input from mainnet tx: + # https://etherscan.io/tx/0xcb3dc8f3b14f1cda0c16a619a112102a8ec70dce1b3f1b28272227cf8d5fbb0e + tx_data = ( + bytes.fromhex( + # versioned_hash (32) + "018156B94FE9735E573BAB36DAD05D60FEB720D424CCD20AAF719343C31E4246" + ) + + bytes.fromhex( + # z (32) + "019123BCB9D06356701F7BE08B4494625B87A7B02EDC566126FB81F6306E915F" + ) + + bytes.fromhex( + # y (32) + "6C2EB1E94C2532935B8465351BA1BD88EABE2B3FA1AADFF7D1CD816E8315BD38" + ) + + bytes.fromhex( + # kzg_commitment (48) + "A9546D41993E10DF2A7429B8490394EA9EE62807BAE6F326D1044A51581306F58D4B9DFD5931E044688855280FF3799E" + ) + + bytes.fromhex( + # kzg_proof (48) + "A2EA83D9391E0EE42E0C650ACC7A1F842A7D385189485DDB4FD54ADE3D9FD50D608167DCA6C776AAD4B8AD5C20691BFE" + ) + ) + else: + precompile_min_input = { + 0x01: 128, # ECRECOVER + 0x02: 0, # SHA256 (accepts empty) + 0x03: 0, # RIPEMD160 (accepts empty) + 0x04: 0, # IDENTITY (accepts empty) + 0x05: 96, # MODEXP + 0x06: 128, # BN256ADD + 0x07: 96, # BN256MUL + 0x08: 0, # BN256PAIRING (empty is valid) + 0x09: 213, # BLAKE2F + 0x0B: 256, # BLS12_G1_ADD + 0x0C: 160, # BLS12_G1_MSM + 0x0D: 512, # BLS12_G2_ADD + 0x0E: 288, # BLS12_G2_MSM + 0x0F: 384, # BLS12_PAIRING + 0x10: 64, # BLS12_MAP_FP_TO_G1 + 0x11: 128, # BLS12_MAP_FP2_TO_G2 + 0x100: 160, # P256VERIFY + } + + input_size = precompile_min_input.get(addr_int, 0) + tx_data = bytes([0x00] * input_size if input_size > 0 else []) + + tx = Transaction( + sender=alice, + to=precompile, + value=value, + gas_limit=5_000_000, + data=tx_data, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + precompile: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=value) + ] + if value > 0 + else [], + storage_reads=[], + storage_changes=[], + code_changes=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + }, + ) + + +@pytest.mark.parametrize_by_fork( + "precompile", + lambda fork: [ + pytest.param(addr, id=f"0x{int.from_bytes(addr, 'big'):02x}") + for addr in fork.precompiles(block_number=0, timestamp=0) + ], +) +def test_bal_precompile_call( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + precompile: Address, +) -> None: + """ + Ensure BAL records precompile when called via contract. + + Alice calls Oracle contract which calls precompile. + BAL must include precompile with no balance/storage/code changes. + """ + alice = pre.fund_eoa() + + # Oracle contract that calls the precompile + oracle = pre.deploy_contract( + code=Op.CALL(100_000, precompile, 0, 0, 0, 0, 0) + Op.STOP + ) + + tx = Transaction( + sender=alice, + to=oracle, + gas_limit=200_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle: BalAccountExpectation.empty(), + precompile: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + }, + ) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(0, id="zero_value"), + pytest.param(10**18, id="positive_value"), + ], +) +def test_bal_nonexistent_value_transfer( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + value: int, +) -> None: + """ + Ensure BAL captures non-existent account on value transfer. + + Alice sends value directly to non-existent Bob. + """ + alice = pre.fund_eoa() + bob = Address(0xB0B) + + tx = Transaction( + sender=alice, + to=bob, + value=value, + gas_limit=100_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=value) + ] + if value > 0 + else [], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=value) if value > 0 else Account.NONEXISTENT, + }, + ) + + +@pytest.mark.parametrize( + "account_access_opcode", + [ + pytest.param( + lambda target_addr: Op.BALANCE(target_addr), + id="balance", + ), + pytest.param( + lambda target_addr: Op.EXTCODESIZE(target_addr), + id="extcodesize", + ), + pytest.param( + lambda target_addr: Op.EXTCODECOPY(target_addr, 0, 0, 32), + id="extcodecopy", + ), + pytest.param( + lambda target_addr: Op.EXTCODEHASH(target_addr), + id="extcodehash", + ), + pytest.param( + lambda target_addr: Op.STATICCALL(0, target_addr, 0, 0, 0, 0), + id="staticcall", + ), + pytest.param( + lambda target_addr: Op.DELEGATECALL(0, target_addr, 0, 0, 0, 0), + id="delegatecall", + ), + ], +) +def test_bal_nonexistent_account_access_read_only( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + account_access_opcode: Callable[[Address], Op], +) -> None: + """ + Ensure BAL captures non-existent account access via read-only opcodes. + + Alice calls Oracle contract which uses read-only opcodes to access + non-existent Bob (BALANCE, EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, + STATICCALL, DELEGATECALL). + """ + alice = pre.fund_eoa() + bob = Address(0xB0B) + oracle_balance = 2 * 10**18 + + oracle_code = account_access_opcode(bob) + oracle = pre.deploy_contract(code=oracle_code, balance=oracle_balance) + + tx = Transaction( + sender=alice, + to=oracle, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle: BalAccountExpectation.empty(), + bob: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + oracle: Account(balance=oracle_balance), + bob: Account.NONEXISTENT, + }, + ) + + +@pytest.mark.parametrize( + "opcode_type,value", + [ + pytest.param("call", 0, id="call_zero_value"), + pytest.param("call", 10**18, id="call_positive_value"), + pytest.param("callcode", 0, id="callcode_zero_value"), + pytest.param("callcode", 10**18, id="callcode_positive_value"), + ], +) +def test_bal_nonexistent_account_access_value_transfer( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + opcode_type: str, + value: int, +) -> None: + """ + Ensure BAL captures non-existent account access via CALL/CALLCODE + with value. + + Alice calls Oracle contract which uses CALL or CALLCODE to access + non-existent Bob with value transfer. + - CALL: Transfers value from Oracle to Bob + - CALLCODE: Self-transfer (net zero), Bob accessed for code + """ + alice = pre.fund_eoa() + bob = Address(0xB0B) + oracle_balance = 2 * 10**18 + + if opcode_type == "call": + oracle_code = Op.CALL(100_000, bob, value, 0, 0, 0, 0) + else: # callcode + oracle_code = Op.CALLCODE(100_000, bob, value, 0, 0, 0, 0) + + oracle = pre.deploy_contract(code=oracle_code, balance=oracle_balance) + + tx = Transaction( + sender=alice, + to=oracle, + gas_limit=1_000_000, + ) + + # Calculate expected balances + if opcode_type == "call" and value > 0: + # CALL: Oracle loses value, Bob gains value + oracle_final_balance = oracle_balance - value + bob_final_balance = value + bob_has_balance_change = True + oracle_has_balance_change = True + elif opcode_type == "callcode" and value > 0: + # CALLCODE: Self-transfer (net zero), Bob just accessed for code + oracle_final_balance = oracle_balance + bob_final_balance = 0 + bob_has_balance_change = False + oracle_has_balance_change = False + else: + # Zero value + oracle_final_balance = oracle_balance + bob_final_balance = 0 + bob_has_balance_change = False + oracle_has_balance_change = False + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=oracle_final_balance + ) + ] + if oracle_has_balance_change + else [], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=bob_final_balance + ) + ] + if bob_has_balance_change + else [], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + oracle: Account(balance=oracle_final_balance), + bob: Account(balance=bob_final_balance) + if bob_has_balance_change + else Account.NONEXISTENT, + }, + ) + + +def test_bal_multiple_balance_changes_same_account( + pre: Alloc, + fork: Fork, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL correctly tracks multiple balance changes to same account + across multiple transactions. + + An account that receives funds in TX0 and spends them in TX1 should + have TWO balance change entries in the BAL, one for each transaction. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + charlie = pre.fund_eoa(amount=0) + + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + tx_intrinsic_gas = intrinsic_gas_calculator(calldata=b"", access_list=[]) + + # bob receives funds in tx0, then spends everything in tx1 + gas_price = 10 + tx1_gas_cost = tx_intrinsic_gas * gas_price + spend_amount = 100 + funding_amount = tx1_gas_cost + spend_amount + + tx0 = Transaction( + sender=alice, + to=bob, + value=funding_amount, + gas_limit=tx_intrinsic_gas, + gas_price=gas_price, + ) + + tx1 = Transaction( + sender=bob, + to=charlie, + value=spend_amount, + gas_limit=tx_intrinsic_gas, + gas_price=gas_price, + ) + + bob_balance_after_tx0 = funding_amount + bob_balance_after_tx1 = 0 + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx0, tx1], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1) + ], + ), + bob: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=2, post_nonce=1) + ], + balance_changes=[ + BalBalanceChange( + tx_index=1, + post_balance=bob_balance_after_tx0, + ), + BalBalanceChange( + tx_index=2, + post_balance=bob_balance_after_tx1, + ), + ], + ), + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + tx_index=2, post_balance=spend_amount + ) + ], + ), + } + ), + ) + ], + post={ + bob: Account(nonce=1, balance=bob_balance_after_tx1), + charlie: Account(balance=spend_amount), + }, + ) + + +def test_bal_multiple_storage_writes_same_slot( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + Test that BAL tracks multiple writes to the same storage slot across + transactions in the same block. + + Setup: + - Deploy a contract that increments storage slot 1 on each call + - Alice calls the contract 3 times in the same block + - Each call increments slot 1: 0 -> 1 -> 2 -> 3 + + Expected BAL: + - Contract should have 3 storage_changes for slot 1: + * txIndex 1: postValue = 1 + * txIndex 2: postValue = 2 + * txIndex 3: postValue = 3 + """ + alice = pre.fund_eoa(amount=10**18) + + increment_code = Op.SSTORE(1, Op.ADD(Op.SLOAD(1), 1)) + contract = pre.deploy_contract(code=increment_code) + + tx1 = Transaction(sender=alice, to=contract, gas_limit=200_000) + tx2 = Transaction(sender=alice, to=contract, gas_limit=200_000) + tx3 = Transaction(sender=alice, to=contract, gas_limit=200_000) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx1, tx2, tx3], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + BalNonceChange(tx_index=3, post_nonce=3), + ], + ), + contract: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=1, + slot_changes=[ + BalStorageChange( + tx_index=1, post_value=1 + ), + BalStorageChange( + tx_index=2, post_value=2 + ), + BalStorageChange( + tx_index=3, post_value=3 + ), + ], + ), + ], + storage_reads=[], + balance_changes=[], + code_changes=[], + ), + } + ), + ) + ], + post={ + alice: Account(nonce=3), + contract: Account(storage={1: 3}), + }, + ) + + +@pytest.mark.parametrize( + "intermediate_values", + [ + pytest.param([2], id="depth_1"), + pytest.param([2, 3], id="depth_2"), + pytest.param([2, 3, 4], id="depth_3"), + ], +) +def test_bal_nested_delegatecall_storage_writes_net_zero( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + intermediate_values: list, +) -> None: + """ + Test BAL correctly handles nested DELEGATECALL frames where intermediate + frames write different values but the deepest frame reverts to original. + + Each nesting level writes a different intermediate value, and the deepest + frame writes back the original value, resulting in net-zero change. + + Example for depth=2 (intermediate_values=[2, 3]): + - Pre-state: slot 0 = 1 + - Root frame writes: slot 0 = 2 + - Child frame writes: slot 0 = 3 + - Grandchild frame writes: slot 0 = 1 (back to original) + - Expected: No storage_changes (net-zero overall) + """ + alice = pre.fund_eoa() + starting_value = 1 + + # deepest contract writes back to starting_value + deepest_code = Op.SSTORE(0, starting_value) + Op.STOP + next_contract = pre.deploy_contract(code=deepest_code) + delegate_contracts = [next_contract] + + # Build intermediate contracts (in reverse order) that write then + # DELEGATECALL. Skip the first value since that's for the root contract + for value in reversed(intermediate_values[1:]): + code = ( + Op.SSTORE(0, value) + + Op.DELEGATECALL(100_000, next_contract, 0, 0, 0, 0) + + Op.STOP + ) + next_contract = pre.deploy_contract(code=code) + delegate_contracts.append(next_contract) + + # root_contract writes first intermediate value, then DELEGATECALLs + root_contract = pre.deploy_contract( + code=( + Op.SSTORE(0, intermediate_values[0]) + + Op.DELEGATECALL(100_000, next_contract, 0, 0, 0, 0) + + Op.STOP + ), + storage={0: starting_value}, + ) + + tx = Transaction( + sender=alice, + to=root_contract, + gas_limit=500_000, + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + root_contract: BalAccountExpectation( + storage_reads=[0], + storage_changes=[], # validate no changes + ), + } + # All delegate contracts accessed but no changes + for contract in delegate_contracts: + account_expectations[contract] = BalAccountExpectation.empty() + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + alice: Account(nonce=1), + root_contract: Account(storage={0: starting_value}), + }, + ) + + +def test_bal_create_transaction_empty_code( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL does not record spurious code changes when a CREATE transaction + deploys empty code. + """ + alice = pre.fund_eoa() + contract_address = compute_create_address(address=alice, nonce=0) + + tx = Transaction( + sender=alice, + to=None, + data=b"", + gas_limit=100_000, + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract_address: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + code_changes=[], # ensure no code_changes recorded + ), + } + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + alice: Account(nonce=1), + contract_address: Account(nonce=1, code=b""), + }, + ) + + +def test_bal_cross_tx_storage_revert_to_zero( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures storage changes when tx1 writes a non-zero value + and tx2 reverts it back to zero. This is a regression test for the + blobhash scenario where slot changes were being incorrectly filtered + as net-zero across transaction boundaries. + + Tx1: slot 0 = 0x0 -> 0xABCD (change recorded at tx_index=1) + Tx2: slot 0 = 0xABCD -> 0x0 (change MUST be recorded at tx_index=2) + """ + alice = pre.fund_eoa() + + # Contract that writes to slot 0 based on calldata + contract = pre.deploy_contract(code=Op.SSTORE(0, Op.CALLDATALOAD(0))) + + # Tx1: Write slot 0 = 0xABCD + tx1 = Transaction( + sender=alice, + to=contract, + data=Hash(0xABCD), + gas_limit=100_000, + ) + + # Tx2: Write slot 0 = 0x0 (revert to zero) + tx2 = Transaction( + sender=alice, + to=contract, + data=Hash(0x0), + gas_limit=100_000, + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + ], + ), + contract: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0xABCD), + # CRITICAL: tx2's write to 0x0 MUST appear + # even though it returns slot to original value + BalStorageChange(tx_index=2, post_value=0x0), + ], + ), + ], + ), + } + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx1, tx2], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + alice: Account(nonce=2), + contract: Account(storage={0: 0x0}), + }, + ) + + +def test_bal_cross_block_ripemd160_state_leak( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure internal EVM state for RIMPEMD-160 precompile handling does not + leak between blocks. + + The EVM may track internal state related to the Parity Touch Bug (EIP-161) + when calling RIPEMD-160 (0x03) with zero value. If this state is not + properly reset between blocks, it can cause incorrect BAL entries in + subsequent blocks. + + Prerequisites for triggering the bug: + 1. RIPEMD-160 (0x03) must already exist in state before the call. + 2. Block 1 must call RIPEMD-160 with zero value and complete successfully. + 3. Block 2 must have a TX that triggers an exception (not REVERT). + + Expected behavior: + - Block 1: RIPEMD-160 in BAL (legitimate access) + - Block 2: RIPEMD-160 NOT in BAL (never touched in this block) + + Bug behavior: + - Block 2 incorrectly has RIPEMD-160 in its BAL due to leaked + internal state. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa() + # Pre-fund RIPEMD-160 so it exists before the call. + # This is required to trigger the internal state tracking. + ripemd160_addr = Address(0x03) + pre.fund_address(ripemd160_addr, amount=1) + + # Contract that calls RIPEMD-160 with zero value + ripemd_caller = pre.deploy_contract( + code=Op.CALL(50_000, ripemd160_addr, 0, 0, 0, 0, 0) + Op.STOP + ) + # Contract that triggers an exception + # (stack underflow from ADD on empty stack) + exception_contract = pre.deploy_contract(code=Op.ADD) + + # Block 1: Call RIPEMD-160 successfully + block1 = Block( + txs=[ + Transaction( + sender=alice, + to=ripemd_caller, + gas_limit=100_000, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)] + ), + bob: None, + ripemd_caller: BalAccountExpectation.empty(), + ripemd160_addr: BalAccountExpectation.empty(), + } + ), + ) + + # Block 2: Exception triggers internal exception handling. + # If internal state leaked from Block 1, RIPEMD-160 would incorrectly + # appear in Block 2's BAL. + block2 = Block( + txs=[ + Transaction( + sender=bob, + to=exception_contract, + gas_limit=100_000, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: None, + bob: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)] + ), + # this is the important check + ripemd160_addr: None, + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block1, block2], + post={ + alice: Account(nonce=1), + bob: Account(nonce=1), + ripemd160_addr: Account(balance=1), + }, + ) + + +def test_bal_all_transaction_types( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with all 5 tx types in single block. + + Types: Legacy, EIP-2930, EIP-1559, Blob, EIP-7702. + Each tx writes to contract storage. Access list addresses are pre-warmed + but NOT in BAL. + + Expected BAL: + - All 5 senders: nonce_changes + - Contracts 0-3: storage_changes + - Alice (7702): nonce_changes, code_changes (delegation), storage_changes + - Oracle: empty (delegation target, accessed) + """ + from tests.prague.eip7702_set_code_tx.spec import Spec as Spec7702 + + # Create senders for each transaction type + sender_0 = pre.fund_eoa() # Type 0 - Legacy + sender_1 = pre.fund_eoa() # Type 1 - Access List + sender_2 = pre.fund_eoa() # Type 2 - EIP-1559 + sender_3 = pre.fund_eoa() # Type 3 - Blob + sender_4 = pre.fund_eoa() # Type 4 - EIP-7702 + + # Create contracts for each tx type (except 7702 which uses delegation) + contract_code = Op.SSTORE(0x01, Op.CALLDATALOAD(0)) + Op.STOP + contract_0 = pre.deploy_contract(code=contract_code) + contract_1 = pre.deploy_contract(code=contract_code) + contract_2 = pre.deploy_contract(code=contract_code) + contract_3 = pre.deploy_contract(code=contract_code) + + # For Type 4 (EIP-7702): Alice delegates to Oracle + alice = pre.fund_eoa() + oracle = pre.deploy_contract(code=Op.SSTORE(0x01, 0x05) + Op.STOP) + + # Dummy address to warm in access list + warmed_address = pre.fund_eoa(amount=1) + + # TX1: Type 0 - Legacy transaction + tx_type_0 = Transaction( + ty=0, + sender=sender_0, + to=contract_0, + gas_limit=100_000, + gas_price=10, + data=Hash(0x01), # Value to store + ) + + # TX2: Type 1 - Access List transaction (EIP-2930) + tx_type_1 = Transaction( + ty=1, + sender=sender_1, + to=contract_1, + gas_limit=100_000, + gas_price=10, + data=Hash(0x02), + access_list=[ + AccessList( + address=warmed_address, + storage_keys=[], + ) + ], + ) + + # TX3: Type 2 - EIP-1559 Dynamic fee transaction + tx_type_2 = Transaction( + ty=2, + sender=sender_2, + to=contract_2, + gas_limit=100_000, + max_fee_per_gas=50, + max_priority_fee_per_gas=5, + data=Hash(0x03), + ) + + # TX4: Type 3 - Blob transaction (EIP-4844) + # Blob versioned hashes need KZG version prefix (0x01) + blob_hashes = add_kzg_version([Hash(0xBEEF)], 1) + tx_type_3 = Transaction( + ty=3, + sender=sender_3, + to=contract_3, + gas_limit=100_000, + max_fee_per_gas=50, + max_priority_fee_per_gas=5, + max_fee_per_blob_gas=10, + blob_versioned_hashes=blob_hashes, + data=Hash(0x04), + ) + + # TX5: Type 4 - EIP-7702 Set Code transaction + tx_type_4 = Transaction( + ty=4, + sender=sender_4, + to=alice, + gas_limit=100_000, + max_fee_per_gas=50, + max_priority_fee_per_gas=5, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=0, + signer=alice, + ) + ], + ) + + block = Block( + txs=[tx_type_0, tx_type_1, tx_type_2, tx_type_3, tx_type_4], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + # Type 0 sender + sender_0: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Type 1 sender + sender_1: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=2, post_nonce=1)], + ), + # Type 2 sender + sender_2: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=3, post_nonce=1)], + ), + # Type 3 sender + sender_3: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=4, post_nonce=1)], + ), + # Type 4 sender + sender_4: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=5, post_nonce=1)], + ), + # Contract touched by Type 0 + contract_0: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x01) + ], + ) + ], + ), + # Contract touched by Type 1 + contract_1: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=2, post_value=0x02) + ], + ) + ], + ), + # Note: warmed_address from access_list is NOT in BAL + # because access lists pre-warm but don't record in BAL + # Contract touched by Type 2 + warmed_address: None, # explicit check + contract_2: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=3, post_value=0x03) + ], + ) + ], + ), + # Contract touched by Type 3 + contract_3: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=4, post_value=0x04) + ], + ) + ], + ), + # Alice (Type 4 delegation target, executes oracle code) + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=5, post_nonce=1)], + code_changes=[ + BalCodeChange( + tx_index=5, + new_code=Spec7702.delegation_designation(oracle), + ) + ], + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=5, post_value=0x05) + ], + ) + ], + ), + # Oracle (accessed via delegation) + oracle: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + sender_0: Account(nonce=1), + sender_1: Account(nonce=1), + sender_2: Account(nonce=1), + sender_3: Account(nonce=1), + sender_4: Account(nonce=1), + contract_0: Account(storage={0x01: 0x01}), + contract_1: Account(storage={0x01: 0x02}), + contract_2: Account(storage={0x01: 0x03}), + contract_3: Account(storage={0x01: 0x04}), + alice: Account( + nonce=1, + code=Spec7702.delegation_designation(oracle), + storage={0x01: 0x05}, + ), + }, + ) + + +def test_bal_lexicographic_address_ordering( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL enforces strict lexicographic byte-wise address ordering. + + Addresses: addr_low (0x...01), addr_mid (0x...0100), addr_high (0x01...00). + Endian-trap: addr_endian_low (0x01...02), addr_endian_high (0x02...01). + Contract touches them in reverse order to verify sorting. + + Expected BAL order: low < mid < high < endian_low < endian_high. + Catches endianness bugs in address comparison. + """ + alice = pre.fund_eoa() + + # Create addresses with specific byte patterns for lexicographic testing + # In lexicographic (byte-wise) order: low < mid < high + # addr_low: 0x00...01 (rightmost byte = 0x01) + # addr_mid: 0x00...0100 (second-rightmost byte = 0x01) + # addr_high: 0x01...00 (leftmost byte = 0x01) + addr_low = Address("0x0000000000000000000000000000000000000001") + addr_mid = Address("0x0000000000000000000000000000000000000100") + addr_high = Address("0x0100000000000000000000000000000000000000") + + # Endian-trap addresses: byte-reversals to catch byte-order bugs + # addr_endian_low: 0x01...02 (0x01 at byte 0, 0x02 at byte 19) + # addr_endian_high: 0x02...01 (0x02 at byte 0, 0x01 at byte 19) + # Note: reverse(addr_endian_low) = addr_endian_high + # Correct order: endian_low < endian_high (0x01 < 0x02 at byte 0) + # Reversed bytes would incorrectly get opposite order + addr_endian_low = Address("0x0100000000000000000000000000000000000002") + addr_endian_high = Address("0x0200000000000000000000000000000000000001") + + # Give each address a balance so they exist + addr_balance = 100 + pre[addr_low] = Account(balance=addr_balance) + pre[addr_mid] = Account(balance=addr_balance) + pre[addr_high] = Account(balance=addr_balance) + pre[addr_endian_low] = Account(balance=addr_balance) + pre[addr_endian_high] = Account(balance=addr_balance) + + # Contract that accesses addresses in REVERSE lexicographic order + # to verify sorting is applied correctly + contract_code = ( + Op.BALANCE(addr_high) # Access high first + + Op.POP + + Op.BALANCE(addr_low) # Access low second + + Op.POP + + Op.BALANCE(addr_mid) # Access mid third + + Op.POP + # Access endian-trap addresses in reverse order + + Op.BALANCE(addr_endian_high) # Access endian_high before endian_low + + Op.POP + + Op.BALANCE(addr_endian_low) + + Op.POP + + Op.STOP + ) + + contract = pre.deploy_contract(code=contract_code) + + tx = Transaction( + sender=alice, + to=contract, + gas_limit=1_000_000, + ) + + # BAL must be sorted lexicographically by address bytes + # Order: low < mid < high < endian_low < endian_high + # (sorted by raw address bytes, regardless of access order) + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract: BalAccountExpectation.empty(), + # These addresses appear in BAL due to BALANCE access + # The expectation framework verifies correct order + addr_low: BalAccountExpectation.empty(), + addr_mid: BalAccountExpectation.empty(), + addr_high: BalAccountExpectation.empty(), + # Endian-trap addresses: must be sorted correctly despite being + # byte-reversals of each other + addr_endian_low: BalAccountExpectation.empty(), + addr_endian_high: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + contract: Account(), + addr_low: Account(balance=addr_balance), + addr_mid: Account(balance=addr_balance), + addr_high: Account(balance=addr_balance), + addr_endian_low: Account(balance=addr_balance), + addr_endian_high: Account(balance=addr_balance), + }, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py new file mode 100644 index 0000000000..920b8bc344 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py @@ -0,0 +1,260 @@ +""" +Tests for EIP-7928 BAL cross-index tracking. + +Tests that state changes are correctly tracked across different block indices: +- Index 1..N: Regular transactions +- Index N+1: Post-execution system operations + +Includes tests for system contracts (withdrawal/consolidation) cross-index +tracking and NOOP filtering behavior. +""" + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + BalAccountExpectation, + BalStorageChange, + BalStorageSlot, + Block, + BlockAccessListExpectation, + BlockchainTestFiller, + Bytecode, + Op, + Transaction, +) + +from .spec import ref_spec_7928 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path +REFERENCE_SPEC_VERSION = ref_spec_7928.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + +WITHDRAWAL_REQUEST_ADDRESS = Address( + 0x00000961EF480EB55E80D19AD83579A64C007002 +) +CONSOLIDATION_REQUEST_ADDRESS = Address( + 0x0000BBDDC7CE488642FB579F8B00F3A590007251 +) + + +def test_bal_withdrawal_contract_cross_index( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test that the withdrawal system contract shows storage changes at both + index 1 (during transaction) and index 2 (during post-execution). + + This verifies that slots 0x01 and 0x03 are: + 1. Incremented during the transaction (index 1) + 2. Reset during post-execution (index 2) + """ + sender = pre.fund_eoa() + + withdrawal_calldata = ( + (b"\x01" + b"\x00" * 47) # validator pubkey + + (b"\x00" * 8) # amount + ) + + tx = Transaction( + sender=sender, + to=WITHDRAWAL_REQUEST_ADDRESS, + value=1, + data=withdrawal_calldata, + gas_limit=1_000_000, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + WITHDRAWAL_REQUEST_ADDRESS: BalAccountExpectation( + # slots 0x01 and 0x03 change at BOTH indices + storage_changes=[ + BalStorageSlot( + slot=0x01, # Request count + slot_changes=[ + BalStorageChange( + # Incremented during tx + tx_index=1, + post_value=1, + ), + BalStorageChange( + # Reset during post-exec + tx_index=2, + post_value=0, + ), + ], + ), + BalStorageSlot( + slot=0x03, # Target count + slot_changes=[ + BalStorageChange( + # Incremented during tx + tx_index=1, + post_value=1, + ), + BalStorageChange( + # Reset during post-exec + tx_index=2, + post_value=0, + ), + ], + ), + ], + ), + } + ), + ) + ], + post={}, + ) + + +def test_bal_consolidation_contract_cross_index( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test that the consolidation system contract shows storage changes at both + index 1 (during transaction) and index 2 (during post-execution). + """ + sender = pre.fund_eoa() + + consolidation_calldata = ( + (b"\x01" + b"\x00" * 47) # source pubkey + + (b"\x02" + b"\x00" * 47) # target pubkey + ) + + tx = Transaction( + sender=sender, + to=CONSOLIDATION_REQUEST_ADDRESS, + value=1, + data=consolidation_calldata, + gas_limit=1_000_000, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + CONSOLIDATION_REQUEST_ADDRESS: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange( + # Incremented during tx + tx_index=1, + post_value=1, + ), + BalStorageChange( + # Reset during post-exec + tx_index=2, + post_value=0, + ), + ], + ), + BalStorageSlot( + slot=0x03, + slot_changes=[ + BalStorageChange( + # Incremented during tx + tx_index=1, + post_value=1, + ), + BalStorageChange( + # Reset during post-exec + tx_index=2, + post_value=0, + ), + ], + ), + ], + ), + } + ), + ) + ], + post={}, + ) + + +def test_bal_noop_write_filtering( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test that NOOP writes (writing same value or 0 to empty) are filtered. + + This verifies that: + 1. Writing 0 to an uninitialized slot doesn't appear in BAL + 2. Writing the same value to a slot doesn't appear in BAL + 3. Only actual changes are tracked + """ + test_code = Bytecode( + # Write 0 to uninitialized slot 1 (noop) + Op.SSTORE(1, 0) + # Write 42 to slot 2 + + Op.SSTORE(2, 42) + # Write 100 to slot 3 (will be same as pre-state, should be filtered) + + Op.SSTORE(3, 100) + # Write 200 to slot 4 (different from pre-state 150, should appear) + + Op.SSTORE(4, 200) + ) + + sender = pre.fund_eoa() + test_address = pre.deploy_contract( + code=test_code, + storage={3: 100, 4: 150}, + ) + + tx = Transaction( + sender=sender, + to=test_address, + gas_limit=100_000, + ) + + # Expected BAL should only show actual changes + expected_block_access_list = BlockAccessListExpectation( + account_expectations={ + test_address: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=2, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=42), + ], + ), + BalStorageSlot( + slot=4, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=200), + ], + ), + ], + ), + } + ) + + block = Block( + txs=[tx], + expected_block_access_list=expected_block_access_list, + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + test_address: Account(storage={2: 42, 3: 100, 4: 200}), + }, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py new file mode 100644 index 0000000000..edb8295c17 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py @@ -0,0 +1,810 @@ +"""Tests for the effects of EIP-4895 withdrawals on EIP-7928.""" + +import pytest +from execution_testing import ( + EOA, + Account, + Address, + Alloc, + BalAccountExpectation, + BalBalanceChange, + BalCodeChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + Block, + BlockAccessListExpectation, + BlockchainTestFiller, + Environment, + Fork, + Header, + Initcode, + Op, + Transaction, + Withdrawal, + compute_create_address, +) + +from .spec import ref_spec_7928 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path +REFERENCE_SPEC_VERSION = ref_spec_7928.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + +GWEI = 10**9 + + +def test_bal_withdrawal_empty_block( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures withdrawal balance changes in empty block. + + Charlie starts with 1 gwei balance (existing account). + Block with 0 transactions and 1 withdrawal of 10 gwei to Charlie. + Charlie ends with 11 gwei balance. + """ + charlie = pre.fund_eoa(amount=1 * GWEI) + + block = Block( + txs=[], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=charlie, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=11 * GWEI) + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + charlie: Account(balance=11 * GWEI), + }, + ) + + +def test_bal_withdrawal_and_transaction( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures both transaction and withdrawal balance changes. + + Alice starts with 1 ETH, Bob starts with 0, Charlie starts with 0. + Alice sends 5 wei to Bob. + Charlie receives 10 gwei withdrawal. + Bob ends with 5 wei, Charlie ends with 10 gwei. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + charlie = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=alice, + to=bob, + value=5, + max_fee_per_gas=50, + max_priority_fee_per_gas=5, + ) + + block = Block( + txs=[tx], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=charlie, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=5) + ], + ), + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=2, post_balance=10 * GWEI) + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=5), + charlie: Account(balance=10 * GWEI), + }, + ) + + +def test_bal_withdrawal_to_nonexistent_account( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures withdrawal to non-existent account. + + Charlie is a non-existent address (not in pre-state). + Block with 0 transactions and 1 withdrawal of 10 gwei to Charlie. + Charlie ends with 10 gwei balance. + """ + charlie = Address(0xCC) + + block = Block( + txs=[], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=charlie, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10 * GWEI) + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + charlie: Account(balance=10 * GWEI), + }, + ) + + +def test_bal_withdrawal_no_evm_execution( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures withdrawal without triggering EVM execution. + + Oracle contract starts with 0 balance and storage slot 0x01 = 0x42. + Oracle's code writes 0xFF to slot 0x01 when called. + Block with 0 transactions and 1 withdrawal of 10 gwei to Oracle. + Storage slot 0x01 remains 0x42 (EVM never executes). + """ + oracle = pre.deploy_contract( + code=Op.SSTORE(0x01, 0xFF), + storage={0x01: 0x42}, + ) + + block = Block( + txs=[], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=oracle, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + oracle: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10 * GWEI) + ], + storage_reads=[], + storage_changes=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + oracle: Account( + balance=10 * GWEI, + storage={0x01: 0x42}, + ), + }, + ) + + +def test_bal_withdrawal_and_state_access_same_account( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures both state access and withdrawal to same address. + + Oracle contract starts with 0 balance and storage slot 0x01 = 0x42. + Alice calls Oracle (reads slot 0x01, writes 0x99 to slot 0x02). + Oracle receives withdrawal of 10 gwei. + Both state access and withdrawal are captured in BAL. + """ + alice = pre.fund_eoa() + oracle = pre.deploy_contract( + code=Op.SLOAD(0x01) + Op.SSTORE(0x02, 0x99), + storage={0x01: 0x42}, + ) + + tx = Transaction( + sender=alice, + to=oracle, + gas_limit=1_000_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=oracle, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle: BalAccountExpectation( + storage_reads=[0x01], + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x99) + ], + ) + ], + balance_changes=[ + BalBalanceChange(tx_index=2, post_balance=10 * GWEI) + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + oracle: Account( + balance=10 * GWEI, + storage={0x01: 0x42, 0x02: 0x99}, + ), + }, + ) + + +def test_bal_withdrawal_and_value_transfer_same_address( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures both value transfer and withdrawal to same address. + + Alice starts with 1 ETH, Bob starts with 0. + Alice sends 5 gwei to Bob. + Bob receives withdrawal of 10 gwei. + Bob ends with 15 gwei (5 from tx + 10 from withdrawal). + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=alice, + to=bob, + value=5 * GWEI, + gas_price=0xA, + ) + + block = Block( + txs=[tx], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=bob, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=5 * GWEI), + BalBalanceChange(tx_index=2, post_balance=15 * GWEI), + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=15 * GWEI), + }, + ) + + +def test_bal_multiple_withdrawals_same_address( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL accumulates multiple withdrawals to same address. + + Charlie starts with 0 balance. + Block empty block with 3 withdrawals to Charlie: 5 gwei, 10 gwei, 15 gwei. + Charlie ends with 30 gwei balance (cumulative). + """ + charlie = pre.fund_eoa(amount=0) + + block = Block( + txs=[], + withdrawals=[ + Withdrawal(index=i, validator_index=i, address=charlie, amount=amt) + for i, amt in enumerate([5, 10, 15]) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=30 * GWEI) + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + charlie: Account(balance=30 * GWEI), + }, + ) + + +def test_bal_withdrawal_and_selfdestruct( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures withdrawal to self-destructed contract address. + + Oracle contract starts with 100 gwei balance. + Alice triggers Oracle to self-destruct, sending balance to Bob. + Oracle receives withdrawal of 50 gwei after self-destructing. + Oracle ends with 50 gwei (funded by withdrawal). + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + oracle = pre.deploy_contract( + balance=100 * GWEI, + code=Op.SELFDESTRUCT(bob), + ) + + tx = Transaction( + sender=alice, + to=oracle, + gas_limit=1_000_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=oracle, + amount=50, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=100 * GWEI) + ], + ), + oracle: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=0), + BalBalanceChange(tx_index=2, post_balance=50 * GWEI), + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=100 * GWEI), + oracle: Account(balance=50 * GWEI), + }, + ) + + +def test_bal_withdrawal_and_new_contract( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures withdrawal to newly created contract. + + Alice deploys Oracle contract with 5 gwei initial balance. + Oracle receives withdrawal of 10 gwei in same block. + Oracle ends with 15 gwei (5 from deployment + 10 from withdrawal). + """ + alice = pre.fund_eoa() + + code = Op.STOP + initcode = Initcode(deploy_code=code) + oracle = compute_create_address(address=alice) + + tx = Transaction( + sender=alice, + to=None, + data=initcode, + value=5 * GWEI, + gas_limit=1_000_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=oracle, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle: BalAccountExpectation( + code_changes=[BalCodeChange(tx_index=1, new_code=code)], + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=5 * GWEI), + BalBalanceChange(tx_index=2, post_balance=15 * GWEI), + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + oracle: Account(balance=15 * GWEI, code=code), + }, + ) + + +@pytest.mark.parametrize( + "initial_balance", + [ + pytest.param(5 * GWEI, id="existing_account"), + pytest.param(0, id="nonexistent_account"), + ], +) +def test_bal_zero_withdrawal( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + initial_balance: int, +) -> None: + """ + Ensure BAL handles zero-amount withdrawal correctly. + + Charlie either exists with initial balance or is non-existent. + Block with 0 transactions and 1 zero-amount withdrawal to Charlie. + Charlie appears in BAL but with empty changes, balance unchanged. + """ + if initial_balance > 0: + charlie = pre.fund_eoa(amount=initial_balance) + else: + charlie = EOA(0xCC) + + block = Block( + txs=[], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=charlie, + amount=0, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + charlie: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + charlie: Account(balance=initial_balance) + if initial_balance > 0 + else Account.NONEXISTENT, + }, + ) + + +@pytest.mark.parametrize_by_fork( + "precompile", + lambda fork: [ + pytest.param(addr, id=f"0x{int.from_bytes(addr, 'big'):02x}") + for addr in fork.precompiles(block_number=0, timestamp=0) + ], +) +def test_bal_withdrawal_to_precompiles( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + precompile: Address, +) -> None: + """ + Ensure BAL captures withdrawal to precompile addresses. + + Block with 0 transactions and 1 withdrawal of 10 gwei to precompile. + Precompile ends with 10 gwei balance. + """ + block = Block( + txs=[], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=precompile, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + precompile: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10 * GWEI) + ], + storage_reads=[], + storage_changes=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + precompile: Account(balance=10 * GWEI), + }, + ) + + +def test_bal_withdrawal_largest_amount( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures withdrawal with largest amount. + + Block with 0 transactions and 1 withdrawal of maximum + uint64 value (2^64-1)Gwei to Charlie. + Charlie ends with (2^64-1) Gwei. + """ + charlie = pre.fund_eoa(amount=0) + max_amount = 2**64 - 1 + + block = Block( + txs=[], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=charlie, + amount=max_amount, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + charlie: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=max_amount * GWEI + ) + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + charlie: Account(balance=max_amount * GWEI), + }, + ) + + +def test_bal_withdrawal_to_coinbase( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork: Fork, +) -> None: + """ + Ensure BAL captures withdrawal to coinbase address. + + Block with 1 transaction and 1 withdrawal to coinbase/fee recipient. + Coinbase receives both transaction fees and withdrawal. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + coinbase = pre.fund_eoa(amount=0) + + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_gas_calculator() + tx_gas_limit = intrinsic_gas + 1000 + gas_price = 0xA + + tx = Transaction( + sender=alice, + to=bob, + value=5, + gas_limit=tx_gas_limit, + gas_price=gas_price, + ) + + # Calculate tip to coinbase + genesis_env = Environment(base_fee_per_gas=0x7) + base_fee_per_gas = fork.base_fee_per_gas_calculator()( + parent_base_fee_per_gas=int(genesis_env.base_fee_per_gas or 0), + parent_gas_used=0, + parent_gas_limit=genesis_env.gas_limit, + ) + tip_to_coinbase = (gas_price - base_fee_per_gas) * intrinsic_gas + coinbase_final_balance = tip_to_coinbase + (10 * GWEI) + + block = Block( + txs=[tx], + fee_recipient=coinbase, + header_verify=Header(base_fee_per_gas=base_fee_per_gas), + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=coinbase, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=5) + ], + ), + coinbase: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=tip_to_coinbase + ), + BalBalanceChange( + tx_index=2, post_balance=coinbase_final_balance + ), + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=5), + coinbase: Account(balance=coinbase_final_balance), + }, + genesis_environment=genesis_env, + ) + + +def test_bal_withdrawal_to_coinbase_empty_block( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures withdrawal to coinbase when there are no transactions. + + Empty block with 1 withdrawal of 10 gwei to coinbase/fee recipient. + Coinbase receives only withdrawal (no transaction fees). + """ + coinbase = pre.fund_eoa(amount=0) + + block = Block( + txs=[], + fee_recipient=coinbase, + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=coinbase, + amount=10, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + coinbase: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10 * GWEI) + ], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + coinbase: Account(balance=10 * GWEI), + }, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py index 5ff9e7b135..a0a6a1e121 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -16,6 +16,7 @@ BlockchainTestFiller, Op, Transaction, + Withdrawal, ) from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 @@ -608,3 +609,465 @@ def test_bal_7702_delegated_via_call_opcode( blocks=[block], post=post, ) + + +def test_bal_7702_null_address_delegation_no_code_change( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL does not record spurious code changes when delegating to + NULL_ADDRESS (sets code to empty on an account that already has + empty code). + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=alice, + to=bob, + value=10, + gas_limit=1_000_000, + authorization_list=[ + AuthorizationTuple( + address=0, + nonce=1, + signer=alice, + ) + ], + ) + + # `alice` should appear in BAL with nonce change only, NOT code change + # because setting code from b"" to b"" is a net-zero change + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + code_changes=[], # explicit check for no code changes + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + } + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + alice: Account(nonce=2, code=b""), + bob: Account(balance=10), + }, + ) + + +def test_bal_7702_double_auth_reset( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures the net code change when multiple authorizations + occur in the same transaction (double auth). + + This test verifies that when: + 1. First auth sets delegation to CONTRACT_A + 2. Second auth resets delegation to empty (address 0) + + The BAL should show the NET change (empty -> empty), not intermediate + states. This is a regression test for the bug where the BAL showed + the first auth's code but the final state was empty. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + relayer = pre.fund_eoa() + + contract_a = pre.deploy_contract(code=Op.STOP) + + # Transaction with double auth: + # 1. First sets delegation to contract_a + # 2. Second resets to empty + tx = Transaction( + sender=relayer, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=contract_a, + nonce=0, + signer=alice, + ), + AuthorizationTuple( + address=0, # Reset to empty + nonce=1, + signer=alice, + ), + ], + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=2) + ], + code_changes=[], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10) + ] + ), + relayer: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1) + ], + ), + contract_a: None, + } + ), + ) + ], + post={ + alice: Account(nonce=2, code=b""), # Final code is empty + bob: Account(balance=10), + relayer: Account(nonce=1), + }, + ) + + +def test_bal_7702_double_auth_swap( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures the net code change when double auth swaps + delegation targets. + + This test verifies that when: + 1. First auth sets delegation to CONTRACT_A + 2. Second auth changes delegation to CONTRACT_B + + The BAL should show the final code change (empty -> CONTRACT_B), + not the intermediate CONTRACT_A. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + relayer = pre.fund_eoa() + + contract_a = pre.deploy_contract(code=Op.STOP) + contract_b = pre.deploy_contract(code=Op.STOP) + + tx = Transaction( + sender=relayer, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=contract_a, + nonce=0, + signer=alice, + ), + AuthorizationTuple( + address=contract_b, # Override to contract_b + nonce=1, + signer=alice, + ), + ], + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + code_changes=[ + # Should show final code (CONTRACT_B), not CONTRACT_A + BalCodeChange( + tx_index=1, + new_code=Spec7702.delegation_designation(contract_b), + ) + ], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Neither contract appears in BAL during delegation setup + contract_a: None, + contract_b: None, + } + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + alice: Account( + nonce=2, code=Spec7702.delegation_designation(contract_b) + ), + bob: Account(balance=10), + relayer: Account(nonce=1), + }, + ) + + +def test_bal_selfdestruct_to_7702_delegation( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with SELFDESTRUCT to 7702 delegated account. + + Tx1: Alice delegates to Oracle. + Tx2: Victim (balance=100) selfdestructs to Alice. + SELFDESTRUCT transfers balance without executing recipient code. + + Expected BAL: + - Alice tx1: code_changes (delegation), nonce_changes + - Alice tx2: balance_changes (+100) + - Victim tx2: balance_changes (100β†’0) + - Oracle: MUST NOT appear (SELFDESTRUCT doesn't execute recipient code) + """ + # Alice (EOA) will receive delegation then receive selfdestruct balance + # Use explicit initial balance for clarity + alice_initial_balance = 10**18 # 1 ETH default + alice = pre.fund_eoa(amount=alice_initial_balance) + bob = pre.fund_eoa(amount=0) # Just to be the recipient of tx + + # Oracle contract that Alice will delegate to + oracle = pre.deploy_contract(code=Op.SSTORE(0x01, 0x42) + Op.STOP) + + victim_balance = 100 + + # Victim contract that selfdestructs to Alice + victim = pre.deploy_contract( + code=Op.SELFDESTRUCT(alice), + balance=victim_balance, + ) + + # Relayer for tx1 (delegation) + relayer = pre.fund_eoa() + + # Tx1: Alice authorizes delegation to Oracle + tx1 = Transaction( + sender=relayer, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=0, + signer=alice, + ) + ], + ) + + # Caller contract that triggers selfdestruct on victim + caller = pre.deploy_contract(code=Op.CALL(100_000, victim, 0, 0, 0, 0, 0)) + + # Tx2: Trigger selfdestruct on victim (victim sends balance to Alice) + tx2 = Transaction( + nonce=1, + sender=relayer, + to=caller, + gas_limit=1_000_000, + gas_price=0xA, + ) + + alice_final_balance = alice_initial_balance + victim_balance + + account_expectations = { + alice: BalAccountExpectation( + # tx1: nonce change for auth, code change for delegation + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + code_changes=[ + BalCodeChange( + tx_index=1, + new_code=Spec7702.delegation_designation(oracle), + ) + ], + # tx2: balance change from selfdestruct + balance_changes=[ + BalBalanceChange(tx_index=2, post_balance=alice_final_balance) + ], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + ], + ), + caller: BalAccountExpectation.empty(), + # Victim (selfdestructing contract): balance changes to 0 + # Explicitly verify ALL fields to avoid false positives + victim: BalAccountExpectation( + nonce_changes=[], # Contract nonce unchanged + balance_changes=[BalBalanceChange(tx_index=2, post_balance=0)], + code_changes=[], # Code unchanged (post-Cancun SELFDESTRUCT) + storage_changes=[], # No storage changes + storage_reads=[], # No storage reads + ), + # Oracle MUST NOT appear in tx2 - SELFDESTRUCT doesn't execute + # recipient code, so delegation target is never accessed + oracle: None, + } + + block = Block( + txs=[tx1, tx2], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + alice: Account( + nonce=1, + code=Spec7702.delegation_designation(oracle), + balance=alice_final_balance, + ), + bob: Account(balance=10), + relayer: Account(nonce=2), + # Victim still exists but with 0 balance (post-Cancun SELFDESTRUCT) + victim: Account(balance=0), + } + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +GWEI = 10**9 + + +def test_bal_withdrawal_to_7702_delegation( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with withdrawal to 7702 delegated account. + + Tx1: Alice delegates to Oracle. Withdrawal: 10 gwei to Alice. + Withdrawals credit balance without executing code. + + Expected BAL: + - Alice tx1: code_changes (delegation), nonce_changes + - Alice tx2: balance_changes (+10 gwei) + - Oracle: MUST NOT appear (withdrawals don't execute recipient code) + """ + # Alice (EOA) will receive delegation then receive withdrawal + alice_initial_balance = 10**18 # 1 ETH default + alice = pre.fund_eoa(amount=alice_initial_balance) + bob = pre.fund_eoa(amount=0) # Recipient of tx value + + # Oracle contract that Alice will delegate to + # If delegation were followed, this would write to storage + oracle = pre.deploy_contract(code=Op.SSTORE(0x01, 0x42) + Op.STOP) + + # Relayer for the delegation tx + relayer = pre.fund_eoa() + + withdrawal_amount_gwei = 10 + + # Tx1: Alice authorizes delegation to Oracle + tx1 = Transaction( + sender=relayer, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=0, + signer=alice, + ) + ], + ) + + alice_final_balance = alice_initial_balance + ( + withdrawal_amount_gwei * GWEI + ) + + account_expectations = { + alice: BalAccountExpectation( + # tx1: nonce change for auth, code change for delegation + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + code_changes=[ + BalCodeChange( + tx_index=1, + new_code=Spec7702.delegation_designation(oracle), + ) + ], + # tx2 (withdrawal): balance change + balance_changes=[ + BalBalanceChange(tx_index=2, post_balance=alice_final_balance) + ], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Oracle MUST NOT appear - withdrawals don't execute recipient code, + # so delegation target is never accessed + oracle: None, + } + + block = Block( + txs=[tx1], + withdrawals=[ + Withdrawal( + index=0, + validator_index=0, + address=alice, + amount=withdrawal_amount_gwei, + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + alice: Account( + nonce=1, + code=Spec7702.delegation_designation(oracle), + balance=alice_final_balance, + ), + bob: Account(balance=10), + relayer: Account(nonce=1), + } + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py index 5e5ebaefe3..1c12cec89e 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py @@ -15,12 +15,16 @@ """ from enum import Enum +from typing import Callable, Dict import pytest from execution_testing import ( Account, + Address, Alloc, BalAccountExpectation, + BalBalanceChange, + BalNonceChange, BalStorageChange, BalStorageSlot, Block, @@ -28,8 +32,10 @@ BlockchainTestFiller, Bytecode, Fork, + Initcode, Op, Transaction, + compute_create_address, ) from .spec import ref_spec_7928 @@ -526,18 +532,45 @@ def test_bal_delegatecall_and_oog( @pytest.mark.parametrize( - "fails_at_extcodecopy", - [True, False], - ids=["oog_at_extcodecopy", "successful_extcodecopy"], + "oog_scenario,memory_offset,copy_size", + [ + pytest.param("success", 0, 0, id="successful_extcodecopy"), + pytest.param("oog_at_cold_access", 0, 0, id="oog_at_cold_access"), + pytest.param( + "oog_at_memory_large_offset", + 0x10000, + 32, + id="oog_at_memory_large_offset", + ), + pytest.param( + "oog_at_memory_boundary", + 256, + 32, + id="oog_at_memory_boundary", + ), + ], ) def test_bal_extcodecopy_and_oog( pre: Alloc, blockchain_test: BlockchainTestFiller, fork: Fork, - fails_at_extcodecopy: bool, + oog_scenario: str, + memory_offset: int, + copy_size: int, ) -> None: """ Ensure BAL handles EXTCODECOPY and OOG during EXTCODECOPY appropriately. + + Tests various OOG scenarios: + - success: EXTCODECOPY completes, target appears in BAL + - oog_at_cold_access: OOG before cold access, target NOT in BAL + - oog_at_memory_large_offset: OOG at memory expansion (large offset), + target NOT in BAL + - oog_at_memory_boundary: OOG at memory expansion (boundary case), + target NOT in BAL + + Gas for all components (cold access + copy + memory expansion) must be + checked BEFORE recording account access. """ alice = pre.fund_eoa() gas_costs = fork.gas_costs() @@ -547,13 +580,20 @@ def test_bal_extcodecopy_and_oog( code=Bytecode(Op.PUSH1(0x42) + Op.STOP) ) - # Create contract that attempts to copy code from target + # Build EXTCODECOPY contract with appropriate PUSH sizes + if memory_offset <= 0xFF: + dest_push = Op.PUSH1(memory_offset) + elif memory_offset <= 0xFFFF: + dest_push = Op.PUSH2(memory_offset) + else: + dest_push = Op.PUSH3(memory_offset) + extcodecopy_contract_code = Bytecode( - Op.PUSH1(0) # size - copy 0 bytes to minimize memory expansion cost + Op.PUSH1(copy_size) + Op.PUSH1(0) # codeOffset - + Op.PUSH1(0) # destOffset - + Op.PUSH20(target_contract) # address - + Op.EXTCODECOPY # Copy code (cold access + base cost) + + dest_push # destOffset + + Op.PUSH20(target_contract) + + Op.EXTCODECOPY + Op.STOP ) @@ -562,19 +602,37 @@ def test_bal_extcodecopy_and_oog( intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() intrinsic_gas_cost = intrinsic_gas_calculator() - # Costs: - # - 4 PUSH operations = G_VERY_LOW * 4 - # - EXTCODECOPY cold = G_COLD_ACCOUNT_ACCESS + (G_COPY * words) - # where words = ceil32(size) // 32 = ceil32(0) // 32 = 0 + # Calculate costs push_cost = gas_costs.G_VERY_LOW * 4 - extcodecopy_cold_cost = ( - gas_costs.G_COLD_ACCOUNT_ACCESS - ) # + (G_COPY * 0) = 0 - tx_gas_limit = intrinsic_gas_cost + push_cost + extcodecopy_cold_cost - - if fails_at_extcodecopy: - # subtract 1 gas to ensure OOG at EXTCODECOPY - tx_gas_limit -= 1 + cold_access_cost = gas_costs.G_COLD_ACCOUNT_ACCESS + copy_cost = gas_costs.G_COPY * ((copy_size + 31) // 32) + + if oog_scenario == "success": + # Provide enough gas for everything including memory expansion + words = (memory_offset + copy_size + 31) // 32 + memory_cost = (words * gas_costs.G_MEMORY) + (words * words // 512) + execution_cost = push_cost + cold_access_cost + copy_cost + memory_cost + tx_gas_limit = intrinsic_gas_cost + execution_cost + target_in_bal = True + elif oog_scenario == "oog_at_cold_access": + # Provide gas for pushes but 1 less than cold access cost + execution_cost = push_cost + cold_access_cost + tx_gas_limit = intrinsic_gas_cost + execution_cost - 1 + target_in_bal = False + elif oog_scenario == "oog_at_memory_large_offset": + # Provide gas for push + cold access + copy, but NOT memory expansion + execution_cost = push_cost + cold_access_cost + copy_cost + tx_gas_limit = intrinsic_gas_cost + execution_cost + target_in_bal = False + elif oog_scenario == "oog_at_memory_boundary": + # Calculate memory cost and provide exactly 1 less than needed + words = (memory_offset + copy_size + 31) // 32 + memory_cost = (words * gas_costs.G_MEMORY) + (words * words // 512) + execution_cost = push_cost + cold_access_cost + copy_cost + memory_cost + tx_gas_limit = intrinsic_gas_cost + execution_cost - 1 + target_in_bal = False + else: + raise ValueError(f"Invariant: unknown oog_scenario {oog_scenario}") tx = Transaction( sender=alice, @@ -587,11 +645,10 @@ def test_bal_extcodecopy_and_oog( expected_block_access_list=BlockAccessListExpectation( account_expectations={ extcodecopy_contract: BalAccountExpectation.empty(), - # Target should only appear if EXTCODECOPY succeeded **( - {target_contract: None} - if fails_at_extcodecopy - else {target_contract: BalAccountExpectation.empty()} + {target_contract: BalAccountExpectation.empty()} + if target_in_bal + else {target_contract: None} ), } ), @@ -606,3 +663,1220 @@ def test_bal_extcodecopy_and_oog( target_contract: Account(), }, ) + + +@pytest.mark.parametrize( + "self_destruct_in_same_tx", [True, False], ids=["same_tx", "new_tx"] +) +@pytest.mark.parametrize( + "pre_funded", [True, False], ids=["pre_funded", "not_pre_funded"] +) +def test_bal_self_destruct( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_destruct_in_same_tx: bool, + pre_funded: bool, +) -> None: + """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + selfdestruct_code = ( + Op.SLOAD(0x01) # Read from storage slot 0x01 + + Op.SSTORE(0x02, 0x42) # Write to storage slot 0x02 + + Op.SELFDESTRUCT(bob) + ) + # A pre existing self-destruct contract with initial storage + kaboom = pre.deploy_contract(code=selfdestruct_code, storage={0x01: 0x123}) + + # A template for self-destruct contract + self_destruct_init_code = Initcode(deploy_code=selfdestruct_code) + template = pre.deploy_contract(code=self_destruct_init_code) + + transfer_amount = expected_recipient_balance = 100 + pre_fund_amount = 10 + + if self_destruct_in_same_tx: + # The goal is to create a self-destructing contract in the same + # transaction to trigger deletion of code as per EIP-6780. + # The factory contract below creates a new self-destructing + # contract and calls it in this transaction. + + bytecode_size = len(self_destruct_init_code) + factory_bytecode = ( + # Clone template memory + Op.EXTCODECOPY(template, 0, 0, bytecode_size) + # Fund 100 wei and deploy the clone + + Op.CREATE(transfer_amount, 0, bytecode_size) + # Call the clone, which self-destructs + + Op.CALL(1_000_000, Op.DUP6, 0, 0, 0, 0, 0) + + Op.STOP + ) + + factory = pre.deploy_contract(code=factory_bytecode) + kaboom_same_tx = compute_create_address(address=factory, nonce=1) + + # Determine which account will be self-destructed + self_destructed_account = ( + kaboom_same_tx if self_destruct_in_same_tx else kaboom + ) + + if pre_funded: + expected_recipient_balance += pre_fund_amount + pre.fund_address( + address=self_destructed_account, amount=pre_fund_amount + ) + + tx = Transaction( + sender=alice, + to=factory if self_destruct_in_same_tx else kaboom, + value=transfer_amount, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=expected_recipient_balance + ) + ] + ), + self_destructed_account: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=0) + ] + if pre_funded + else [], + # Accessed slots for same-tx are recorded as reads (0x02) + storage_reads=[0x01, 0x02] + if self_destruct_in_same_tx + else [0x01], + # Storage changes are recorded for non-same-tx + # self-destructs + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ] + if not self_destruct_in_same_tx + else [], + code_changes=[], # should not be present + nonce_changes=[], # should not be present + ), + } + ), + ) + + post: Dict[Address, Account] = { + alice: Account(nonce=1), + bob: Account(balance=expected_recipient_balance), + } + + # If the account was self-destructed in the same transaction, + # we expect the account to non-existent and its balance to be 0. + if self_destruct_in_same_tx: + post.update( + { + factory: Account( + nonce=2, # incremented after CREATE + balance=0, # spent on CREATE + code=factory_bytecode, + ), + kaboom_same_tx: Account.NONEXISTENT, # type: ignore + # The pre-existing contract remains unaffected + kaboom: Account( + balance=0, code=selfdestruct_code, storage={0x01: 0x123} + ), + } + ) + else: + post.update( + { + # This contract was self-destructed in a separate tx. + # From EIP 6780: `SELFDESTRUCT` does not delete any data + # (including storage keys, code, or the account itself). + kaboom: Account( + balance=0, + code=selfdestruct_code, + storage={0x01: 0x123, 0x2: 0x42}, + ), + } + ) + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +@pytest.mark.parametrize("oog_before_state_access", [True, False]) +def test_bal_self_destruct_oog( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork: Fork, + oog_before_state_access: bool, +) -> None: + """ + Test SELFDESTRUCT BAL behavior at gas boundaries. + + SELFDESTRUCT has two gas checkpoints: + 1. static checks: G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS + OOG here = no state access, beneficiary NOT in BAL + 2. state access: same as static checks, plus G_NEW_ACCOUNT for new account + OOG here = enough gas to access state but not enough for new account, + beneficiary IS in BAL + """ + alice = pre.fund_eoa() + # always use new account so we incur extra G_NEW_ACCOUNT cost + # there is no other gas boundary to test between cold access + # and new account + beneficiary = pre.empty_account() + + # selfdestruct_contract: PUSH20 SELFDESTRUCT + selfdestruct_code = Op.SELFDESTRUCT(beneficiary) + selfdestruct_contract = pre.deploy_contract( + code=selfdestruct_code, balance=1000 + ) + + # Gas needed inside the CALL for SELFDESTRUCT: + # - PUSH20: G_VERY_LOW = 3 + # - SELFDESTRUCT: G_SELF_DESTRUCT + # - G_COLD_ACCOUNT_ACCESS (beneficiary cold access) + gas_costs = fork.gas_costs() + exact_static_gas = ( + gas_costs.G_VERY_LOW + + gas_costs.G_SELF_DESTRUCT + + gas_costs.G_COLD_ACCOUNT_ACCESS + ) + + # subtract one from the exact gas to trigger OOG before state access + oog_gas = ( + exact_static_gas - 1 if oog_before_state_access else exact_static_gas + ) + + # caller_contract: CALL with oog_gas + caller_code = Op.CALL(gas=oog_gas, address=selfdestruct_contract) + caller_contract = pre.deploy_contract(code=caller_code) + + tx = Transaction( + sender=alice, + to=caller_contract, + gas_limit=100_000, + ) + + account_expectations: Dict[Address, BalAccountExpectation | None] = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + caller_contract: BalAccountExpectation.empty(), + selfdestruct_contract: BalAccountExpectation.empty(), + # beneficiary only in BAL if we passed check_gas (state accessed) + beneficiary: None + if oog_before_state_access + else BalAccountExpectation.empty(), + } + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + caller_contract: Account(code=caller_code), + # selfdestruct_contract still exists - SELFDESTRUCT failed + selfdestruct_contract: Account( + balance=1000, code=selfdestruct_code + ), + }, + ) + + +def test_bal_storage_write_read_same_frame( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL captures write precedence over read in same call frame. + + Oracle writes to slot 0x01, then reads from slot 0x01 in same call. + The write shadows the read - only the write appears in BAL. + """ + alice = pre.fund_eoa() + + oracle_code = ( + Op.SSTORE(0x01, 0x42) # Write 0x42 to slot 0x01 + + Op.SLOAD(0x01) # Read from slot 0x01 + + Op.STOP + ) + oracle = pre.deploy_contract(code=oracle_code, storage={0x01: 0x99}) + + tx = Transaction( + sender=alice, + to=oracle, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ], + storage_reads=[], # Empty! Write shadows the read + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + oracle: Account(storage={0x01: 0x42}), + }, + ) + + +@pytest.mark.parametrize( + "call_opcode", + [ + pytest.param( + lambda target: Op.CALL(100_000, target, 0, 0, 0, 0, 0), id="call" + ), + pytest.param( + lambda target: Op.DELEGATECALL(100_000, target, 0, 0, 0, 0), + id="delegatecall", + ), + pytest.param( + lambda target: Op.CALLCODE(100_000, target, 0, 0, 0, 0, 0), + id="callcode", + ), + ], +) +def test_bal_storage_write_read_cross_frame( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + call_opcode: Callable[[Bytecode], Bytecode], +) -> None: + """ + Ensure BAL captures write precedence over read across call frames. + + Frame 1: Read slot 0x01 (0x99), write 0x42, then call itself. + Frame 2: Read slot 0x01 (0x42), see it's 0x42 and return. + Both reads are shadowed by the write - only write appears in BAL. + """ + alice = pre.fund_eoa() + + # Oracle code: + # 1. Read slot 0x01 (initial: 0x99, recursive: 0x42) + # 2. If value == 0x42, return (exit recursion) + # 3. Write 0x42 to slot 0x01 + # 4. Call itself recursively + oracle_code = ( + Op.SLOAD(0x01) # Load value from slot 0x01 + + Op.PUSH1(0x42) # Push 0x42 for comparison + + Op.EQ # Check if loaded value == 0x42 + + Op.PUSH1(0x1D) # Jump destination (after SSTORE + CALL) + + Op.JUMPI # If equal, jump to end (exit recursion) + + Op.PUSH1(0x42) # Value to write + + Op.PUSH1(0x01) # Slot 0x01 + + Op.SSTORE # Write 0x42 to slot 0x01 + + call_opcode(Op.ADDRESS) # Call itself + + Op.JUMPDEST # Jump destination for exit + + Op.STOP + ) + + oracle = pre.deploy_contract(code=oracle_code, storage={0x01: 0x99}) + + tx = Transaction( + sender=alice, + to=oracle, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ], + storage_reads=[], # Empty! Write shadows both reads + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + oracle: Account(storage={0x01: 0x42}), + }, + ) + + +def test_bal_create_oog_code_deposit( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork: Fork, +) -> None: + """ + Ensure BAL correctly handles CREATE that runs out of gas during code + deposit. The contract address should appear with empty changes (read + during collision check) but no nonce or code changes (rolled back). + """ + alice = pre.fund_eoa() + + # create init code that returns a very large contract to force OOG + deposited_len = 10_000 + initcode = Op.RETURN(0, deposited_len) + + factory = pre.deploy_contract( + code=Op.MSTORE(0, Op.PUSH32(bytes(initcode))) + + Op.SSTORE( + 1, Op.CREATE(offset=32 - len(initcode), size=len(initcode)) + ) + + Op.STOP, + storage={1: 0xDEADBEEF}, + ) + + contract_address = compute_create_address(address=factory, nonce=1) + + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_gas_calculator( + calldata=b"", + contract_creation=False, + access_list=[], + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=intrinsic_gas + 500_000, # insufficient for deposit + ) + + # BAL expectations: + # - Alice: nonce change (tx sender) + # - Factory: nonce change (CREATE increments factory nonce) + # - Contract address: empty changes (read during collision check, + # nonce/code changes rolled back on OOG) + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + storage_changes=[ + BalStorageSlot( + slot=1, + slot_changes=[ + # SSTORE saves 0 (CREATE failed) + BalStorageChange(tx_index=1, post_value=0), + ], + ) + ], + ), + contract_address: BalAccountExpectation.empty(), + } + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + ], + post={ + alice: Account(nonce=1), + factory: Account(nonce=2, storage={1: 0}), + contract_address: Account.NONEXISTENT, + }, + ) + + +def test_bal_sstore_static_context( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Ensure BAL does not record storage reads when SSTORE fails in static + context. + + Contract A makes STATICCALL to Contract B. Contract B attempts SSTORE, + which should fail immediately without recording any storage reads. + """ + alice = pre.fund_eoa() + + contract_b = pre.deploy_contract(code=Op.SSTORE(0, 5)) + + # Contract A makes STATICCALL to Contract B + # The STATICCALL will fail because B tries SSTORE in static context + # But contract_a continues and writes to its own storage + contract_a = pre.deploy_contract( + code=Op.STATICCALL( + gas=1_000_000, + address=contract_b, + args_offset=0, + args_size=0, + ret_offset=0, + ret_size=0, + ) + + Op.POP # pop the return value (0 = failure) + + Op.SSTORE(0, 1) # this should succeed (non-static context) + ) + + tx = Transaction( + sender=alice, + to=contract_a, + gas_limit=2_000_000, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1) + ], + ), + contract_a: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange( + tx_index=1, post_value=1 + ), + ], + ), + ], + ), + contract_b: BalAccountExpectation.empty(), + } + ), + ) + ], + post={ + contract_a: Account(storage={0: 1}), + contract_b: Account(storage={0: 0}), # SSTORE failed + }, + ) + + +def test_bal_create_contract_init_revert( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + Test that BAL does not include nonce/code changes when CREATE happens + in a call that then REVERTs. + """ + alice = pre.fund_eoa(amount=10**18) + + # Simple init code that returns STOP as deployed code + init_code_bytes = bytes(Op.RETURN(0, 1) + Op.STOP) + + # Factory that does CREATE then REVERTs + factory = pre.deploy_contract( + code=Op.MSTORE(0, Op.PUSH32(init_code_bytes)) + + Op.POP(Op.CREATE(0, 32 - len(init_code_bytes), len(init_code_bytes))) + + Op.REVERT(0, 0) + ) + + # A caller that CALLs factory to CREATE then REVERT + caller = pre.deploy_contract(code=Op.CALL(address=factory)) + + created_address = compute_create_address(address=factory, nonce=1) + + tx = Transaction( + sender=alice, + to=caller, + gas_limit=500_000, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1) + ], + ), + caller: BalAccountExpectation.empty(), + factory: BalAccountExpectation.empty(), + created_address: BalAccountExpectation.empty(), + } + ), + ) + ], + post={ + alice: Account(nonce=1), + caller: Account(nonce=1), + factory: Account(nonce=1), + created_address: Account.NONEXISTENT, + }, + ) + + +def test_bal_call_revert_insufficient_funds( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CALL failure due to insufficient balance (not OOG). + + Contract (balance=100): SLOAD(0x01)β†’CALL(target, value=1000)β†’SSTORE(0x02). + CALL fails because 1000 > 100. Target is 0xDEAD. + + Expected BAL: + - Contract: storage_reads [0x01], storage_changes slot 0x02 (value=0) + - Target: appears in BAL (accessed before balance check fails) + """ + alice = pre.fund_eoa() + + contract_balance = 100 + transfer_amount = 1000 # More than contract has + + # Target address that should be warmed but not receive funds + # Give it a small balance so it's not considered "empty" and pruned + target_balance = 1 + target_address = pre.fund_eoa(amount=target_balance) + + # Contract that: + # 1. SLOAD slot 0x01 + # 2. CALL target with value=1000 (will fail - insufficient funds) + # 3. SSTORE slot 0x02 with CALL result (0 = failure) + contract_code = ( + Op.SLOAD(0x01) # Read from slot 0x01, push to stack + + Op.POP # Discard value + # CALL(gas, addr, value, argsOffset, argsSize, retOffset, retSize) + + Op.CALL(100_000, target_address, transfer_amount, 0, 0, 0, 0) + # CALL result is on stack (0 = failure, 1 = success) + # Stack: [result] + + Op.PUSH1(0x02) # Push slot number + # Stack: [0x02, result] + + Op.SSTORE # SSTORE pops slot (0x02), then value (result) + + Op.STOP + ) + + contract = pre.deploy_contract( + code=contract_code, + balance=contract_balance, + storage={ + 0x02: 0xDEAD + }, # Non-zero initial value so SSTORE(0) is a change + ) + + tx = Transaction( + sender=alice, + to=contract, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract: BalAccountExpectation( + # Storage read for slot 0x01 + storage_reads=[0x01], + # Storage change for slot 0x02 (CALL result = 0) + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Target appears in BAL - accessed before balance check fails + target_address: BalAccountExpectation.empty(), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + contract: Account( + balance=contract_balance, # Unchanged - transfer failed + storage={0x02: 0}, # CALL returned 0 (failure) + ), + target_address: Account(balance=target_balance), # Unchanged + }, + ) + + +def test_bal_create_selfdestruct_to_self_with_call( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with init code that CALLs Oracle, writes storage, then + SELFDESTRUCTs to self. + + Factory CREATE2(endowment=100). + Init: CALL(Oracle)β†’SSTORE(0x01)β†’SELFDESTRUCT(SELF). + + Expected BAL: + - Factory: nonce_changes, balance_changes (loses 100) + - Oracle: storage_changes slot 0x01 + - Created address: storage_reads [0x01] (aborted writeβ†’read), + MUST NOT have nonce/code/storage/balance changes (ephemeral) + """ + alice = pre.fund_eoa() + factory_balance = 1000 + + # Oracle contract that writes to slot 0x01 when called + oracle_code = Op.SSTORE(0x01, 0x42) + Op.STOP + oracle = pre.deploy_contract(code=oracle_code) + + endowment = 100 + + # Init code that: + # 1. Calls Oracle (which writes to its slot 0x01) + # 2. Writes 0x42 to own slot 0x01 + # 3. Selfdestructs to self + initcode_runtime = ( + # CALL(gas, Oracle, value=0, ...) + Op.CALL(100_000, oracle, 0, 0, 0, 0, 0) + + Op.POP + # Write to own storage slot 0x01 + + Op.SSTORE(0x01, 0x42) + # SELFDESTRUCT to self (ADDRESS returns own address) + + Op.SELFDESTRUCT(Op.ADDRESS) + ) + init_code = Initcode(deploy_code=Op.STOP, initcode_prefix=initcode_runtime) + init_code_bytes = bytes(init_code) + init_code_size = len(init_code_bytes) + + # Factory code with embedded initcode (no template contract needed) + # Structure: [execution code] [initcode bytes] + # CODECOPY copies initcode from factory's own code to memory + # + # Two-pass approach: build with placeholder, measure, rebuild + placeholder_offset = 0xFF # Placeholder (same byte size as final value) + factory_execution_template = ( + Op.CODECOPY(0, placeholder_offset, init_code_size) + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=endowment, + offset=0, + size=init_code_size, + salt=0, + ), + ) + + Op.STOP + ) + # Measure execution code size + execution_code_size = len(bytes(factory_execution_template)) + + # Rebuild with actual offset value + factory_execution = ( + Op.CODECOPY(0, execution_code_size, init_code_size) + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=endowment, + offset=0, + size=init_code_size, + salt=0, + ), + ) + + Op.STOP + ) + # Combine execution code with embedded initcode + factory_code = bytes(factory_execution) + init_code_bytes + + factory = pre.deploy_contract(code=factory_code, balance=factory_balance) + + # Calculate the CREATE2 target address + created_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=init_code_bytes, + opcode=Op.CREATE2, + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + # Balance changes: loses endowment (100) + balance_changes=[ + BalBalanceChange( + tx_index=1, + post_balance=factory_balance - endowment, + ) + ], + ), + # Oracle: storage changes for slot 0x01 + oracle: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ], + ), + # Created address: ephemeral (created and destroyed same tx) + # - storage_reads for slot 0x01 (aborted write becomes read) + # - NO nonce/code/storage/balance changes + created_address: BalAccountExpectation( + storage_reads=[0x01], + storage_changes=[], + nonce_changes=[], + code_changes=[], + balance_changes=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + factory: Account(nonce=2, balance=factory_balance - endowment), + oracle: Account(storage={0x01: 0x42}), + # Created address doesn't exist - destroyed in same tx + created_address: Account.NONEXISTENT, + }, + ) + + +def test_bal_create2_collision( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CREATE2 collision against pre-existing contract. + + Pre-existing contract has code=STOP, nonce=1. + Factory (nonce=1, slot[0]=0xDEAD) executes CREATE2 targeting it. + + Expected BAL: + - Factory: nonce_changes (1β†’2), storage_changes slot 0 (0xDEADβ†’0) + - Collision address: empty (accessed during collision check) + - Collision address MUST NOT have nonce_changes or code_changes + """ + alice = pre.fund_eoa() + + # Init code that deploys simple STOP contract + init_code = Initcode(deploy_code=Op.STOP) + init_code_bytes = bytes(init_code) + + # Factory code: CREATE2 and store result in slot 0 + factory_code = ( + # Push init code to memory + Op.MSTORE(0, Op.PUSH32(init_code_bytes)) + # SSTORE(0, CREATE2(...)) - stores CREATE2 result in slot 0 + + Op.SSTORE( + 0x00, + Op.CREATE2( + value=0, + offset=32 - len(init_code_bytes), + size=len(init_code_bytes), + salt=0, + ), + ) + + Op.STOP + ) + + # Deploy factory - it starts with nonce=1 by default + factory = pre.deploy_contract( + code=factory_code, + storage={0x00: 0xDEAD}, # Initial value to prove SSTORE works + ) + + # Calculate the CREATE2 target address + collision_address = compute_create_address( + address=factory, + nonce=1, + salt=0, + initcode=init_code_bytes, + opcode=Op.CREATE2, + ) + + # Set up the collision by pre-populating the target address + # This contract has code (STOP) and nonce=1, causing collision + pre[collision_address] = Account( + code=Op.STOP, + nonce=1, + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + # Nonce incremented 1β†’2 even on failed CREATE2 + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + # Storage changes: slot 0 = 0xDEAD β†’ 0 (CREATE2 returned 0) + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Collision address: empty (accessed but no state changes) + # Explicitly verify ALL fields are empty + collision_address: BalAccountExpectation( + nonce_changes=[], # MUST NOT have nonce changes + balance_changes=[], # MUST NOT have balance changes + code_changes=[], # MUST NOT have code changes + storage_changes=[], # MUST NOT have storage changes + storage_reads=[], # MUST NOT have storage reads + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + factory: Account(nonce=2, storage={0x00: 0}), + # Collision address unchanged - contract still exists + collision_address: Account(code=bytes(Op.STOP), nonce=1), + }, + ) + + +def test_bal_transient_storage_not_tracked( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL excludes EIP-1153 transient storage (TSTORE/TLOAD). + + Contract: TSTORE(0x01, 0x42)β†’TLOAD(0x01)β†’SSTORE(0x02, result). + + Expected BAL: + - storage_changes: slot 0x02 (persistent) + - MUST NOT include slot 0x01 (transient storage not persisted) + """ + alice = pre.fund_eoa() + + # Contract that uses transient storage then persists to regular storage + contract_code = ( + # TSTORE slot 0x01 with value 0x42 (transient storage) + Op.TSTORE(0x01, 0x42) + # TLOAD slot 0x01 (transient storage read) + + Op.TLOAD(0x01) + # Result (0x42) is on stack, store it in persistent slot 0x02 + + Op.PUSH1(0x02) + + Op.SSTORE # SSTORE pops slot (0x02), then value (0x42) + + Op.STOP + ) + + contract = pre.deploy_contract(code=contract_code) + + tx = Transaction( + sender=alice, + to=contract, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + contract: BalAccountExpectation( + # Persistent storage change for slot 0x02 + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x42) + ], + ) + ], + # MUST NOT include slot 0x01 in storage_reads + # Transient storage operations don't pollute BAL + storage_reads=[], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + contract: Account(storage={0x02: 0x42}), + }, + ) + + +def test_bal_selfdestruct_to_precompile( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with SELFDESTRUCT to precompile (ecrecover 0x01). + + Victim (balance=100) selfdestructs to precompile 0x01. + + Expected BAL: + - Victim: balance_changes (100β†’0) + - Precompile 0x01: balance_changes (0β†’100), no code/nonce changes + """ + alice = pre.fund_eoa() + + contract_balance = 100 + ecrecover_precompile = Address(1) # 0x0000...0001 + + # Contract that selfdestructs to ecrecover precompile + victim_code = Op.SELFDESTRUCT(ecrecover_precompile) + + victim = pre.deploy_contract(code=victim_code, balance=contract_balance) + + # Caller that triggers the selfdestruct + caller_code = Op.CALL(100_000, victim, 0, 0, 0, 0, 0) + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + tx = Transaction( + sender=alice, + to=caller, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + caller: BalAccountExpectation.empty(), + # Victim (selfdestructing contract): balance changes 100β†’0 + # Explicitly verify ALL fields to avoid false positives + victim: BalAccountExpectation( + nonce_changes=[], # Contract nonce unchanged + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=0) + ], + code_changes=[], # Code unchanged (post-Cancun) + storage_changes=[], # No storage changes + storage_reads=[], # No storage reads + ), + # Precompile receives selfdestruct balance + # Explicitly verify ALL fields to avoid false positives + ecrecover_precompile: BalAccountExpectation( + nonce_changes=[], # MUST NOT have nonce changes + balance_changes=[ + BalBalanceChange( + tx_index=1, post_balance=contract_balance + ) + ], + code_changes=[], # MUST NOT have code changes + storage_changes=[], # MUST NOT have storage changes + storage_reads=[], # MUST NOT have storage reads + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + caller: Account(), + # Victim still exists with 0 balance (post-Cancun SELFDESTRUCT) + victim: Account(balance=0), + # Precompile has received the balance + ecrecover_precompile: Account(balance=contract_balance), + }, + ) + + +def test_bal_create_early_failure( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +) -> None: + """ + Test BAL with CREATE failure due to insufficient endowment. + + Factory (balance=50) attempts CREATE(value=100). + Fails before nonce increment (before track_address). + Distinct from collision where address IS accessed. + + Expected BAL: + - Alice: nonce_changes + - Factory: storage_changes slot 0 (0xDEADβ†’0), NO nonce_changes + - Contract address: MUST NOT appear (never accessed) + """ + alice = pre.fund_eoa() + + factory_balance = 50 + endowment = 100 # More than factory has + + # Simple init code that deploys STOP + init_code = Initcode(deploy_code=Op.STOP) + init_code_bytes = bytes(init_code) + + # Factory code: CREATE(value=endowment) and store result in slot 0 + factory_code = ( + # Push init code to memory + Op.MSTORE(0, Op.PUSH32(init_code_bytes)) + # SSTORE(0, CREATE(value, offset, size)) + + Op.SSTORE( + 0x00, + Op.CREATE( + value=endowment, # 100 > 50, will fail + offset=32 - len(init_code_bytes), + size=len(init_code_bytes), + ), + ) + + Op.STOP + ) + + # Deploy factory with insufficient balance for the CREATE endowment + factory = pre.deploy_contract( + code=factory_code, + balance=factory_balance, + storage={0x00: 0xDEAD}, # Initial value to prove SSTORE works + ) + + # Calculate what the contract address WOULD be (but it won't be created) + would_be_contract_address = compute_create_address( + address=factory, nonce=1 + ) + + tx = Transaction( + sender=alice, + to=factory, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + factory: BalAccountExpectation( + # NO nonce_changes - CREATE failed before increment_nonce + nonce_changes=[], + # Storage changes: slot 0 = 0xDEAD β†’ 0 (CREATE returned 0) + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0) + ], + ) + ], + ), + # Contract address MUST NOT appear in BAL - never accessed + # (CREATE failed before track_address was called) + would_be_contract_address: None, + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + # Factory nonce unchanged (still 1), balance unchanged + factory: Account( + nonce=1, balance=factory_balance, storage={0x00: 0} + ), + # Contract was never created + would_be_contract_address: Account.NONEXISTENT, + }, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md index 945330b628..29d6b2c187 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -29,8 +29,8 @@ | `test_bal_pure_contract_call` | Ensure BAL captures contract access for pure computation calls | Alice calls `PureContract` that performs pure arithmetic (ADD operation) without storage or balance changes | BAL MUST include Alice and `PureContract` in `account_changes`, and `nonce_changes` for Alice. | βœ… Completed | | `test_bal_create2_to_A_read_then_selfdestruct` | BAL records balance change for A and storage access (no persistent change) | Tx0: Alice sends ETH to address **A**. Tx1: Deployer `CREATE2` a contract **at A**; contract does `SLOAD(B)` and immediately `SELFDESTRUCT(beneficiary=X)` in the same tx. | BAL **MUST** include **A** with `balance_changes` (funding in Tx0 and transfer on selfdestruct in Tx1). BAL **MUST** include storage key **B** as an accessed `StorageKey`, and **MUST NOT** include **B** under `storage_changes` (no persistence due to same-tx create+destruct). | 🟑 Planned | | `test_bal_create2_to_A_write_then_selfdestruct` | BAL records balance change for A and storage access even if a write occurred (no persistent change) | Tx0: Alice sends ETH to **A**. Tx1: Deployer `CREATE2` contract **at A**; contract does `SSTORE(B, v)` (optionally `SLOAD(B)`), then `SELFDESTRUCT(beneficiary=Y)` in the same tx. | BAL **MUST** include **A** with `balance_changes` (Tx0 fund; Tx1 outflow to `Y`). BAL **MUST** include **B** as `StorageKey` accessed, and **MUST NOT** include **B** under `storage_changes` (ephemeral write discarded because the contract was created and destroyed in the same tx). | 🟑 Planned | -| `test_bal_precompile_funded_then_called` | BAL records precompile with balance change (fund) and access (call) | **Tx0**: Alice sends `1 ETH` to `ecrecover` (0x01). **Tx1**: Alice (or Bob) calls `ecrecover` with valid input and `0 ETH`. | BAL **MUST** include address `0x01` with `balance_changes` (from Tx0). No `storage_changes` or `code_changes`. | 🟑 Planned | -| `test_bal_precompile_call_only` | BAL records precompile when called with no balance change | Alice calls `ecrecover` (0x01) with a valid input, sending **0 ETH**. | BAL **MUST** include address `0x01` in access list, with **no** `balance_changes`, `storage_changes`, or `code_changes`. | 🟑 Planned | +| `test_bal_precompile_funded` | BAL records precompile value transfer with or without balance change | Alice sends value to precompile (all precompiles) via direct transaction. Parameterized: (1) with value (1 ETH), (2) without value (0 ETH). | For with_value: BAL **MUST** include precompile with `balance_changes`. For no_value: BAL **MUST** include precompile with empty `balance_changes`. No `storage_changes` or `code_changes` in either case. | βœ… Completed | +| `test_bal_precompile_call` | BAL records precompile when called via contract | Alice calls Oracle contract which calls precompile (all precompiles) via CALL opcode with 0 ETH | BAL **MUST** include Alice with `nonce_changes`, Oracle with empty changes, and precompile with empty changes. No `balance_changes`, `storage_changes`, or `code_changes` for precompile. | βœ… Completed | | `test_bal_7702_delegated_create` | BAL tracks EIP-7702 delegation indicator write and contract creation | Alice sends a type-4 (7702) tx authorizing herself to delegate to `Deployer` code which executes `CREATE` | BAL MUST include for **Alice**: `code_changes` (delegation indicator), `nonce_changes` (increment from 7702 processing), and `balance_changes` (post-gas). For **Child**: `code_changes` (runtime bytecode) and `nonce_changes = 1`. | 🟑 Planned | | `test_bal_7702_delegation_create` | Ensure BAL captures creation of EOA delegation | Alice authorizes delegation to contract `Oracle`. Transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends 7702 tx herself. (2) Sponsored: `Relayer` sends 7702 tx on Alice's behalf. | BAL **MUST** include Alice: `code_changes` (delegation designation `0xef0100\|\|address(Oracle)`),`nonce_changes` (increment). Bob: `balance_changes` (receives 10 wei). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes`.`Oracle` **MUST NOT** be present in BAL - the account is never accessed. | βœ… Completed | | `test_bal_7702_delegation_update` | Ensure BAL captures update of existing EOA delegation | Alice first delegates to `Oracle1`, then in second tx updates delegation to `Oracle2`. Each transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends both 7702 txs herself. (2) Sponsored: `Relayer` sends both 7702 txs on Alice's behalf. | BAL **MUST** include Alice: first tx has `code_changes` (delegation designation `0xef0100\|\|address(Oracle1)`),`nonce_changes`. Second tx has`code_changes` (delegation designation `0xef0100\|\|address(Oracle2)`),`nonce_changes`. Bob:`balance_changes` (receives 10 wei on each tx). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes` for both transactions. `Oracle1` and `Oracle2` **MUST NOT** be present in BAL - accounts are never accessed. | βœ… Completed | @@ -39,15 +39,26 @@ | `test_bal_7702_invalid_nonce_authorization` | Ensure BAL handles failed authorization due to wrong nonce | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect nonce, causing silent authorization failure | BAL **MUST** include Alice with empty changes (account access), Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include `Oracle` (authorization failed, no delegation) | βœ… Completed | | `test_bal_7702_invalid_chain_id_authorization` | Ensure BAL handles failed authorization due to wrong chain id | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect chain id, causing authorization failure before account access | BAL **MUST** include Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include Alice (authorization fails before loading account) or `Oracle` (authorization failed, no delegation) | βœ… Completed | | `test_bal_7702_delegated_via_call_opcode` | Ensure BAL captures delegation target when a contract uses *CALL opcodes to call a delegated account | Pre-deployed contract `Alice` delegated to `Oracle`. `Caller` contract uses CALL/CALLCODE/DELEGATECALL/STATICCALL to call `Alice`. Bob sends transaction to `Caller`. | BAL **MUST** include Bob: `nonce_changes`. `Caller`: empty changes (account access). `Alice`: empty changes (account access - delegated account being called). `Oracle`: empty changes (delegation target access). | βœ… Completed | +| `test_bal_7702_null_address_delegation` | Ensure BAL does not record spurious code changes for net-zero code operations | Alice sends transaction with authorization delegating to NULL_ADDRESS (0x0), which sets code to `b""` on an account that already has `b""` code. Transaction sends 10 wei to Bob. | BAL **MUST** include Alice with `nonce_changes` (tx nonce + auth nonce increment) but **MUST NOT** include `code_changes` (setting `b"" -> b""` is net-zero and filtered out). Bob: `balance_changes` (receives 10 wei). This ensures net-zero code change is not recorded. | βœ… Completed | +| `test_bal_7702_double_auth_reset` | Ensure BAL captures net code change when double auth resets delegation | `Relayer` sends transaction with two authorizations for Alice: (1) First auth sets delegation to `CONTRACT_A` at nonce=0, (2) Second auth resets delegation to empty (address 0) at nonce=1. Transaction sends 10 wei to Bob. Per EIP-7702, only the last authorization takes effect. | BAL **MUST** include Alice with `nonce_changes` (both auths increment nonce to 2) but **MUST NOT** include `code_changes` (net change is empty β†’ empty). Bob: `balance_changes` (receives 10 wei). Relayer: `nonce_changes`. `CONTRACT_A` **MUST NOT** be in BAL (never accessed). This is a regression test for the bug where BAL showed first auth's code despite final state being empty. | βœ… Completed | +| `test_bal_7702_double_auth_swap` | Ensure BAL captures final code when double auth swaps delegation targets | `Relayer` sends transaction with two authorizations for Alice: (1) First auth sets delegation to `CONTRACT_A` at nonce=0, (2) Second auth changes delegation to `CONTRACT_B` at nonce=1. Transaction sends 10 wei to Bob. Per EIP-7702, only the last authorization takes effect. | BAL **MUST** include Alice with `nonce_changes` (both auths increment nonce to 2) and `code_changes` (final code is delegation designation for `CONTRACT_B`, not `CONTRACT_A`). Bob: `balance_changes` (receives 10 wei). Relayer: `nonce_changes`. Neither `CONTRACT_A` nor `CONTRACT_B` appear in BAL during delegation setup (never accessed). This ensures BAL shows final state, not intermediate changes. | βœ… Completed | | `test_bal_sstore_and_oog` | Ensure BAL handles OOG during SSTORE execution at various gas boundaries (EIP-2200 stipend and implicit SLOAD) | Alice calls contract that attempts `SSTORE` to cold slot `0x01`. Parameterized: (1) OOG at EIP-2200 stipend check (2300 gas after PUSH opcodes) - fails before implicit SLOAD, (2) OOG at stipend + 1 (2301 gas) - passes stipend check but fails after implicit SLOAD, (3) OOG at exact gas - 1, (4) Successful SSTORE with exact gas. | For case (1): BAL **MUST NOT** include slot `0x01` in `storage_reads` or `storage_changes` (fails before implicit SLOAD). For cases (2) and (3): BAL **MUST** include slot `0x01` in `storage_reads` (implicit SLOAD occurred) but **MUST NOT** include in `storage_changes` (write didn't complete). For case (4): BAL **MUST** include slot `0x01` in `storage_changes` only (successful write; read is filtered by builder). | βœ… Completed | +| `test_bal_sstore_static_context` | Ensure BAL does not capture spurious storage access when SSTORE fails in static context | Alice calls contract with `STATICCALL` which attempts `SSTORE` to slot `0x01`. SSTORE must fail before any storage access occurs. | BAL **MUST NOT** include slot `0x01` in `storage_reads` or `storage_changes`. Static context check happens before storage access, preventing spurious reads. Alice has `nonce_changes` and `balance_changes` (gas cost). Target contract included with empty changes. | βœ… Completed | | `test_bal_sload_and_oog` | Ensure BAL handles OOG during SLOAD execution correctly | Alice calls contract that attempts `SLOAD` from cold slot `0x01`. Parameterized: (1) OOG at SLOAD opcode (insufficient gas), (2) Successful SLOAD execution. | For OOG case: BAL **MUST NOT** contain slot `0x01` in `storage_reads` since storage wasn't accessed. For success case: BAL **MUST** contain slot `0x01` in `storage_reads`. | βœ… Completed | | `test_bal_balance_and_oog` | Ensure BAL handles OOG during BALANCE opcode execution correctly | Alice calls contract that attempts `BALANCE` opcode on cold target account. Parameterized: (1) OOG at BALANCE opcode (insufficient gas), (2) Successful BALANCE execution. | For OOG case: BAL **MUST NOT** include target account (wasn't accessed). For success case: BAL **MUST** include target account in `account_changes`. | βœ… Completed | | `test_bal_extcodesize_and_oog` | Ensure BAL handles OOG during EXTCODESIZE opcode execution correctly | Alice calls contract that attempts `EXTCODESIZE` opcode on cold target contract. Parameterized: (1) OOG at EXTCODESIZE opcode (insufficient gas), (2) Successful EXTCODESIZE execution. | For OOG case: BAL **MUST NOT** include target contract (wasn't accessed). For success case: BAL **MUST** include target contract in `account_changes`. | βœ… Completed | | `test_bal_call_and_oog` | Ensure BAL handles OOG during CALL opcode execution correctly | Alice calls contract that attempts `CALL` to cold target contract. Parameterized: (1) OOG at CALL opcode (insufficient gas), (2) Successful CALL execution. | For OOG case: BAL **MUST NOT** include target contract (wasn't accessed). For success case: BAL **MUST** include target contract in `account_changes`. | βœ… Completed | | `test_bal_delegatecall_and_oog` | Ensure BAL handles OOG during DELEGATECALL opcode execution correctly | Alice calls contract that attempts `DELEGATECALL` to cold target contract. Parameterized: (1) OOG at DELEGATECALL opcode (insufficient gas), (2) Successful DELEGATECALL execution. | For OOG case: BAL **MUST NOT** include target contract (wasn't accessed). For success case: BAL **MUST** include target contract in `account_changes`. | βœ… Completed | -| `test_bal_extcodecopy_and_oog` | Ensure BAL handles OOG during EXTCODECOPY opcode execution correctly | Alice calls contract that attempts `EXTCODECOPY` from cold target contract. Parameterized: (1) OOG at EXTCODECOPY opcode (insufficient gas), (2) Successful EXTCODECOPY execution. | For OOG case: BAL **MUST NOT** include target contract (wasn't accessed). For success case: BAL **MUST** include target contract in `account_changes`. | βœ… Completed | +| `test_bal_extcodecopy_and_oog` | Ensure BAL handles OOG during EXTCODECOPY at various failure points | Alice calls contract that attempts `EXTCODECOPY` from cold target contract. Parameterized: (1) Successful EXTCODECOPY, (2) OOG at cold access (insufficient gas for account access), (3) OOG at memory expansion with large offset (64KB offset, gas covers cold access + copy but NOT memory expansion), (4) OOG at memory expansion boundary (256 byte offset, gas is exactly 1 less than needed). | For success case: BAL **MUST** include target contract. For all OOG cases: BAL **MUST NOT** include target contract. Gas for ALL components (cold access + copy + memory expansion) must be checked BEFORE recording account access. | βœ… Completed | | `test_bal_oog_7702_delegated_cold_cold` | Ensure BAL handles OOG during EIP-7702 delegated account loading when both accounts are cold | Alice calls cold delegated account Bob (7702) which delegates to cold `TargetContract` with insufficient gas for second cold load | BAL **MUST** include Bob in `account_changes` (first cold load succeeds) but **MUST NOT** include `TargetContract` (second cold load fails due to OOG) | 🟑 Planned | | `test_bal_oog_7702_delegated_warm_cold` | Ensure BAL handles OOG during EIP-7702 delegated account loading when first account is warm, second is cold | Alice calls warm delegated account Bob (7702) which delegates to cold `TargetContract` with insufficient gas for second cold load | BAL **MUST** include Bob in `account_changes` (warm load succeeds) but **MUST NOT** include `TargetContract` (cold load fails due to OOG) | 🟑 Planned | +| `test_bal_multiple_balance_changes_same_account` | Ensure BAL tracks multiple balance changes to same account across transactions | Alice funds Bob (starts at 0) in tx0 with exact amount needed. Bob spends everything in tx1 to Charlie. Bob's balance: 0 β†’ funding_amount β†’ 0 | BAL **MUST** include Bob with two `balance_changes`: one at txIndex=1 (receives funds) and one at txIndex=2 (balance returns to 0). This tests balance tracking across two transactions. | βœ… Completed | +| `test_bal_multiple_storage_writes_same_slot` | Ensure BAL tracks multiple writes to same storage slot across transactions | Alice calls contract 3 times in same block. Contract increments slot 1 on each call: 0 β†’ 1 β†’ 2 β†’ 3 | BAL **MUST** include contract with slot 1 having three `slot_changes`: txIndex=1 (value 1), txIndex=2 (value 2), txIndex=3 (value 3). Each transaction's write must be recorded separately. | βœ… Completed | +| `test_bal_nested_delegatecall_storage_writes_net_zero` | Ensure BAL correctly filters net-zero storage changes across nested DELEGATECALL frames | Parametrized by nesting depth (1-3). Root contract has slot 0 = 1. Each frame writes a different intermediate value via DELEGATECALL chain, deepest frame writes back to original value (1). Example depth=2: 1 β†’ 2 β†’ 3 β†’ 1 | BAL **MUST** include root contract with `storage_reads` for slot 0 but **MUST NOT** include `storage_changes` (net-zero). All delegate contracts **MUST** have empty changes. Tests that frame merging correctly removes parent's intermediate writes when child reverts to pre-tx value. | βœ… Completed | +| `test_bal_cross_tx_storage_revert_to_zero` | Ensure BAL captures storage changes when tx2 reverts slot back to original value (blobhash regression test) | Alice sends tx1 writing slot 0=0xABCD (from 0x0), then tx2 writing slot 0=0x0 (back to original) | BAL **MUST** include contract with slot 0 having two `slot_changes`: txIndex=1 (0xABCD) and txIndex=2 (0x0). Cross-transaction net-zero **MUST NOT** be filtered. | βœ… Completed | +| `test_bal_create_contract_init_revert` | Ensure BAL correctly handles CREATE when parent call reverts | Caller calls factory, factory executes CREATE (succeeds), then factory REVERTs rolling back the CREATE | BAL **MUST** include Alice with `nonce_changes`. Caller and factory with no changes (reverted). Created contract address appears in BAL but **MUST NOT** have `nonce_changes` or `code_changes` (CREATE was rolled back). Contract address **MUST NOT** exist in post-state. | βœ… Completed | +| `test_bal_create_oog_code_deposit` | Ensure BAL correctly handles CREATE OOG during code deposit | Alice calls factory contract that executes CREATE with init code returning 10,000 bytes. Transaction has insufficient gas for code deposit. Factory nonce increments, CREATE returns 0 and stores in slot 1. | BAL **MUST** include Alice with `nonce_changes`. Factory with `nonce_changes` (incremented by CREATE) and `storage_changes` (slot 1 = 0). Contract address with empty changes (read during collision check). **MUST NOT** include nonce or code changes for contract address (rolled back on OOG). Contract address **MUST NOT** exist in post-state. | βœ… Completed | +| `test_bal_create_early_failure` | Ensure BAL correctly handles CREATE that fails before accessing contract address | Factory (balance=50) attempts CREATE(value=100). CREATE fails due to insufficient endowment (100 > 50). Factory stores CREATE result (0) in slot 0. | BAL **MUST** include Alice with `nonce_changes`. Factory with `storage_changes` (slot 0 = 0) but **MUST NOT** have `nonce_changes` (CREATE failed before nonce increment). Contract address **MUST NOT** appear in BAL (never accessed - CREATE failed before `track_address`). This is distinct from collision/OOG failures where contract address IS in BAL. | βœ… Completed | | `test_bal_invalid_missing_nonce` | Verify clients reject blocks with BAL missing required nonce changes | Alice sends transaction to Bob; BAL modifier removes Alice's nonce change entry | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate that all sender accounts have nonce changes recorded. | βœ… Completed | | `test_bal_invalid_nonce_value` | Verify clients reject blocks with incorrect nonce values in BAL | Alice sends transaction to Bob; BAL modifier changes Alice's nonce to incorrect value | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate nonce values match actual state transitions. | βœ… Completed | | `test_bal_invalid_storage_value` | Verify clients reject blocks with incorrect storage values in BAL | Alice calls contract that writes to storage; BAL modifier changes storage value to incorrect value | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate storage change values match actual state transitions. | βœ… Completed | @@ -58,5 +69,44 @@ | `test_bal_invalid_complex_corruption` | Verify clients reject blocks with multiple BAL corruptions | Alice calls contract with storage writes; BAL has multiple issues: wrong account, missing nonce, wrong storage value | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** detect any corruption regardless of other issues. | βœ… Completed | | `test_bal_invalid_missing_account` | Verify clients reject blocks with missing required account entries in BAL | Alice sends transaction to Bob; BAL modifier removes Bob's account entry (recipient should be included) | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate all accessed accounts are present. | βœ… Completed | | `test_bal_invalid_balance_value` | Verify clients reject blocks with incorrect balance values in BAL | Alice sends value to Bob; BAL modifier changes balance to incorrect value | Block **MUST** be rejected with `INVALID_BLOCK_ACCESS_LIST` exception. Clients **MUST** validate balance change values match actual state transitions. | βœ… Completed | -| `test_bal_empty_block_no_coinbase` | Verify BAL correctly handles empty blocks without including coinbase | Block with 0 transactions, no withdrawals. System contracts may perform operations (EIP-2935 parent hash, EIP-4788 beacon root if active). | BAL **MUST NOT** include the coinbase/fee recipient (receives no fees). BAL **MAY** include system contract addresses (EIP-2935 `HISTORY_STORAGE_ADDRESS`, EIP-4788 `BEACON_ROOTS_ADDRESS`) with `storage_changes` at `tx_index=0` (pre-execution system operations). Maximum 4 system contract addresses if all active. | 🟑 Planned | -| `test_bal_empty_block_withdrawal_to_coinbase` | Verify BAL includes coinbase when it receives EIP-4895 withdrawal even in empty block | Block with 0 transactions but contains EIP-4895 withdrawal(s) with coinbase as recipient. System contracts may perform operations. | BAL **MUST** include coinbase with `balance_changes` at `tx_index=1` (post-execution: len(txs)+1 = 0+1). BAL **MAY** include system contract addresses with `storage_changes` at `tx_index=0` (pre-execution system operations). This confirms that coinbase inclusion depends on actual state changes, not transaction presence. | 🟑 Planned | +| `test_bal_empty_block_no_coinbase` | Ensure BAL correctly handles empty blocks without including coinbase | Block with 0 transactions, no withdrawals. System contracts may perform operations (EIP-2935 parent hash, EIP-4788 beacon root if active). | BAL **MUST NOT** include the coinbase/fee recipient (receives no fees). BAL **MAY** include system contract addresses (EIP-2935 `HISTORY_STORAGE_ADDRESS`, EIP-4788 `BEACON_ROOTS_ADDRESS`) with `storage_changes` at `tx_index=0` (pre-execution system operations). | βœ… Completed | +| `test_bal_coinbase_zero_tip` | Ensure BAL includes coinbase even when priority fee is zero | Block with 1 transaction: Alice sends 5 wei to Bob with priority fee = 0 (base fee burned post-EIP-1559) | BAL **MUST** include Alice with `balance_changes` (gas cost) and `nonce_changes`. BAL **MUST** include Bob with `balance_changes`. BAL **MUST** include coinbase with empty changes. | βœ… Completed | +| `test_bal_withdrawal_empty_block` | Ensure BAL captures withdrawal balance changes in empty block | Charlie starts with 1 gwei. Block with 0 transactions and 1 withdrawal of 10 gwei to Charlie | BAL **MUST** include Charlie with `balance_changes` at `block_access_index = 1`. Charlie's `balance_changes` **MUST** show final balance of 11 gwei. All other fields (storage_reads, storage_changes, nonce_changes, code_changes) **MUST** be empty. | βœ… Completed | +| `test_bal_withdrawal_and_transaction` | Ensure BAL captures both transaction and withdrawal balance changes | Block with 1 transaction: Alice sends 5 wei to Bob. 1 withdrawal of 10 gwei to Charlie | BAL **MUST** include Alice with `nonce_changes` and `balance_changes` at `block_access_index = 1`. BAL **MUST** include Bob with `balance_changes` at `block_access_index = 1`. BAL **MUST** include Charlie with `balance_changes` at `block_access_index = 2` showing final balance after receiving 10 gwei. All other fields for Charlie **MUST** be empty. | βœ… Completed | +| `test_bal_withdrawal_to_nonexistent_account` | Ensure BAL captures withdrawal to non-existent account | Block with 1 withdrawal of 10 gwei to non-existent account Charlie | BAL **MUST** include Charlie with `balance_changes` at `block_access_index = 1` showing final balance of 10 gwei. All other fields (storage_reads, storage_changes, nonce_changes, code_changes) **MUST** be empty. | βœ… Completed | +| `test_bal_withdrawal_no_evm_execution` | Ensure BAL captures withdrawal without triggering EVM execution | Contract `Oracle` with storage slot 0x01 = 0x42. `Oracle` code writes to slot 0x01 when called. Block with 1 withdrawal of 10 gwei to `Oracle` | BAL **MUST** include `Oracle` with `balance_changes` at `block_access_index = 1` showing final balance after receiving 10 gwei. Storage slot 0x01 **MUST** remain 0x42 and all other fields (storage_reads, storage_changes, nonce_changes, code_changes) **MUST** be empty. | βœ… Completed | +| `test_bal_withdrawal_and_state_access_same_account` | Ensure BAL captures both state access and withdrawal to same address | Contract `Oracle` with storage slot 0x01 = 0x42. Block with 1 transaction: Alice calls `Oracle` (reads from slot 0x01, writes to slot 0x02). 1 withdrawal of 10 gwei to `Oracle` | BAL **MUST** include `Oracle` with `storage_reads` for slot 0x01 and `storage_changes` for slot 0x02 at `block_access_index = 1`. `Oracle` **MUST** also have `balance_changes` at `block_access_index = 2` showing final balance after receiving 10 gwei. Both state access and withdrawal **MUST** be captured. | βœ… Completed | +| `test_bal_withdrawal_and_value_transfer_same_address` | Ensure BAL captures both transaction value transfer and withdrawal to same address | Block with 1 transaction: Alice sends 5 gwei to Bob. 1 withdrawal of 10 gwei to Bob | BAL **MUST** include Alice with `nonce_changes` and `balance_changes` at `block_access_index = 1`. BAL **MUST** include Bob with `balance_changes` at `block_access_index = 1` showing balance after receiving 5 gwei. Bob **MUST** also have `balance_changes` at `block_access_index = 2` showing balance after receiving 10 gwei withdrawal. Bob's final post-state balance **MUST** be 15 gwei (cumulative). | βœ… Completed | +| `test_bal_multiple_withdrawals_same_address` | Ensure BAL accumulates multiple withdrawals to same address | Block with 3 withdrawals to Charlie: 5 gwei, 10 gwei, 15 gwei | BAL **MUST** include Charlie with `balance_changes` at `block_access_index = 1` showing final balance of 30 gwei. All other fields (storage_reads, storage_changes, nonce_changes, code_changes) **MUST** be empty. | βœ… Completed | +| `test_bal_withdrawal_and_selfdestruct` | Ensure BAL captures withdrawal to self-destructed contract address | Contract `Oracle` with 100 gwei balance. Block with 1 transaction: `Oracle` self-destructs sending balance to Bob. 1 withdrawal of 50 gwei to `Oracle`'s address | BAL **MUST** include `Oracle` with `balance_changes` showing 0 balance at `block_access_index = 1` (after self-destruct). BAL **MUST** include Bob with `balance_changes` showing 100 gwei received from self-destruct at `block_access_index = 1`. `Oracle` **MUST** also have `balance_changes` at `block_access_index = 2` showing 50 gwei after withdrawal. Both self-destruct and withdrawal **MUST** be captured. | βœ… Completed | +| `test_bal_withdrawal_and_new_contract` | Ensure BAL captures withdrawal to newly created contract | Block with 1 transaction: Alice deploys contract `Oracle` with 5 gwei initial balance. 1 withdrawal of 10 gwei to `Oracle` | BAL **MUST** include `Oracle` with `code_changes` and `balance_changes` showing 5 gwei at `block_access_index = 1`. `Oracle` **MUST** also have `balance_changes` at `block_access_index = 2` showing balance after receiving 10 gwei withdrawal. `Oracle`'s final post-state balance **MUST** be 15 gwei (cumulative). | βœ… Completed | +| `test_bal_zero_withdrawal` | Ensure BAL handles zero-amount withdrawal correctly | Block with 0 transactions and 1 zero-amount withdrawal (0 gwei) to Charlie. Two variations: Charlie has existing balance (5 gwei) or Charlie is non-existent. | BAL **MUST** include Charlie at `block_access_index = 1` with empty changes. Balance remains unchanged. | βœ… Completed | +| `test_bal_withdrawal_to_precompiles` | Ensure BAL captures withdrawal to precompile addresses | Block with 1 withdrawal of 10 gwei to precompile address (all precompiles) | BAL **MUST** include precompile address with `balance_changes` at `block_access_index = 1` showing final balance of 10 gwei. All other fields (storage_reads, storage_changes, nonce_changes, code_changes) **MUST** be empty. | βœ… Completed | +| `test_bal_withdrawal_largest_amount` | Ensure BAL captures withdrawal with largest amount | Block with 1 withdrawal of maximum uint64 value (2^64-1 gwei) to Charlie | BAL **MUST** include Charlie with `balance_changes` at `block_access_index = 1` showing final balance of (2^64-1) * 10^9 wei. All other fields (storage_reads, storage_changes, nonce_changes, code_changes) **MUST** be empty. | βœ… Completed | +| `test_bal_withdrawal_to_coinbase` | Ensure BAL captures withdrawal to coinbase address | Block with 1 transaction: Alice sends 5 wei to Bob. 1 withdrawal of 10 gwei to coinbase/fee recipient | BAL **MUST** include coinbase with `balance_changes` at `block_access_index = 1` showing balance after transaction fees. Coinbase **MUST** also have `balance_changes` at `block_access_index = 2` showing balance after receiving 10 gwei withdrawal. Coinbase's final post-state balance **MUST** include both transaction fees and withdrawal. | βœ… Completed | +| `test_bal_withdrawal_to_coinbase_empty_block` | Ensure BAL captures withdrawal to coinbase even when there are no transactions (no fees) | Block with 0 transactions and 1 withdrawal of 10 gwei to coinbase/fee recipient | BAL **MUST** include coinbase with `balance_changes` at `block_access_index = 1` showing final balance of 10 gwei. All other fields (storage_reads, storage_changes, nonce_changes, code_changes) **MUST** be empty. | βœ… Completed | +| `test_bal_nonexistent_value_transfer` | Ensure BAL captures non-existent account on value transfer | Alice sends value (0 wei or 1 ETH) to non-existent account Bob (address never funded or accessed before) via direct transfer | For zero value: BAL **MUST** include Alice with `nonce_changes` and Bob (non-existent) with empty changes. For positive value: BAL **MUST** include Bob with `balance_changes` showing received amount. | βœ… Completed | +| `test_bal_nonexistent_account_access_read_only` | Ensure BAL captures non-existent account accessed via read-only account-reading opcodes | Alice calls `Oracle` contract which uses read-only account access opcodes (`BALANCE`, `EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`, `STATICCALL`, `DELEGATECALL`) on non-existent account Bob. | BAL **MUST** include Alice with `nonce_changes`, `Oracle` with empty changes, and Bob with empty changes (account accessed but no state modifications). | βœ… Completed | +| `test_bal_nonexistent_account_access_value_transfer` | Ensure BAL captures non-existent account accessed via CALL/CALLCODE with value transfers | Alice calls `Oracle` contract which uses `CALL` or `CALLCODE` on non-existent account Bob. Tests both zero and positive value transfers. | BAL **MUST** include Alice with `nonce_changes`. For CALL with positive value: `Oracle` with `balance_changes` (loses value), Bob with `balance_changes` (receives value). For CALLCODE with value or zero value transfers: `Oracle` and Bob with empty changes (CALLCODE self-transfer = net zero). | βœ… Completed | +| `test_bal_storage_write_read_same_frame` | Ensure BAL captures write precedence over read in same call frame (writes shadow reads) | Alice calls `Oracle` which writes (`SSTORE`) value `0x42` to slot `0x01`, then reads (`SLOAD`) from slot `0x01` in the same call frame | BAL **MUST** include `Oracle` with slot `0x01` in `storage_changes` showing final value `0x42`. Slot `0x01` **MUST NOT** appear in `storage_reads` (write shadows the subsequent read in same frame). | βœ… Completed | +| `test_bal_storage_write_read_cross_frame` | Ensure BAL captures write precedence over read across call frames (writes shadow reads cross-frame) | Alice calls `Oracle`. First call reads slot `0x01` (sees initial value), writes `0x42` to slot `0x01`, then calls itself (via `CALL`, `DELEGATECALL`, or `CALLCODE`). Second call reads slot `0x01` (sees `0x42`) and exits. | BAL **MUST** include `Oracle` with slot `0x01` in `storage_changes` showing final value `0x42`. Slot `0x01` **MUST NOT** appear in `storage_reads` (write shadows both the read before it in same frame and the read in the recursive call). | βœ… Completed | +| `test_bal_create_transaction_empty_code` | Ensure BAL does not record spurious code changes for CREATE transaction deploying empty code | Alice sends CREATE transaction with empty initcode (deploys code `b""`). Contract address gets nonce = 1 and code = `b""`. | BAL **MUST** include Alice with `nonce_changes` and created contract with `nonce_changes` but **MUST NOT** include `code_changes` for contract (setting `b"" -> b""` is net-zero). | βœ… Completed | +| `test_bal_cross_block_precompile_state_leak` | Ensure internal EVM state for precompile handling does not leak between blocks | Block 1: Alice calls RIPEMD-160 (0x03) with zero value (RIPEMD-160 must be pre-funded). Block 2: Bob's transaction triggers an exception (stack underflow). | BAL for Block 1 **MUST** include RIPEMD-160. BAL for Block 2 **MUST NOT** include RIPEMD-160 (never accessed in Block 2). Internal state from Parity Touch Bug (EIP-161) handling must be reset between blocks. | βœ… Completed | +| `test_bal_all_transaction_types` | Ensure BAL correctly captures state changes from all transaction types in a single block | Single block with 5 transactions: Type 0 (Legacy), Type 1 (EIP-2930 Access List), Type 2 (EIP-1559), Type 3 (EIP-4844 Blob), Type 4 (EIP-7702 Set Code). Each tx writes to contract storage. Note: Access list addresses are pre-warmed but NOT recorded in BAL (no state access). | BAL **MUST** include: (1) All 5 senders with `nonce_changes`. (2) Contracts 0-3 with `storage_changes`. (3) Alice (7702 target) with `nonce_changes`, `code_changes` (delegation), `storage_changes`. (4) Oracle (delegation source) with empty changes. | βœ… Completed | +| `test_bal_create2_collision` | Ensure BAL handles CREATE2 address collision correctly | Factory contract (nonce=1, storage slot 0=0xDEAD) executes `CREATE2(salt=0, initcode)` targeting address that already has `code=STOP, nonce=1`. Pre-deploy contract at calculated CREATE2 target address before factory deployment. | BAL **MUST** include: (1) Factory with `nonce_changes` (1β†’2, incremented even on failed CREATE2), `storage_changes` for slot 0 (0xDEADβ†’0, stores failure). (2) Collision address with empty changes (accessed during collision check, no state changes). CREATE2 returns 0. Collision address **MUST NOT** have `nonce_changes` or `code_changes`. | βœ… Completed | +| `test_bal_create_selfdestruct_to_self_with_call` | Ensure BAL handles init code that calls external contract then selfdestructs to itself | Factory executes `CREATE2` with endowment=100. Init code (embedded in factory via CODECOPY): (1) `CALL(Oracle, 0)` - Oracle writes to its storage slot 0x01. (2) `SSTORE(0x01, 0x42)` - write to own storage. (3) `SELFDESTRUCT(SELF)` - selfdestruct to own address. Contract created and destroyed in same tx. | BAL **MUST** include: (1) Factory with `nonce_changes`, `balance_changes` (loses 100). (2) Oracle with `storage_changes` for slot 0x01 (external call succeeded). (3) Created address with `storage_reads` for slot 0x01 (aborted write becomes read) - **MUST NOT** have `nonce_changes`, `code_changes`, `storage_changes`, or `balance_changes` (ephemeral contract, balance burned via SELFDESTRUCT to self). | βœ… Completed | +| `test_bal_selfdestruct_to_7702_delegation` | Ensure BAL correctly handles SELFDESTRUCT to a 7702 delegated account (no code execution on recipient) | Tx1: Alice authorizes delegation to Oracle (sets code to `0xef0100\|\|Oracle`). Tx2: Victim contract (balance=100) executes `SELFDESTRUCT(Alice)`. Two separate transactions in same block. Note: Alice starts with initial balance which accumulates with selfdestruct. | BAL **MUST** include: (1) Alice at tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_index=2 with `balance_changes` (receives selfdestruct). (3) Victim at tx_index=2 with `balance_changes` (100β†’0). **Oracle MUST NOT appear in tx2** - per EVM spec, SELFDESTRUCT transfers balance without executing recipient code, so delegation target is never accessed. | βœ… Completed | +| `test_bal_call_revert_insufficient_funds` | Ensure BAL handles CALL failure due to insufficient balance (not OOG) | Contract (balance=100, storage slot 0x02=0xDEAD) executes: `SLOAD(0x01), CALL(target, value=1000), SSTORE(0x02, result)`. CALL fails because 1000 > 100. Target address 0xDEAD (pre-existing with non-zero balance to avoid pruning). Note: slot 0x02 must start non-zero so SSTORE(0) is a change. | BAL **MUST** include: (1) Contract with `storage_reads` for slot 0x01, `storage_changes` for slot 0x02 (value=0, CALL returned failure). (2) Target (0xDEAD) **MUST** appear in BAL with empty changes - target is accessed before balance check fails. | βœ… Completed | +| `test_bal_lexicographic_address_ordering` | Ensure BAL enforces strict lexicographic byte-wise ordering | Pre-fund three addresses with specific byte patterns: `addr_low = 0x0000...0001`, `addr_mid = 0x0000...0100`, `addr_high = 0x0100...0000`. Contract touches them in reverse order: `BALANCE(addr_high), BALANCE(addr_low), BALANCE(addr_mid)`. Additionally, include two endian-trap addresses that are byte-reversals of each other: `addr_endian_low = 0x0100000000000000000000000000000000000002`, `addr_endian_high = 0x0200000000000000000000000000000000000001`. Note: `reverse(addr_endian_low) = addr_endian_high`. Correct lexicographic order: `addr_endian_low < addr_endian_high` (0x01 < 0x02 at byte 0). If implementation incorrectly reverses bytes before comparing, it would get `addr_endian_low > addr_endian_high` (wrong). | BAL account list **MUST** be sorted lexicographically by address bytes: `addr_low` < `addr_mid` < `addr_high` < `addr_endian_low` < `addr_endian_high`, regardless of access order. The endian-trap addresses specifically catch byte-reversal bugs where addresses are compared with wrong byte order. Complements `test_bal_invalid_account_order` which tests rejection; this tests correct generation. | βœ… Completed | +| `test_bal_transient_storage_not_tracked` | Ensure BAL excludes EIP-1153 transient storage operations | Contract executes: `TSTORE(0x01, 0x42)` (transient write), `TLOAD(0x01)` (transient read), `SSTORE(0x02, result)` (persistent write using transient value). | BAL **MUST** include slot 0x02 in `storage_changes` (persistent storage was modified). BAL **MUST NOT** include slot 0x01 in `storage_reads` or `storage_changes` (transient storage is not persisted, not needed for stateless execution). This verifies TSTORE/TLOAD don't pollute BAL. | βœ… Completed | +| `test_bal_selfdestruct_to_precompile` | Ensure BAL captures SELFDESTRUCT with precompile as beneficiary | Caller triggers victim contract (balance=100) to execute `SELFDESTRUCT(0x0000...0001)` (ecrecover precompile). Precompile starts with balance=0. | BAL **MUST** include: (1) Contract with `balance_changes` (100β†’0, loses balance to selfdestruct). (2) Precompile address 0x01 with `balance_changes` (0β†’100, receives selfdestruct balance). Precompile **MUST NOT** have `code_changes` or `nonce_changes`. This complements `test_bal_withdrawal_to_precompiles` (withdrawal) and `test_bal_precompile_funded` (tx value). | βœ… Completed | +| `test_bal_self_destruct_oog` | Ensure BAL correctly tracks SELFDESTRUCT beneficiary based on gas boundaries | Alice calls `Caller` contract which CALLs `SelfDestructContract` with precisely controlled gas. `SelfDestructContract` attempts SELFDESTRUCT to new account `Beneficiary`. Static gas = G_VERY_LOW + G_SELF_DESTRUCT + G_COLD_ACCOUNT_ACCESS. Parameterized: (1) OOG before state access (gas = static - 1), (2) OOG after state access (gas = static, but insufficient for G_NEW_ACCOUNT). | For OOG before state access: BAL **MUST NOT** include `Beneficiary` (no state access occurred). For OOG after state access: BAL **MUST** include `Beneficiary` with empty changes (state was accessed before G_NEW_ACCOUNT check failed). Both cases: Alice with `nonce_changes`, `Caller` and `SelfDestructContract` with empty changes. Contract balance unchanged. | βœ… Completed | +| `test_bal_withdrawal_to_7702_delegation` | Ensure BAL correctly handles withdrawal to a 7702 delegated account (no code execution on recipient) | Tx1: Alice authorizes delegation to Oracle (sets code to `0xef0100\|\|Oracle`). Withdrawal: 10 gwei sent to Alice. Single block with tx + withdrawal. | BAL **MUST** include: (1) Alice at tx_index=1 with `code_changes` (delegation), `nonce_changes`. (2) Alice at tx_index=2 with `balance_changes` (receives withdrawal). **Oracle MUST NOT appear** - withdrawals credit balance without executing recipient code, so delegation target is never accessed. This complements `test_bal_selfdestruct_to_7702_delegation` (selfdestruct) and `test_bal_withdrawal_no_evm_execution` (withdrawal to contract). | βœ… Completed | +| `test_init_collision_create_tx` | Ensure BAL tracks CREATE collisions correctly (pre-Amsterdam test with BAL) | CREATE transaction targeting address with existing storage aborts | BAL **MUST** show empty expectations for collision address (no changes occur due to abort) | βœ… Completed | +| `test_call_to_pre_authorized_oog` | Ensure BAL handles OOG during EIP-7702 delegation access (pre-Amsterdam test with BAL) | Call to delegated account that OOGs before accessing delegation contract | BAL **MUST** include auth_signer (code read for delegation check) but **MUST NOT** include delegation contract (OOG before access) | βœ… Completed | +| `test_selfdestruct_created_in_same_tx_with_revert` | Ensure BAL tracks selfdestruct with revert correctly (pre-Amsterdam test with BAL) | Contract created and selfdestructed in same tx with nested revert | BAL **MUST** track storage reads and balance changes for selfdestruct even with reverts | βœ… Completed | +| `test_value_transfer_gas_calculation` | Ensure BAL correctly tracks OOG scenarios for CALL/CALLCODE/DELEGATECALL/STATICCALL (pre-Amsterdam test with BAL) | Nested calls with precise gas limits to test OOG behavior. For CALL with OOG: target account is read. For CALLCODE/DELEGATECALL/STATICCALL with OOG: target account **NOT** read (OOG before state access) | For CALL: target in BAL even with OOG. For CALLCODE/DELEGATECALL/STATICCALL: target **NOT** in BAL when OOG (state access deferred until after gas check) | βœ… Completed | +| `test_bal_7702_double_auth_reset_minimal` | Ensure BAL tracks multiple 7702 nonce increments but filters net-zero code change | Single transaction contains two EIP-7702 authorizations for `Alice`: (1) first auth sets delegation `0xef0100\|\|Oracle`, (2) second auth clears delegation back to empty. Transaction sends 10 wei to `Bob`. Two variants: (a) Self-funded: `Alice` is tx sender (one tx nonce bump + two auth bumps β†’ nonce 0β†’3). (b) Sponsored: `Relayer` is tx sender (`Alice` only in auths β†’ nonce 0β†’2 for `Alice`, plus one nonce bump for `Relayer`). | Variant (a): BAL **MUST** include `Alice` with `nonce_changes` 0β†’3. Variant (b): BAL **MUST** include `Alice` with `nonce_changes` 0β†’2 and `Relayer` with its own `nonce_changes`. For both variants, BAL **MUST NOT** include `code_changes` for `Alice` (net code is empty), **MUST** include `Bob` with `balance_changes` (receives 10 wei), and `Oracle` **MUST NOT** appear in BAL. | 🟑 Planned | +| `test_bal_selfdestruct_send_to_sender` | Ensure BAL tracks SELFDESTRUCT sending all funds back to the tx sender (no burn) | Pre-state: contract `C` exists from a prior transaction with non-empty code and balance = 100 wei. EOA `Alice` sends a transaction calling `C`. `C`’s code executes `SELFDESTRUCT(Alice)`. Under EIP-6780, because `C` was not created in this transaction, SELFDESTRUCT does not delete code or storage; it only transfers the entire 100 wei balance from `C` to `Alice`. Final post-state: `C` still exists with the same code and balance = 0; `Alice`’s balance increased by 100 wei (ignoring gas for this test). | BAL **MUST** include `Alice` with `nonce_changes` (tx sender) and `balance_changes` reflecting receipt of 100 wei, and **MUST** include `C` with `balance_changes` 100β†’0 and no `code_changes`. BAL **MUST NOT** include any other accounts. This test ensures SELFDESTRUCT-to-sender is modeled as a pure value transfer (no burn, no code deletion). | 🟑 Planned | +| `test_bal_selfdestruct_burn_balance` | Ensure BAL records ETH burn when a contract created in the same tx selfdestructs to itself | Pre-state: `Alice` sends a CREATE/CREATE2 transaction that deploys contract `D` with an initial endowment of 100 wei. In the same transaction, `D`’s runtime executes and calls `SELFDESTRUCT(D)` (beneficiary is its own address). Under EIP-6780, because `D` is created and selfdestructs in the same transaction and the target equals the caller, the 100 wei is **burned** (removed from total supply). Final post-state: `D` has balance = 0 and is considered destroyed per same-tx semantics. | BAL **MUST** include `D` with `balance_changes` 100β†’0 (burn) and may include `code_changes` indicating deletion if BAL models code removal; no other account **MUST** have `balance_changes` corresponding to this 100 wei, i.e. no recipient appears. `Alice` **MAY** appear only with `nonce_changes` and gas-related balance changes; the key assertion is that the 100 wei leaves `D` and is not credited to any other account. | 🟑 Planned | + diff --git a/tests/cancun/create/__init__.py b/tests/cancun/create/__init__.py new file mode 100644 index 0000000000..5297fcc089 --- /dev/null +++ b/tests/cancun/create/__init__.py @@ -0,0 +1 @@ +"""Create tests starting at Cancun.""" diff --git a/tests/cancun/create/test_create_oog_from_eoa_refunds.py b/tests/cancun/create/test_create_oog_from_eoa_refunds.py new file mode 100644 index 0000000000..ee7f14571a --- /dev/null +++ b/tests/cancun/create/test_create_oog_from_eoa_refunds.py @@ -0,0 +1,417 @@ +""" +Tests for CREATE OOG scenarios from EOA refunds. + +Tests that verify refunds are not applied on contract creation +when the creation runs out of gas. +""" + +from dataclasses import dataclass +from enum import Enum +from typing import Dict + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + BalAccountExpectation, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + Block, + BlockAccessListExpectation, + BlockchainTestFiller, + Fork, + Op, + Transaction, + compute_create2_address, + compute_create_address, +) + +pytestmark = pytest.mark.valid_from("Cancun") + + +class OogScenario(Enum): + """Different ways a CREATE can run out of gas or succeed.""" + + NO_OOG = "no_oog" + OOG_CODE_DEPOSIT = "oog_code_deposit" # OOG due to code deposit cost + OOG_INVALID = "oog_invalid_opcode" # OOG due to INVALID opcode + + +class RefundType(Enum): + """Different refund mechanisms tested.""" + + SSTORE_DIRECT = "sstore_in_init_code" + SSTORE_CALL = "sstore_via_call" + SSTORE_DELEGATECALL = "sstore_via_delegatecall" + SSTORE_CALLCODE = "sstore_via_callcode" + SELFDESTRUCT = "selfdestruct_via_call" + LOG_OP = "log_operations" + NESTED_CREATE = "nested_create_in_init_code" + NESTED_CREATE2 = "nested_create2_in_init_code" + + +@dataclass +class HelperContracts: + """Container for deployed helper contract addresses.""" + + sstore_refund: Address + selfdestruct: Address + log_op: Address + init_code: Address + + +def deploy_helper_contracts(pre: Alloc) -> HelperContracts: + """Deploy all helper contracts needed for the tests.""" + # Simple contract to reset sstore and get refund: sstore(1, 0) + sstore_refund_code = Op.SSTORE(1, 0) + Op.STOP + sstore_refund = pre.deploy_contract( + code=sstore_refund_code, + storage={1: 1}, + ) + + # Simple contract that self-destructs to refund + selfdestruct_code = Op.SELFDESTRUCT(Op.ORIGIN) + Op.STOP + selfdestruct = pre.deploy_contract( + code=selfdestruct_code, + storage={1: 1}, + ) + + # Simple contract that performs log operations + log_op_code = ( + Op.MSTORE(0, 0xFF) + + Op.LOG0(0, 32) + + Op.LOG1(0, 32, 0xFA) + + Op.LOG2(0, 32, 0xFA, 0xFB) + + Op.LOG3(0, 32, 0xFA, 0xFB, 0xFC) + + Op.LOG4(0, 32, 0xFA, 0xFB, 0xFC, 0xFD) + + Op.STOP + ) + log_op = pre.deploy_contract( + code=log_op_code, + storage={1: 1}, + ) + + # Init code that successfully creates contract but contains a refund + # sstore(0, 1); sstore(0, 0); return(0, 1) + init_code_with_refund = Op.SSTORE(0, 1) + Op.SSTORE(0, 0) + Op.RETURN(0, 1) + init_code = pre.deploy_contract( + code=init_code_with_refund, + ) + + return HelperContracts( + sstore_refund=sstore_refund, + selfdestruct=selfdestruct, + log_op=log_op, + init_code=init_code, + ) + + +def build_init_code( + refund_type: RefundType, + oog_scenario: OogScenario, + helpers: HelperContracts, +) -> bytes: + """ + Build init code based on refund type and OOG scenario. + + All init codes: + - Write to storage slot 0 + - Optionally trigger refund mechanism + - End with either small return (success) or large return/INVALID (OOG) + """ + # Common prefix: sstore(0, 1) to mark storage access + prefix = Op.SSTORE(0, 1) + + # Build the refund-triggering portion based on type + if refund_type == RefundType.SSTORE_DIRECT: + # Direct sstore refund: sstore(1, 1); sstore(1, 0) + refund_code = Op.SSTORE(1, 1) + Op.SSTORE(1, 0) + + elif refund_type == RefundType.SSTORE_CALL: + # Call to sstore refund helper + refund_code = Op.POP( + Op.CALL(Op.GAS, helpers.sstore_refund, 0, 0, 0, 0, 0) + ) + + elif refund_type == RefundType.SSTORE_DELEGATECALL: + # Delegatecall to sstore refund helper (needs local storage setup) + refund_code = Op.SSTORE(1, 1) + Op.POP( + Op.DELEGATECALL(Op.GAS, helpers.sstore_refund, 0, 0, 0, 0) + ) + + elif refund_type == RefundType.SSTORE_CALLCODE: + refund_code = Op.SSTORE(1, 1) + Op.POP( + Op.CALLCODE(Op.GAS, helpers.sstore_refund, 0, 0, 0, 0, 0) + ) + + elif refund_type == RefundType.SELFDESTRUCT: + refund_code = Op.POP( + Op.CALL(Op.GAS, helpers.selfdestruct, 0, 0, 0, 0, 0) + ) + + elif refund_type == RefundType.LOG_OP: + # call to log op helper + refund_code = Op.POP(Op.CALL(Op.GAS, helpers.log_op, 0, 0, 0, 0, 0)) + + elif refund_type == RefundType.NESTED_CREATE: + # Nested CREATE with refund in init code + # extcodecopy the init code helper and CREATE from it + refund_code = ( + Op.SSTORE(1, 1) + + Op.SSTORE(1, 0) + + Op.EXTCODECOPY( + helpers.init_code, 0, 0, Op.EXTCODESIZE(helpers.init_code) + ) + + Op.POP(Op.CREATE(0, 0, Op.EXTCODESIZE(helpers.init_code))) + ) + + elif refund_type == RefundType.NESTED_CREATE2: + # Nested CREATE2 with refund in init code + refund_code = ( + Op.SSTORE(1, 1) + + Op.SSTORE(1, 0) + + Op.EXTCODECOPY( + helpers.init_code, 0, 0, Op.EXTCODESIZE(helpers.init_code) + ) + + Op.POP(Op.CREATE2(0, 0, Op.EXTCODESIZE(helpers.init_code), 0)) + ) + else: + refund_code = Op.STOP + + # Build the ending based on OOG scenario + if oog_scenario == OogScenario.NO_OOG: + # Return 1 byte of code (cheap code deposit) + if refund_type in ( + RefundType.NESTED_CREATE, + RefundType.NESTED_CREATE2, + ): + # For nested creates, return after init code length + ending = Op.RETURN(Op.ADD(Op.EXTCODESIZE(helpers.init_code), 1), 1) + else: + ending = Op.RETURN(0, 1) + + elif oog_scenario == OogScenario.OOG_CODE_DEPOSIT: + # Return 5000 bytes of code - code deposit cost exceeds available gas + if refund_type in ( + RefundType.NESTED_CREATE, + RefundType.NESTED_CREATE2, + ): + ending = Op.RETURN( + Op.ADD(Op.EXTCODESIZE(helpers.init_code), 1), 5000 + ) + else: + ending = Op.RETURN(0, 5000) + + elif oog_scenario == OogScenario.OOG_INVALID: + # INVALID opcode causes OOG (all gas consumed, no refund) + ending = Op.INVALID + + else: + ending = Op.STOP + + return bytes(prefix + refund_code + ending) + + +@pytest.mark.parametrize( + "oog_scenario", + [ + pytest.param(OogScenario.NO_OOG, id="no_oog"), + pytest.param(OogScenario.OOG_CODE_DEPOSIT, id="oog_code_deposit"), + pytest.param(OogScenario.OOG_INVALID, id="oog_invalid_opcode"), + ], +) +@pytest.mark.parametrize( + "refund_type", + [ + pytest.param(RefundType.SSTORE_DIRECT, id="sstore_direct"), + pytest.param(RefundType.SSTORE_CALL, id="sstore_call"), + pytest.param(RefundType.SSTORE_DELEGATECALL, id="sstore_delegatecall"), + pytest.param(RefundType.SSTORE_CALLCODE, id="sstore_callcode"), + pytest.param(RefundType.SELFDESTRUCT, id="selfdestruct"), + pytest.param(RefundType.LOG_OP, id="log_op"), + pytest.param(RefundType.NESTED_CREATE, id="nested_create"), + pytest.param(RefundType.NESTED_CREATE2, id="nested_create2"), + ], +) +@pytest.mark.ported_from( + [ + "https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/stCreateTest/CreateOOGFromEOARefundsFiller.yml", + ], + pr=["https://github.com/ethereum/execution-specs/pull/1831"], +) +def test_create_oog_from_eoa_refunds( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + refund_type: RefundType, + oog_scenario: OogScenario, + fork: Fork, +) -> None: + """ + Test CREATE from EOA with various refund mechanisms and OOG scenarios. + + Verifies that: + 1. Refunds are not applied when contract creation runs Out of Gas + 2. When OOG occurs, the sender's balance is fully consumed (no refund) + 3. When OOG occurs, the contract is not created + + For BAL (Block Access List) tracking: + - NoOoG: Storage writes should be recorded as `storage_changes` + - OoG: Storage writes should be converted to `storage_reads` since + the CREATE failed and all state changes were reverted + """ + helpers = deploy_helper_contracts(pre) + sender = pre.fund_eoa(amount=4_000_000) + init_code = build_init_code(refund_type, oog_scenario, helpers) + created_address = compute_create_address(address=sender, nonce=0) + + tx = Transaction( + sender=sender, + to=None, + data=init_code, + gas_limit=400_000, + ) + + post: Dict[Address, Account | None] = { + sender: Account(nonce=1), + } + + if oog_scenario == OogScenario.NO_OOG: + # contract created with code 0x00 (1 byte from memory) + if refund_type == RefundType.NESTED_CREATE: + # Nested CREATE increments the created contract's nonce to 2 + post[created_address] = Account( + nonce=2, + code=b"\x00", + storage={0: 1}, # successful write + ) + + nested_created = compute_create_address( + address=created_address, nonce=1 + ) + post[nested_created] = Account( + nonce=1, + code=b"\x00", + storage={}, + ) + elif refund_type == RefundType.NESTED_CREATE2: + # nested create2 increments the created contract's nonce to 2 + post[created_address] = Account( + nonce=2, + code=b"\x00", + storage={0: 1}, + ) + + nested_created = compute_create2_address( + address=created_address, + salt=0, + initcode=Op.SSTORE(0, 1) + Op.SSTORE(0, 0) + Op.RETURN(0, 1), + ) + post[nested_created] = Account( + nonce=1, + code=b"\x00", + storage={}, + ) + else: + post[created_address] = Account( + nonce=1, + code=b"\x00", + storage={0: 1}, + ) + post[sender] = Account(nonce=1) + else: + # OOG case: contract not created, sender balance is fully consumed + post[created_address] = Account.NONEXISTENT + post[sender] = Account( + nonce=1, + balance=0, + ) + + if refund_type == RefundType.SELFDESTRUCT: + selfdestruct_code = Op.SELFDESTRUCT(Op.ORIGIN) + Op.STOP + if oog_scenario == OogScenario.NO_OOG: + # selfdestruct succeeded, balance is 0 + post[helpers.selfdestruct] = Account( + balance=0, + nonce=1, + ) + else: + # OOG: selfdestruct reverted, helper unchanged + post[helpers.selfdestruct] = Account( + code=bytes(selfdestruct_code), + nonce=1, + storage={1: 1}, + ) + + bal_expectation = None + if fork.header_bal_hash_required(): + if oog_scenario == OogScenario.NO_OOG: + # Success: storage write to slot 0 persists + expected_nonce = ( + 2 + if refund_type + in (RefundType.NESTED_CREATE, RefundType.NESTED_CREATE2) + else 1 + ) + created_bal = BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=expected_nonce) + ], + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=1) + ], + ), + ], + storage_reads=( + # noop write 0 -> 1 -> 0 + [1] + if refund_type + in ( + RefundType.SSTORE_DIRECT, + RefundType.SSTORE_DELEGATECALL, + RefundType.SSTORE_CALLCODE, + RefundType.NESTED_CREATE, + RefundType.NESTED_CREATE2, + ) + else [] + ), + ) + else: + # OOG case: storage writes converted to reads + # All refund types write to slot 0, most also write to slot 1 + if refund_type in ( + RefundType.SSTORE_DIRECT, + RefundType.SSTORE_DELEGATECALL, + RefundType.SSTORE_CALLCODE, + RefundType.NESTED_CREATE, + RefundType.NESTED_CREATE2, + ): + # write to both slot 0 and slot 1 (noop write 0 -> 1 -> 0) + created_bal = BalAccountExpectation( + storage_changes=[], + storage_reads=[0, 1], + ) + else: + # SSTORE_CALL, SELFDESTRUCT, LOG_OP only write to slot 0 + created_bal = BalAccountExpectation( + storage_changes=[], + storage_reads=[0], + ) + bal_expectation = BlockAccessListExpectation( + account_expectations={ + sender: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + created_address: created_bal, + } + ) + + blockchain_test( + pre=pre, + blocks=[Block(txs=[tx], expected_block_access_list=bal_expectation)], + post=post, + ) diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py index e0bfa59ec2..ab2d22f112 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py @@ -8,8 +8,16 @@ Account, Address, Alloc, + BalAccountExpectation, + BalBalanceChange, + BalCodeChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessListExpectation, Bytecode, Environment, + Fork, Initcode, Op, StateTestFiller, @@ -343,6 +351,7 @@ def test_selfdestruct_created_in_same_tx_with_revert( # noqa SC200 selfdestruct_with_transfer_initcode_copy_from_address: Address, recursive_revert_contract_address: Address, recursive_revert_contract_code: Bytecode, + fork: Fork, ) -> None: """ Given: @@ -427,7 +436,69 @@ def test_selfdestruct_created_in_same_tx_with_revert( # noqa SC200 gas_limit=500_000, ) - state_test(env=env, pre=pre, post=post, tx=tx) + expected_block_access_list = None + if fork.header_bal_hash_required(): + account_expectations = {} + + if selfdestruct_on_outer_call > 0: + account_expectations[ + selfdestruct_with_transfer_contract_address + ] = BalAccountExpectation( + storage_reads=[0, 1], # Storage was accessed + nonce_changes=[], + balance_changes=[], + code_changes=[], + storage_changes=[], + ) + account_expectations[selfdestruct_recipient_address] = ( + BalAccountExpectation( + balance_changes=[ + BalBalanceChange( + tx_index=1, + post_balance=1 + if selfdestruct_on_outer_call == 1 + else 2, + ) + ], + ) + ) + else: + account_expectations[ + selfdestruct_with_transfer_contract_address + ] = BalAccountExpectation( + storage_reads=[1], + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + balance_changes=[BalBalanceChange(tx_index=1, post_balance=1)], + code_changes=[ + BalCodeChange( + tx_index=1, + new_code=selfdestruct_with_transfer_contract_code, + ), + ], + storage_changes=[ + BalStorageSlot( + slot=0, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=1), + ], + ), + ], + ) + account_expectations[selfdestruct_recipient_address] = ( + BalAccountExpectation.empty() + ) + + expected_block_access_list = BlockAccessListExpectation( + account_expectations=account_expectations + ) + + state_test( + env=env, + pre=pre, + post=post, + tx=tx, + expected_block_access_list=expected_block_access_list, + ) @pytest.mark.parametrize( diff --git a/tests/frontier/opcodes/test_call_and_callcode_gas_calculation.py b/tests/frontier/opcodes/test_call_and_callcode_gas_calculation.py index abc9d9a28c..015217521d 100644 --- a/tests/frontier/opcodes/test_call_and_callcode_gas_calculation.py +++ b/tests/frontier/opcodes/test_call_and_callcode_gas_calculation.py @@ -40,6 +40,11 @@ Account, Address, Alloc, + BalAccountExpectation, + BalBalanceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessListExpectation, Bytecode, Environment, Op, @@ -71,33 +76,42 @@ def sufficient_gas( Calculate the sufficient gas for the nested call opcode with positive value transfer. """ - # memory_exp_cost is zero for our case. + gas_costs = fork.gas_costs() + cost = 0 if fork >= Berlin: - cost += 2600 # call and address_access_cost + cost += gas_costs.G_COLD_ACCOUNT_ACCESS elif Byzantium <= fork < Berlin: - cost += 700 # call + cost += 700 # Pre-Berlin warm call cost elif fork == Homestead: - cost += 40 # call + cost += 40 # Homestead call cost cost += 1 # mandatory callee gas allowance else: raise Exception("Only forks Homestead and >=Byzantium supported") is_value_call = callee_opcode in [Op.CALL, Op.CALLCODE] if is_value_call: - cost += 9000 # positive_value_cost + cost += gas_costs.G_CALL_VALUE if callee_opcode == Op.CALL: - cost += 25000 # empty_account_cost + cost += gas_costs.G_NEW_ACCOUNT - cost += callee_init_stack_gas + sufficient = callee_init_stack_gas + cost - return cost + return sufficient @pytest.fixture -def callee_code(pre: Alloc, callee_opcode: Op, fork: Fork) -> Bytecode: +def empty_account(pre: Alloc) -> Address: + """A guaranteed-to-be-empty account.""" + return pre.empty_account() + + +@pytest.fixture +def callee_code( + callee_opcode: Op, fork: Fork, empty_account: Address +) -> Bytecode: """ Code called by the caller contract: PUSH1 0x00 * 4 @@ -119,7 +133,7 @@ def callee_code(pre: Alloc, callee_opcode: Op, fork: Fork) -> Bytecode: return callee_opcode( unchecked=False, gas=1 if fork < Byzantium else Op.GAS, - address=pre.empty_account(), + address=empty_account, args_offset=0, args_size=0, ret_offset=0, @@ -197,6 +211,67 @@ def post( # noqa: D103 } +@pytest.fixture +def expected_block_access_list( + fork: Fork, + caller_address: Address, + callee_address: Address, + callee_opcode: Bytecode, + empty_account: Account, + gas_shortage: int, +) -> None | BlockAccessListExpectation: + """The expected block access list for >=Amsterdam cases.""" + if fork.header_bal_hash_required(): + if callee_opcode == Op.CALL: + if gas_shortage: + # call runs OOG after state access due to `is_account_alive` in + # `create_gas_cost` check + empty_account_expectation = BalAccountExpectation.empty() + else: + empty_account_expectation = BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=1) + ] + ) + else: + if gas_shortage: + # runs OOG before accessing empty acct (not read) + empty_account_expectation = None + else: + # if successful, only read is recorded + empty_account_expectation = BalAccountExpectation.empty() + + return BlockAccessListExpectation( + account_expectations={ + empty_account: empty_account_expectation, + caller_address: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=4) + ], + storage_reads=[0] if gas_shortage else [], + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=1), + ], + ), + ] + if not gas_shortage + else [], + ), + callee_address: BalAccountExpectation( + balance_changes=( + [BalBalanceChange(tx_index=1, post_balance=2)] + if not gas_shortage and callee_opcode == Op.CALL + else [] + ), + ), + } + ) + return None + + @pytest.mark.parametrize( "callee_opcode", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL] ) @@ -207,12 +282,19 @@ def test_value_transfer_gas_calculation( pre: Alloc, caller_tx: Transaction, post: Dict[str, Account], + expected_block_access_list: BlockAccessListExpectation, ) -> None: """ Tests the nested CALL/CALLCODE/DELEGATECALL/STATICCALL opcode gas consumption with a positive value transfer. """ - state_test(env=Environment(), pre=pre, post=post, tx=caller_tx) + state_test( + env=Environment(), + pre=pre, + post=post, + tx=caller_tx, + expected_block_access_list=expected_block_access_list, + ) @pytest.mark.parametrize( diff --git a/tests/json_infra/__init__.py b/tests/json_infra/__init__.py index fd2db5553f..6fa7c5dbbe 100644 --- a/tests/json_infra/__init__.py +++ b/tests/json_infra/__init__.py @@ -27,9 +27,13 @@ class _FixtureSource(TypedDict): "fixture_path": "tests/json_infra/fixtures/ethereum_tests", }, "latest_fork_tests": { - "url": "https://github.com/ethereum/execution-spec-tests/releases/download/v5.0.0/fixtures_develop.tar.gz", + "url": "https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz", "fixture_path": "tests/json_infra/fixtures/latest_fork_tests", }, + "amsterdam_tests": { + "url": "https://github.com/ethereum/execution-spec-tests/releases/download/bal%40v1.8.0/fixtures_bal.tar.gz", + "fixture_path": "tests/json_infra/fixtures/amsterdam_tests", + }, } diff --git a/tests/json_infra/helpers/load_blockchain_tests.py b/tests/json_infra/helpers/load_blockchain_tests.py index f390a1c1fd..6e86c48d56 100644 --- a/tests/json_infra/helpers/load_blockchain_tests.py +++ b/tests/json_infra/helpers/load_blockchain_tests.py @@ -138,6 +138,9 @@ def runtest(self) -> None: if hasattr(genesis_header, "requests_root"): parameters.append(()) + if hasattr(genesis_header, "block_access_list_hash"): + parameters.append([]) + genesis_block = load.fork.Block(*parameters) genesis_header_hash = hex_to_bytes( diff --git a/tests/paris/eip7610_create_collision/test_initcollision.py b/tests/paris/eip7610_create_collision/test_initcollision.py index 181b4575b1..b8382f634d 100644 --- a/tests/paris/eip7610_create_collision/test_initcollision.py +++ b/tests/paris/eip7610_create_collision/test_initcollision.py @@ -7,7 +7,10 @@ from execution_testing import ( Account, Alloc, + BalAccountExpectation, + BlockAccessListExpectation, Bytecode, + Fork, Initcode, Op, StateTestFiller, @@ -66,6 +69,7 @@ def test_init_collision_create_tx( collision_balance: int, collision_code: bytes, initcode: Bytecode, + fork: Fork, ) -> None: """ Test that a contract creation transaction exceptionally aborts when @@ -89,6 +93,14 @@ def test_init_collision_create_tx( code=collision_code, ) + expected_block_access_list = None + if fork.header_bal_hash_required(): + expected_block_access_list = BlockAccessListExpectation( + account_expectations={ + created_contract_address: BalAccountExpectation.empty() + } + ) + state_test( pre=pre, post={ @@ -97,6 +109,7 @@ def test_init_collision_create_tx( ), }, tx=tx, + expected_block_access_list=expected_block_access_list, ) diff --git a/tests/prague/eip7702_set_code_tx/test_gas.py b/tests/prague/eip7702_set_code_tx/test_gas.py index 048a84c44b..b415684bf7 100644 --- a/tests/prague/eip7702_set_code_tx/test_gas.py +++ b/tests/prague/eip7702_set_code_tx/test_gas.py @@ -18,6 +18,9 @@ Address, Alloc, AuthorizationTuple, + BalAccountExpectation, + BalNonceChange, + BlockAccessListExpectation, Bytecode, Bytes, ChainConfig, @@ -1269,6 +1272,25 @@ def test_call_to_pre_authorized_oog( sender=pre.fund_eoa(), ) + expected_block_access_list = None + if fork.header_bal_hash_required(): + # Sender nonce changes, callee is accessed but storage unchanged (OOG) + # auth_signer is tracked (we read its code to check delegation) + # delegation is NOT tracked (OOG before reading it) + account_expectations = { + tx.sender: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + callee_address: BalAccountExpectation.empty(), + # read for calculating delegation access cost: + auth_signer: BalAccountExpectation.empty(), + # OOG - not enough gas for delegation access: + delegation: None, + } + expected_block_access_list = BlockAccessListExpectation( + account_expectations=account_expectations + ) + state_test( pre=pre, tx=tx, @@ -1277,4 +1299,5 @@ def test_call_to_pre_authorized_oog( auth_signer: Account(code=Spec.delegation_designation(delegation)), delegation: Account(storage=Storage()), }, + expected_block_access_list=expected_block_access_list, ) diff --git a/tests/static/state_tests/stCreateTest/CreateOOGFromEOARefundsFiller.yml b/tests/static/state_tests/stCreateTest/CreateOOGFromEOARefundsFiller.yml deleted file mode 100644 index 4ce14cc9fe..0000000000 --- a/tests/static/state_tests/stCreateTest/CreateOOGFromEOARefundsFiller.yml +++ /dev/null @@ -1,483 +0,0 @@ -CreateOOGFromEOARefunds: - # Test that verifies the refunds are not applied on contract creation when the creation runs Out of Gas - env: - currentCoinbase: 2adc25665018aa1fe0e6bc666dac8fc2697ff9ba - currentDifficulty: '0x20000' - currentGasLimit: 0x100000000 - currentNumber: "1" - currentTimestamp: "1000" - - pre: - #### MAIN CALLER - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - balance: '0x3d0900' - code: '0x' - nonce: '1' - storage: {} - - ### HELPER CONTRACTS - # Simple contract to reset sstore and refund - 00000000000000000000000000000000000c0deA: - balance: '0' - code: | - :yul berlin - { - // Simple SSTORE to zero to get a refund - sstore(1, 0) - } - nonce: '1' - storage: { - '1': '1' - } - - # Simple contract that self-destructs to refund - 00000000000000000000000000000000000c0deD: - balance: '0' - code: | - :yul berlin - { - selfdestruct(origin()) - } - nonce: '1' - storage: { - '1': '1' - } - - # Simple contract that performs log operations - 00000000000000000000000000000000000c0de0: - balance: '0' - code: | - :yul berlin - { - mstore(0, 0xff) - log0(0, 32) - log1(0, 32, 0xfa) - log2(0, 32, 0xfa, 0xfb) - log3(0, 32, 0xfa, 0xfb, 0xfc) - log4(0, 32, 0xfa, 0xfb, 0xfc, 0xfd) - } - nonce: '1' - storage: { - '1': '1' - } - - # Init code that successfully creates contract but contains a refund - 00000000000000000000000000000000000c0de1: - balance: '0' - code: | - :yul berlin - { - sstore(0, 1) - sstore(0, 0) - return(0, 1) - } - nonce: '1' - storage: {} - - - transaction: - data: - # Create from EOA, Sstore Refund in Init Code, no OoG - - :label SStore_Refund_NoOoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - return(0, 1) - } - - # Create from EOA, Sstore Refund in Init Code, OoG on Code Deposit - - :label SStore_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - return(0, 5000) - } - - # Create from EOA, Sstore Refund in Init Code, OoG on Invalid opcode - - :label SStore_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - invalid() - } - - # Create from EOA, Sstore Refund in Call, no OoG - - :label SStore_Refund_NoOoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0, 0)) - return(0, 1) - } - - # Create from EOA, Sstore Refund in Call, OoG on Code Deposit - - :label SStore_Refund_OoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0, 0)) - return(0, 5000) - } - - # Create from EOA, Sstore Refund in Call, OoG on Invalid opcode - - :label SStore_Refund_OoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0, 0)) - invalid() - } - - # Create from EOA, Sstore Refund in DelegateCall, no OoG - - :label SStore_Refund_NoOoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - pop(delegatecall(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0)) - return(0, 1) - } - - # Create from EOA, Sstore Refund in DelegateCall, OoG on Code Deposit - - :label SStore_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - pop(delegatecall(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0)) - return(0, 5000) - } - - # Create from EOA, Sstore Refund in DelegateCall, OoG on Invalid opcode - - :label SStore_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - pop(delegatecall(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0)) - invalid() - } - - # Create from EOA, Sstore Refund in CallCode, no OoG - - :label SStore_Refund_NoOoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - pop(callcode(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0, 0)) - return(0, 1) - } - - # Create from EOA, Sstore Refund in CallCode, OoG on Code Deposit - - :label SStore_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - pop(callcode(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0, 0)) - return(0, 5000) - } - - # Create from EOA, Sstore Refund in CallCode, OoG on Invalid opcode - - :label SStore_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - pop(callcode(gas(), 0x00000000000000000000000000000000000c0deA, 0, 0, 0, 0, 0)) - invalid() - } - - # Create from EOA, Refund Self-destruct call, no OoG - - :label SelfDestruct_Refund_NoOoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0deD, 0, 0, 0, 0, 0)) - return(0, 1) - } - - # Create from EOA, Refund Self-destruct call, OoG on Code Deposit - - :label SelfDestruct_Refund_OoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0deD, 0, 0, 0, 0, 0)) - return(0, 5000) - } - - # Create from EOA, Refund Self-destruct call, OoG on Invalid opcode - - :label SelfDestruct_Refund_OoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0deD, 0, 0, 0, 0, 0)) - invalid() - } - - # Create from EOA, Log operation in call, no OoG - - :label LogOp_NoOoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0de0, 0, 0, 0, 0, 0)) - return(0, 1) - } - - # Create from EOA, Log operation in call, OoG on Code Deposit - - :label LogOp_OoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0de0, 0, 0, 0, 0, 0)) - return(0, 5000) - } - - # Create from EOA, Log operation in call, OoG on Invalid opcode - - :label LogOp_OoG :yul berlin { - sstore(0, 1) - pop(call(gas(), 0x00000000000000000000000000000000000c0de0, 0, 0, 0, 0, 0)) - invalid() - } - - # Create from EOA, Refund within CREATE, no OoG - - :label SStore_Create_Refund_NoOoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - let initcodeaddr := 0x00000000000000000000000000000000000c0de1 - let initcodelength := extcodesize(initcodeaddr) - extcodecopy(initcodeaddr, 0, 0, initcodelength) - pop(create(0, 0, initcodelength)) - return(add(initcodelength, 1), 1) - } - - # Create from EOA, Refund within CREATE, OoG on Code Deposit - - :label SStore_Create_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - let initcodeaddr := 0x00000000000000000000000000000000000c0de1 - let initcodelength := extcodesize(initcodeaddr) - extcodecopy(initcodeaddr, 0, 0, initcodelength) - pop(create(0, 0, initcodelength)) - return(add(initcodelength, 1), 5000) - } - - # Create from EOA, Refund within CREATE, OoG on Invalid opcode - - :label SStore_Create_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - let initcodeaddr := 0x00000000000000000000000000000000000c0de1 - let initcodelength := extcodesize(initcodeaddr) - extcodecopy(initcodeaddr, 0, 0, initcodelength) - pop(create(0, 0, initcodelength)) - invalid() - } - - # Create2 from EOA, Refund within CREATE, no OoG - - :label SStore_Create2_Refund_NoOoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - let initcodeaddr := 0x00000000000000000000000000000000000c0de1 - //let initcodelength := extcodesize(initcodeaddr) - //extcodecopy(initcodeaddr, 0, 0, initcodelength) - - //protection from solc version changing the init code - - let initcodelength := 15 - mstore(0, 0x6001600055600060005560016000f30000000000000000000000000000000000) - - pop(create2(0, 0, initcodelength, 0)) - return(add(initcodelength, 1), 1) - } - - # Create2 from EOA, Refund within CREATE, OoG on Code Deposit - - :label SStore_Create2_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - let initcodeaddr := 0x00000000000000000000000000000000000c0de1 - let initcodelength := extcodesize(initcodeaddr) - extcodecopy(initcodeaddr, 0, 0, initcodelength) - pop(create2(0, 0, initcodelength, 0)) - return(add(initcodelength, 1), 5000) - } - - # Create2 from EOA, Refund within CREATE, OoG on Invalid opcode - - :label SStore_Create2_Refund_OoG :yul berlin { - sstore(0, 1) - sstore(1, 1) - sstore(1, 0) - let initcodeaddr := 0x00000000000000000000000000000000000c0de1 - let initcodelength := extcodesize(initcodeaddr) - extcodecopy(initcodeaddr, 0, 0, initcodelength) - pop(create2(0, 0, initcodelength, 0)) - invalid() - } - - gasLimit: - - 0x61a80 - gasPrice: '10' - nonce: '1' - to: "" - secretKey: "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" - value: - - 0 - - expect: - - - indexes: - data: - - :label SStore_Refund_NoOoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - nonce: 1 - code: '0x00' - storage: { - '0': 1 - } - - indexes: - data: - - :label SStore_Refund_OoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - # When we OoG, we use up all the gas regardless of the refunds - balance: 0 - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - shouldnotexist: 1 - - - - indexes: - data: - - :label SelfDestruct_Refund_NoOoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - nonce: 1 - code: '0x00' - storage: { - '0': 1 - } - 00000000000000000000000000000000000c0deD: - balance: 0 - nonce: 1 - - - indexes: - data: - - :label SelfDestruct_Refund_OoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - # When we OoG, we use up all the gas regardless of the refunds - balance: 0 - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - shouldnotexist: 1 - - 00000000000000000000000000000000000c0deD: - code: '0x32FF' - nonce: '1' - storage: { - '1': '1' - } - - - indexes: - data: - - :label LogOp_NoOoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - nonce: 1 - code: '0x00' - storage: { - '0': 1 - } - - indexes: - data: - - :label LogOp_OoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - # When we OoG, we use up all the gas regardless of the refunds - balance: 0 - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - shouldnotexist: 1 - - - indexes: - data: - - :label SStore_Create_Refund_NoOoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - nonce: 2 - code: '0x00' - storage: { - '0': 1 - } - e3476106159f87477ad639e3ddcbb6b240efe459: - nonce: 1 - code: '0x00' - storage: {} - - - indexes: - data: - - :label SStore_Create_Refund_OoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - # When we OoG, we use up all the gas regardless of the refunds - balance: 0 - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - shouldnotexist: 1 - e3476106159f87477ad639e3ddcbb6b240efe459: - shouldnotexist: 1 - - - indexes: - data: - - :label SStore_Create2_Refund_NoOoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - nonce: 2 - code: '0x00' - storage: { - '0': 1 - } - 1eeb9ca3824a07c140fc01aa562a3a896f44e790: - nonce: 1 - code: '0x00' - storage: {} - - - indexes: - data: - - :label SStore_Create2_Refund_OoG - network: - - '>=Cancun' - result: - - a94f5374fce5edbc8e2a8697c15331677e6ebf0b: - # When we OoG, we use up all the gas regardless of the refunds - balance: 0 - nonce: 2 - - ec0e71ad0a90ffe1909d27dac207f7680abba42d: - shouldnotexist: 1 - 1eeb9ca3824a07c140fc01aa562a3a896f44e790: - shouldnotexist: 1 diff --git a/whitelist.txt b/whitelist.txt index 4f73730fd5..0d4f493ffc 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -1335,4 +1335,10 @@ ZeroPaddedHexNumber zfill zkevm Zsh -zsh \ No newline at end of file +zsh +slot1 +slot2 +lexicographically +uint16 +uint128 +630m