Skip to content

Commit e15a37d

Browse files
cloudgrayaljo242
andauthored
fix: align gas estimation logic with go-ethereum v1.16.3 (#766)
* WIP: add estimateGas test file * fix: align gas estimation binary search with geth * fix(x/vm): fix EstimateGasInternal - let EstimateGasInternal call ApplyMessageWithConfig with internal = true - fix lowest possible gas * apply EIP-7623: Data-heavy transactions pay the floor gas * fix: state overrides handling * fix test code for gas estimation * chore: update CHANGELOG.md * test(x/vm): fix test code * chore: resolve codeQL warnings * fix(x/vm): StateDB.Commit * test(jsonrpc): enhance eth_estimateGas test case * chore: remove temporary file for test * chore: apply code review --------- Co-authored-by: Alex | Cosmos Labs <[email protected]>
1 parent 256d913 commit e15a37d

File tree

12 files changed

+181
-34
lines changed

12 files changed

+181
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
### BUG FIXES
1414

1515
- [\#748](https://github.com/cosmos/evm/pull/748) Fix DynamicFeeChecker in Cosmos ante handler to respect NoBaseFee feemarkets' parameter.
16+
- [\#766](https://github.com/cosmos/evm/pull/766) Align gas estimation logic with go-ethereum v1.16.3
1617
- [\#769](https://github.com/cosmos/evm/pull/769) Fix erc20 ibc middleware to not to validate sender address format.
1718
- [\#756](https://github.com/cosmos/evm/pull/756) Fix error message typo in NewMsgCancelProposal.
1819
- [\#772](https://github.com/cosmos/evm/pull/772) Avoid panic on close if evm mempool not used.

rpc/types/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (diff *StateOverride) Apply(db *statedb.StateDB, precompiles vm.Precompiled
127127
// Apply state diff into specified accounts.
128128
if account.StateDiff != nil {
129129
for key, value := range *account.StateDiff {
130-
db.SetState(addr, key, value)
130+
db.SetStateOverride(addr, key, value)
131131
}
132132
}
133133
}

tests/integration/x/vm/test_grpc_query.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ func (s *KeeperTestSuite) TestEstimateGas() {
574574
return types.TransactionArgs{}
575575
},
576576
true,
577-
ethparams.TxGasContractCreation,
577+
53793,
578578
false,
579579
config.DefaultGasCap,
580580
},
@@ -699,7 +699,7 @@ func (s *KeeperTestSuite) TestEstimateGas() {
699699
}
700700
},
701701
true,
702-
1187108,
702+
1197697,
703703
false,
704704
config.DefaultGasCap,
705705
},
@@ -727,7 +727,7 @@ func (s *KeeperTestSuite) TestEstimateGas() {
727727
}
728728
},
729729
true,
730-
51880,
730+
52669,
731731
false,
732732
config.DefaultGasCap,
733733
},
@@ -809,7 +809,7 @@ func (s *KeeperTestSuite) TestEstimateGas() {
809809
}
810810
},
811811
true,
812-
1187108,
812+
1197697,
813813
true,
814814
config.DefaultGasCap,
815815
},
@@ -838,7 +838,7 @@ func (s *KeeperTestSuite) TestEstimateGas() {
838838
}
839839
},
840840
true,
841-
51880,
841+
52669,
842842
true,
843843
config.DefaultGasCap,
844844
},
@@ -1067,7 +1067,7 @@ func (s *KeeperTestSuite) TestEstimateGasWithStateOverrides() {
10671067
return string(bz)
10681068
},
10691069
true,
1070-
49140,
1070+
52114,
10711071
false,
10721072
config.DefaultGasCap,
10731073
},

tests/jsonrpc/simulator/namespaces/eth.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,16 @@ func estimateNativeTransferGas(rCtx *types.RPCContext) (string, error) {
15321532
return "", err
15331533
}
15341534

1535+
if rCtx.EnableComparison && rCtx.Geth != nil && rCtx.Geth.Client != nil {
1536+
gethGasLimit, err := rCtx.Geth.EstimateGas(context.Background(), callMsg)
1537+
if err != nil {
1538+
return "", fmt.Errorf("geth estimateGas failed: %w", err)
1539+
}
1540+
if gasLimit != gethGasLimit {
1541+
return "", fmt.Errorf("estimateGas mismatch (native): evmd=%d geth=%d", gasLimit, gethGasLimit)
1542+
}
1543+
}
1544+
15351545
return fmt.Sprintf("0x%x", gasLimit), nil
15361546
}
15371547

@@ -1567,6 +1577,16 @@ func estimateERC20TransferGas(rCtx *types.RPCContext) (string, bool, error) {
15671577
return "", false, err
15681578
}
15691579

1580+
if rCtx.EnableComparison && rCtx.Geth != nil && rCtx.Geth.Client != nil {
1581+
gethGas, err := rCtx.Geth.EstimateGas(context.Background(), erc20Call)
1582+
if err != nil {
1583+
return "", false, fmt.Errorf("geth estimateGas failed: %w", err)
1584+
}
1585+
if gas != gethGas {
1586+
return "", false, fmt.Errorf("estimateGas mismatch (erc20 transfer): evmd=%d geth=%d", gas, gethGas)
1587+
}
1588+
}
1589+
15701590
return fmt.Sprintf("0x%x", gas), false, nil
15711591
}
15721592

@@ -1631,6 +1651,16 @@ func estimateERC20OverrideGas(rCtx *types.RPCContext) (string, bool, error) {
16311651
return "", false, err
16321652
}
16331653

1654+
if rCtx.EnableComparison && rCtx.Geth != nil && rCtx.Geth.Client != nil {
1655+
var gethGas hexutil.Uint64
1656+
if err := rCtx.Geth.RPCClient().Call(&gethGas, string(MethodNameEthEstimateGas), overrideParams, "latest", overrideState); err != nil {
1657+
return "", false, fmt.Errorf("geth estimateGas failed: %w", err)
1658+
}
1659+
if uint64(overrideGas) != uint64(gethGas) {
1660+
return "", false, fmt.Errorf("estimateGas mismatch (erc20 override): evmd=%d geth=%d", uint64(overrideGas), uint64(gethGas))
1661+
}
1662+
}
1663+
16341664
return fmt.Sprintf("0x%x", uint64(overrideGas)), false, nil
16351665
}
16361666

x/vm/keeper/grpc_query.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ func (k Keeper) EstimateGasInternal(c context.Context, req *types.EthCallRequest
308308

309309
// Binary search the gas requirement, as it may be higher than the amount used
310310
var (
311-
lo = ethparams.TxGas - 1
311+
lo uint64
312312
hi uint64
313313
gasCap uint64
314314
)
@@ -410,8 +410,9 @@ func (k Keeper) EstimateGasInternal(c context.Context, req *types.EthCallRequest
410410
// resetting the gasMeter after increasing the sequence to have an accurate gas estimation on EVM extensions transactions
411411
tmpCtx = buildTraceCtx(tmpCtx, msg.GasLimit)
412412
}
413-
// pass false to not commit StateDB
414-
rsp, err = k.ApplyMessageWithConfig(tmpCtx, *msg, nil, false, cfg, txConfig, false, overrides)
413+
// pass "commit" as false to avoid committing StateDB
414+
// pass "internal" as true to avoid applying feemarketParams.MinGasMultiplier for gas estimation.
415+
rsp, err = k.ApplyMessageWithConfig(tmpCtx, *msg, nil, false, cfg, txConfig, true, overrides)
415416
if err != nil {
416417
if errors.Is(err, core.ErrIntrinsicGas) || errors.Is(err, core.ErrFloorDataGas) {
417418
return true, nil, nil // Special case, raise gas limit
@@ -461,6 +462,12 @@ func (k Keeper) EstimateGasInternal(c context.Context, req *types.EthCallRequest
461462
// If no larger allowance is available, fail fast
462463
return nil, fmt.Errorf("gas required exceeds allowance (%d)", hi)
463464
}
465+
// For almost any transaction, the gas consumed by the unconstrained execution
466+
// above lower-bounds the gas limit required for it to succeed. One exception
467+
// is those that explicitly check gas remaining in order to execute within a
468+
// given limit, but we probably don't want to return the lowest possible gas
469+
// limit for these cases anyway.
470+
lo = result.GasUsed - 1
464471

465472
// There's a fairly high chance for the transaction to execute successfully
466473
// with gasLimit set to the first execution's usedGas + gasRefund. Explicitly

x/vm/keeper/state_transition.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,9 @@ func (k *Keeper) ApplyMessageWithConfig(
393393
overrides *rpctypes.StateOverride,
394394
) (*types.MsgEthereumTxResponse, error) {
395395
var (
396-
ret []byte // return bytes from evm execution
397-
vmErr error // vm errors do not effect consensus and are therefore not assigned to err
396+
ret []byte // return bytes from evm execution
397+
vmErr error // vm errors do not effect consensus and are therefore not assigned to err
398+
floorDataGas uint64
398399
)
399400

400401
stateDB := statedb.New(ctx, k, txConfig)
@@ -443,7 +444,7 @@ func (k *Keeper) ApplyMessageWithConfig(
443444
return nil, errorsmod.Wrap(core.ErrIntrinsicGas, "apply message")
444445
}
445446
if rules.IsPrague {
446-
floorDataGas, err := core.FloorDataGas(msg.Data)
447+
floorDataGas, err = core.FloorDataGas(msg.Data)
447448
if err != nil {
448449
return nil, err
449450
}
@@ -498,10 +499,6 @@ func (k *Keeper) ApplyMessageWithConfig(
498499
refundQuotient = params.RefundQuotientEIP3529
499500
}
500501

501-
if internal {
502-
refundQuotient = 1 // full refund on internal calls
503-
}
504-
505502
// calculate gas refund
506503
if msg.GasLimit < leftoverGas {
507504
return nil, errorsmod.Wrap(types.ErrGasOverflow, "apply message")
@@ -513,6 +510,20 @@ func (k *Keeper) ApplyMessageWithConfig(
513510
// update leftoverGas and temporaryGasUsed with refund amount
514511
leftoverGas += refund
515512
temporaryGasUsed := maxUsedGas - refund
513+
if rules.IsPrague {
514+
// After EIP-7623: Data-heavy transactions pay the floor gas.
515+
if temporaryGasUsed < floorDataGas {
516+
prev := leftoverGas
517+
leftoverGas = msg.GasLimit - floorDataGas
518+
temporaryGasUsed = floorDataGas
519+
if vmCfg.Tracer != nil && vmCfg.Tracer.OnGasChange != nil {
520+
vmCfg.Tracer.OnGasChange(prev, leftoverGas, tracing.GasChangeTxDataFloor)
521+
}
522+
}
523+
if maxUsedGas < floorDataGas {
524+
maxUsedGas = floorDataGas
525+
}
526+
}
516527

517528
// EVM execution error needs to be available for the JSON-RPC client
518529
var vmError string

x/vm/statedb/journal.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ func newJournal() *journal {
5151
}
5252
}
5353

54+
// reset clears the journal so it can be reused without reallocating.
55+
func (j *journal) reset() {
56+
j.entries = j.entries[:0]
57+
for _, k := range j.sortedDirties() {
58+
delete(j.dirties, k)
59+
}
60+
}
61+
5462
// sortedDirties sort the dirty addresses for deterministic iteration
5563
func (j *journal) sortedDirties() []common.Address {
5664
keys := make([]common.Address, 0, len(j.dirties))
@@ -117,8 +125,10 @@ type (
117125
prev uint64
118126
}
119127
storageChange struct {
120-
account *common.Address
121-
key, prevalue common.Hash
128+
account *common.Address
129+
key common.Hash
130+
preValue common.Hash
131+
origValue common.Hash
122132
}
123133
transientStorageChange struct {
124134
account *common.Address
@@ -240,7 +250,7 @@ func (ch codeChange) Dirtied() *common.Address {
240250
}
241251

242252
func (ch storageChange) Revert(s *StateDB) {
243-
s.getStateObject(*ch.account).setState(ch.key, ch.prevalue)
253+
s.getStateObject(*ch.account).setState(ch.key, ch.preValue, ch.origValue)
244254
}
245255

246256
func (ch storageChange) Dirtied() *common.Address {

x/vm/statedb/state_object.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,13 +251,15 @@ func (s *stateObject) SetState(key common.Hash, value common.Hash) common.Hash {
251251
if prev == value {
252252
return prev
253253
}
254+
origin := s.GetCommittedState(key)
254255
// New value is different, update and journal the change
255256
s.db.journal.append(storageChange{
256-
account: &s.address,
257-
key: key,
258-
prevalue: prev,
257+
account: &s.address,
258+
key: key,
259+
preValue: prev,
260+
origValue: origin,
259261
})
260-
s.setState(key, value)
262+
s.setState(key, value, origin)
261263
return prev
262264
}
263265

@@ -270,6 +272,26 @@ func (s *stateObject) SetStorage(storage Storage) {
270272
s.dirtyStorage = make(Storage)
271273
}
272274

273-
func (s *stateObject) setState(key, value common.Hash) {
275+
// setState updates a value in account dirty storage. The dirtiness will be
276+
// removed if the value being set equals to the original value.
277+
func (s *stateObject) setState(key, value, origin common.Hash) {
278+
// Storage slot is set back to its original value, undo the dirty marker
279+
if value == origin {
280+
delete(s.dirtyStorage, key)
281+
return
282+
}
274283
s.dirtyStorage[key] = value
275284
}
285+
286+
// SetStateOverride installs the provided storage value as part of the base state used for
287+
// simulations. Subsequent reads treat the slot as if it were committed on-chain.
288+
func (s *stateObject) SetStateOverride(key, value common.Hash) {
289+
if s.originStorage == nil {
290+
s.originStorage = make(Storage)
291+
}
292+
s.originStorage[key] = value
293+
delete(s.dirtyStorage, key)
294+
if s.overrideStorage != nil {
295+
s.overrideStorage[key] = value
296+
}
297+
}

x/vm/statedb/statedb.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) {
142142
delete(s.stateObjects, obj.address)
143143
}
144144
}
145+
// Invalidate journal because reverting across transactions is not allowed.
146+
s.clearJournalAndRefund()
145147
}
146148

147149
// New creates a new state from a given trie.
@@ -331,6 +333,25 @@ func (s *StateDB) GetStateAndCommittedState(addr common.Address, hash common.Has
331333
return common.Hash{}, common.Hash{}
332334
}
333335

336+
// SetStateOverride installs the provided storage value as part of the base state used by simulations.
337+
func (s *StateDB) SetStateOverride(addr common.Address, key, value common.Hash) {
338+
stateObject := s.getOrNewStateObject(addr)
339+
if stateObject != nil {
340+
stateObject.SetStateOverride(key, value)
341+
}
342+
}
343+
344+
func (s *StateDB) clearJournalAndRefund() {
345+
if s.journal == nil {
346+
s.journal = newJournal()
347+
} else {
348+
s.journal.reset()
349+
}
350+
s.validRevisions = nil
351+
s.nextRevisionID = 0
352+
s.refund = 0
353+
}
354+
334355
// GetRefund returns the current value of the refund counter.
335356
func (s *StateDB) GetRefund() uint64 {
336357
return s.refund
@@ -697,19 +718,19 @@ func (s *StateDB) Commit() error {
697718
if s.writeCache != nil {
698719
s.writeCache()
699720
}
700-
return s.commitWithCtx(s.ctx)
721+
return s.commitWithCtx(s.ctx, false)
701722
}
702723

703724
// CommitWithCacheCtx writes the dirty states to keeper using the cacheCtx.
704725
// This function is used before any precompile call to make sure the cacheCtx
705726
// is updated with the latest changes within the tx (StateDB's journal entries).
706727
func (s *StateDB) CommitWithCacheCtx() error {
707-
return s.commitWithCtx(s.cacheCtx)
728+
return s.commitWithCtx(s.cacheCtx, true)
708729
}
709730

710731
// commitWithCtx writes the dirty states to keeper
711732
// using the provided context
712-
func (s *StateDB) commitWithCtx(ctx sdk.Context) error {
733+
func (s *StateDB) commitWithCtx(ctx sdk.Context, keepDirty bool) error {
713734
for _, addr := range s.journal.sortedDirties() {
714735
obj := s.stateObjects[addr]
715736
if obj.selfDestructed {
@@ -735,6 +756,32 @@ func (s *StateDB) commitWithCtx(ctx sdk.Context) error {
735756
} else {
736757
s.keeper.SetState(ctx, obj.Address(), key, valueBytes)
737758
}
759+
760+
// Track the persisted value as the new baseline so later writes compare
761+
// against the most recently flushed state. In go-ethereum the same happens
762+
// during commitStorage: originStorage follows the latest flush and act as
763+
// the reference for future dirty detection. The actual revert path still
764+
// consults the journal's origvalue, so keeping this cache in sync during
765+
// cacheCtx flushes is safe even if a precompile subsequently reverts.
766+
if obj.originStorage == nil {
767+
obj.originStorage = make(Storage)
768+
}
769+
if len(valueBytes) == 0 {
770+
delete(obj.originStorage, key)
771+
} else {
772+
obj.originStorage[key] = obj.dirtyStorage[key]
773+
}
774+
// Only the final Commit against the root context should clear dirtyStorage.
775+
// During precompile execution we pass keepDirty=true so that the slots remain
776+
// marked dirty after cacheCtx flushes, letting the outer transaction still
777+
// persist those writes (or revert them via the journal) once execution finishes.
778+
//
779+
// This is essential after SetState started syncing originStorage: the cacheCtx
780+
// flush must leave the dirty slots intact so the final Commit() can push the
781+
// same updates into the keeper context.
782+
if !keepDirty {
783+
delete(obj.dirtyStorage, key)
784+
}
738785
}
739786
}
740787
}

x/vm/statedb/statedb_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,7 @@ func (suite *StateDBTestSuite) TestState() {
295295
{"set state even if same as original value (due to possible reverts within precompile calls)", func(db *statedb.StateDB) {
296296
db.SetState(address, key1, value1)
297297
db.SetState(address, key1, common.Hash{})
298-
}, statedb.Storage{
299-
key1: common.Hash{},
300-
}},
298+
}, statedb.Storage{}},
301299
{"set state", func(db *statedb.StateDB) {
302300
// check empty initial state
303301
suite.Require().Equal(common.Hash{}, db.GetState(address, key1))

0 commit comments

Comments
 (0)