From 0cb25d9a8280e9f673530ad0acb38f7f85988889 Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Tue, 5 Oct 2021 17:10:37 -0600 Subject: [PATCH] Less broken things All tests pass at the expense of cardboarded and duck-taped code. This needs some serious refactoring, perhaps more tests, then squashing all these WIP commits. --- README.md | 259 +++++++++++------- eth_tester/backends/pyevm/main.py | 11 +- eth_tester/backends/pyevm/serializers.py | 84 +++--- eth_tester/normalization/inbound.py | 26 +- eth_tester/normalization/outbound.py | 24 +- eth_tester/utils/backend_testing.py | 59 +++- eth_tester/validation/common.py | 5 +- eth_tester/validation/inbound.py | 52 ++-- eth_tester/validation/outbound.py | 91 +++--- .../validation/test_inbound_validation.py | 97 +++++-- .../validation/test_outbound_validation.py | 176 +++++++----- 11 files changed, 571 insertions(+), 313 deletions(-) diff --git a/README.md b/README.md index 48b284c6..8aa5c127 100644 --- a/README.md +++ b/README.md @@ -21,45 +21,79 @@ pip install eth-tester >>> from eth_tester import EthereumTester >>> t = EthereumTester() >>> t.get_accounts() -('0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1', - '0x7d577a597B2742b498Cb5Cf0C26cDCD726d39E6e', - '0xDCEceAF3fc5C0a63d195d69b1A90011B7B19650D', - '0x598443F1880Ef585B21f1d7585Bd0577402861E5', - '0x13cBB8D99C6C4e0f2728C7d72606e78A29C4E224', - '0x77dB2BEBBA79Db42a978F896968f4afCE746ea1F', - '0x24143873e0E0815fdCBcfFDbe09C979CbF9Ad013', - '0x10A1c1CB95c92EC31D3f22C66Eef1d9f3F258c6B', - '0xe0FC04FA2d34a66B779fd5CEe748268032a146c0', - '0x90F0B1EBbbA1C1936aFF7AAf20a7878FF9e04B6c') ->>> t.get_balance('0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1') +('0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', + '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', + '0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69', + '0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718', + '0xe1AB8145F7E55DC933d51a18c793F901A3A0b276', + '0xE57bFE9F44b819898F47BF37E5AF72a0783e1141', + '0xd41c057fd1c78805AAC12B0A94a405c0461A6FBb', + '0xF1F6619B38A98d6De0800F1DefC0a6399eB6d30C', + '0xF7Edc8FA1eCc32967F827C9043FcAe6ba73afA5c', + '0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528') + +>>> t.get_balance('0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf') 1000000000000000000000000 ->>> t.send_transaction({'from': '0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1', 'to': '0x7d577a597B2742b498Cb5Cf0C26cDCD726d39E6e', 'gas': 21000, 'value': 1}) -'0x140c1da1370a908e4c0f7c6e33bb97182011707c6a9aff954bef1084c8a48b25' ->>> t.get_transaction_by_hash('0x140c1da1370a908e4c0f7c6e33bb97182011707c6a9aff954bef1084c8a48b25') -{'block_hash': '0x89c03ecb6bbf3ff533b04a663fa98d59c9d985de806d1a9dcafaad7c993ee6e8', - 'block_number': 0, - 'data': '0x', - 'from': '0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1', - 'gas': 21000, - 'gas_price': 1, - 'hash': '0x140c1da1370a908e4c0f7c6e33bb97182011707c6a9aff954bef1084c8a48b25', + +>>> t.send_transaction({ +... 'from': '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', +... 'to': '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', +... 'gas': 30000,, +... 'value': 1, +... 'max_fee_per_gas': 1000000000, +... 'max_priority_fee_per_gas': 1000000000, +... 'chain_id': 131277322940537, +... 'access_list': ( +... { +... 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', +... 'storage_keys': ( +... '0x0000000000000000000000000000000000000000000000000000000000000003', +... '0x0000000000000000000000000000000000000000000000000000000000000007', +... ) +... }, +... { +... 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', +... 'storage_keys': () +... }, +... ) +... }) +'0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109' + +>>> t.get_transaction_by_hash('0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109') +{'hash': '0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109', 'nonce': 0, - 'r': 114833774457827084417823702749930473879683934597320921824765632039428214735160, - 's': 52192522150044217242428968890330558187037131043598164958282684822175843828481, - 'to': '0x7d577a597B2742b498Cb5Cf0C26cDCD726d39E6e', + 'block_hash': '0xd481955268d1f3db58ee61685a899a35e33e8fd35b9cc0812f85b9f06757140e', + 'block_number': 1, 'transaction_index': 0, - 'v': 27, - 'value': 1} - ->>> t.get_transaction_receipt('0x140c1da1370a908e4c0f7c6e33bb97182011707c6a9aff954bef1084c8a48b25') -{'block_hash': '0x89c03ecb6bbf3ff533b04a663fa98d59c9d985de806d1a9dcafaad7c993ee6e8', - 'block_number': 0, + 'from': '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', + 'to': '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', + 'value': 1, + 'gas': 30000, + 'data': '0x', + 'r': 60071646517429056848243893841817235885102606421189844318110381014348740252962, + 's': 55731679314783756278323646144996847004593793888590884914350251538533006990589, + 'chain_id': 131277322940537, + 'max_fee_per_gas': 1000000000, + 'max_priority_fee_per_gas': 1000000000, + 'access_list': ({'address': '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', + 'storage_keys': ('0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000007')}, + {'address': '0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413', + 'storage_keys': ()}), + 'y_parity': 0} + + + +>>> t.get_transaction_receipt('0x86acbf39865cd2fe86db7203742d2652bc1b58b10a3996befe1ee81738f1f58e') +{'transaction_hash': '0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109', + 'transaction_index': 0, + 'block_number': 1, + 'block_hash': '0xd481955268d1f3db58ee61685a899a35e33e8fd35b9cc0812f85b9f06757140e', + 'cumulative_gas_used': 29600, + 'gas_used': 29600, 'contract_address': None, - 'cumulative_gas_used': 21000, - 'gas_used': 21000, 'logs': (), - 'transaction_hash': '0x140c1da1370a908e4c0f7c6e33bb97182011707c6a9aff954bef1084c8a48b25', - 'transaction_index': 0} + 'status': 1} ``` @@ -130,13 +164,13 @@ types. * Hexidecimal values **must** be text (not byte) strings. The `0x` prefix is optional. * Any address which contains mixed-case alpha characters will be validated as a checksummed address as specified by [EIP-55](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md) -* 32-byte hashes **must** be hexidecimal encoded. +* 32-byte hashes **must** be hexadecimal encoded. * Numeric values **must** be in their integer representation. Similarly, ethereum tester ensures that return values conform to similar rules. -* 32-byte hashes will be returned in their hexidecimal encoded representation. -* Addresses will be returned in their hexidecimal representation and EIP55 checksummed. +* 32-byte hashes will be returned in their hexadecimal encoded representation. +* Addresses will be returned in their hexadecimal representation and EIP55 checksummed. * Numeric values will be returned as integers. @@ -164,8 +198,8 @@ The `EthereumTester` object is the sole API entrypoint. Instantiation of this object accepts the following parameters. - `backend`: The chain backend being used. See the [chain backends](#backends) -- `validator`: The validator to used. See the [validators](#validation) -- `normalizer`: The normalizer to used. See the [normalizers](#normalization) +- `validator`: The validator being used. See the [validators](#validation) +- `normalizer`: The normalizer being used. See the [normalizers](#normalization) - `auto_mine_transactions`: If *truthy* transactions will be automatically mined at the time they are submitted. See [`enable_auto_mine_transactions`](#api-enable_auto_mine_transactions) and [`disable_auto_mine_transactions`](#api-disable_auto_mine_transactions). - `fork_blocks`: configures which block numbers the various network hard fork rules will be activated. See [fork-rules](#fork-rules) @@ -181,7 +215,7 @@ object accepts the following parameters. ### Fork Rules -Ethereum tester uses the Byzantium rules, starting at block 0. +Ethereum tester uses the London fork rules, starting at block 0. ### Time Travel @@ -201,7 +235,7 @@ of the latest block. ### Mining Manually mining blocks can be done with the following API. The `coinbase` -parameter of these methods **must** be a hexidecimal encoded address. +parameter of these methods **must** be a hexadecimal encoded address. @@ -221,7 +255,7 @@ Mines a single new block, returning the mined block's hash. #### Auto-mining transactions -By default all transactions are mined immediately. This means that each transaction you send will result in a new block being mined, and that all blocks will only ever have at most a single transaction. This behavior can be controlled with the following methods. +By default, all transactions are mined immediately. This means that each transaction you send will result in a new block being mined, and that all blocks will only ever have at most a single transaction. This behavior can be controlled with the following methods. @@ -239,7 +273,7 @@ Turns **off** auto-mining of transactions. ### Accounts The following API can be used to interact with account data. The `account` -parameter in these methods **must** be a hexidecimal encode address. +parameter in these methods **must** be a hexadecimal encode address. `EthereumTester.get_accounts()` @@ -363,20 +397,24 @@ transaction cannot be found. ```python >>> t.get_transaction_by_hash('0x140c1da1370a908e4c0f7c6e33bb97182011707c6a9aff954bef1084c8a48b25') -{'block_hash': '0x89c03ecb6bbf3ff533b04a663fa98d59c9d985de806d1a9dcafaad7c993ee6e8', - 'block_number': 0, - 'hash': '0x140c1da1370a908e4c0f7c6e33bb97182011707c6a9aff954bef1084c8a48b25', +{'hash': '0x256baca0284f7b76238e56964dec01dce423cb11415e5f47b341a0af088bb85d', + 'nonce': 1, + 'block_hash': '0xc25d844e866b4f3f3126e5d5ceba949a7ee12b597a3d54115da63e8596531f58', + 'block_number': 2, 'transaction_index': 0, - 'from': '0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1', - 'to': '0x7d577a597B2742b498Cb5Cf0C26cDCD726d39E6e', + 'from': '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', + 'to': '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', 'value': 1, - 'gas': 21000, - 'gas_price': 1, - 'nonce': 0, + 'gas': 30000, 'data': '0x', - 'v': 27, - 'r': 114833774457827084417823702749930473879683934597320921824765632039428214735160, - 's': 52192522150044217242428968890330558187037131043598164958282684822175843828481} + 'r': 113110058401990022576610285176909677614233866647576678068104192469544316041227, + 's': 56040876284016779278390826082806410689817968450346221730224961588578254081442, + 'chain_id': 131277322940537, + 'max_fee_per_gas': 1000000000, + 'max_priority_fee_per_gas': 1000000000, + 'access_list': (), + 'y_parity': 1} + ``` > Note: For unmined transaction, `transaction_index`, `block_number` and `block_hash` will all be `None`. @@ -396,25 +434,26 @@ cannot be found. ```python >>> t.get_block_by_number(1) -{'difficulty': 131072, - 'extra_data': '0x0000000000000000000000000000000000000000000000000000000000000000', - 'gas_limit': 999023468, - 'gas_used': 0, - 'hash': '0x0f50c8ea0f67ce0b7bff51ae866159edc443bde87de2ab26010a15b777244ddd', - 'logs_bloom': 0, - 'miner': '0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1', - 'nonce': '0x0000000000000000', - 'number': 1, - 'parent_hash': '0x89c03ecb6bbf3ff533b04a663fa98d59c9d985de806d1a9dcafaad7c993ee6e8', +{'number': 1, + 'hash': '0xd481955268d1f3db58ee61685a899a35e33e8fd35b9cc0812f85b9f06757140e', + 'parent_hash': '0x5be984ab842071903ee443a5dee92603bef42de35b4e10928e753f7e88a7163a', + 'nonce': '0x0000000000000042', 'sha3_uncles': '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', - 'size': 472, - 'state_root': '0xbd92123803c9e71018617ce3dc6cbbdf130973bdbd0e14ff340c57c8a835b74b', - 'timestamp': 1410973360, - 'total_difficulty': 262144, - 'transactions': (), - 'transactions_root': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + 'logs_bloom': 0, + 'transactions_root': '0xef1e11d99f7db22fd93c6a10d44753d4a93e9f6ecb2f1e5030a0a91f1d3b07ac', + 'receipts_root': '0x611e48488cf80b4c31f01ad45b6ebea533a68255a6d0240d434d9366a3582010', + 'state_root': '0x9ce568dcaa6f130d733b333304f2c26a19334ed328a7eb9bb31707306381ba65', + 'miner': '0x0000000000000000000000000000000000000000', + 'difficulty': 131072, + 'total_difficulty': 131072, + 'size': 751, + 'extra_data': '0x0000000000000000000000000000000000000000000000000000000000000000', + 'gas_limit': 3141592, + 'gas_used': 29600, + 'timestamp': 1633669276, + 'transactions': ('0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109',), 'uncles': (), - 'base_fee_per_gas': 1000000000} + 'base_fee_per_gas': 875000000} ``` @@ -431,25 +470,26 @@ cannot be found. ```python >>> t.get_block_by_hash('0x0f50c8ea0f67ce0b7bff51ae866159edc443bde87de2ab26010a15b777244ddd') -{'difficulty': 131072, - 'extra_data': '0x0000000000000000000000000000000000000000000000000000000000000000', - 'gas_limit': 999023468, - 'gas_used': 0, - 'hash': '0x0f50c8ea0f67ce0b7bff51ae866159edc443bde87de2ab26010a15b777244ddd', - 'logs_bloom': 0, - 'miner': '0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1', - 'nonce': '0x0000000000000000', - 'number': 1, - 'parent_hash': '0x89c03ecb6bbf3ff533b04a663fa98d59c9d985de806d1a9dcafaad7c993ee6e8', +{'number': 1, + 'hash': '0xd481955268d1f3db58ee61685a899a35e33e8fd35b9cc0812f85b9f06757140e', + 'parent_hash': '0x5be984ab842071903ee443a5dee92603bef42de35b4e10928e753f7e88a7163a', + 'nonce': '0x0000000000000042', 'sha3_uncles': '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', - 'size': 472, - 'state_root': '0xbd92123803c9e71018617ce3dc6cbbdf130973bdbd0e14ff340c57c8a835b74b', - 'timestamp': 1410973360, - 'total_difficulty': 262144, - 'transactions': (), - 'transactions_root': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + 'logs_bloom': 0, + 'transactions_root': '0xef1e11d99f7db22fd93c6a10d44753d4a93e9f6ecb2f1e5030a0a91f1d3b07ac', + 'receipts_root': '0x611e48488cf80b4c31f01ad45b6ebea533a68255a6d0240d434d9366a3582010', + 'state_root': '0x9ce568dcaa6f130d733b333304f2c26a19334ed328a7eb9bb31707306381ba65', + 'miner': '0x0000000000000000000000000000000000000000', + 'difficulty': 131072, + 'total_difficulty': 131072, + 'size': 751, + 'extra_data': '0x0000000000000000000000000000000000000000000000000000000000000000', + 'gas_limit': 3141592, + 'gas_used': 29600, + 'timestamp': 1633669276, + 'transactions': ('0xc20b90af87bc65c3d748cf0a1fa54f3a86ffc94348e0fd91a70f1c5ba6ef4109',), 'uncles': (), - 'base_fee_per_gas': 1000000000} + 'base_fee_per_gas': 875000000} ``` @@ -492,15 +532,33 @@ found for the given hash. A transaction is a formatted as a dictionary with the following keys and values. -* `from`: The address of the account sending the transaction (hexidecimal string). -* `to`: The address of the account the transaction is being sent to. Empty string should be used to trigger contract creation (hexidecimal string). +* `from`: The address of the account sending the transaction (hexadecimal string). +* `to`: The address of the account the transaction is being sent to. Empty string should be used to trigger contract creation (hexadecimal string). * `gas`: Sets the gas limit for transaction execution (integer). -* `gas_price`: Sets the price per unit of gas in wei that will be paid for transaction execution (integer). * `value`: The amount of ether in wei that should be sent with the transaction (integer). -* `data`: The data for the transaction (hexidecimal string). +* `data`: The data for the transaction (hexadecimal string). + +In addition to the above, the following parameters are added based on the type of transaction being sent: + +#### Legacy transactions +* `gas_price`: Sets the price per unit of gas in wei that will be paid for transaction execution (integer). + +#### Access list transactions (EIP-2930) +* `chain_id`: The integer id for the chain the transaction is meant to interact with +* `gas_price`: Sets the price per unit of gas in wei that will be paid for transaction execution (integer). +* `access_list` (optional): Specifies accounts and storage slots expected to be accessed, based on the transaction, in order to +gain a discount on the gas for those executions (see quickstart example for usage). + +#### Dynamic fee transactions (EIP-1559) +* `chain_id`: The integer id for the chain the transaction is meant to interact with +* `max_fee_per_gas`: Sets the maximum fee per unit of gas in wei that will be paid for transaction execution (integer). +* `max_priority_fee_per_gas`: Sets the fee per unit of gas in wei that is sent to the miner as an incentive for mining the transaction (integer). +* `access_list` (optional): Specifies accounts and storage slots expected to be accessed, based on the transaction, in order to +gain a discount on the gas for those executions (see quickstart example for usage). + -#### Methods +### Methods @@ -795,20 +853,15 @@ to `PyEVM.generate_genesis_params`. # Generates the following `dict`: # custom_genesis_params = { -# "bloom": 0, # "coinbase": GENESIS_COINBASE, # "difficulty": GENESIS_DIFFICULTY, # "extra_data": GENESIS_EXTRA_DATA, # "gas_limit": 4500000 # <<< Overidden Value <<< -# "gas_used": 0, # "mix_hash": GENESIS_MIX_HASH, # "nonce": GENESIS_NONCE, -# "block_number": GENESIS_BLOCK_NUMBER, -# "parent_hash": GENESIS_PARENT_HASH, # "receipt_root": BLANK_ROOT_HASH, # "timestamp": int(time.time()), # "transaction_root": BLANK_ROOT_HASH, -# "uncles_hash": EMPTY_RLP_LIST_HASH, # "base_fee_per_gas": 1000000000, # } ``` @@ -884,9 +937,9 @@ The canonical format is intended for low level handling by backends. The normal format is intended for use by end users. -* 32 byte hashes: `0x` prefixed hexidecimal encoded text strings (not byte strings) -* Arbitrary length strings: `0x` prefixed hexidecimal encoded text strings (not byte strings) -* Addresses: `0x` prefixed and EIP55 checksummed hexidecimal encoded text strings (not byte strings) +* 32 byte hashes: `0x` prefixed hexadecimal encoded text strings (not byte strings) +* Arbitrary length strings: `0x` prefixed hexadecimal encoded text strings (not byte strings) +* Addresses: `0x` prefixed and EIP55 checksummed hexadecimal encoded text strings (not byte strings) * Integers: `int` * Array Types: `tuple` @@ -916,7 +969,7 @@ passed in during instantiation. If no value was provided, the default normalizer will be used from `eth_tester.normalization.default.DefaultNormalizer`. -The specifics of this object are beyong the scope of this document. +The specifics of this object are beyond the scope of this document. ### Validation @@ -926,7 +979,7 @@ passed in during instantiation. If no value was provided, the default validator will be used from `eth_tester.validation.default.DefaultValidator`. -The specifics of this object are beyong the scope of this document. +The specifics of this object are beyond the scope of this document. # Use with Web3.py diff --git a/eth_tester/backends/pyevm/main.py b/eth_tester/backends/pyevm/main.py index ba5d2ea4..01474637 100644 --- a/eth_tester/backends/pyevm/main.py +++ b/eth_tester/backends/pyevm/main.py @@ -8,7 +8,6 @@ from eth_abi.exceptions import ( DecodingError ) -from toolz import dissoc from eth_utils import ( encode_hex, @@ -120,6 +119,7 @@ def generate_genesis_state_for_keys(account_keys, overrides=None): def get_default_genesis_params(overrides=None): + # commented out params became un-configurable in London default_genesis_params = { # "bloom": 0, "coinbase": GENESIS_COINBASE, @@ -451,6 +451,11 @@ def _normalize_transaction(self, transaction, block_number='latest'): yield 'value', 0 if 'to' not in transaction: yield 'to', b'' + if ( + all(_ in transaction for _ in ('max_fee_per_gas', 'max_priority_fee_per_gas')) and + 'access_list' not in transaction + ): + yield 'access_list', [] def _get_normalized_and_unsigned_evm_transaction(self, transaction, block_number='latest'): normalized_transaction = self._normalize_transaction(transaction, block_number) @@ -470,14 +475,10 @@ def _get_normalized_and_signed_evm_transaction(self, transaction, block_number=' def _create_type_aware_unsigned_transaction(self, normalized_txn): if all(_ in normalized_txn for _ in ("access_list", "gas_price")): - # validated type='0x1' so can pop it back out since type is signed separate from txn - normalized_txn = dissoc(normalized_txn, 'type') return self.chain.get_transaction_builder().new_unsigned_access_list_transaction( **normalized_txn ) elif all(_ in normalized_txn for _ in ("max_fee_per_gas", "max_priority_fee_per_gas")): - # validated type='0x2' so can pop it back out since type is signed separate from txn - normalized_txn = dissoc(normalized_txn, 'type') return self.chain.get_transaction_builder().new_unsigned_dynamic_fee_transaction( **normalized_txn ) diff --git a/eth_tester/backends/pyevm/serializers.py b/eth_tester/backends/pyevm/serializers.py index 6b9c9c56..1e43e83e 100644 --- a/eth_tester/backends/pyevm/serializers.py +++ b/eth_tester/backends/pyevm/serializers.py @@ -1,6 +1,10 @@ import rlp -from toolz import dissoc, merge +from toolz import ( + merge, +) + +from eth_tester.exceptions import ValidationError from eth_tester.utils.address import ( generate_contract_address, ) @@ -48,6 +52,7 @@ def serialize_block(block, full_transaction, is_pending): "timestamp": block.header.timestamp, "transactions": transactions, "uncles": [uncle.hash for uncle in block.uncles], + "base_fee_per_gas": block.header.base_fee_per_gas, } @@ -67,44 +72,57 @@ def serialize_transaction(block, transaction, transaction_index, is_pending): "value": transaction.value, "gas": transaction.gas, "data": transaction.data, + "r": transaction.r, + "s": transaction.s, } - if hasattr(transaction, 'gas_price'): - if hasattr(transaction, 'access_list'): + if _field_in_transaction(transaction, 'gas_price'): + if _field_in_transaction(transaction, 'access_list'): # access list transaction - serialized_transaction = merge( - common_transaction_params, - { - 'gas_price': transaction.gas_price, - 'access_list': transaction.access_list, - } - ) + type_specific_params = { + 'chain_id': transaction.chain_id, + 'gas_price': transaction.gas_price, + 'access_list': transaction.access_list or [], + 'y_parity': transaction.y_parity, + } else: # legacy transaction - serialized_transaction = merge( - common_transaction_params, - { - "gas_price": transaction.gas_price, - "v": transaction.v, - "r": transaction.r, - "s": transaction.s, - } - ) - elif ( - hasattr(transaction, 'max_fee_per_gas') and - hasattr(transaction, 'max_priority_fee_per_gas') - ): - # dynamic fee transaction - serialized_transaction = merge( - common_transaction_params, - { - '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 None, + type_specific_params = { + 'gas_price': transaction.gas_price, + 'v': transaction.v } - ) + elif (_field_in_transaction(transaction, _) for _ in ( + 'max_fee_per_gas' and 'max_priority_fee_per_gas' + )): + # dynamic fee transaction + type_specific_params = { + '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 [], + 'y_parity': transaction.y_parity, + } else: - raise NotImplementedError('transaction type not recognized for serialization') - return serialized_transaction + raise ValidationError('Transaction serialization error') + return merge(common_transaction_params, type_specific_params) + + +def _field_in_transaction(transaction, field): + """ + There are many different classes of transactions, we have to be able to search for a + particular field depending on the type of transaction - from a dict, to *LegacyTransaction to + *TypedTransaction. + """ + if not isinstance(transaction, dict): + try: + txn_dict = transaction.as_dict() + if not isinstance(txn_dict, dict): + txn_dict = txn_dict.dictionary + except AttributeError: + pass + try: + return field in txn_dict + except (NameError, TypeError): + return hasattr(transaction, field) def serialize_transaction_receipt( diff --git a/eth_tester/normalization/inbound.py b/eth_tester/normalization/inbound.py index 83acf21e..22ed06cc 100644 --- a/eth_tester/normalization/inbound.py +++ b/eth_tester/normalization/inbound.py @@ -1,6 +1,10 @@ from __future__ import absolute_import -from eth_utils import encode_hex +from eth_utils import ( + encode_hex, + remove_0x_prefix, + to_bytes, +) from eth_utils.curried import ( apply_one_of_formatters, @@ -83,6 +87,19 @@ def normalize_private_key(value): (is_hex, to_canonical_address), )) + +def _normalize_inbound_access_list(access_list): + normalized_access_list = [] + for entry in access_list: + normalized_address = to_bytes(hexstr=entry.get('address')) + normalized_storage_keys = [] + for k in entry.get('storage_keys'): + stripped_0x = remove_0x_prefix(k) + normalized_storage_keys.append(int(stripped_0x)) + normalized_access_list.append((normalized_address, tuple(normalized_storage_keys))) + return tuple(normalized_access_list) + + TRANSACTION_NORMALIZERS = { 'chain_id': identity, 'type': encode_hex, @@ -95,13 +112,12 @@ def normalize_private_key(value): 'nonce': identity, 'value': identity, 'data': decode_hex, - 'access_list': identity, + 'access_list': _normalize_inbound_access_list, 'r': identity, 's': identity, 'v': identity, + 'y_parity': identity, } - - normalize_transaction = partial(normalize_dict, normalizers=TRANSACTION_NORMALIZERS) @@ -116,8 +132,6 @@ def normalize_private_key(value): 'data': decode_hex, 'topics': partial(normalize_array, normalizer=decode_hex), } - - normalize_log_entry = partial(normalize_dict, normalizers=LOG_ENTRY_NORMALIZERS) diff --git a/eth_tester/normalization/outbound.py b/eth_tester/normalization/outbound.py index 79ca7f75..af2f3981 100644 --- a/eth_tester/normalization/outbound.py +++ b/eth_tester/normalization/outbound.py @@ -1,7 +1,5 @@ from __future__ import absolute_import -from eth_utils import is_integer - from eth_utils.curried import ( apply_one_of_formatters, to_checksum_address, @@ -23,6 +21,7 @@ normalize_dict, normalize_array, ) +from ..utils.encoding import int_to_32byte_big_endian normalize_account = to_checksum_address @@ -33,9 +32,23 @@ (is_canonical_address, to_checksum_address), )) + +def normalize_access_list(access_list): + formatted_access_list = [] + for entry in access_list: + account = to_checksum_address(entry['account']) + storage_keys = tuple( + [encode_hex(int_to_32byte_big_endian(k)) for k in entry['storage_keys']] + ) + formatted_access_list.append({ + 'address': account, + 'storage_keys': tuple(storage_keys) + }) + return tuple(formatted_access_list) + + TRANSACTION_NORMALIZERS = { "chain_id": identity, - "type": encode_hex, "hash": encode_hex, "nonce": identity, "block_hash": partial(normalize_if, conditional_fn=is_bytes, normalizer=encode_hex), @@ -49,10 +62,11 @@ "max_fee_per_gas": identity, "max_priority_fee_per_gas": identity, "data": encode_hex, - "access_list": identity, - "v": identity, + "access_list": normalize_access_list, "r": identity, "s": identity, + "v": identity, + "y_parity": identity, } normalize_transaction = partial(normalize_dict, normalizers=TRANSACTION_NORMALIZERS) diff --git a/eth_tester/utils/backend_testing.py b/eth_tester/utils/backend_testing.py index 482f23b6..bcd5454e 100644 --- a/eth_tester/utils/backend_testing.py +++ b/eth_tester/utils/backend_testing.py @@ -322,25 +322,68 @@ def test_send_transaction(self, eth_tester, test_transaction): self._send_and_check_transaction(eth_tester, test_transaction, accounts[0]) - def test_outbound_transaction_formatters(self, eth_tester): + def test_send_access_list_transaction(self, eth_tester): accounts = eth_tester.get_accounts() assert accounts, "No accounts available for transaction sending" - test_transaction = { + access_list_transaction = { + 'chain_id': 123456789, + 'from': accounts[0], + 'to': accounts[0], + 'value': 1, + 'gas': 40000, + 'gas_price': 1000000000, + 'access_list': [], + } + self._send_and_check_transaction(eth_tester, access_list_transaction, accounts[0]) + + # with access list + access_list_transaction['access_list'] = ( + { + 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', + 'storage_keys': ( + '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000007', + ) + }, + { + 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', + 'storage_keys': () + }, + ) + self._send_and_check_transaction(eth_tester, access_list_transaction, accounts[0]) + + def test_send_dynamic_fee_transaction(self, eth_tester): + accounts = eth_tester.get_accounts() + assert accounts, "No accounts available for transaction sending" + + dynamic_fee_transaction = { 'chain_id': 123456789, - 'type': '0x02', 'from': accounts[0], 'to': accounts[0], 'value': 1, 'gas': 40000, 'max_fee_per_gas': 2000000000, 'max_priority_fee_per_gas': 1000000000, - 'access_list': ((b'\xf0' * 20, (2, 3)),), + 'access_list': [], } - - txn_hash = eth_tester.send_transaction(test_transaction) - txn = eth_tester.get_transaction_by_hash(txn_hash) - assert txn + self._send_and_check_transaction(eth_tester, dynamic_fee_transaction, accounts[0]) + + # with access list + dynamic_fee_transaction['access_list'] = ( + { + 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', + 'storage_keys': ( + '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000007', + ) + }, + { + 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', + 'storage_keys': () + }, + ) + self._send_and_check_transaction(eth_tester, dynamic_fee_transaction, accounts[0]) def test_block_number_auto_mine_transactions_enabled(self, eth_tester): eth_tester.mine_blocks() diff --git a/eth_tester/validation/common.py b/eth_tester/validation/common.py index 740f528a..d6929dd5 100644 --- a/eth_tester/validation/common.py +++ b/eth_tester/validation/common.py @@ -129,9 +129,7 @@ def validate_transaction_params(value): )): raise ValidationError("legacy gas price and dynamic fee transaction parameters present") if "type" in value: - if value["type"] in ("0x1", 1) and "access_list" not in value: - raise ValidationError("type 1 transaction is missing access_list") - if value["type"] in ("0x2", 2) and not all(_ in value for _ in ( + if value["type"] == "0x2" and not (_ in value for _ in ( "max_fee_per_gas", "max_priority_fee_per_gas" )): raise ValidationError("type 2 transaction is missing fee values") @@ -154,7 +152,6 @@ def validate_dict(value, key_validators): validate_transaction_params(value) # if transaction else: 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 d4f07278..fa1c7f2c 100644 --- a/eth_tester/validation/inbound.py +++ b/eth_tester/validation/inbound.py @@ -1,11 +1,10 @@ from __future__ import unicode_literals import binascii -from toolz import merge from eth_utils import ( is_boolean, - is_bytes, is_checksum_address, + is_checksum_address, is_checksum_formatted_address, is_dict, is_hex, @@ -148,8 +147,9 @@ def validate_private_key(value): TRANSACTION_KEYS = { + # note that the transaction 'type' is not included, though it is a valid param, because it will + # never be passed to the backend and thus is not necessary. 'chain_id', - 'type', 'from', 'to', 'gas', @@ -198,9 +198,7 @@ def validate_transaction(value, txn_internal_type): if txn_internal_type == 'send': required_keys = {'from', 'gas'} elif txn_internal_type == 'send_signed': - required_keys = {'from', 'gas'} - if 'type' not in value.keys(): # legacy transaction - required_keys = required_keys | SIGNED_TRANSACTION_KEYS + required_keys = {'from', 'gas'} | SIGNED_TRANSACTION_KEYS elif txn_internal_type in {'estimate', 'call'}: required_keys = {'from'} else: @@ -222,10 +220,6 @@ def validate_transaction(value, txn_internal_type): elif 'to' in value and value['to'] == '': validate_text(value['to']) - _type = value.get('type') - if _type and not (is_text(_type) and is_hex(_type)): - raise ValidationError('Transaction \'type\' must be a hexadecimal encoded string.') - if 'gas' in value: validate_uint256(value['gas']) @@ -246,7 +240,7 @@ def validate_transaction(value, txn_internal_type): if 'data' in value: bad_data_message = ( - "Transaction data must be a hexadecimal encoded string. Got: " + "Transaction \'data\' must be a hexadecimal encoded string. Got: " "{}".format(value['data']) ) if not is_text(value['data']): @@ -263,7 +257,7 @@ def validate_transaction(value, txn_internal_type): raise ValidationError(bad_data_message) if 'access_list' in value: - _validate_access_list(value['access_list']) + _validate_inbound_access_list(value['access_list']) if txn_internal_type == 'send_signed': validate_uint256(value['r']) @@ -271,19 +265,37 @@ def validate_transaction(value, txn_internal_type): validate_uint8(value['v']) -def _validate_access_list(access_list): +def _validate_inbound_access_list(access_list): + """ + Validates the structure of an inbound access list. This is similar to the JSON-RPC structure + for an access list only with `under_score` keys rather than `camelCase`. + + >>> _access_list = ( + ... { + ... 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', + ... 'storage_keys': ( + ... '0x0000000000000000000000000000000000000000000000000000000000000003', + ... '0x0000000000000000000000000000000000000000000000000000000000000007', + ... ) + ... }, + ... { + ... 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', + ... 'storage_keys': () + ... }, + ... ) + """ if not is_list_like(access_list): - raise ValidationError('access_list is not list-like.') + raise ValidationError('access_list is not list-like') for entry in access_list: - if not is_list_like(entry) and len(entry) != 2: + if not is_dict(entry) and len(entry) != 2: raise ValidationError(f'access_list entry not properly formatted: {entry}') - address = entry[0] - storage_keys = entry[1] - if not (is_bytes(address) and len(address) == 20): - raise ValidationError(f'access_list address not properly formatted: {address}') + address = entry.get('address') + storage_keys = entry.get('storage_keys') + if not is_hex_address(address): + raise ValidationError(f'access_list address must be a hexadecimal address: {address}') if not is_list_like(storage_keys): raise ValidationError(f'access_list storage keys are not list-like: {storage_keys}') - if len(storage_keys) > 0 and (not all(is_integer(k) for k in storage_keys)): + if len(storage_keys) > 0 and not all(is_32byte_hex_string(k) for k in storage_keys): raise ValidationError( f'one or more access list storage keys not formatted properly: {storage_keys}' ) diff --git a/eth_tester/validation/outbound.py b/eth_tester/validation/outbound.py index 9a760958..ed46b5fe 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_canonical_address, is_list_like, + is_bytes, is_canonical_address, is_integer, is_list_like, ) from eth_utils.toolz import ( @@ -45,7 +45,7 @@ def validate_nonce(value): validate_bytes(value) if len(value) != 8: raise ValidationError( - "Must be of length 8. Got: {} of lenght {}".format(value, len(value)) + "Must be of length 8. Got: {} of length {}".format(value, len(value)) ) @@ -87,13 +87,25 @@ def validate_signature_v(value): raise ValidationError("The `v` portion of the signature must be 0, 1, 27, 28 or >= 35") -def validate_access_list(value): - if not is_list_like(value): - raise ValidationError('access_list is not list-like') - - -TRANSACTION_VALIDATORS = { - "chain_id": if_not_null(validate_uint256), +def _validate_outbound_access_list(access_list): + if not is_list_like(access_list): + raise ValidationError('access_list is not list-like.') + for entry in access_list: + if not is_list_like(entry) and len(entry) != 2: + raise ValidationError(f'access_list entry not properly formatted: {entry}') + address = entry[0] + storage_keys = entry[1] + if not (is_bytes(address) and len(address) == 20): + raise ValidationError(f'access_list address not properly formatted: {address}') + if not is_list_like(storage_keys): + raise ValidationError(f'access_list storage keys are not list-like: {storage_keys}') + if len(storage_keys) > 0 and (not all(is_integer(k) for k in storage_keys)): + raise ValidationError( + f'one or more access list storage keys not formatted properly: {storage_keys}' + ) + + +LEGACY_TRANSACTION_VALIDATORS = { "hash": validate_32_byte_string, "nonce": validate_uint256, "block_hash": if_not_null(validate_32_byte_string), @@ -103,33 +115,47 @@ def validate_access_list(value): "to": if_not_create_address(validate_canonical_address), "value": validate_uint256, "gas": validate_uint256, - "gas_price": if_not_null(validate_uint256), + "gas_price": validate_uint256, "data": validate_bytes, - "v": if_not_null(validate_signature_v), - "r": if_not_null(validate_uint256), - "s": if_not_null(validate_uint256), + "v": validate_signature_v, + "r": validate_uint256, + "s": validate_uint256, } +validate_legacy_transaction = partial(validate_dict, key_validators=LEGACY_TRANSACTION_VALIDATORS) -TYPED_TRANSACTION_VALIDATORS = dissoc( - merge( - TRANSACTION_VALIDATORS, - { - "type": if_not_null(validate_uint256), - "max_fee_per_gas": if_not_null(validate_uint256), - "max_priority_fee_per_gas": if_not_null(validate_uint256), - "access_list": if_not_null(validate_access_list), - } - ), - 'gasPrice', 'v', 'r', 's', # dissociate these keys from newly merged dict + +ACCESS_LIST_TRANSACTION_VALIDATORS = merge( + dissoc(LEGACY_TRANSACTION_VALIDATORS, 'v'), # y_parity in place of v for typed transactions + { + "chain_id": validate_uint256, + "y_parity": validate_signature_v, + "access_list": if_not_null(_validate_outbound_access_list), + } ) -validate_typed_transaction = partial(validate_dict, key_validators=TYPED_TRANSACTION_VALIDATORS) +validate_access_list_transaction = partial( + validate_dict, key_validators=ACCESS_LIST_TRANSACTION_VALIDATORS +) + + +DYNAMIC_FEE_TRANSACTION_VALIDATORS = merge( + dissoc(ACCESS_LIST_TRANSACTION_VALIDATORS, 'gas_price'), # max fees in place of gas_price + { + "max_fee_per_gas": validate_uint256, + "max_priority_fee_per_gas": validate_uint256, + } +) +validate_dynamic_fee_transaction = partial( + validate_dict, key_validators=DYNAMIC_FEE_TRANSACTION_VALIDATORS +) + validate_transaction = partial( - validate_dict, - key_validators=merge( - TRANSACTION_VALIDATORS, - TYPED_TRANSACTION_VALIDATORS, - ) + validate_any, + validators=( + partial(validate_dict, key_validators=LEGACY_TRANSACTION_VALIDATORS), + partial(validate_dict, key_validators=ACCESS_LIST_TRANSACTION_VALIDATORS), + partial(validate_dict, key_validators=DYNAMIC_FEE_TRANSACTION_VALIDATORS), + ), ) @@ -170,8 +196,9 @@ def validate_access_list(value): validate_any, validators=( partial(validate_array, validator=validate_32_byte_string), - partial(validate_array, validator=validate_transaction), - partial(validate_array, validator=validate_typed_transaction), + partial(validate_array, validator=validate_legacy_transaction), + partial(validate_array, validator=validate_access_list_transaction), + partial(validate_array, validator=validate_dynamic_fee_transaction), ), ), "uncles": partial(validate_array, validator=validate_32_byte_string), diff --git a/tests/core/validation/test_inbound_validation.py b/tests/core/validation/test_inbound_validation.py index b1c6bac1..493c8b53 100644 --- a/tests/core/validation/test_inbound_validation.py +++ b/tests/core/validation/test_inbound_validation.py @@ -184,7 +184,6 @@ def test_filter_params_input_validation(validator, filter_params, is_valid): @to_dict def _make_transaction( chain_id=None, - _type=None, _from=None, to=None, gas=None, @@ -199,7 +198,6 @@ def _make_transaction( v=None, access_list=None): yield from _yield_key_value_if_value_not_none('chain_id', chain_id) - yield from _yield_key_value_if_value_not_none('type', _type) yield from _yield_key_value_if_value_not_none('from', _from) yield from _yield_key_value_if_value_not_none('to', to) yield from _yield_key_value_if_value_not_none('gas', gas) @@ -246,50 +244,93 @@ def _make_transaction( ('send_signed', _make_transaction(_from=ADDRESS_A, gas=21000), False), ('send_signed', _make_transaction(_from=ADDRESS_A, gas=21000, r=1, s=1, v=1), True), ('send_signed', _make_transaction(_from=ADDRESS_A, gas=21000, r=1, s=1, v=256), False), - ( - 'send', - _make_transaction( - _type='0x1', + ( # access list txn + 'send', _make_transaction( _from=ADDRESS_A, to='', gas=21000, gas_price=10000, # properly formatted access list - access_list=((b'\xf0' * 20, (b'\0' * 32, b'\xff' * 32)),), - ), - True + access_list=( + { + 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', + 'storage_keys': ( + '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000007', + ) + }, + { + 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', + 'storage_keys': () + }, + ), + ), True ), ( - 'send', - _make_transaction( - _type='0x1', + 'send', _make_transaction( _from=ADDRESS_A, to='', gas=21000, + gas_price=10000, # improperly formatted access list storage key - access_list=((b'\xf0' * 20, (b'\0' * 32, b'\xff' * 31)),), - ), - False + access_list=( + { + 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', + 'storage_keys': ( + '3', + '0x0000000000000000000000000000000000000000000000000000000000000007', + ) + }, + { + 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', + 'storage_keys': () + }, + ), + ), False ), ( - 'send', - _make_transaction( - _type='0x1', + 'send', _make_transaction( _from=ADDRESS_A, to='', gas=21000, # improperly formatted access list address - access_list=((b'\xf0' * 19, (b'\0' * 32, b'\xff' * 32)),), - ), - False + access_list=( + { + 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', + 'storage_keys': ( + '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000007', + ) + }, + { + 'address': b'', + 'storage_keys': () + }, + ), + ), False ), - ( - 'send', - _make_transaction( # improperly formatted transaction type - _from=ADDRESS_A, to='', gas=21000, _type=2, - max_fee_per_gas=1000000000, max_priority_fee_per_gas=1000000000 - ), - False + ( # dynamic fee txn + 'send', _make_transaction( + _from=ADDRESS_A, + to='', + gas=21000, + max_fee_per_gas=1000000000, + max_priority_fee_per_gas=1000000000, + # properly formatted access list + access_list=( + { + 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', + 'storage_keys': ( + '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000007', + ) + }, + { + 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', + 'storage_keys': () + }, + ), + ), True ), ), ) diff --git a/tests/core/validation/test_outbound_validation.py b/tests/core/validation/test_outbound_validation.py index 70f78a4c..8e6ff7fa 100644 --- a/tests/core/validation/test_outbound_validation.py +++ b/tests/core/validation/test_outbound_validation.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import pytest -from toolz import dissoc +from toolz import dissoc, merge from eth_utils import ( encode_hex, @@ -50,27 +50,23 @@ def test_block_hash_output_validation(validator, block_hash, is_valid): HASH31 = b'\x00' * 31 -def _make_transaction(chain_id=123456789, - _type=None, - hash=ZERO_32BYTES, - nonce=0, - block_hash=ZERO_32BYTES, - block_number=0, - transaction_index=0, - _from=ZERO_ADDRESS, - to=ZERO_ADDRESS, - value=0, - gas=21000, - gas_price=1, - max_fee_per_gas=2000000000, - max_priority_fee_per_gas=1000000000, - data=b'', - v=0, - r=0, - s=0): - txn = { - "chain_id": chain_id, - "type": _type, +def _make_legacy_txn( + hash=ZERO_32BYTES, + nonce=0, + block_hash=ZERO_32BYTES, + block_number=0, + transaction_index=0, + _from=ZERO_ADDRESS, + to=ZERO_ADDRESS, + value=0, + gas=21000, + gas_price=1, + data=b'', + v=0, + r=0, + s=0 +): + return { "hash": hash, "nonce": nonce, "block_hash": block_hash, @@ -81,58 +77,95 @@ def _make_transaction(chain_id=123456789, "value": value, "gas": gas, "gas_price": gas_price, - "max_fee_per_gas": max_fee_per_gas, - "max_priority_fee_per_gas": max_priority_fee_per_gas, "data": data, "v": v, "r": r, "s": s, } - empty_keys = [] - for k, v in txn.items(): - if v is None: - empty_keys.append(k) - for k in empty_keys: - txn = dissoc(txn, k) - return 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", ), + { + "chain_id": chain_id, + "access_list": access_list, + "y_parity": y_parity, + } + ) + + +def _make_dynamic_fee_txn( + chain_id=131277322940537, + max_fee_per_gas=2000000000, + max_priority_fee_per_gas=1000000000, + access_list=[], + y_parity=0, + **kwargs, +): + legacy_kwargs = dissoc( + dict(**kwargs), + "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"), + { + "chain_id": chain_id, + "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, + } + ) @pytest.mark.parametrize( "transaction,is_valid", ( - (_make_transaction(max_fee_per_gas=None, max_priority_fee_per_gas=None), True), # legacy - (_make_transaction(gas_price=None, _type=2), True), # dynamic fee transaction - (_make_transaction(hash=HASH32_AS_TEXT), False), - (_make_transaction(hash=HASH31), False), - (_make_transaction(nonce=-1), False), - (_make_transaction(nonce=1.0), False), - (_make_transaction(nonce=True), False), - (_make_transaction(value=-1), False), - (_make_transaction(value=1.0), False), - (_make_transaction(value=True), False), - (_make_transaction(block_number=-1), False), - (_make_transaction(block_number=1.0), False), - (_make_transaction(block_number=True), False), - (_make_transaction(gas=-1), False), - (_make_transaction(gas=1.0), False), - (_make_transaction(gas=True), False), - (_make_transaction(gas_price=-1), False), - (_make_transaction(gas_price=1.0), False), - (_make_transaction(gas_price=True), False), - (_make_transaction(data=''), False), - (_make_transaction(data='0x'), False), - (_make_transaction(block_hash=HASH32_AS_TEXT), False), - (_make_transaction(block_hash=HASH31), False), - ( - _make_transaction( - transaction_index=None, - block_hash=None, - block_number=None, - max_fee_per_gas=None, - max_priority_fee_per_gas=None, - ), - True - ), + (_make_legacy_txn(), True), + (_make_access_list_txn(), True), + (_make_dynamic_fee_txn(), True), + (_make_legacy_txn(hash=HASH32_AS_TEXT), False), + (_make_legacy_txn(hash=HASH31), False), + (_make_legacy_txn(nonce=-1), False), + (_make_legacy_txn(nonce=1.0), False), + (_make_legacy_txn(nonce=True), False), + (_make_legacy_txn(value=-1), False), + (_make_legacy_txn(value=1.0), False), + (_make_legacy_txn(value=True), False), + (_make_legacy_txn(block_number=-1), False), + (_make_legacy_txn(block_number=1.0), False), + (_make_legacy_txn(block_number=True), False), + (_make_legacy_txn(gas=-1), False), + (_make_legacy_txn(gas=1.0), False), + (_make_legacy_txn(gas=True), False), + (_make_legacy_txn(gas_price=-1), False), + (_make_legacy_txn(gas_price=1.0), False), + (_make_legacy_txn(gas_price=True), False), + (_make_legacy_txn(data=''), False), + (_make_legacy_txn(data='0x'), False), + (_make_legacy_txn(block_hash=HASH32_AS_TEXT), False), + (_make_legacy_txn(block_hash=HASH31), False), + (_make_legacy_txn(transaction_index=None, block_hash=None, block_number=None), True), + (_make_access_list_txn(transaction_index=None, block_hash=None, block_number=None,), True), + (_make_dynamic_fee_txn(transaction_index=None, block_hash=None, block_number=None,), True), + (_make_access_list_txn(chain_id='1'), False), + (_make_dynamic_fee_txn(chain_id='1'), False), + (_make_access_list_txn(chain_id=-1), False), + (_make_dynamic_fee_txn(chain_id=-1), False), + (_make_access_list_txn(chain_id=None), False), + (_make_dynamic_fee_txn(chain_id=None), False), + (_make_dynamic_fee_txn(max_fee_per_gas=1.0), False), + (_make_dynamic_fee_txn(max_priority_fee_per_gas=1.0), False), + (_make_dynamic_fee_txn(max_fee_per_gas='1'), False), + (_make_dynamic_fee_txn(max_priority_fee_per_gas='1'), False), + (_make_access_list_txn(access_list=((b'\xf0' * 20, (0, 2)),),), True), + (_make_dynamic_fee_txn(access_list=((b'\xf0' * 20, (0, 2)),),), True), + (_make_access_list_txn(access_list=((b'\xf0' * 19, (0, 2)),),), False), + (_make_dynamic_fee_txn(access_list=((b'\xf0' * 19, (0, 2)),),), False), + (_make_access_list_txn(access_list=((b'\xf0' * 20, ('0', 2)),),), False), + (_make_dynamic_fee_txn(access_list=((b'\xf0' * 20, ('0', 2)),),), False), ) ) def test_transaction_output_validation(validator, transaction, is_valid): @@ -341,14 +374,19 @@ def _make_block(number=0, (_make_block(timestamp=-1), False), (_make_block(timestamp=1.0), False), (_make_block(timestamp=True), False), + (_make_block(base_fee_per_gas=1000000000), True), + (_make_block(base_fee_per_gas=-1000000000), False), + (_make_block(base_fee_per_gas=1000000000.0), False), + (_make_block(base_fee_per_gas='1000000000'), False), (_make_block(uncles=[ZERO_32BYTES]), True), (_make_block(uncles=[ZERO_32BYTES, HASH32_AS_TEXT]), False), (_make_block(transactions=[ZERO_32BYTES]), True), - (_make_block(transactions=[_make_transaction(gas_price=None, _type=2)]), True), - ( - _make_block(transactions=[ZERO_32BYTES, _make_transaction(gas_price=None, _type=2)]), - False - ), + (_make_block(transactions=[_make_legacy_txn()]), True), + (_make_block(transactions=[ZERO_32BYTES, _make_legacy_txn()]), False), + (_make_block(transactions=[_make_access_list_txn()]), True), + (_make_block(transactions=[ZERO_32BYTES, _make_access_list_txn()]), False), + (_make_block(transactions=[_make_dynamic_fee_txn()]), True), + (_make_block(transactions=[ZERO_32BYTES, _make_dynamic_fee_txn()]), False), (_make_block(transactions=[ZERO_32BYTES, HASH32_AS_TEXT]), False), ) )