Skip to content

Commit

Permalink
new(tests): EIP-7623: Add gas consumption tests
Browse files Browse the repository at this point in the history
  • Loading branch information
marioevz committed Dec 11, 2024
1 parent fda5ed3 commit f6fb3cf
Show file tree
Hide file tree
Showing 4 changed files with 674 additions and 304 deletions.
340 changes: 340 additions & 0 deletions tests/prague/eip7623_increase_calldata_cost/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
"""
Fixtures for the EIP-7623 tests.
"""

from typing import Callable, List, Sequence

import pytest

from ethereum_test_forks import Fork
from ethereum_test_tools import (
EOA,
AccessList,
Address,
Alloc,
AuthorizationTuple,
Bytecode,
Bytes,
Hash,
)
from ethereum_test_tools import Opcodes as Op
from ethereum_test_tools import Transaction, TransactionException

from .helpers import DataTestType


@pytest.fixture
def sender(pre: Alloc) -> EOA:
"""
Create the sender account.
"""
return pre.fund_eoa()


@pytest.fixture
def to(
request: pytest.FixtureRequest,
pre: Alloc,
) -> Address | None:
"""
Create the sender account.
"""
if hasattr(request, "param"):
param = request.param
else:
param = Op.STOP

if param is None:
return None
if isinstance(param, Address):
return param
if isinstance(param, Bytecode):
return pre.deploy_contract(param)

raise ValueError(f"Invalid value for `to` fixture: {param}")


@pytest.fixture
def protected() -> bool:
"""
Whether the transaction is protected or not. Only valid for type-0 transactions.
"""
return True


@pytest.fixture
def access_list() -> List[AccessList] | None:
"""
Access list for the transaction.
"""
return None


@pytest.fixture
def authorization_existing_authority() -> bool:
"""
Whether the transaction has an existing authority in the authorization list.
"""
return False


@pytest.fixture
def authorization_list(
request: pytest.FixtureRequest,
pre: Alloc,
authorization_existing_authority: bool,
) -> List[AuthorizationTuple] | None:
"""
Authorization list for the transaction.
This fixture needs to be parametrized indirectly in order to generate the authorizations with
valid signers using `pre` in this function, and the parametrized value should be a list of
addresses.
"""
if not hasattr(request, "param"):
return None
if request.param is None:
return None
return [
AuthorizationTuple(
signer=pre.fund_eoa(1 if authorization_existing_authority else 0), address=address
)
for address in request.param
]


@pytest.fixture
def blob_versioned_hashes() -> Sequence[Hash] | None:
"""
Versioned hashes for the transaction.
"""
return None


@pytest.fixture
def contract_creating_tx(to: Address | None) -> bool:
"""
Whether the transaction creates a contract or not.
"""
return to is None


def floor_cost_find(
floor_data_gas_cost_calculator: Callable[[int], int],
intrinsic_gas_cost_calculator: Callable[[int], int],
) -> int:
"""
Find the minimum amount of tokens that will trigger the floor gas cost, by using a binary
search and the intrinsic gas cost and floor data calculators.
"""
# Start with 1000 tokens and if the intrinsic gas cost is greater than the floor gas cost,
# multiply the number of tokens by 2 until it's not.
tokens = 1000
while floor_data_gas_cost_calculator(tokens) < intrinsic_gas_cost_calculator(tokens):
tokens *= 2

# Binary search to find the minimum number of tokens that will trigger the floor gas cost.
left = 0
right = tokens
while left < right:
tokens = (left + right) // 2
if floor_data_gas_cost_calculator(tokens) < intrinsic_gas_cost_calculator(tokens):
left = tokens + 1
else:
right = tokens
tokens = left

if floor_data_gas_cost_calculator(tokens) > intrinsic_gas_cost_calculator(tokens):
tokens -= 1

# Verify that increasing the tokens by one would always trigger the floor gas cost.
assert (
floor_data_gas_cost_calculator(tokens) <= intrinsic_gas_cost_calculator(tokens)
) and floor_data_gas_cost_calculator(tokens + 1) > intrinsic_gas_cost_calculator(
tokens + 1
), "invalid case"

return tokens


@pytest.fixture
def intrinsic_gas_data_floor_minimum_delta() -> int:
"""
Induce a minimum delta between the transaction intrinsic gas cost and the
floor data gas cost.
"""
return 0


@pytest.fixture
def tx_data(
fork: Fork,
data_test_type: DataTestType,
access_list: List[AccessList] | None,
authorization_list: List[AuthorizationTuple] | None,
contract_creating_tx: bool,
intrinsic_gas_data_floor_minimum_delta: int,
) -> Bytes:
"""
All tests in this file use data that is generated dynamically depending on the case and the
attributes of the transaction in order to reach the edge cases where the floor gas cost is
equal or barely greater than the intrinsic gas cost.
We have two different types of tests:
- FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS: The floor gas cost is less than or equal
to the intrinsic gas cost, which means that the size of the tokens in the data are not
enough to trigger the floor gas cost.
- FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS: The floor gas cost is greater than the intrinsic
gas cost, which means that the size of the tokens in the data are enough to trigger the
floor gas cost.
E.g. Given a transaction with a single access list and a single storage key, its intrinsic gas
cost (as of Prague fork) can be calculated as:
- 21,000 gas for the transaction
- 2,400 gas for the access list
- 1,900 gas for the storage key
- 16 gas for each non-zero byte in the data
- 4 gas for each zero byte in the data
Its floor data gas cost can be calculated as:
- 21,000 gas for the transaction
- 40 gas for each non-zero byte in the data
- 10 gas for each zero byte in the data
Notice that the data included in the transaction affects both the intrinsic gas cost and the
floor data cost, but at different rates.
The purpose of this function is to find the exact amount of data where the floor data gas
cost starts exceeding the intrinsic gas cost.
After a binary search we find that adding 717 tokens of data (179 non-zero bytes +
1 zero byte) triggers the floor gas cost.
Therefore, this function will return a Bytes object with 179 non-zero bytes and 1 zero byte
for `FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS` and a Bytes object with 179 non-zero bytes
and no zero bytes for `FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS`
"""

def tokens_to_data(tokens: int) -> Bytes:
return Bytes(b"\x01" * (tokens // 4) + b"\x00" * (tokens % 4))

fork_intrinsic_cost_calculator = fork.transaction_intrinsic_cost_calculator()

def transaction_intrinsic_cost_calculator(tokens: int) -> int:
return (
fork_intrinsic_cost_calculator(
calldata=tokens_to_data(tokens),
contract_creation=contract_creating_tx,
access_list=access_list,
authorization_list_or_count=authorization_list,
return_cost_deducted_prior_execution=True,
)
+ intrinsic_gas_data_floor_minimum_delta
)

fork_data_floor_cost_calculator = fork.transaction_data_floor_cost_calculator()

def transaction_data_floor_cost_calculator(tokens: int) -> int:
return fork_data_floor_cost_calculator(data=tokens_to_data(tokens))

# Start with zero data and check the difference in the gas calculator between the
# intrinsic gas cost and the floor gas cost.
if transaction_data_floor_cost_calculator(0) >= transaction_intrinsic_cost_calculator(0):
# Special case which is a transaction with no extra intrinsic gas costs other than the
# data cost, any data will trigger the floor gas cost.
if data_test_type == DataTestType.FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS:
return Bytes(b"")
else:
return Bytes(b"\0")

tokens = floor_cost_find(
floor_data_gas_cost_calculator=transaction_data_floor_cost_calculator,
intrinsic_gas_cost_calculator=transaction_intrinsic_cost_calculator,
)

if data_test_type == DataTestType.FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS:
return tokens_to_data(tokens + 1)
return tokens_to_data(tokens)


@pytest.fixture
def tx_gas_delta() -> int:
"""
Gas delta to modify the gas amount included with the transaction.
If negative, the transaction will be invalid because the intrinsic gas cost is greater than the
gas limit.
This value operates regardless of whether the floor data gas cost is reached or not.
If the value is greater than zero, the transaction will also be valid and the test will check
that transaction processing does not consume more gas than it should.
"""
return 0


@pytest.fixture
def tx_gas(
fork: Fork,
tx_data: Bytes,
access_list: List[AccessList] | None,
authorization_list: List[AuthorizationTuple] | None,
contract_creating_tx: bool,
tx_gas_delta: int,
) -> int:
"""
Gas limit for the transaction.
The calculated value takes into account the normal intrinsic gas cost and the floor data gas
cost.
The gas delta is added to the intrinsic gas cost to generate different test scenarios.
"""
intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
return (
intrinsic_gas_cost_calculator(
calldata=tx_data,
contract_creation=contract_creating_tx,
access_list=access_list,
authorization_list_or_count=authorization_list,
)
+ tx_gas_delta
)


@pytest.fixture
def tx_error(tx_gas_delta: int) -> TransactionException | None:
"""
Transaction error, only expected if the gas delta is negative.
"""
return TransactionException.INTRINSIC_GAS_TOO_LOW if tx_gas_delta < 0 else None


@pytest.fixture
def tx(
sender: EOA,
ty: int,
tx_data: Bytes,
to: Address | None,
protected: bool,
access_list: List[AccessList] | None,
authorization_list: List[AuthorizationTuple] | None,
blob_versioned_hashes: Sequence[Hash] | None,
tx_gas: int,
tx_error: TransactionException | None,
) -> Transaction:
"""
Create the transaction used in each test.
"""
return Transaction(
ty=ty,
sender=sender,
data=tx_data,
to=to,
protected=protected,
access_list=access_list,
authorization_list=authorization_list,
gas_limit=tx_gas,
blob_versioned_hashes=blob_versioned_hashes,
error=tx_error,
)
24 changes: 24 additions & 0 deletions tests/prague/eip7623_increase_calldata_cost/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Helpers for testing EIP-7623.
"""

from enum import Enum, auto


class DataTestType(Enum):
"""
Enum for the different types of data tests.
"""

FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS = auto()
FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS = auto()


class GasTestType(Enum):
"""
Enum for the different types of gas tests.
"""

CONSUME_ZERO_GAS = auto()
CONSUME_ALL_GAS = auto()
CONSUME_ALL_GAS_WITH_REFUND = auto()
Loading

0 comments on commit f6fb3cf

Please sign in to comment.