From 321934d817440eca51a84e5e7e754823f605de6b Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Fri, 26 Jun 2026 15:40:55 +0200 Subject: [PATCH] eth/gasestimator,internal/ethapi: apply precompile overrides in eth_estimateGas eth_estimateGas accepts state overrides, including movePrecompileToAddress. DoEstimateGas builds the overridden precompile set and applies it to the state (#31795), but never passed it to the gas estimator, so the EVM used for estimation kept the default precompiles. As a result estimates ignored any precompile move. Thread the overridden precompile set through gasestimator.Options and call evm.SetPrecompiles on the estimation EVM, mirroring the eth_call path. The existing TestEstimateGasWithMovePrecompile did not catch this: its expected value falls within the estimator's allowed overestimation ratio for both the correct and buggy results. Add TestEstimateGasMovePrecompileAway, which moves a precompile off its address and calls the now-empty address; the expected intrinsic-only cost is well outside the error ratio, so it fails without the fix (estimating 21766 instead of 21000). Co-Authored-By: Claude Opus 4.8 (1M context) --- eth/gasestimator/gasestimator.go | 5 ++++ internal/ethapi/api.go | 1 + internal/ethapi/api_test.go | 46 ++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index f45fc0d8c9..2ee49476aa 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -44,6 +44,8 @@ type Options struct { BlobBaseFee *big.Int // BlobBaseFee optionally overrides the blob base fee in the execution context. + Precompiles vm.PrecompiledContracts // Precompiles optionally overrides the default set of precompiles (e.g. for state overrides that move precompiles). + ErrorRatio float64 // Allowed overestimation ratio for faster estimation termination } @@ -245,6 +247,9 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio } evm := vm.NewEVM(evmContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true}) defer evm.Release() + if opts.Precompiles != nil { + evm.SetPrecompiles(opts.Precompiles) + } // Monitor the outer context and interrupt the EVM upon cancellation. To avoid // a dangling goroutine until the outer estimation finishes, create an internal diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 2eb4dee3c0..572b2d1abe 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -943,6 +943,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr Header: header, State: state, BlobBaseFee: blobBaseFee, + Precompiles: precompiles, ErrorRatio: estimateGasErrorRatio, } // Set any required transaction default, but make sure the gas cap itself is not messed with diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 80a9036ecc..033843f2e9 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -3984,6 +3984,52 @@ func TestEstimateGasWithMovePrecompile(t *testing.T) { } } +// TestEstimateGasMovePrecompileAway checks that eth_estimateGas honours a +// movePrecompileToAddress override on the *source* address: after a precompile +// is moved away, calling its old address must behave like a call to an empty +// account (intrinsic gas only). Without threading the overridden precompile set +// into the estimator, the old address would still execute the precompile and the +// estimate would be inflated. +func TestEstimateGasMovePrecompileAway(t *testing.T) { + t.Parallel() + var ( + accounts = newAccounts(1) + genesis = &core.Genesis{ + Config: params.MergedTestChainConfig, + Alloc: types.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + ) + backend := newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + b.SetPoS() + }) + api := NewBlockChainAPI(backend) + // Move the RIPEMD-160 precompile (0x3) onto the SHA-256 address (0x2), + // then call the now-vacated 0x3 with empty calldata. The call must cost + // the bare intrinsic gas, since 0x3 no longer hosts a precompile. + var ( + ripemdAddr = common.BytesToAddress([]byte{0x3}) + sha256Addr = common.BytesToAddress([]byte{0x2}) + args = TransactionArgs{ + From: &accounts[0].addr, + To: &ripemdAddr, + } + overrides = &override.StateOverride{ + ripemdAddr: override.OverrideAccount{ + MovePrecompileTo: &sha256Addr, + }, + } + ) + gas, err := api.EstimateGas(context.Background(), args, nil, overrides, nil) + if err != nil { + t.Fatalf("EstimateGas failed: %v", err) + } + if uint64(gas) != params.TxGas { + t.Fatalf("mismatched gas: %d, want %d", gas, params.TxGas) + } +} + func TestEIP7910Config(t *testing.T) { var ( newUint64 = func(val uint64) *uint64 { return &val }