Skip to content

Commit

Permalink
feat(forks): Add transaction_intrinsic_cost_calculator
Browse files Browse the repository at this point in the history
  • Loading branch information
marioevz committed Aug 30, 2024
1 parent b946a02 commit b933107
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 10 deletions.
30 changes: 29 additions & 1 deletion src/ethereum_test_forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from semver import Version

from ethereum_test_base_types import Address
from ethereum_test_base_types import AccessList, Address
from ethereum_test_base_types.conversions import BytesConvertible
from ethereum_test_vm import EVMCodeType, Opcodes

Expand Down Expand Up @@ -51,6 +51,24 @@ def __call__(self, *, data: BytesConvertible) -> int:
pass


class TransactionIntrinsicCostCalculator(Protocol):
"""
A protocol to calculate the intrinsic gas cost of a transaction for a given fork.
"""

def __call__(
self,
*,
calldata: BytesConvertible = b"",
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
) -> int:
"""
Returns the intrinsic gas cost of a transaction given its properties.
"""
pass


class BaseForkMeta(ABCMeta):
"""
Metaclass for BaseFork
Expand Down Expand Up @@ -217,6 +235,16 @@ def calldata_gas_calculator(
"""
pass

@classmethod
@abstractmethod
def transaction_intrinsic_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
) -> TransactionIntrinsicCostCalculator:
"""
Returns a callable that calculates the intrinsic gas cost of a transaction for the fork.
"""
pass

@classmethod
@abstractmethod
def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int:
Expand Down
97 changes: 95 additions & 2 deletions src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@

from semver import Version

from ethereum_test_base_types import Address, Bytes
from ethereum_test_base_types import AccessList, Address, Bytes
from ethereum_test_base_types.conversions import BytesConvertible
from ethereum_test_vm import EVMCodeType, Opcodes

from ..base_fork import BaseFork, CalldataGasCalculator, MemoryExpansionGasCalculator
from ..base_fork import (
BaseFork,
CalldataGasCalculator,
MemoryExpansionGasCalculator,
TransactionIntrinsicCostCalculator,
)
from ..gas_costs import GasCosts

CURRENT_FILE = Path(realpath(__file__))
Expand Down Expand Up @@ -189,6 +194,34 @@ def fn(*, data: BytesConvertible) -> int:

return fn

@classmethod
def transaction_intrinsic_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
) -> TransactionIntrinsicCostCalculator:
"""
Returns a callable that calculates the intrinsic gas cost of a transaction for the fork.
"""
gas_costs = cls.gas_costs(block_number, timestamp)
calldata_gas_calculator = cls.calldata_gas_calculator(block_number, timestamp)

def fn(
*,
calldata: BytesConvertible = b"",
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
) -> int:
assert access_list is None, f"Access list is not supported in {cls.name()}"
intrinsic_cost: int = gas_costs.G_TRANSACTION

if contract_creation:
intrinsic_cost += gas_costs.G_INITCODE_WORD * ceiling_division(
len(Bytes(calldata)), 32
)

return intrinsic_cost + calldata_gas_calculator(data=calldata)

return fn

@classmethod
def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int:
"""
Expand Down Expand Up @@ -500,6 +533,35 @@ def valid_opcodes(
"""
return [Opcodes.DELEGATECALL] + super(Homestead, cls).valid_opcodes()

@classmethod
def transaction_intrinsic_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
) -> TransactionIntrinsicCostCalculator:
"""
At Homestead, the transaction intrinsic cost needs to take contract creation into account.
"""
super_fn = super(Homestead, cls).transaction_intrinsic_cost_calculator(
block_number, timestamp
)
gas_costs = cls.gas_costs(block_number, timestamp)

def fn(
*,
calldata: BytesConvertible = b"",
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
) -> int:
intrinsic_cost: int = super_fn(
calldata=calldata,
contract_creation=contract_creation,
access_list=access_list,
)
if contract_creation:
intrinsic_cost += gas_costs.G_TRANSACTION_CREATE
return intrinsic_cost

return fn


class Byzantium(Homestead):
"""
Expand Down Expand Up @@ -654,6 +716,37 @@ def contract_creating_tx_types(cls, block_number: int = 0, timestamp: int = 0) -
"""
return [1] + super(Berlin, cls).contract_creating_tx_types(block_number, timestamp)

@classmethod
def transaction_intrinsic_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
) -> TransactionIntrinsicCostCalculator:
"""
At Berlin, the transaction intrinsic cost needs to take the access list into account
"""
super_fn = super(Berlin, cls).transaction_intrinsic_cost_calculator(
block_number, timestamp
)
gas_costs = cls.gas_costs(block_number, timestamp)

def fn(
*,
calldata: BytesConvertible = b"",
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
) -> int:
intrinsic_cost: int = super_fn(
calldata=calldata,
contract_creation=contract_creation,
)
if access_list is not None:
for access in access_list:
intrinsic_cost += gas_costs.G_ACCESS_LIST_ADDRESS
for _ in access.storage_keys:
intrinsic_cost += gas_costs.G_ACCESS_LIST_STORAGE
return intrinsic_cost

return fn


class London(Berlin):
"""
Expand Down
67 changes: 60 additions & 7 deletions src/ethereum_test_forks/tests/test_forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,27 @@

from typing import Mapping, cast

import pytest
from semver import Version

from ..base_fork import Fork
from ..forks.forks import Berlin, Cancun, Frontier, London, Paris, Prague, Shanghai
from ..forks.forks import (
Berlin,
Cancun,
Frontier,
Homestead,
Istanbul,
London,
Paris,
Prague,
Shanghai,
)
from ..forks.transition import BerlinToLondonAt5, ParisToShanghaiAtTime15k
from ..helpers import (
forks_from,
forks_from_until,
get_closest_fork_with_solc_support,
get_deployed_forks,
get_development_forks,
get_forks,
get_forks_with_solc_support,
transition_fork_from_to,
Expand Down Expand Up @@ -196,7 +206,7 @@ class PreAllocTransitionFork(PrePreAllocFork):
pass


def test_pre_alloc():
def test_pre_alloc(): # noqa: D103
assert PrePreAllocFork.pre_allocation() == {"test": "test"}
assert PreAllocFork.pre_allocation() == {"test": "test", "test2": "test2"}
assert PreAllocTransitionFork.pre_allocation() == {
Expand All @@ -209,21 +219,64 @@ def test_pre_alloc():
}


def test_precompiles():
def test_precompiles(): # noqa: D103
Cancun.precompiles() == list(range(11))[1:]


def test_tx_types():
def test_tx_types(): # noqa: D103
Cancun.tx_types() == list(range(4))


def test_solc_versioning():
def test_solc_versioning(): # noqa: D103
assert len(get_forks_with_solc_support(Version.parse("0.8.20"))) == 13
assert len(get_forks_with_solc_support(Version.parse("0.8.24"))) > 13


def test_closest_fork_supported_by_solc():
def test_closest_fork_supported_by_solc(): # noqa: D103
assert get_closest_fork_with_solc_support(Paris, Version.parse("0.8.20")) == Paris
assert get_closest_fork_with_solc_support(Cancun, Version.parse("0.8.20")) == Shanghai
assert get_closest_fork_with_solc_support(Cancun, Version.parse("0.8.24")) == Cancun
assert get_closest_fork_with_solc_support(Prague, Version.parse("0.8.24")) == Cancun


@pytest.mark.parametrize(
"fork",
[
pytest.param(Berlin, id="Berlin"),
pytest.param(Istanbul, id="Istanbul"),
pytest.param(Homestead, id="Homestead"),
pytest.param(Frontier, id="Frontier"),
],
)
@pytest.mark.parametrize(
"calldata",
[
pytest.param(b"\0", id="zero-data"),
pytest.param(b"\1", id="non-zero-data"),
],
)
@pytest.mark.parametrize(
"create_tx",
[False, True],
)
def test_tx_intrinsic_gas_functions(fork: Fork, calldata: bytes, create_tx: bool): # noqa: D103
intrinsic_gas = 21_000
if calldata == b"\0":
intrinsic_gas += 4
else:
if fork >= Istanbul:
intrinsic_gas += 16
else:
intrinsic_gas += 68

if create_tx:
if fork >= Homestead:
intrinsic_gas += 32000
intrinsic_gas += 2
assert (
fork.transaction_intrinsic_cost_calculator()(
calldata=calldata,
contract_creation=create_tx,
)
== intrinsic_gas
)

0 comments on commit b933107

Please sign in to comment.