-
Notifications
You must be signed in to change notification settings - Fork 146
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
Conversation
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) |
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.
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.
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.
It does now
a54e162
to
33fa647
Compare
…es from same account
eth_tester/main.py
Outdated
# 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'])] |
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.
and chainId equals?
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.
Probably, but way less important than the others.
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.
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: |
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.
Not very performant. If that is a concern, can make _pending_transaction
a dict with key=hash value=transaction
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.
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.
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.
Not a concern, at least for now. If someone wants to test a really high pending count, we can revisit.
9d5b036
to
27079c1
Compare
If this is ok, docs for |
eth_tester/main.py
Outdated
# 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'])] |
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.
Probably, but way less important than the others.
eth_tester/main.py
Outdated
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 |
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.
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: |
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.
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.
eth_tester/main.py
Outdated
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) |
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.
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.
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.
Good point, it also makes the flow in get_transaction_by_hash
easier to read
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.
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.
eth_tester/main.py
Outdated
self._pending_transactions = remove_matching_transaction_from_list( | ||
self._pending_transactions, pending_transaction) | ||
self._pending_transactions.append(pending_transaction) | ||
self.revert_to_snapshot(snapshot) |
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.
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.
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.
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
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.
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)
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.
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? |
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.
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
.
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.
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.
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.
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)) |
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.
Can we be strict here and reject transactions that have invalid transaction params?
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.
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.
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.
These transactions are added to the list from
get_transaction_by_hash
Fair enough, I'm okay skipping this validation.
eth_tester/main.py
Outdated
if not self.auto_mine_transactions: | ||
snapshot = self.take_snapshot() | ||
|
||
transaction_hash = func(self, *args, **kwargs) |
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.
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.
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.
Ah yes that definitely is easier to read 😄
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.
@monokh if this is ready, it looks good to me.
@monokh lemme know if your ready for me to merge/release this. |
@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 |
Exclude huge and unnecessary venv*/ from dist
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
andsend_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
Fixes #59