From 208256f07aab4aaecad6e86d7e62288e71f3c195 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 1 Jul 2025 08:55:46 +0000 Subject: [PATCH 01/35] =?UTF-8?q?=F0=9F=9A=A7=20wip(EIP-7928):=20Block-lev?= =?UTF-8?q?el=20Access=20Lists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unscheduled/eip7928_block-level_access_lists/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/unscheduled/eip7928_block-level_access_lists/__init__.py diff --git a/tests/unscheduled/eip7928_block-level_access_lists/__init__.py b/tests/unscheduled/eip7928_block-level_access_lists/__init__.py new file mode 100644 index 00000000000..2f715be28c3 --- /dev/null +++ b/tests/unscheduled/eip7928_block-level_access_lists/__init__.py @@ -0,0 +1 @@ +"""Tests for [EIP-7928: Block-level Access Lists](https://eips.ethereum.org/EIPS/eip-7928).""" From ab00c0a52f4fcb8d25e9c2472080ccbbd8623ffa Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 1 Jul 2025 09:06:25 +0000 Subject: [PATCH 02/35] =?UTF-8?q?=E2=9C=A8=20feat(EIP-7928):=20Spec=20para?= =?UTF-8?q?ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eip7928_block-level_access_lists/spec.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/unscheduled/eip7928_block-level_access_lists/spec.py diff --git a/tests/unscheduled/eip7928_block-level_access_lists/spec.py b/tests/unscheduled/eip7928_block-level_access_lists/spec.py new file mode 100644 index 00000000000..56b4a954ad2 --- /dev/null +++ b/tests/unscheduled/eip7928_block-level_access_lists/spec.py @@ -0,0 +1,44 @@ +"""Reference spec for [EIP-7928: Block-level Access Lists.](https://eips.ethereum.org/EIPS/eip-7928).""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Reference specification.""" + + git_path: str + version: str + + +ref_spec_7928 = ReferenceSpec( + git_path="EIPS/eip-7928.md", + version="35732baa14cfea785d9c58d5f18033392b7ed886", +) + + +@dataclass(frozen=True) +class Spec: + """Constants and parameters from EIP-7928.""" + + # SSZ encoding is used for block access list data structures + BAL_ENCODING_FORMAT: str = "SSZ" + + # Maximum limits for block access list data structures + TARGET_MAX_GAS_LIMIT = 600_000_000 + MAX_TXS: int = 30_000 + MAX_SLOTS: int = 300_000 + MAX_ACCOUNTS: int = 300_000 + # TODO: Use this as a function of the current fork. + MAX_CODE_SIZE: int = 24_576 # 24 KiB + + # Type size constants + ADDRESS_SIZE: int = 20 # Ethereum address size in bytes + STORAGE_KEY_SIZE: int = 32 # Storage slot key size in bytes + STORAGE_VALUE_SIZE: int = 32 # Storage value size in bytes + HASH_SIZE: int = 32 # Hash size in bytes + + # Numeric type limits + MAX_TX_INDEX: int = 2**16 - 1 # uint16 max value + MAX_BALANCE: int = 2**128 - 1 # uint128 max value + MAX_NONCE: int = 2**64 - 1 # uint64 max value From 323866746aa85e81d530bb8e768b0fab2f22d5dc Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 1 Jul 2025 09:35:46 +0000 Subject: [PATCH 03/35] =?UTF-8?q?=F0=9F=A7=B9=20chore(EIP-7928):=20Rename?= =?UTF-8?q?=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__init__.py | 0 .../spec.py | 0 .../test_validate_bal.py | 416 ++++++++++++++++++ 3 files changed, 416 insertions(+) rename tests/unscheduled/{eip7928_block-level_access_lists => eip7928_block_level_access_lists}/__init__.py (100%) rename tests/unscheduled/{eip7928_block-level_access_lists => eip7928_block_level_access_lists}/spec.py (100%) create mode 100644 tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py diff --git a/tests/unscheduled/eip7928_block-level_access_lists/__init__.py b/tests/unscheduled/eip7928_block_level_access_lists/__init__.py similarity index 100% rename from tests/unscheduled/eip7928_block-level_access_lists/__init__.py rename to tests/unscheduled/eip7928_block_level_access_lists/__init__.py diff --git a/tests/unscheduled/eip7928_block-level_access_lists/spec.py b/tests/unscheduled/eip7928_block_level_access_lists/spec.py similarity index 100% rename from tests/unscheduled/eip7928_block-level_access_lists/spec.py rename to tests/unscheduled/eip7928_block_level_access_lists/spec.py diff --git a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py new file mode 100644 index 00000000000..d2de8df8708 --- /dev/null +++ b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py @@ -0,0 +1,416 @@ +"""Tests for validating EIP-7928: Block-level Access Lists (BAL).""" + +import pytest + +from ethereum_test_tools import ( + Alloc, + BlockchainTestFiller, +) + + +@pytest.mark.valid_from("Amsterdam") +class TestBlockAccessListValidity: + """Test block access list validity and data structure integrity.""" + + def test_bal_hash_basic_transaction( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL hash generation for basic ETH transfer.""" + # TODO: Implement BAL hash validation for basic ETH transfer + pass + + def test_bal_hash_storage_operations( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL hash generation for storage read/write operations.""" + # TODO: Implement BAL hash validation for storage operations + pass + + def test_bal_hash_balance_changes( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL hash generation for balance changes.""" + # TODO: Implement BAL hash validation for balance changes + pass + + def test_bal_hash_nonce_changes( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL hash generation for nonce changes.""" + # TODO: Implement BAL hash validation for nonce changes + pass + + def test_bal_hash_code_changes( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL hash generation for contract code changes (CREATE/CREATE2).""" + # TODO: Implement BAL hash validation for code changes + pass + + def test_bal_ordering_requirements( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test that BAL entries follow strict address and storage key ordering.""" + # TODO: Implement ordering validation tests + pass + + def test_bal_completeness_validation( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test that BAL includes all accessed accounts and storage slots.""" + # TODO: Implement completeness validation tests + pass + + def test_bal_empty_block( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL hash generation for empty blocks (only coinbase access).""" + # TODO: Implement empty block BAL validation + pass + + def test_bal_multiple_transactions( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL hash generation for blocks with multiple transactions.""" + # TODO: Implement multiple transaction BAL validation + pass + + def test_bal_failed_transaction_inclusion( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test that failed transactions still contribute to BAL.""" + # TODO: Implement failed transaction BAL inclusion tests + pass + + +@pytest.mark.valid_from("Amsterdam") +class TestBlockAccessListSSZEncoding: + """Test SSZ encoding/decoding of block access list data structures.""" + + def test_ssz_encoding_storage_changes( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test SSZ round-trip encoding for storage changes.""" + # TODO: Implement SSZ encoding tests for storage changes + pass + + def test_ssz_encoding_balance_changes( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test SSZ round-trip encoding for balance changes.""" + # TODO: Implement SSZ encoding tests for balance changes + pass + + def test_ssz_encoding_nonce_changes( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test SSZ round-trip encoding for nonce changes.""" + # TODO: Implement SSZ encoding tests for nonce changes + pass + + def test_ssz_encoding_code_changes( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test SSZ round-trip encoding for code changes.""" + # TODO: Implement SSZ encoding tests for code changes + pass + + def test_ssz_encoding_full_bal( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test SSZ round-trip encoding for complete BAL structure.""" + # TODO: Implement full BAL SSZ encoding tests + pass + + +@pytest.mark.valid_from("Amsterdam") +class TestBlockAccessListEdgeCases: + """Test edge cases and error conditions for block access lists.""" + + def test_bal_large_storage_operations( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL with large numbers of storage operations.""" + # TODO: Implement large storage operation tests + pass + + def test_bal_contract_selfdestruct( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL handling of contract self-destruct operations.""" + # TODO: Implement self-destruct BAL tests + pass + + def test_bal_create2_deterministic_addresses( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL with CREATE2 deterministic contract addresses.""" + # TODO: Implement CREATE2 BAL tests + pass + + def test_bal_zero_value_changes( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL handling of zero-value state changes.""" + # TODO: Implement zero-value change tests + pass + + def test_bal_maximum_block_size( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL with maximum possible block size and access patterns.""" + # TODO: Implement maximum block size BAL tests + pass + + +@pytest.mark.valid_from("Amsterdam") +class TestBlockAccessListValidationFailures: + """Test validation failure scenarios for malformed or incorrect BALs.""" + + def test_invalid_bal_hash_rejection( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test that blocks with invalid BAL hashes are rejected.""" + # TODO: Implement invalid BAL hash rejection tests + pass + + def test_incomplete_bal_rejection( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test that blocks with incomplete BALs are rejected.""" + # TODO: Implement incomplete BAL rejection tests + pass + + def test_incorrect_ordering_rejection( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test that blocks with incorrectly ordered BAL entries are rejected.""" + # TODO: Implement incorrect ordering rejection tests + pass + + def test_malformed_ssz_rejection( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test that blocks with malformed SSZ-encoded BALs are rejected.""" + # TODO: Implement malformed SSZ rejection tests + pass + + +@pytest.mark.valid_from("Amsterdam") +class TestBlockAccessListLimits: + """Test EIP-7928 specification limits and boundaries.""" + + def test_max_transactions_limit( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with MAX_TXS transactions per block.""" + # TODO: Test with Spec.MAX_TXS (30,000) transactions + pass + + def test_max_accounts_limit( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with MAX_ACCOUNTS accessed accounts.""" + # TODO: Test with Spec.MAX_ACCOUNTS (300,000) accessed accounts + pass + + def test_max_storage_slots_limit( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with MAX_SLOTS storage slots accessed.""" + # TODO: Test with Spec.MAX_SLOTS (300,000) storage slots + pass + + def test_max_code_size_limit( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with MAX_CODE_SIZE contract deployments.""" + # TODO: Test with Spec.MAX_CODE_SIZE (24,576 bytes) contract code + pass + + def test_max_tx_index_boundary( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation at MAX_TX_INDEX boundary.""" + # TODO: Test with transaction index at Spec.MAX_TX_INDEX (65,535) + pass + + def test_max_balance_boundary( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with MAX_BALANCE values.""" + # TODO: Test with balance at Spec.MAX_BALANCE (2^128 - 1) + pass + + def test_max_nonce_boundary( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with MAX_NONCE values.""" + # TODO: Test with nonce at Spec.MAX_NONCE (2^64 - 1) + pass + + def test_target_max_gas_limit_boundary( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation at TARGET_MAX_GAS_LIMIT.""" + # TODO: Test with block gas limit at Spec.TARGET_MAX_GAS_LIMIT (600,000,000) + pass + + def test_address_size_validation( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with ADDRESS_SIZE requirements.""" + # TODO: Test address size validation (Spec.ADDRESS_SIZE = 20 bytes) + pass + + def test_storage_key_size_validation( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with STORAGE_KEY_SIZE requirements.""" + # TODO: Test storage key size validation (Spec.STORAGE_KEY_SIZE = 32 bytes) + pass + + def test_storage_value_size_validation( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with STORAGE_VALUE_SIZE requirements.""" + # TODO: Test storage value size validation (Spec.STORAGE_VALUE_SIZE = 32 bytes) + pass + + def test_hash_size_validation( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with HASH_SIZE requirements.""" + # TODO: Test BAL hash size validation (Spec.HASH_SIZE = 32 bytes) + pass + + +@pytest.mark.valid_from("Amsterdam") +class TestBlockAccessListBoundaryConditions: + """Test boundary conditions and edge cases for EIP-7928 limits.""" + + def test_exceed_max_transactions( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL rejection when exceeding MAX_TXS.""" + # TODO: Test rejection with Spec.MAX_TXS + 1 transactions + pass + + def test_exceed_max_accounts( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL rejection when exceeding MAX_ACCOUNTS.""" + # TODO: Test rejection with Spec.MAX_ACCOUNTS + 1 accounts + pass + + def test_exceed_max_storage_slots( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL rejection when exceeding MAX_SLOTS.""" + # TODO: Test rejection with Spec.MAX_SLOTS + 1 storage slots + pass + + def test_exceed_max_code_size( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL rejection when exceeding MAX_CODE_SIZE.""" + # TODO: Test rejection with Spec.MAX_CODE_SIZE + 1 byte contract + pass + + def test_zero_values_at_boundaries( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with zero values at type boundaries.""" + # TODO: Test with zero balances, nonces, and transaction indices + pass + + def test_minimum_valid_values( + self, + pre: Alloc, + blockchain_test: BlockchainTestFiller, + ): + """Test BAL validation with minimum valid values.""" + # TODO: Test with minimum valid balances, nonces, and indices + pass From 0c43e3f52faff547fb0b60d67385d0a3448d9012 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 1 Jul 2025 09:36:18 +0000 Subject: [PATCH 04/35] =?UTF-8?q?=E2=9C=A8=20feat(EIP-7928):=20Test=20case?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_validate_bal.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py index d2de8df8708..6557733aefb 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py +++ b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py @@ -9,8 +9,8 @@ @pytest.mark.valid_from("Amsterdam") -class TestBlockAccessListValidity: - """Test block access list validity and data structure integrity.""" +class TestBALValidity: + """Test BAL validity and data structure integrity.""" def test_bal_hash_basic_transaction( self, @@ -104,8 +104,8 @@ def test_bal_failed_transaction_inclusion( @pytest.mark.valid_from("Amsterdam") -class TestBlockAccessListSSZEncoding: - """Test SSZ encoding/decoding of block access list data structures.""" +class TestBALEncoding: + """Test SSZ encoding/decoding of BAL data structures.""" def test_ssz_encoding_storage_changes( self, @@ -154,8 +154,8 @@ def test_ssz_encoding_full_bal( @pytest.mark.valid_from("Amsterdam") -class TestBlockAccessListEdgeCases: - """Test edge cases and error conditions for block access lists.""" +class TestBALEdgeCases: + """Test edge cases and error conditions for BAL.""" def test_bal_large_storage_operations( self, @@ -204,7 +204,7 @@ def test_bal_maximum_block_size( @pytest.mark.valid_from("Amsterdam") -class TestBlockAccessListValidationFailures: +class TestBALValidationFailures: """Test validation failure scenarios for malformed or incorrect BALs.""" def test_invalid_bal_hash_rejection( @@ -245,7 +245,7 @@ def test_malformed_ssz_rejection( @pytest.mark.valid_from("Amsterdam") -class TestBlockAccessListLimits: +class TestBALLimits: """Test EIP-7928 specification limits and boundaries.""" def test_max_transactions_limit( @@ -358,7 +358,7 @@ def test_hash_size_validation( @pytest.mark.valid_from("Amsterdam") -class TestBlockAccessListBoundaryConditions: +class TestBALBoundaryConditions: """Test boundary conditions and edge cases for EIP-7928 limits.""" def test_exceed_max_transactions( From b1a9d2fed16ec0493c06778dea3ebe525624b156 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Thu, 3 Jul 2025 14:45:01 +0000 Subject: [PATCH 05/35] =?UTF-8?q?=E2=9C=A8=20feat(EIP-7928):=20Add=20BAL?= =?UTF-8?q?=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 + src/ethereum_test_fixtures/blockchain.py | 5 ++ src/ethereum_test_forks/base_fork.py | 6 ++ src/pytest_plugins/eels_resolutions.json | 32 +++++----- .../eip7928_block_level_access_lists/spec.py | 3 + .../test_validate_bal.py | 58 ++++++++++++++++--- 6 files changed, 82 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48ba580d04a..6228e50fa5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dependencies = [ "eth-abi>=5.2.0", "joblib>=1.4.2", "ckzg>=2.1.1", + "ethereum-execution", ] [project.urls] @@ -175,3 +176,4 @@ required-version = ">=0.7.0" [tool.uv.sources] ethereum-spec-evm-resolver = { git = "https://github.com/spencer-tb/ethereum-spec-evm-resolver", rev = "aec6a628b8d0f1c791a8378c5417a089566135ac" } +ethereum-execution = { git = "https://github.com/nerolation/execution-specs", rev = "bals" } diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index 61688a4c3dc..46b022e6190 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -18,6 +18,8 @@ import ethereum_rlp as eth_rlp import pytest + +from ethereum.osaka.ssz_types import BlockAccessList from ethereum_types.numeric import Uint from pydantic import AliasChoices, Field, PlainSerializer, computed_field, model_validator @@ -165,6 +167,7 @@ class FixtureHeader(CamelModel): None ) requests_hash: Annotated[Hash, HeaderForkRequirement("requests")] | None = Field(None) + bal_hash: Annotated[Hash, HeaderForkRequirement("bal")] | None = Field(None) fork: Fork | None = Field(None, exclude=True) @@ -262,6 +265,8 @@ class FixtureExecutionPayload(CamelModel): transactions: List[Bytes] withdrawals: List[Withdrawal] | None = None + # block_access_lists: BlockAccessList | None = Field(None, description="Block Access List") + @classmethod def from_fixture_header( cls, diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 329e4b99cc0..92d38944ebb 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -269,6 +269,12 @@ def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) -> """Return true if the header must contain beacon chain requests.""" pass + @classmethod + @abstractmethod + def header_bal_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 diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index 96e5416e9cc..03d28ae5ced 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -41,20 +41,20 @@ "same_as": "EELSMaster" }, "BPO1": { - "same_as": "EELSMaster" - }, - "BPO2": { - "same_as": "EELSMaster" - }, - "BPO3": { - "same_as": "EELSMaster" - }, - "BPO4": { - "same_as": "EELSMaster" - }, - "Amsterdam": { - "git_url": "https://github.com/fselmo/execution-specs.git", - "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "59ff2946ca30879079b2ef2b601ae09106ca08f7" - } + "same_as": "EELSMaster" + }, + "BPO2": { + "same_as": "EELSMaster" + }, + "BPO3": { + "same_as": "EELSMaster" + }, + "BPO4": { + "same_as": "EELSMaster" + }, + "Amsterdam": { + "git_url": "https://github.com/fselmo/execution-specs.git", + "branch": "feat/amsterdam-fork-and-block-access-lists", + "commit": "59ff2946ca30879079b2ef2b601ae09106ca08f7" + } } diff --git a/tests/unscheduled/eip7928_block_level_access_lists/spec.py b/tests/unscheduled/eip7928_block_level_access_lists/spec.py index 56b4a954ad2..5de8763da96 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/spec.py +++ b/tests/unscheduled/eip7928_block_level_access_lists/spec.py @@ -2,6 +2,9 @@ from dataclasses import dataclass +ACTIVATION_FORK_NAME = "Cancun" +"""The fork name for EIP-7928 activation.""" + @dataclass(frozen=True) class ReferenceSpec: diff --git a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py index 6557733aefb..e082badab76 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py +++ b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py @@ -2,13 +2,22 @@ import pytest +from ethereum_test_base_types import Bytes from ethereum_test_tools import ( + Account, Alloc, + Block, BlockchainTestFiller, + Transaction, ) +from .spec import ACTIVATION_FORK_NAME, ref_spec_7928 -@pytest.mark.valid_from("Amsterdam") +REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path +REFERENCE_SPEC_VERSION = ref_spec_7928.version + + +@pytest.mark.valid_from(ACTIVATION_FORK_NAME) class TestBALValidity: """Test BAL validity and data structure integrity.""" @@ -18,8 +27,41 @@ def test_bal_hash_basic_transaction( blockchain_test: BlockchainTestFiller, ): """Test BAL hash generation for basic ETH transfer.""" - # TODO: Implement BAL hash validation for basic ETH transfer - pass + # Setup accounts for basic ETH transfer + + transfer_amount = 1000 + sender = pre.fund_eoa() + recipient = pre.fund_eoa(amount=0) + + # Create a basic ETH transfer transaction + tx = Transaction( + sender=sender, + to=recipient, + value=1000, + ) + + # This represents a mock SSZ-encoded Block Access List + mock_bal = Bytes(b"Mock BAL DATA") + bal_hash = mock_bal.keccak256() + + # Create block with custom header that includes BAL hash + block = Block(txs=[tx], bal_hash=bal_hash) + + # Execute the blockchain test + blockchain_test( + pre=pre, + blocks=[block], + post={ + sender: Account( + nonce=1, + ), + recipient: Account(balance=transfer_amount), + }, + ) + + # Note: In the generated fixture, the block header will include: + # - bal_hash: the computed hash of the Block Access List + # - bal_data: the SSZ-encoded Block Access List data (when framework supports it) def test_bal_hash_storage_operations( self, @@ -103,7 +145,7 @@ def test_bal_failed_transaction_inclusion( pass -@pytest.mark.valid_from("Amsterdam") +@pytest.mark.valid_from(ACTIVATION_FORK_NAME) class TestBALEncoding: """Test SSZ encoding/decoding of BAL data structures.""" @@ -153,7 +195,7 @@ def test_ssz_encoding_full_bal( pass -@pytest.mark.valid_from("Amsterdam") +@pytest.mark.valid_from(ACTIVATION_FORK_NAME) class TestBALEdgeCases: """Test edge cases and error conditions for BAL.""" @@ -203,7 +245,7 @@ def test_bal_maximum_block_size( pass -@pytest.mark.valid_from("Amsterdam") +@pytest.mark.valid_from(ACTIVATION_FORK_NAME) class TestBALValidationFailures: """Test validation failure scenarios for malformed or incorrect BALs.""" @@ -244,7 +286,7 @@ def test_malformed_ssz_rejection( pass -@pytest.mark.valid_from("Amsterdam") +@pytest.mark.valid_from(ACTIVATION_FORK_NAME) class TestBALLimits: """Test EIP-7928 specification limits and boundaries.""" @@ -357,7 +399,7 @@ def test_hash_size_validation( pass -@pytest.mark.valid_from("Amsterdam") +@pytest.mark.valid_from(ACTIVATION_FORK_NAME) class TestBALBoundaryConditions: """Test boundary conditions and edge cases for EIP-7928 limits.""" From bfaba985de868a9e5071b4848869bdf31f359b9a Mon Sep 17 00:00:00 2001 From: raxhvl Date: Sun, 6 Jul 2025 08:39:20 +0000 Subject: [PATCH 06/35] =?UTF-8?q?=F0=9F=A7=B9=20chore(EIP-7928):=20bal=5Fh?= =?UTF-8?q?ash=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ethereum_test_fixtures/blockchain.py | 4 +++- src/ethereum_test_forks/base_fork.py | 2 +- src/ethereum_test_forks/forks/forks.py | 9 +++++++++ src/ethereum_test_types/block_types.py | 5 +++++ .../unscheduled/eip7928_block_level_access_lists/spec.py | 2 +- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index 46b022e6190..03e4b895e30 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -167,7 +167,7 @@ class FixtureHeader(CamelModel): None ) requests_hash: Annotated[Hash, HeaderForkRequirement("requests")] | None = Field(None) - bal_hash: Annotated[Hash, HeaderForkRequirement("bal")] | None = Field(None) + bal_hash: Annotated[Hash, HeaderForkRequirement("bal_hash")] | None = Field(None) fork: Fork | None = Field(None, exclude=True) @@ -234,7 +234,9 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> "FixtureHead extras = { "state_root": state_root, "requests_hash": Requests() if fork.header_requests_required(0, 0) else None, + "bal_hash": Hash(0) if fork.header_bal_hash_required(0, 0) else None, "fork": fork, + } return FixtureHeader(**environment_values, **extras) diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 92d38944ebb..2ce7a897057 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -271,7 +271,7 @@ def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) -> @classmethod @abstractmethod - def header_bal_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: + 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 diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 74c974c1f30..96d43879e3b 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -1846,3 +1846,12 @@ class Amsterdam(Osaka): def is_deployed(cls) -> bool: """Return True if this fork is deployed.""" return False + + +class BlockAccessLists(Prague): + """A development fork for Block Access Lists.""" + + @classmethod + def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: + """Hash of the block access list is required starting from this fork.""" + return True diff --git a/src/ethereum_test_types/block_types.py b/src/ethereum_test_types/block_types.py index 4b8ceab597a..4b9d2632b48 100644 --- a/src/ethereum_test_types/block_types.py +++ b/src/ethereum_test_types/block_types.py @@ -112,6 +112,7 @@ class Environment(EnvironmentGeneric[ZeroPaddedHexNumber]): ommers: List[Hash] = Field(default_factory=list) withdrawals: List[Withdrawal] | None = Field(None) extra_data: Bytes = Field(Bytes(b"\x00"), exclude=True) + bal_hash: Hash | None = Field(None) @computed_field # type: ignore[misc] @cached_property @@ -171,6 +172,10 @@ def set_fork_requirements(self, fork: Fork) -> "Environment": ): updated_values["parent_beacon_block_root"] = 0 + if fork.header_bal_hash_required(number, timestamp) and self.bal_hash is None: + # TODO:(@BAL): This should be set to the actual bal_hash value. + updated_values["bal_hash"] = Hash(0) + return self.copy(**updated_values) def __hash__(self) -> int: diff --git a/tests/unscheduled/eip7928_block_level_access_lists/spec.py b/tests/unscheduled/eip7928_block_level_access_lists/spec.py index 5de8763da96..7d09ec8c781 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/spec.py +++ b/tests/unscheduled/eip7928_block_level_access_lists/spec.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -ACTIVATION_FORK_NAME = "Cancun" +ACTIVATION_FORK_NAME = "BlockAccessLists" """The fork name for EIP-7928 activation.""" From 7489c352c138e7b487bd8b90a7329dc524ad34c5 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Sun, 6 Jul 2025 16:22:19 +0000 Subject: [PATCH 07/35] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Populate=20bal=5F?= =?UTF-8?q?hash=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ethereum_test_specs/blockchain.py | 3 +++ src/ethereum_test_types/block_types.py | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index c225dbf1815..4afa0acbbd4 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -121,6 +121,7 @@ class Header(CamelModel): excess_blob_gas: Removable | HexNumber | None = None parent_beacon_block_root: Removable | Hash | None = None requests_hash: Removable | Hash | None = None + bal_hash: Removable | Hash | None = None REMOVE_FIELD: ClassVar[Removable] = Removable() """ @@ -269,6 +270,8 @@ def set_environment(self, env: Environment) -> Environment: new_env_values["blob_gas_used"] = self.blob_gas_used if not isinstance(self.parent_beacon_block_root, Removable): new_env_values["parent_beacon_block_root"] = self.parent_beacon_block_root + if not isinstance(self.requests_hash, Removable): + new_env_values["bal_hash"] = self.bal_hash """ These values are required, but they depend on the previous environment, so they can be calculated here. diff --git a/src/ethereum_test_types/block_types.py b/src/ethereum_test_types/block_types.py index 4b9d2632b48..d7349345f64 100644 --- a/src/ethereum_test_types/block_types.py +++ b/src/ethereum_test_types/block_types.py @@ -112,6 +112,8 @@ class Environment(EnvironmentGeneric[ZeroPaddedHexNumber]): ommers: List[Hash] = Field(default_factory=list) withdrawals: List[Withdrawal] | None = Field(None) extra_data: Bytes = Field(Bytes(b"\x00"), exclude=True) + + # EIP-7928: Block-level access lists bal_hash: Hash | None = Field(None) @computed_field # type: ignore[misc] @@ -172,10 +174,6 @@ def set_fork_requirements(self, fork: Fork) -> "Environment": ): updated_values["parent_beacon_block_root"] = 0 - if fork.header_bal_hash_required(number, timestamp) and self.bal_hash is None: - # TODO:(@BAL): This should be set to the actual bal_hash value. - updated_values["bal_hash"] = Hash(0) - return self.copy(**updated_values) def __hash__(self) -> int: From 5dbd10bc126ac2e666a782828b4e1839e0c79803 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Sun, 6 Jul 2025 18:00:00 +0000 Subject: [PATCH 08/35] =?UTF-8?q?=E2=9C=A8=20feat(EIP-7928):=20Add=20BAL?= =?UTF-8?q?=20to=20block=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ethereum_test_fixtures/blockchain.py | 5 +++-- src/ethereum_test_specs/blockchain.py | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index 03e4b895e30..2928f05f5ab 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -267,8 +267,6 @@ class FixtureExecutionPayload(CamelModel): transactions: List[Bytes] withdrawals: List[Withdrawal] | None = None - # block_access_lists: BlockAccessList | None = Field(None, description="Block Access List") - @classmethod def from_fixture_header( cls, @@ -435,6 +433,9 @@ class FixtureBlockBase(CamelModel): ommers: List[FixtureHeader] = Field(default_factory=list, alias="uncleHeaders") withdrawals: List[FixtureWithdrawal] | None = None execution_witness: WitnessChunk | None = None + block_access_lists: Bytes | None = Field( + None, description="Serialized EIP-7928 Block Access Lists" + ) @computed_field(alias="blocknumber") # type: ignore[misc] @cached_property diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 4afa0acbbd4..2e3c677998e 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -241,6 +241,10 @@ class Block(Header): """ Post state for verification after block execution in BlockchainTest """ + block_access_lists: Bytes | None = Field(None) + """ + EIP-7928: Block-level access lists (serialized). + """ def set_environment(self, env: Environment) -> Environment: """ @@ -270,8 +274,13 @@ def set_environment(self, env: Environment) -> Environment: new_env_values["blob_gas_used"] = self.blob_gas_used if not isinstance(self.parent_beacon_block_root, Removable): new_env_values["parent_beacon_block_root"] = self.parent_beacon_block_root - if not isinstance(self.requests_hash, Removable): - new_env_values["bal_hash"] = self.bal_hash + if not isinstance(self.requests_hash, Removable) and self.block_access_lists is not None: + new_env_values["bal_hash"] = self.block_access_lists.keccak256() + if ( + not isinstance(self.block_access_lists, Removable) + and self.block_access_lists is not None + ): + new_env_values["block_access_lists"] = self.block_access_lists """ These values are required, but they depend on the previous environment, so they can be calculated here. @@ -311,6 +320,7 @@ class BuiltBlock(CamelModel): expected_exception: BLOCK_EXCEPTION_TYPE = None engine_api_error_code: EngineAPIError | None = None fork: Fork + block_access_lists: Bytes def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock: """Get a FixtureBlockBase from the built block.""" @@ -322,6 +332,7 @@ def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock: if self.withdrawals is not None else None ), + block_access_lists=self.block_access_lists, fork=self.fork, ).with_rlp(txs=self.txs) @@ -606,6 +617,7 @@ def generate_block_data( expected_exception=block.exception, engine_api_error_code=block.engine_api_error_code, fork=fork, + block_access_lists=block.block_access_lists, ) try: From a5059e73324a8cece3c8413c3813df629b2ccf63 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Mon, 7 Jul 2025 07:28:05 +0000 Subject: [PATCH 09/35] =?UTF-8?q?=E2=9C=A8=20feat(EIP-7928):=20Add=20pokeb?= =?UTF-8?q?al=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_validate_bal.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py index e082badab76..ce3f58f3aad 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py +++ b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py @@ -1,8 +1,8 @@ """Tests for validating EIP-7928: Block-level Access Lists (BAL).""" import pytest +from pokebal.bal.types import BlockAccessList -from ethereum_test_base_types import Bytes from ethereum_test_tools import ( Account, Alloc, @@ -29,6 +29,9 @@ def test_bal_hash_basic_transaction( """Test BAL hash generation for basic ETH transfer.""" # Setup accounts for basic ETH transfer + # TODO: Populate BAL. + bal = BlockAccessList() + transfer_amount = 1000 sender = pre.fund_eoa() recipient = pre.fund_eoa(amount=0) @@ -40,12 +43,8 @@ def test_bal_hash_basic_transaction( value=1000, ) - # This represents a mock SSZ-encoded Block Access List - mock_bal = Bytes(b"Mock BAL DATA") - bal_hash = mock_bal.keccak256() - # Create block with custom header that includes BAL hash - block = Block(txs=[tx], bal_hash=bal_hash) + block = Block(txs=[tx], block_access_lists=bal.serialize()) # Execute the blockchain test blockchain_test( From 907bb72344846fde47f743130c0d618aaa21051f Mon Sep 17 00:00:00 2001 From: raxhvl Date: Mon, 28 Jul 2025 05:25:16 +0000 Subject: [PATCH 10/35] =?UTF-8?q?=E2=9C=A8=20feat:=20update=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ethereum_test_specs/blockchain.py | 1 + src/ethereum_test_types/block_types.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 2e3c677998e..2402732226f 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -276,6 +276,7 @@ def set_environment(self, env: Environment) -> Environment: new_env_values["parent_beacon_block_root"] = self.parent_beacon_block_root if not isinstance(self.requests_hash, Removable) and self.block_access_lists is not None: new_env_values["bal_hash"] = self.block_access_lists.keccak256() + new_env_values["block_access_lists"] = self.block_access_lists if ( not isinstance(self.block_access_lists, Removable) and self.block_access_lists is not None diff --git a/src/ethereum_test_types/block_types.py b/src/ethereum_test_types/block_types.py index d7349345f64..268296f49a1 100644 --- a/src/ethereum_test_types/block_types.py +++ b/src/ethereum_test_types/block_types.py @@ -115,6 +115,7 @@ class Environment(EnvironmentGeneric[ZeroPaddedHexNumber]): # EIP-7928: Block-level access lists bal_hash: Hash | None = Field(None) + block_access_lists: Bytes | None = Field(None) @computed_field # type: ignore[misc] @cached_property From 674dc25e0dcba0ff9f50047488f5c80b83045820 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 12 Aug 2025 09:25:02 +0000 Subject: [PATCH 11/35] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20Checklist=20and=20b?= =?UTF-8?q?asic=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../checklist.md | 9 + .../test_validate_bal.py | 632 ++++++------------ 2 files changed, 215 insertions(+), 426 deletions(-) create mode 100644 tests/unscheduled/eip7928_block_level_access_lists/checklist.md diff --git a/tests/unscheduled/eip7928_block_level_access_lists/checklist.md b/tests/unscheduled/eip7928_block_level_access_lists/checklist.md new file mode 100644 index 00000000000..e48e66fd62e --- /dev/null +++ b/tests/unscheduled/eip7928_block_level_access_lists/checklist.md @@ -0,0 +1,9 @@ +# EIP-7928 Block Access Lists (BAL) Test Checklist + +| Function Name | Goal | Setup | Expectation | Status | +|---------------|------|-------|-------------|--------| +| `test_bal_nonce_changes` | Ensure BAL captures changes to nonce | Alice sends 100 wei to Bob | BAL MUST include changes to Alice's nonce. | ✅ Completed | +| `test_bal_balance_changes` | Ensure BAL captures changes to balance | Alice sends 100 wei to Bob | BAL MUST include balance change for Alice, Bob, and Coinbase | ✅ Completed | +| `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | ✅ Completed | +| `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | ✅ Completed | +| `test_bal_code_changes` | Ensure BAL captures changes to account code | Alice deploys factory contract that creates new contract | BAL MUST include code changes for newly deployed contract | ✅ Completed | diff --git a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py index ce3f58f3aad..aadf3add75b 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py +++ b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py @@ -1,7 +1,6 @@ """Tests for validating EIP-7928: Block-level Access Lists (BAL).""" import pytest -from pokebal.bal.types import BlockAccessList from ethereum_test_tools import ( Account, @@ -18,440 +17,221 @@ @pytest.mark.valid_from(ACTIVATION_FORK_NAME) -class TestBALValidity: - """Test BAL validity and data structure integrity.""" - - def test_bal_hash_basic_transaction( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL hash generation for basic ETH transfer.""" - # Setup accounts for basic ETH transfer - - # TODO: Populate BAL. - bal = BlockAccessList() - - transfer_amount = 1000 - sender = pre.fund_eoa() - recipient = pre.fund_eoa(amount=0) - - # Create a basic ETH transfer transaction - tx = Transaction( - sender=sender, - to=recipient, - value=1000, - ) - - # Create block with custom header that includes BAL hash - block = Block(txs=[tx], block_access_lists=bal.serialize()) - - # Execute the blockchain test - blockchain_test( - pre=pre, - blocks=[block], - post={ - sender: Account( - nonce=1, - ), - recipient: Account(balance=transfer_amount), - }, - ) - - # Note: In the generated fixture, the block header will include: - # - bal_hash: the computed hash of the Block Access List - # - bal_data: the SSZ-encoded Block Access List data (when framework supports it) - - def test_bal_hash_storage_operations( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL hash generation for storage read/write operations.""" - # TODO: Implement BAL hash validation for storage operations - pass - - def test_bal_hash_balance_changes( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL hash generation for balance changes.""" - # TODO: Implement BAL hash validation for balance changes - pass - - def test_bal_hash_nonce_changes( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL hash generation for nonce changes.""" - # TODO: Implement BAL hash validation for nonce changes - pass - - def test_bal_hash_code_changes( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL hash generation for contract code changes (CREATE/CREATE2).""" - # TODO: Implement BAL hash validation for code changes - pass - - def test_bal_ordering_requirements( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test that BAL entries follow strict address and storage key ordering.""" - # TODO: Implement ordering validation tests - pass - - def test_bal_completeness_validation( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test that BAL includes all accessed accounts and storage slots.""" - # TODO: Implement completeness validation tests - pass - - def test_bal_empty_block( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL hash generation for empty blocks (only coinbase access).""" - # TODO: Implement empty block BAL validation - pass - - def test_bal_multiple_transactions( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL hash generation for blocks with multiple transactions.""" - # TODO: Implement multiple transaction BAL validation - pass - - def test_bal_failed_transaction_inclusion( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test that failed transactions still contribute to BAL.""" - # TODO: Implement failed transaction BAL inclusion tests - pass +def test_bal_nonce_changes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures changes to nonce.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=alice, + to=bob, + value=100, + ) + + block = Block(txs=[tx]) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=100), + }, + block_access_list={ + "account_changes": [ + { + "address": alice, + "nonce_changes": [{"tx_index": 0, "post_nonce": 1}], + }, + ] + }, + ) @pytest.mark.valid_from(ACTIVATION_FORK_NAME) -class TestBALEncoding: - """Test SSZ encoding/decoding of BAL data structures.""" - - def test_ssz_encoding_storage_changes( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test SSZ round-trip encoding for storage changes.""" - # TODO: Implement SSZ encoding tests for storage changes - pass - - def test_ssz_encoding_balance_changes( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test SSZ round-trip encoding for balance changes.""" - # TODO: Implement SSZ encoding tests for balance changes - pass - - def test_ssz_encoding_nonce_changes( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test SSZ round-trip encoding for nonce changes.""" - # TODO: Implement SSZ encoding tests for nonce changes - pass - - def test_ssz_encoding_code_changes( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test SSZ round-trip encoding for code changes.""" - # TODO: Implement SSZ encoding tests for code changes - pass - - def test_ssz_encoding_full_bal( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test SSZ round-trip encoding for complete BAL structure.""" - # TODO: Implement full BAL SSZ encoding tests - pass +def test_bal_balance_changes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures changes to balance.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=alice, + to=bob, + value=100, + ) + + block = Block(txs=[tx]) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=100), + }, + block_access_list={ + "account_changes": [ + { + "address": alice, + "balance_changes": [ + { + "tx_index": 0, + "post_balance": pre[alice].balance - 100, + } + ], + }, + { + "address": bob, + "balance_changes": [{"tx_index": 0, "post_balance": 100}], + }, + # TODO: Validate coinbase + ] + }, + ) @pytest.mark.valid_from(ACTIVATION_FORK_NAME) -class TestBALEdgeCases: - """Test edge cases and error conditions for BAL.""" - - def test_bal_large_storage_operations( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL with large numbers of storage operations.""" - # TODO: Implement large storage operation tests - pass - - def test_bal_contract_selfdestruct( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL handling of contract self-destruct operations.""" - # TODO: Implement self-destruct BAL tests - pass - - def test_bal_create2_deterministic_addresses( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL with CREATE2 deterministic contract addresses.""" - # TODO: Implement CREATE2 BAL tests - pass - - def test_bal_zero_value_changes( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL handling of zero-value state changes.""" - # TODO: Implement zero-value change tests - pass - - def test_bal_maximum_block_size( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL with maximum possible block size and access patterns.""" - # TODO: Implement maximum block size BAL tests - pass +def test_bal_storage_writes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures storage writes.""" + from ethereum_test_tools.vm.opcode import Opcodes as Op + + # Alice calls contract that writes to storage slot 0x01 + storage_contract = pre.deploy_contract(code=Op.SSTORE(0x01, 0x42) + Op.STOP) + alice = pre.fund_eoa() + + tx = Transaction( + sender=alice, + to=storage_contract, + gas_limit=100000, + ) + + block = Block(txs=[tx]) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + storage_contract: Account(storage={0x01: 0x42}), + }, + block_access_list={ + "account_changes": [ + { + "address": storage_contract, + "storage_changes": [ + { + "slot": 0x01, + "slot_changes": [ + { + "tx_index": 0, + "post_value": 0x42, + } + ], + } + ], + }, + ] + }, + ) @pytest.mark.valid_from(ACTIVATION_FORK_NAME) -class TestBALValidationFailures: - """Test validation failure scenarios for malformed or incorrect BALs.""" - - def test_invalid_bal_hash_rejection( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test that blocks with invalid BAL hashes are rejected.""" - # TODO: Implement invalid BAL hash rejection tests - pass - - def test_incomplete_bal_rejection( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test that blocks with incomplete BALs are rejected.""" - # TODO: Implement incomplete BAL rejection tests - pass - - def test_incorrect_ordering_rejection( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test that blocks with incorrectly ordered BAL entries are rejected.""" - # TODO: Implement incorrect ordering rejection tests - pass - - def test_malformed_ssz_rejection( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test that blocks with malformed SSZ-encoded BALs are rejected.""" - # TODO: Implement malformed SSZ rejection tests - pass +def test_bal_storage_reads( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures storage reads.""" + from ethereum_test_tools.vm.opcode import Opcodes as Op + + storage_contract = pre.deploy_contract( + code=Op.SLOAD(0x01) + Op.STOP, + storage={0x01: 0x42}, # Pre-populate storage + ) + alice = pre.fund_eoa() + + tx = Transaction( + sender=alice, + to=storage_contract, + gas_limit=100000, + ) + + block = Block(txs=[tx]) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + storage_contract: Account(storage={0x01: 0x42}), + }, + block_access_list={ + "account_changes": [ + { + "address": storage_contract, + "storage_reads": [0x01], + }, + ] + }, + ) @pytest.mark.valid_from(ACTIVATION_FORK_NAME) -class TestBALLimits: - """Test EIP-7928 specification limits and boundaries.""" - - def test_max_transactions_limit( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with MAX_TXS transactions per block.""" - # TODO: Test with Spec.MAX_TXS (30,000) transactions - pass - - def test_max_accounts_limit( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with MAX_ACCOUNTS accessed accounts.""" - # TODO: Test with Spec.MAX_ACCOUNTS (300,000) accessed accounts - pass - - def test_max_storage_slots_limit( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with MAX_SLOTS storage slots accessed.""" - # TODO: Test with Spec.MAX_SLOTS (300,000) storage slots - pass - - def test_max_code_size_limit( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with MAX_CODE_SIZE contract deployments.""" - # TODO: Test with Spec.MAX_CODE_SIZE (24,576 bytes) contract code - pass - - def test_max_tx_index_boundary( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation at MAX_TX_INDEX boundary.""" - # TODO: Test with transaction index at Spec.MAX_TX_INDEX (65,535) - pass - - def test_max_balance_boundary( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with MAX_BALANCE values.""" - # TODO: Test with balance at Spec.MAX_BALANCE (2^128 - 1) - pass - - def test_max_nonce_boundary( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with MAX_NONCE values.""" - # TODO: Test with nonce at Spec.MAX_NONCE (2^64 - 1) - pass - - def test_target_max_gas_limit_boundary( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation at TARGET_MAX_GAS_LIMIT.""" - # TODO: Test with block gas limit at Spec.TARGET_MAX_GAS_LIMIT (600,000,000) - pass - - def test_address_size_validation( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with ADDRESS_SIZE requirements.""" - # TODO: Test address size validation (Spec.ADDRESS_SIZE = 20 bytes) - pass - - def test_storage_key_size_validation( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with STORAGE_KEY_SIZE requirements.""" - # TODO: Test storage key size validation (Spec.STORAGE_KEY_SIZE = 32 bytes) - pass - - def test_storage_value_size_validation( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with STORAGE_VALUE_SIZE requirements.""" - # TODO: Test storage value size validation (Spec.STORAGE_VALUE_SIZE = 32 bytes) - pass - - def test_hash_size_validation( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with HASH_SIZE requirements.""" - # TODO: Test BAL hash size validation (Spec.HASH_SIZE = 32 bytes) - pass - - -@pytest.mark.valid_from(ACTIVATION_FORK_NAME) -class TestBALBoundaryConditions: - """Test boundary conditions and edge cases for EIP-7928 limits.""" - - def test_exceed_max_transactions( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL rejection when exceeding MAX_TXS.""" - # TODO: Test rejection with Spec.MAX_TXS + 1 transactions - pass - - def test_exceed_max_accounts( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL rejection when exceeding MAX_ACCOUNTS.""" - # TODO: Test rejection with Spec.MAX_ACCOUNTS + 1 accounts - pass - - def test_exceed_max_storage_slots( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL rejection when exceeding MAX_SLOTS.""" - # TODO: Test rejection with Spec.MAX_SLOTS + 1 storage slots - pass - - def test_exceed_max_code_size( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL rejection when exceeding MAX_CODE_SIZE.""" - # TODO: Test rejection with Spec.MAX_CODE_SIZE + 1 byte contract - pass - - def test_zero_values_at_boundaries( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with zero values at type boundaries.""" - # TODO: Test with zero balances, nonces, and transaction indices - pass - - def test_minimum_valid_values( - self, - pre: Alloc, - blockchain_test: BlockchainTestFiller, - ): - """Test BAL validation with minimum valid values.""" - # TODO: Test with minimum valid balances, nonces, and indices - pass +def test_bal_code_changes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures changes to account code.""" + from ethereum_test_tools.vm.opcode import Opcodes as Op + + deployed_code = Op.PUSH1(0x42) + Op.PUSH1(0x00) + Op.SSTORE + Op.STOP + + factory_code = ( + Op.PUSH32(deployed_code) # Contract code + + Op.PUSH1(0x00) # Memory offset + + Op.MSTORE # Store code in memory + + Op.PUSH1(len(deployed_code)) # Code size + + Op.PUSH1(0x00) # Memory offset + + Op.PUSH1(0x00) # Value to send + + Op.CREATE # CREATE opcode + + Op.STOP + ) + + factory_contract = pre.deploy_contract(code=factory_code) + alice = pre.fund_eoa() + + tx = Transaction( + sender=alice, + to=factory_contract, + gas_limit=200000, + ) + + block = Block(txs=[tx]) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + }, + block_access_list={ + "account_changes": [ + { + "address": alice, + "nonce_changes": [{"tx_index": 0, "post_nonce": 1}], + }, + { + "address": factory_contract, + "code_changes": [{"tx_index": 0, "new_code": deployed_code}], + }, + ] + }, + ) From b0c8bd2f4b696dd5cd7e14d82af3fe8f00e147f5 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 12 Aug 2025 10:22:12 +0000 Subject: [PATCH 12/35] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../checklist.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/unscheduled/eip7928_block_level_access_lists/checklist.md b/tests/unscheduled/eip7928_block_level_access_lists/checklist.md index e48e66fd62e..ba53d4f65d5 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/checklist.md +++ b/tests/unscheduled/eip7928_block_level_access_lists/checklist.md @@ -1,9 +1,11 @@ # EIP-7928 Block Access Lists (BAL) Test Checklist -| Function Name | Goal | Setup | Expectation | Status | -|---------------|------|-------|-------------|--------| -| `test_bal_nonce_changes` | Ensure BAL captures changes to nonce | Alice sends 100 wei to Bob | BAL MUST include changes to Alice's nonce. | ✅ Completed | -| `test_bal_balance_changes` | Ensure BAL captures changes to balance | Alice sends 100 wei to Bob | BAL MUST include balance change for Alice, Bob, and Coinbase | ✅ Completed | -| `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | ✅ Completed | -| `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | ✅ Completed | -| `test_bal_code_changes` | Ensure BAL captures changes to account code | Alice deploys factory contract that creates new contract | BAL MUST include code changes for newly deployed contract | ✅ Completed | +| Function Name | Goal | Setup | Expectation | Scope | Status | +|---------------|------|-------|-------------|-------|--------| +| `test_bal_nonce_changes` | Ensure BAL captures changes to nonce | Alice sends 100 wei to Bob | BAL MUST include changes to Alice's nonce. | tx | ✅ Completed | +| `test_bal_balance_changes` | Ensure BAL captures changes to balance | Alice sends 100 wei to Bob | BAL MUST include balance change for Alice, Bob, and Coinbase | tx | ✅ Completed | +| `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | tx | ✅ Completed | +| `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | tx | ✅ Completed | +| `test_bal_code_changes` | Ensure BAL captures changes to account code | Alice deploys factory contract that creates new contract | BAL MUST include code changes for newly deployed contract | tx | ✅ Completed | + +> ℹ️ Scope describes whether a test spans a single transaction (`tx`) or entire block (`blk`). From 06464aff7ef34db1ad319de7fa008eb381ebdbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= <51536394+nerolation@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:24:01 +0200 Subject: [PATCH 13/35] Add more sophisticated test cases (#2029) Co-authored-by: raxhvl <10168946+raxhvl@users.noreply.github.com> --- .../checklist.md | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/unscheduled/eip7928_block_level_access_lists/checklist.md b/tests/unscheduled/eip7928_block_level_access_lists/checklist.md index ba53d4f65d5..3c371be939a 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/checklist.md +++ b/tests/unscheduled/eip7928_block_level_access_lists/checklist.md @@ -1,11 +1,18 @@ # EIP-7928 Block Access Lists (BAL) Test Checklist -| Function Name | Goal | Setup | Expectation | Scope | Status | -|---------------|------|-------|-------------|-------|--------| -| `test_bal_nonce_changes` | Ensure BAL captures changes to nonce | Alice sends 100 wei to Bob | BAL MUST include changes to Alice's nonce. | tx | ✅ Completed | -| `test_bal_balance_changes` | Ensure BAL captures changes to balance | Alice sends 100 wei to Bob | BAL MUST include balance change for Alice, Bob, and Coinbase | tx | ✅ Completed | -| `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | tx | ✅ Completed | -| `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | tx | ✅ Completed | -| `test_bal_code_changes` | Ensure BAL captures changes to account code | Alice deploys factory contract that creates new contract | BAL MUST include code changes for newly deployed contract | tx | ✅ Completed | +| Function Name | Goal | Setup | Expectation | Status | +|---------------|------|-------|-------------|--------| +| `test_bal_nonce_changes` | Ensure BAL captures changes to nonce | Alice sends 100 wei to Bob | BAL MUST include changes to Alice's nonce. | ✅ Completed | +| `test_bal_balance_changes` | Ensure BAL captures changes to balance | Alice sends 100 wei to Bob | BAL MUST include balance change for Alice, Bob, and Coinbase | ✅ Completed | +| `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | ✅ Completed | +| `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | ✅ Completed | +| `test_bal_code_changes` | Ensure BAL captures changes to account code | Alice deploys factory contract that creates new contract | BAL MUST include code changes for newly deployed contract | ✅ Completed | +| `test_bal_2930_slot_listed_but_untouched` | Ensure 2930 access list alone doesn’t appear in BAL | Include `(KV, S=0x01)` in tx’s EIP-2930 access list; tx executes code that does **no** `SLOAD`/`SSTORE` to `S` (e.g., pure arithmetic/log). | BAL **MUST NOT** contain any entry for `(KV, S)` — neither reads nor writes — because the slot wasn’t touched. | 🟡 Planned | +| `test_bal_2930_slot_listed_and_modified` | Ensure BAL records writes only because the slot is touched | Same access list as above, but tx executes `SSTORE` to `S`. | BAL **MUST** include `storage_changes` for `(KV, S)` (and no separate read record for that slot if implementation deduplicates). Presence in the access list is irrelevant; inclusion is due to the actual write. | 🟡 Planned | +| `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_self_transfer` | BAL handles self-transfers correctly | Alice sends `1 ETH` to **Alice** | BAL MUST include **one** entry for Alice with `balance_changes` reflecting **gas only** (value cancels out) and a nonce change; Coinbase balance updated for fees; no separate recipient row. | 🟡 Planned | +| `test_bal_system_contracts_2935_4788` | BAL includes pre-exec system writes for parent hash & beacon root | Build a block with `N` normal txs; 2935 & 4788 active | BAL MUST include `HISTORY_STORAGE_ADDRESS` (EIP-2935) and `BEACON_ROOTS_ADDRESS` (EIP-4788) with `storage_changes` to ring-buffer slots; each write uses `tx_index = N` (system op). | 🟡 Planned | +| `test_bal_system_dequeue_withdrawals_eip7002` | BAL tracks post-exec system dequeues for withdrawals | Pre-populate EIP-7002 withdrawal requests; produce a block where dequeues occur | BAL MUST include the 7002 system contract with `storage_changes` (queue head/tail slots 0–3) using `tx_index = len(txs)` and balance changes for withdrawal recipients. | 🟡 Planned | +| `test_bal_system_dequeue_consolidations_eip7251` | BAL tracks post-exec system dequeues for consolidations | Pre-populate EIP-7251 consolidation requests; produce a block where dequeues occur | BAL MUST include the 7251 system contract with `storage_changes` (queue slots 0–3) using `tx_index = len(txs)`. | 🟡 Planned | > ℹ️ Scope describes whether a test spans a single transaction (`tx`) or entire block (`blk`). From d583ef8c2b1ac2a55e5cd014c0765ce354639fdd Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 19 Aug 2025 20:59:06 -0600 Subject: [PATCH 14/35] WIP: Attempt to get EEST + EELS bals PRs talking to each other --- pyproject.toml | 2 - src/ethereum_clis/types.py | 1 + src/ethereum_test_fixtures/blockchain.py | 3 - src/ethereum_test_specs/blockchain.py | 61 ++- src/ethereum_test_tools/__init__.py | 14 + src/ethereum_test_types/__init__.py | 16 + src/ethereum_test_types/block_access_list.py | 282 +++++++++++ .../__init__.py | 0 .../checklist.md | 0 .../eip7928_block_level_access_lists/spec.py | 4 +- .../test_block_access_lists.py | 265 ++++++++++ .../test_validate_bal.py | 237 --------- uv.lock | 468 ++++++++++-------- 13 files changed, 884 insertions(+), 469 deletions(-) create mode 100644 src/ethereum_test_types/block_access_list.py rename tests/{unscheduled => osaka}/eip7928_block_level_access_lists/__init__.py (100%) rename tests/{unscheduled => osaka}/eip7928_block_level_access_lists/checklist.md (100%) rename tests/{unscheduled => osaka}/eip7928_block_level_access_lists/spec.py (93%) create mode 100644 tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py delete mode 100644 tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py diff --git a/pyproject.toml b/pyproject.toml index 6228e50fa5f..48ba580d04a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ dependencies = [ "eth-abi>=5.2.0", "joblib>=1.4.2", "ckzg>=2.1.1", - "ethereum-execution", ] [project.urls] @@ -176,4 +175,3 @@ required-version = ">=0.7.0" [tool.uv.sources] ethereum-spec-evm-resolver = { git = "https://github.com/spencer-tb/ethereum-spec-evm-resolver", rev = "aec6a628b8d0f1c791a8378c5417a089566135ac" } -ethereum-execution = { git = "https://github.com/nerolation/execution-specs", rev = "bals" } diff --git a/src/ethereum_clis/types.py b/src/ethereum_clis/types.py index a30e55aed4f..957e38b7f11 100644 --- a/src/ethereum_clis/types.py +++ b/src/ethereum_clis/types.py @@ -57,6 +57,7 @@ class Result(CamelModel): blob_gas_used: HexNumber | None = None requests_hash: Hash | None = None requests: List[Bytes] | None = None + block_access_list: Bytes | None = Field(None, alias="blockAccessList") block_exception: Annotated[ BlockExceptionWithMessage | UndefinedException | None, ExceptionMapperValidator ] = None diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index 2928f05f5ab..db7f4c81d08 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -18,8 +18,6 @@ import ethereum_rlp as eth_rlp import pytest - -from ethereum.osaka.ssz_types import BlockAccessList from ethereum_types.numeric import Uint from pydantic import AliasChoices, Field, PlainSerializer, computed_field, model_validator @@ -236,7 +234,6 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> "FixtureHead "requests_hash": Requests() if fork.header_requests_required(0, 0) else None, "bal_hash": Hash(0) if fork.header_bal_hash_required(0, 0) else None, "fork": fork, - } return FixtureHeader(**environment_values, **extras) diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 2402732226f..f735591c192 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -241,7 +241,7 @@ class Block(Header): """ Post state for verification after block execution in BlockchainTest """ - block_access_lists: Bytes | None = Field(None) + block_access_list: Bytes | None = Field(None) """ EIP-7928: Block-level access lists (serialized). """ @@ -274,14 +274,14 @@ def set_environment(self, env: Environment) -> Environment: new_env_values["blob_gas_used"] = self.blob_gas_used if not isinstance(self.parent_beacon_block_root, Removable): new_env_values["parent_beacon_block_root"] = self.parent_beacon_block_root - if not isinstance(self.requests_hash, Removable) and self.block_access_lists is not None: - new_env_values["bal_hash"] = self.block_access_lists.keccak256() - new_env_values["block_access_lists"] = self.block_access_lists + if not isinstance(self.requests_hash, Removable) and self.block_access_list is not None: + new_env_values["bal_hash"] = self.block_access_list.keccak256() + new_env_values["block_access_list"] = self.block_access_list if ( - not isinstance(self.block_access_lists, Removable) - and self.block_access_lists is not None + not isinstance(self.block_access_list, Removable) + and self.block_access_list is not None ): - new_env_values["block_access_lists"] = self.block_access_lists + new_env_values["block_access_list"] = self.block_access_list """ These values are required, but they depend on the previous environment, so they can be calculated here. @@ -321,7 +321,7 @@ class BuiltBlock(CamelModel): expected_exception: BLOCK_EXCEPTION_TYPE = None engine_api_error_code: EngineAPIError | None = None fork: Fork - block_access_lists: Bytes + block_access_list: Bytes def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock: """Get a FixtureBlockBase from the built block.""" @@ -333,7 +333,7 @@ def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock: if self.withdrawals is not None else None ), - block_access_lists=self.block_access_lists, + block_access_list=self.block_access_list, fork=self.fork, ).with_rlp(txs=self.txs) @@ -425,6 +425,12 @@ class BlockchainTest(BaseTest): Exclude the post state from the fixture output. In this case, the state verification is only performed based on the state root. """ + expected_block_access_list: Any | None = None # Will be BAL type + """ + Expected block access list for verification. + If set, verifies that the block access list returned by the client matches expectations. + Use the BAL builder API to construct expectations. + """ supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [ BlockchainFixture, @@ -618,7 +624,7 @@ def generate_block_data( expected_exception=block.exception, engine_api_error_code=block.engine_api_error_code, fork=fork, - block_access_lists=block.block_access_lists, + block_access_list=transition_tool_output.result.block_access_list or Bytes(b""), ) try: @@ -672,6 +678,34 @@ def verify_post_state(self, t8n, t8n_state: Alloc, expected_state: Alloc | None print_traces(t8n.get_traces()) raise e + def verify_block_access_list(self, actual_bal: Bytes | None, expected_bal: Any | None): + """ + Verify that the actual block access list matches expectations. + + Args: + actual_bal: The block access list returned by the client (RLP-encoded) + expected_bal: The expected BlockAccessList object + + """ + if expected_bal is None: + return + + from ethereum_test_types.block_access_list import BlockAccessList + + if not isinstance(expected_bal, BlockAccessList): + raise Exception( + f"Invalid expected_bal type: {type(expected_bal)}. Use BlockAccessList." + ) + + if actual_bal is None or actual_bal == Bytes(b""): + raise Exception("Expected block access list but got none or empty.") + + # Verify using the BlockAccessList's verification method + try: + expected_bal.verify_against(actual_bal) + except Exception as e: + raise Exception("Block access list verification failed.") from e + def make_fixture( self, t8n: TransitionTool, @@ -699,6 +733,13 @@ def make_fixture( last_block=i == len(self.blocks) - 1, ) fixture_blocks.append(built_block.get_fixture_block()) + + # Verify block access list if expected + if self.expected_block_access_list is not None: + self.verify_block_access_list( + built_block.block_access_list, self.expected_block_access_list + ) + if block.exception is None: # Update env, alloc and last block hash for the next block. alloc = built_block.alloc diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 049db8c8579..18fa9543ec6 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -43,7 +43,14 @@ EOA, Alloc, AuthorizationTuple, + BalAccountChange, + BalBalanceChange, + BalCodeChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, Blob, + BlockAccessList, ChainConfig, ConsolidationRequest, DepositRequest, @@ -97,9 +104,16 @@ "Address", "Alloc", "AuthorizationTuple", + "BalAccountChange", + "BalBalanceChange", + "BalCodeChange", + "BalNonceChange", + "BalStorageChange", + "BalStorageSlot", "BaseFixture", "BaseTest", "Blob", + "BlockAccessList", "BlobsTest", "BlobsTestFiller", "Block", diff --git a/src/ethereum_test_types/__init__.py b/src/ethereum_test_types/__init__.py index fde9e1b75ec..7a0248bd68d 100644 --- a/src/ethereum_test_types/__init__.py +++ b/src/ethereum_test_types/__init__.py @@ -2,6 +2,15 @@ from .account_types import EOA, Alloc from .blob_types import Blob +from .block_access_list import ( + BalAccountChange, + BalBalanceChange, + BalCodeChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessList, +) from .block_types import ( Environment, EnvironmentDefaults, @@ -36,7 +45,14 @@ __all__ = ( "Alloc", "AuthorizationTuple", + "BalAccountChange", + "BalBalanceChange", + "BalCodeChange", + "BalNonceChange", + "BalStorageChange", + "BalStorageSlot", "Blob", + "BlockAccessList", "ChainConfig", "ChainConfigDefaults", "ConsolidationRequest", diff --git a/src/ethereum_test_types/block_access_list.py b/src/ethereum_test_types/block_access_list.py new file mode 100644 index 00000000000..d19fbbb4523 --- /dev/null +++ b/src/ethereum_test_types/block_access_list.py @@ -0,0 +1,282 @@ +""" +Block Access List (BAL) models for EIP-7928. + +Following the established pattern in the codebase (AccessList, AuthorizationTuple), +these are simple data classes that can be composed together. +""" + +from typing import Any, Dict, List, Optional + +import rlp +from pydantic import Field + +from ethereum_test_base_types import Address, Bytes, CamelModel, HexNumber +from ethereum_test_base_types.conversions import BytesConvertible, to_bytes + + +class BalNonceChange(CamelModel): + """Represents a nonce change in the block access list.""" + + tx_index: int = Field(..., description="Transaction index where the change occurred") + post_nonce: int = Field(..., description="Nonce value after the transaction") + + +class BalBalanceChange(CamelModel): + """Represents a balance change in the block access list.""" + + tx_index: int = Field(..., description="Transaction index where the change occurred") + post_balance: HexNumber = Field(..., description="Balance after the transaction") + + +class BalCodeChange(CamelModel): + """Represents a code change in the block access list.""" + + tx_index: int = Field(..., description="Transaction index where the change occurred") + new_code: Bytes = Field(..., description="New code bytes") + + +class BalStorageChange(CamelModel): + """Represents a change to a specific storage slot.""" + + tx_index: int = Field(..., description="Transaction index where the change occurred") + post_value: HexNumber = Field(..., description="Value after the transaction") + + +class BalStorageSlot(CamelModel): + """Represents all changes to a specific storage slot.""" + + slot: HexNumber = Field(..., description="Storage slot key") + slot_changes: List[BalStorageChange] = Field( + default_factory=list, description="List of changes to this slot" + ) + + +class BalAccountChange(CamelModel): + """Represents all changes to a specific account in a block.""" + + address: Address = Field(..., description="Account address") + nonce_changes: Optional[List[BalNonceChange]] = Field( + None, description="List of nonce changes" + ) + balance_changes: Optional[List[BalBalanceChange]] = Field( + None, description="List of balance changes" + ) + code_changes: Optional[List[BalCodeChange]] = Field(None, description="List of code changes") + storage_changes: Optional[List[BalStorageSlot]] = Field( + None, description="List of storage changes" + ) + storage_reads: Optional[List[HexNumber]] = Field( + None, description="List of storage slots that were read" + ) + + +class BlockAccessList(CamelModel): + """ + Expected Block Access List for verification. + + This follows the same pattern as AccessList and AuthorizationTuple - + a simple data class that can be used directly in tests. + + Example: + expected_block_access_list = BlockAccessList( + account_changes=[ + BalAccountChange( + address=alice, + nonce_changes=[ + BalNonceChange(tx_index=0, post_nonce=1) + ], + balance_changes=[ + BalBalanceChange(tx_index=0, post_balance=9000) + ] + ), + BalAccountChange( + address=bob, + balance_changes=[ + BalBalanceChange(tx_index=0, post_balance=100) + ] + ), + ] + ) + + """ + + account_changes: List[BalAccountChange] = Field( + default_factory=list, description="List of account changes in the block" + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for serialization.""" + return self.model_dump(exclude_none=True) + + def verify_against(self, actual_rlp: BytesConvertible) -> None: + """ + Verify that the actual BAL from the client matches this expected BAL. + + Args: + actual_rlp: RLP-encoded BAL from the client + + Raises: + Exception: If verification fails + + """ + # Decode the actual BAL + actual_bal = self._decode_rlp(actual_rlp) + + # Verify all expected accounts are present + for expected_account in self.account_changes: + actual_account = self._find_account(actual_bal, expected_account.address) + if actual_account is None: + raise Exception( + f"Expected account {expected_account.address} not found in actual BAL" + ) + + # Verify nonce changes + if expected_account.nonce_changes: + self._verify_nonce_changes( + expected_account.address, + expected_account.nonce_changes, + actual_account.get("nonce_changes", []), + ) + + # Verify balance changes + if expected_account.balance_changes: + self._verify_balance_changes( + expected_account.address, + expected_account.balance_changes, + actual_account.get("balance_changes", []), + ) + + # Similar for other change types... + + def _decode_rlp(self, rlp_data: BytesConvertible) -> Dict[str, Any]: + """Decode RLP data to dictionary.""" + decoded = rlp.decode(to_bytes(rlp_data)) + + account_changes = [] + for account_entry in decoded: + if len(account_entry) < 2: + continue + + address = Address(account_entry[0]) + account_data = { + "address": address, + "storage_changes": account_entry[1] if len(account_entry) > 1 else [], + "storage_reads": account_entry[2] if len(account_entry) > 2 else [], + "balance_changes": account_entry[3] if len(account_entry) > 3 else [], + "nonce_changes": account_entry[4] if len(account_entry) > 4 else [], + "code_changes": account_entry[5] if len(account_entry) > 5 else [], + } + account_changes.append(account_data) + + return {"account_changes": account_changes} + + def _find_account(self, bal: Dict[str, Any], address: Address) -> Optional[Dict[str, Any]]: + """Find an account in the decoded BAL.""" + # Convert address to bytes for comparison + address_bytes = bytes(address) if hasattr(address, "__bytes__") else address + + for account in bal.get("account_changes", []): + account_addr = account.get("address") + # Convert to bytes if needed for comparison + if isinstance(account_addr, Address): + account_addr = bytes(account_addr) + if account_addr == address_bytes: + return account + return None + + def _verify_nonce_changes( + self, address: Address, expected: List[BalNonceChange], actual: List[Any] + ) -> None: + """Verify nonce changes match.""" + # Decode actual changes for better error reporting + actual_decoded = [] + for act_change in actual: + if len(act_change) >= 2: + tx_index = ( + int.from_bytes(act_change[0], "big") + if isinstance(act_change[0], bytes) and act_change[0] + else (0 if isinstance(act_change[0], bytes) else act_change[0]) + ) + post_nonce = ( + int.from_bytes(act_change[1], "big") + if isinstance(act_change[1], bytes) and act_change[1] + else (0 if isinstance(act_change[1], bytes) else act_change[1]) + ) + actual_decoded.append(f"tx_index={tx_index} post_nonce={post_nonce}") + + for exp_change in expected: + found = False + for act_change in actual: + # Each nonce change is [tx_index, post_nonce] + if len(act_change) >= 2: + # Convert bytes to int if needed + tx_index = ( + int.from_bytes(act_change[0], "big") + if isinstance(act_change[0], bytes) and act_change[0] + else (0 if isinstance(act_change[0], bytes) else act_change[0]) + ) + post_nonce = ( + int.from_bytes(act_change[1], "big") + if isinstance(act_change[1], bytes) and act_change[1] + else (0 if isinstance(act_change[1], bytes) else act_change[1]) + ) + if tx_index == exp_change.tx_index and post_nonce == exp_change.post_nonce: + found = True + break + if not found: + raise Exception( + f"Account {address}: Nonce change mismatch\n" + f" Expected: tx_index={exp_change.tx_index} " + f"post_nonce={exp_change.post_nonce}\n" + f" Actual nonce changes: {actual_decoded}" + ) + + def _verify_balance_changes( + self, address: Address, expected: List[BalBalanceChange], actual: List[Any] + ) -> None: + """Verify balance changes match.""" + # Decode actual changes for better error reporting + actual_decoded = [] + for act_change in actual: + if len(act_change) >= 2: + tx_index = ( + int.from_bytes(act_change[0], "big") + if isinstance(act_change[0], bytes) and act_change[0] + else (0 if isinstance(act_change[0], bytes) else act_change[0]) + ) + post_balance = ( + int.from_bytes(act_change[1], "big") + if isinstance(act_change[1], bytes) and act_change[1] + else (0 if isinstance(act_change[1], bytes) else int(act_change[1])) + ) + actual_decoded.append(f"tx_index={tx_index} post_balance={post_balance}") + + for exp_change in expected: + found = False + for act_change in actual: + # Each balance change is [tx_index, post_balance] + if len(act_change) >= 2: + # Convert bytes to int if needed, handling empty bytes + tx_index = ( + int.from_bytes(act_change[0], "big") + if isinstance(act_change[0], bytes) and act_change[0] + else (0 if isinstance(act_change[0], bytes) else act_change[0]) + ) + # Balance is stored as bytes/int, need to compare properly + post_balance = ( + int.from_bytes(act_change[1], "big") + if isinstance(act_change[1], bytes) and act_change[1] + else (0 if isinstance(act_change[1], bytes) else int(act_change[1])) + ) + if tx_index == exp_change.tx_index and post_balance == int( + exp_change.post_balance + ): + found = True + break + if not found: + raise Exception( + f"Account {address}: Balance change mismatch\n" + f" Expected: tx_index={exp_change.tx_index} " + f"post_balance={exp_change.post_balance}\n" + f" Actual balance changes: {actual_decoded}" + ) diff --git a/tests/unscheduled/eip7928_block_level_access_lists/__init__.py b/tests/osaka/eip7928_block_level_access_lists/__init__.py similarity index 100% rename from tests/unscheduled/eip7928_block_level_access_lists/__init__.py rename to tests/osaka/eip7928_block_level_access_lists/__init__.py diff --git a/tests/unscheduled/eip7928_block_level_access_lists/checklist.md b/tests/osaka/eip7928_block_level_access_lists/checklist.md similarity index 100% rename from tests/unscheduled/eip7928_block_level_access_lists/checklist.md rename to tests/osaka/eip7928_block_level_access_lists/checklist.md diff --git a/tests/unscheduled/eip7928_block_level_access_lists/spec.py b/tests/osaka/eip7928_block_level_access_lists/spec.py similarity index 93% rename from tests/unscheduled/eip7928_block_level_access_lists/spec.py rename to tests/osaka/eip7928_block_level_access_lists/spec.py index 7d09ec8c781..a91b00f8a22 100644 --- a/tests/unscheduled/eip7928_block_level_access_lists/spec.py +++ b/tests/osaka/eip7928_block_level_access_lists/spec.py @@ -2,8 +2,8 @@ from dataclasses import dataclass -ACTIVATION_FORK_NAME = "BlockAccessLists" -"""The fork name for EIP-7928 activation.""" +# ACTIVATION_FORK_NAME = "BlockAccessLists" +# """The fork name for EIP-7928 activation.""" @dataclass(frozen=True) diff --git a/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py new file mode 100644 index 00000000000..a156ba3772a --- /dev/null +++ b/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py @@ -0,0 +1,265 @@ +"""Tests for EIP-7928 using the consistent data class pattern.""" + +import pytest + +from ethereum_test_tools import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Transaction, +) +from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_types.block_access_list import ( + BalAccountChange, + BalBalanceChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessList, +) + +from .spec import ref_spec_7928 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path +REFERENCE_SPEC_VERSION = ref_spec_7928.version + + +@pytest.mark.valid_from("Osaka") +def test_bal_nonce_changes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures changes to nonce.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=alice, + to=bob, + value=100, + ) + + block = Block(txs=[tx]) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + bob: Account(balance=100), + }, + expected_block_access_list=BlockAccessList( + account_changes=[ + BalAccountChange( + address=alice, + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1) + ], + ), + ] + ), + ) + + +@pytest.mark.valid_from("Osaka") +def test_bal_balance_changes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork, +): + """Ensure BAL captures changes to balance.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas_cost = intrinsic_gas_calculator( + calldata=b"", + contract_creation=False, + access_list=[], + ) + tx_gas_limit = intrinsic_gas_cost + 1000 # add a small buffer + + tx = Transaction( + sender=alice, + to=bob, + value=100, + gas_limit=tx_gas_limit, + gas_price=1_000_000_000, + ) + + block = Block(txs=[tx]) + alice_initial_balance = pre[alice].balance + + # Account for both the value sent and gas cost (gas_price * gas_used) + alice_final_balance = alice_initial_balance - 100 - (intrinsic_gas_cost * 1_000_000_000) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1, balance=alice_final_balance), + bob: Account(balance=100), + }, + expected_block_access_list=BlockAccessList( + account_changes=[ + BalAccountChange( + address=alice, + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1) + ], + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=alice_final_balance) + ], + ), + BalAccountChange( + address=bob, + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=100) + ], + ), + # TODO: Validate coinbase + ] + ), + ) + + +@pytest.mark.valid_from("Osaka") +def test_bal_storage_writes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures storage writes.""" + storage_contract = pre.deploy_contract(code=Op.SSTORE(0x01, 0x42) + Op.STOP) + alice = pre.fund_eoa() + + tx = Transaction( + sender=alice, + to=storage_contract, + gas_limit=100000, + ) + + block = Block(txs=[tx]) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + storage_contract: Account(storage={0x01: 0x42}), + }, + expected_block_access_list=BlockAccessList( + account_changes=[ + BalAccountChange( + address=storage_contract, + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange( + tx_index=1, post_value=0x42 + ) + ], + ) + ], + ), + ] + ), + ) + + +@pytest.mark.valid_from("Osaka") +def test_bal_storage_reads( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures storage reads.""" + storage_contract = pre.deploy_contract( + code=Op.SLOAD(0x01) + Op.STOP, + storage={0x01: 0x42}, + ) + alice = pre.fund_eoa() + + tx = Transaction( + sender=alice, + to=storage_contract, + gas_limit=100000, + ) + + block = Block(txs=[tx]) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + storage_contract: Account(storage={0x01: 0x42}), + }, + expected_block_access_list=BlockAccessList( + account_changes=[ + BalAccountChange( + address=storage_contract, + storage_reads=[0x01], + ), + ] + ), + ) + + +@pytest.mark.valid_from("Osaka") +def test_bal_code_changes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL captures changes to account code.""" + deployed_code = Op.PUSH1(0x42) + Op.PUSH1(0x00) + Op.SSTORE + Op.STOP + + deployed_code_bytes = bytes(deployed_code) + factory_code = ( + Op.PUSH32(deployed_code_bytes) # Contract code + + Op.PUSH1(0x00) # Memory offset + + Op.MSTORE # Store code in memory + + Op.PUSH1(len(deployed_code_bytes)) # Code size + + Op.PUSH1(0x00) # Memory offset + + Op.PUSH1(0x00) # Value to send + + Op.CREATE # CREATE opcode + + Op.STOP + ) + + factory_contract = pre.deploy_contract(code=factory_code) + alice = pre.fund_eoa() + + tx = Transaction( + sender=alice, + to=factory_contract, + gas_limit=500000, + ) + + block = Block(txs=[tx]) + + # The CREATE opcode will deploy to a deterministic address + # We'll need to calculate or determine what that address will be + # For now, we'll focus on the factory contract having a code change + blockchain_test( + pre=pre, + blocks=[block], + post={ + alice: Account(nonce=1), + factory_contract: Account(), + # The newly created contract would be here but we'd need to calculate its address + }, + # Note: This test might need adjustment based on how CREATE addresses are calculated + # and how code changes are tracked in the BAL + expected_block_access_list=BlockAccessList( + account_changes=[ + # { + # "address": alice, + # "nonce_changes": [{"tx_index": 0, "post_nonce": 1}], + # }, + # { + # "address": factory_contract, + # "code_changes": [{"tx_index": 0, "new_code": deployed_code}], + # }, + ] + ), + ) diff --git a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py b/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py deleted file mode 100644 index aadf3add75b..00000000000 --- a/tests/unscheduled/eip7928_block_level_access_lists/test_validate_bal.py +++ /dev/null @@ -1,237 +0,0 @@ -"""Tests for validating EIP-7928: Block-level Access Lists (BAL).""" - -import pytest - -from ethereum_test_tools import ( - Account, - Alloc, - Block, - BlockchainTestFiller, - Transaction, -) - -from .spec import ACTIVATION_FORK_NAME, ref_spec_7928 - -REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path -REFERENCE_SPEC_VERSION = ref_spec_7928.version - - -@pytest.mark.valid_from(ACTIVATION_FORK_NAME) -def test_bal_nonce_changes( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -): - """Ensure BAL captures changes to nonce.""" - alice = pre.fund_eoa() - bob = pre.fund_eoa(amount=0) - - tx = Transaction( - sender=alice, - to=bob, - value=100, - ) - - block = Block(txs=[tx]) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - bob: Account(balance=100), - }, - block_access_list={ - "account_changes": [ - { - "address": alice, - "nonce_changes": [{"tx_index": 0, "post_nonce": 1}], - }, - ] - }, - ) - - -@pytest.mark.valid_from(ACTIVATION_FORK_NAME) -def test_bal_balance_changes( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -): - """Ensure BAL captures changes to balance.""" - alice = pre.fund_eoa() - bob = pre.fund_eoa(amount=0) - - tx = Transaction( - sender=alice, - to=bob, - value=100, - ) - - block = Block(txs=[tx]) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - bob: Account(balance=100), - }, - block_access_list={ - "account_changes": [ - { - "address": alice, - "balance_changes": [ - { - "tx_index": 0, - "post_balance": pre[alice].balance - 100, - } - ], - }, - { - "address": bob, - "balance_changes": [{"tx_index": 0, "post_balance": 100}], - }, - # TODO: Validate coinbase - ] - }, - ) - - -@pytest.mark.valid_from(ACTIVATION_FORK_NAME) -def test_bal_storage_writes( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -): - """Ensure BAL captures storage writes.""" - from ethereum_test_tools.vm.opcode import Opcodes as Op - - # Alice calls contract that writes to storage slot 0x01 - storage_contract = pre.deploy_contract(code=Op.SSTORE(0x01, 0x42) + Op.STOP) - alice = pre.fund_eoa() - - tx = Transaction( - sender=alice, - to=storage_contract, - gas_limit=100000, - ) - - block = Block(txs=[tx]) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - storage_contract: Account(storage={0x01: 0x42}), - }, - block_access_list={ - "account_changes": [ - { - "address": storage_contract, - "storage_changes": [ - { - "slot": 0x01, - "slot_changes": [ - { - "tx_index": 0, - "post_value": 0x42, - } - ], - } - ], - }, - ] - }, - ) - - -@pytest.mark.valid_from(ACTIVATION_FORK_NAME) -def test_bal_storage_reads( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -): - """Ensure BAL captures storage reads.""" - from ethereum_test_tools.vm.opcode import Opcodes as Op - - storage_contract = pre.deploy_contract( - code=Op.SLOAD(0x01) + Op.STOP, - storage={0x01: 0x42}, # Pre-populate storage - ) - alice = pre.fund_eoa() - - tx = Transaction( - sender=alice, - to=storage_contract, - gas_limit=100000, - ) - - block = Block(txs=[tx]) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - storage_contract: Account(storage={0x01: 0x42}), - }, - block_access_list={ - "account_changes": [ - { - "address": storage_contract, - "storage_reads": [0x01], - }, - ] - }, - ) - - -@pytest.mark.valid_from(ACTIVATION_FORK_NAME) -def test_bal_code_changes( - pre: Alloc, - blockchain_test: BlockchainTestFiller, -): - """Ensure BAL captures changes to account code.""" - from ethereum_test_tools.vm.opcode import Opcodes as Op - - deployed_code = Op.PUSH1(0x42) + Op.PUSH1(0x00) + Op.SSTORE + Op.STOP - - factory_code = ( - Op.PUSH32(deployed_code) # Contract code - + Op.PUSH1(0x00) # Memory offset - + Op.MSTORE # Store code in memory - + Op.PUSH1(len(deployed_code)) # Code size - + Op.PUSH1(0x00) # Memory offset - + Op.PUSH1(0x00) # Value to send - + Op.CREATE # CREATE opcode - + Op.STOP - ) - - factory_contract = pre.deploy_contract(code=factory_code) - alice = pre.fund_eoa() - - tx = Transaction( - sender=alice, - to=factory_contract, - gas_limit=200000, - ) - - block = Block(txs=[tx]) - - blockchain_test( - pre=pre, - blocks=[block], - post={ - alice: Account(nonce=1), - }, - block_access_list={ - "account_changes": [ - { - "address": alice, - "nonce_changes": [{"tx_index": 0, "post_nonce": 1}], - }, - { - "address": factory_contract, - "code_changes": [{"tx_index": 0, "new_code": deployed_code}], - }, - ] - }, - ) diff --git a/uv.lock b/uv.lock index 47304fae5a1..3d7cf003638 100644 --- a/uv.lock +++ b/uv.lock @@ -95,11 +95,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.7.14" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] @@ -149,50 +149,55 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] @@ -312,56 +317,77 @@ wheels = [ [[package]] name = "coverage" -version = "7.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/40/916786453bcfafa4c788abee4ccd6f592b5b5eca0cd61a32a4e5a7ef6e02/coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba", size = 212152, upload-time = "2025-07-03T10:52:53.562Z" }, - { url = "https://files.pythonhosted.org/packages/9f/66/cc13bae303284b546a030762957322bbbff1ee6b6cb8dc70a40f8a78512f/coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa", size = 212540, upload-time = "2025-07-03T10:52:55.196Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3c/d56a764b2e5a3d43257c36af4a62c379df44636817bb5f89265de4bf8bd7/coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a", size = 245097, upload-time = "2025-07-03T10:52:56.509Z" }, - { url = "https://files.pythonhosted.org/packages/b1/46/bd064ea8b3c94eb4ca5d90e34d15b806cba091ffb2b8e89a0d7066c45791/coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc", size = 242812, upload-time = "2025-07-03T10:52:57.842Z" }, - { url = "https://files.pythonhosted.org/packages/43/02/d91992c2b29bc7afb729463bc918ebe5f361be7f1daae93375a5759d1e28/coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2", size = 244617, upload-time = "2025-07-03T10:52:59.239Z" }, - { url = "https://files.pythonhosted.org/packages/b7/4f/8fadff6bf56595a16d2d6e33415841b0163ac660873ed9a4e9046194f779/coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c", size = 244263, upload-time = "2025-07-03T10:53:00.601Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d2/e0be7446a2bba11739edb9f9ba4eff30b30d8257370e237418eb44a14d11/coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd", size = 242314, upload-time = "2025-07-03T10:53:01.932Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7d/dcbac9345000121b8b57a3094c2dfcf1ccc52d8a14a40c1d4bc89f936f80/coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74", size = 242904, upload-time = "2025-07-03T10:53:03.478Z" }, - { url = "https://files.pythonhosted.org/packages/41/58/11e8db0a0c0510cf31bbbdc8caf5d74a358b696302a45948d7c768dfd1cf/coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6", size = 214553, upload-time = "2025-07-03T10:53:05.174Z" }, - { url = "https://files.pythonhosted.org/packages/3a/7d/751794ec8907a15e257136e48dc1021b1f671220ecccfd6c4eaf30802714/coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7", size = 215441, upload-time = "2025-07-03T10:53:06.472Z" }, - { url = "https://files.pythonhosted.org/packages/62/5b/34abcedf7b946c1c9e15b44f326cb5b0da852885312b30e916f674913428/coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62", size = 213873, upload-time = "2025-07-03T10:53:07.699Z" }, - { url = "https://files.pythonhosted.org/packages/53/d7/7deefc6fd4f0f1d4c58051f4004e366afc9e7ab60217ac393f247a1de70a/coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0", size = 212344, upload-time = "2025-07-03T10:53:09.3Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/ee03c95d32be4d519e6a02e601267769ce2e9a91fc8faa1b540e3626c680/coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3", size = 212580, upload-time = "2025-07-03T10:53:11.52Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9f/826fa4b544b27620086211b87a52ca67592622e1f3af9e0a62c87aea153a/coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1", size = 246383, upload-time = "2025-07-03T10:53:13.134Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b3/4477aafe2a546427b58b9c540665feff874f4db651f4d3cb21b308b3a6d2/coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615", size = 243400, upload-time = "2025-07-03T10:53:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c2/efffa43778490c226d9d434827702f2dfbc8041d79101a795f11cbb2cf1e/coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b", size = 245591, upload-time = "2025-07-03T10:53:15.872Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e7/a59888e882c9a5f0192d8627a30ae57910d5d449c80229b55e7643c078c4/coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9", size = 245402, upload-time = "2025-07-03T10:53:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/92/a5/72fcd653ae3d214927edc100ce67440ed8a0a1e3576b8d5e6d066ed239db/coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f", size = 243583, upload-time = "2025-07-03T10:53:18.781Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f5/84e70e4df28f4a131d580d7d510aa1ffd95037293da66fd20d446090a13b/coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d", size = 244815, upload-time = "2025-07-03T10:53:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/39/e7/d73d7cbdbd09fdcf4642655ae843ad403d9cbda55d725721965f3580a314/coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355", size = 214719, upload-time = "2025-07-03T10:53:21.521Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d6/7486dcc3474e2e6ad26a2af2db7e7c162ccd889c4c68fa14ea8ec189c9e9/coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0", size = 215509, upload-time = "2025-07-03T10:53:22.853Z" }, - { url = "https://files.pythonhosted.org/packages/b7/34/0439f1ae2593b0346164d907cdf96a529b40b7721a45fdcf8b03c95fcd90/coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b", size = 213910, upload-time = "2025-07-03T10:53:24.472Z" }, - { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" }, - { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" }, - { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" }, - { url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" }, - { url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" }, - { url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" }, - { url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" }, - { url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" }, - { url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" }, - { url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" }, - { url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" }, - { url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" }, - { url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" }, - { url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" }, - { url = "https://files.pythonhosted.org/packages/d7/85/f8bbefac27d286386961c25515431482a425967e23d3698b75a250872924/coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050", size = 204013, upload-time = "2025-07-03T10:54:12.084Z" }, - { url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" }, +version = "7.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload-time = "2025-08-17T00:26:43.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/ba/2c9817e62018e7d480d14f684c160b3038df9ff69c5af7d80e97d143e4d1/coverage-7.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05d5f98ec893d4a2abc8bc5f046f2f4367404e7e5d5d18b83de8fde1093ebc4f", size = 216514, upload-time = "2025-08-17T00:24:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/093412a959a6b6261446221ba9fb23bb63f661a5de70b5d130763c87f916/coverage-7.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9267efd28f8994b750d171e58e481e3bbd69e44baed540e4c789f8e368b24b88", size = 216914, upload-time = "2025-08-17T00:24:35.881Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/2fdf4a71cfe93b07eae845ebf763267539a7d8b7e16b062f959d56d7e433/coverage-7.10.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4456a039fdc1a89ea60823d0330f1ac6f97b0dbe9e2b6fb4873e889584b085fb", size = 247308, upload-time = "2025-08-17T00:24:37.61Z" }, + { url = "https://files.pythonhosted.org/packages/ba/16/33f6cded458e84f008b9f6bc379609a6a1eda7bffe349153b9960803fc11/coverage-7.10.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c2bfbd2a9f7e68a21c5bd191be94bfdb2691ac40d325bac9ef3ae45ff5c753d9", size = 249241, upload-time = "2025-08-17T00:24:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/84/98/9c18e47c889be58339ff2157c63b91a219272503ee32b49d926eea2337f2/coverage-7.10.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab7765f10ae1df7e7fe37de9e64b5a269b812ee22e2da3f84f97b1c7732a0d8", size = 251346, upload-time = "2025-08-17T00:24:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/6d/07/00a6c0d53e9a22d36d8e95ddd049b860eef8f4b9fd299f7ce34d8e323356/coverage-7.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a09b13695166236e171ec1627ff8434b9a9bae47528d0ba9d944c912d33b3d2", size = 249037, upload-time = "2025-08-17T00:24:41.904Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/1e1b944d6a6483d07bab5ef6ce063fcf3d0cc555a16a8c05ebaab11f5607/coverage-7.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5c9e75dfdc0167d5675e9804f04a56b2cf47fb83a524654297000b578b8adcb7", size = 247090, upload-time = "2025-08-17T00:24:43.193Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/2ce5ab8a728b8e25ced077111581290ffaef9efaf860a28e25435ab925cf/coverage-7.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c751261bfe6481caba15ec005a194cb60aad06f29235a74c24f18546d8377df0", size = 247732, upload-time = "2025-08-17T00:24:44.906Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f3/706c4a24f42c1c5f3a2ca56637ab1270f84d9e75355160dc34d5e39bb5b7/coverage-7.10.4-cp311-cp311-win32.whl", hash = "sha256:051c7c9e765f003c2ff6e8c81ccea28a70fb5b0142671e4e3ede7cebd45c80af", size = 218961, upload-time = "2025-08-17T00:24:46.241Z" }, + { url = "https://files.pythonhosted.org/packages/e8/aa/6b9ea06e0290bf1cf2a2765bba89d561c5c563b4e9db8298bf83699c8b67/coverage-7.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a647b152f10be08fb771ae4a1421dbff66141e3d8ab27d543b5eb9ea5af8e52", size = 219851, upload-time = "2025-08-17T00:24:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/8b/be/f0dc9ad50ee183369e643cd7ed8f2ef5c491bc20b4c3387cbed97dd6e0d1/coverage-7.10.4-cp311-cp311-win_arm64.whl", hash = "sha256:b09b9e4e1de0d406ca9f19a371c2beefe3193b542f64a6dd40cfcf435b7d6aa0", size = 218530, upload-time = "2025-08-17T00:24:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/781c9e4dd57cabda2a28e2ce5b00b6be416015265851060945a5ed4bd85e/coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79", size = 216706, upload-time = "2025-08-17T00:24:51.528Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8c/51255202ca03d2e7b664770289f80db6f47b05138e06cce112b3957d5dfd/coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e", size = 216939, upload-time = "2025-08-17T00:24:53.171Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/df11131483698660f94d3c847dc76461369782d7a7644fcd72ac90da8fd0/coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e", size = 248429, upload-time = "2025-08-17T00:24:54.934Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/13ac5eda7300e160bf98f082e75f5c5b4189bf3a883dd1ee42dbedfdc617/coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0", size = 251178, upload-time = "2025-08-17T00:24:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/f63b56a58ad0bec68a840e7be6b7ed9d6f6288d790760647bb88f5fea41e/coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62", size = 252313, upload-time = "2025-08-17T00:24:57.692Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b6/79338f1ea27b01266f845afb4485976211264ab92407d1c307babe3592a7/coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a", size = 250230, upload-time = "2025-08-17T00:24:59.293Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/3b24f1da3e0286a4dc5832427e1d448d5296f8287464b1ff4a222abeeeb5/coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23", size = 248351, upload-time = "2025-08-17T00:25:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/d59412f869e49dcc5b89398ef3146c8bfaec870b179cc344d27932e0554b/coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927", size = 249788, upload-time = "2025-08-17T00:25:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/cc/52/04a3b733f40a0cc7c4a5b9b010844111dbf906df3e868b13e1ce7b39ac31/coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a", size = 219131, upload-time = "2025-08-17T00:25:03.79Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/12909fc0b83888197b3ec43a4ac7753589591c08d00d9deda4158df2734e/coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b", size = 219939, upload-time = "2025-08-17T00:25:05.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/058bb3220fdd6821bada9685eadac2940429ab3c97025ce53549ff423cc1/coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a", size = 218572, upload-time = "2025-08-17T00:25:06.897Z" }, + { url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload-time = "2025-08-17T00:25:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload-time = "2025-08-17T00:25:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload-time = "2025-08-17T00:25:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584, upload-time = "2025-08-17T00:25:13.483Z" }, + { url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856, upload-time = "2025-08-17T00:25:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015, upload-time = "2025-08-17T00:25:16.759Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908, upload-time = "2025-08-17T00:25:18.232Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525, upload-time = "2025-08-17T00:25:20.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173, upload-time = "2025-08-17T00:25:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969, upload-time = "2025-08-17T00:25:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601, upload-time = "2025-08-17T00:25:25.295Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445, upload-time = "2025-08-17T00:25:27.129Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676, upload-time = "2025-08-17T00:25:28.641Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002, upload-time = "2025-08-17T00:25:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178, upload-time = "2025-08-17T00:25:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402, upload-time = "2025-08-17T00:25:33.339Z" }, + { url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957, upload-time = "2025-08-17T00:25:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718, upload-time = "2025-08-17T00:25:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848, upload-time = "2025-08-17T00:25:37.754Z" }, + { url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833, upload-time = "2025-08-17T00:25:39.252Z" }, + { url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897, upload-time = "2025-08-17T00:25:40.772Z" }, + { url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160, upload-time = "2025-08-17T00:25:42.229Z" }, + { url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717, upload-time = "2025-08-17T00:25:43.875Z" }, + { url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994, upload-time = "2025-08-17T00:25:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038, upload-time = "2025-08-17T00:25:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575, upload-time = "2025-08-17T00:25:48.613Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927, upload-time = "2025-08-17T00:25:50.881Z" }, + { url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930, upload-time = "2025-08-17T00:25:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862, upload-time = "2025-08-17T00:25:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360, upload-time = "2025-08-17T00:25:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449, upload-time = "2025-08-17T00:25:57.984Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246, upload-time = "2025-08-17T00:25:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825, upload-time = "2025-08-17T00:26:01.44Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462, upload-time = "2025-08-17T00:26:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675, upload-time = "2025-08-17T00:26:04.606Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176, upload-time = "2025-08-17T00:26:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341, upload-time = "2025-08-17T00:26:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600, upload-time = "2025-08-17T00:26:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036, upload-time = "2025-08-17T00:26:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794, upload-time = "2025-08-17T00:26:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946, upload-time = "2025-08-17T00:26:15.899Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226, upload-time = "2025-08-17T00:26:17.566Z" }, + { url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346, upload-time = "2025-08-17T00:26:19.311Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368, upload-time = "2025-08-17T00:26:21.011Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365, upload-time = "2025-08-17T00:26:41.479Z" }, ] [package.optional-dependencies] @@ -371,43 +397,43 @@ toml = [ [[package]] name = "cryptography" -version = "45.0.5" +version = "45.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, - { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, - { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, - { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, - { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, - { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, - { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, - { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, - { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, - { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878, upload-time = "2025-07-02T13:06:06.339Z" }, - { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" }, - { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" }, - { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" }, - { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669, upload-time = "2025-08-05T23:59:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022, upload-time = "2025-08-05T23:59:16.954Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802, upload-time = "2025-08-05T23:59:18.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706, upload-time = "2025-08-05T23:59:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740, upload-time = "2025-08-05T23:59:21.525Z" }, + { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874, upload-time = "2025-08-05T23:59:23.017Z" }, ] [[package]] @@ -662,14 +688,14 @@ provides-extras = ["test", "lint", "docs"] [[package]] name = "ethereum-hive" -version = "0.1.0a1" +version = "0.1.0a2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/e8/da004286abff126b7e56268f7930bdc23437f4ae21c42ed57da090444d8b/ethereum_hive-0.1.0a1.tar.gz", hash = "sha256:36c5c6f1a6c231054d7fa9cee25180488a362fedb4c0570ba09e3d725f1b51c9", size = 73831, upload-time = "2025-07-09T10:36:51.192Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9b/f15c86866245ad5c22dbc98f433158fdeaab3e96eae4efd1bf6a12a7717c/ethereum_hive-0.1.0a2.tar.gz", hash = "sha256:9ddc08a8dfe2828a9a4abfca1c0972f88882fa06fb6a3e111ec013b66906f843", size = 74796, upload-time = "2025-07-15T05:38:36.983Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/fa/fa7a5980cb88f467e2ca9946f52c7a0a3568481260f57d43dbb1a510e95b/ethereum_hive-0.1.0a1-py3-none-any.whl", hash = "sha256:f3c878eba5dcf25151410db3f68412a15934d79c0cf212eb9784746c6f346785", size = 37126, upload-time = "2025-07-09T10:36:25.665Z" }, + { url = "https://files.pythonhosted.org/packages/74/60/1daf13c49bbefed6b67dd8a5d1280386e1ad22c9ec61737d44047064632e/ethereum_hive-0.1.0a2-py3-none-any.whl", hash = "sha256:0b0253146428240725e2b83a86bbc9eab975c460b8d76ce3937ba5295aa2448b", size = 37578, upload-time = "2025-07-15T05:38:35.334Z" }, ] [[package]] @@ -705,14 +731,14 @@ dependencies = [ [[package]] name = "ethereum-types" -version = "0.2.3" +version = "0.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/23/cb6fdde984cc3632b478d68daaad9b8faccb3f83338882d3060e8cb2999e/ethereum_types-0.2.3.tar.gz", hash = "sha256:9cdbfda64052102ff57e6119464493d070f67126c2596081f6c791da90e834cd", size = 15216, upload-time = "2024-11-18T14:11:27.292Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/10/82c0b2e4e153d937c28300394671b69bd6c5e669422ea669514e4c3b76b3/ethereum_types-0.2.4.tar.gz", hash = "sha256:9012fd9c5f81795302ac1510631b47e79420ba6154c0e16d0417588a18fd0c90", size = 15402, upload-time = "2025-07-23T21:16:33.042Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/0e/d327da9d255b030f8eee252cac85602982c30b87263a4b553e780154fe84/ethereum_types-0.2.3-py3-none-any.whl", hash = "sha256:a09a0d53a82cb9e3df734302d5b7eb92e01f524b81eb9d5b1240c77b796add5d", size = 10381, upload-time = "2024-11-18T14:11:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/9b/93/b11cf0f238f1cb24bee873650b883839fc8bd7e36b7c66e9bc4779469528/ethereum_types-0.2.4-py3-none-any.whl", hash = "sha256:38496286d55ed6010abef30b6807b00be7ba8fa82165ed4f987c2c29ad13b2ed", size = 10562, upload-time = "2025-07-23T21:16:32.191Z" }, ] [[package]] @@ -726,11 +752,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.18.0" +version = "3.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, ] [[package]] @@ -759,26 +785,26 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.44" +version = "3.1.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, ] [[package]] name = "griffe" -version = "1.7.3" +version = "1.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/ca/29f36e00c74844ae50d139cf5a8b1751887b2f4d5023af65d460268ad7aa/griffe-1.12.1.tar.gz", hash = "sha256:29f5a6114c0aeda7d9c86a570f736883f8a2c5b38b57323d56b3d1c000565567", size = 411863, upload-time = "2025-08-14T21:08:15.38Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" }, + { url = "https://files.pythonhosted.org/packages/13/f2/4fab6c3e5bcaf38a44cc8a974d2752eaad4c129e45d6533d926a30edd133/griffe-1.12.1-py3-none-any.whl", hash = "sha256:2d7c12334de00089c31905424a00abcfd931b45b8b516967f224133903d302cc", size = 138940, upload-time = "2025-08-14T21:08:13.382Z" }, ] [[package]] @@ -907,14 +933,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] @@ -1110,11 +1136,12 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.15" +version = "9.6.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, + { name = "click" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -1125,9 +1152,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/c1/f804ba2db2ddc2183e900befe7dad64339a34fa935034e1ab405289d0a97/mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5", size = 3951836, upload-time = "2025-07-01T10:14:15.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/02/51115cdda743e1551c5c13bdfaaf8c46b959acc57ba914d8ec479dd2fe1f/mkdocs_material-9.6.17.tar.gz", hash = "sha256:48ae7aec72a3f9f501a70be3fbd329c96ff5f5a385b67a1563e5ed5ce064affe", size = 4032898, upload-time = "2025-08-15T16:09:21.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/30/dda19f0495a9096b64b6b3c07c4bfcff1c76ee0fc521086d53593f18b4c0/mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a", size = 8716840, upload-time = "2025-07-01T10:14:13.18Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7c/0f0d44c92c8f3068930da495b752244bd59fd87b5b0f9571fa2d2a93aee7/mkdocs_material-9.6.17-py3-none-any.whl", hash = "sha256:221dd8b37a63f52e580bcab4a7e0290e4a6f59bd66190be9c3d40767e05f9417", size = 9229230, upload-time = "2025-08-15T16:09:18.301Z" }, ] [[package]] @@ -1141,7 +1168,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.29.1" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -1151,23 +1178,23 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686, upload-time = "2025-03-31T08:33:11.997Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload-time = "2025-03-31T08:33:09.661Z" }, + { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, ] [[package]] name = "mkdocstrings-python" -version = "1.16.12" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/7c/6dfd8ad59c0eebae167168528ed6cad00116f58ef2327686149f7b25d175/mkdocstrings_python-1.17.0.tar.gz", hash = "sha256:c6295962b60542a9c7468a3b515ce8524616ca9f8c1a38c790db4286340ba501", size = 200408, upload-time = "2025-08-14T21:18:14.568Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ac/b1fcc937f4ecd372f3e857162dea67c45c1e2eedbac80447be516e3372bb/mkdocstrings_python-1.17.0-py3-none-any.whl", hash = "sha256:49903fa355dfecc5ad0b891e78ff5d25d30ffd00846952801bbe8331e123d4b0", size = 124778, upload-time = "2025-08-14T21:18:12.821Z" }, ] [[package]] @@ -1472,15 +1499,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.16" +version = "10.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, ] [[package]] @@ -1678,60 +1705,71 @@ wheels = [ [[package]] name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669, upload-time = "2024-11-06T20:09:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684, upload-time = "2024-11-06T20:09:32.915Z" }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589, upload-time = "2024-11-06T20:09:35.504Z" }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121, upload-time = "2024-11-06T20:09:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275, upload-time = "2024-11-06T20:09:40.371Z" }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257, upload-time = "2024-11-06T20:09:43.059Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727, upload-time = "2024-11-06T20:09:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667, upload-time = "2024-11-06T20:09:49.828Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963, upload-time = "2024-11-06T20:09:51.819Z" }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700, upload-time = "2024-11-06T20:09:53.982Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592, upload-time = "2024-11-06T20:09:56.222Z" }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929, upload-time = "2024-11-06T20:09:58.642Z" }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213, upload-time = "2024-11-06T20:10:00.867Z" }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734, upload-time = "2024-11-06T20:10:03.361Z" }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052, upload-time = "2024-11-06T20:10:05.179Z" }, - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" }, - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" }, +version = "2025.7.34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714, upload-time = "2025-07-31T00:21:16.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/85/f497b91577169472f7c1dc262a5ecc65e39e146fc3a52c571e5daaae4b7d/regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8", size = 484594, upload-time = "2025-07-31T00:19:13.927Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c5/ad2a5c11ce9e6257fcbfd6cd965d07502f6054aaa19d50a3d7fd991ec5d1/regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a", size = 289294, upload-time = "2025-07-31T00:19:15.395Z" }, + { url = "https://files.pythonhosted.org/packages/8e/01/83ffd9641fcf5e018f9b51aa922c3e538ac9439424fda3df540b643ecf4f/regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68", size = 285933, upload-time = "2025-07-31T00:19:16.704Z" }, + { url = "https://files.pythonhosted.org/packages/77/20/5edab2e5766f0259bc1da7381b07ce6eb4401b17b2254d02f492cd8a81a8/regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78", size = 792335, upload-time = "2025-07-31T00:19:18.561Z" }, + { url = "https://files.pythonhosted.org/packages/30/bd/744d3ed8777dce8487b2606b94925e207e7c5931d5870f47f5b643a4580a/regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719", size = 858605, upload-time = "2025-07-31T00:19:20.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/93754176289718d7578c31d151047e7b8acc7a8c20e7706716f23c49e45e/regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33", size = 905780, upload-time = "2025-07-31T00:19:21.876Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/c689f274a92deffa03999a430505ff2aeace408fd681a90eafa92fdd6930/regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083", size = 798868, upload-time = "2025-07-31T00:19:23.222Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/39673688805d139b33b4a24851a71b9978d61915c4d72b5ffda324d0668a/regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3", size = 781784, upload-time = "2025-07-31T00:19:24.59Z" }, + { url = "https://files.pythonhosted.org/packages/18/bd/4c1cab12cfabe14beaa076523056b8ab0c882a8feaf0a6f48b0a75dab9ed/regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d", size = 852837, upload-time = "2025-07-31T00:19:25.911Z" }, + { url = "https://files.pythonhosted.org/packages/cb/21/663d983cbb3bba537fc213a579abbd0f263fb28271c514123f3c547ab917/regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd", size = 844240, upload-time = "2025-07-31T00:19:27.688Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2d/9beeeb913bc5d32faa913cf8c47e968da936af61ec20af5d269d0f84a100/regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a", size = 787139, upload-time = "2025-07-31T00:19:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f5/9b9384415fdc533551be2ba805dd8c4621873e5df69c958f403bfd3b2b6e/regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1", size = 264019, upload-time = "2025-07-31T00:19:31.129Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/e069ed94debcf4cc9626d652a48040b079ce34c7e4fb174f16874958d485/regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a", size = 276047, upload-time = "2025-07-31T00:19:32.497Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/3bafbe9d1fd1db77355e7fbbbf0d0cfb34501a8b8e334deca14f94c7b315/regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0", size = 268362, upload-time = "2025-07-31T00:19:34.094Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f0/31d62596c75a33f979317658e8d261574785c6cd8672c06741ce2e2e2070/regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50", size = 485492, upload-time = "2025-07-31T00:19:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/d8/16/b818d223f1c9758c3434be89aa1a01aae798e0e0df36c1f143d1963dd1ee/regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f", size = 290000, upload-time = "2025-07-31T00:19:37.175Z" }, + { url = "https://files.pythonhosted.org/packages/cd/70/69506d53397b4bd6954061bae75677ad34deb7f6ca3ba199660d6f728ff5/regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130", size = 286072, upload-time = "2025-07-31T00:19:38.612Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/536a216d5f66084fb577bb0543b5cb7de3272eb70a157f0c3a542f1c2551/regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46", size = 797341, upload-time = "2025-07-31T00:19:40.119Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/733f8168449e56e8f404bb807ea7189f59507cbea1b67a7bbcd92f8bf844/regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4", size = 862556, upload-time = "2025-07-31T00:19:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/19/dd/59c464d58c06c4f7d87de4ab1f590e430821345a40c5d345d449a636d15f/regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0", size = 910762, upload-time = "2025-07-31T00:19:43Z" }, + { url = "https://files.pythonhosted.org/packages/37/a8/b05ccf33ceca0815a1e253693b2c86544932ebcc0049c16b0fbdf18b688b/regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b", size = 801892, upload-time = "2025-07-31T00:19:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/b993cb2e634cc22810afd1652dba0cae156c40d4864285ff486c73cd1996/regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01", size = 786551, upload-time = "2025-07-31T00:19:46.127Z" }, + { url = "https://files.pythonhosted.org/packages/2d/79/7849d67910a0de4e26834b5bb816e028e35473f3d7ae563552ea04f58ca2/regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77", size = 856457, upload-time = "2025-07-31T00:19:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/91/c6/de516bc082524b27e45cb4f54e28bd800c01efb26d15646a65b87b13a91e/regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da", size = 848902, upload-time = "2025-07-31T00:19:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/519ff8ba15f732db099b126f039586bd372da6cd4efb810d5d66a5daeda1/regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282", size = 788038, upload-time = "2025-07-31T00:19:50.794Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7d/aabb467d8f57d8149895d133c88eb809a1a6a0fe262c1d508eb9dfabb6f9/regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588", size = 264417, upload-time = "2025-07-31T00:19:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/3b/39/bd922b55a4fc5ad5c13753274e5b536f5b06ec8eb9747675668491c7ab7a/regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62", size = 275387, upload-time = "2025-07-31T00:19:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/c61d2fdcecb754a40475a3d1ef9a000911d3e3fc75c096acf44b0dfb786a/regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176", size = 268482, upload-time = "2025-07-31T00:19:55.183Z" }, + { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334, upload-time = "2025-07-31T00:19:56.58Z" }, + { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942, upload-time = "2025-07-31T00:19:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991, upload-time = "2025-07-31T00:19:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415, upload-time = "2025-07-31T00:20:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487, upload-time = "2025-07-31T00:20:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717, upload-time = "2025-07-31T00:20:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943, upload-time = "2025-07-31T00:20:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664, upload-time = "2025-07-31T00:20:08.818Z" }, + { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457, upload-time = "2025-07-31T00:20:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008, upload-time = "2025-07-31T00:20:11.823Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101, upload-time = "2025-07-31T00:20:13.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401, upload-time = "2025-07-31T00:20:15.233Z" }, + { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368, upload-time = "2025-07-31T00:20:16.711Z" }, + { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482, upload-time = "2025-07-31T00:20:18.189Z" }, + { url = "https://files.pythonhosted.org/packages/ac/23/6376f3a23cf2f3c00514b1cdd8c990afb4dfbac3cb4a68b633c6b7e2e307/regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a", size = 485385, upload-time = "2025-07-31T00:20:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/73/5b/6d4d3a0b4d312adbfd6d5694c8dddcf1396708976dd87e4d00af439d962b/regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435", size = 289788, upload-time = "2025-07-31T00:20:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/92/71/5862ac9913746e5054d01cb9fb8125b3d0802c0706ef547cae1e7f4428fa/regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac", size = 286136, upload-time = "2025-07-31T00:20:26.146Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/5b505dc447eb71278eba10d5ec940769ca89c1af70f0468bfbcb98035dc2/regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72", size = 797753, upload-time = "2025-07-31T00:20:27.919Z" }, + { url = "https://files.pythonhosted.org/packages/86/38/3e3dc953d13998fa047e9a2414b556201dbd7147034fbac129392363253b/regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e", size = 863263, upload-time = "2025-07-31T00:20:29.803Z" }, + { url = "https://files.pythonhosted.org/packages/68/e5/3ff66b29dde12f5b874dda2d9dec7245c2051f2528d8c2a797901497f140/regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751", size = 910103, upload-time = "2025-07-31T00:20:31.313Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fe/14176f2182125977fba3711adea73f472a11f3f9288c1317c59cd16ad5e6/regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4", size = 801709, upload-time = "2025-07-31T00:20:33.323Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/80d4e66ed24f1ba876a9e8e31b709f9fd22d5c266bf5f3ab3c1afe683d7d/regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98", size = 786726, upload-time = "2025-07-31T00:20:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/c3ebb30e04a56c046f5c85179dc173818551037daae2c0c940c7b19152cb/regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7", size = 857306, upload-time = "2025-07-31T00:20:37.12Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b2/a4dc5d8b14f90924f27f0ac4c4c4f5e195b723be98adecc884f6716614b6/regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47", size = 848494, upload-time = "2025-07-31T00:20:38.818Z" }, + { url = "https://files.pythonhosted.org/packages/0d/21/9ac6e07a4c5e8646a90b56b61f7e9dac11ae0747c857f91d3d2bc7c241d9/regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e", size = 787850, upload-time = "2025-07-31T00:20:40.478Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/d51204e28e7bc54f9a03bb799b04730d7e54ff2718862b8d4e09e7110a6a/regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb", size = 269730, upload-time = "2025-07-31T00:20:42.253Z" }, + { url = "https://files.pythonhosted.org/packages/74/52/a7e92d02fa1fdef59d113098cb9f02c5d03289a0e9f9e5d4d6acccd10677/regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae", size = 278640, upload-time = "2025-07-31T00:20:44.42Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/a815529b559b1771080faa90c3ab401730661f99d495ab0071649f139ebd/regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64", size = 271757, upload-time = "2025-07-31T00:20:46.355Z" }, ] [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1739,9 +1777,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -1939,11 +1977,11 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20250516" +version = "6.0.12.20250809" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378, upload-time = "2025-05-16T03:08:04.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/21/52ffdbddea3c826bc2758d811ccd7f766912de009c5cf096bd5ebba44680/types_pyyaml-6.0.12.20250809.tar.gz", hash = "sha256:af4a1aca028f18e75297da2ee0da465f799627370d74073e96fee876524f61b5", size = 17385, upload-time = "2025-08-09T03:14:34.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/35/3e/0346d09d6e338401ebf406f12eaf9d0b54b315b86f1ec29e34f1a0aedae9/types_pyyaml-6.0.12.20250809-py3-none-any.whl", hash = "sha256:032b6003b798e7de1a1ddfeefee32fac6486bdfe4845e0ae0e7fb3ee4512b52f", size = 20277, upload-time = "2025-08-09T03:14:34.055Z" }, ] [[package]] From 43b55490d54a9d10e45b6bde3dc274fdeaabd041 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 20 Aug 2025 18:17:49 -0600 Subject: [PATCH 15/35] refactor(bal): Use full bal in t8n; verify and build fixtures - Use the full bal object in the t8n. We can use this to model_validate() into our pydantic model. - Also, pass the bal_hash via t8n so that once we do build the BAL, validate the explicitly defined values, rlp encode it, then hash it, we can compare the full BAL against the hash and make sure that even though we only validated the part that was important for our test, the full BAL is also always valid. Bonus: - Added a new type for ``StorageKey`` because these were not being rlp encoded properly without the left padding. This was tricky to find and I think this can be a common issue. New type here seemed appropriate. --- src/ethereum_clis/types.py | 4 +- src/ethereum_test_base_types/__init__.py | 2 + src/ethereum_test_base_types/base_types.py | 14 + src/ethereum_test_fixtures/blockchain.py | 12 +- src/ethereum_test_forks/forks/forks.py | 5 + src/ethereum_test_specs/blockchain.py | 30 +- src/ethereum_test_types/block_access_list.py | 400 ++++++++++-------- .../test_block_access_lists.py | 18 +- 8 files changed, 285 insertions(+), 200 deletions(-) diff --git a/src/ethereum_clis/types.py b/src/ethereum_clis/types.py index 957e38b7f11..9f7ea8ddad3 100644 --- a/src/ethereum_clis/types.py +++ b/src/ethereum_clis/types.py @@ -13,6 +13,7 @@ UndefinedException, ) from ethereum_test_types import Alloc, Environment, Transaction, TransactionReceipt +from ethereum_test_types.block_access_list import BlockAccessList class TransactionExceptionWithMessage(ExceptionWithMessage[TransactionException]): @@ -57,7 +58,8 @@ class Result(CamelModel): blob_gas_used: HexNumber | None = None requests_hash: Hash | None = None requests: List[Bytes] | None = None - block_access_list: Bytes | None = Field(None, alias="blockAccessList") + block_access_list: BlockAccessList | None = Field(None, alias="blockAccessList") + block_access_list_hash: Hash | None = Field(None, alias="blockAccessListHash") block_exception: Annotated[ BlockExceptionWithMessage | UndefinedException | None, ExceptionMapperValidator ] = None diff --git a/src/ethereum_test_base_types/__init__.py b/src/ethereum_test_base_types/__init__.py index c7e63a7f280..3bb8c6a3707 100644 --- a/src/ethereum_test_base_types/__init__.py +++ b/src/ethereum_test_base_types/__init__.py @@ -14,6 +14,7 @@ HexNumber, Number, NumberBoundTypeVar, + StorageKey, Wei, ZeroPaddedHexNumber, ) @@ -72,6 +73,7 @@ "RLPSerializable", "SignableRLPSerializable", "Storage", + "StorageKey", "StorageRootType", "TestAddress", "TestAddress2", diff --git a/src/ethereum_test_base_types/base_types.py b/src/ethereum_test_base_types/base_types.py index 77a254120ea..d50155ac38a 100644 --- a/src/ethereum_test_base_types/base_types.py +++ b/src/ethereum_test_base_types/base_types.py @@ -383,6 +383,20 @@ class Hash(FixedSizeBytes[32]): # type: ignore pass +class StorageKey(FixedSizeBytes[32]): # type: ignore + """ + Storage key type that automatically applies left padding for values shorter + than 32 bytes. + """ + + def __new__(cls, value, **kwargs): + """Create a new StorageKey with automatic left padding.""" + # Always apply left_padding for storage keys unless explicitly set to False + if "left_padding" not in kwargs: + kwargs["left_padding"] = True + return super().__new__(cls, value, **kwargs) + + class Bloom(FixedSizeBytes[256]): # type: ignore """Class that helps represent blooms in tests.""" diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index db7f4c81d08..9260faf3299 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -165,7 +165,9 @@ class FixtureHeader(CamelModel): None ) requests_hash: Annotated[Hash, HeaderForkRequirement("requests")] | None = Field(None) - bal_hash: Annotated[Hash, HeaderForkRequirement("bal_hash")] | None = Field(None) + block_access_list_hash: Annotated[Hash, HeaderForkRequirement("bal_hash")] | None = Field( + None, alias="blockAccessListHash" + ) fork: Fork | None = Field(None, exclude=True) @@ -232,7 +234,8 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> "FixtureHead extras = { "state_root": state_root, "requests_hash": Requests() if fork.header_requests_required(0, 0) else None, - "bal_hash": Hash(0) if fork.header_bal_hash_required(0, 0) else None, + # TODO: How should we handle the genesis block access list? Is `Hash(0)` fine? + "block_access_list_hash": Hash(0) if fork.header_bal_hash_required(0, 0) else None, "fork": fork, } return FixtureHeader(**environment_values, **extras) @@ -430,8 +433,9 @@ class FixtureBlockBase(CamelModel): ommers: List[FixtureHeader] = Field(default_factory=list, alias="uncleHeaders") withdrawals: List[FixtureWithdrawal] | None = None execution_witness: WitnessChunk | None = None - block_access_lists: Bytes | None = Field( - None, description="Serialized EIP-7928 Block Access Lists" + block_access_list: Bytes | None = Field( + None, description="Serialized EIP-7928 Block Access List", + alias="blockAccessList" ) @computed_field(alias="blocknumber") # type: ignore[misc] diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 96d43879e3b..1a4220318da 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -1598,6 +1598,11 @@ def engine_forkchoice_updated_version( class Osaka(Prague, solc_name="cancun"): """Osaka fork.""" + @classmethod + def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: + """Hash of the block access list is required starting from Osaka fork.""" + return True + # update some blob constants BLOB_CONSTANTS = { **Prague.BLOB_CONSTANTS, # same base constants as prague diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index f735591c192..e2461e2b01a 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -52,6 +52,7 @@ from ethereum_test_fixtures.common import FixtureBlobSchedule from ethereum_test_forks import Fork from ethereum_test_types import Alloc, Environment, Removable, Requests, Transaction, Withdrawal +from ethereum_test_types.block_access_list import BlockAccessList from .base import BaseTest, OpMode, verify_result from .debugging import print_traces @@ -321,7 +322,7 @@ class BuiltBlock(CamelModel): expected_exception: BLOCK_EXCEPTION_TYPE = None engine_api_error_code: EngineAPIError | None = None fork: Fork - block_access_list: Bytes + block_access_list: BlockAccessList | None def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock: """Get a FixtureBlockBase from the built block.""" @@ -333,7 +334,7 @@ def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock: if self.withdrawals is not None else None ), - block_access_list=self.block_access_list, + block_access_list=self.block_access_list.rlp() if self.block_access_list else None, fork=self.fork, ).with_rlp(txs=self.txs) @@ -606,6 +607,17 @@ def generate_block_data( header.requests_hash = Hash(Requests(requests_lists=list(block.requests))) requests_list = block.requests + if fork.header_bal_hash_required(header.number, header.timestamp): + if transition_tool_output.result.block_access_list is not None: + rlp = transition_tool_output.result.block_access_list.rlp() + computed_bal_hash = Hash(rlp.keccak256()) + if computed_bal_hash != header.block_access_list_hash: + raise Exception( + "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. @@ -624,7 +636,7 @@ def generate_block_data( expected_exception=block.exception, engine_api_error_code=block.engine_api_error_code, fork=fork, - block_access_list=transition_tool_output.result.block_access_list or Bytes(b""), + block_access_list=transition_tool_output.result.block_access_list, ) try: @@ -678,27 +690,27 @@ def verify_post_state(self, t8n, t8n_state: Alloc, expected_state: Alloc | None print_traces(t8n.get_traces()) raise e - def verify_block_access_list(self, actual_bal: Bytes | None, expected_bal: Any | None): + def verify_block_access_list( + self, actual_bal: BlockAccessList | None, expected_bal: Any | None + ): """ Verify that the actual block access list matches expectations. Args: - actual_bal: The block access list returned by the client (RLP-encoded) + actual_bal: The BlockAccessList returned by the client expected_bal: The expected BlockAccessList object """ if expected_bal is None: return - from ethereum_test_types.block_access_list import BlockAccessList - if not isinstance(expected_bal, BlockAccessList): raise Exception( f"Invalid expected_bal type: {type(expected_bal)}. Use BlockAccessList." ) - if actual_bal is None or actual_bal == Bytes(b""): - raise Exception("Expected block access list but got none or empty.") + if actual_bal is None: + raise Exception("Expected block access list but got none.") # Verify using the BlockAccessList's verification method try: diff --git a/src/ethereum_test_types/block_access_list.py b/src/ethereum_test_types/block_access_list.py index d19fbbb4523..ac8db72bba4 100644 --- a/src/ethereum_test_types/block_access_list.py +++ b/src/ethereum_test_types/block_access_list.py @@ -5,53 +5,68 @@ these are simple data classes that can be composed together. """ -from typing import Any, Dict, List, Optional +from typing import Any, ClassVar, Dict, List, Optional -import rlp from pydantic import Field -from ethereum_test_base_types import Address, Bytes, CamelModel, HexNumber -from ethereum_test_base_types.conversions import BytesConvertible, to_bytes +from ethereum_test_base_types import ( + Address, + Bytes, + CamelModel, + HexNumber, + RLPSerializable, + StorageKey, +) -class BalNonceChange(CamelModel): +class BalNonceChange(CamelModel, RLPSerializable): """Represents a nonce change in the block access list.""" tx_index: int = Field(..., description="Transaction index where the change occurred") post_nonce: int = Field(..., description="Nonce value after the transaction") + rlp_fields: ClassVar[List[str]] = ["tx_index", "post_nonce"] -class BalBalanceChange(CamelModel): + +class BalBalanceChange(CamelModel, RLPSerializable): """Represents a balance change in the block access list.""" tx_index: int = Field(..., description="Transaction index where the change occurred") post_balance: HexNumber = Field(..., description="Balance after the transaction") + rlp_fields: ClassVar[List[str]] = ["tx_index", "post_balance"] + -class BalCodeChange(CamelModel): +class BalCodeChange(CamelModel, RLPSerializable): """Represents a code change in the block access list.""" tx_index: int = Field(..., description="Transaction index where the change occurred") new_code: Bytes = Field(..., description="New code bytes") + rlp_fields: ClassVar[List[str]] = ["tx_index", "new_code"] -class BalStorageChange(CamelModel): + +class BalStorageChange(CamelModel, RLPSerializable): """Represents a change to a specific storage slot.""" tx_index: int = Field(..., description="Transaction index where the change occurred") - post_value: HexNumber = Field(..., description="Value after the transaction") + post_value: StorageKey = Field(..., description="Value after the transaction") + + rlp_fields: ClassVar[List[str]] = ["tx_index", "post_value"] -class BalStorageSlot(CamelModel): +class BalStorageSlot(CamelModel, RLPSerializable): """Represents all changes to a specific storage slot.""" - slot: HexNumber = Field(..., description="Storage slot key") + slot: StorageKey = Field(..., description="Storage slot key") slot_changes: List[BalStorageChange] = Field( default_factory=list, description="List of changes to this slot" ) + rlp_fields: ClassVar[List[str]] = ["slot", "slot_changes"] -class BalAccountChange(CamelModel): + +class BalAccountChange(CamelModel, RLPSerializable): """Represents all changes to a specific account in a block.""" address: Address = Field(..., description="Account address") @@ -65,12 +80,46 @@ class BalAccountChange(CamelModel): storage_changes: Optional[List[BalStorageSlot]] = Field( None, description="List of storage changes" ) - storage_reads: Optional[List[HexNumber]] = Field( + storage_reads: Optional[List[StorageKey]] = Field( None, description="List of storage slots that were read" ) + rlp_fields: ClassVar[List[str]] = [ + "address", + "storage_changes", + "storage_reads", + "balance_changes", + "nonce_changes", + "code_changes", + ] + + def to_list(self, signing: bool = False) -> List[Any]: + """ + Override to handle None list fields properly. + None list fields should serialize as empty lists, not empty bytes. + """ + from ethereum_test_base_types.serialization import to_serializable_element + + result: list[Any] = [] + for field_name in self.rlp_fields: + value = getattr(self, field_name) + + # Special handling for None list fields - they should be empty lists + if value is None and field_name in [ + "storage_changes", + "storage_reads", + "balance_changes", + "nonce_changes", + "code_changes", + ]: + result.append([]) + else: + result.append(to_serializable_element(value)) + + return result -class BlockAccessList(CamelModel): + +class BlockAccessList(CamelModel, RLPSerializable): """ Expected Block Access List for verification. @@ -104,179 +153,186 @@ class BlockAccessList(CamelModel): default_factory=list, description="List of account changes in the block" ) + rlp_fields: ClassVar[List[str]] = ["account_changes"] + + def to_list(self, signing: bool = False) -> List[Any]: + """ + Override to_list to return the account changes list directly. + + The BlockAccessList IS the list of account changes, not a container + that contains a list, per EIP-7928. + """ + # Return the list of accounts directly, not wrapped in another list + from ethereum_test_base_types.serialization import to_serializable_element + + return to_serializable_element(self.account_changes) + def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return self.model_dump(exclude_none=True) - def verify_against(self, actual_rlp: BytesConvertible) -> None: + def verify_against(self, actual_bal: "BlockAccessList") -> None: """ Verify that the actual BAL from the client matches this expected BAL. Args: - actual_rlp: RLP-encoded BAL from the client + actual_bal: The BlockAccessList model from the client Raises: Exception: If verification fails """ - # Decode the actual BAL - actual_bal = self._decode_rlp(actual_rlp) - - # Verify all expected accounts are present - for expected_account in self.account_changes: - actual_account = self._find_account(actual_bal, expected_account.address) - if actual_account is None: - raise Exception( - f"Expected account {expected_account.address} not found in actual BAL" - ) - - # Verify nonce changes - if expected_account.nonce_changes: - self._verify_nonce_changes( - expected_account.address, - expected_account.nonce_changes, - actual_account.get("nonce_changes", []), - ) - - # Verify balance changes - if expected_account.balance_changes: - self._verify_balance_changes( - expected_account.address, - expected_account.balance_changes, - actual_account.get("balance_changes", []), - ) - - # Similar for other change types... - - def _decode_rlp(self, rlp_data: BytesConvertible) -> Dict[str, Any]: - """Decode RLP data to dictionary.""" - decoded = rlp.decode(to_bytes(rlp_data)) - - account_changes = [] - for account_entry in decoded: - if len(account_entry) < 2: - continue + actual_accounts_by_addr = {acc.address: acc for acc in actual_bal.account_changes} + expected_accounts_by_addr = {acc.address: acc for acc in self.account_changes} - address = Address(account_entry[0]) - account_data = { - "address": address, - "storage_changes": account_entry[1] if len(account_entry) > 1 else [], - "storage_reads": account_entry[2] if len(account_entry) > 2 else [], - "balance_changes": account_entry[3] if len(account_entry) > 3 else [], - "nonce_changes": account_entry[4] if len(account_entry) > 4 else [], - "code_changes": account_entry[5] if len(account_entry) > 5 else [], - } - account_changes.append(account_data) - - return {"account_changes": account_changes} - - def _find_account(self, bal: Dict[str, Any], address: Address) -> Optional[Dict[str, Any]]: - """Find an account in the decoded BAL.""" - # Convert address to bytes for comparison - address_bytes = bytes(address) if hasattr(address, "__bytes__") else address - - for account in bal.get("account_changes", []): - account_addr = account.get("address") - # Convert to bytes if needed for comparison - if isinstance(account_addr, Address): - account_addr = bytes(account_addr) - if account_addr == address_bytes: - return account - return None - - def _verify_nonce_changes( - self, address: Address, expected: List[BalNonceChange], actual: List[Any] + # Check for missing accounts + missing_accounts = set(expected_accounts_by_addr.keys()) - set( + actual_accounts_by_addr.keys() + ) + if missing_accounts: + raise Exception( + "Expected accounts not found in actual BAL: " + f"{', '.join(str(a) for a in missing_accounts)}" + ) + + # Verify each expected account + for address, expected_account in expected_accounts_by_addr.items(): + actual_account = actual_accounts_by_addr[address] + + try: + self._compare_account_changes(expected_account, actual_account) + except AssertionError as e: + raise Exception(f"Account {address}: {str(e)}") from e + + def _compare_account_changes( + self, expected: BalAccountChange, actual: BalAccountChange ) -> None: - """Verify nonce changes match.""" - # Decode actual changes for better error reporting - actual_decoded = [] - for act_change in actual: - if len(act_change) >= 2: - tx_index = ( - int.from_bytes(act_change[0], "big") - if isinstance(act_change[0], bytes) and act_change[0] - else (0 if isinstance(act_change[0], bytes) else act_change[0]) - ) - post_nonce = ( - int.from_bytes(act_change[1], "big") - if isinstance(act_change[1], bytes) and act_change[1] - else (0 if isinstance(act_change[1], bytes) else act_change[1]) - ) - actual_decoded.append(f"tx_index={tx_index} post_nonce={post_nonce}") - - for exp_change in expected: - found = False - for act_change in actual: - # Each nonce change is [tx_index, post_nonce] - if len(act_change) >= 2: - # Convert bytes to int if needed - tx_index = ( - int.from_bytes(act_change[0], "big") - if isinstance(act_change[0], bytes) and act_change[0] - else (0 if isinstance(act_change[0], bytes) else act_change[0]) - ) - post_nonce = ( - int.from_bytes(act_change[1], "big") - if isinstance(act_change[1], bytes) and act_change[1] - else (0 if isinstance(act_change[1], bytes) else act_change[1]) + """ + Compare two BalAccountChange models with detailed error reporting. + + Only validates fields that were explicitly set in the expected model, + using model_fields_set to determine what was intentionally specified. + """ + # Only check fields that were explicitly set in the expected model + for field_name in expected.model_fields_set: + if field_name == "address": + continue # Already matched by account lookup + + expected_value = getattr(expected, field_name) + actual_value = getattr(actual, field_name) + + # If we explicitly set a field to None, verify it's None/empty + if expected_value is None: + if actual_value is not None and actual_value != []: + raise AssertionError( + f"Expected {field_name} to be empty/None but found: {actual_value}" ) - if tx_index == exp_change.tx_index and post_nonce == exp_change.post_nonce: - found = True - break - if not found: - raise Exception( - f"Account {address}: Nonce change mismatch\n" - f" Expected: tx_index={exp_change.tx_index} " - f"post_nonce={exp_change.post_nonce}\n" - f" Actual nonce changes: {actual_decoded}" - ) - - def _verify_balance_changes( - self, address: Address, expected: List[BalBalanceChange], actual: List[Any] - ) -> None: - """Verify balance changes match.""" - # Decode actual changes for better error reporting - actual_decoded = [] - for act_change in actual: - if len(act_change) >= 2: - tx_index = ( - int.from_bytes(act_change[0], "big") - if isinstance(act_change[0], bytes) and act_change[0] - else (0 if isinstance(act_change[0], bytes) else act_change[0]) - ) - post_balance = ( - int.from_bytes(act_change[1], "big") - if isinstance(act_change[1], bytes) and act_change[1] - else (0 if isinstance(act_change[1], bytes) else int(act_change[1])) - ) - actual_decoded.append(f"tx_index={tx_index} post_balance={post_balance}") - - for exp_change in expected: - found = False - for act_change in actual: - # Each balance change is [tx_index, post_balance] - if len(act_change) >= 2: - # Convert bytes to int if needed, handling empty bytes - tx_index = ( - int.from_bytes(act_change[0], "big") - if isinstance(act_change[0], bytes) and act_change[0] - else (0 if isinstance(act_change[0], bytes) else act_change[0]) + continue + + # If actual is ``None`` but we expected something, raise + if actual_value is None: + raise AssertionError(f"Expected {field_name} but found none") + + if field_name == "storage_reads": + # Convert to comparable format (both are lists of 32-byte values) + expected_set = {bytes(v) if hasattr(v, "__bytes__") else v for v in expected_value} + actual_set = {bytes(v) if hasattr(v, "__bytes__") else v for v in actual_value} + if expected_set != actual_set: + missing = expected_set - actual_set + extra = actual_set - expected_set + msg = "Storage reads mismatch." + if missing: + msg += f" Missing: { + [v.hex() if isinstance(v, bytes) else str(v) for v in missing] + }." + if extra: + msg += f" Extra: { + [v.hex() if isinstance(v, bytes) else str(v) for v in extra] + }." + raise AssertionError(msg) + + elif isinstance(expected_value, list): + # For lists of changes, use the model_dump approach for comparison + expected_data = [ + item.model_dump() if hasattr(item, "model_dump") else item + for item in expected_value + ] + actual_data = [ + item.model_dump() if hasattr(item, "model_dump") else item + for item in actual_value + ] + + if not self._compare_change_lists(field_name, expected_data, actual_data): + # The comparison method will raise with details + pass + + def _compare_change_lists(self, field_name: str, expected: List, actual: List) -> bool: + """Compare lists of change objects using set operations for better error messages.""" + if field_name == "storage_changes": + # Storage changes are nested (slot -> changes) + expected_by_slot = {slot["slot"]: slot["slot_changes"] for slot in expected} + actual_by_slot = {slot["slot"]: slot["slot_changes"] for slot in actual} + + missing_slots = set(expected_by_slot.keys()) - set(actual_by_slot.keys()) + if missing_slots: + raise AssertionError(f"Missing storage slots: {missing_slots}") + + for slot, exp_changes in expected_by_slot.items(): + act_changes = actual_by_slot.get(slot, []) + # Handle Hash/bytes for post_value comparison + exp_set = { + ( + c["tx_index"], + bytes(c["post_value"]) + if hasattr(c["post_value"], "__bytes__") + else c["post_value"], ) - # Balance is stored as bytes/int, need to compare properly - post_balance = ( - int.from_bytes(act_change[1], "big") - if isinstance(act_change[1], bytes) and act_change[1] - else (0 if isinstance(act_change[1], bytes) else int(act_change[1])) + for c in exp_changes + } + act_set = { + ( + c["tx_index"], + bytes(c["post_value"]) + if hasattr(c["post_value"], "__bytes__") + else c["post_value"], ) - if tx_index == exp_change.tx_index and post_balance == int( - exp_change.post_balance - ): - found = True - break - if not found: - raise Exception( - f"Account {address}: Balance change mismatch\n" - f" Expected: tx_index={exp_change.tx_index} " - f"post_balance={exp_change.post_balance}\n" - f" Actual balance changes: {actual_decoded}" - ) + for c in act_changes + } + + if exp_set != act_set: + missing = exp_set - act_set + extra = act_set - exp_set + msg = f"Slot {slot} changes mismatch." + if missing: + msg += f" Missing: {missing}." + if extra: + msg += f" Extra: {extra}." + raise AssertionError(msg) + else: + # Create comparable tuples for each change type + if field_name == "nonce_changes": + expected_set = {(c["tx_index"], c["post_nonce"]) for c in expected} + actual_set = {(c["tx_index"], c["post_nonce"]) for c in actual} + item_type = "nonce" + elif field_name == "balance_changes": + expected_set = {(c["tx_index"], int(c["post_balance"])) for c in expected} + actual_set = {(c["tx_index"], int(c["post_balance"])) for c in actual} + item_type = "balance" + elif field_name == "code_changes": + expected_set = {(c["tx_index"], bytes(c["new_code"])) for c in expected} + actual_set = {(c["tx_index"], bytes(c["new_code"])) for c in actual} + item_type = "code" + else: + raise ValueError("Unexpected type") + + if expected_set != actual_set: + missing = expected_set - actual_set + extra = actual_set - expected_set + msg = f"{item_type.capitalize()} changes mismatch." + if missing: + msg += f" Missing: {missing}." + if extra: + msg += f" Extra: {extra}." + raise AssertionError(msg) + + return True diff --git a/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py index a156ba3772a..3a3935af989 100644 --- a/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py @@ -53,9 +53,7 @@ def test_bal_nonce_changes( account_changes=[ BalAccountChange( address=alice, - nonce_changes=[ - BalNonceChange(tx_index=1, post_nonce=1) - ], + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], ), ] ), @@ -105,18 +103,14 @@ def test_bal_balance_changes( account_changes=[ BalAccountChange( address=alice, - nonce_changes=[ - BalNonceChange(tx_index=1, post_nonce=1) - ], + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], balance_changes=[ BalBalanceChange(tx_index=1, post_balance=alice_final_balance) ], ), BalAccountChange( address=bob, - balance_changes=[ - BalBalanceChange(tx_index=1, post_balance=100) - ], + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], ), # TODO: Validate coinbase ] @@ -155,11 +149,7 @@ def test_bal_storage_writes( storage_changes=[ BalStorageSlot( slot=0x01, - slot_changes=[ - BalStorageChange( - tx_index=1, post_value=0x42 - ) - ], + slot_changes=[BalStorageChange(tx_index=1, post_value=0x42)], ) ], ), From 6a4e6bb3c9ffc54a58b1b985d2f127bc26ccaa34 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 20 Aug 2025 18:52:53 -0600 Subject: [PATCH 16/35] chore(lint): Fix lint issues, assign method to Frontier to be overridden --- src/ethereum_test_forks/forks/forks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 1a4220318da..15c000e16f9 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -315,6 +315,11 @@ def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) -> """At genesis, header must not contain beacon chain requests.""" 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.""" + return False + @classmethod def engine_new_payload_version( cls, block_number: int = 0, timestamp: int = 0 From 9bb02f775e9fc8953c21b4df2b3e1db82e788fd8 Mon Sep 17 00:00:00 2001 From: fselmo Date: Thu, 21 Aug 2025 11:33:04 -0600 Subject: [PATCH 17/35] fix(tests,lint): Set up code changes test case; fix remaining lint issues --- .../test_block_access_lists.py | 73 ++++++++++++------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py index 3a3935af989..269d84eea7b 100644 --- a/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py @@ -8,11 +8,13 @@ Block, BlockchainTestFiller, Transaction, + compute_create_address, ) from ethereum_test_tools.vm.opcode import Opcodes as Op from ethereum_test_types.block_access_list import ( BalAccountChange, BalBalanceChange, + BalCodeChange, BalNonceChange, BalStorageChange, BalStorageSlot, @@ -87,7 +89,9 @@ def test_bal_balance_changes( ) block = Block(txs=[tx]) - alice_initial_balance = pre[alice].balance + alice_account = pre[alice] + assert alice_account is not None, "Alice account should exist" + alice_initial_balance = alice_account.balance # Account for both the value sent and gas cost (gas_price * gas_used) alice_final_balance = alice_initial_balance - 100 - (intrinsic_gas_cost * 1_000_000_000) @@ -202,17 +206,32 @@ def test_bal_code_changes( blockchain_test: BlockchainTestFiller, ): """Ensure BAL captures changes to account code.""" - deployed_code = Op.PUSH1(0x42) + Op.PUSH1(0x00) + Op.SSTORE + Op.STOP + runtime_code = Op.STOP + runtime_code_bytes = bytes(runtime_code) + + init_code = ( + Op.PUSH1(len(runtime_code_bytes)) # size = 1 + + Op.DUP1 # duplicate size for return + + Op.PUSH1(0x0C) # offset in init code where runtime code starts + + Op.PUSH1(0x00) # dest offset + + Op.CODECOPY # copy runtime code to memory + + Op.PUSH1(0x00) # memory offset for return + + Op.RETURN # return runtime code + + runtime_code # the actual runtime code to deploy + ) + init_code_bytes = bytes(init_code) - deployed_code_bytes = bytes(deployed_code) + # Factory contract that uses CREATE to deploy factory_code = ( - Op.PUSH32(deployed_code_bytes) # Contract code - + Op.PUSH1(0x00) # Memory offset - + Op.MSTORE # Store code in memory - + Op.PUSH1(len(deployed_code_bytes)) # Code size - + Op.PUSH1(0x00) # Memory offset - + Op.PUSH1(0x00) # Value to send - + Op.CREATE # CREATE opcode + # Push init code to memory + Op.PUSH32(init_code_bytes) + + Op.PUSH1(0x00) + + Op.MSTORE # Store at memory position 0 + # CREATE parameters: value, offset, size + + Op.PUSH1(len(init_code_bytes)) # size of init code + + Op.PUSH1(32 - len(init_code_bytes)) # offset in memory (account for padding) + + Op.PUSH1(0x00) # value = 0 (no ETH sent) + + Op.CREATE # Deploy the contract + Op.STOP ) @@ -227,29 +246,33 @@ def test_bal_code_changes( block = Block(txs=[tx]) - # The CREATE opcode will deploy to a deterministic address - # We'll need to calculate or determine what that address will be - # For now, we'll focus on the factory contract having a code change + created_contract = compute_create_address(address=factory_contract, nonce=1) + blockchain_test( pre=pre, blocks=[block], post={ alice: Account(nonce=1), - factory_contract: Account(), - # The newly created contract would be here but we'd need to calculate its address + factory_contract: Account(nonce=2), # incremented by CREATE to 2 + created_contract: Account( + code=runtime_code_bytes, + storage={}, + ), }, - # Note: This test might need adjustment based on how CREATE addresses are calculated - # and how code changes are tracked in the BAL expected_block_access_list=BlockAccessList( account_changes=[ - # { - # "address": alice, - # "nonce_changes": [{"tx_index": 0, "post_nonce": 1}], - # }, - # { - # "address": factory_contract, - # "code_changes": [{"tx_index": 0, "new_code": deployed_code}], - # }, + BalAccountChange( + address=alice, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + BalAccountChange( + address=factory_contract, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], + ), + BalAccountChange( + address=created_contract, + code_changes=[BalCodeChange(tx_index=1, new_code=runtime_code_bytes)], + ), ] ), ) From d9b2550b0a8953ce57375396c58a9c9ac751d58b Mon Sep 17 00:00:00 2001 From: fselmo Date: Thu, 21 Aug 2025 12:39:36 -0600 Subject: [PATCH 18/35] refactor(forks): move BAL tests to Amsterdam - chore(cleanup): Remove reference to told BlockAccessLists fork - refactor(fork): ``Amsterdam`` above old EOF fork. --- src/ethereum_test_forks/forks/forks.py | 32 ++++++------------- .../__init__.py | 0 .../checklist.md | 0 .../eip7928_block_level_access_lists/spec.py | 3 -- .../test_block_access_lists.py | 10 +++--- 5 files changed, 14 insertions(+), 31 deletions(-) rename tests/{osaka => amsterdam}/eip7928_block_level_access_lists/__init__.py (100%) rename tests/{osaka => amsterdam}/eip7928_block_level_access_lists/checklist.md (100%) rename tests/{osaka => amsterdam}/eip7928_block_level_access_lists/spec.py (93%) rename tests/{osaka => amsterdam}/eip7928_block_level_access_lists/test_block_access_lists.py (97%) diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 15c000e16f9..54211aedf0a 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -1603,11 +1603,6 @@ def engine_forkchoice_updated_version( class Osaka(Prague, solc_name="cancun"): """Osaka fork.""" - @classmethod - def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: - """Hash of the block access list is required starting from Osaka fork.""" - return True - # update some blob constants BLOB_CONSTANTS = { **Prague.BLOB_CONSTANTS, # same base constants as prague @@ -1818,6 +1813,15 @@ class BPO5(BPO4, bpo_fork=True): pass +class Amsterdam(Osaka): + """Amsterdam fork.""" + + @classmethod + def is_deployed(cls) -> bool: + """Return True if this fork is deployed.""" + return False + + class EOFv1(Prague, solc_name="cancun"): """EOF fork.""" @@ -1847,21 +1851,3 @@ def is_deployed(cls) -> bool: development. """ return False - - -class Amsterdam(Osaka): - """Amsterdam fork.""" - - @classmethod - def is_deployed(cls) -> bool: - """Return True if this fork is deployed.""" - return False - - -class BlockAccessLists(Prague): - """A development fork for Block Access Lists.""" - - @classmethod - def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: - """Hash of the block access list is required starting from this fork.""" - return True diff --git a/tests/osaka/eip7928_block_level_access_lists/__init__.py b/tests/amsterdam/eip7928_block_level_access_lists/__init__.py similarity index 100% rename from tests/osaka/eip7928_block_level_access_lists/__init__.py rename to tests/amsterdam/eip7928_block_level_access_lists/__init__.py diff --git a/tests/osaka/eip7928_block_level_access_lists/checklist.md b/tests/amsterdam/eip7928_block_level_access_lists/checklist.md similarity index 100% rename from tests/osaka/eip7928_block_level_access_lists/checklist.md rename to tests/amsterdam/eip7928_block_level_access_lists/checklist.md diff --git a/tests/osaka/eip7928_block_level_access_lists/spec.py b/tests/amsterdam/eip7928_block_level_access_lists/spec.py similarity index 93% rename from tests/osaka/eip7928_block_level_access_lists/spec.py rename to tests/amsterdam/eip7928_block_level_access_lists/spec.py index a91b00f8a22..56b4a954ad2 100644 --- a/tests/osaka/eip7928_block_level_access_lists/spec.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/spec.py @@ -2,9 +2,6 @@ from dataclasses import dataclass -# ACTIVATION_FORK_NAME = "BlockAccessLists" -# """The fork name for EIP-7928 activation.""" - @dataclass(frozen=True) class ReferenceSpec: diff --git a/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py similarity index 97% rename from tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py rename to tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 269d84eea7b..d1b708e140e 100644 --- a/tests/osaka/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -27,7 +27,7 @@ REFERENCE_SPEC_VERSION = ref_spec_7928.version -@pytest.mark.valid_from("Osaka") +@pytest.mark.valid_from("Amsterdam") def test_bal_nonce_changes( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -62,7 +62,7 @@ def test_bal_nonce_changes( ) -@pytest.mark.valid_from("Osaka") +@pytest.mark.valid_from("Amsterdam") def test_bal_balance_changes( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -122,7 +122,7 @@ def test_bal_balance_changes( ) -@pytest.mark.valid_from("Osaka") +@pytest.mark.valid_from("Amsterdam") def test_bal_storage_writes( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -162,7 +162,7 @@ def test_bal_storage_writes( ) -@pytest.mark.valid_from("Osaka") +@pytest.mark.valid_from("Amsterdam") def test_bal_storage_reads( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -200,7 +200,7 @@ def test_bal_storage_reads( ) -@pytest.mark.valid_from("Osaka") +@pytest.mark.valid_from("Amsterdam") def test_bal_code_changes( pre: Alloc, blockchain_test: BlockchainTestFiller, From eb66f677b0dad51cc0961f90f61606d36f01f6e6 Mon Sep 17 00:00:00 2001 From: fselmo Date: Thu, 21 Aug 2025 13:30:06 -0600 Subject: [PATCH 19/35] chore: Fix CI lint issues not present locally - Update some docstrings to remove AI excessive verbosity and add CodeChanges case to example. --- src/ethereum_test_types/block_access_list.py | 23 ++++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/ethereum_test_types/block_access_list.py b/src/ethereum_test_types/block_access_list.py index ac8db72bba4..f0335cd6b4e 100644 --- a/src/ethereum_test_types/block_access_list.py +++ b/src/ethereum_test_types/block_access_list.py @@ -17,6 +17,7 @@ RLPSerializable, StorageKey, ) +from ethereum_test_base_types.serialization import to_serializable_element class BalNonceChange(CamelModel, RLPSerializable): @@ -123,9 +124,6 @@ class BlockAccessList(CamelModel, RLPSerializable): """ Expected Block Access List for verification. - This follows the same pattern as AccessList and AuthorizationTuple - - a simple data class that can be used directly in tests. - Example: expected_block_access_list = BlockAccessList( account_changes=[ @@ -142,7 +140,10 @@ class BlockAccessList(CamelModel, RLPSerializable): address=bob, balance_changes=[ BalBalanceChange(tx_index=0, post_balance=100) - ] + ], + code_changes=[ + BalCodeChange(tx_index=0, new_code=b"0x1234") + ], ), ] ) @@ -163,8 +164,6 @@ def to_list(self, signing: bool = False) -> List[Any]: that contains a list, per EIP-7928. """ # Return the list of accounts directly, not wrapped in another list - from ethereum_test_base_types.serialization import to_serializable_element - return to_serializable_element(self.account_changes) def to_dict(self) -> Dict[str, Any]: @@ -242,13 +241,13 @@ def _compare_account_changes( extra = actual_set - expected_set msg = "Storage reads mismatch." if missing: - msg += f" Missing: { - [v.hex() if isinstance(v, bytes) else str(v) for v in missing] - }." + missing_str = [ + v.hex() if isinstance(v, bytes) else str(v) for v in missing + ] + msg += f" Missing: {missing_str}." if extra: - msg += f" Extra: { - [v.hex() if isinstance(v, bytes) else str(v) for v in extra] - }." + extra_str = [v.hex() if isinstance(v, bytes) else str(v) for v in extra] + msg += f" Extra: {extra_str}." raise AssertionError(msg) elif isinstance(expected_value, list): From 1fe330b0e455d1fbce5c66d01100b86a83350f80 Mon Sep 17 00:00:00 2001 From: fselmo Date: Fri, 22 Aug 2025 09:33:03 -0600 Subject: [PATCH 20/35] refactor(bal): Apply comments from first pass at PR - Use ``Number`` instead of ``int`` where appropriate - Use ``default_factory=list`` without ``Optional`` and ``None`` where appropriate --- src/ethereum_test_types/block_access_list.py | 58 ++++++-------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/src/ethereum_test_types/block_access_list.py b/src/ethereum_test_types/block_access_list.py index f0335cd6b4e..f838bcc830c 100644 --- a/src/ethereum_test_types/block_access_list.py +++ b/src/ethereum_test_types/block_access_list.py @@ -5,7 +5,7 @@ these are simple data classes that can be composed together. """ -from typing import Any, ClassVar, Dict, List, Optional +from typing import Any, ClassVar, Dict, List from pydantic import Field @@ -14,6 +14,7 @@ Bytes, CamelModel, HexNumber, + Number, RLPSerializable, StorageKey, ) @@ -23,8 +24,8 @@ class BalNonceChange(CamelModel, RLPSerializable): """Represents a nonce change in the block access list.""" - tx_index: int = Field(..., description="Transaction index where the change occurred") - post_nonce: int = Field(..., description="Nonce value after the transaction") + tx_index: Number = Field(..., description="Transaction index where the change occurred") + post_nonce: Number = Field(..., description="Nonce value after the transaction") rlp_fields: ClassVar[List[str]] = ["tx_index", "post_nonce"] @@ -32,7 +33,7 @@ class BalNonceChange(CamelModel, RLPSerializable): class BalBalanceChange(CamelModel, RLPSerializable): """Represents a balance change in the block access list.""" - tx_index: int = Field(..., description="Transaction index where the change occurred") + tx_index: Number = Field(..., description="Transaction index where the change occurred") post_balance: HexNumber = Field(..., description="Balance after the transaction") rlp_fields: ClassVar[List[str]] = ["tx_index", "post_balance"] @@ -41,7 +42,7 @@ class BalBalanceChange(CamelModel, RLPSerializable): class BalCodeChange(CamelModel, RLPSerializable): """Represents a code change in the block access list.""" - tx_index: int = Field(..., description="Transaction index where the change occurred") + tx_index: Number = Field(..., description="Transaction index where the change occurred") new_code: Bytes = Field(..., description="New code bytes") rlp_fields: ClassVar[List[str]] = ["tx_index", "new_code"] @@ -50,7 +51,7 @@ class BalCodeChange(CamelModel, RLPSerializable): class BalStorageChange(CamelModel, RLPSerializable): """Represents a change to a specific storage slot.""" - tx_index: int = Field(..., description="Transaction index where the change occurred") + tx_index: Number = Field(..., description="Transaction index where the change occurred") post_value: StorageKey = Field(..., description="Value after the transaction") rlp_fields: ClassVar[List[str]] = ["tx_index", "post_value"] @@ -71,18 +72,20 @@ class BalAccountChange(CamelModel, RLPSerializable): """Represents all changes to a specific account in a block.""" address: Address = Field(..., description="Account address") - nonce_changes: Optional[List[BalNonceChange]] = Field( - None, description="List of nonce changes" + nonce_changes: List[BalNonceChange] = Field( + default_factory=list, description="List of nonce changes" ) - balance_changes: Optional[List[BalBalanceChange]] = Field( - None, description="List of balance changes" + balance_changes: List[BalBalanceChange] = Field( + default_factory=list, description="List of balance changes" ) - code_changes: Optional[List[BalCodeChange]] = Field(None, description="List of code changes") - storage_changes: Optional[List[BalStorageSlot]] = Field( - None, description="List of storage changes" + code_changes: List[BalCodeChange] = Field( + default_factory=list, description="List of code changes" ) - storage_reads: Optional[List[StorageKey]] = Field( - None, description="List of storage slots that were read" + storage_changes: List[BalStorageSlot] = Field( + default_factory=list, description="List of storage changes" + ) + storage_reads: List[StorageKey] = Field( + default_factory=list, description="List of storage slots that were read" ) rlp_fields: ClassVar[List[str]] = [ @@ -94,31 +97,6 @@ class BalAccountChange(CamelModel, RLPSerializable): "code_changes", ] - def to_list(self, signing: bool = False) -> List[Any]: - """ - Override to handle None list fields properly. - None list fields should serialize as empty lists, not empty bytes. - """ - from ethereum_test_base_types.serialization import to_serializable_element - - result: list[Any] = [] - for field_name in self.rlp_fields: - value = getattr(self, field_name) - - # Special handling for None list fields - they should be empty lists - if value is None and field_name in [ - "storage_changes", - "storage_reads", - "balance_changes", - "nonce_changes", - "code_changes", - ]: - result.append([]) - else: - result.append(to_serializable_element(value)) - - return result - class BlockAccessList(CamelModel, RLPSerializable): """ From 07f4013906a693c2f3d1eb8da01f39e4b9bc7353 Mon Sep 17 00:00:00 2001 From: fselmo Date: Fri, 22 Aug 2025 11:11:18 -0600 Subject: [PATCH 21/35] refactor(bal): Split BAL into two classes - Use a root model class for the t8n BAL model. This lets us stick closer to the EIP definition where the BAL itself is a list. - The class doesn't need to be `RLPSerializable`, just have simple instructions to serialize its elements (`rlp()`). - Create a `BlockAccessListExpectation` class to represent the expected BAL for a given block. This class can describe different expectations for the test cases, such as defining mutations to be used for invalid tests. --- src/ethereum_clis/types.py | 9 +- src/ethereum_test_specs/blockchain.py | 14 +--- src/ethereum_test_types/__init__.py | 2 + src/ethereum_test_types/block_access_list.py | 83 ++++++++++--------- src/pytest_plugins/eels_resolutions.json | 2 +- .../test_block_access_lists.py | 12 +-- 6 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/ethereum_clis/types.py b/src/ethereum_clis/types.py index 9f7ea8ddad3..8c2d34a0781 100644 --- a/src/ethereum_clis/types.py +++ b/src/ethereum_clis/types.py @@ -12,8 +12,13 @@ TransactionException, UndefinedException, ) -from ethereum_test_types import Alloc, Environment, Transaction, TransactionReceipt -from ethereum_test_types.block_access_list import BlockAccessList +from ethereum_test_types import ( + Alloc, + BlockAccessList, + Environment, + Transaction, + TransactionReceipt, +) class TransactionExceptionWithMessage(ExceptionWithMessage[TransactionException]): diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index e2461e2b01a..98ce0a546b4 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -52,7 +52,7 @@ from ethereum_test_fixtures.common import FixtureBlobSchedule from ethereum_test_forks import Fork from ethereum_test_types import Alloc, Environment, Removable, Requests, Transaction, Withdrawal -from ethereum_test_types.block_access_list import BlockAccessList +from ethereum_test_types.block_access_list import BlockAccessList, BlockAccessListExpectation from .base import BaseTest, OpMode, verify_result from .debugging import print_traces @@ -426,11 +426,11 @@ class BlockchainTest(BaseTest): Exclude the post state from the fixture output. In this case, the state verification is only performed based on the state root. """ - expected_block_access_list: Any | None = None # Will be BAL type + expected_block_access_list: BlockAccessListExpectation | None = None """ Expected block access list for verification. If set, verifies that the block access list returned by the client matches expectations. - Use the BAL builder API to construct expectations. + Use BlockAccessListExpectation to define partial validation expectations. """ supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [ @@ -691,7 +691,7 @@ def verify_post_state(self, t8n, t8n_state: Alloc, expected_state: Alloc | None raise e def verify_block_access_list( - self, actual_bal: BlockAccessList | None, expected_bal: Any | None + self, actual_bal: BlockAccessList | None, expected_bal: BlockAccessListExpectation | None ): """ Verify that the actual block access list matches expectations. @@ -704,15 +704,9 @@ def verify_block_access_list( if expected_bal is None: return - if not isinstance(expected_bal, BlockAccessList): - raise Exception( - f"Invalid expected_bal type: {type(expected_bal)}. Use BlockAccessList." - ) - if actual_bal is None: raise Exception("Expected block access list but got none.") - # Verify using the BlockAccessList's verification method try: expected_bal.verify_against(actual_bal) except Exception as e: diff --git a/src/ethereum_test_types/__init__.py b/src/ethereum_test_types/__init__.py index 7a0248bd68d..3fac2dd37d9 100644 --- a/src/ethereum_test_types/__init__.py +++ b/src/ethereum_test_types/__init__.py @@ -10,6 +10,7 @@ BalStorageChange, BalStorageSlot, BlockAccessList, + BlockAccessListExpectation, ) from .block_types import ( Environment, @@ -53,6 +54,7 @@ "BalStorageSlot", "Blob", "BlockAccessList", + "BlockAccessListExpectation", "ChainConfig", "ChainConfigDefaults", "ConsolidationRequest", diff --git a/src/ethereum_test_types/block_access_list.py b/src/ethereum_test_types/block_access_list.py index f838bcc830c..38ac288b6e6 100644 --- a/src/ethereum_test_types/block_access_list.py +++ b/src/ethereum_test_types/block_access_list.py @@ -5,14 +5,16 @@ these are simple data classes that can be composed together. """ -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, List +import ethereum_rlp as eth_rlp from pydantic import Field from ethereum_test_base_types import ( Address, Bytes, CamelModel, + EthereumTestRootModel, HexNumber, Number, RLPSerializable, @@ -98,56 +100,61 @@ class BalAccountChange(CamelModel, RLPSerializable): ] -class BlockAccessList(CamelModel, RLPSerializable): +class BlockAccessList(EthereumTestRootModel[List[BalAccountChange]]): """ - Expected Block Access List for verification. + Block Access List for t8n tool communication and fixtures. + + This model represents the BAL exactly as defined in EIP-7928 - it is itself a list + of account changes (root model), not a container. Used for: + - Communication with t8n tools + - Fixture generation + - RLP encoding for hash verification Example: - expected_block_access_list = BlockAccessList( + bal = BlockAccessList([ + BalAccountChange(address=alice, nonce_changes=[...]), + BalAccountChange(address=bob, balance_changes=[...]) + ]) + + """ + + root: List[BalAccountChange] = Field(default_factory=list) + + def to_list(self) -> List[Any]: + """Return the list for RLP encoding per EIP-7928.""" + return to_serializable_element(self.root) + + def rlp(self) -> Bytes: + """Return the RLP encoded block access list for hash verification.""" + return Bytes(eth_rlp.encode(self.to_list())) + + +class BlockAccessListExpectation(CamelModel): + """ + Block Access List expectation model for test writing. + + This model is used to define expected BAL values in tests. It supports: + - Partial validation (only checks explicitly set fields) + - Convenient test syntax with named parameters + - Verification against actual BAL from t8n + + Example: + # In test definition + expected_block_access_list = BlockAccessListExpectation( account_changes=[ BalAccountChange( address=alice, - nonce_changes=[ - BalNonceChange(tx_index=0, post_nonce=1) - ], - balance_changes=[ - BalBalanceChange(tx_index=0, post_balance=9000) - ] - ), - BalAccountChange( - address=bob, - balance_changes=[ - BalBalanceChange(tx_index=0, post_balance=100) - ], - code_changes=[ - BalCodeChange(tx_index=0, new_code=b"0x1234") - ], - ), + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)] + ) ] ) """ account_changes: List[BalAccountChange] = Field( - default_factory=list, description="List of account changes in the block" + default_factory=list, description="Expected account changes to verify" ) - rlp_fields: ClassVar[List[str]] = ["account_changes"] - - def to_list(self, signing: bool = False) -> List[Any]: - """ - Override to_list to return the account changes list directly. - - The BlockAccessList IS the list of account changes, not a container - that contains a list, per EIP-7928. - """ - # Return the list of accounts directly, not wrapped in another list - return to_serializable_element(self.account_changes) - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary for serialization.""" - return self.model_dump(exclude_none=True) - def verify_against(self, actual_bal: "BlockAccessList") -> None: """ Verify that the actual BAL from the client matches this expected BAL. @@ -159,7 +166,7 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None: Exception: If verification fails """ - actual_accounts_by_addr = {acc.address: acc for acc in actual_bal.account_changes} + actual_accounts_by_addr = {acc.address: acc for acc in actual_bal.root} expected_accounts_by_addr = {acc.address: acc for acc in self.account_changes} # Check for missing accounts diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index 03d28ae5ced..58033a11327 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -55,6 +55,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "59ff2946ca30879079b2ef2b601ae09106ca08f7" + "commit": "f4c49309e90e74b51d043b06c045735b54e997b0" } } 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 d1b708e140e..e63d8c1cdc5 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 @@ -18,7 +18,7 @@ BalNonceChange, BalStorageChange, BalStorageSlot, - BlockAccessList, + BlockAccessListExpectation, ) from .spec import ref_spec_7928 @@ -51,7 +51,7 @@ def test_bal_nonce_changes( alice: Account(nonce=1), bob: Account(balance=100), }, - expected_block_access_list=BlockAccessList( + expected_block_access_list=BlockAccessListExpectation( account_changes=[ BalAccountChange( address=alice, @@ -103,7 +103,7 @@ def test_bal_balance_changes( alice: Account(nonce=1, balance=alice_final_balance), bob: Account(balance=100), }, - expected_block_access_list=BlockAccessList( + expected_block_access_list=BlockAccessListExpectation( account_changes=[ BalAccountChange( address=alice, @@ -146,7 +146,7 @@ def test_bal_storage_writes( alice: Account(nonce=1), storage_contract: Account(storage={0x01: 0x42}), }, - expected_block_access_list=BlockAccessList( + expected_block_access_list=BlockAccessListExpectation( account_changes=[ BalAccountChange( address=storage_contract, @@ -189,7 +189,7 @@ def test_bal_storage_reads( alice: Account(nonce=1), storage_contract: Account(storage={0x01: 0x42}), }, - expected_block_access_list=BlockAccessList( + expected_block_access_list=BlockAccessListExpectation( account_changes=[ BalAccountChange( address=storage_contract, @@ -259,7 +259,7 @@ def test_bal_code_changes( storage={}, ), }, - expected_block_access_list=BlockAccessList( + expected_block_access_list=BlockAccessListExpectation( account_changes=[ BalAccountChange( address=alice, From 89f38b2d98ef833d7b824a56bdd8b4d1f56503c8 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 26 Aug 2025 11:56:42 -0600 Subject: [PATCH 22/35] refactor(tests): Rename checklist.md -> test_cases.md for BAL test case tracking --- .../{checklist.md => test_cases.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/amsterdam/eip7928_block_level_access_lists/{checklist.md => test_cases.md} (100%) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/checklist.md b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md similarity index 100% rename from tests/amsterdam/eip7928_block_level_access_lists/checklist.md rename to tests/amsterdam/eip7928_block_level_access_lists/test_cases.md From 9eb95ab96e0929bdd5785ef0627b1fd860bde595 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 26 Aug 2025 16:42:29 -0600 Subject: [PATCH 23/35] feat(tests,tools): [WIP] initial invalid BAL tests --- src/ethereum_test_forks/forks/forks.py | 5 + src/ethereum_test_specs/blockchain.py | 38 +- .../__init__.py} | 84 ++- .../block_access_list/modifiers.py | 387 +++++++++++++ .../test_block_access_lists.py | 8 +- .../test_block_access_lists_invalid.py | 545 ++++++++++++++++++ 6 files changed, 1053 insertions(+), 14 deletions(-) rename src/ethereum_test_types/{block_access_list.py => block_access_list/__init__.py} (83%) create mode 100644 src/ethereum_test_types/block_access_list/modifiers.py create mode 100644 tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 54211aedf0a..bbfefb4aa22 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -1816,6 +1816,11 @@ 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).""" + return block_number > 0 # Not required in genesis block + @classmethod def is_deployed(cls) -> bool: """Return True if this fork is deployed.""" diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 98ce0a546b4..ae06195c29a 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -208,6 +208,10 @@ class Block(Header): An RLP modifying header which values would be used to override the ones returned by the `ethereum_clis.TransitionTool`. """ + expected_block_access_list: BlockAccessListExpectation | None = None + """ + If set, the block access list will be verified and potentially corrupted for invalid tests. + """ exception: BLOCK_EXCEPTION_TYPE = None """ If set, the block is expected to be rejected by the client. @@ -624,6 +628,14 @@ def generate_block_data( header = block.rlp_modifier.apply(header) header.fork = fork # Deleted during `apply` because `exclude=True` + # Process block access list - apply transformer if present for invalid tests + bal = transition_tool_output.result.block_access_list + if block.expected_block_access_list is not None and bal is not None: + # Use to_fixture_bal to validate and potentially transform the BAL + bal = block.expected_block_access_list.to_fixture_bal(bal) + # Don't update the header hash - leave it as the hash of the correct BAL + # This creates a mismatch that should cause the block to be rejected + built_block = BuiltBlock( header=header, alloc=transition_tool_output.alloc, @@ -636,7 +648,7 @@ def generate_block_data( expected_exception=block.exception, engine_api_error_code=block.engine_api_error_code, fork=fork, - block_access_list=transition_tool_output.result.block_access_list, + block_access_list=bal, ) try: @@ -648,14 +660,20 @@ def generate_block_data( and block.rlp_modifier is None and block.requests is None and not block.skip_exception_verification + and not ( + block.expected_block_access_list is not None + and block.expected_block_access_list.modifier is not None + ) ): # Only verify block level exception if: - # - No transaction exception was raised, because these are not reported as block - # exceptions. - # - No RLP modifier was specified, because the modifier is what normally - # produces the block exception. - # - No requests were specified, because modified requests are also what normally - # produces the block exception. + # - No transaction exception was raised, because these are not + # reported as block exceptions. + # - No RLP modifier was specified, because the modifier is what + # normally produces the block exception. + # - No requests were specified, because modified requests are also + # what normally produces the block exception. + # - No BAL modifier was specified, because modified BAL also + # produces block exceptions. built_block.verify_block_exception( transition_tool_exceptions_reliable=t8n.exception_mapper.reliable, ) @@ -740,11 +758,7 @@ def make_fixture( ) fixture_blocks.append(built_block.get_fixture_block()) - # Verify block access list if expected - if self.expected_block_access_list is not None: - self.verify_block_access_list( - built_block.block_access_list, self.expected_block_access_list - ) + # BAL verification already done in to_fixture_bal() if expected_block_access_list set if block.exception is None: # Update env, alloc and last block hash for the next block. diff --git a/src/ethereum_test_types/block_access_list.py b/src/ethereum_test_types/block_access_list/__init__.py similarity index 83% rename from src/ethereum_test_types/block_access_list.py rename to src/ethereum_test_types/block_access_list/__init__.py index 38ac288b6e6..597012842e7 100644 --- a/src/ethereum_test_types/block_access_list.py +++ b/src/ethereum_test_types/block_access_list/__init__.py @@ -5,7 +5,7 @@ these are simple data classes that can be composed together. """ -from typing import Any, ClassVar, List +from typing import Any, Callable, ClassVar, List, Optional import ethereum_rlp as eth_rlp from pydantic import Field @@ -23,6 +23,20 @@ from ethereum_test_base_types.serialization import to_serializable_element +def compose( + *modifiers: Callable[["BlockAccessList"], "BlockAccessList"], +) -> Callable[["BlockAccessList"], "BlockAccessList"]: + """Compose multiple modifiers into a single modifier.""" + + def composed(bal: BlockAccessList) -> BlockAccessList: + result = bal + for modifier in modifiers: + result = modifier(result) + return result + + return composed + + class BalNonceChange(CamelModel, RLPSerializable): """Represents a nonce change in the block access list.""" @@ -155,6 +169,60 @@ class BlockAccessListExpectation(CamelModel): default_factory=list, description="Expected account changes to verify" ) + modifier: Optional[Callable[["BlockAccessList"], "BlockAccessList"]] = Field( + None, + exclude=True, + description="Optional modifier to modify the BAL for invalid tests", + ) + + def modify( + self, *modifiers: Callable[["BlockAccessList"], "BlockAccessList"] + ) -> "BlockAccessListExpectation": + """ + Create a new expectation with a modifier for invalid test cases. + + Args: + modifiers: One or more functions that take and return a BlockAccessList + + Returns: + A new BlockAccessListExpectation instance with the modifiers applied + + Example: + from ethereum_test_types.block_access_list.modifiers import remove_nonces + + expectation = BlockAccessListExpectation( + account_changes=[...] + ).modify(remove_nonces(alice)) + + """ + new_instance = self.model_copy(deep=True) + new_instance.modifier = compose(*modifiers) + return new_instance + + def to_fixture_bal(self, t8n_bal: "BlockAccessList") -> "BlockAccessList": + """ + Convert t8n BAL to fixture BAL, optionally applying transformations. + + 1. First validates expectations are met (if any) + 2. Then applies modifier if specified (for invalid tests) + + Args: + t8n_bal: The BlockAccessList from t8n tool + + Returns: + The potentially transformed BlockAccessList for the fixture + + """ + # Only validate if we have expectations + if self.account_changes: + self.verify_against(t8n_bal) + + # Apply modifier if present (for invalid tests) + if self.modifier: + return self.modifier(t8n_bal) + + return t8n_bal + def verify_against(self, actual_bal: "BlockAccessList") -> None: """ Verify that the actual BAL from the client matches this expected BAL. @@ -320,3 +388,17 @@ def _compare_change_lists(self, field_name: str, expected: List, actual: List) - raise AssertionError(msg) return True + + +__all__ = [ + # Core models + "BlockAccessList", + "BlockAccessListExpectation", + # Change types + "BalAccountChange", + "BalNonceChange", + "BalBalanceChange", + "BalCodeChange", + "BalStorageChange", + "BalStorageSlot", +] diff --git a/src/ethereum_test_types/block_access_list/modifiers.py b/src/ethereum_test_types/block_access_list/modifiers.py new file mode 100644 index 00000000000..718ed50814c --- /dev/null +++ b/src/ethereum_test_types/block_access_list/modifiers.py @@ -0,0 +1,387 @@ +""" +BAL modifier functions for invalid test cases. + +This module provides modifier functions that can be used to modify Block Access Lists +in various ways for testing invalid block scenarios. They are composable and can be +combined to create complex modifications. +""" + +from typing import Callable, List + +from ethereum_test_base_types import Address, Number + +from .. import BalCodeChange +from . import ( + BalAccountChange, + BalBalanceChange, + BalNonceChange, + BalStorageChange, + BlockAccessList, +) + + +def remove_accounts(*addresses: Address) -> Callable[[BlockAccessList], BlockAccessList]: + """Remove entire account entries from the BAL.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address not in addresses: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def remove_nonces(*addresses: Address) -> Callable[[BlockAccessList], BlockAccessList]: + """Remove nonce changes from specified accounts.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address in addresses: + # Create a copy without nonce changes + new_account = account_change.model_copy(deep=True) + new_account.nonce_changes = [] + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def remove_balances(*addresses: Address) -> Callable[[BlockAccessList], BlockAccessList]: + """Remove balance changes from specified accounts.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address in addresses: + # Create a copy without balance changes + new_account = account_change.model_copy(deep=True) + new_account.balance_changes = [] + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def remove_storage(*addresses: Address) -> Callable[[BlockAccessList], BlockAccessList]: + """Remove storage changes from specified accounts.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address in addresses: + # Create a copy without storage changes + new_account = account_change.model_copy(deep=True) + new_account.storage_changes = [] + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def remove_storage_reads(*addresses: Address) -> Callable[[BlockAccessList], BlockAccessList]: + """Remove storage reads from specified accounts.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address in addresses: + # Create a copy without storage reads + new_account = account_change.model_copy(deep=True) + new_account.storage_reads = [] + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def remove_code(*addresses: Address) -> Callable[[BlockAccessList], BlockAccessList]: + """Remove code changes from specified accounts.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address in addresses: + # Create a copy without code changes + new_account = account_change.model_copy(deep=True) + new_account.code_changes = [] + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def modify_nonce( + address: Address, tx_index: int, nonce: int +) -> Callable[[BlockAccessList], BlockAccessList]: + """Set an incorrect nonce value for a specific account and transaction.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address == address: + new_account = account_change.model_copy(deep=True) + # Find and modify the specific nonce change + for i, nonce_change in enumerate(new_account.nonce_changes or []): + if nonce_change.tx_index == tx_index: + new_account.nonce_changes[i] = BalNonceChange( + tx_index=tx_index, post_nonce=nonce + ) + break + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def modify_balance( + address: Address, tx_index: int, balance: int +) -> Callable[[BlockAccessList], BlockAccessList]: + """Set an incorrect balance value for a specific account and transaction.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address == address: + new_account = account_change.model_copy(deep=True) + # Find and modify the specific balance change + if new_account.balance_changes: + for i, balance_change in enumerate(new_account.balance_changes): + if balance_change.tx_index == tx_index: + # Create new balance change with wrong value + new_account.balance_changes[i] = BalBalanceChange( + tx_index=tx_index, post_balance=balance + ) + break + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def modify_storage( + address: Address, tx_index: int, slot: int, value: int +) -> Callable[[BlockAccessList], BlockAccessList]: + """Set an incorrect storage value for a specific account, transaction, and slot.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address == address: + new_account = account_change.model_copy(deep=True) + # Find and modify the specific storage change (nested structure) + if new_account.storage_changes: + for storage_slot in new_account.storage_changes: + if storage_slot.slot == slot: + for j, change in enumerate(storage_slot.slot_changes): + if change.tx_index == tx_index: + # Create new storage change with wrong value + storage_slot.slot_changes[j] = BalStorageChange( + tx_index=tx_index, post_value=value + ) + break + break + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def modify_code( + address: Address, tx_index: int, code: bytes +) -> Callable[[BlockAccessList], BlockAccessList]: + """Set an incorrect code value for a specific account and transaction.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address == address: + new_account = account_change.model_copy(deep=True) + # Find and modify the specific code change + if new_account.code_changes: + for i, code_change in enumerate(new_account.code_changes): + if code_change.tx_index == tx_index: + # Create new code change with wrong value + new_account.code_changes[i] = BalCodeChange( + tx_index=tx_index, post_code=code + ) + break + new_root.append(new_account) + else: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def swap_tx_indices(tx1: int, tx2: int) -> Callable[[BlockAccessList], BlockAccessList]: + """Swap transaction indices throughout the BAL, modifying tx ordering.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + new_account = account_change.model_copy(deep=True) + + # Swap in nonce changes + if new_account.nonce_changes: + for nonce_change in new_account.nonce_changes: + if nonce_change.tx_index == tx1: + nonce_change.tx_index = Number(tx2) + elif nonce_change.tx_index == tx2: + nonce_change.tx_index = Number(tx1) + + # Swap in balance changes + if new_account.balance_changes: + for balance_change in new_account.balance_changes: + if balance_change.tx_index == tx1: + balance_change.tx_index = Number(tx2) + elif balance_change.tx_index == tx2: + balance_change.tx_index = Number(tx1) + + # Swap in storage changes (nested structure) + if new_account.storage_changes: + for storage_slot in new_account.storage_changes: + for storage_change in storage_slot.slot_changes: + if storage_change.tx_index == tx1: + storage_change.tx_index = Number(tx2) + elif storage_change.tx_index == tx2: + storage_change.tx_index = Number(tx1) + + # Note: storage_reads is just a list of StorageKey, no tx_index to swap + + # Swap in code changes + if new_account.code_changes: + for code_change in new_account.code_changes: + if code_change.tx_index == tx1: + code_change.tx_index = Number(tx2) + elif code_change.tx_index == tx2: + code_change.tx_index = Number(tx1) + + new_root.append(new_account) + + return BlockAccessList(root=new_root) + + return transform + + +def append_account( + account_change: BalAccountChange, +) -> Callable[[BlockAccessList], BlockAccessList]: + """Append an account to account changes.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = list(bal.root) + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +def duplicate_account(address: Address) -> Callable[[BlockAccessList], BlockAccessList]: + """Duplicate an account entry in the BAL.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + new_root.append(account_change) + if account_change.address == address: + # Add duplicate immediately after + new_root.append(account_change.model_copy(deep=True)) + return BlockAccessList(root=new_root) + + return transform + + +def reverse_accounts() -> Callable[[BlockAccessList], BlockAccessList]: + """Reverse the order of accounts in the BAL.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + return BlockAccessList(root=list(reversed(bal.root))) + + return transform + + +def sort_accounts_by_address() -> Callable[[BlockAccessList], BlockAccessList]: + """Sort accounts by address (may modify expected ordering).""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + sorted_root = sorted(bal.root, key=lambda x: x.address) + return BlockAccessList(root=sorted_root) + + return transform + + +def reorder_accounts(indices: List[int]) -> Callable[[BlockAccessList], BlockAccessList]: + """Reorder accounts according to the provided index list.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + if len(indices) != len(bal.root): + raise ValueError("Index list length must match number of accounts") + new_root = [bal.root[i] for i in indices] + return BlockAccessList(root=new_root) + + return transform + + +def clear_all() -> Callable[[BlockAccessList], BlockAccessList]: + """Return an empty BAL.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + return BlockAccessList(root=[]) + + return transform + + +def keep_only(*addresses: Address) -> Callable[[BlockAccessList], BlockAccessList]: + """Keep only the specified accounts, removing all others.""" + + def transform(bal: BlockAccessList) -> BlockAccessList: + new_root = [] + for account_change in bal.root: + if account_change.address in addresses: + new_root.append(account_change) + return BlockAccessList(root=new_root) + + return transform + + +__all__ = [ + # Core functions + # Account-level modifiers + "remove_accounts", + "append_account", + "duplicate_account", + "reverse_accounts", + "keep_only", + # Field-level modifiers + "remove_nonces", + "remove_balances", + "remove_storage", + "remove_storage_reads", + "remove_code", + # Value modifiers + "modify_nonce", + "modify_balance", + "modify_storage", + "modify_code", + # Transaction index modifiers + "swap_tx_indices", +] 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 e63d8c1cdc5..584a6e729ed 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 @@ -7,6 +7,7 @@ Alloc, Block, BlockchainTestFiller, + Storage, Transaction, compute_create_address, ) @@ -128,7 +129,12 @@ def test_bal_storage_writes( blockchain_test: BlockchainTestFiller, ): """Ensure BAL captures storage writes.""" - storage_contract = pre.deploy_contract(code=Op.SSTORE(0x01, 0x42) + Op.STOP) + storage = Storage({0x01: 0}) # type: ignore + storage_contract = pre.deploy_contract( + code=Op.SSTORE(0x01, 0x42) + Op.STOP, + # pre-fill with canary value to detect writes in post-state + storage=storage.canary(), + ) alice = pre.fund_eoa() tx = Transaction( diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py new file mode 100644 index 00000000000..c557e711c2c --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_invalid.py @@ -0,0 +1,545 @@ +""" +Test cases for invalid Block Access Lists. + +These tests verify that clients properly reject blocks with corrupted BALs +""" + +import pytest + +from ethereum_test_exceptions import BlockException +from ethereum_test_tools import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Storage, + Transaction, +) +from ethereum_test_tools import ( + Opcodes as Op, +) +from ethereum_test_types.block_access_list import ( + BalAccountChange, + BalBalanceChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessListExpectation, +) +from ethereum_test_types.block_access_list.modifiers import ( + append_account, + duplicate_account, + modify_balance, + modify_nonce, + modify_storage, + remove_accounts, + remove_balances, + remove_nonces, + reverse_accounts, + swap_tx_indices, +) + +from .spec import ref_spec_7928 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path +REFERENCE_SPEC_VERSION = ref_spec_7928.version + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_missing_nonce( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL is missing required nonce changes.""" + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + receiver: None, + }, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=sender, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + ] + ).modify(remove_nonces(sender)), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_nonce_value( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL contains incorrect nonce value.""" + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), # Unchanged + receiver: None, + }, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=sender, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + ] + ).modify(modify_nonce(sender, tx_index=1, nonce=42)), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_storage_value( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL contains incorrect storage values.""" + sender = pre.fund_eoa(amount=10**18) + + # Simple storage contract with canary values + storage = Storage({1: 0, 2: 0, 3: 0}) # type: ignore + contract = pre.deploy_contract( + code=Op.SSTORE(1, 1) + Op.SSTORE(2, 2) + Op.SSTORE(3, 3), + storage=storage.canary(), + ) + + tx = Transaction( + sender=sender, + to=contract, + gas_limit=100_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + contract: Account(storage=storage.canary()), + }, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=contract, + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x01)], + ), + BalStorageSlot( + slot=0x02, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x02)], + ), + BalStorageSlot( + slot=0x03, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x03)], + ), + ], + ), + ] + ).modify( + # Corrupt storage value for slot 0x02 + modify_storage(contract, tx_index=1, slot=0x02, value=0xFF) + ), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_tx_order( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL has incorrect transaction ordering.""" + sender1 = pre.fund_eoa(amount=10**18) + sender2 = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + tx1 = Transaction( + sender=sender1, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + tx2 = Transaction( + sender=sender2, + to=receiver, + value=2 * 10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender1: Account(balance=10**18, nonce=0), + sender2: Account(balance=10**18, nonce=0), + receiver: None, + }, + blocks=[ + Block( + txs=[tx1, tx2], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=sender1, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + BalAccountChange( + address=sender2, + nonce_changes=[BalNonceChange(tx_index=2, post_nonce=1)], + ), + BalAccountChange( + address=receiver, + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10**15), + BalBalanceChange(tx_index=2, post_balance=3 * 10**15), + ], + ), + ] + ).modify(swap_tx_indices(1, 2)), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_account( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL contains accounts that don't exist.""" + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + phantom = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + receiver: None, + phantom: None, + }, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=sender, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + ] + ).modify( + append_account( + BalAccountChange( + address=phantom, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ) + ) + ), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_duplicate_account( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL contains duplicate account entries.""" + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + receiver: None, + }, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=sender, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + BalAccountChange( + address=receiver, + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10**15)], + ), + ] + ).modify(duplicate_account(sender)), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_account_order( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL has incorrect account ordering.""" + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + receiver: None, + }, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=sender, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + BalAccountChange( + address=receiver, + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10**15)], + ), + ] + ).modify(reverse_accounts()), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_complex_corruption( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test complex BAL corruption with multiple transformations.""" + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + storage = Storage({1: 0, 2: 0}) # type: ignore + contract = pre.deploy_contract( + code=Op.SSTORE(1, 1) + Op.SSTORE(2, 2), + storage=storage.canary(), + ) + + tx1 = Transaction( + sender=sender, + to=contract, + gas_limit=100_000, + ) + + tx2 = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + contract: Account(storage=storage.canary()), + receiver: None, + }, + blocks=[ + Block( + txs=[tx1, tx2], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=sender, + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + ], + ), + BalAccountChange( + address=contract, + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x01)], + ), + BalStorageSlot( + slot=0x02, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x02)], + ), + ], + ), + BalAccountChange( + address=receiver, + balance_changes=[BalBalanceChange(tx_index=2, post_balance=10**15)], + ), + ] + ).modify( + remove_nonces(sender), + modify_storage(contract, tx_index=1, slot=0x01, value=0xFF), + remove_balances(receiver), + swap_tx_indices(1, 2), + ), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_missing_account( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL is missing an entire account.""" + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + receiver: None, + }, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=sender, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + BalAccountChange( + address=receiver, + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10**15)], + ), + ] + ).modify(remove_accounts(receiver)), + ) + ], + ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.exception_test +def test_bal_invalid_balance_value( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +): + """Test that clients reject blocks where BAL contains incorrect balance value.""" + sender = pre.fund_eoa(amount=10**18) + receiver = pre.fund_eoa(amount=0) + + tx = Transaction( + sender=sender, + to=receiver, + value=10**15, + gas_limit=21_000, + ) + + blockchain_test( + pre=pre, + post={ + sender: Account(balance=10**18, nonce=0), + receiver: None, + }, + blocks=[ + Block( + txs=[tx], + exception=BlockException.INCORRECT_BLOCK_FORMAT, + expected_block_access_list=BlockAccessListExpectation( + account_changes=[ + BalAccountChange( + address=receiver, + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10**15)], + ), + ] + ).modify(modify_balance(receiver, tx_index=1, balance=999999)), + ) + ], + ) From 95eb66124d1791f0f942559050129985d1a2729c Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 27 Aug 2025 08:24:07 -0600 Subject: [PATCH 24/35] chore(spec-resolver): Update commit hash to point to for spec resolver --- src/pytest_plugins/eels_resolutions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index 58033a11327..93ea3446288 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -55,6 +55,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "f4c49309e90e74b51d043b06c045735b54e997b0" + "commit": "410d16b02f0d3d368099f79317b0f9d8fc5abde4" } } From f8eaee751d7f07b78a608c6796e49bc74e8700e2 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 27 Aug 2025 15:28:46 -0600 Subject: [PATCH 25/35] fix(lint,resolver): Use latest commit in resolver; fix lint --- src/ethereum_test_fixtures/blockchain.py | 3 +-- src/pytest_plugins/eels_resolutions.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index 9260faf3299..c4d53abc325 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -434,8 +434,7 @@ class FixtureBlockBase(CamelModel): withdrawals: List[FixtureWithdrawal] | None = None execution_witness: WitnessChunk | None = None block_access_list: Bytes | None = Field( - None, description="Serialized EIP-7928 Block Access List", - alias="blockAccessList" + None, description="Serialized EIP-7928 Block Access List", alias="blockAccessList" ) @computed_field(alias="blocknumber") # type: ignore[misc] diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index 93ea3446288..c4b061b6958 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -55,6 +55,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "410d16b02f0d3d368099f79317b0f9d8fc5abde4" + "commit": "a5c7b29a658320c2432de78883d350e9f4444d14" } } From 41f0ed51cde0e055c027b057ebcbfaee255795c2 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 2 Sep 2025 13:13:59 -0600 Subject: [PATCH 26/35] Address comments from PR #2067 --- src/ethereum_clis/types.py | 4 +- src/ethereum_test_fixtures/blockchain.py | 9 ++-- src/ethereum_test_forks/forks/forks.py | 2 +- src/ethereum_test_specs/blockchain.py | 49 +++++-------------- .../block_access_list/__init__.py | 8 +-- src/pytest_plugins/eels_resolutions.json | 30 ++++++------ .../eip7928_block_level_access_lists/spec.py | 4 +- 7 files changed, 40 insertions(+), 66 deletions(-) diff --git a/src/ethereum_clis/types.py b/src/ethereum_clis/types.py index 8c2d34a0781..27a13a1748b 100644 --- a/src/ethereum_clis/types.py +++ b/src/ethereum_clis/types.py @@ -63,8 +63,8 @@ class Result(CamelModel): blob_gas_used: HexNumber | None = None requests_hash: Hash | None = None requests: List[Bytes] | None = None - block_access_list: BlockAccessList | None = Field(None, alias="blockAccessList") - block_access_list_hash: Hash | None = Field(None, alias="blockAccessListHash") + block_access_list: BlockAccessList | None = None + block_access_list_hash: Hash | None = None block_exception: Annotated[ BlockExceptionWithMessage | UndefinedException | None, ExceptionMapperValidator ] = None diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index c4d53abc325..6eb1be566ce 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -234,8 +234,7 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> "FixtureHead extras = { "state_root": state_root, "requests_hash": Requests() if fork.header_requests_required(0, 0) else None, - # TODO: How should we handle the genesis block access list? Is `Hash(0)` fine? - "block_access_list_hash": Hash(0) if fork.header_bal_hash_required(0, 0) else None, + "block_access_list_hash": Hash([]) if fork.header_bal_hash_required(0, 0) else None, "fork": fork, } return FixtureHeader(**environment_values, **extras) @@ -267,6 +266,10 @@ class FixtureExecutionPayload(CamelModel): transactions: List[Bytes] withdrawals: List[Withdrawal] | None = None + block_access_list: Bytes | None = Field( + None, description="RLP-serialized EIP-7928 Block Access List" + ) + @classmethod def from_fixture_header( cls, @@ -434,7 +437,7 @@ class FixtureBlockBase(CamelModel): withdrawals: List[FixtureWithdrawal] | None = None execution_witness: WitnessChunk | None = None block_access_list: Bytes | None = Field( - None, description="Serialized EIP-7928 Block Access List", alias="blockAccessList" + None, description="Serialized EIP-7928 Block Access List" ) @computed_field(alias="blocknumber") # type: ignore[misc] diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index bbfefb4aa22..b032354811e 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -1819,7 +1819,7 @@ class Amsterdam(Osaka): @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).""" - return block_number > 0 # Not required in genesis block + return True @classmethod def is_deployed(cls) -> bool: diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index ae06195c29a..152b730964d 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -430,12 +430,6 @@ class BlockchainTest(BaseTest): Exclude the post state from the fixture output. In this case, the state verification is only performed based on the state root. """ - expected_block_access_list: BlockAccessListExpectation | None = None - """ - Expected block access list for verification. - If set, verifies that the block access list returned by the client matches expectations. - Use BlockAccessListExpectation to define partial validation expectations. - """ supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [ BlockchainFixture, @@ -612,15 +606,18 @@ def generate_block_data( requests_list = block.requests if fork.header_bal_hash_required(header.number, header.timestamp): - if transition_tool_output.result.block_access_list is not None: - rlp = transition_tool_output.result.block_access_list.rlp() - computed_bal_hash = Hash(rlp.keccak256()) - if computed_bal_hash != header.block_access_list_hash: - raise Exception( - "Block access list hash in header does not match the " - f"computed hash from BAL: {header.block_access_list_hash} " - f"!= {computed_bal_hash}" - ) + 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 @@ -708,28 +705,6 @@ def verify_post_state(self, t8n, t8n_state: Alloc, expected_state: Alloc | None print_traces(t8n.get_traces()) raise e - def verify_block_access_list( - self, actual_bal: BlockAccessList | None, expected_bal: BlockAccessListExpectation | None - ): - """ - Verify that the actual block access list matches expectations. - - Args: - actual_bal: The BlockAccessList returned by the client - expected_bal: The expected BlockAccessList object - - """ - if expected_bal is None: - return - - if actual_bal is None: - raise Exception("Expected block access list but got none.") - - try: - expected_bal.verify_against(actual_bal) - except Exception as e: - raise Exception("Block access list verification failed.") from e - def make_fixture( self, t8n: TransitionTool, diff --git a/src/ethereum_test_types/block_access_list/__init__.py b/src/ethereum_test_types/block_access_list/__init__.py index 597012842e7..01b30542eb6 100644 --- a/src/ethereum_test_types/block_access_list/__init__.py +++ b/src/ethereum_test_types/block_access_list/__init__.py @@ -5,7 +5,7 @@ these are simple data classes that can be composed together. """ -from typing import Any, Callable, ClassVar, List, Optional +from typing import Any, Callable, ClassVar, List import ethereum_rlp as eth_rlp from pydantic import Field @@ -169,7 +169,7 @@ class BlockAccessListExpectation(CamelModel): default_factory=list, description="Expected account changes to verify" ) - modifier: Optional[Callable[["BlockAccessList"], "BlockAccessList"]] = Field( + modifier: Callable[["BlockAccessList"], "BlockAccessList"] | None = Field( None, exclude=True, description="Optional modifier to modify the BAL for invalid tests", @@ -281,10 +281,6 @@ def _compare_account_changes( ) continue - # If actual is ``None`` but we expected something, raise - if actual_value is None: - raise AssertionError(f"Expected {field_name} but found none") - if field_name == "storage_reads": # Convert to comparable format (both are lists of 32-byte values) expected_set = {bytes(v) if hasattr(v, "__bytes__") else v for v in expected_value} diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index c4b061b6958..a5aae290805 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -42,19 +42,19 @@ }, "BPO1": { "same_as": "EELSMaster" - }, - "BPO2": { - "same_as": "EELSMaster" - }, - "BPO3": { - "same_as": "EELSMaster" - }, - "BPO4": { - "same_as": "EELSMaster" - }, - "Amsterdam": { - "git_url": "https://github.com/fselmo/execution-specs.git", - "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "a5c7b29a658320c2432de78883d350e9f4444d14" - } + }, + "BPO2": { + "same_as": "EELSMaster" + }, + "BPO3": { + "same_as": "EELSMaster" + }, + "BPO4": { + "same_as": "EELSMaster" + }, + "Amsterdam": { + "git_url": "https://github.com/fselmo/execution-specs.git", + "branch": "feat/amsterdam-fork-and-block-access-lists", + "commit": "a5c7b29a658320c2432de78883d350e9f4444d14" + } } diff --git a/tests/amsterdam/eip7928_block_level_access_lists/spec.py b/tests/amsterdam/eip7928_block_level_access_lists/spec.py index 56b4a954ad2..3e8ebfa3ae9 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/spec.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/spec.py @@ -21,8 +21,8 @@ class ReferenceSpec: class Spec: """Constants and parameters from EIP-7928.""" - # SSZ encoding is used for block access list data structures - BAL_ENCODING_FORMAT: str = "SSZ" + # RLP encoding is used for block access list data structures + BAL_ENCODING_FORMAT: str = "RLP" # Maximum limits for block access list data structures TARGET_MAX_GAS_LIMIT = 600_000_000 From 38b6dbfa8331cba6e0d7a7babec9ac828826d3fe Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 2 Sep 2025 14:25:47 -0600 Subject: [PATCH 27/35] chore(docs): Add changelog entries for BAL updates --- docs/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 06f6664d814..439934b4153 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -141,6 +141,7 @@ Users can select any of the artifacts depending on their testing needs for their - 🔀 Move Prague to stable and Osaka to develop ([#1573](https://github.com/ethereum/execution-spec-tests/pull/1573)). - ✨ Add a `pytest.mark.with_all_typed_transactions` marker that creates default typed transactions for each `tx_type` supported by the current `fork` ([#1890](https://github.com/ethereum/execution-spec-tests/pull/1890)). - ✨ Add basic support for ``Amsterdam`` fork in order to begin testing Glamsterdam ([#2069](https://github.com/ethereum/execution-spec-tests/pull/2069)). +- ✨ [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928): Add initial framework support for `Block Level Access Lists (BAL)` testing for Amsterdam ([#2067](https://github.com/ethereum/execution-spec-tests/pull/2067)). ### 🧪 Test Cases @@ -154,12 +155,13 @@ Users can select any of the artifacts depending on their testing needs for their - ✨ [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951): add test cases for `P256VERIFY` precompile to support secp256r1 curve [#1670](https://github.com/ethereum/execution-spec-tests/pull/1670). - ✨ Introduce blockchain tests for benchmark to cover the scenario of pure ether transfers [#1742](https://github.com/ethereum/execution-spec-tests/pull/1742). - ✨ [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934): Add test cases for the block RLP max limit of 10MiB ([#1730](https://github.com/ethereum/execution-spec-tests/pull/1730)). -- ✨ [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) Add count leading zeros (CLZ) opcode tests for Osaka ([#1733](https://github.com/ethereum/execution-spec-tests/pull/1733)). +- ✨ [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939): Add count leading zeros (CLZ) opcode tests for Osaka ([#1733](https://github.com/ethereum/execution-spec-tests/pull/1733)). - ✨ [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934): Add additional test cases for block RLP max limit with all typed transactions and for a log-creating transactions ([#1890](https://github.com/ethereum/execution-spec-tests/pull/1890)). - ✨ [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825): Pre-Osaka tests have been updated to either (1) dynamically adapt to the transaction gas limit cap, or (2) reduce overall gas consumption to fit the new limit ([#1924](https://github.com/ethereum/EIPs/pull/1924), [#1928](https://github.com/ethereum/EIPs/pull/1928), [#1980](https://github.com/ethereum/EIPs/pull/1980)). - ✨ [EIP-7918](https://eips.ethereum.org/EIPS/eip-7918): Blob base fee bounded by execution cost test cases (initial), includes some adjustments to [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) tests ([#1685](https://github.com/ethereum/execution-spec-tests/pull/1685)). - 🔀 Adds the max blob transaction limit to the tests including updates to [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) for Osaka ([#1884](https://github.com/ethereum/execution-spec-tests/pull/1884)). - 🐞 Fix issues when filling block rlp size limit tests with ``--generate-pre-alloc-groups`` ([#1989](https://github.com/ethereum/execution-spec-tests/pull/1989)). +- ✨ [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928): Add test cases for `Block Level Access Lists (BAL)` to Amsterdam ([#2067](https://github.com/ethereum/execution-spec-tests/pull/2067)). ## [v4.5.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v4.5.0) - 2025-05-14 From c6055afa073e878b7a12a9477a872c630ec2c2d2 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 2 Sep 2025 15:34:43 -0600 Subject: [PATCH 28/35] refactor(types): Refactor BAL checks for explicit exclusion of acct changes - From comments on PR #2067, we should allow for an explicit exclusion case for an entire set of account changes. If a test case warrants a sanity check that some account might have been impacted but shouldn't be accounted for, we should be able to specify that explicitly. --- src/ethereum_test_fixtures/blockchain.py | 5 +- src/ethereum_test_specs/blockchain.py | 4 +- .../block_access_list/__init__.py | 106 +++++++++++------- .../test_block_access_lists.py | 47 ++++---- 4 files changed, 91 insertions(+), 71 deletions(-) diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index 6eb1be566ce..a6abed49f2f 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -38,6 +38,7 @@ from ethereum_test_exceptions import EngineAPIError, ExceptionInstanceOrList from ethereum_test_forks import Fork, Paris from ethereum_test_types import ( + BlockAccessList, Environment, Requests, Transaction, @@ -234,7 +235,9 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> "FixtureHead extras = { "state_root": state_root, "requests_hash": Requests() if fork.header_requests_required(0, 0) else None, - "block_access_list_hash": Hash([]) if fork.header_bal_hash_required(0, 0) else None, + "block_access_list_hash": ( + BlockAccessList().rlp_hash if fork.header_bal_hash_required(0, 0) else None + ), "fork": fork, } return FixtureHeader(**environment_values, **extras) diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 152b730964d..ded2c37bd72 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -338,7 +338,7 @@ def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock: if self.withdrawals is not None else None ), - block_access_list=self.block_access_list.rlp() if self.block_access_list else None, + block_access_list=self.block_access_list.rlp if self.block_access_list else None, fork=self.fork, ).with_rlp(txs=self.txs) @@ -611,7 +611,7 @@ def generate_block_data( "by the transition tool" ) - rlp = transition_tool_output.result.block_access_list.rlp() + 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 " diff --git a/src/ethereum_test_types/block_access_list/__init__.py b/src/ethereum_test_types/block_access_list/__init__.py index 01b30542eb6..a1525c0e17c 100644 --- a/src/ethereum_test_types/block_access_list/__init__.py +++ b/src/ethereum_test_types/block_access_list/__init__.py @@ -5,7 +5,8 @@ these are simple data classes that can be composed together. """ -from typing import Any, Callable, ClassVar, List +from functools import cached_property +from typing import Any, Callable, ClassVar, Dict, List import ethereum_rlp as eth_rlp from pydantic import Field @@ -138,10 +139,40 @@ def to_list(self) -> List[Any]: """Return the list for RLP encoding per EIP-7928.""" return to_serializable_element(self.root) + @cached_property def rlp(self) -> Bytes: """Return the RLP encoded block access list for hash verification.""" return Bytes(eth_rlp.encode(self.to_list())) + @cached_property + def rlp_hash(self) -> Bytes: + """Return the hash of the RLP encoded block access list.""" + return self.rlp.keccak256() + + +class BalAccountExpectation(CamelModel): + """ + Represents expected changes to a specific account in a block. + + Same as BalAccountChange but without the address field, used for expectations. + """ + + nonce_changes: List[BalNonceChange] = Field( + default_factory=list, description="List of expected nonce changes" + ) + balance_changes: List[BalBalanceChange] = Field( + default_factory=list, description="List of expected balance changes" + ) + code_changes: List[BalCodeChange] = Field( + default_factory=list, description="List of expected code changes" + ) + storage_changes: List[BalStorageSlot] = Field( + default_factory=list, description="List of expected storage changes" + ) + storage_reads: List[StorageKey] = Field( + default_factory=list, description="List of expected read storage slots" + ) + class BlockAccessListExpectation(CamelModel): """ @@ -151,22 +182,23 @@ class BlockAccessListExpectation(CamelModel): - Partial validation (only checks explicitly set fields) - Convenient test syntax with named parameters - Verification against actual BAL from t8n + - Explicit exclusion of addresses (using None values) Example: # In test definition expected_block_access_list = BlockAccessListExpectation( - account_changes=[ - BalAccountChange( - address=alice, + account_expectations={ + alice: BalAccountExpectation( nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)] - ) - ] + ), + bob: None, # Bob should NOT be in the BAL + } ) """ - account_changes: List[BalAccountChange] = Field( - default_factory=list, description="Expected account changes to verify" + account_expectations: Dict[Address, BalAccountExpectation | None] = Field( + default_factory=dict, description="Expected account changes or exclusions to verify" ) modifier: Callable[["BlockAccessList"], "BlockAccessList"] | None = Field( @@ -191,7 +223,7 @@ def modify( from ethereum_test_types.block_access_list.modifiers import remove_nonces expectation = BlockAccessListExpectation( - account_changes=[...] + account_expectations={...} ).modify(remove_nonces(alice)) """ @@ -213,8 +245,7 @@ def to_fixture_bal(self, t8n_bal: "BlockAccessList") -> "BlockAccessList": The potentially transformed BlockAccessList for the fixture """ - # Only validate if we have expectations - if self.account_changes: + if self.account_expectations: self.verify_against(t8n_bal) # Apply modifier if present (for invalid tests) @@ -235,49 +266,42 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None: """ actual_accounts_by_addr = {acc.address: acc for acc in actual_bal.root} - expected_accounts_by_addr = {acc.address: acc for acc in self.account_changes} - # Check for missing accounts - missing_accounts = set(expected_accounts_by_addr.keys()) - set( - actual_accounts_by_addr.keys() - ) - if missing_accounts: - raise Exception( - "Expected accounts not found in actual BAL: " - f"{', '.join(str(a) for a in missing_accounts)}" - ) - - # Verify each expected account - for address, expected_account in expected_accounts_by_addr.items(): - actual_account = actual_accounts_by_addr[address] - - try: - self._compare_account_changes(expected_account, actual_account) - except AssertionError as e: - raise Exception(f"Account {address}: {str(e)}") from e - - def _compare_account_changes( - self, expected: BalAccountChange, actual: BalAccountChange + for address, expectation in self.account_expectations.items(): + if expectation is None: + # check explicit exclusion of address when set to `None` + if address in actual_accounts_by_addr: + raise Exception(f"Address {address} should not be in BAL but was found") + else: + # Address should be in BAL with expected values + if address not in actual_accounts_by_addr: + raise Exception(f"Expected address {address} not found in actual BAL") + + actual_account = actual_accounts_by_addr[address] + try: + self._compare_account_expectations(expectation, actual_account) + except AssertionError as e: + raise Exception(f"Account {address}: {str(e)}") from e + + def _compare_account_expectations( + self, expected: BalAccountExpectation, actual: BalAccountChange ) -> None: """ - Compare two BalAccountChange models with detailed error reporting. + Compare expected account changes with actual BAL account entry. Only validates fields that were explicitly set in the expected model, using model_fields_set to determine what was intentionally specified. """ # Only check fields that were explicitly set in the expected model for field_name in expected.model_fields_set: - if field_name == "address": - continue # Already matched by account lookup - expected_value = getattr(expected, field_name) actual_value = getattr(actual, field_name) - # If we explicitly set a field to None, verify it's None/empty + # explicit check for None if expected_value is None: if actual_value is not None and actual_value != []: raise AssertionError( - f"Expected {field_name} to be empty/None but found: {actual_value}" + f"Expected {field_name} to be `None` but found: {actual_value}" ) continue @@ -314,7 +338,8 @@ def _compare_account_changes( # The comparison method will raise with details pass - def _compare_change_lists(self, field_name: str, expected: List, actual: List) -> bool: + @staticmethod + def _compare_change_lists(field_name: str, expected: List, actual: List) -> bool: """Compare lists of change objects using set operations for better error messages.""" if field_name == "storage_changes": # Storage changes are nested (slot -> changes) @@ -390,6 +415,7 @@ def _compare_change_lists(self, field_name: str, expected: List, actual: List) - # Core models "BlockAccessList", "BlockAccessListExpectation", + "BalAccountExpectation", # Change types "BalAccountChange", "BalNonceChange", 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 584a6e729ed..f4fa786cc51 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 @@ -13,7 +13,7 @@ ) from ethereum_test_tools.vm.opcode import Opcodes as Op from ethereum_test_types.block_access_list import ( - BalAccountChange, + BalAccountExpectation, BalBalanceChange, BalCodeChange, BalNonceChange, @@ -53,12 +53,11 @@ def test_bal_nonce_changes( bob: Account(balance=100), }, expected_block_access_list=BlockAccessListExpectation( - account_changes=[ - BalAccountChange( - address=alice, + account_expectations={ + alice: BalAccountExpectation( nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], ), - ] + } ), ) @@ -105,20 +104,17 @@ def test_bal_balance_changes( bob: Account(balance=100), }, expected_block_access_list=BlockAccessListExpectation( - account_changes=[ - BalAccountChange( - address=alice, + account_expectations={ + alice: BalAccountExpectation( nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], balance_changes=[ BalBalanceChange(tx_index=1, post_balance=alice_final_balance) ], ), - BalAccountChange( - address=bob, + bob: BalAccountExpectation( balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], ), - # TODO: Validate coinbase - ] + } ), ) @@ -153,9 +149,8 @@ def test_bal_storage_writes( storage_contract: Account(storage={0x01: 0x42}), }, expected_block_access_list=BlockAccessListExpectation( - account_changes=[ - BalAccountChange( - address=storage_contract, + account_expectations={ + storage_contract: BalAccountExpectation( storage_changes=[ BalStorageSlot( slot=0x01, @@ -163,7 +158,7 @@ def test_bal_storage_writes( ) ], ), - ] + } ), ) @@ -196,12 +191,11 @@ def test_bal_storage_reads( storage_contract: Account(storage={0x01: 0x42}), }, expected_block_access_list=BlockAccessListExpectation( - account_changes=[ - BalAccountChange( - address=storage_contract, + account_expectations={ + storage_contract: BalAccountExpectation( storage_reads=[0x01], ), - ] + } ), ) @@ -266,19 +260,16 @@ def test_bal_code_changes( ), }, expected_block_access_list=BlockAccessListExpectation( - account_changes=[ - BalAccountChange( - address=alice, + account_expectations={ + alice: BalAccountExpectation( nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], ), - BalAccountChange( - address=factory_contract, + factory_contract: BalAccountExpectation( nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)], ), - BalAccountChange( - address=created_contract, + created_contract: BalAccountExpectation( code_changes=[BalCodeChange(tx_index=1, new_code=runtime_code_bytes)], ), - ] + } ), ) From d0eb1060d379aef0ce6091e27ec3c24420ad5c86 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 2 Sep 2025 16:23:41 -0600 Subject: [PATCH 29/35] fix(bal): Fix explicit empty checks for account changes; add unit tests --- .../block_access_list/__init__.py | 8 +- .../tests/test_block_access_lists.py | 208 ++++++++++++++++++ 2 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 src/ethereum_test_types/tests/test_block_access_lists.py diff --git a/src/ethereum_test_types/block_access_list/__init__.py b/src/ethereum_test_types/block_access_list/__init__.py index a1525c0e17c..fac6f906c2c 100644 --- a/src/ethereum_test_types/block_access_list/__init__.py +++ b/src/ethereum_test_types/block_access_list/__init__.py @@ -297,11 +297,11 @@ def _compare_account_expectations( expected_value = getattr(expected, field_name) actual_value = getattr(actual, field_name) - # explicit check for None - if expected_value is None: - if actual_value is not None and actual_value != []: + # empty list explicitly set (no changes expected) + if not expected_value: + if actual_value: raise AssertionError( - f"Expected {field_name} to be `None` but found: {actual_value}" + f"Expected {field_name} to be empty but found: {actual_value}" ) continue diff --git a/src/ethereum_test_types/tests/test_block_access_lists.py b/src/ethereum_test_types/tests/test_block_access_lists.py new file mode 100644 index 00000000000..852894bc252 --- /dev/null +++ b/src/ethereum_test_types/tests/test_block_access_lists.py @@ -0,0 +1,208 @@ +"""Unit tests for BlockAccessListExpectation validation.""" + +import pytest + +from ethereum_test_base_types import Address +from ethereum_test_types.block_access_list import ( + BalAccountChange, + BalAccountExpectation, + BalBalanceChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessList, + BlockAccessListExpectation, +) + + +def test_address_exclusion_validation_passes(): + """Test that address exclusion works when address is not in BAL.""" + alice = Address("0x000000000000000000000000000000000000000a") + bob = Address("0x000000000000000000000000000000000000000b") + + actual_bal = BlockAccessList( + [ + BalAccountChange( + address=alice, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + ] + ) + + expectation = BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation(nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)]), + bob: None, # expect Bob is not in BAL (correctly) + } + ) + + expectation.verify_against(actual_bal) + + +def test_address_exclusion_validation_raises_when_address_is_present(): + """Test that validation fails when excluded address is in BAL.""" + alice = Address("0x000000000000000000000000000000000000000a") + bob = Address("0x000000000000000000000000000000000000000b") + + actual_bal = BlockAccessList( + [ + BalAccountChange( + address=alice, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + BalAccountChange( + address=bob, + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], + ), + ] + ) + + expectation = BlockAccessListExpectation( + # explicitly expect Bob to NOT be in BAL (wrongly) + account_expectations={bob: None}, + ) + + with pytest.raises(Exception, match="should not be in BAL but was found"): + expectation.verify_against(actual_bal) + + +def test_empty_list_validation(): + """Test that empty list validates correctly.""" + alice = Address("0x000000000000000000000000000000000000000a") + + actual_bal = BlockAccessList( + [ + BalAccountChange( + address=alice, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + balance_changes=[], # no balance changes + ), + ] + ) + + expectation = BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + balance_changes=[], # explicitly expect no balance changes + ), + } + ) + + expectation.verify_against(actual_bal) + + +def test_empty_list_validation_fails(): + """Test that validation fails when expecting empty but field has values.""" + alice = Address("0x000000000000000000000000000000000000000a") + + actual_bal = BlockAccessList( + [ + BalAccountChange( + address=alice, + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], + ), + ] + ) + + expectation = BlockAccessListExpectation( + account_expectations={ + # expect no balance changes (wrongly) + alice: BalAccountExpectation(balance_changes=[]), + } + ) + + with pytest.raises(Exception, match="Expected balance_changes to be empty"): + expectation.verify_against(actual_bal) + + +def test_partial_validation(): + """Test that unset fields are not validated.""" + alice = Address("0x000000000000000000000000000000000000000a") + + # Actual BAL has multiple types of changes + actual_bal = BlockAccessList( + [ + BalAccountChange( + address=alice, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], + storage_reads=[0x01, 0x02], + ), + ] + ) + + # Only validate nonce changes, ignore balance and storage + expectation = BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + # balance_changes and storage_reads not set and won't be validated + ), + } + ) + + expectation.verify_against(actual_bal) + + +def test_storage_changes_validation(): + """Test storage changes validation.""" + contract = Address("0x000000000000000000000000000000000000000c") + + # Actual BAL with storage changes + actual_bal = BlockAccessList( + [ + BalAccountChange( + address=contract, + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x42)], + ) + ], + ), + ] + ) + + # Expect the same storage changes + expectation = BlockAccessListExpectation( + account_expectations={ + contract: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x42)], + ) + ], + ), + } + ) + + expectation.verify_against(actual_bal) + + +def test_missing_expected_address(): + """Test that validation fails when expected address is missing.""" + alice = Address("0x000000000000000000000000000000000000000a") + bob = Address("0x000000000000000000000000000000000000000b") + + actual_bal = BlockAccessList( + [ + BalAccountChange( + address=alice, + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + ] + ) + + expectation = BlockAccessListExpectation( + account_expectations={ + # wrongly expect Bob to be present + bob: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + } + ) + + with pytest.raises(Exception, match="Expected address .* not found in actual BAL"): + expectation.verify_against(actual_bal) From 8570378f479496d5241e3189a80053e740ef50ed Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 2 Sep 2025 17:56:03 -0600 Subject: [PATCH 30/35] feat(bal): Extend BAL support within framework - Add BAL to engine execution payload as defined in: https://github.com/ethereum/consensus-specs/pull/4526/files#diff-4b950f0c895b4d521c9e8103d638e73a4c7746c6aea51250994425a1efd6f4c8R55 - Add BAL support for state tests to be able to create blockchain tests --- src/ethereum_test_fixtures/blockchain.py | 6 +++++- src/ethereum_test_specs/blockchain.py | 1 + src/ethereum_test_specs/state.py | 9 ++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index a6abed49f2f..b4126841a57 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -279,15 +279,17 @@ def from_fixture_header( header: FixtureHeader, transactions: List[Transaction], withdrawals: List[Withdrawal] | None, + block_access_list: Bytes | None = None, ) -> "FixtureExecutionPayload": """ Return FixtureExecutionPayload from a FixtureHeader, a list - of transactions and a list of withdrawals. + of transactions, a list of withdrawals, and an optional block access list. """ return cls( **header.model_dump(exclude={"rlp"}, exclude_none=True), transactions=[tx.rlp() for tx in transactions], withdrawals=withdrawals, + block_access_list=block_access_list, ) @@ -342,6 +344,7 @@ def from_fixture_header( transactions: List[Transaction], withdrawals: List[Withdrawal] | None, requests: List[Bytes] | None, + block_access_list: Bytes | None = None, **kwargs, ) -> "FixtureEngineNewPayload": """Create `FixtureEngineNewPayload` from a `FixtureHeader`.""" @@ -355,6 +358,7 @@ def from_fixture_header( header=header, transactions=transactions, withdrawals=withdrawals, + block_access_list=block_access_list, ) params: List[Any] = [execution_payload] diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index ded2c37bd72..fa9de37334e 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -369,6 +369,7 @@ def get_fixture_engine_new_payload(self) -> FixtureEngineNewPayload: requests=self.requests, validation_error=self.expected_exception, error_code=self.engine_api_error_code, + block_access_list=self.block_access_list.rlp if self.block_access_list else None, ) def verify_transactions(self, transition_tool_exceptions_reliable: bool) -> List[int]: diff --git a/src/ethereum_test_specs/state.py b/src/ethereum_test_specs/state.py index f2962747da6..badab6a5101 100644 --- a/src/ethereum_test_specs/state.py +++ b/src/ethereum_test_specs/state.py @@ -29,7 +29,12 @@ FixtureTransaction, ) from ethereum_test_forks import Fork -from ethereum_test_types import Alloc, Environment, Transaction +from ethereum_test_types import ( + Alloc, + BlockAccessListExpectation, + Environment, + Transaction, +) from .base import BaseTest, OpMode from .blockchain import Block, BlockchainTest, Header @@ -50,6 +55,7 @@ class StateTest(BaseTest): engine_api_error_code: Optional[EngineAPIError] = None blockchain_test_header_verify: Optional[Header] = None blockchain_test_rlp_modifier: Optional[Header] = None + expected_block_access_list: Optional[BlockAccessListExpectation] = None chain_id: int = 1 supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [ @@ -164,6 +170,7 @@ def generate_blockchain_test(self, *, fork: Fork) -> BlockchainTest: pre=self.pre, post=self.post, blocks=self._generate_blockchain_blocks(fork=fork), + expected_block_access_list=self.expected_block_access_list, ) def make_state_test_fixture( From e0c2fba3370223c1489fba56d41e03f4f344d242 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 2 Sep 2025 17:57:32 -0600 Subject: [PATCH 31/35] fix(warnings): Silence warning for unused reference in .md file --- docs/running_tests/test_formats/blockchain_test_sync.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/running_tests/test_formats/blockchain_test_sync.md b/docs/running_tests/test_formats/blockchain_test_sync.md index b1ff96e93d3..2f2cb588c38 100644 --- a/docs/running_tests/test_formats/blockchain_test_sync.md +++ b/docs/running_tests/test_formats/blockchain_test_sync.md @@ -15,7 +15,7 @@ The test works by: 3. Having the sync client synchronize from the client under test. 4. Verifying that both clients reach the same final state. -A single JSON fixture file is composed of a JSON object where each key-value pair is a different [`SyncFixture`](#syncfixture) test object, with the key string representing the test name. +A single JSON fixture file is composed of a JSON object where each key-value pair is a different [`HiveFixture`](#hivefixture) test object, with the key string representing the test name. The JSON file path plus the test name are used as the unique test identifier, as well as a `{client under test}::sync_{sync client}` identifier. From f7ad842586661b67bf9e5efc26392b81ecbc0f44 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 3 Sep 2025 12:20:09 -0600 Subject: [PATCH 32/35] fix(bal): Move state test expected bal to Block --- src/ethereum_test_specs/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ethereum_test_specs/state.py b/src/ethereum_test_specs/state.py index badab6a5101..15646998b1d 100644 --- a/src/ethereum_test_specs/state.py +++ b/src/ethereum_test_specs/state.py @@ -153,6 +153,7 @@ def _generate_blockchain_blocks(self, *, fork: Fork) -> List[Block]: "ommers": [], "header_verify": self.blockchain_test_header_verify, "rlp_modifier": self.blockchain_test_rlp_modifier, + "expected_block_access_list": self.expected_block_access_list, } if not fork.header_prev_randao_required(): kwargs["difficulty"] = self.env.difficulty @@ -170,7 +171,6 @@ def generate_blockchain_test(self, *, fork: Fork) -> BlockchainTest: pre=self.pre, post=self.post, blocks=self._generate_blockchain_blocks(fork=fork), - expected_block_access_list=self.expected_block_access_list, ) def make_state_test_fixture( From b3eaa93baa3564afb35293651ab8e7fb0fae1a9a Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 3 Sep 2025 12:36:42 -0600 Subject: [PATCH 33/35] feat(release): Set up BAL work for a feature release --- .github/configs/eels_resolutions.json | 5 +++++ .github/configs/feature.yaml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/configs/eels_resolutions.json b/.github/configs/eels_resolutions.json index 7d69c414f85..aedebb763f5 100644 --- a/.github/configs/eels_resolutions.json +++ b/.github/configs/eels_resolutions.json @@ -48,5 +48,10 @@ }, "BPO4": { "same_as": "Osaka" + }, + "Amsterdam": { + "git_url": "https://github.com/fselmo/execution-specs.git", + "branch": "feat/amsterdam-fork-and-block-access-lists", + "commit": "a5c7b29a658320c2432de78883d350e9f4444d14" } } diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 589f245e4a3..2ba62c45c2d 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -15,3 +15,8 @@ benchmark_develop: evm-type: develop fill-params: --fork=Osaka --gas-benchmark-values 1,10,30,45,60,100,150 -m "benchmark and not state_test" ./tests/benchmark feature_only: true + +bal: + evm-type: develop + fill-params: --fork=Amsterdam ./tests/amsterdam/eip7928_block_level_access_lists + feature_only: true From 64e031006aa8dd636d82684b4eff0512268dd414 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 3 Sep 2025 14:37:07 -0600 Subject: [PATCH 34/35] fix: BAL as object in fixture block so RLP is correct --- src/ethereum_test_fixtures/blockchain.py | 7 +++++-- src/ethereum_test_specs/blockchain.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index b4126841a57..c773b2af3ba 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -443,8 +443,8 @@ class FixtureBlockBase(CamelModel): ommers: List[FixtureHeader] = Field(default_factory=list, alias="uncleHeaders") withdrawals: List[FixtureWithdrawal] | None = None execution_witness: WitnessChunk | None = None - block_access_list: Bytes | None = Field( - None, description="Serialized EIP-7928 Block Access List" + block_access_list: BlockAccessList | None = Field( + None, description="EIP-7928 Block Access List" ) @computed_field(alias="blocknumber") # type: ignore[misc] @@ -464,6 +464,9 @@ def with_rlp(self, txs: List[Transaction]) -> "FixtureBlock": if self.withdrawals is not None: block.append([w.to_serializable_list() for w in self.withdrawals]) + if self.block_access_list is not None: + block.append(self.block_access_list.to_list()) + return FixtureBlock( **self.model_dump(), rlp=eth_rlp.encode(block), diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index fa9de37334e..7ee7507ff3c 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -338,7 +338,7 @@ def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock: if self.withdrawals is not None else None ), - block_access_list=self.block_access_list.rlp if self.block_access_list else None, + block_access_list=self.block_access_list if self.block_access_list else None, fork=self.fork, ).with_rlp(txs=self.txs) @@ -367,9 +367,9 @@ def get_fixture_engine_new_payload(self) -> FixtureEngineNewPayload: transactions=self.txs, withdrawals=self.withdrawals, requests=self.requests, + block_access_list=self.block_access_list.rlp if self.block_access_list else None, validation_error=self.expected_exception, error_code=self.engine_api_error_code, - block_access_list=self.block_access_list.rlp if self.block_access_list else None, ) def verify_transactions(self, transition_tool_exceptions_reliable: bool) -> List[int]: From 83ac1cebb0399e9425e002c8aaa870eaec9f71d8 Mon Sep 17 00:00:00 2001 From: felipe Date: Wed, 3 Sep 2025 21:05:38 +0000 Subject: [PATCH 35/35] chore(pytest): Add Amsterdam to ruleset for simulator plugin --- .../consume/simulators/helpers/ruleset.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/pytest_plugins/consume/simulators/helpers/ruleset.py b/src/pytest_plugins/consume/simulators/helpers/ruleset.py index 6d7baef8a12..c1b7195b290 100644 --- a/src/pytest_plugins/consume/simulators/helpers/ruleset.py +++ b/src/pytest_plugins/consume/simulators/helpers/ruleset.py @@ -12,6 +12,7 @@ BPO2, BPO3, BPO4, + Amsterdam, Berlin, BerlinToLondonAt5, BPO1ToBPO2AtTime15k, @@ -473,4 +474,27 @@ def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]: "HIVE_BPO4_TIMESTAMP": 15000, **get_blob_schedule_entries(BPO4), }, + Amsterdam: { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + "HIVE_SHANGHAI_TIMESTAMP": 0, + "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, + "HIVE_AMSTERDAM_TIMESTAMP": 0, + **get_blob_schedule_entries(Amsterdam), + }, }