-
Notifications
You must be signed in to change notification settings - Fork 686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Incremental block construction #598
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ | |
MAX_UNCLE_DEPTH, | ||
) | ||
from evm.db.chain import AsyncChainDB | ||
from evm.db.trie import make_trie_root_and_nodes | ||
from evm.estimators import ( | ||
get_gas_estimator, | ||
) | ||
|
@@ -209,6 +210,13 @@ def get_vm(self, header=None): | |
# | ||
# Execution API | ||
# | ||
@abstractmethod | ||
def apply_transaction(self, transaction): | ||
""" | ||
Applies the transaction to the current tip block. | ||
""" | ||
raise NotImplementedError("Chain classes must implement this method") | ||
|
||
@abstractmethod | ||
def estimate_gas(self, transaction, at_header=None): | ||
""" | ||
|
@@ -480,6 +488,26 @@ def from_genesis_header(cls, chaindb, genesis_header): | |
# | ||
# Mining and Execution API | ||
# | ||
def apply_transaction(self, transaction): | ||
""" | ||
Applies the transaction to the current tip block. | ||
""" | ||
vm = self.get_vm() | ||
block, receipt, computation = vm.apply_transaction(transaction) | ||
receipts = block.get_receipts(self.chaindb) + [receipt] | ||
|
||
tx_root_hash, tx_kv_nodes = make_trie_root_and_nodes(block.transactions) | ||
receipt_root_hash, receipt_kv_nodes = make_trie_root_and_nodes(receipts) | ||
self.chaindb.persist_trie_data_dict(tx_kv_nodes) | ||
self.chaindb.persist_trie_data_dict(receipt_kv_nodes) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 though I'm going to be fixing this in a subsequent PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a big warning to the docstring here stating that this will generate/persist partial block state and that has a big performance hit? |
||
|
||
self.header = block.header.copy( | ||
transaction_root=tx_root_hash, | ||
receipt_root=receipt_root_hash, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we update |
||
|
||
return block.copy(header=self.header), receipt, computation | ||
|
||
def estimate_gas(self, transaction, at_header=None): | ||
if at_header is None: | ||
at_header = self.get_canonical_head() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,7 +63,6 @@ def __init__(self, header, chaindb): | |
self.chaindb = chaindb | ||
block_class = self.get_block_class() | ||
self.block = block_class.from_header(header=header, chaindb=self.chaindb) | ||
self.receipts = [] # type: List[Receipt] | ||
|
||
# | ||
# Logging | ||
|
@@ -79,14 +78,14 @@ def apply_transaction(self, transaction): | |
""" | ||
Apply the transaction to the vm in the current block. | ||
""" | ||
computation, block, receipt = self.state.apply_transaction( | ||
block, receipt, computation = self.state.apply_transaction( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: this ordering is structured from highest to lowest level elements. |
||
transaction, | ||
self.block, | ||
) | ||
self.block = block | ||
self.receipts.append(receipt) | ||
self.chaindb.persist() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it's gross to have db writes sprinkled about. But... I think we'll have to come back to that one in a future PR. |
||
|
||
return computation, self.block | ||
return self.block, receipt, computation | ||
|
||
def execute_bytecode(self, | ||
origin, | ||
|
@@ -130,7 +129,6 @@ def execute_bytecode(self, | |
# Mining | ||
# | ||
def import_block(self, block): | ||
self.receipts = [] | ||
self.block = self.block.copy( | ||
header=self.configure_header( | ||
coinbase=block.header.coinbase, | ||
|
@@ -145,26 +143,42 @@ def import_block(self, block): | |
) | ||
|
||
# run all of the transactions. | ||
for transaction in block.transactions: | ||
execution_data = [ | ||
self.apply_transaction(transaction) | ||
for transaction | ||
in block.transactions | ||
] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something about generating a list this way makes it less obvious that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Fixing in subsequent refactoring steps. |
||
if execution_data: | ||
_, receipts, _ = zip(*execution_data) | ||
else: | ||
receipts = tuple() | ||
|
||
return self.mine_block() | ||
tx_root_hash, tx_kv_nodes = make_trie_root_and_nodes(block.transactions) | ||
receipt_root_hash, receipt_kv_nodes = make_trie_root_and_nodes(receipts) | ||
|
||
def mine_block(self, *args, **kwargs): | ||
""" | ||
Mine the current block. Proxies to self.pack_block method. | ||
""" | ||
tx_root_hash, tx_kv_nodes = make_trie_root_and_nodes(self.block.transactions) | ||
receipt_root_hash, receipt_kv_nodes = make_trie_root_and_nodes(self.receipts) | ||
self.chaindb.persist_trie_data_dict(tx_kv_nodes) | ||
self.chaindb.persist_trie_data_dict(receipt_kv_nodes) | ||
|
||
header = self.block.header.copy( | ||
transaction_root=tx_root_hash, | ||
receipt_root=receipt_root_hash, | ||
if receipts: | ||
gas_used = receipts[-1].gas_used | ||
else: | ||
gas_used = 0 | ||
|
||
self.block = self.block.copy( | ||
header=self.block.header.copy( | ||
transaction_root=tx_root_hash, | ||
receipt_root=receipt_root_hash, | ||
gas_used=gas_used, | ||
), | ||
) | ||
|
||
block = self.pack_block(self.block.copy(header=header), *args, **kwargs) | ||
return self.mine_block() | ||
|
||
def mine_block(self, *args, **kwargs): | ||
""" | ||
Mine the current block. Proxies to self.pack_block method. | ||
""" | ||
block = self.pack_block(self.block, *args, **kwargs) | ||
|
||
if block.number == 0: | ||
return block | ||
|
@@ -218,10 +232,7 @@ def state_in_temp_block(self): | |
header = self.block.header | ||
temp_block = self.generate_block_from_parent_header_and_coinbase(header, header.coinbase) | ||
prev_hashes = (header.hash, ) + self.previous_hashes | ||
gas_used = 0 | ||
if self.receipts: | ||
gas_used = self.receipts[-1].gas_used | ||
state = self.get_state(self.chaindb, temp_block, prev_hashes, gas_used) | ||
state = self.get_state(self.chaindb, temp_block, prev_hashes, gas_used=0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hah, yay for unrelated bugfixes ( |
||
assert state.gas_used == 0, "There must not be any gas used in a fresh temporary block" | ||
|
||
snapshot = state.snapshot() | ||
|
@@ -467,12 +478,9 @@ def get_state(cls, chaindb, block, prev_hashes, gas_used): | |
def state(self): | ||
"""Return current state property | ||
""" | ||
gas_used = 0 | ||
if self.receipts: | ||
gas_used = self.receipts[-1].gas_used | ||
return self.get_state( | ||
chaindb=self.chaindb, | ||
block=self.block, | ||
prev_hashes=self.previous_hashes, | ||
gas_used=gas_used, | ||
gas_used=self.block.header.gas_used, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIUC, before this change, on every call to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add a test to check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is currently handled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then in import_block() above we don't need to update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 I'll get this cleaned up in a subsequent PR. adding a comment. |
||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,8 @@ def vm_logger(): | |
|
||
# level = TRACE_LEVEL_NUM | ||
# level = logging.DEBUG | ||
level = logging.INFO | ||
# level = logging.INFO | ||
level = logging.ERROR | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: This came from me ending up with a terminal full of |
||
|
||
logger.setLevel(level) | ||
handler.setLevel(level) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import pytest | ||
|
||
from evm.utils.address import force_bytes_to_address | ||
|
||
from tests.core.helpers import ( | ||
new_transaction, | ||
) | ||
|
||
|
||
ADDRESS_1010 = force_bytes_to_address(b'\x10\x10') | ||
|
||
|
||
@pytest.fixture | ||
def chain(chain_without_block_validation): | ||
return chain_without_block_validation | ||
|
||
|
||
def test_building_block_incrementally_with_single_transaction(chain, | ||
funded_address, | ||
funded_address_private_key): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style nitpick def long_signature(
thing_a,
thing_b,
thing_c=None,
thing_d=None):
pass |
||
tx = new_transaction( | ||
chain.get_vm(), | ||
from_=funded_address, | ||
to=ADDRESS_1010, | ||
private_key=funded_address_private_key, | ||
) | ||
_, _, computation = chain.apply_transaction(tx) | ||
assert computation.is_success | ||
|
||
mined_block = chain.mine_block() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before mining, there might be other things we want to assert. Perhaps:
|
||
assert len(mined_block.transactions) == 1 | ||
|
||
actual_tx = mined_block.transactions[0] | ||
assert actual_tx == tx | ||
|
||
|
||
def test_building_block_incrementally_with_multiple_transactions(chain, | ||
funded_address, | ||
funded_address_private_key): | ||
txns = [] | ||
for _ in range(3): | ||
tx = new_transaction( | ||
chain.get_vm(), | ||
from_=funded_address, | ||
to=ADDRESS_1010, | ||
private_key=funded_address_private_key, | ||
) | ||
txns.append(tx) | ||
_, _, computation = chain.apply_transaction(tx) | ||
assert computation.is_success | ||
|
||
mined_block = chain.mine_block() | ||
assert len(mined_block.transactions) == 3 | ||
|
||
for left, right in zip(txns, mined_block.transactions): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe: -left, right
+expected, actual |
||
assert left == right |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ def tx(chain, funded_address, funded_address_private_key): | |
return new_transaction(vm, from_, recipient, amount, funded_address_private_key) | ||
|
||
|
||
@pytest.mark.xfail(reason="modification to initial allocation made the block fixture invalid") | ||
def test_import_block_validation(valid_chain, funded_address, funded_address_initial_balance): | ||
block = rlp.decode(valid_block_rlp, sedes=FrontierBlock) | ||
imported_block = valid_chain.import_block(block) | ||
|
@@ -55,8 +56,8 @@ def test_import_block_validation(valid_chain, funded_address, funded_address_ini | |
|
||
def test_import_block(chain, tx): | ||
vm = chain.get_vm() | ||
computation, _ = vm.apply_transaction(tx) | ||
assert not computation.is_error | ||
*_, computation = vm.apply_transaction(tx) | ||
assert computation.is_success | ||
|
||
# add pending so that we can confirm it gets removed when imported to a block | ||
chain.add_pending_transaction(tx) | ||
|
@@ -86,6 +87,7 @@ def test_empty_transaction_lookups(chain): | |
chain.get_pending_transaction(b'\0' * 32) | ||
|
||
|
||
@pytest.mark.xfail(reason="modification to initial allocation made the block fixture invalid") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I broke these tests by modifying the genesis state for the fixture (by increasing the initial ether allotment to the |
||
def test_canonical_chain(valid_chain): | ||
genesis_header = valid_chain.chaindb.get_canonical_block_header_by_number( | ||
constants.GENESIS_BLOCK_NUMBER) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,13 +3,16 @@ | |
from eth_utils import ( | ||
decode_hex, | ||
to_canonical_address, | ||
to_wei, | ||
) | ||
from eth_keys import KeyAPI | ||
from eth_keys import keys | ||
|
||
from evm import Chain | ||
from evm import constants | ||
from evm.db import get_db_backend | ||
from evm.db.chain import ChainDB | ||
# TODO: tests should not be locked into one set of VM rules. Look at expanding | ||
# to all mainnet vms. | ||
from evm.vm.forks.spurious_dragon import SpuriousDragonVM | ||
|
||
|
||
|
@@ -19,7 +22,7 @@ def import_block_without_validation(chain, block): | |
|
||
@pytest.fixture | ||
def funded_address_private_key(): | ||
return KeyAPI().PrivateKey( | ||
return keys.PrivateKey( | ||
decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8') | ||
) | ||
|
||
|
@@ -31,7 +34,7 @@ def funded_address(funded_address_private_key): | |
|
||
@pytest.fixture | ||
def funded_address_initial_balance(): | ||
return 10000000000 | ||
return to_wei(1000, 'ether') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: This is the change that broke the block import fixture |
||
|
||
|
||
@pytest.fixture | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels a little weird. We're getting back the block after the transaction, but it doesn't have the receipts that were also produced as a part of the transaction and some of the final header fields.
I think I get where you're eventually going: that we shouldn't return a block at all, but the building pieces that are needed to form the block. It is just hard to track which of those changes are still going in the block and which aren't. Maybe that's fine for this PR, where you're just trying to enable this one feature of incremental building, and come back for a reorg later.
A
TransactionDiff
is continuing to look like a nice encapsulation, which would include all the things that change in the block, plus the receipt, and the computation.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, though I'm going to push fixing this out a bit as I'm about to make a number of incremental changes. Good spot as I hadn't quite connected that logic (should not return a block unless the block is actually complete).