diff --git a/tests/cancun/eip4844_blobs/test_blob_txs.py b/tests/cancun/eip4844_blobs/test_blob_txs.py index 5ae5db4f33..3f217c21e2 100644 --- a/tests/cancun/eip4844_blobs/test_blob_txs.py +++ b/tests/cancun/eip4844_blobs/test_blob_txs.py @@ -33,6 +33,7 @@ from ethereum_test_tools import Opcodes as Op from ethereum_test_tools import ( Removable, + StateTestFiller, Storage, TestAddress, TestAddress2, @@ -305,22 +306,50 @@ def pre( # noqa: D103 """ return { TestAddress: Account(balance=total_account_minimum_balance + account_balance_modifier), + TestAddress2: Account(balance=10**9), } @pytest.fixture def env( parent_excess_blob_gas: Optional[int], + parent_blobs: int, ) -> Environment: """ Prepare the environment for all test cases. """ + excess_blob_gas = parent_excess_blob_gas if parent_excess_blob_gas else 0 + if parent_blobs: + # We increase the excess blob gas of the genesis because + # we cannot include blobs in the genesis, so the + # test blobs are actually in block 1. + excess_blob_gas += Spec.TARGET_BLOB_GAS_PER_BLOCK return Environment( - excess_blob_gas=parent_excess_blob_gas, + excess_blob_gas=excess_blob_gas, blob_gas_used=0, ) +@pytest.fixture +def state_env( + parent_excess_blob_gas: Optional[int], + parent_blobs: int, +) -> Environment: + """ + Prepare the environment for all state test cases. + + Main difference is that the excess blob gas is not increased by the target, as + there is no genesis block -> block 1 transition, and therefore the excess blob gas + is not decreased by the target. + """ + return Environment( + excess_blob_gas=SpecHelpers.calc_excess_blob_gas_from_blob_count( + parent_excess_blob_gas=parent_excess_blob_gas if parent_excess_blob_gas else 0, + parent_blob_count=parent_blobs, + ), + ) + + @pytest.fixture def engine_api_error_code() -> Optional[EngineAPIError]: """ @@ -358,20 +387,21 @@ def block_timestamp() -> int: @pytest.fixture -def expected_blob_gas_used( +def expected_blob_gas_used_from_transition_tool( fork: Fork, txs: List[Transaction], block_number: int, block_timestamp: int, ) -> Optional[int | Removable]: """ - Calculates the blob gas used by the test block. + Calculates the blob gas used by the test block, without taking into account + failed transactions """ if not fork.header_blob_gas_used_required( block_number=block_number, timestamp=block_timestamp ): return Header.EMPTY_FIELD - return sum([Spec.get_total_blob_gas(tx) for tx in txs]) + return sum([Spec.get_total_blob_gas(tx) for tx in txs if not tx.error]) @pytest.fixture @@ -396,27 +426,86 @@ def expected_excess_blob_gas( @pytest.fixture -def blocks( - expected_blob_gas_used: Optional[int | Removable], +def header_verify( + txs: List[Transaction], + expected_blob_gas_used_from_transition_tool: Optional[int | Removable], expected_excess_blob_gas: Optional[int | Removable], +) -> Header: + """ + Header fields to verify from the transition tool. + """ + header_verify = Header() + header_verify.blob_gas_used = expected_blob_gas_used_from_transition_tool + header_verify.excess_blob_gas = expected_excess_blob_gas + if len([tx for tx in txs if not tx.error]) == 0: + header_verify.gas_used = 0 + return header_verify + + +@pytest.fixture +def all_blob_gas_used( + fork: Fork, + txs: List[Transaction], + block_number: int, + block_timestamp: int, +) -> Optional[int | Removable]: + """ + Calculates the blob gas used by the test block taking into account failed transactions. + """ + if not fork.header_blob_gas_used_required( + block_number=block_number, timestamp=block_timestamp + ): + return Header.EMPTY_FIELD + return sum([Spec.get_total_blob_gas(tx) for tx in txs]) + + +@pytest.fixture +def rlp_modifier( + all_blob_gas_used: Optional[int | Removable], +) -> Optional[Header]: + """ + Header fields to modify on the output block in the BlockchainTest. + """ + if all_blob_gas_used == Header.EMPTY_FIELD: + return None + return Header( + blob_gas_used=all_blob_gas_used, + ) + + +@pytest.fixture +def test_block( txs: List[Transaction], block_error: Optional[str], engine_api_error_code: Optional[EngineAPIError], + header_verify: Optional[Header], + rlp_modifier: Optional[Header], +) -> Block: + """ + Test block for all test cases. + """ + return Block( + txs=txs, + exception=block_error, + engine_api_error_code=engine_api_error_code, + header_verify=header_verify, + rlp_modifier=rlp_modifier, + ) + + +@pytest.fixture +def blocks( + non_zero_blob_gas_used_genesis_block: Block, + test_block: Block, ) -> List[Block]: """ Prepare the list of blocks for all test cases. """ - return [ - Block( - txs=txs, - exception=block_error, - engine_api_error_code=engine_api_error_code, - header_verify=Header( - blob_gas_used=expected_blob_gas_used, - excess_blob_gas=expected_excess_blob_gas, - ), - ) - ] + return ( + [test_block] + if non_zero_blob_gas_used_genesis_block is None + else [non_zero_blob_gas_used_genesis_block, test_block] + ) def all_valid_blob_combinations() -> List[Tuple[int, ...]]: @@ -500,21 +589,22 @@ def test_valid_blob_tx_combinations( "parent_excess_blobs,parent_blobs,tx_max_fee_per_blob_gas,tx_error", [ # tx max_blob_gas_cost of the transaction is not enough - ( + pytest.param( SpecHelpers.get_min_excess_blobs_for_blob_gas_price(2) - 1, # blob gas price is 1 SpecHelpers.target_blobs_per_block() + 1, # blob gas cost increases to 2 1, # tx max_blob_gas_cost is 1 "insufficient max fee per blob gas", + id="insufficient_max_fee_per_blob_gas", ), # tx max_blob_gas_cost of the transaction is zero, which is invalid - ( + pytest.param( 0, # blob gas price is 1 0, # blob gas cost stays put at 1 0, # tx max_blob_gas_cost is 0 "invalid max fee per blob gas", + id="invalid_max_fee_per_blob_gas", ), ], - ids=["insufficient_max_fee_per_blob_gas", "invalid_max_fee_per_blob_gas"], ) @pytest.mark.valid_from("Cancun") def test_invalid_tx_max_fee_per_blob_gas( @@ -522,8 +612,6 @@ def test_invalid_tx_max_fee_per_blob_gas( pre: Dict, env: Environment, blocks: List[Block], - parent_blobs: int, - non_zero_blob_gas_used_genesis_block: Block, ): """ Reject blocks with invalid blob txs due to: @@ -531,12 +619,6 @@ def test_invalid_tx_max_fee_per_blob_gas( - tx max_fee_per_blob_gas is barely not enough - tx max_fee_per_blob_gas is zero """ - if parent_blobs: - pre[TestAddress2] = Account(balance=10**9) - blocks.insert(0, non_zero_blob_gas_used_genesis_block) - if env.excess_blob_gas is not None: - assert isinstance(env.excess_blob_gas, int) - env.excess_blob_gas += Spec.TARGET_BLOB_GAS_PER_BLOCK blockchain_test( pre=pre, post={}, @@ -545,6 +627,49 @@ def test_invalid_tx_max_fee_per_blob_gas( ) +@pytest.mark.parametrize( + "parent_excess_blobs,parent_blobs,tx_max_fee_per_blob_gas,tx_error", + [ + # tx max_blob_gas_cost of the transaction is not enough + pytest.param( + SpecHelpers.get_min_excess_blobs_for_blob_gas_price(2) - 1, # blob gas price is 1 + SpecHelpers.target_blobs_per_block() + 1, # blob gas cost increases to 2 + 1, # tx max_blob_gas_cost is 1 + "insufficient max fee per blob gas", + id="insufficient_max_fee_per_blob_gas", + ), + # tx max_blob_gas_cost of the transaction is zero, which is invalid + pytest.param( + 0, # blob gas price is 1 + 0, # blob gas cost stays put at 1 + 0, # tx max_blob_gas_cost is 0 + "invalid max fee per blob gas", + id="invalid_max_fee_per_blob_gas", + ), + ], +) +@pytest.mark.valid_from("Cancun") +def test_invalid_tx_max_fee_per_blob_gas_state( + state_test_only: StateTestFiller, + state_env: Environment, + pre: Dict, + txs: List[Transaction], +): + """ + Reject blocks with invalid blob txs due to: + + - tx max_fee_per_blob_gas is barely not enough + - tx max_fee_per_blob_gas is zero + """ + assert len(txs) == 1 + state_test_only( + pre=pre, + post={}, + tx=txs[0], + env=state_env, + ) + + @pytest.mark.parametrize( "tx_max_fee_per_gas,tx_error", [ @@ -558,21 +683,26 @@ def test_invalid_tx_max_fee_per_blob_gas( ) @pytest.mark.valid_from("Cancun") def test_invalid_normal_gas( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, + state_env: Environment, pre: Dict, - env: Environment, - blocks: List[Block], + txs: List[Transaction], + header_verify: Optional[Header], + rlp_modifier: Optional[Header], ): """ Reject blocks with invalid blob txs due to: - Sufficient max fee per blob gas, but insufficient max fee per gas """ - blockchain_test( + assert len(txs) == 1 + state_test( pre=pre, post={}, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, + blockchain_test_header_verify=header_verify, + blockchain_test_rlp_modifier=rlp_modifier, ) @@ -580,7 +710,7 @@ def test_invalid_normal_gas( "blobs_per_tx", invalid_blob_combinations(), ) -@pytest.mark.parametrize("block_error", ["invalid_blob_count"]) +@pytest.mark.parametrize("tx_error", ["maximum blob gas allowance exceeded"]) @pytest.mark.valid_from("Cancun") def test_invalid_block_blob_count( blockchain_test: BlockchainTestFiller, @@ -595,6 +725,11 @@ def test_invalid_block_blob_count( This test is parametrized with all blob transaction combinations exceeding `MAX_BLOBS_PER_BLOCK` by one for a given block, and therefore if value of `MAX_BLOBS_PER_BLOCK` changes, this test is automatically updated. + + TODO (marioevz): When InclusionTest format is developed, this needs to generate such test also. + InclusionTest: Format similar to BlockchainTest that, instead of doing full block validation, + it only validates whether the last transaction is a valid transaction to be included in the + block or not. """ blockchain_test( pre=pre, @@ -616,10 +751,10 @@ def test_invalid_block_blob_count( @pytest.mark.parametrize("tx_error", ["insufficient_account_balance"], ids=[""]) @pytest.mark.valid_from("Cancun") def test_insufficient_balance_blob_tx( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, + state_env: Environment, pre: Dict, - env: Environment, - blocks: List[Block], + txs: List[Transaction], ): """ Reject blocks where user cannot afford the blob gas specified (but @@ -630,11 +765,12 @@ def test_insufficient_balance_blob_tx( - Transactions with and without calldata - Transactions with max fee per blob gas lower or higher than the priority fee """ - blockchain_test( + assert len(txs) == 1 + state_test( pre=pre, post={}, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -666,19 +802,21 @@ def test_insufficient_balance_blob_tx_combinations( @pytest.mark.parametrize( - "blobs_per_tx,tx_error,block_error", + "blobs_per_tx,tx_error", [ - ([0], "zero blob tx", "zero blob tx"), - ([SpecHelpers.max_blobs_per_block() + 1], None, "too many blobs"), + ([0], "zero blob tx"), + ([SpecHelpers.max_blobs_per_block() + 1], "too many blobs"), ], ids=["too_few_blobs", "too_many_blobs"], ) @pytest.mark.valid_from("Cancun") def test_invalid_tx_blob_count( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, + state_env: Environment, pre: Dict, - env: Environment, - blocks: List[Block], + txs: List[Transaction], + header_verify: Optional[Header], + rlp_modifier: Optional[Header], ): """ Reject blocks that include blob transactions with invalid blob counts: @@ -686,11 +824,14 @@ def test_invalid_tx_blob_count( - `blob count == 0` in type 3 transaction - `blob count > MAX_BLOBS_PER_BLOCK` in type 3 transaction """ - blockchain_test( + assert len(txs) == 1 + state_test( pre=pre, post={}, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, + blockchain_test_header_verify=header_verify, + blockchain_test_rlp_modifier=rlp_modifier, ) @@ -707,6 +848,45 @@ def test_invalid_tx_blob_count( [to_hash_bytes(1)] + add_kzg_version([to_hash_bytes(2)], Spec.BLOB_COMMITMENT_VERSION_KZG) ], + ], + ids=[ + "single_blob", + "multiple_blobs", + "multiple_blobs_single_bad_hash_1", + "multiple_blobs_single_bad_hash_2", + ], +) +@pytest.mark.parametrize("tx_error", ["invalid blob versioned hash"], ids=[""]) +@pytest.mark.valid_from("Cancun") +def test_invalid_blob_hash_versioning_single_tx( + state_test: StateTestFiller, + state_env: Environment, + pre: Dict, + txs: List[Transaction], + header_verify: Optional[Header], + rlp_modifier: Optional[Header], +): + """ + Reject blob transactions with invalid blob hash version, including: + + - Transaction with single blob with invalid version + - Transaction with multiple blobs all with invalid version + - Transaction with multiple blobs either with invalid version + """ + assert len(txs) == 1 + state_test( + pre=pre, + post={}, + tx=txs[0], + env=state_env, + blockchain_test_header_verify=header_verify, + blockchain_test_rlp_modifier=rlp_modifier, + ) + + +@pytest.mark.parametrize( + "blob_hashes_per_tx", + [ [ add_kzg_version([to_hash_bytes(1)], Spec.BLOB_COMMITMENT_VERSION_KZG), [to_hash_bytes(2)], @@ -727,19 +907,15 @@ def test_invalid_tx_blob_count( ], ], ids=[ - "single_tx_single_blob", - "single_tx_multiple_blobs", - "single_tx_multiple_blobs_single_bad_hash_1", - "single_tx_multiple_blobs_single_bad_hash_2", - "multiple_txs_single_blob", - "multiple_txs_multiple_blobs", - "multiple_txs_multiple_blobs_single_bad_hash_1", - "multiple_txs_multiple_blobs_single_bad_hash_2", + "single_blob", + "multiple_blobs", + "multiple_blobs_single_bad_hash_1", + "multiple_blobs_single_bad_hash_2", ], ) @pytest.mark.parametrize("tx_error", ["invalid blob versioned hash"], ids=[""]) @pytest.mark.valid_from("Cancun") -def test_invalid_blob_hash_versioning( +def test_invalid_blob_hash_versioning_multiple_txs( blockchain_test: BlockchainTestFiller, pre: Dict, env: Environment, @@ -749,9 +925,6 @@ def test_invalid_blob_hash_versioning( Reject blocks that include blob transactions with invalid blob hash version, including: - - Single blob transaction with single blob with invalid version - - Single blob transaction with multiple blobs all with invalid version - - Single blob transaction with multiple blobs either with invalid version - Multiple blob transactions with single blob all with invalid version - Multiple blob transactions with multiple blobs all with invalid version - Multiple blob transactions with multiple blobs only one with invalid version @@ -767,7 +940,6 @@ def test_invalid_blob_hash_versioning( @pytest.mark.parametrize( "destination_account,tx_error", [(None, "no_contract_creating_blob_txs")], ids=[""] ) -# TODO: Uncomment after #242 -> https://github.com/ethereum/execution-spec-tests/issues/242 @pytest.mark.skip(reason="Unable to fill due to invalid field in transaction") @pytest.mark.valid_from("Cancun") def test_invalid_blob_tx_contract_creation( @@ -854,11 +1026,11 @@ def opcode( @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") def test_blob_tx_attribute_opcodes( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, opcode: Tuple[bytes, Storage.StorageDictType], - env: Environment, - blocks: List[Block], + state_env: Environment, + txs: List[Transaction], destination_account: str, ): """ @@ -867,6 +1039,7 @@ def test_blob_tx_attribute_opcodes( - ORIGIN - CALLER """ + assert len(txs) == 1 code, storage = opcode pre[destination_account] = Account(code=code) post = { @@ -874,11 +1047,11 @@ def test_blob_tx_attribute_opcodes( storage=storage, ) } - blockchain_test( + state_test( pre=pre, post=post, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -887,17 +1060,18 @@ def test_blob_tx_attribute_opcodes( @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") def test_blob_tx_attribute_value_opcode( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, opcode: Tuple[bytes, Storage.StorageDictType], - env: Environment, - blocks: List[Block], + state_env: Environment, + txs: List[Transaction], tx_value: int, destination_account: str, ): """ Test the VALUE opcode with different blob type transaction value amounts. """ + assert len(txs) == 1 code, storage = opcode pre[destination_account] = Account(code=code) post = { @@ -906,11 +1080,11 @@ def test_blob_tx_attribute_value_opcode( balance=tx_value, ) } - blockchain_test( + state_test( pre=pre, post=post, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -935,11 +1109,11 @@ def test_blob_tx_attribute_value_opcode( @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") def test_blob_tx_attribute_calldata_opcodes( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, opcode: Tuple[bytes, Storage.StorageDictType], - env: Environment, - blocks: List[Block], + state_env: Environment, + txs: List[Transaction], destination_account: str, ): """ @@ -949,6 +1123,7 @@ def test_blob_tx_attribute_calldata_opcodes( - CALLDATASIZE - CALLDATACOPY """ + assert len(txs) == 1 code, storage = opcode pre[destination_account] = Account(code=code) post = { @@ -956,11 +1131,11 @@ def test_blob_tx_attribute_calldata_opcodes( storage=storage, ) } - blockchain_test( + state_test( pre=pre, post=post, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -971,11 +1146,11 @@ def test_blob_tx_attribute_calldata_opcodes( @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") def test_blob_tx_attribute_gasprice_opcode( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, opcode: Tuple[bytes, Storage.StorageDictType], - env: Environment, - blocks: List[Block], + state_env: Environment, + txs: List[Transaction], destination_account: str, ): """ @@ -986,6 +1161,7 @@ def test_blob_tx_attribute_gasprice_opcode( - Priority fee below data fee - Priority fee above data fee """ + assert len(txs) == 1 code, storage = opcode pre[destination_account] = Account(code=code) post = { @@ -993,11 +1169,11 @@ def test_blob_tx_attribute_gasprice_opcode( storage=storage, ) } - blockchain_test( + state_test( pre=pre, post=post, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -1016,9 +1192,9 @@ def test_blob_tx_attribute_gasprice_opcode( ) @pytest.mark.valid_at_transition_to("Cancun") def test_blob_type_tx_pre_fork( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, - blocks: List[Block], + txs: List[Transaction], ): """ Reject blocks with blob type transactions before Cancun fork. @@ -1026,9 +1202,10 @@ def test_blob_type_tx_pre_fork( Blocks sent by NewPayloadV2 (Shanghai) that contain blob type transactions, furthermore blobs field within NewPayloadV2 method must be computed as INVALID, due to an invalid block hash. """ - blockchain_test( + assert len(txs) == 1 + state_test( pre=pre, post={}, - blocks=blocks, - genesis_environment=Environment(), # `env` fixture has blob fields + tx=txs[0], + env=Environment(), # `env` fixture has blob fields )