-
Notifications
You must be signed in to change notification settings - Fork 20.1k
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
internal/ethapi: optimize & clean up EstimateGas #27710
Conversation
c17c73c
to
9b8e908
Compare
// If the error is not nil(consensus error), it means the provided message | ||
// call or transaction will never be accepted no matter how much gas it is | ||
// assigned. Return the error directly, don't struggle any more. | ||
if mid > lo*2 { |
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.
There is likely room for further average-case improvement by tweaking (lowering) the constant multiplier here, but this would worsen the worst-case and also introduces new boundary conditions that would need to be checked.
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 is more of an acceptance that there are a much larger number of transactions on the low size, 21k - 500k gas?
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.
bit of both I guess: most txs don't need much higher gas limit than their gas used, and most txs don't require near the full block limit of gas. I guess this trick takes advantage of both facts. (FWIW EstimateGas doesn't seem to be called for simple 21k gas transfers by wallets like MetaMask so I excluded those from the analysis)
// probably don't want to return a lowest possible gas limit for these cases anyway. | ||
lo = gasUsed - 1 | ||
|
||
// Binary search for the smallest gas limit that allows the tx to execute successfully. |
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 really so much binary search if you adjust the mid point yeah?
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's a pure binary search after 1-2 doublings of 'lo' in the typical case, lg(n) in the worst. Wasn't sure it's worth clarifying that detail, but I certainly could if it helps.
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 it's still basically a binary search, it divides up the range of possible outcomes every iteration, and discards one of the two subranges. The trick here is that the point where the range is divided is not on on the arithmetic middle.
Some clarifying might be good, e.g. what you wrote below, something like
most txs don't need much higher gas limit than their gas used, and most txs don't require near the full block limit of gas, so the selection of where to bisect the range is skewed to favour the low side.
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.
added, though at the line where the midpoint is adjusted rather than here.
// If the error is not nil(consensus error), it means the provided message | ||
// call or transaction will never be accepted no matter how much gas it is | ||
// assigned. Return the error directly, don't struggle any more. | ||
if mid > lo*2 { |
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 is more of an acceptance that there are a much larger number of transactions on the low size, 21k - 500k gas?
84b4579
to
b920b2b
Compare
Whilst I understand that this code might make things a bit faster, but they also make it a lot more complicated. Do we have a specific use case where we need that extra drop of speed? |
Another interesting optimization we could consider is whether it's worth it to hone in on the exact gas usage needed, or if something "close enough" is enough. I.e. By binary searching until Lowering to accuracy from 1 gas to 1024 gas for example would save 10 iterations of the binary search. |
My main goal was to improve the handling around handling tx revert, which contributed to a recent issue I encountered that ending up fixed here. Bulk of the change to this end is around moving the unconstrained execution up front instead leaving it to the end, which I felt makes the impl more understandable. The additional complexity I did end up adding is ~4 LOC (L1228 initialization of lo, and the the midpoint adjustment in the main loop) -- seemed worth the savings given this is a very expensive call. Figured RPC providers that make use of this would benefit. |
Agree that the "lowest possible" aspect of this function is probably unnecessary. |
internal/ethapi/api.go
Outdated
|
||
// ErrorCode returns the JSON error code for a revertal. | ||
// See: https://github.com/ethereum/wiki/wiki/JSON-RPC-Error-Codes-Improvement-Proposal | ||
func (e *revertError) ErrorCode() int { |
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.
The ErrorCode
and ErrorData
methods are used by the RPC server and will be returned as part of the JSON-RPC response. So they need to be kept here.
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.
ahah, restored.
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.
ahah, restored.
Is it? I don't see it??
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.
weird, I wonder if I undid it when I merged with upstream. let me try again.
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 I got it this time :/
It'd be nice to have the script you used to sample the transactions. That way we could also repeat your analysis. |
54f14c3
to
aac3cdf
Compare
there is no standalone script unfortunately. the analysis was over logs from a hacked client which invoked both old & new implementations for each call and logged the # of iterations of both. I then added a hook in the txpool to call EstimateGas for each sampled transaction, and let it run over night before extracting the log data and dumping into a spreadsheet. |
aac3cdf
to
0230a4a
Compare
There's a reasonably good test in besu that covers some funny edge cases with the 1/64th rule TestDepth.sol
|
0230a4a
to
80fd388
Compare
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.
LGTM, some minor nits, and I wonder if @fjl's comments were really addressed?
internal/ethapi/api.go
Outdated
err = overrides.Apply(state) | ||
if err != nil { |
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.
err = overrides.Apply(state) | |
if err != nil { | |
if err := overrides.Apply(state); err != nil { |
I nkow you just lifted it up, but still...
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.
done
// probably don't want to return a lowest possible gas limit for these cases anyway. | ||
lo = gasUsed - 1 | ||
|
||
// Binary search for the smallest gas limit that allows the tx to execute successfully. |
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 it's still basically a binary search, it divides up the range of possible outcomes every iteration, and discards one of the two subranges. The trick here is that the point where the range is divided is not on on the arithmetic middle.
Some clarifying might be good, e.g. what you wrote below, something like
most txs don't need much higher gas limit than their gas used, and most txs don't require near the full block limit of gas, so the selection of where to bisect the range is skewed to favour the low side.
80fd388
to
38c489d
Compare
38c489d
to
2e3f647
Compare
My concerns were adressed. I can't really comment on the binary search thing, seems alright though. |
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.
LGTM
Optimizations: - Previously, if a transaction was reverting, EstimateGas would exhibit worst-case behavior and binary search up to the max gas limit (~40 state-clone + tx executions). This change allows EstimateGas to return after only a single unconstrained execution in this scenario. - Uses the gas used from the unconstrained execution to bias the remaining binary search towards the likely solution in a simple way that doesn't impact the worst case. For a typical contract-invoking transaction, this reduces the median number of state-clone+executions from 25 to 18 (28% reduction). Cleanup: - added & improved function + code comments - correct the EstimateGas documentation to clarify the gas limit determination is at latest block, not pending, if the blockNr is unspecified.
)" This reverts commit 8be8e98.
)" This reverts commit 8be8e98.
Conflicts: internal/ethapi/api.go: We modified call to DoCall, upstream moved it to an internal function, kept our modifications in the new place.
Optimizations:
Cleanup:
The performance analysis was conducted over 2300 contract-invoking transactions randomly sampled (via tx hash starting with "00") from the mempool over the past 24 hours. These txs were put through the new implementation & old implementation with the exact same state when the tx hit the mempool, and the # of executions for each recorded. The output from both calls was also compared to ensure they were equivalent. There were a few (<< 1%) instances where the values differed, upon further investigation these were due to transactions that monitor gas-remaining and attempt to adjust computation accordingly (such as batch NFT minting, example).