diff --git a/Makefile b/Makefile index 3bd555a0e1..512881f0e1 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ clean-pyc: lint: flake8 evm + flake8 tests --exclude="" test: py.test --tb native tests diff --git a/evm/chain.py b/evm/chain.py index e7f552b3ae..7447c296f3 100644 --- a/evm/chain.py +++ b/evm/chain.py @@ -4,6 +4,7 @@ from operator import itemgetter from eth_utils import ( + pad_right, to_tuple, ) from evm.consensus.pow import ( @@ -41,6 +42,7 @@ from evm.utils.hexidecimal import ( encode_hex, ) +from evm.utils.rlp import diff_rlp_object from evm.state import State @@ -67,26 +69,29 @@ def __init__(self, db, header): self.header = header @classmethod - def configure(cls, name=None, vm_configuration=None): - if vm_configuration is None: - vms_by_range = cls.vms_by_range - else: - # Organize the Chain classes by their starting blocks. - validate_vm_block_numbers(tuple( - block_number - for block_number, _ - in vm_configuration - )) + def configure(cls, name, vm_configuration, **overrides): + if 'vms_by_range' in overrides: + raise ValueError("Cannot override vms_by_range.") + + for key in overrides: + if not hasattr(cls, key): + raise TypeError( + "The Chain.configure cannot set attributes that are not " + "already present on the base class. The attribute `{0}` was " + "not found on the base class `{1}`".format(key, cls) + ) - vms_by_range = collections.OrderedDict(sorted(vm_configuration, key=itemgetter(0))) + validate_vm_block_numbers(tuple( + block_number + for block_number, _ + in vm_configuration + )) - if name is None: - name = cls.__name__ + # Organize the Chain classes by their starting blocks. + overrides['vms_by_range'] = collections.OrderedDict( + sorted(vm_configuration, key=itemgetter(0))) - props = { - 'vms_by_range': vms_by_range, - } - return type(name, (cls,), props) + return type(name, (cls,), overrides) # # Convenience and Helpers @@ -238,6 +243,7 @@ def import_block(self, block): parent_chain = self.get_parent_chain(block) imported_block = parent_chain.get_vm().import_block(block) + self.ensure_blocks_are_equal(imported_block, block) # It feels wrong to call validate_block() on self here, but we do that # because we want to look up the recent uncles starting from the # current canonical chain head. @@ -249,6 +255,27 @@ def import_block(self, block): return imported_block + def ensure_blocks_are_equal(self, block1, block2): + if block1 == block2: + return + diff = diff_rlp_object(block1, block2) + longest_field_name = max(len(field_name) for field_name, _, _ in diff) + error_message = ( + "Mismatch between block and imported block on {0} fields:\n - {1}".format( + len(diff), + "\n - ".join(tuple( + "{0}:\n (actual) : {1}\n (expected): {2}".format( + pad_right(field_name, longest_field_name, ' '), + actual, + expected, + ) + for field_name, actual, expected + in diff + )), + ) + ) + raise ValidationError(error_message) + def get_parent_chain(self, block): try: parent_header = self.get_block_header_by_hash( diff --git a/evm/constants.py b/evm/constants.py index 88404b37d6..2f29b7bbb3 100644 --- a/evm/constants.py +++ b/evm/constants.py @@ -154,7 +154,7 @@ GENESIS_COINBASE = ZERO_ADDRESS GENESIS_NONCE = b'\x00\x00\x00\x00\x00\x00\x00*' # 42 encoded as big-endian-integer GENESIS_MIX_HASH = ZERO_HASH32 -GENESIS_EXTRA_DATA = b'', +GENESIS_EXTRA_DATA = b'' GENESIS_INITIAL_ALLOC = {} diff --git a/evm/vm/base.py b/evm/vm/base.py index a06644b13a..0599707462 100644 --- a/evm/vm/base.py +++ b/evm/vm/base.py @@ -2,18 +2,11 @@ import logging -from eth_utils import ( - pad_right, -) - from evm.constants import ( BLOCK_REWARD, NEPHEW_REWARD, UNCLE_DEPTH_PENALTY_FACTOR, ) -from evm.exceptions import ( - ValidationError, -) from evm.logic.invalid import ( InvalidOpcode, ) @@ -24,9 +17,6 @@ from evm.utils.blocks import ( get_block_header_by_hash, ) -from evm.utils.rlp import ( - diff_rlp_object, -) class VM(object): @@ -95,7 +85,8 @@ def apply_transaction(self, transaction): Apply the transaction to the vm in the current block. """ computation = self.execute_transaction(transaction) - # NOTE: mutation + # NOTE: mutation. Needed in order to update self.state_db, so we should be able to get rid + # of this once we fix https://github.com/pipermerriam/py-evm/issues/67 self.block = self.block.add_transaction( transaction=transaction, computation=computation, @@ -151,27 +142,7 @@ def import_block(self, block): for uncle in block.uncles: self.block.add_uncle(uncle) - mined_block = self.mine_block() - if mined_block != block: - diff = diff_rlp_object(mined_block, block) - longest_field_name = max(len(field_name) for field_name, _, _ in diff) - error_message = ( - "Mismatch between block and imported block on {0} fields:\n - {1}".format( - len(diff), - "\n - ".join(tuple( - "{0}:\n (actual) : {1}\n (expected): {2}".format( - pad_right(field_name, longest_field_name, ' '), - actual, - expected, - ) - for field_name, actual, expected - in diff - )), - ) - ) - raise ValidationError(error_message) - - return mined_block + return self.mine_block() def mine_block(self, *args, **kwargs): """ diff --git a/evm/vm/flavors/frontier/blocks.py b/evm/vm/flavors/frontier/blocks.py index cbc1421fe9..3bc3782f1c 100644 --- a/evm/vm/flavors/frontier/blocks.py +++ b/evm/vm/flavors/frontier/blocks.py @@ -307,6 +307,9 @@ def add_uncle(self, uncle): self.header.uncles_hash = keccak(rlp.encode(self.uncles)) return self + # TODO: Check with Piper what's the use case for having the mine() method allowing callsites + # to override header attributes, since the only place it's used we don't pass any kwarg and + # hence it just performs block-level validation. def mine(self, **kwargs): """ - `uncles_hash` diff --git a/tests/core/chain-object/test_chain.py b/tests/core/chain-object/test_chain.py new file mode 100644 index 0000000000..8c0142c892 --- /dev/null +++ b/tests/core/chain-object/test_chain.py @@ -0,0 +1,42 @@ +import rlp + +from eth_utils import decode_hex + +from evm import constants +from evm.vm.flavors.frontier.blocks import FrontierBlock + +from tests.core.fixtures import ( # noqa: F401 + chain, + chain_without_block_validation, + valid_block_rlp, +) +from tests.core.helpers import new_transaction + + +def test_import_block_validation(chain): # noqa: F811 + block = rlp.decode(valid_block_rlp, sedes=FrontierBlock, db=chain.db) + imported_block = chain.import_block(block) + assert len(imported_block.transactions) == 1 + tx = imported_block.transactions[0] + assert tx.value == 10 + vm = chain.get_vm() + assert vm.state_db.get_balance( + decode_hex("095e7baea6a6c7c4c2dfeb977efac326af552d87")) == tx.value + tx_gas = tx.gas_price * constants.GAS_TX + assert vm.state_db.get_balance(chain.funded_address) == ( + chain.funded_address_initial_balance - tx.value - tx_gas) + + +def test_import_block(chain_without_block_validation): # noqa: F811 + chain = chain_without_block_validation # noqa: F811 + recipient = decode_hex('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0c') + amount = 100 + vm = chain.get_vm() + from_ = chain.funded_address + tx = new_transaction(vm, from_, recipient, amount, chain.funded_address_private_key) + computation = vm.apply_transaction(tx) + assert computation.error is None + block = chain.import_block(vm.block) + assert block.transactions == [tx] + assert chain.get_block_by_hash(block.hash) == block + assert chain.get_canonical_block_by_number(block.number) == block diff --git a/tests/core/chain-object/test_chain_retrieval_of_vm_class.py b/tests/core/chain-object/test_chain_retrieval_of_vm_class.py index 4d179ac405..bcb3537791 100644 --- a/tests/core/chain-object/test_chain_retrieval_of_vm_class.py +++ b/tests/core/chain-object/test_chain_retrieval_of_vm_class.py @@ -21,6 +21,7 @@ def test_get_vm_class_for_block_number(): chain_class = Chain.configure( + name='TestChain', vm_configuration=( (constants.GENESIS_BLOCK_NUMBER, FrontierVM), (constants.HOMESTEAD_MAINNET_BLOCK, HomesteadVM), @@ -38,13 +39,13 @@ def test_get_vm_class_for_block_number(): def test_invalid_if_no_vm_configuration(): - chain_class = Chain.configure(vm_configuration=()) + chain_class = Chain.configure('TestChain', vm_configuration=()) with pytest.raises(ValueError): chain_class(get_db_backend(), BlockHeader(1, 0, 100)) def test_vm_not_found_if_no_matching_block_number(): - chain_class = Chain.configure(vm_configuration=( + chain_class = Chain.configure('TestChain', vm_configuration=( (10, FrontierVM), )) chain = chain_class(get_db_backend(), BlockHeader(1, 0, 100)) @@ -54,12 +55,12 @@ def test_vm_not_found_if_no_matching_block_number(): def test_configure_invalid_block_number_in_vm_configuration(): with pytest.raises(ValidationError): - Chain.configure(vm_configuration=[(-1, FrontierVM)]) + Chain.configure('TestChain', vm_configuration=[(-1, FrontierVM)]) def test_configure_duplicate_block_numbers_in_vm_configuration(): with pytest.raises(ValidationError): - Chain.configure(vm_configuration=[ + Chain.configure('TestChain', vm_configuration=[ (0, FrontierVM), (0, HomesteadVM), ]) diff --git a/tests/core/fixtures.py b/tests/core/fixtures.py new file mode 100644 index 0000000000..47b505e796 --- /dev/null +++ b/tests/core/fixtures.py @@ -0,0 +1,128 @@ +import pytest + +from eth_utils import ( + decode_hex, + to_canonical_address, +) + +from evm import Chain +from evm import constants +from evm.db import get_db_backend +from evm.utils.address import private_key_to_address +from evm.vm.flavors.frontier import FrontierVM + + +@pytest.fixture +def chain(): + """ + Return a Chain object containing just the genesis block. + + The Chain's state includes one funded account, which can be found in the funded_address in the + chain itself. + + This Chain will perform all validations when importing new blocks, so only valid and finalized + blocks can be used with it. If you want to test importing arbitrarily constructe, not + finalized blocks, use the chain_without_block_validation fixture instead. + """ + genesis_params = { + "bloom": 0, + "coinbase": to_canonical_address("8888f1f195afa192cfee860698584c030f4c9db1"), + "difficulty": 131072, + "extra_data": b"B", + "gas_limit": 3141592, + "gas_used": 0, + "mix_hash": decode_hex( + "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + "nonce": decode_hex("0102030405060708"), + "block_number": 0, + "parent_hash": decode_hex( + "0000000000000000000000000000000000000000000000000000000000000000"), + "receipt_root": decode_hex( + "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + "state_root": decode_hex( + "cafd881ab193703b83816c49ff6c2bf6ba6f464a1be560c42106128c8dbc35e7"), + "timestamp": 1422494849, + "transaction_root": decode_hex( + "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + "uncles_hash": decode_hex( + "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") + } + funded_addr = to_canonical_address("a94f5374fce5edbc8e2a8697c15331677e6ebf0b") + initial_balance = 10000000000 + genesis_state = { + funded_addr: { + "balance": initial_balance, + "nonce": 0, + "code": b"", + "storage": {} + } + } + klass = Chain.configure( + name='TestChain', + vm_configuration=( + (constants.GENESIS_BLOCK_NUMBER, FrontierVM), + )) + chain = klass.from_genesis(get_db_backend(), genesis_params, genesis_state) + chain.funded_address = funded_addr + chain.funded_address_initial_balance = initial_balance + return chain + + +# This block is a child of the genesis defined in the chain fixture above and contains a single tx +# that transfers 10 wei from 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b to +# 0x095e7baea6a6c7c4c2dfeb977efac326af552d87. +valid_block_rlp = decode_hex( + "0xf90260f901f9a07285abd5b24742f184ad676e31f6054663b3529bc35ea2fcad8a3e0f642a46f7a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0964e6c9995e7e3757e934391b4f16b50c20409ee4eb9abd4c4617cb805449b9aa053d5b71a8fbb9590de82d69dfa4ac31923b0c8afce0d30d0d8d1e931f25030dca0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845754132380a0194605bacef646779359318c7b5899559a5bf4074bbe2cfb7e1b83b1504182dd88e0205813b22e5a9cf861f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba0f3266921c93d600c43f6fa4724b7abae079b35b9e95df592f95f9f3445e94c88a012f977552ebdb7a492cf35f3106df16ccb4576ebad4113056ee1f52cbe4978c1c0") # noqa: E501 + + +@pytest.fixture +def chain_without_block_validation(): + """ + Return a Chain object containing just the genesis block. + + This Chain does not perform any validation when importing new blocks. + + The Chain's state includes one funded account and a private key for it, which can be found in + the funded_address and private_keys variables in the chain itself. + """ + # Disable block validation so that we don't need to construct finalized blocks. + overrides = { + 'ensure_blocks_are_equal': lambda self, b1, b2: None, + 'validate_block': lambda self, block: None, + } + klass = Chain.configure( + name='TestChainWithoutBlockValidation', + vm_configuration=( + (constants.GENESIS_BLOCK_NUMBER, FrontierVM), + ), + **overrides, + ) + private_key = decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8') + funded_addr = private_key_to_address(private_key) + initial_balance = 100000000 + genesis_params = { + 'block_number': constants.GENESIS_BLOCK_NUMBER, + 'difficulty': constants.GENESIS_DIFFICULTY, + 'gas_limit': constants.GENESIS_GAS_LIMIT, + 'parent_hash': constants.GENESIS_PARENT_HASH, + 'coinbase': constants.GENESIS_COINBASE, + 'nonce': constants.GENESIS_NONCE, + 'mix_hash': constants.GENESIS_MIX_HASH, + 'extra_data': constants.GENESIS_EXTRA_DATA, + 'timestamp': 1501851927, + 'state_root': decode_hex( + '0x9d354f9b5ba851a35eced279ef377111387197581429cfcc7f744ef89a30b5d4') + } + genesis_state = { + funded_addr: { + 'balance': initial_balance, + 'nonce': 0, + 'code': b'', + 'storage': {}, + } + } + chain = klass.from_genesis(get_db_backend(), genesis_params, genesis_state) + chain.funded_address = funded_addr + chain.funded_address_initial_balance = initial_balance + chain.funded_address_private_key = private_key + return chain diff --git a/tests/core/helpers.py b/tests/core/helpers.py new file mode 100644 index 0000000000..d31a4a1461 --- /dev/null +++ b/tests/core/helpers.py @@ -0,0 +1,10 @@ +def new_transaction(vm, from_, to, amount, private_key, gas_price=10, gas=100000): + """ + Create and return a transaction sending amount from to . + + The transaction will be signed with the given private key. + """ + nonce = vm.state_db.get_nonce(from_) + tx = vm.create_unsigned_transaction( + nonce=nonce, gas_price=gas_price, gas=gas, to=to, value=amount, data=b'') + return tx.as_signed_transaction(private_key) diff --git a/tests/core/vm/test_vm.py b/tests/core/vm/test_vm.py new file mode 100644 index 0000000000..f08c130334 --- /dev/null +++ b/tests/core/vm/test_vm.py @@ -0,0 +1,48 @@ +from eth_utils import decode_hex + +from evm import constants + +from tests.core.fixtures import chain_without_block_validation # noqa: F401 +from tests.core.helpers import new_transaction + + +def test_apply_transaction(chain_without_block_validation): # noqa: F811 + chain = chain_without_block_validation # noqa: F811 + vm = chain.get_vm() + tx_idx = len(vm.block.transactions) + recipient = decode_hex('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0c') + amount = 100 + from_ = chain.funded_address + tx = new_transaction(vm, from_, recipient, amount, chain.funded_address_private_key) + computation = vm.apply_transaction(tx) + assert computation.error is None + tx_gas = tx.gas_price * constants.GAS_TX + assert vm.state_db.get_balance(from_) == ( + chain.funded_address_initial_balance - amount - tx_gas) + assert vm.state_db.get_balance(recipient) == amount + block = vm.block + assert block.transactions[tx_idx] == tx + assert block.header.gas_used == constants.GAS_TX + assert block.header.state_root == computation.state_db.root_hash + + +def test_mine_block(chain_without_block_validation): # noqa: F811 + chain = chain_without_block_validation # noqa: F811 + vm = chain.get_vm() + block = vm.mine_block() + assert vm.state_db.get_balance(block.header.coinbase) == constants.BLOCK_REWARD + assert block.header.state_root == vm.state_db.root_hash + + +def test_import_block(chain_without_block_validation): # noqa: F811 + chain = chain_without_block_validation # noqa: F811 + vm = chain.get_vm() + recipient = decode_hex('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0c') + amount = 100 + from_ = chain.funded_address + tx = new_transaction(vm, from_, recipient, amount, chain.funded_address_private_key) + computation = vm.apply_transaction(tx) + assert computation.error is None + parent_vm = chain.get_parent_chain(vm.block).get_vm() + block = parent_vm.import_block(vm.block) + assert block.transactions == [tx]