Skip to content

Commit

Permalink
add fields to receipts and tests, more refactoring
Browse files Browse the repository at this point in the history
- add effective_gas_price and type to transaction receipt object
- add tests for calculating effective_gas_price and checking type in txn receipts
- update README.md example displaying the new fields
  • Loading branch information
fselmo committed Oct 20, 2021
1 parent 86bee37 commit 475d12c
Show file tree
Hide file tree
Showing 16 changed files with 359 additions and 128 deletions.
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pip install eth-tester
>>> t.send_transaction({
... 'from': '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf',
... 'to': '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF',
... 'gas': 30000,,
... 'gas': 30000,
... 'value': 1,
... 'max_fee_per_gas': 1000000000,
... 'max_priority_fee_per_gas': 1000000000,
Expand All @@ -60,9 +60,10 @@ pip install eth-tester
'0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109'

>>> t.get_transaction_by_hash('0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109')
{'hash': '0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109',
{'type': '0x2',
'hash': '0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109',
'nonce': 0,
'block_hash': '0xd481955268d1f3db58ee61685a899a35e33e8fd35b9cc0812f85b9f06757140e',
'block_hash': '0x28b95514984b0abbd91d88f1a542eaeeb810c24e0234e09891b7d6b3f94f47ed',
'block_number': 1,
'transaction_index': 0,
'from': '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf',
Expand All @@ -80,19 +81,21 @@ pip install eth-tester
'0x0000000000000000000000000000000000000000000000000000000000000007')},
{'address': '0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413',
'storage_keys': ()}),
'y_parity': 0}
'y_parity': 0,
'gas_price': 1000000000}



>>> t.get_transaction_receipt('0x86acbf39865cd2fe86db7203742d2652bc1b58b10a3996befe1ee81738f1f58e')
>>> t.get_transaction_receipt('0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109')
{'transaction_hash': '0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109',
'transaction_index': 0,
'block_number': 1,
'block_hash': '0xd481955268d1f3db58ee61685a899a35e33e8fd35b9cc0812f85b9f06757140e',
'block_hash': '0x28b95514984b0abbd91d88f1a542eaeeb810c24e0234e09891b7d6b3f94f47ed',
'cumulative_gas_used': 29600,
'gas_used': 29600,
'effective_gas_price': 1000000000,
'contract_address': None,
'logs': (),
'type': '0x2',
'status': 1}
```

Expand Down
17 changes: 17 additions & 0 deletions eth_tester/backends/mock/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
def extract_transaction_type(transaction):
return (
'0x2' if 'max_fee_per_gas' in transaction
else '0x1' if 'max_fee_per_gas' not in transaction and 'access_list' in transaction
else '0x0' # legacy transactions being '0x0' taken from current geth version v1.10.10
)


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']
)
61 changes: 34 additions & 27 deletions eth_tester/backends/mock/factory.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import functools
import time

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

from eth_tester.backends.common import merge_genesis_overrides
from eth_tester.constants import EIP_1559_TRANSACTION_PARAMS
from eth_tester.utils.address import (
generate_contract_address,
)
Expand Down Expand Up @@ -88,6 +93,11 @@ 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 EIP_1559_TRANSACTION_PARAMS)
or not any(_ in transaction for _ in EIP_1559_TRANSACTION_PARAMS + ('gas_price',))
)

if overrides is None:
overrides = {}

Expand All @@ -105,7 +115,7 @@ def _fill_transaction(transaction, block, transaction_index, is_pending, overrid
yield 'r', overrides.get('r', transaction.get('r', 12345))
yield 's', overrides.get('s', transaction.get('s', 67890))

if 'gas_price' not in transaction:
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)
Expand All @@ -128,7 +138,7 @@ def _fill_transaction(transaction, block, transaction_index, is_pending, overrid

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 'access_list', overrides.get('access_list', transaction.get('access_list', ()))
yield 'y_parity', overrides.get('y_parity', transaction.get('y_parity', 0))


Expand Down Expand Up @@ -185,32 +195,29 @@ 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 Down
4 changes: 3 additions & 1 deletion 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 Down
24 changes: 21 additions & 3 deletions eth_tester/backends/mock/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from eth_tester.backends.mock.common import (
calculate_effective_gas_price,
extract_transaction_type,
)
from eth_utils.toolz import (
assoc,
partial,
Expand Down Expand Up @@ -27,15 +31,25 @@ 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:
return assoc(
serialized_transaction,
'gas_price',
calculate_effective_gas_price(transaction, block)
)

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 +59,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))
)
14 changes: 10 additions & 4 deletions eth_tester/backends/pyevm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from eth_keys import KeyAPI

from eth_tester.constants import EIP_1559_TRANSACTION_PARAMS
from eth_tester.exceptions import (
BackendDistributionNotFound,
BlockNotFound,
Expand Down Expand Up @@ -438,12 +439,18 @@ def get_base_fee(self, block_number='latest'):
#
@to_dict
def _normalize_transaction(self, transaction, block_number='latest'):
is_dynamic_fee_transaction = 'gas_price' not in transaction # default to dynamic fee txn
base_fee = self.get_base_fee(block_number)

is_dynamic_fee_transaction = (
any(_ in transaction for _ in EIP_1559_TRANSACTION_PARAMS)
or not any(_ in transaction for _ in EIP_1559_TRANSACTION_PARAMS + ('gas_price',))
)

for key in transaction:
if key == 'from':
if key in ('from', 'type'):
continue
yield key, transaction[key]

if 'nonce' not in transaction:
yield 'nonce', self.get_nonce(transaction['from'], block_number)
if 'data' not in transaction:
Expand All @@ -454,11 +461,10 @@ def _normalize_transaction(self, transaction, block_number='latest'):
yield 'to', b''

if is_dynamic_fee_transaction:
if not any(_ in transaction for _ in ('max_fee_per_gas', 'max_priority_fee_per_gas')):
if not any(_ in transaction for _ in EIP_1559_TRANSACTION_PARAMS):
yield 'max_fee_per_gas', 1 * 10**9
yield 'max_priority_fee_per_gas', 1 * 10**9
elif 'max_priority_fee_per_gas' in transaction and 'max_fee_per_gas' not in transaction:
base_fee = self.get_base_fee(block_number)
yield 'max_fee_per_gas', transaction['max_priority_fee_per_gas'] + 2 * base_fee

if is_dynamic_fee_transaction or 'access_list' in transaction:
Expand Down
40 changes: 36 additions & 4 deletions eth_tester/backends/pyevm/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ def serialize_transaction_hash(block, transaction, transaction_index, is_pending


def serialize_transaction(block, transaction, transaction_index, is_pending):
txn_type = _extract_transaction_type(transaction)

common_transaction_params = {
"type": txn_type,
"hash": transaction.hash,
"nonce": transaction.nonce,
"block_hash": None if is_pending else block.hash,
Expand All @@ -91,7 +94,7 @@ def serialize_transaction(block, transaction, transaction_index, is_pending):
type_specific_params = {
'chain_id': transaction.chain_id,
'gas_price': transaction.gas_price,
'access_list': transaction.access_list or [],
'access_list': transaction.access_list or (),
'y_parity': transaction.y_parity,
}
else:
Expand All @@ -108,11 +111,17 @@ def serialize_transaction(block, transaction, transaction_index, is_pending):
'chain_id': transaction.chain_id,
'max_fee_per_gas': transaction.max_fee_per_gas,
'max_priority_fee_per_gas': transaction.max_priority_fee_per_gas,
'access_list': transaction.access_list or [],
'access_list': transaction.access_list or (),
'y_parity': transaction.y_parity,

# TODO: Sometime in early 2022 (the merge?), the inclusion of gas_price will be removed
# from dynamic fee transactions and we can get rid of this behavior.
# https://github.com/ethereum/execution-specs/pull/251
'gas_price': _calculate_effective_gas_price(transaction, block, txn_type),
}
else:
raise ValidationError('Transaction serialization error')

return merge(common_transaction_params, type_specific_params)


Expand All @@ -128,7 +137,7 @@ def _field_in_transaction(transaction, field):
# all legacy transactions inherit from BaseTransaction
return field in transaction.as_dict()
elif isinstance(transaction, TypedTransaction):
# all typed transaction inherit from TypedTransaction
# all typed transactions inherit from TypedTransaction
return hasattr(transaction, field)


Expand All @@ -140,6 +149,7 @@ def serialize_transaction_receipt(
is_pending
):
receipt = receipts[transaction_index]
_txn_type = _extract_transaction_type(transaction)

if transaction.to == b'':
contract_addr = generate_contract_address(
Expand All @@ -153,20 +163,21 @@ def serialize_transaction_receipt(
origin_gas = 0
else:
origin_gas = receipts[transaction_index - 1].gas_used

return {
"transaction_hash": transaction.hash,
"transaction_index": None if is_pending else transaction_index,
"block_number": None if is_pending else block.number,
"block_hash": None if is_pending else block.hash,
"cumulative_gas_used": receipt.gas_used,
"gas_used": receipt.gas_used - origin_gas,
"effective_gas_price": _calculate_effective_gas_price(transaction, block, _txn_type),
"contract_address": contract_addr,
"logs": [
serialize_log(block, transaction, transaction_index, log, log_index, is_pending)
for log_index, log in enumerate(receipt.logs)
],
'state_root': receipt.state_root,
'type': _txn_type,
}


Expand All @@ -182,3 +193,24 @@ def serialize_log(block, transaction, transaction_index, log, log_index, is_pend
"data": log.data,
"topics": [int_to_32byte_big_endian(topic) for topic in log.topics],
}


def _extract_transaction_type(transaction):
if isinstance(transaction, TypedTransaction):
try:
transaction.gas_price # noqa: 201
return '0x1'
except AttributeError:
return '0x2'
return '0x0' # legacy transactions being '0x0' taken from current geth version v1.10.10


def _calculate_effective_gas_price(transaction, block, transaction_type):
return (
min(
transaction.max_fee_per_gas,
transaction.max_priority_fee_per_gas + block.header.base_fee_per_gas
)
if transaction_type == '0x2'
else transaction.gas_price
)
5 changes: 5 additions & 0 deletions eth_tester/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,8 @@
SECPK1_Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240
SECPK1_Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424
SECPK1_G = (SECPK1_Gx, SECPK1_Gy)

#
# EIP CONSTANTS
#
EIP_1559_TRANSACTION_PARAMS = ('max_fee_per_gas', 'max_priority_fee_per_gas')
Loading

0 comments on commit 475d12c

Please sign in to comment.