Skip to content

Commit 2a0eb3a

Browse files
vladjdkaljo242Eric-Warehime
authored
perf: optimize gas estimation (#538)
* Fix error semantics * changelog * Add benchmark (#542) Co-authored-by: Alex | Interchain Labs <[email protected]> --------- Co-authored-by: Alex | Interchain Labs <[email protected]> Co-authored-by: Eric Warehime <[email protected]>
1 parent af53a0f commit 2a0eb3a

File tree

7 files changed

+338
-135
lines changed

7 files changed

+338
-135
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
### IMPROVEMENTS
1616

17+
- [\#538](https://github.com/cosmos/evm/pull/538) Optimize `eth_estimateGas` gRPC path: short-circuit plain transfers, add optimistic gas bound based on `MaxUsedGas`.
1718
- [\#513](https://github.com/cosmos/evm/pull/513) Replace `TestEncodingConfig` with production `EncodingConfig` in encoding package to remove test dependencies from production code.
1819
- [\#467](https://github.com/cosmos/evm/pull/467) Replace GlobalEVMMempool by passing to JSONRPC on initiate.
1920
- [\#352](https://github.com/cosmos/evm/pull/352) Remove the creation of a Geth EVM instance, stateDB during the AnteHandler balance check.

api/cosmos/evm/vm/v1/tx.pulsar.go

Lines changed: 128 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

evmd/tests/integration/x_vm_test.go

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,87 @@
11
package integration
22

33
import (
4+
"encoding/json"
45
"testing"
56

6-
"github.com/stretchr/testify/suite"
7-
7+
"github.com/cosmos/evm/server/config"
88
"github.com/cosmos/evm/tests/integration/x/vm"
9+
"github.com/cosmos/evm/testutil/integration/evm/network"
10+
"github.com/cosmos/evm/testutil/keyring"
11+
feemarkettypes "github.com/cosmos/evm/x/feemarket/types"
12+
"github.com/cosmos/evm/x/vm/types"
13+
"github.com/ethereum/go-ethereum/common"
14+
15+
"github.com/stretchr/testify/require"
16+
"github.com/stretchr/testify/suite"
917
)
1018

19+
func BenchmarkGasEstimation(b *testing.B) {
20+
// Setup benchmark test environment
21+
keys := keyring.New(2)
22+
// Set custom balance based on test params
23+
customGenesis := network.CustomGenesisState{}
24+
feemarketGenesis := feemarkettypes.DefaultGenesisState()
25+
feemarketGenesis.Params.NoBaseFee = true
26+
customGenesis[feemarkettypes.ModuleName] = feemarketGenesis
27+
opts := []network.ConfigOption{
28+
network.WithPreFundedAccounts(keys.GetAllAccAddrs()...),
29+
network.WithCustomGenesis(customGenesis),
30+
}
31+
nw := network.NewUnitTestNetwork(CreateEvmd, opts...)
32+
// gh := grpc.NewIntegrationHandler(nw)
33+
// tf := factory.New(nw, gh)
34+
35+
chainConfig := types.DefaultChainConfig(nw.GetEIP155ChainID().Uint64())
36+
// get the denom and decimals set on chain initialization
37+
// because we'll need to set them again when resetting the chain config
38+
denom := types.GetEVMCoinDenom()
39+
extendedDenom := types.GetEVMCoinExtendedDenom()
40+
displayDenom := types.GetEVMCoinDisplayDenom()
41+
decimals := types.GetEVMCoinDecimals()
42+
43+
configurator := types.NewEVMConfigurator()
44+
configurator.ResetTestConfig()
45+
err := configurator.
46+
WithChainConfig(chainConfig).
47+
WithEVMCoinInfo(types.EvmCoinInfo{
48+
Denom: denom,
49+
ExtendedDenom: extendedDenom,
50+
DisplayDenom: displayDenom,
51+
Decimals: decimals,
52+
}).
53+
Configure()
54+
require.NoError(b, err)
55+
56+
// Use simple transaction args for consistent benchmarking
57+
args := types.TransactionArgs{
58+
To: &common.Address{},
59+
}
60+
61+
marshalArgs, err := json.Marshal(args)
62+
require.NoError(b, err)
63+
64+
req := types.EthCallRequest{
65+
Args: marshalArgs,
66+
GasCap: config.DefaultGasCap,
67+
ProposerAddress: nw.GetContext().BlockHeader().ProposerAddress,
68+
}
69+
70+
// Reset timer to exclude setup time
71+
b.ResetTimer()
72+
73+
// Run the benchmark
74+
for i := 0; i < b.N; i++ {
75+
_, err := nw.GetEvmClient().EstimateGas(
76+
nw.GetContext(),
77+
&req,
78+
)
79+
if err != nil {
80+
b.Fatal(err)
81+
}
82+
}
83+
}
84+
1185
func TestKeeperTestSuite(t *testing.T) {
1286
s := vm.NewKeeperTestSuite(CreateEvmd)
1387
s.EnableFeemarket = false

proto/cosmos/evm/vm/v1/tx.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ message MsgEthereumTxResponse {
7171
string vm_error = 4;
7272
// gas_used specifies how much gas was consumed by the transaction
7373
uint64 gas_used = 5;
74+
// max_used_gas specifies the gas consumed by the transaction, not including refunds
75+
uint64 max_used_gas = 6;
7476
}
7577

7678
// MsgUpdateParams defines a Msg for updating the x/vm module parameters.

x/vm/keeper/grpc_query.go

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -404,20 +404,32 @@ func (k Keeper) EstimateGasInternal(c context.Context, req *types.EthCallRequest
404404
return len(rsp.VmError) > 0, rsp, nil
405405
}
406406

407-
// Execute the binary search and hone in on an executable gas limit
408-
hi, err = types.BinSearch(lo, hi, executable)
407+
// Adapted from go-ethereum gas estimator for early short-circuit and optimistic bounds:
408+
// https://github.com/ethereum/go-ethereum/blob/v1.16.2/eth/gasestimator/gasestimator.go
409+
410+
// If the transaction is a plain value transfer, short circuit estimation and
411+
// directly try 21000. Returning 21000 without any execution is dangerous as
412+
// some tx field combos might bump the price up even for plain transfers (e.g.
413+
// unused access list items). Ever so slightly wasteful, but safer overall.
414+
if len(msg.Data) == 0 && msg.To != nil {
415+
acct := k.GetAccountWithoutBalance(ctx, *msg.To)
416+
if acct == nil || !acct.IsContract() {
417+
failed, _, err := executable(ethparams.TxGas)
418+
if err == nil && !failed {
419+
return &types.EstimateGasResponse{Gas: ethparams.TxGas}, nil
420+
}
421+
}
422+
}
423+
424+
// We first execute the transaction at the highest allowable gas limit, since if this fails we
425+
// can return error immediately.
426+
failed, result, err := executable(hi)
409427
if err != nil {
410428
return nil, err
411429
}
412-
413-
// Reject the transaction as invalid if it still fails at the highest allowance
414-
if hi == gasCap {
415-
failed, result, err := executable(hi)
416-
if err != nil {
417-
return nil, err
418-
}
419-
420-
if failed {
430+
if failed {
431+
// Preserve Cosmos error semantics when the cap is reached
432+
if hi == gasCap {
421433
if result != nil && result.VmError != vm.ErrOutOfGas.Error() {
422434
if result.VmError == vm.ErrExecutionReverted.Error() {
423435
return &types.EstimateGasResponse{
@@ -427,10 +439,34 @@ func (k Keeper) EstimateGasInternal(c context.Context, req *types.EthCallRequest
427439
}
428440
return nil, errors.New(result.VmError)
429441
}
430-
// Otherwise, the specified gas cap is too low
431442
return nil, fmt.Errorf("gas required exceeds allowance (%d)", gasCap)
432443
}
444+
// If no larger allowance is available, fail fast
445+
return nil, fmt.Errorf("gas required exceeds allowance (%d)", hi)
433446
}
447+
448+
// There's a fairly high chance for the transaction to execute successfully
449+
// with gasLimit set to the first execution's usedGas + gasRefund. Explicitly
450+
// check that gas amount and use as a limit for the binary search.
451+
optimisticGasLimit := (result.MaxUsedGas + ethparams.CallStipend) * 64 / 63
452+
if optimisticGasLimit < hi {
453+
failed, _, err = executable(optimisticGasLimit)
454+
if err != nil {
455+
return nil, err
456+
}
457+
if failed {
458+
lo = optimisticGasLimit
459+
} else {
460+
hi = optimisticGasLimit
461+
}
462+
}
463+
464+
// Binary search for the smallest gas limit that allows the tx to execute successfully.
465+
hi, err = types.BinSearch(lo, hi, executable)
466+
if err != nil {
467+
return nil, err
468+
}
469+
434470
return &types.EstimateGasResponse{Gas: hi}, nil
435471
}
436472

x/vm/keeper/state_transition.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -460,12 +460,12 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, msg core.Message, trace
460460
return nil, errorsmod.Wrap(types.ErrGasOverflow, "apply message")
461461
}
462462
// refund gas
463-
temporaryGasUsed := msg.GasLimit - leftoverGas
464-
refund := GasToRefund(stateDB.GetRefund(), temporaryGasUsed, refundQuotient)
463+
maxUsedGas := msg.GasLimit - leftoverGas
464+
refund := GasToRefund(stateDB.GetRefund(), maxUsedGas, refundQuotient)
465465

466466
// update leftoverGas and temporaryGasUsed with refund amount
467467
leftoverGas += refund
468-
temporaryGasUsed -= refund
468+
temporaryGasUsed := maxUsedGas - refund
469469

470470
// EVM execution error needs to be available for the JSON-RPC client
471471
var vmError string
@@ -508,11 +508,12 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, msg core.Message, trace
508508
}
509509

510510
return &types.MsgEthereumTxResponse{
511-
GasUsed: gasUsed.TruncateInt().Uint64(),
512-
VmError: vmError,
513-
Ret: ret,
514-
Logs: types.NewLogsFromEth(stateDB.Logs()),
515-
Hash: txConfig.TxHash.Hex(),
511+
GasUsed: gasUsed.TruncateInt().Uint64(),
512+
MaxUsedGas: maxUsedGas,
513+
VmError: vmError,
514+
Ret: ret,
515+
Logs: types.NewLogsFromEth(stateDB.Logs()),
516+
Hash: txConfig.TxHash.Hex(),
516517
}, nil
517518
}
518519

x/vm/types/tx.pb.go

Lines changed: 74 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)