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

Gas limit estimation feature #14041

Conversation

amit-momin
Copy link
Contributor

@amit-momin amit-momin commented Aug 5, 2024

BCI-3658

  • Added gas limit estimation feature to the EVM gas estimators
  • Added EstimateGasLimit config to toggle the new feature

@amit-momin amit-momin marked this pull request as ready for review August 8, 2024 22:31
@amit-momin amit-momin requested review from a team as code owners August 8, 2024 22:31
fee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...)
if err != nil {
return
if e.geCfg.EstimateGasLimit() {
Copy link
Contributor

Choose a reason for hiding this comment

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

For readability, could you create a new function, called getEstimatedGasUsed()?
And the ethereum.CallMsg, .geCfg.EstimateGasLimit() checks, client calls, please move all to that function.
This makes it very clear to reader what part is for the GetFee(), vs for getEstimatedGasUsed().

@@ -261,32 +264,53 @@ func (e *evmFeeEstimator) L1Oracle() rollups.L1Oracle {
return e.EvmEstimator.L1Oracle()
}

func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) {
func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, toAddress *common.Address, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

After your changes, the return value from this function, param "chainSpecificFeeLimit", doesn't represent the absolute gas limit that caller provided. Instead, it represents what we estimate this specific attempt would consume, provided these fee params are used.

So better to rename the param to suggestedGasLimit. And in comment for this function, explain what this is.

Also, could you check the callers of this functions, whether they use this return value correctly, and next time when they gas bump, they pass in the input param "feeLimit" as original limit sent by caller, not the feeLimit used in previous attempt before a gas bump.

// Do not return error if estimating gas failed, we can still use the provided limit instead since it is an upper limit
e.lggr.Errorw("failed to estimate gas limit. falling back to provided gas limit.", "toAddress", toAddress, "data", calldata, "gasLimit", chainSpecificFeeLimit, "error", estimateErr)
} else {
chainSpecificFeeLimit = estimatedGasLimit
Copy link
Contributor

Choose a reason for hiding this comment

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

This estimatedGasLimit is the estimated Gas returned to us by the chain.
Due to fluctuations or errors, the actual gas used may be slightly more than this, causing the actual Tx to fail.
I think we should add a buffer on top of what the RPC returns to us.
Make that buffer a config value, EstimatedGasBuffer, set to default of 25%.
This protects us from fluctuations.

Also, you need to ensure that feeLimit > estimatedGasLimit. If not, then we can error out. Basically the chain is saying the gas used will be more than what the callers allows us to use, thus making this Tx infeasible.

Lastly, ensure that SuggestedGasLimit = MIN(feeLimit, estimatedGasLimit+Buffer)

Copy link
Contributor Author

@amit-momin amit-momin Aug 13, 2024

Choose a reason for hiding this comment

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

I wanted to avoid adding another config so I was using the existing LimitMultiplier as the buffer. Though, I could see the distinction between the 2 causing confusion because LimitMultiplier was expected to be added on top of the provided limit but estimated gas * EstimatedGasBuffer would be expected to stay under the provided limit

@@ -157,6 +157,7 @@ type MockGasEstimatorConfig struct {
FeeCapDefaultF *assets.Wei
LimitMaxF uint64
ModeF string
EstimateGasLimitF bool
Copy link
Contributor

Choose a reason for hiding this comment

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

curious what's the F for ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Think we do this because the linter doesn't like it when the field has the same name as the method to access it. Not sure why "F" but just following the pattern here

@@ -186,7 +186,7 @@ func (w *chainWriter) GetFeeComponents(ctx context.Context) (*commontypes.ChainF
return nil, fmt.Errorf("gas estimator not available")
}

fee, _, err := w.ge.GetFee(ctx, nil, 0, w.maxGasPrice)
fee, _, err := w.ge.GetFee(ctx, nil, 0, w.maxGasPrice, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

If e.geCfg.EstimateGasLimit() is enabled, will we get/log error in this case with empty callMsg?

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 don't think we would because all of the parameters for the eth_estimateGas call are actually optional. We'd probably get some default value if EstimateGasLimit is enabled but it won't be used in cases like this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Joe does have a point though that if CW wants to access gas limits they would have to update their API and provide a to address. Something to keep in mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, they would. If we move forward with exposing EstimateGas to products, we had discussed we'd do it through this. But they'd have to update the other parameters too.

@@ -33,11 +33,11 @@ type EvmFeeEstimator interface {

// L1Oracle returns the L1 gas price oracle only if the chain has one, e.g. OP stack L2s and Arbitrum.
L1Oracle() rollups.L1Oracle
GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error)
GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, toAddress *common.Address, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps using common.Address instead of a pointer simplifies the API?

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 kept this as a pointer so GetFee callers that don't care about the gas limit can pass a nil like in the case of the ChainWriter.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense, but we also need to add a check here to make sure the address is properly set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The To field in ethereum.CallMsg is actually *common.Address so we can pass this directly in whether it is set or not.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, but I'm guessing if we do and it's empty it's going to use the 0x0 which might not be the intended behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If it's a nil address I think geth handles it properly as an estimation for contract creation. If someone passes a non-nil empty common.Address, it's still a valid address for estimation.

if err != nil {
return
if e.geCfg.EstimateGasLimit() {
callMsg.Gas = chainSpecificFeeLimit
Copy link
Collaborator

@dimriou dimriou Aug 13, 2024

Choose a reason for hiding this comment

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

  1. We don't have to set a gas limit to get an estimation. This can be omitted.
  2. I'm interested in knowing what is the exact requirement from the product teams regarding gas limit estimation. Personally, I would remove any requirements for GasLimit limits if EstimateGasLimit is enabled. Usually, when we have fallback values people tend to set them wrong because they're not aware what is the impact. I've observed the same pattern with the gas estimators where we have default values that are only used once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. I was using it as a way to cap the estimation to the limit products sent us but I'll remove this and add that restriction in code for better flexibility
  2. At least for the initial solution that's needed for Mantle, I'm trying to honor the gas limits products provide as a cap to avoid any changes in behavior/assumptions. Functions has mentioned that they would need us to honor the cap otherwise it could lead to failures at the contract level. We can discuss more in the thread though.

Data: calldata,
}
estimatedGas, estimateErr := e.ethClient.EstimateGas(ctx, callMsg)
if estimateErr != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the future, we could perhaps add a retry.

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 we technically already do. This would surface as a gas estimator error which are retryable here

dimriou
dimriou previously approved these changes Aug 21, 2024
huangzhen1997
huangzhen1997 previously approved these changes Aug 21, 2024
@amit-momin amit-momin dismissed stale reviews from huangzhen1997 and dimriou via 9e896c0 August 21, 2024 21:25
huangzhen1997
huangzhen1997 previously approved these changes Aug 21, 2024
Copy link
Contributor

@huangzhen1997 huangzhen1997 left a comment

Choose a reason for hiding this comment

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

lgtm, one small nit

e.lggr.Errorw("failed to estimate gas limit. falling back to provided gas limit.", "callMsg", callMsg, "providedGasLimit", providedGasLimit, "error", estimateErr)
return providedGasLimit, nil
}
return estimatedFeeLimit, fmt.Errorf("gas estimation failed and provided gas limit is 0: %w", estimateErr)
Copy link
Contributor

@huangzhen1997 huangzhen1997 Aug 21, 2024

Choose a reason for hiding this comment

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

nit:
for providedGasLimit , do we want to mention this provided gas limit is calculated from provided fee and multiplier ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Think it's worth calling out to be explicit. I've made the change.

// If provided gas limit is not 0, fallback to it if the buffer causes the estimated gas limit to exceed it
// The provided gas limit should be used as an upper bound to avoid unexpected behavior for products
if providedGasLimit > 0 && estimatedFeeLimit > providedGasLimit {
e.lggr.Debugw("estimated gas limit with buffer exceeds provided limit. falling back to the provided gas limit", "estimatedGasLimit", estimatedFeeLimit, "providedGasLimit", providedGasLimit)
Copy link
Contributor

Choose a reason for hiding this comment

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

noticed one more here for the multiplier to make it consistent with line 369

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 catch! I just updated all of the logs. Also updated the log to be written more like spoken word than a formula.

@prashantkumar1982 prashantkumar1982 added this pull request to the merge queue Aug 22, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 22, 2024
@prashantkumar1982 prashantkumar1982 added this pull request to the merge queue Aug 22, 2024
Merged via the queue into develop with commit 8d818ea Aug 22, 2024
150 of 151 checks passed
@prashantkumar1982 prashantkumar1982 deleted the BCI-3658-Implement-the-dynamic-gas-limit-estimation-feature branch August 22, 2024 20: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.

4 participants