-
Notifications
You must be signed in to change notification settings - Fork 232
evm: rewrite eth_estimateGas for panic safety and performance #2424
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
0e63a3a
fix(evm): add panic safety to eth_estimateGas. Fix usage of incorrect…
Unique-Divine a19d1db
fix(evm): use geth efficiency improvements and improved BinSearch wit…
Unique-Divine c0ebe60
fix(evm-e2e): newer assertion is more lenient
Unique-Divine c24babf
fix: add suggestion from Cursor bot
Unique-Divine 8968faa
fix: bug with ineffectual assignment
Unique-Divine 5ef5c85
ci: add new gen-changelog command to automate its maintenance more
Unique-Divine dd6db35
more pr feedback
Unique-Divine f0bf277
Merge branch 'main' into ud/eth-estimate-gas
onikonychev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -264,7 +264,7 @@ func (k *Keeper) EthCall( | |
| } | ||
|
|
||
| ctx := sdk.UnwrapSDKContext(goCtx) | ||
| ctx = ctx.WithValue(SimulationContextKey, true) | ||
| ctx = ctx.WithValue(evm.CtxKeyEvmSimulation, true) | ||
|
|
||
| var args evm.JsonTxArgs | ||
| err := json.Unmarshal(req.Args, &args) | ||
|
|
@@ -295,15 +295,9 @@ func (k *Keeper) EthCall( | |
| } | ||
|
|
||
| // EstimateGas: Implements the gRPC query for "/eth.evm.v1.Query/EstimateGas". | ||
| // EstimateGas implements eth_estimateGas rpc api. | ||
| func (k Keeper) EstimateGas( | ||
| goCtx context.Context, req *evm.EthCallRequest, | ||
| ) (*evm.EstimateGasResponse, error) { | ||
| return k.EstimateGasForEvmCallType(goCtx, req, evm.CallTypeRPC) | ||
| } | ||
|
|
||
| // EstimateGasForEvmCallType estimates the gas cost of a transaction. This can be | ||
| // called with the "eth_estimateGas" JSON-RPC method or smart contract query. | ||
| // This estimates the lowest possible gas limit that allows a transaction to run | ||
| // successfully with the provided context options. This can be called with the | ||
| // "eth_estimateGas" JSON-RPC method. | ||
| // | ||
| // When [EstimateGas] is called from the JSON-RPC client, we need to reset the | ||
| // gas meter before simulating the transaction (tx) to have an accurate gas | ||
|
|
@@ -316,16 +310,16 @@ func (k Keeper) EstimateGas( | |
| // Returns: | ||
| // - A response containing the estimated gas cost. | ||
| // - An error if the gas estimation process encounters any issues. | ||
| func (k Keeper) EstimateGasForEvmCallType( | ||
| goCtx context.Context, req *evm.EthCallRequest, fromType evm.CallType, | ||
| func (k Keeper) EstimateGas( | ||
| goCtx context.Context, req *evm.EthCallRequest, | ||
| ) (*evm.EstimateGasResponse, error) { | ||
| if err := req.Validate(); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| ctx := sdk.UnwrapSDKContext(goCtx) | ||
| ctx = ctx.WithValue(SimulationContextKey, true) | ||
| evmCfg := k.GetEVMConfig(ctx) | ||
| rootCtx := sdk.UnwrapSDKContext(goCtx). | ||
| WithValue(evm.CtxKeyEvmSimulation, true) | ||
| evmCfg := k.GetEVMConfig(rootCtx) | ||
|
|
||
| if req.GasCap < gethparams.TxGas { | ||
| return nil, grpcstatus.Errorf(grpccodes.InvalidArgument, "gas cap cannot be lower than %d", gethparams.TxGas) | ||
|
|
@@ -338,54 +332,74 @@ func (k Keeper) EstimateGasForEvmCallType( | |
| } | ||
|
|
||
| // ApplyMessageWithConfig expect correct nonce set in msg | ||
| nonce := k.GetAccNonce(ctx, args.GetFrom()) | ||
| nonce := k.GetAccNonce(rootCtx, args.GetFrom()) | ||
| args.Nonce = (*hexutil.Uint64)(&nonce) | ||
|
|
||
| // Binary search the gas requirement, as it may be higher than the amount used | ||
| var ( | ||
| lo = gethparams.TxGas - 1 | ||
| hi uint64 | ||
| gasCap uint64 | ||
| // Set smart lower bound based on the gas used in the first execution | ||
| // (base case). | ||
| lo uint64 | ||
| hi uint64 | ||
|
|
||
| // executable runs one probe at a specific gas limit. | ||
| // - Rewrites evmMsg.GasLimit to the probed value. | ||
| // - Constructs a fresh SDB on a context with an infinite gas meter and zero | ||
| // KV/transient KV gas costs, isolating the probe from store-gas panics. | ||
| // - Defers a panic classifier, where SDK/go-ethereum "out of gas" | ||
| // panics result in { vmError=true, err=nil }. Any other panic gets | ||
| // bubbled up through the call stack. | ||
| // - Returns (vmError, resp, err) where vmError signals VM-level failure | ||
| // (incl. OOG/revert), and err signals consensus/unexpected failure. | ||
| executable func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) | ||
| ) | ||
|
|
||
| // Determine the highest gas limit can be used during the estimation. | ||
| // Start with block gas limit | ||
| params := rootCtx.ConsensusParams() | ||
| if params != nil && params.Block != nil && params.Block.MaxGas > 0 { | ||
| hi = uint64(params.Block.MaxGas) | ||
| } else { | ||
| // Fallback to gasCap if block params not available | ||
| hi = req.GasCap | ||
| } | ||
|
|
||
| // Override with user-provided gas limit if it's valid | ||
| if args.Gas != nil && uint64(*args.Gas) >= gethparams.TxGas { | ||
| hi = uint64(*args.Gas) | ||
| } else { | ||
| // Query block gas limit | ||
| params := ctx.ConsensusParams() | ||
| if params != nil && params.Block != nil && params.Block.MaxGas > 0 { | ||
| hi = uint64(params.Block.MaxGas) | ||
| } else { | ||
| hi = req.GasCap | ||
| } | ||
| } | ||
|
|
||
| // TODO: Recap the highest gas limit with account's available balance. | ||
| // Recap the highest gas allowance with specified gascap. | ||
| if req.GasCap != 0 && hi > req.GasCap { | ||
| hi = req.GasCap | ||
| } | ||
|
|
||
| gasCap = hi | ||
|
|
||
| // convert the tx args to an ethereum message | ||
| evmMsg, err := args.ToMessage(req.GasCap, evmCfg.BaseFeeWei) | ||
| if err != nil { | ||
| return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) | ||
| } | ||
|
|
||
| // NOTE: the errors from the executable below should be consistent with | ||
| // go-ethereum, so we don't wrap them with the gRPC status code Create a | ||
| // helper to check if a gas allowance results in an executable transaction. | ||
| executable := func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) { | ||
| // update the message with the new gas value | ||
| evmMsg = core.Message{ | ||
| executable = func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) { | ||
| defer func() { | ||
| // Recover OOG panics as a normal VM failure so the binary search can | ||
| // increase gas. Any non-OOG panic aborts the search with a | ||
| // contextual error for diagnostics. | ||
| oog, perr := evm.RecoverOutOfGasPanic(fmt.Sprintf(`eth_estimateGas { gas: %d }`, gas)) | ||
| if oog { | ||
| vmError, rsp, err = true, nil, nil | ||
| return | ||
| } else if perr != nil { | ||
| err = perr // Unexpected panic -> Abort the search | ||
| return | ||
| } | ||
| }() | ||
Unique-Divine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| evmMsg = core.Message{ // update the message with the new gas value | ||
| To: evmMsg.To, | ||
| From: evmMsg.From, | ||
| Nonce: evmMsg.Nonce, | ||
| Value: evmMsg.Value, | ||
| GasLimit: gas, // <---- This one changed | ||
| GasLimit: gas, // <---- This one changes | ||
| GasPrice: evmMsg.GasPrice, | ||
| GasFeeCap: evmMsg.GasFeeCap, | ||
| GasTipCap: evmMsg.GasTipCap, | ||
|
|
@@ -397,37 +411,35 @@ func (k Keeper) EstimateGasForEvmCallType( | |
| SkipFromEOACheck: evmMsg.SkipFromEOACheck, | ||
| } | ||
|
|
||
| tmpCtx := ctx | ||
| if fromType == evm.CallTypeRPC { | ||
| tmpCtx, _ = ctx.CacheContext() | ||
| // Initialize SDB | ||
| sdb := k.NewSDB( | ||
| rootCtx, | ||
| k.TxConfig(rootCtx, rootCtx.EvmTxHash()), | ||
| ) | ||
| sdb.SetCtx( | ||
| sdb.Ctx(). | ||
| WithGasMeter(eth.NewInfiniteGasMeterWithLimit(evmMsg.GasLimit)). | ||
| WithKVGasConfig(storetypes.GasConfig{}). | ||
| WithTransientKVGasConfig(storetypes.GasConfig{}), | ||
| ) | ||
|
|
||
| acct := k.GetAccount(tmpCtx, evmMsg.From) | ||
| acct := k.GetAccount(sdb.Ctx(), evmMsg.From) | ||
|
|
||
| from := evmMsg.From | ||
| if acct == nil { | ||
| acc := k.accountKeeper.NewAccountWithAddress(tmpCtx, from[:]) | ||
| k.accountKeeper.SetAccount(tmpCtx, acc) | ||
| acct = NewEmptyAccount() | ||
| } | ||
| // When submitting a transaction, the `EthIncrementSenderSequence` ante handler increases the account nonce | ||
| acct.Nonce = nonce + 1 | ||
| err = k.SetAccount(tmpCtx, from, *acct) | ||
| if err != nil { | ||
| return true, nil, err | ||
| } | ||
| // resetting the gasMeter after increasing the sequence to have an accurate gas estimation on EVM extensions transactions | ||
| gasMeter := eth.NewInfiniteGasMeterWithLimit(evmMsg.GasLimit) | ||
| tmpCtx = tmpCtx.WithGasMeter(gasMeter). | ||
| WithKVGasConfig(storetypes.GasConfig{}). | ||
| WithTransientKVGasConfig(storetypes.GasConfig{}) | ||
| from := evmMsg.From | ||
| if acct == nil { | ||
| acc := k.accountKeeper.NewAccountWithAddress(sdb.Ctx(), from[:]) | ||
| k.accountKeeper.SetAccount(sdb.Ctx(), acc) | ||
| acct = NewEmptyAccount() | ||
| } | ||
| // When submitting a transaction, the `EthIncrementSenderSequence` ante handler increases the account nonce | ||
| acct.Nonce = nonce + 1 | ||
| err = k.SetAccount(sdb.Ctx(), from, *acct) | ||
| if err != nil { | ||
| return true, nil, err | ||
Unique-Divine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // pass false to not commit StateDB | ||
| sdb := NewSDB( | ||
| ctx, | ||
| &k, | ||
| NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())), | ||
| ) | ||
| evmObj := k.NewEVM(tmpCtx, evmMsg, evmCfg, nil /*tracer*/, sdb) | ||
| evmObj := k.NewEVM(sdb.Ctx(), evmMsg, evmCfg, nil /*tracer*/, sdb) | ||
| rsp, err = k.ApplyEvmMsg(evmMsg, evmObj, false /*commit*/) | ||
| if err != nil { | ||
| if strings.Contains(err.Error(), core.ErrIntrinsicGas.Error()) { | ||
|
|
@@ -438,31 +450,39 @@ func (k Keeper) EstimateGasForEvmCallType( | |
| return len(rsp.VmError) > 0, rsp, nil | ||
| } | ||
|
|
||
| // Execute the binary search and hone in on an executable gas limit | ||
| hi, err = evm.BinSearch(lo, hi, executable) | ||
| // BASE CASE: Jumping straight into binary search is extermely inefficient. | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Instead, execute at the highest allowable gas limit first to validate and | ||
| // set a smarter lower bound. | ||
| failed, result, err := executable(hi) | ||
| if err != nil { | ||
| return nil, err | ||
| return nil, fmt.Errorf("eth call exec error: %w", err) | ||
| } | ||
|
|
||
| // The gas limit is now the highest gas limit that results in an executable transaction | ||
| // Reject the transaction as invalid if it still fails at the highest allowance | ||
| if hi == gasCap { | ||
| failed, result, err := executable(hi) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("eth call exec error: %w", err) | ||
| } | ||
|
|
||
| if failed && result != nil { | ||
| // If the base case fails for non-gas reasons, return the error immediately | ||
| if failed { | ||
| if result != nil && result.VmError != "" && result.VmError != vm.ErrOutOfGas.Error() { | ||
| if result.VmError == vm.ErrExecutionReverted.Error() { | ||
| return nil, fmt.Errorf("estimate gas VMError: %w", evm.NewRevertError(result.Ret)) | ||
| } | ||
|
|
||
| if result.VmError == vm.ErrOutOfGas.Error() { | ||
| return nil, fmt.Errorf("gas required gas limit (%d)", gasCap) | ||
| } | ||
|
|
||
| return nil, fmt.Errorf("estimate gas VMError: %s", result.VmError) | ||
| } | ||
| return nil, fmt.Errorf("gas required exceeds allowance (%d)", hi) | ||
| } | ||
|
|
||
| // Set smart lower bound based on actual gas used | ||
| if result.GasUsed > 0 { | ||
| lo = result.GasUsed - 1 | ||
| } else { | ||
| lo = 0 | ||
| } | ||
|
Comment on lines
+488
to
+492
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is great, it could transform binary search into a couple of tries only
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it's likely the first run will get the value neeeded |
||
|
|
||
| // Execute the binary search and hone in on an executable gas limit | ||
| estimateTolerance := evm.GasEstimateErrorRatioTolerance | ||
| if rootCtx.Value(evm.CtxKeyGasEstimateZeroTolerance) == true { | ||
| estimateTolerance = 0.00 | ||
| } | ||
| hi, err = evm.BinSearch(lo, hi, executable, estimateTolerance) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return &evm.EstimateGasResponse{Gas: hi}, nil | ||
|
|
@@ -483,7 +503,7 @@ func (k Keeper) TraceTx( | |
| contextHeight := max(req.BlockNumber, 1) | ||
|
|
||
| ctx := sdk.UnwrapSDKContext(goCtx) | ||
| ctx = ctx.WithValue(SimulationContextKey, true) | ||
| ctx = ctx.WithValue(evm.CtxKeyEvmSimulation, true) | ||
| ctx = ctx.WithBlockHeight(contextHeight) | ||
| ctx = ctx.WithBlockTime(req.BlockTime) | ||
| ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) | ||
|
|
@@ -580,7 +600,7 @@ func (k Keeper) TraceCall( | |
| contextHeight := max(req.BlockNumber, 1) | ||
|
|
||
| ctx := sdk.UnwrapSDKContext(goCtx) | ||
| ctx = ctx.WithValue(SimulationContextKey, true) | ||
| ctx = ctx.WithValue(evm.CtxKeyEvmSimulation, true) | ||
| ctx = ctx.WithBlockHeight(contextHeight) | ||
| ctx = ctx.WithBlockTime(req.BlockTime) | ||
| ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) | ||
|
|
@@ -670,7 +690,7 @@ func (k Keeper) TraceBlock( | |
| WithConsensusParams(&cmtproto.ConsensusParams{ | ||
| Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, | ||
| }) | ||
| ctx = ctx.WithValue(SimulationContextKey, true) | ||
| ctx = ctx.WithValue(evm.CtxKeyEvmSimulation, true) | ||
|
|
||
| evmCfg := k.GetEVMConfig(ctx) | ||
|
|
||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 logic to determine the upper bound
hiis functionally correct, but its structure could be clearer. It currently initializeshifrom the block/request gas cap, then potentially overrides it withargs.Gas, and finally caps it again withreq.GasCap.A more direct structure that first checks for
args.Gasand then falls back to other limits would improve readability and maintainability, aligning more closely with the previous implementation and standard practice.