Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions tests/osaka/eip7823_modexp_upper_bounds/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""Conftest for EIP-7823 tests."""

from typing import Dict

import pytest

from ethereum_test_forks import Fork, Osaka
from ethereum_test_tools import Account, Address, Alloc, Storage, Transaction, keccak256
from ethereum_test_tools.vm.opcode import Opcodes as Op
from ethereum_test_types import Environment

from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput
from ..eip7883_modexp_gas_increase.spec import Spec, Spec7883


@pytest.fixture
def call_contract_post_storage() -> Storage:
"""
Storage of the test contract after the transaction is executed.
Note: Fixture `call_contract_code` fills the actual expected storage values.
"""
return Storage()


@pytest.fixture
def call_succeeds(
total_gas_used: int, fork: Fork, env: Environment, modexp_input: ModExpInput
) -> bool:
"""
By default, depending on the expected output, we can deduce if the call is expected to succeed
or fail.
"""
# Transaction gas limit exceeded
tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit
if total_gas_used > tx_gas_limit_cap:
return False

# Input length exceeded
base_length, exp_length, mod_length = modexp_input.get_declared_lengths()
if (
base_length > Spec.MAX_LENGTH_BYTES
or exp_length > Spec.MAX_LENGTH_BYTES
or mod_length > Spec.MAX_LENGTH_BYTES
) and fork >= Osaka:
return False

return True


@pytest.fixture
def gas_measure_contract(
pre: Alloc,
fork: Fork,
modexp_expected: bytes,
precompile_gas: int,
call_contract_post_storage: Storage,
call_succeeds: bool,
) -> Address:
"""
Deploys a contract that measures ModExp gas consumption and execution result.

Always stored:
storage[0]: precompile call success
storage[1]: return data length from precompile
Only if the precompile call succeeds:
storage[2]: gas consumed by precompile
storage[3]: hash of return data from precompile
"""
call_code = Op.CALL(
precompile_gas,
Spec.MODEXP_ADDRESS,
0,
0,
Op.CALLDATASIZE(),
0,
0,
)

gas_costs = fork.gas_costs()
extra_gas = (
gas_costs.G_WARM_ACCOUNT_ACCESS
+ (gas_costs.G_VERY_LOW * (len(Op.CALL.kwargs) - 1)) # type: ignore
+ gas_costs.G_BASE # CALLDATASIZE
+ gas_costs.G_BASE # GAS
)

# Build the gas measurement contract code
# Stack operations:
# [gas_start]
# [gas_start, call_result]
# [gas_start, call_result, gas_end]
# [gas_start, gas_end, call_result]
call_result_measurement = Op.GAS + call_code + Op.GAS + Op.SWAP1

# Calculate gas consumed: gas_start - (gas_end + extra_gas)
# Stack Operation:
# [gas_start, gas_end]
# [gas_start, gas_end, extra_gas]
# [gas_start, gas_end + extra_gas]
# [gas_end + extra_gas, gas_start]
# [gas_consumed]
gas_calculation = Op.PUSH2[extra_gas] + Op.ADD + Op.SWAP1 + Op.SUB

code = (
Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE)
+ Op.SSTORE(call_contract_post_storage.store_next(call_succeeds), call_result_measurement)
+ Op.SSTORE(
call_contract_post_storage.store_next(len(modexp_expected) if call_succeeds else 0),
Op.RETURNDATASIZE(),
)
)

if call_succeeds:
code += Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation)
code += Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE())
code += Op.SSTORE(
call_contract_post_storage.store_next(keccak256(bytes(modexp_expected))),
Op.SHA3(0, Op.RETURNDATASIZE()),
)
return pre.deploy_contract(code)


@pytest.fixture
def precompile_gas(fork: Fork, modexp_input: ModExpInput) -> int:
"""Calculate gas cost for the ModExp precompile and verify it matches expected gas."""
spec = Spec if fork < Osaka else Spec7883
try:
calculated_gas = spec.calculate_gas_cost(modexp_input)
return calculated_gas
except Exception:
# Used for `test_modexp_invalid_inputs` we expect the call to not succeed.
# Return is for completeness.
return 500 if fork >= Osaka else 200


@pytest.fixture
def tx(
pre: Alloc,
gas_measure_contract: Address,
modexp_input: ModExpInput,
tx_gas_limit: int,
) -> Transaction:
"""Transaction to measure gas consumption of the ModExp precompile."""
return Transaction(
sender=pre.fund_eoa(),
to=gas_measure_contract,
data=bytes(modexp_input),
gas_limit=tx_gas_limit,
)


@pytest.fixture
def total_gas_used(
fork: Fork, modexp_expected: bytes, modexp_input: ModExpInput, precompile_gas: int
) -> int:
"""Transaction gas limit used for the test (Can be overridden in the test)."""
intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator()
extra_gas = 500_000

total_gas = (
extra_gas
+ intrinsic_gas_cost_calculator(calldata=bytes(modexp_input))
+ memory_expansion_gas_calculator(new_bytes=len(bytes(modexp_input)))
+ precompile_gas
)

return total_gas


@pytest.fixture
def tx_gas_limit(total_gas_used: int, fork: Fork, env: Environment) -> int:
"""Transaction gas limit used for the test (Can be overridden in the test)."""
tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit
return min(tx_gas_limit_cap, total_gas_used)


@pytest.fixture
def post(
gas_measure_contract: Address,
call_contract_post_storage: Storage,
) -> Dict[Address, Account]:
"""Return expected post state with gas consumption check."""
return {
gas_measure_contract: Account(storage=call_contract_post_storage),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
precompile/test/call_contexts/set_code = Covered in EIP-7702 cases
precompile/test/call_contexts/normal = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/call_contexts/delegate = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/call_contexts/static = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/call_contexts/callcode = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/call_contexts/tx_entry = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/call_contexts/initcode/CREATE = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/call_contexts/initcode/tx = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/call_contexts/set_code = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/inputs/valid = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/inputs/valid/boundary = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/inputs/all_zeros = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/inputs/invalid = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/inputs/invalid/crypto = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/inputs/invalid/corrupted = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/value_transfer/no_fee = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/out_of_bounds/max_plus_one = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/input_lengths/zero = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/input_lengths/dynamic/valid = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/input_lengths/dynamic/too_long = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/gas_usage/dynamic/exact = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/gas_usage/dynamic/oog = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/excessive_gas_usage = Covered in osaka/eip7883_modexp_gas_increase
precompile/test/fork_transition/after/warm = Covered in osaka/eip7883_modexp_gas_increase
gas_cost_changes/test/gas_updates_measurement = Covered in osaka/eip7883_modexp_gas_increase
gas_cost_changes/test/fork_transition/before = Covered in osaka/eip7883_modexp_gas_increase
gas_cost_changes/test/fork_transition/after = Covered in osaka/eip7883_modexp_gas_increase
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
precompile/test/value_transfer/fee/under = No value is required
precompile/test/value_transfer/fee/exact = No value is required
precompile/test/value_transfer/fee/over = No value is required
precompile/test/input_lengths/static/correct = The Modexp input length is not static
precompile/test/input_lengths/static/too_short = The Modexp input length is not static
precompile/test/input_lengths/static/too_long = The Modexp input length is not static
precompile/test/input_lengths/dynamic/too_short = there would be no padding for precompile
precompile/test/gas_usage/constant/oog = The Modexp gas cost is dynamic
precompile/test/gas_usage/constant/exact = The Modexp gas cost is dynamic
precompile/test/fork_transition/before/invalid_input = Modexp is not new precompile, it is still valid befork fork activation
precompile/test/fork_transition/before/zero_gas = Modexp is not new precompile, it is still valid befork fork activation
precompile/test/fork_transition/before/cold = Modexp is not new precompile, it is still valid befork fork activation
gas_cost_changes/test/out_of_gas = No Out-of-gas scenario in Modexp
system_contract = EIP does not include a new system contract
opcode = EIP does not introduce a new opcode
removed_precompile = EIP does not remove a precompile
transaction_type = EIP does not introduce a new transaction type
block_header_field = EIP does not add any new block header fields
gas_refunds_changes = EIP does not introduce any gas refund changes
blob_count_changes = EIP does not introduce any blob count changes
execution_layer_request = EIP does not introduce an execution layer request
new_transaction_validity_constraint = EIP does not introduce a new transaction validity constraint
modified_transaction_validity_constraint = EIP does not introduce a modified transaction validity constraint
block_body_field = EIP does not add any new block body fields
14 changes: 14 additions & 0 deletions tests/osaka/eip7823_modexp_upper_bounds/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Defines EIP-7823 specification constants and functions."""

from dataclasses import dataclass


@dataclass(frozen=True)
class ReferenceSpec:
"""Defines the reference spec version and git path."""

git_path: str
version: str


ref_spec_7823 = ReferenceSpec("EIPS/eip-7823.md", "c8321494fdfbfda52ad46c3515a7ca5dc86b857c")
Loading
Loading