From 475d12c3510bc9f08eb01ab8a7164f587567f834 Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Tue, 19 Oct 2021 11:59:43 -0600 Subject: [PATCH] add fields to receipts and tests, more refactoring - 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 --- README.md | 17 +- eth_tester/backends/mock/common.py | 17 ++ eth_tester/backends/mock/factory.py | 61 +++--- eth_tester/backends/mock/main.py | 4 +- eth_tester/backends/mock/serializers.py | 24 ++- eth_tester/backends/pyevm/main.py | 14 +- eth_tester/backends/pyevm/serializers.py | 40 +++- eth_tester/constants.py | 5 + eth_tester/main.py | 13 ++ eth_tester/normalization/inbound.py | 22 +-- eth_tester/normalization/outbound.py | 28 +-- eth_tester/utils/backend_testing.py | 180 ++++++++++++++++-- eth_tester/validation/common.py | 20 +- eth_tester/validation/inbound.py | 5 + eth_tester/validation/outbound.py | 11 +- .../validation/test_outbound_validation.py | 26 ++- 16 files changed, 359 insertions(+), 128 deletions(-) create mode 100644 eth_tester/backends/mock/common.py diff --git a/README.md b/README.md index 8aa5c127..cbec9f58 100644 --- a/README.md +++ b/README.md @@ -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, @@ -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', @@ -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} ``` diff --git a/eth_tester/backends/mock/common.py b/eth_tester/backends/mock/common.py new file mode 100644 index 00000000..6c8d92df --- /dev/null +++ b/eth_tester/backends/mock/common.py @@ -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'] + ) diff --git a/eth_tester/backends/mock/factory.py b/eth_tester/backends/mock/factory.py index ecd31589..40a5c8f7 100644 --- a/eth_tester/backends/mock/factory.py +++ b/eth_tester/backends/mock/factory.py @@ -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, @@ -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, ) @@ -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 = {} @@ -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) @@ -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)) @@ -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 diff --git a/eth_tester/backends/mock/main.py b/eth_tester/backends/mock/main.py index 332a4220..db33e2aa 100644 --- a/eth_tester/backends/mock/main.py +++ b/eth_tester/backends/mock/main.py @@ -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']), @@ -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) diff --git a/eth_tester/backends/mock/serializers.py b/eth_tester/backends/mock/serializers.py index 19350b1e..1b4b1733 100644 --- a/eth_tester/backends/mock/serializers.py +++ b/eth_tester/backends/mock/serializers.py @@ -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, @@ -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 @@ -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)) ) diff --git a/eth_tester/backends/pyevm/main.py b/eth_tester/backends/pyevm/main.py index be9b95c1..43e7e7cc 100644 --- a/eth_tester/backends/pyevm/main.py +++ b/eth_tester/backends/pyevm/main.py @@ -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, @@ -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: @@ -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: diff --git a/eth_tester/backends/pyevm/serializers.py b/eth_tester/backends/pyevm/serializers.py index 02b905b2..de1cb1b4 100644 --- a/eth_tester/backends/pyevm/serializers.py +++ b/eth_tester/backends/pyevm/serializers.py @@ -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, @@ -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: @@ -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) @@ -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) @@ -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( @@ -153,7 +163,6 @@ 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, @@ -161,12 +170,14 @@ def serialize_transaction_receipt( "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, } @@ -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 + ) diff --git a/eth_tester/constants.py b/eth_tester/constants.py index 23f0dbc5..7bc85095 100644 --- a/eth_tester/constants.py +++ b/eth_tester/constants.py @@ -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') diff --git a/eth_tester/main.py b/eth_tester/main.py index 3e27db79..3afc46b5 100644 --- a/eth_tester/main.py +++ b/eth_tester/main.py @@ -71,6 +71,7 @@ def func_wrapper(self, *args, **kwargs): try: transaction_hash = func(self, *args, **kwargs) pending_transaction = self.get_transaction_by_hash(transaction_hash) + pending_transaction = _clean_pending_transaction_for_sending(pending_transaction) # Remove any pending transactions with the same nonce self._pending_transactions = remove_matching_transaction_from_list( self._pending_transactions, pending_transaction) @@ -78,6 +79,18 @@ def func_wrapper(self, *args, **kwargs): finally: self.revert_to_snapshot(snapshot) return transaction_hash + + def _clean_pending_transaction_for_sending(pending_transaction): + cleaned_transaction = dissoc(pending_transaction, 'type') + + # 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 + if 'gas_price' and 'max_fee_per_gas' in pending_transaction: + cleaned_transaction = dissoc(cleaned_transaction, 'gas_price') + + return cleaned_transaction + return func_wrapper diff --git a/eth_tester/normalization/inbound.py b/eth_tester/normalization/inbound.py index 5dff3e5c..c739915b 100644 --- a/eth_tester/normalization/inbound.py +++ b/eth_tester/normalization/inbound.py @@ -1,15 +1,10 @@ from __future__ import absolute_import -from toolz import compose, curry, dissoc - from eth_utils import ( - encode_hex, remove_0x_prefix, to_bytes, ) - from eth_utils.curried import ( - apply_formatters_to_dict, apply_one_of_formatters, decode_hex, is_address, @@ -91,14 +86,6 @@ def normalize_private_key(value): )) -@curry -def remove_gas_price_if_dynamic_fee_transaction(txn_dict): - if all(_ in txn_dict for _ in ('max_fee_per_gas', 'max_priority_fee_per_gas')): - return dissoc(txn_dict, 'gas_price') - else: - return txn_dict - - def _normalize_inbound_access_list(access_list): return tuple([ tuple([ @@ -111,7 +98,6 @@ def _normalize_inbound_access_list(access_list): TRANSACTION_NORMALIZERS = { 'chain_id': identity, - 'type': encode_hex, 'from': to_canonical_address, 'to': to_empty_or_canonical_address, 'gas': identity, @@ -127,13 +113,7 @@ def _normalize_inbound_access_list(access_list): 'v': identity, 'y_parity': identity, } -normalize_transaction = compose( - # TODO: At some point (the merge?), the inclusion of gas_price==max_fee_per_gas will be removed - # from dynamic fee transactions and we can get rid of this behavior. - # https://github.com/ethereum/execution-specs/pull/251 - remove_gas_price_if_dynamic_fee_transaction, - apply_formatters_to_dict(TRANSACTION_NORMALIZERS), -) +normalize_transaction = partial(normalize_dict, normalizers=TRANSACTION_NORMALIZERS) LOG_ENTRY_NORMALIZERS = { diff --git a/eth_tester/normalization/outbound.py b/eth_tester/normalization/outbound.py index a3f134a1..80f37d10 100644 --- a/eth_tester/normalization/outbound.py +++ b/eth_tester/normalization/outbound.py @@ -1,9 +1,6 @@ from __future__ import absolute_import -from toolz import assoc, curry - from eth_utils.curried import ( - apply_formatters_to_dict, apply_one_of_formatters, to_checksum_address, encode_hex, @@ -36,14 +33,6 @@ )) -@curry -def fill_gas_price_if_dynamic_fee_transaction(txn_dict): - if all(_ in txn_dict for _ in ('max_fee_per_gas', 'max_priority_fee_per_gas')): - return assoc(txn_dict, 'gas_price', txn_dict.get('max_fee_per_gas')) - else: - return txn_dict - - def _normalize_outbound_access_list(access_list): return tuple([ { @@ -57,6 +46,7 @@ def _normalize_outbound_access_list(access_list): TRANSACTION_NORMALIZERS = { + "type": identity, "chain_id": identity, "hash": encode_hex, "nonce": identity, @@ -77,13 +67,7 @@ def _normalize_outbound_access_list(access_list): "v": identity, "y_parity": identity, } -normalize_transaction = compose( - # TODO: At some point (the merge?), the inclusion of gas_price==max_fee_per_gas will be removed - # from dynamic fee transactions and we can get rid of this behavior. - # https://github.com/ethereum/execution-specs/pull/251 - fill_gas_price_if_dynamic_fee_transaction, - apply_formatters_to_dict(TRANSACTION_NORMALIZERS), -) +normalize_transaction = partial(normalize_dict, normalizers=TRANSACTION_NORMALIZERS) def is_transaction_hash_list(value): @@ -127,8 +111,6 @@ def is_transaction_object_list(value): ), "uncles": partial(normalize_array, normalizer=encode_hex), } - - normalize_block = partial(normalize_dict, normalizers=BLOCK_NORMALIZERS) @@ -151,8 +133,6 @@ def is_transaction_object_list(value): "data": encode_hex, "topics": partial(normalize_array, normalizer=encode_hex), } - - normalize_log_entry = partial(normalize_dict, normalizers=LOG_ENTRY_NORMALIZERS) @@ -166,6 +146,7 @@ def is_transaction_object_list(value): normalizer=encode_hex, ), "cumulative_gas_used": identity, + "effective_gas_price": identity, "gas_used": identity, "contract_address": partial( normalize_if, @@ -174,7 +155,6 @@ def is_transaction_object_list(value): ), "logs": partial(normalize_array, normalizer=normalize_log_entry), "state_root": identity, + "type": identity, } - - normalize_receipt = partial(normalize_dict, normalizers=RECEIPT_NORMALIZERS) diff --git a/eth_tester/utils/backend_testing.py b/eth_tester/utils/backend_testing.py index a650f48a..d067fb03 100644 --- a/eth_tester/utils/backend_testing.py +++ b/eth_tester/utils/backend_testing.py @@ -68,7 +68,6 @@ "gas": 21000, } - TRANSACTION_WTH_NONCE = assoc(SIMPLE_TRANSACTION, 'nonce', 0) CONTRACT_TRANSACTION_EMPTY_TO = { @@ -281,7 +280,7 @@ def test_send_raw_transaction_valid_raw_transaction(self, eth_tester, is_pending # transaction: 'to': '0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A', 'from': # '0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A', 'value': 1337, 'nonce': 0 - # 'gas': 21000, 'gasPrice': 1000000000, and signed with `test_key` + # 'gas': 21000, 'gas_price': 1000000000, and signed with `test_key` transaction_hex = "0xf86580843b9aca008252089419e7e376e7c213b7e7e7e46cc70a5dd086daff2a820539801ba0b101c1f9dc0c588c0194a1093f06e6b30d1fd16d31014ef5851311b7bfbf419ea01cfa8757b7863a630ef7491c62b03d9cd9dff395f61b5df500cc665f0fa5b027" # noqa: E501 if is_pending: @@ -333,6 +332,41 @@ def test_send_transaction(self, eth_tester, test_transaction): self._send_and_check_transaction(eth_tester, test_transaction, accounts[0]) + def test_send_transaction_raises_with_legacy_and_dynamic_fee_fields( + self, eth_tester + ): + accounts = eth_tester.get_accounts() + assert accounts, "No accounts available for transaction sending" + + test_transaction = { + "to": accounts[0], + "from": accounts[0], + "value": 1, + "gas": 21000, + "gas_price": 1234567890, + "max_fee_per_gas": 1000000000, + "max_priority_fee_per_gas": 1000000000, + } + with pytest.raises(ValidationError): + self._send_and_check_transaction(eth_tester, test_transaction, accounts[0]) + + def test_send_transaction_no_gas_price_or_dynamic_fees(self, eth_tester): + # test that we default to a dynamic fee transaction which, while pending, will include the + # gas_price as equal to the max_fee_per_gas until that is removed. + accounts = eth_tester.get_accounts() + assert accounts, "No accounts available for transaction sending" + + test_transaction = dissoc(SIMPLE_TRANSACTION, 'gas_price') + test_transaction = assoc(test_transaction, 'from', accounts[0]) + + txn_hash = eth_tester.send_transaction(test_transaction) + sent_transaction = eth_tester.get_transaction_by_hash(txn_hash) + + assert sent_transaction.get('gas_price') == 1000000000 + assert sent_transaction.get('max_fee_per_gas') == 1000000000 + assert sent_transaction.get('max_priority_fee_per_gas') == 1000000000 + assert sent_transaction.get('access_list') == () + def test_send_access_list_transaction(self, eth_tester): accounts = eth_tester.get_accounts() assert accounts, "No accounts available for transaction sending" @@ -346,7 +380,10 @@ def test_send_access_list_transaction(self, eth_tester): 'gas_price': 1000000000, 'access_list': [], } - self._send_and_check_transaction(eth_tester, access_list_transaction, accounts[0]) + txn_hash = eth_tester.send_transaction(access_list_transaction) + txn = eth_tester.get_transaction_by_hash(txn_hash) + assert eth_tester.get_transaction_receipt(txn_hash)['type'] == '0x1' + self._check_transactions(access_list_transaction, txn) # with non-empty access list access_list_transaction['access_list'] = ( @@ -362,7 +399,10 @@ def test_send_access_list_transaction(self, eth_tester): 'storage_keys': () }, ) - self._send_and_check_transaction(eth_tester, access_list_transaction, accounts[0]) + txn_hash = eth_tester.send_transaction(access_list_transaction) + txn = eth_tester.get_transaction_by_hash(txn_hash) + assert eth_tester.get_transaction_receipt(txn_hash)['type'] == '0x1' + self._check_transactions(access_list_transaction, txn) def test_send_dynamic_fee_transaction(self, eth_tester): accounts = eth_tester.get_accounts() @@ -377,7 +417,10 @@ def test_send_dynamic_fee_transaction(self, eth_tester): 'max_fee_per_gas': 2000000000, 'max_priority_fee_per_gas': 1000000000, } - self._send_and_check_transaction(eth_tester, dynamic_fee_transaction, accounts[0]) + txn_hash = eth_tester.send_transaction(dynamic_fee_transaction) + txn = eth_tester.get_transaction_by_hash(txn_hash) + assert eth_tester.get_transaction_receipt(txn_hash)['type'] == '0x2' + self._check_transactions(dynamic_fee_transaction, txn) # with non-empty access list dynamic_fee_transaction['access_list'] = ( @@ -393,7 +436,10 @@ def test_send_dynamic_fee_transaction(self, eth_tester): 'storage_keys': () }, ) - self._send_and_check_transaction(eth_tester, dynamic_fee_transaction, accounts[0]) + txn_hash = eth_tester.send_transaction(dynamic_fee_transaction) + txn = eth_tester.get_transaction_by_hash(txn_hash) + assert eth_tester.get_transaction_receipt(txn_hash)['type'] == '0x2' + self._check_transactions(dynamic_fee_transaction, txn) def test_block_number_auto_mine_transactions_enabled(self, eth_tester): eth_tester.mine_blocks() @@ -498,7 +544,6 @@ def test_auto_mine_transactions_disabled_returns_hashes_when_enabled(self, eth_t "gas": 21000, "nonce": 0, }) - sent_transactions = eth_tester.enable_auto_mine_transactions() assert sent_transactions == [tx1, tx2_replacement] @@ -685,24 +730,137 @@ def test_get_transaction_by_hash_for_unmined_transaction(self, eth_tester): assert transaction['hash'] == transaction_hash assert transaction['block_hash'] is None + def test_get_transaction_receipt_for_unmined_transaction_raises(self, eth_tester): + eth_tester.disable_auto_mine_transactions() + transaction_hash = eth_tester.send_transaction({ + "from": eth_tester.get_accounts()[0], + "to": BURN_ADDRESS, + "gas": 21000, + }) + with pytest.raises(TransactionNotFound): + eth_tester.get_transaction_receipt(transaction_hash) + def test_get_transaction_receipt_for_mined_transaction(self, eth_tester): transaction_hash = eth_tester.send_transaction({ "from": eth_tester.get_accounts()[0], "to": BURN_ADDRESS, "gas": 21000, }) + mined_transaction = eth_tester.get_transaction_by_hash(transaction_hash) + max_fee = mined_transaction['max_fee_per_gas'] + assert max_fee == 1000000000 + receipt = eth_tester.get_transaction_receipt(transaction_hash) assert receipt['transaction_hash'] == transaction_hash + assert receipt['type'] == '0x2' + assert receipt['effective_gas_price'] == max_fee + + @pytest.mark.parametrize( + 'type_specific_params,_type', + ( + ( + {'gas_price': 1234567890}, + '0x0' # legacy transactions being '0x0' taken from current geth version v1.10.10 + ), + ( + { + 'gas_price': 1234567890, + 'access_list': ({'address': '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', 'storage_keys': ()},), # noqa: E501 + }, + '0x1' # access list transaction + ), + ( + { + 'max_fee_per_gas': 1000000000, + 'max_priority_fee_per_gas': 1000000000, + 'access_list': ( + { + 'address': '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', + 'storage_keys': () + }, + ), + }, + '0x2' # dynamic fee transaction + ), + ) + ) + def test_receipt_transaction_type_for_mined_transaction( + self, eth_tester, type_specific_params, _type + ): + transaction_hash = eth_tester.send_transaction( + merge( + { + "from": eth_tester.get_accounts()[0], + "to": BURN_ADDRESS, + "gas": 25000, + }, + type_specific_params + ) + ) + receipt = eth_tester.get_transaction_receipt(transaction_hash) + assert receipt['transaction_hash'] == transaction_hash + assert receipt['type'] == _type + + def test_receipt_effective_gas_price_for_mined_transaction_legacy(self, eth_tester): + gas_price = 1234567890 - def test_get_transaction_receipt_for_unmined_transaction_raises(self, eth_tester): - eth_tester.disable_auto_mine_transactions() transaction_hash = eth_tester.send_transaction({ "from": eth_tester.get_accounts()[0], "to": BURN_ADDRESS, "gas": 21000, + "gas_price": gas_price, }) - with pytest.raises(TransactionNotFound): - eth_tester.get_transaction_receipt(transaction_hash) + receipt = eth_tester.get_transaction_receipt(transaction_hash) + assert receipt['transaction_hash'] == transaction_hash + assert receipt['type'] == '0x0' + assert receipt['effective_gas_price'] == gas_price + + def test_receipt_effective_gas_price_for_mined_transaction_base_fee_limit(self, eth_tester): + priority_fee = 1500000000 + + transaction_hash = eth_tester.send_transaction({ + "from": eth_tester.get_accounts()[0], + "to": BURN_ADDRESS, + "gas": 21000, + "max_priority_fee_per_gas": priority_fee, + "max_fee_per_gas": priority_fee * 2 + }) + receipt = eth_tester.get_transaction_receipt(transaction_hash) + + base_fee = eth_tester.get_block_by_number(receipt['block_number'])['base_fee_per_gas'] + + # base fee should be 875000000 since genesis was 1000000000 + assert base_fee == 875000000 + + assert receipt['transaction_hash'] == transaction_hash + assert receipt['type'] == '0x2' + # the max fee is higher than (base fee + priority fee) so the latter should be the + # effective gas price + assert receipt['effective_gas_price'] == base_fee + priority_fee + + def test_receipt_effective_gas_price_for_mined_transaction_max_fee_limit(self, eth_tester): + priority_fee = 1500000000 + max_fee = priority_fee + 1 # arbitrary, to differentiate from the priority fee + + transaction_hash = eth_tester.send_transaction({ + "from": eth_tester.get_accounts()[0], + "to": BURN_ADDRESS, + "gas": 21000, + "max_priority_fee_per_gas": priority_fee, + "max_fee_per_gas": max_fee + }) + receipt = eth_tester.get_transaction_receipt(transaction_hash) + + base_fee = eth_tester.get_block_by_number(receipt['block_number'])['base_fee_per_gas'] + + # base fee should be 875000000 since genesis was 1000000000 + assert base_fee == 875000000 + + assert receipt['transaction_hash'] == transaction_hash + assert receipt['type'] == '0x2' + # the max fee is lower than (base fee + priority fee) so the former should be the + # effective gas price + assert receipt['effective_gas_price'] == max_fee def test_call_return13(self, eth_tester): self.skip_if_no_evm_execution() diff --git a/eth_tester/validation/common.py b/eth_tester/validation/common.py index 266d02dd..9045be62 100644 --- a/eth_tester/validation/common.py +++ b/eth_tester/validation/common.py @@ -4,10 +4,7 @@ import functools -from toolz import dissoc - from eth_utils import ( - is_boolean, is_bytes, is_text, is_dict, @@ -113,23 +110,16 @@ def validate_no_extra_keys(value, allowed_keys): def validate_has_required_keys(value, required_keys): missing_keys = tuple(sorted(set(required_keys).difference(value.keys()))) - missing_keys = dissoc(missing_keys, 'base_fee_per_gas') # pre-london blocks won't have this + # pre-london blocks won't have these: if missing_keys: raise ValidationError( - "Blocks must contain all of the keys '{}'. Missing the keys: '{}'".format( + "dict must contain all of the keys '{}'. Missing the keys: '{}'".format( "/".join(tuple(sorted(required_keys))), "/".join(missing_keys), ) ) -def validate_transaction_params(value): - if "gas_price" in value and any(_ in value for _ in ( - "max_fee_per_gas", "max_priority_fee_per_gas" - )): - raise ValidationError("legacy gas price and dynamic fee transaction parameters present") - - @to_dict def _accumulate_dict_errors(value, validators): for key, validator_fn in validators.items(): @@ -143,10 +133,8 @@ def _accumulate_dict_errors(value, validators): def validate_dict(value, key_validators): validate_is_dict(value) validate_no_extra_keys(value, key_validators.keys()) - if "to" in key_validators: - validate_transaction_params(value) # if transaction - else: - validate_has_required_keys(value, key_validators.keys()) + validate_has_required_keys(value, key_validators.keys()) + key_errors = _accumulate_dict_errors(value, key_validators) if key_errors: key_messages = tuple( diff --git a/eth_tester/validation/inbound.py b/eth_tester/validation/inbound.py index f4b14b85..5b691ae9 100644 --- a/eth_tester/validation/inbound.py +++ b/eth_tester/validation/inbound.py @@ -147,6 +147,7 @@ def validate_private_key(value): TRANSACTION_KEYS = { + 'type', 'chain_id', 'from', 'to', @@ -241,9 +242,13 @@ def validate_transaction(value, txn_internal_type): if 'max_fee_per_gas' in value: validate_uint256(value['max_fee_per_gas']) + if 'gas_price' in value: + raise ValidationError('Legacy and EIP-1559 values in transaction') if 'max_priority_fee_per_gas' in value: validate_uint256(value['max_priority_fee_per_gas']) + if 'gas_price' in value: + raise ValidationError('Legacy and EIP-1559 values in transaction') if 'value' in value: validate_uint256(value['value']) diff --git a/eth_tester/validation/outbound.py b/eth_tester/validation/outbound.py index 6833b95f..811c6350 100644 --- a/eth_tester/validation/outbound.py +++ b/eth_tester/validation/outbound.py @@ -3,7 +3,7 @@ from toolz import dissoc, merge from eth_utils import ( - is_bytes, is_canonical_address, is_integer, is_list_like, + is_bytes, is_canonical_address, is_hex, is_integer, is_list_like, to_int, ) from eth_utils.toolz import ( @@ -80,6 +80,10 @@ def validate_log_entry_type(value): validate_log_entry = partial(validate_dict, key_validators=LOG_ENTRY_VALIDATORS) +def validate_transaction_type(value): + return is_hex(value) and to_int(hexstr=value) in (0, 1, 2) + + def validate_signature_v(value): validate_positive_integer(value) @@ -112,6 +116,7 @@ def _validate_outbound_access_list(access_list): LEGACY_TRANSACTION_VALIDATORS = { + "type": validate_transaction_type, "hash": validate_32_byte_string, "nonce": validate_uint256, "block_hash": if_not_null(validate_32_byte_string), @@ -144,7 +149,7 @@ def _validate_outbound_access_list(access_list): DYNAMIC_FEE_TRANSACTION_VALIDATORS = merge( - dissoc(ACCESS_LIST_TRANSACTION_VALIDATORS, 'gas_price'), # max fees in place of gas_price + ACCESS_LIST_TRANSACTION_VALIDATORS, { "max_fee_per_gas": validate_uint256, "max_priority_fee_per_gas": validate_uint256, @@ -171,10 +176,12 @@ def _validate_outbound_access_list(access_list): "block_number": if_not_null(validate_positive_integer), "block_hash": if_not_null(validate_32_byte_string), "cumulative_gas_used": validate_positive_integer, + "effective_gas_price": if_not_null(validate_positive_integer), "gas_used": validate_positive_integer, "contract_address": if_not_null(validate_canonical_address), "logs": partial(validate_array, validator=validate_log_entry), "state_root": validate_bytes, + "type": validate_transaction_type, } validate_receipt = partial(validate_dict, key_validators=RECEIPT_VALIDATORS) diff --git a/tests/core/validation/test_outbound_validation.py b/tests/core/validation/test_outbound_validation.py index 1a16f05a..5eefcf20 100644 --- a/tests/core/validation/test_outbound_validation.py +++ b/tests/core/validation/test_outbound_validation.py @@ -67,6 +67,7 @@ def _make_legacy_txn( s=0 ): return { + "type": '0x0', "hash": hash, "nonce": nonce, "block_hash": block_hash, @@ -87,8 +88,9 @@ def _make_legacy_txn( def _make_access_list_txn(chain_id=131277322940537, access_list=[], y_parity=0, **kwargs,): legacy_kwargs = dissoc(dict(**kwargs), "chain_id", "access_list", "y_parity") return merge( - dissoc(_make_legacy_txn(**legacy_kwargs), "v", ), + dissoc(_make_legacy_txn(**legacy_kwargs), "v"), { + "type": "0x1", "chain_id": chain_id, "access_list": access_list, "y_parity": y_parity, @@ -96,6 +98,11 @@ def _make_access_list_txn(chain_id=131277322940537, access_list=[], y_parity=0, ) +# This is an outbound transaction so we still keep the gas_price for now since the gas_price is +# the min(max_fee_per_gas, base_fee_per_gas + max_priority_fee_per_gas). +# 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 def _make_dynamic_fee_txn( chain_id=131277322940537, max_fee_per_gas=2000000000, @@ -109,13 +116,13 @@ def _make_dynamic_fee_txn( "chain_id", "max_fee_per_gas", "max_priority_fee_per_gas", "access_list", "y_parity" ) return merge( - dissoc(_make_legacy_txn(**legacy_kwargs), "v", "gas_price"), + _make_access_list_txn( + chain_id=chain_id, access_list=access_list, y_parity=y_parity, **legacy_kwargs + ), { - "chain_id": chain_id, + "type": "0x2", "max_fee_per_gas": max_fee_per_gas, "max_priority_fee_per_gas": max_priority_fee_per_gas, - "access_list": access_list, - "y_parity": y_parity, } ) @@ -236,10 +243,11 @@ def _make_receipt(transaction_hash=ZERO_32BYTES, block_hash=ZERO_32BYTES, cumulative_gas_used=0, gas_used=21000, + effective_gas_price=1000000000, contract_address=None, logs=None, - state_root=b'\x00' - ): + state_root=b'\x00', + _type='0x0'): return { "transaction_hash": transaction_hash, "transaction_index": transaction_index, @@ -247,9 +255,11 @@ def _make_receipt(transaction_hash=ZERO_32BYTES, "block_hash": block_hash, "cumulative_gas_used": cumulative_gas_used, "gas_used": gas_used, + "effective_gas_price": effective_gas_price, "contract_address": contract_address, "logs": logs or [], - "state_root": state_root + "state_root": state_root, + "type": _type, }