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

Handle pending transactions #60

Merged
merged 12 commits into from
Feb 20, 2018

Conversation

monokh
Copy link
Contributor

@monokh monokh commented Feb 10, 2018

What was wrong?

Eth tester does not handle pending transactions correctly when disabling auto mining transactions:

Sending a transaction with the same nonce causes an invalid nonce error when auto mining transactions are disabled.

The issue is that send_transaction and send_raw_transaction immediately execute on the vm's state and thereby increment the valid nonce.

How was it fixed?

Implement a transaction pool at the eth-tester level so that transactions are only executed when auto mining is enabled again.

Cute Animal Picture

Cute animal picture

Fixes #59

@monokh monokh changed the title Handle pending transactions: initial implementation Handle pending transactions Feb 10, 2018
eth_tester.disable_auto_mine_transactions()
transaction_hash = eth_tester.send_transaction({
"from": eth_tester.get_accounts()[0],
"to": BURN_ADDRESS,
"gas": 21000,
})
receipt = eth_tester.get_transaction_receipt(transaction_hash)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to eth_getTransactionReceipt this shouldn't work.

Changing this test made me realise that get_transaction_by_hash should be able to retrieve the pending transaction - it doesn't right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does now

# Remove any pending transactions with the same nonce
self._pending_transactions = [tx for tx in self._pending_transactions
if not (pending_transaction['nonce'] == tx['nonce']
and pending_transaction['from'] == tx['from'])]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

and chainId equals?

Copy link
Member

Choose a reason for hiding this comment

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

Probably, but way less important than the others.

Copy link
Contributor

Choose a reason for hiding this comment

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

I expect the node to reject a transaction if you send it with a different chainid than the node is syncing with, so the handling of failed transactions should be sufficient.

@@ -244,15 +273,25 @@ def get_nonce(self, account, block_number="latest"):
#
# Blocks, Transactions, Receipts
#

def _get_pending_transaction_by_hash(self, transaction_hash):
for transaction in self._pending_transactions:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not very performant. If that is a concern, can make _pending_transaction a dict with key=hash value=transaction

Copy link
Member

Choose a reason for hiding this comment

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

Eh, it'd need to be an ordered dict to preserve transaction order right? I think that would be fine, but it's not a big deal. The performance hit here isn't likely to be measurable against the actual evm execution overhead.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not a concern, at least for now. If someone wants to test a really high pending count, we can revisit.

@monokh
Copy link
Contributor Author

monokh commented Feb 12, 2018

If this is ok, docs for get_transaction_by_hash and enable_auto_mine_transactions will need updating to reflect the change of behaviour

# Remove any pending transactions with the same nonce
self._pending_transactions = [tx for tx in self._pending_transactions
if not (pending_transaction['nonce'] == tx['nonce']
and pending_transaction['from'] == tx['from'])]
Copy link
Member

Choose a reason for hiding this comment

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

Probably, but way less important than the others.

else:
pending_transaction = self.get_transaction_by_hash(transaction_hash)
# Remove any pending transactions with the same nonce
self._pending_transactions = [tx for tx in self._pending_transactions
Copy link
Member

Choose a reason for hiding this comment

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

Can we unroll this comprehension into a small stand alone utility function for better readability as well as easier stand-alone testing.

@to_tuple
def (tx, pending_transactions):
    for transaction in pending_transactions:
        ...
        yield transaction

Something that's got the same logic, but unrolled into an easier to read code body.

@@ -244,15 +273,25 @@ def get_nonce(self, account, block_number="latest"):
#
# Blocks, Transactions, Receipts
#

def _get_pending_transaction_by_hash(self, transaction_hash):
for transaction in self._pending_transactions:
Copy link
Member

Choose a reason for hiding this comment

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

Eh, it'd need to be an ordered dict to preserve transaction order right? I think that would be fine, but it's not a big deal. The performance hit here isn't likely to be measurable against the actual evm execution overhead.

self.validator.validate_outbound_transaction(raw_transaction)
transaction = self.normalizer.normalize_outbound_transaction(raw_transaction)
return transaction
pending_transaction = self._get_pending_transaction_by_hash(transaction_hash)
Copy link
Member

Choose a reason for hiding this comment

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

Rather than having this function return None when the transaction isn't found, I think it'd be better if it raised TransactionNotFound. This way nobody accidentally returns None because they forgot to check the return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, it also makes the flow in get_transaction_by_hash easier to read

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'd like to hear your thoughts on everything, but the try/finally block is the only thing that really needs to be addressed (either by a code change, or explaining why I'm wrong :P... ) before being merged.

self._pending_transactions = remove_matching_transaction_from_list(
self._pending_transactions, pending_transaction)
self._pending_transactions.append(pending_transaction)
self.revert_to_snapshot(snapshot)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm thinking this revert might need to be in a try/finally block. Otherwise, if there is any kind of exception after a state change, we will fail to roll back.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I understand. Isn't revert_to_snapshot doing the rollback? And if that fails, is there really a meaningful way to continue?

It feels to me like the whole function should completely cleanly otherwise the final state is not really usable

Copy link
Contributor

@carver carver Feb 16, 2018

Choose a reason for hiding this comment

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

Ah, let me clarify with some code. Something like this:

snapshot = self.take_snapshot()

try:
    transaction_hash = func(self, *args, **kwargs)
    pending_transaction = self.get_transaction_by_hash(transaction_hash)
    ...
    self._pending_transactions.append(pending_transaction)
finally:
    self.revert_to_snapshot(snapshot)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh 😄 I see what you mean. That's it updated.

@@ -0,0 +1,31 @@
# TODO: Should this be moved to a common package like eth-utils?
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, VALID_TRANSACTION_PARAMS seems to have popped up in several places already, so worth talking about extracting it. eth-utils feels a little wrong, nothing else in there is quite so tightly bound to chain implementation details as "which fields are in a transaction". Maybe we could add a Transaction rlp class to https://github.com/ethereum/eth-rlp and have VALID_TRANSACTION_PARAMS be something like Transaction.fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. I thought maybe the utility functions could also go somewhere. Would like to tackle this as a separate issue as both eth-tester and web3.py need changed.

Copy link
Member

@pipermerriam pipermerriam Feb 19, 2018

Choose a reason for hiding this comment

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

Note that we shouldn't call it Transaction because we're on the cusp of having multiple disparate transaction types with different fields. Should be namespaced in some way to allow for easy introduction of new transaction types.

@@ -285,6 +326,10 @@ def get_transaction_receipt(self, transaction_hash):
#
def enable_auto_mine_transactions(self):
self.auto_mine_transactions = True
sent_transaction_hashes = [self.send_transaction(extract_valid_transaction_params(tx))
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we be strict here and reject transactions that have invalid transaction params?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is too much of un unlikely scenario to be handled. These transactions are added to the list from get_transaction_by_hash which should be valid. The only real purpose of this extraction is to not send the params like v,r,s from the dict that is returned by get_transaction_by_hash.

We could add validation when we add the transaction to the list but I don't really see the purpose.

Copy link
Contributor

Choose a reason for hiding this comment

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

These transactions are added to the list from get_transaction_by_hash

Fair enough, I'm okay skipping this validation.

if not self.auto_mine_transactions:
snapshot = self.take_snapshot()

transaction_hash = func(self, *args, **kwargs)
Copy link
Contributor

@carver carver Feb 17, 2018

Choose a reason for hiding this comment

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

I think we should revert if func raises an exception too.

Maybe we should just have two main blocks, whether auto_mine is on, and just duplicate the func() call, like:

+        if self.auto_mine_transactions:
+            transaction_hash = func(self, *args, **kwargs)
+            self.mine_block()
+        else:
+            snapshot = self.take_snapshot()
+            try:
+                transaction_hash = func(self, *args, **kwargs)
+                pending_transaction = self.get_transaction_by_hash(transaction_hash)
+                # Remove any pending transactions with the same nonce
+                self._pending_transactions = remove_matching_transaction_from_list(
+                    self._pending_transactions, pending_transaction)
+                self._pending_transactions.append(pending_transaction)
+            finally:
+                self.revert_to_snapshot(snapshot)

I find that easier to follow than the repeated "if auto-mine" tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes that definitely is easier to read 😄

Copy link
Member

@pipermerriam pipermerriam left a comment

Choose a reason for hiding this comment

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

@monokh if this is ready, it looks good to me.

@pipermerriam
Copy link
Member

@monokh lemme know if your ready for me to merge/release this.

@monokh
Copy link
Contributor Author

monokh commented Feb 19, 2018

@pipermerriam Yes, please merge and release :) then I can update ethereum/web3.py#586 to use the new version. I tested locally and all the failing tests were resolved with these 'eth-tester' changes

@carver carver merged commit 0b18e94 into ethereum:master Feb 20, 2018
pacrob pushed a commit to pacrob/eth-tester that referenced this pull request Oct 25, 2023
Exclude huge and unnecessary venv*/ from dist
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.

Handle pending transactions with a tx pool
3 participants