Skip to content
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

Merged

Conversation

pipermerriam
Copy link
Member

@pipermerriam pipermerriam commented Apr 25, 2018

Fixes #579

What was wrong?

The way in which eth-tester uses py-evm requires the ability to incrementally apply transactions to the tip block and then to mine the block when ready.

Care is being taken to ensure that VM.import_block is still able to operate with a low amount of overhead when importing the block.

How was it fixed?

This is still a work in progress

Implemented Chain.add_transaction.

Cute Animal Picture

put a cute animal picture link inside the parentheses

@pipermerriam pipermerriam force-pushed the piper/incremental-block-construction branch 2 times, most recently from 16f372f to cda3fee Compare April 26, 2018 23:53
@@ -79,16 +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(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: this ordering is structured from highest to lowest level elements.

@@ -14,7 +14,8 @@ def vm_logger():

# level = TRACE_LEVEL_NUM
# level = logging.DEBUG
level = logging.INFO
# level = logging.INFO
level = logging.ERROR
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This came from me ending up with a terminal full of INFO level logging messages long enough to push the actual test failure off the history of my terminal.

assert chain.estimate_gas(next_pending_tx, chain.header) == 722760


@pytest.mark.xfail(reason="modification to initial allocation made the block fixture invalid")
Copy link
Member Author

Choose a reason for hiding this comment

The 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 funded_address. I decided not to fix it because I'll be completely reworking this test suite as part of #599


@pytest.fixture
def funded_address_initial_balance():
return to_wei(1000, 'ether')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This is the change that broke the block import fixture

@pipermerriam pipermerriam requested review from carver and gsalgado and removed request for carver April 27, 2018 02:49
@pipermerriam
Copy link
Member Author

@carver @gsalgado This is a gross pull request. I'm basically planning to largely refactor in entirety pretty much every section of code that this PR touches so if you guys are ok with it, review with a focus on functional correctness.

@pipermerriam pipermerriam changed the title WIP: Incremental block construction Incremental block construction Apr 27, 2018
@pipermerriam pipermerriam force-pushed the piper/incremental-block-construction branch from c095a4f to bd4ba13 Compare April 27, 2018 20:49
Copy link
Contributor

@carver carver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this change highlights the need for #599. Ultimately most of these comments here are forward-looking to that change, just pointing out places that I think need love.

Apart from style tweaks, the main thing that I think could improve this PR is a couple extra assertions in one of the tests. (commented inline)


def test_building_block_incrementally_with_single_transaction(chain,
funded_address,
funded_address_private_key):
Copy link
Contributor

Choose a reason for hiding this comment

The 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

_, _, computation = chain.apply_transaction(tx)
assert computation.is_success

mined_block = chain.mine_block()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before mining, there might be other things we want to assert. Perhaps:

  • that the transaction is present in the pending block
  • that it is not present in the latest block

mined_block = chain.mine_block()
assert len(mined_block.transactions) == 3

for left, right in zip(txns, mined_block.transactions):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

-left, right
+expected, actual

"""
vm = self.get_vm()
block, receipt, computation = vm.apply_transaction(transaction)
receipts = block.get_receipts(self.chaindb) + [receipt]
Copy link
Contributor

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.

Copy link
Member Author

@pipermerriam pipermerriam Apr 29, 2018

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).

evm/vm/base.py Outdated
transaction,
self.block,
)
self.block = block
self.receipts.append(receipt)
self.chaindb.persist()
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

self.apply_transaction(transaction)
for transaction
in block.transactions
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something about generating a list this way makes it less obvious that apply_transaction() is also mutating the state in the VM. I guess if we become super consistent about apply => "mutation", then it's not a problem.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Fixing in subsequent refactoring steps.

evm/vm/base.py Outdated
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, yay for unrelated bugfixes (gas_used should always start at 0 in a temp 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)
self.chaindb.persist_trie_data_dict(tx_kv_nodes)
self.chaindb.persist_trie_data_dict(receipt_kv_nodes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Just wondering, since now there's no "clean up function", after calling Chain.apply_transaction(transaction) (Maybe it's miner to select transactions or eth-tester), it seems the transactions and receipts will be in the database permanently...?
  2. Those four lines appear twice, maybe add a simple function to deal with it?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we update gas_used here as well?

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, before this change, on every call to VM.apply_transaction(), the VM.state would include the gas_used up to the last applied tx, but now it won't as self.block.header.gas_used is only updated at the end of VM.import_block(). Is that ok or can it be a problem when applying transactions that would exceed the block's gas limit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a test to check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently handled. State.apply_transaction calls State.add_transaction which updates the block header's gas_used. And VM.apply_transaction updates self.block on each call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then in import_block() above we don't need to update gas_used when we do self.block.copy()?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

@pipermerriam pipermerriam merged commit a42dfb0 into ethereum:master Apr 30, 2018
@pipermerriam pipermerriam deleted the piper/incremental-block-construction branch April 30, 2018 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ability to incrementally build a block
4 participants