Skip to content

London support #206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 3, 2021
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
17 changes: 13 additions & 4 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
Unreleased
v0.6.0-beta.1
-------------

Unreleased

- Features

- London support (https://github.com/ethereum/eth-tester/pull/206)
- Upgrade py-evm to v0.5.0-alpha.1 for London support
- Default to London
- Support access list transactions and dynamic fee transactions
- Transaction param support for `access_list`, `type`, `max_fee_per_gas`, `max_priority_fee_per_gas`
- Transaction receipt param support for `type` and `effective_gas_price`
- Block param support for `base_fee_per_gas`
- Support for custom mnemonic when initializing the Backend for EthTester

- Misc

- Adjust wording in README regarding genesis parameters


v0.5.0-beta.4
-------------

Released 2021-04-12

- Features

- Upgrade py-evm to v0.4.0-alpha.4 for Python 3.9 support
https://github.com/ethereum/eth-tester/pull/205
- Upgrade py-evm to v0.4.0-alpha.4 for Python 3.9 support
https://github.com/ethereum/eth-tester/pull/205
- Upgrade py-evm to v0.4.0-alpha.3, for Berlin support
Default to Berlin
https://github.com/ethereum/eth-tester/pull/204
Expand Down
272 changes: 163 additions & 109 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion eth_tester/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def get_chain_backend_class(backend_import_path=None):
backend_import_path = get_import_path(PyEVMBackend)
else:
warnings.warn(UserWarning(
"Ethereum Tester: No backend was explicitely set, and no *full* "
"Ethereum Tester: No backend was explicitly set, and no *full* "
"backends were available. Falling back to the `MockBackend` "
"which does not support all EVM functionality. Please refer to "
"the `eth-tester` documentation for information on what "
Expand Down
9 changes: 9 additions & 0 deletions eth_tester/backends/mock/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
def calculate_effective_gas_price(transaction, block):
return (
min(
transaction['max_fee_per_gas'],
transaction['max_priority_fee_per_gas'] + block['base_fee_per_gas']
)
if 'max_fee_per_gas' in transaction
else transaction['gas_price']
)
190 changes: 108 additions & 82 deletions eth_tester/backends/mock/factory.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import functools
import time

from eth_tester.backends.mock.common import (
calculate_effective_gas_price,
)
from eth_tester.utils.transactions import (
extract_transaction_type,
)
from eth_utils import (
apply_to_return_value,
is_bytes,
Expand All @@ -20,6 +26,7 @@
)

from eth_tester.backends.common import merge_genesis_overrides
from eth_tester.constants import DYNAMIC_FEE_TRANSACTION_PARAMS
from eth_tester.utils.address import (
generate_contract_address,
)
Expand All @@ -28,6 +35,8 @@
ZERO_32BYTES = b'\x00' * 32
ZERO_8BYTES = b'\x00' * 8
ZERO_ADDRESS = b'\x00' * 20
BLOCK_ELASTICITY_MULTIPLIER = 2
BASE_FEE_MAX_CHANGE_DENOMINATOR = 8


@apply_to_return_value(b'|'.join)
Expand Down Expand Up @@ -86,69 +95,53 @@ def create_transaction(transaction, block, transaction_index, is_pending, overri

@to_dict
def _fill_transaction(transaction, block, transaction_index, is_pending, overrides=None):
is_dynamic_fee_transaction = (
any(_ in transaction for _ in DYNAMIC_FEE_TRANSACTION_PARAMS)
or not any(_ in transaction for _ in DYNAMIC_FEE_TRANSACTION_PARAMS + ('gas_price',))
)

if overrides is None:
overrides = {}

if 'nonce' in overrides:
yield 'nonce', overrides['nonce']
else:
yield 'nonce', 0

if 'hash' in overrides:
if 'hash' in overrides: # else calculate hash after all fields are filled
yield 'hash', overrides['hash']
else:
# calculate hash after all fields are filled
pass

if 'from' in overrides:
yield 'from', overrides['from']
else:
yield 'from', transaction['from']

if 'gas' in overrides:
yield 'gas', overrides['gas']
else:
yield 'gas', transaction['gas']
# Here, we yield the key with the overrides value if it exists, else either the transaction
# value if it exists or a default value
yield 'nonce', overrides.get('nonce', 0)
yield 'from', overrides.get('from', transaction.get('from'))
yield 'to', overrides.get('to', transaction.get('to', b''))
yield 'data', overrides.get('data', transaction.get('data', b''))
yield 'value', overrides.get('value', transaction.get('value', 0))
yield 'gas', overrides.get('gas', transaction.get('gas'))
yield 'r', overrides.get('r', transaction.get('r', 12345))
yield 's', overrides.get('s', transaction.get('s', 67890))

if is_dynamic_fee_transaction:
# dynamic fee transaction (type = 2)
yield 'max_fee_per_gas', overrides.get(
'max_fee_per_gas', transaction.get('max_fee_per_gas', 1000000000)
)
yield 'max_priority_fee_per_gas', overrides.get(
'max_priority_fee_per_gas', transaction.get('max_priority_fee_per_gas', 1000000000)
)
yield from _yield_typed_transaction_fields(overrides, transaction)

else:
yield 'gas_price', overrides.get('gas_price', transaction.get('gas_price'))
if 'access_list' in transaction:
# access list transaction (type = 1)
yield from _yield_typed_transaction_fields(overrides, transaction)

if 'gas_price' in overrides:
yield 'gas_price', overrides['gas_price']
else:
yield 'gas_price', transaction.get('gas_price', 1) # TODO: make configurable

if 'to' in overrides:
yield 'to', overrides['to']
else:
yield 'to', transaction.get('to', b'')

if 'data' in overrides:
yield 'data', overrides['data']
else:
yield 'data', transaction.get('data', b'')

if 'value' in overrides:
yield 'value', overrides['value']
else:
yield 'value', transaction.get('value', 0)

if 'nonce' in overrides:
yield 'nonce', overrides['nonce']
else:
yield 'nonce', transaction.get('nonce', 0)

if 'v' in overrides:
yield 'v', overrides['v']
else:
yield 'v', transaction.get('v', 27)
else:
# legacy transaction
yield 'v', overrides.get('v', transaction.get('v', 27))

if 'r' in overrides:
yield 'r', overrides['r']
else:
yield 'r', transaction.get('r', 12345)

if 's' in overrides:
yield 's', overrides['s']
else:
yield 's', transaction.get('s', 67890)
def _yield_typed_transaction_fields(overrides, transaction):
yield 'chain_id', overrides.get('chain_id', transaction.get('chain_id', 131277322940537))
yield 'access_list', overrides.get('access_list', transaction.get('access_list', ()))
yield 'y_parity', overrides.get('y_parity', transaction.get('y_parity', 0))


@to_dict
Expand Down Expand Up @@ -200,36 +193,33 @@ def make_log(transaction, block, transaction_index, log_index, overrides=None):


@to_dict
def make_receipt(transaction, block, transaction_index, overrides=None):
def make_receipt(transaction, block, _transaction_index, overrides=None):
if overrides is None:
overrides = {}

if 'gas_used' in overrides:
gas_used = overrides['gas_used']
else:
gas_used = 21000
gas_used = overrides.get('gas_used', 21000)
yield 'gas_used', gas_used

if 'cumulative_gas_used' in overrides:
yield 'cumulative_gas_used', overrides['cumulative_gas_used']
else:
yield 'cumulative_gas_used', block['gas_used'] + gas_used

if 'contract_address' in overrides:
yield 'contract_address', overrides['contract_address']
else:
contract_address = generate_contract_address(transaction['from'], transaction['nonce'])
yield 'contract_address', contract_address

if 'logs' in overrides:
yield 'logs', overrides['logs']
else:
yield 'logs', []

if 'transaction_hash' in overrides:
yield 'transaction_hash', overrides['transaction_hash']
else:
yield 'transaction_hash', transaction['hash']
yield 'logs', overrides.get('logs', [])
yield 'transaction_hash', overrides.get('transaction_hash', transaction.get('hash'))
yield (
'cumulative_gas_used',
overrides.get('cumulative_gas_used', block.get('gas_used') + gas_used)
)
yield (
'effective_gas_price',
overrides.get('effective_gas_price', calculate_effective_gas_price(transaction, block))
)
yield (
'type',
overrides.get('type', transaction.get('type', extract_transaction_type(transaction)))
)
yield (
'contract_address',
overrides.get(
'contract_address',
generate_contract_address(transaction['from'], transaction['nonce'])
)
)


GENESIS_NONCE = b'\x00\x00\x00\x00\x00\x00\x00*' # 42 encoded as big-endian-integer
Expand All @@ -253,11 +243,12 @@ def make_genesis_block(overrides=None):
"total_difficulty": 131072,
"size": 0,
"extra_data": ZERO_32BYTES,
"gas_limit": 3141592,
"gas_limit": 30029122, # gas limit at London fork block 12965000 on mainnet
"gas_used": 0,
"timestamp": int(time.time()),
"transactions": [],
"uncles": [],
"base_fee_per_gas": 1000000000, # base fee at London fork block 12965000 on mainnet
}
if overrides is not None:
genesis_block = merge_genesis_overrides(defaults=default_genesis_block,
Expand Down Expand Up @@ -368,3 +359,38 @@ def make_block_from_parent(parent_block, overrides=None):
yield 'uncles', overrides['uncles']
else:
yield 'uncles', []

if 'base_fee_per_gas' in overrides:
yield 'base_fee_per_gas', overrides['base_fee_per_gas']
else:
yield 'base_fee_per_gas', _calculate_expected_base_fee_per_gas(parent_block)


def _calculate_expected_base_fee_per_gas(parent_block) -> int:
"""py-evm logic for calculating the base fee from parent header"""
parent_base_fee_per_gas = parent_block['base_fee_per_gas']

parent_gas_target = parent_block['gas_limit'] // BLOCK_ELASTICITY_MULTIPLIER
parent_gas_used = parent_block['gas_used']

if parent_gas_used == parent_gas_target:
return parent_base_fee_per_gas

elif parent_gas_used > parent_gas_target:
gas_used_delta = parent_gas_used - parent_gas_target
overburnt_wei = parent_base_fee_per_gas * gas_used_delta
base_fee_per_gas_delta = max(
overburnt_wei // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR,
1,
)
return parent_base_fee_per_gas + base_fee_per_gas_delta

else:
gas_used_delta = parent_gas_target - parent_gas_used
underburnt_wei = parent_base_fee_per_gas * gas_used_delta
base_fee_per_gas_delta = (
underburnt_wei
// parent_gas_target
// BASE_FEE_MAX_CHANGE_DENOMINATOR
)
return max(parent_base_fee_per_gas - base_fee_per_gas_delta, 0)
6 changes: 4 additions & 2 deletions eth_tester/backends/mock/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,10 @@ def get_transaction_receipt(self, transaction_hash):
raise TransactionNotFound(
f"No transaction found for hash: {transaction_hash}"
)
_, block, transaction_index = self._get_transaction_by_hash(transaction_hash)
transaction, block, transaction_index = self._get_transaction_by_hash(transaction_hash)
return serialize_receipt(
receipt,
transaction,
block,
transaction_index,
is_pending=(block['number'] == self.block['number']),
Expand Down Expand Up @@ -284,6 +285,7 @@ def send_raw_transaction(self, raw_transaction):
'from': _generate_dummy_address(0),
'hash': transaction_hash,
'gas': 21000,
'gas_price': 1000000000,
}
return self.send_transaction(transaction)

Expand All @@ -304,7 +306,7 @@ def send_transaction(self, transaction):
return full_transaction['hash']

def send_signed_transaction(self, signed_transaction):
transaction = dissoc(signed_transaction, 'r', 's', 'v')
transaction = dissoc(signed_transaction, 'r', 's', 'v', 'y_parity')
return self.send_transaction(transaction)

def estimate_gas(self, transaction):
Expand Down
29 changes: 26 additions & 3 deletions eth_tester/backends/mock/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
from eth_tester.backends.mock.common import (
calculate_effective_gas_price,
)
from eth_tester.utils.transactions import (
extract_transaction_type,
)
from eth_utils.toolz import (
assoc,
partial,
Expand Down Expand Up @@ -27,15 +33,28 @@ def serialize_full_transaction(transaction, block, transaction_index, is_pending
block_number = block['number']
block_hash = block['hash']

return pipe(
serialized_transaction = pipe(
transaction,
partial(assoc, key='block_number', value=block_number),
partial(assoc, key='block_hash', value=block_hash),
partial(assoc, key='transaction_index', value=transaction_index),
partial(assoc, key='type', value=extract_transaction_type(transaction))
)

if 'gas_price' in transaction:
return serialized_transaction
else:
# TODO: Sometime in 2022 the inclusion of gas_price may be removed from dynamic fee
# transactions and we can get rid of this behavior.
# https://github.com/ethereum/execution-specs/pull/251
gas_price = (
transaction['max_fee_per_gas'] if is_pending
else calculate_effective_gas_price(transaction, block)
)
return assoc(serialized_transaction, 'gas_price', gas_price)


def serialize_receipt(transaction, block, transaction_index, is_pending):
def serialize_receipt(receipt, transaction, block, transaction_index, is_pending):
if is_pending:
block_number = None
block_hash = None
Expand All @@ -45,9 +64,13 @@ def serialize_receipt(transaction, block, transaction_index, is_pending):
block_hash = block['hash']

return pipe(
transaction,
receipt,
partial(assoc, key='block_number', value=block_number),
partial(assoc, key='block_hash', value=block_hash),
partial(assoc, key='transaction_index', value=transaction_index),
partial(assoc, key='state_root', value=b'\x00'),
partial(assoc, key='effective_gas_price', value=(
calculate_effective_gas_price(transaction, block)
)),
partial(assoc, key='type', value=extract_transaction_type(transaction))
)
Loading