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

Improve gas estimations #5968

Closed
wants to merge 11 commits into from
Closed

Improve gas estimations #5968

wants to merge 11 commits into from

Conversation

emlautarom1
Copy link
Contributor

Fixes #5706

Changes

  • Change Gas estimation algorithm for more consistent results

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

Some tests were updated to reflect the behavior changes.

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

Remarks

The code for Gas estimation is still very complex compared to the competition (see go-ethereum implementation: https://github.com/ethereum/go-ethereum/blob/2274a03e339f213361453590b54917bbfd0a0c31/internal/ethapi/api.go#L1137). A refactor would be highly suggested.

- Cap Tx 'GasLimit'
- Set block gas limit
- Removed 'NotSupported' exceptions, unused fields, redundant casts
@emlautarom1 emlautarom1 self-assigned this Jul 26, 2023
@emlautarom1
Copy link
Contributor Author

emlautarom1 commented Jul 26, 2023

About gas estimation:

When a user triggers a eth_estimateGas, the payload can include an optional gas field to limit the amount of gas to be used in the tx, that is, the estimation will be at most gas. If no gas value is provided, an arbitrary default of 100_000_000 is used at the moment by Nethermind.

Here is (or was) the buggy code: If the gas value is higher than what block gas limit where the tx will be executed, an alternative gas estimation is used. This alternative estimation is not clear to me, nor are the reasons for it to exist. Since the default value is 100_000_000, on mainnet which has a block gas limit of 30_000_000, this alternative estimation is used. In practice this method is the most used given that users rarely include a gas field when asking for estimations.

This PR limits the tx gas value to be at most the block gas limit, and then uses the "default" gas estimation method, which involves a binary search to find the required amount of gas until no OutOfGas exception is triggered.

@emlautarom1
Copy link
Contributor Author

A different problem regarding gas estimations comes when a contract performs arbitrary checks on the remaining gas, like in #5637. Here, the contract used makes the following check:

require(gasleft() >= safeTxGas, "Not enough gas to execute safe transaction");

If the remaining gas doesn't satisfy the condition, execution halts and all changes get reverted, but importantly, this does not fail with an OutOfGas exception. Remember that Nethermind tries to find the minimum amount of gas required for the transaction to not fail with an OutOfGas exception, not the minimum amount of gas for the transaction to be completed successfully.

Here, there are as far as I can tell two options:

  • Change the estimation method to find the minimum amount of gas required for the tx to go through.
  • Change the contract to fail with an OutOfGas exception, somehow.

return Math.Max(intrinsicGas, gasTracer.GasSpent + gasTracer.CalculateAdditionalGasRequired(tx, releaseSpec));
}

tx.GasLimit = Math.Min(tx.GasLimit, header.GasLimit); // Limit Gas to the header
Copy link
Member

Choose a reason for hiding this comment

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

What geth does here? Does it use they 100mln gas limit from transaction or 30mln gas limit for block?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Geth uses the gas value provided by the user as long as it's at least the minimum amount of gas required by any Tx (21_000), or the block gas limit by default (so in this case, 30_000_000).

Comment on lines 39 to 43
if (tx.GasLimit > header.GasLimit)
{
return Math.Max(intrinsicGas, gasTracer.GasSpent + gasTracer.CalculateAdditionalGasRequired(tx, releaseSpec));
}

Copy link
Member

Choose a reason for hiding this comment

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

This wasn't bad edge case fallback if we didn't have that gas limit discrepancy, maybe worth to keep it?

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 where would we use this fallback? The problem that I had with this piece of code is that I cannot fully grasp how is this actually computing the estimated gas. I'd only keep the binary search approach with appropriate defaults and timeouts through cancellation tokens.

@emlautarom1
Copy link
Contributor Author

Added a CancellationToken to stop estimation if requested

@emlautarom1 emlautarom1 marked this pull request as ready for review July 27, 2023 20:54
# Conflicts:
#	src/Nethermind/Nethermind.Evm/Tracing/GasEstimator.cs
- Update test suite with higher estimations (mostly +1)
- Removes ugly hack that dealt with class mutation
@@ -458,7 +458,7 @@ public void Can_estimate_with_destroy_refund_and_below_intrinsic_pre_berlin()
tracer.CalculateAdditionalGasRequired(tx, releaseSpec).Should().Be(24080);
tracer.GasSpent.Should().Be(35228L);
long estimate = estimator.Estimate(tx, block.Header, tracer);
estimate.Should().Be(59307);
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 cannot fully figure out why this estimations are higher than what we had before.

- Value can be < 0, so we ignore it if that's the case
{
return gasTracer.CalculateAdditionalGasRequired(tx, releaseSpec);
return BinarySearchEstimate(leftBound, rightBound, tx, header, cancellationToken)
+ Math.Max(0, gasTracer.CalculateAdditionalGasRequired(tx, releaseSpec));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a chance (looks more like a bug) for the AdditionalGasRequired to be < 0, so we ignore it in those cases.

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be handled inside the tracer?

Copy link
Member

Choose a reason for hiding this comment

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

Why this is even added? I don't see this in the original code, if we estimate with actual running this shouldn't be needed and probably makes the tests behave differently in the first place?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why this is even added?

I had to add this after running the whole test suite and seeing that there were failures that weren't flaky tests: there are integrations tests that verify the gas estimation (which have been updated on this PR) that fail if we don't include this AdditionalGasRequired.

Comment on lines +95 to +100
long originalGasLimit = transaction.GasLimit;

transaction.GasLimit = gasLimit;
_transactionProcessor.CallAndRestore(transaction, block, tracer);
_transactionProcessor.CallAndRestore(transaction, block, tracer.WithCancellation(cancellationToken));

transaction.GasLimit = originalGasLimit;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We want to preserve the original Gas limit so we are forced to do this workaround. Ideally, Transaction would implement ICloneable, so we could make a copy with the desired amount of Gas instead of mutating the original one.

{
return gasTracer.CalculateAdditionalGasRequired(tx, releaseSpec);
return BinarySearchEstimate(leftBound, rightBound, tx, header, cancellationToken)
+ Math.Max(0, gasTracer.CalculateAdditionalGasRequired(tx, releaseSpec));
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be handled inside the tracer?

{
return gasTracer.CalculateAdditionalGasRequired(tx, releaseSpec);
return BinarySearchEstimate(leftBound, rightBound, tx, header, cancellationToken)
+ Math.Max(0, gasTracer.CalculateAdditionalGasRequired(tx, releaseSpec));
Copy link
Member

Choose a reason for hiding this comment

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

Why this is even added? I don't see this in the original code, if we estimate with actual running this shouldn't be needed and probably makes the tests behave differently in the first place?

@emlautarom1 emlautarom1 mentioned this pull request Jul 31, 2023
16 tasks
@emlautarom1
Copy link
Contributor Author

emlautarom1 commented Jul 31, 2023

Superseded by #5973. Reason for this decision is documented there.

@emlautarom1 emlautarom1 deleted the fix/gas_estimation branch July 31, 2023 13:44
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.

eth_estimateGas occasionally returns invalid gas limit and causing tx to run out of gas
3 participants