From 9b475ba5cae547fd1619411ad68748ba6d512596 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 19 May 2026 02:28:29 -0500 Subject: [PATCH 1/7] ethapi, eth: implement eth_baseFee RPC method Assisted-by: Claude:claude-sonnet-4-6 --- eth/api_backend.go | 8 ++++++++ eth/filters/IBackend.go | 14 ++++++++++++++ internal/ethapi/api.go | 5 +++++ internal/ethapi/api_test.go | 1 + internal/ethapi/backend.go | 1 + internal/ethapi/transaction_args_test.go | 1 + 6 files changed, 30 insertions(+) diff --git a/eth/api_backend.go b/eth/api_backend.go index 19b16fb70b..add844c98e 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/filtermaps" @@ -527,6 +528,13 @@ func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastB return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) } +func (b *EthAPIBackend) BaseFee(ctx context.Context) *big.Int { + if b.ChainConfig().IsLondon(b.CurrentHeader().Number) { + return eip1559.CalcBaseFee(b.ChainConfig(), b.CurrentHeader()) + } + return nil +} + func (b *EthAPIBackend) BlobBaseFee(ctx context.Context) *big.Int { if excess := b.CurrentHeader().ExcessBlobGas; excess != nil && b.ChainConfig().BlobScheduleConfig != nil { return eip4844.CalcBlobFee(b.ChainConfig(), b.CurrentHeader()) diff --git a/eth/filters/IBackend.go b/eth/filters/IBackend.go index 7a0bc7565a..b02bb348e3 100644 --- a/eth/filters/IBackend.go +++ b/eth/filters/IBackend.go @@ -66,6 +66,20 @@ func (mr *MockBackendMockRecorder) AccountManager() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountManager", reflect.TypeOf((*MockBackend)(nil).AccountManager)) } +// BaseFee mocks base method. +func (m *MockBackend) BaseFee(arg0 context.Context) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BaseFee", arg0) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// BaseFee indicates an expected call of BaseFee. +func (mr *MockBackendMockRecorder) BaseFee(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseFee", reflect.TypeOf((*MockBackend)(nil).BaseFee), arg0) +} + // BlobBaseFee mocks base method. func (m *MockBackend) BlobBaseFee(arg0 context.Context) *big.Int { m.ctrl.T.Helper() diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 71f0c178e6..0cdabc10d3 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -213,6 +213,11 @@ func (api *EthereumAPI) BlobBaseFee(ctx context.Context) *hexutil.Big { return (*hexutil.Big)(api.b.BlobBaseFee(ctx)) } +// BaseFee returns the base fee of the next block. +func (api *EthereumAPI) BaseFee(ctx context.Context) *hexutil.Big { + return (*hexutil.Big)(api.b.BaseFee(ctx)) +} + // Syncing returns false in case the node is currently not syncing with the network. It can be up-to-date or has not // yet received the latest block headers from its peers. In case it is synchronizing: // - startingBlock: block number this node started to synchronize from diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index ce26d7db3a..ea4cdd4da0 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -546,6 +546,7 @@ func (b testBackend) FeeHistory(ctx context.Context, blockCount uint64, lastBloc return nil, nil, nil, nil, nil, nil, nil } func (b testBackend) BlobBaseFee(ctx context.Context) *big.Int { return new(big.Int) } +func (b testBackend) BaseFee(ctx context.Context) *big.Int { return new(big.Int) } func (b testBackend) ChainDb() ethdb.Database { return b.db } func (b testBackend) AccountManager() *accounts.Manager { return b.accman } func (b testBackend) ExtRPCEnabled() bool { return false } diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 5113edce0c..07d326b5df 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -52,6 +52,7 @@ type Backend interface { SuggestGasTipCap(ctx context.Context) (*big.Int, error) FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) BlobBaseFee(ctx context.Context) *big.Int + BaseFee(ctx context.Context) *big.Int ChainDb() ethdb.Database AccountManager() *accounts.Manager ExtRPCEnabled() bool diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index f815b5fc0a..f1baa890e9 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -333,6 +333,7 @@ func (b *backendMock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { return big.NewInt(42), nil } func (b *backendMock) BlobBaseFee(ctx context.Context) *big.Int { return big.NewInt(42) } +func (b *backendMock) BaseFee(ctx context.Context) *big.Int { return big.NewInt(0) } func (b *backendMock) CurrentHeader() *types.Header { return b.current } func (b *backendMock) ChainConfig() *params.ChainConfig { return b.config } From 7e3a7045b194991ba967ff45c28983d4dd67f2e3 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 19 May 2026 14:49:19 -0500 Subject: [PATCH 2/7] eth/filters: regenerate IBackend mock via go generate Assisted-by: Claude:claude-sonnet-4-6 --- eth/filters/IBackend.go | 141 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/eth/filters/IBackend.go b/eth/filters/IBackend.go index b02bb348e3..2495721e5d 100644 --- a/eth/filters/IBackend.go +++ b/eth/filters/IBackend.go @@ -10,8 +10,6 @@ import ( reflect "reflect" time "time" - gomock "go.uber.org/mock/gomock" - ethereum "github.com/ethereum/go-ethereum" accounts "github.com/ethereum/go-ethereum/accounts" common "github.com/ethereum/go-ethereum/common" @@ -21,12 +19,14 @@ import ( filtermaps "github.com/ethereum/go-ethereum/core/filtermaps" state "github.com/ethereum/go-ethereum/core/state" stateless "github.com/ethereum/go-ethereum/core/stateless" + txpool "github.com/ethereum/go-ethereum/core/txpool" types "github.com/ethereum/go-ethereum/core/types" vm "github.com/ethereum/go-ethereum/core/vm" ethdb "github.com/ethereum/go-ethereum/ethdb" event "github.com/ethereum/go-ethereum/event" params "github.com/ethereum/go-ethereum/params" rpc "github.com/ethereum/go-ethereum/rpc" + gomock "github.com/golang/mock/gomock" ) // MockBackend is a mock of Backend interface. @@ -52,6 +52,34 @@ func (m *MockBackend) EXPECT() *MockBackendMockRecorder { return m.recorder } +// AcceptPreconfTxs mocks base method. +func (m *MockBackend) AcceptPreconfTxs() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AcceptPreconfTxs") + ret0, _ := ret[0].(bool) + return ret0 +} + +// AcceptPreconfTxs indicates an expected call of AcceptPreconfTxs. +func (mr *MockBackendMockRecorder) AcceptPreconfTxs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptPreconfTxs", reflect.TypeOf((*MockBackend)(nil).AcceptPreconfTxs)) +} + +// AcceptPrivateTxs mocks base method. +func (m *MockBackend) AcceptPrivateTxs() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AcceptPrivateTxs") + ret0, _ := ret[0].(bool) + return ret0 +} + +// AcceptPrivateTxs indicates an expected call of AcceptPrivateTxs. +func (mr *MockBackendMockRecorder) AcceptPrivateTxs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptPrivateTxs", reflect.TypeOf((*MockBackend)(nil).AcceptPrivateTxs)) +} + // AccountManager mocks base method. func (m *MockBackend) AccountManager() *accounts.Manager { m.ctrl.T.Helper() @@ -167,6 +195,21 @@ func (mr *MockBackendMockRecorder) ChainDb() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainDb", reflect.TypeOf((*MockBackend)(nil).ChainDb)) } +// CheckPreconfStatus mocks base method. +func (m *MockBackend) CheckPreconfStatus(arg0 common.Hash) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckPreconfStatus", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckPreconfStatus indicates an expected call of CheckPreconfStatus. +func (mr *MockBackendMockRecorder) CheckPreconfStatus(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckPreconfStatus", reflect.TypeOf((*MockBackend)(nil).CheckPreconfStatus), arg0) +} + // CurrentBlock mocks base method. func (m *MockBackend) CurrentBlock() *types.Header { m.ctrl.T.Helper() @@ -770,6 +813,34 @@ func (mr *MockBackendMockRecorder) Pending() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pending", reflect.TypeOf((*MockBackend)(nil).Pending)) } +// PreconfEnabled mocks base method. +func (m *MockBackend) PreconfEnabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PreconfEnabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// PreconfEnabled indicates an expected call of PreconfEnabled. +func (mr *MockBackendMockRecorder) PreconfEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreconfEnabled", reflect.TypeOf((*MockBackend)(nil).PreconfEnabled)) +} + +// PrivateTxEnabled mocks base method. +func (m *MockBackend) PrivateTxEnabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrivateTxEnabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// PrivateTxEnabled indicates an expected call of PrivateTxEnabled. +func (mr *MockBackendMockRecorder) PrivateTxEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrivateTxEnabled", reflect.TypeOf((*MockBackend)(nil).PrivateTxEnabled)) +} + // ProtocolVersion mocks base method. func (m *MockBackend) ProtocolVersion() uint { m.ctrl.T.Helper() @@ -784,6 +855,18 @@ func (mr *MockBackendMockRecorder) ProtocolVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProtocolVersion", reflect.TypeOf((*MockBackend)(nil).ProtocolVersion)) } +// PurgePrivateTx mocks base method. +func (m *MockBackend) PurgePrivateTx(arg0 common.Hash) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "PurgePrivateTx", arg0) +} + +// PurgePrivateTx indicates an expected call of PurgePrivateTx. +func (mr *MockBackendMockRecorder) PurgePrivateTx(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PurgePrivateTx", reflect.TypeOf((*MockBackend)(nil).PurgePrivateTx), arg0) +} + // PurgeWhitelistedCheckpoint mocks base method. func (m *MockBackend) PurgeWhitelistedCheckpoint() { m.ctrl.T.Helper() @@ -892,6 +975,18 @@ func (mr *MockBackendMockRecorder) RPCTxSyncMaxTimeout() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPCTxSyncMaxTimeout", reflect.TypeOf((*MockBackend)(nil).RPCTxSyncMaxTimeout)) } +// RecordPrivateTx mocks base method. +func (m *MockBackend) RecordPrivateTx(arg0 common.Hash) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RecordPrivateTx", arg0) +} + +// RecordPrivateTx indicates an expected call of RecordPrivateTx. +func (mr *MockBackendMockRecorder) RecordPrivateTx(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordPrivateTx", reflect.TypeOf((*MockBackend)(nil).RecordPrivateTx), arg0) +} + // SendTx mocks base method. func (m *MockBackend) SendTx(arg0 context.Context, arg1 *types.Transaction) error { m.ctrl.T.Helper() @@ -994,6 +1089,34 @@ func (mr *MockBackendMockRecorder) SubmitHashrate(arg0, arg1 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitHashrate", reflect.TypeOf((*MockBackend)(nil).SubmitHashrate), arg0, arg1) } +// SubmitPrivateTx mocks base method. +func (m *MockBackend) SubmitPrivateTx(arg0 *types.Transaction) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubmitPrivateTx", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SubmitPrivateTx indicates an expected call of SubmitPrivateTx. +func (mr *MockBackendMockRecorder) SubmitPrivateTx(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitPrivateTx", reflect.TypeOf((*MockBackend)(nil).SubmitPrivateTx), arg0) +} + +// SubmitTxForPreconf mocks base method. +func (m *MockBackend) SubmitTxForPreconf(arg0 *types.Transaction) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubmitTxForPreconf", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SubmitTxForPreconf indicates an expected call of SubmitTxForPreconf. +func (mr *MockBackendMockRecorder) SubmitTxForPreconf(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitTxForPreconf", reflect.TypeOf((*MockBackend)(nil).SubmitTxForPreconf), arg0) +} + // SubmitWork mocks base method. func (m *MockBackend) SubmitWork(arg0 types.BlockNonce, arg1, arg2 common.Hash) (bool, error) { m.ctrl.T.Helper() @@ -1194,6 +1317,20 @@ func (mr *MockBackendMockRecorder) TxPoolContentFrom(arg0 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxPoolContentFrom", reflect.TypeOf((*MockBackend)(nil).TxPoolContentFrom), arg0) } +// TxStatus mocks base method. +func (m *MockBackend) TxStatus(arg0 common.Hash) txpool.TxStatus { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxStatus", arg0) + ret0, _ := ret[0].(txpool.TxStatus) + return ret0 +} + +// TxStatus indicates an expected call of TxStatus. +func (mr *MockBackendMockRecorder) TxStatus(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxStatus", reflect.TypeOf((*MockBackend)(nil).TxStatus), arg0) +} + // UnprotectedAllowed mocks base method. func (m *MockBackend) UnprotectedAllowed() bool { m.ctrl.T.Helper() From 867b8d0dc531ecc3c3b8234437d2cdcb9ef1f0b5 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 3 Jun 2026 17:39:57 -0500 Subject: [PATCH 3/7] eth/filters: regenerate IBackend mock via go generate Assisted-by: Claude:claude-sonnet-4-6 --- eth/filters/IBackend.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/eth/filters/IBackend.go b/eth/filters/IBackend.go index 31690bd877..c0d867643a 100644 --- a/eth/filters/IBackend.go +++ b/eth/filters/IBackend.go @@ -100,6 +100,20 @@ func (mr *MockBackendMockRecorder) AccountManager() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountManager", reflect.TypeOf((*MockBackend)(nil).AccountManager)) } +// BaseFee mocks base method. +func (m *MockBackend) BaseFee(ctx context.Context) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BaseFee", ctx) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// BaseFee indicates an expected call of BaseFee. +func (mr *MockBackendMockRecorder) BaseFee(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseFee", reflect.TypeOf((*MockBackend)(nil).BaseFee), ctx) +} + // BlobBaseFee mocks base method. func (m *MockBackend) BlobBaseFee(ctx context.Context) *big.Int { m.ctrl.T.Helper() @@ -436,17 +450,17 @@ func (mr *MockBackendMockRecorder) GetCanonicalTransaction(txHash any) *gomock.C } // GetEVM mocks base method. -func (m *MockBackend) GetEVM(ctx context.Context, arg1 *state.StateDB, header *types.Header, vmConfig *vm.Config, blockCtx *vm.BlockContext) *vm.EVM { +func (m *MockBackend) GetEVM(ctx context.Context, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockCtx *vm.BlockContext) *vm.EVM { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEVM", ctx, arg1, header, vmConfig, blockCtx) + ret := m.ctrl.Call(m, "GetEVM", ctx, state, header, vmConfig, blockCtx) ret0, _ := ret[0].(*vm.EVM) return ret0 } // GetEVM indicates an expected call of GetEVM. -func (mr *MockBackendMockRecorder) GetEVM(ctx, arg1, header, vmConfig, blockCtx any) *gomock.Call { +func (mr *MockBackendMockRecorder) GetEVM(ctx, state, header, vmConfig, blockCtx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEVM", reflect.TypeOf((*MockBackend)(nil).GetEVM), ctx, arg1, header, vmConfig, blockCtx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEVM", reflect.TypeOf((*MockBackend)(nil).GetEVM), ctx, state, header, vmConfig, blockCtx) } // GetFinalizedBlockNumber mocks base method. From 57b8e90a71baeab4b0945569dc60311516086d6f Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 3 Jun 2026 17:43:32 -0500 Subject: [PATCH 4/7] eth: snapshot CurrentHeader once in BaseFee and BlobBaseFee Assisted-by: Claude:claude-sonnet-4-6 --- eth/api_backend.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/eth/api_backend.go b/eth/api_backend.go index add844c98e..38a3bef16e 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -529,15 +529,17 @@ func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastB } func (b *EthAPIBackend) BaseFee(ctx context.Context) *big.Int { - if b.ChainConfig().IsLondon(b.CurrentHeader().Number) { - return eip1559.CalcBaseFee(b.ChainConfig(), b.CurrentHeader()) + header := b.CurrentHeader() + if b.ChainConfig().IsLondon(header.Number) { + return eip1559.CalcBaseFee(b.ChainConfig(), header) } return nil } func (b *EthAPIBackend) BlobBaseFee(ctx context.Context) *big.Int { - if excess := b.CurrentHeader().ExcessBlobGas; excess != nil && b.ChainConfig().BlobScheduleConfig != nil { - return eip4844.CalcBlobFee(b.ChainConfig(), b.CurrentHeader()) + header := b.CurrentHeader() + if header.ExcessBlobGas != nil && b.ChainConfig().BlobScheduleConfig != nil { + return eip4844.CalcBlobFee(b.ChainConfig(), header) } return nil } From 19c5a976ac293d6741ec2d05fb54a62cd7a71cab Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 3 Jun 2026 18:00:48 -0500 Subject: [PATCH 5/7] test(eth): add unit tests for BaseFee and BlobBaseFee Assisted-by: Claude:claude-sonnet-4-6 --- eth/api_backend_test.go | 76 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/eth/api_backend_test.go b/eth/api_backend_test.go index 7aa430a5eb..2a1b931417 100644 --- a/eth/api_backend_test.go +++ b/eth/api_backend_test.go @@ -349,6 +349,82 @@ func TestRelayMethodWiring(t *testing.T) { }) } +func TestBaseFee(t *testing.T) { + t.Run("LondonActive", func(t *testing.T) { + b := initBackend(false) + defer b.eth.blockchain.Stop() + if b.BaseFee(t.Context()) == nil { + t.Fatal("expected non-nil BaseFee when London is active") + } + }) + + t.Run("PreLondon", func(t *testing.T) { + // NonActivatedConfig has no LondonBlock — IsLondon always returns false. + db := rawdb.NewMemoryDatabase() + genesis := &core.Genesis{ + Config: params.NonActivatedConfig, + Difficulty: big.NewInt(1), + } + chain, err := core.NewBlockChain(db, genesis, ethash.NewFaker(), nil) + require.NoError(t, err) + defer chain.Stop() + b := &EthAPIBackend{eth: &Ethereum{blockchain: chain}} + if b.BaseFee(t.Context()) != nil { + t.Fatal("expected nil BaseFee when London is inactive") + } + }) +} + +func TestBlobBaseFee(t *testing.T) { + t.Run("CancunActive", func(t *testing.T) { + // MergedTestChainConfig has Cancun at block 0 with a BlobScheduleConfig. + // Genesis sets ExcessBlobGas = new(uint64) so the guard passes. + b := initBackend(false) + defer b.eth.blockchain.Stop() + if b.BlobBaseFee(t.Context()) == nil { + t.Fatal("expected non-nil BlobBaseFee when Cancun is active") + } + }) + + t.Run("NoExcessBlobGas", func(t *testing.T) { + // AllEthashProtocolChanges has London at block 0 but no CancunBlock. + // Genesis therefore leaves ExcessBlobGas nil, so BlobBaseFee must return nil. + db := rawdb.NewMemoryDatabase() + genesis := &core.Genesis{ + Config: params.AllEthashProtocolChanges, + BaseFee: big.NewInt(params.InitialBaseFee), + Difficulty: big.NewInt(1), + } + chain, err := core.NewBlockChain(db, genesis, ethash.NewFaker(), nil) + require.NoError(t, err) + defer chain.Stop() + b := &EthAPIBackend{eth: &Ethereum{blockchain: chain}} + if b.BlobBaseFee(t.Context()) != nil { + t.Fatal("expected nil BlobBaseFee when ExcessBlobGas is nil") + } + }) + + t.Run("NoBlobScheduleConfig", func(t *testing.T) { + // Cancun active but BlobScheduleConfig nil: ExcessBlobGas is set by genesis + // (not nil), but the guard short-circuits before calling CalcBlobFee. + db := rawdb.NewMemoryDatabase() + config := *params.MergedTestChainConfig + config.BlobScheduleConfig = nil + genesis := &core.Genesis{ + Config: &config, + Difficulty: common.Big0, + BaseFee: big.NewInt(params.InitialBaseFee), + } + chain, err := core.NewBlockChain(db, genesis, beacon.New(ethash.NewFaker()), nil) + require.NoError(t, err) + defer chain.Stop() + b := &EthAPIBackend{eth: &Ethereum{blockchain: chain}} + if b.BlobBaseFee(t.Context()) != nil { + t.Fatal("expected nil BlobBaseFee when BlobScheduleConfig is nil") + } + }) +} + // TestRelayGracefulShutdownOnStop verifies that the relay shutdown path in // Ethereum.Stop() completes promptly and doesn't hang or panic. func TestRelayGracefulShutdownOnStop(t *testing.T) { From dc876bd895188d145bec16c7e076a86e37451a30 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Mon, 8 Jun 2026 15:52:49 -0500 Subject: [PATCH 6/7] chore: bump Go from 1.26.3 to 1.26.4 Assisted-by: Claude:claude-sonnet-4-6 --- .golangci.yml | 2 +- Dockerfile | 2 +- Dockerfile.alltools | 2 +- cmd/keeper/go.mod | 2 +- go.mod | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 86540a53fe..bf72509031 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,7 @@ # This file configures github.com/golangci/golangci-lint. version: '2' run: - go: '1.26.3' + go: '1.26.4' tests: true linters: default: none diff --git a/Dockerfile b/Dockerfile index 93df2d9b53..aeec620535 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ─── BUILDER STAGE ─────────────────────────────────────────────────────────────── -FROM golang:1.26.3-alpine AS builder +FROM golang:1.26.4-alpine AS builder ARG BOR_DIR=/var/lib/bor/ ENV BOR_DIR=$BOR_DIR diff --git a/Dockerfile.alltools b/Dockerfile.alltools index 72913d1ca9..c2b7fd947c 100644 --- a/Dockerfile.alltools +++ b/Dockerfile.alltools @@ -1,5 +1,5 @@ # Build Geth in a stock Go builder container -FROM golang:1.26.3-alpine AS builder +FROM golang:1.26.4-alpine AS builder RUN apk add --no-cache make gcc musl-dev linux-headers git diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod index 901577a5d3..9842ebee56 100644 --- a/cmd/keeper/go.mod +++ b/cmd/keeper/go.mod @@ -1,6 +1,6 @@ module github.com/ethereum/go-ethereum/cmd/keeper -go 1.26.3 +go 1.26.4 require ( github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260104020744-7268a54d0358 diff --git a/go.mod b/go.mod index 59cc2fe7f4..97e838c1f8 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/ethereum/go-ethereum // Note: Change the go image version in Dockerfile if you change this. -go 1.26.3 +go 1.26.4 require ( github.com/0xPolygon/crand v1.0.3 From 3fa2927bcbd209a376c4be13bcabd94eef755e42 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 10 Jun 2026 02:23:33 -0500 Subject: [PATCH 7/7] fix(eth): check IsLondon(next block) in BaseFee to fix off-by-one at transition Assisted-by: Claude:claude-sonnet-4-6 --- eth/api_backend.go | 2 +- eth/api_backend_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/eth/api_backend.go b/eth/api_backend.go index 38a3bef16e..17871fea42 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -530,7 +530,7 @@ func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastB func (b *EthAPIBackend) BaseFee(ctx context.Context) *big.Int { header := b.CurrentHeader() - if b.ChainConfig().IsLondon(header.Number) { + if b.ChainConfig().IsLondon(new(big.Int).Add(header.Number, common.Big1)) { return eip1559.CalcBaseFee(b.ChainConfig(), header) } return nil diff --git a/eth/api_backend_test.go b/eth/api_backend_test.go index 2a1b931417..04e642ab47 100644 --- a/eth/api_backend_test.go +++ b/eth/api_backend_test.go @@ -358,6 +358,40 @@ func TestBaseFee(t *testing.T) { } }) + t.Run("LondonTransitionBlock", func(t *testing.T) { + // LondonBlock = 1 so the genesis block (number 0) is the pre-London + // parent of the first London block. IsLondon(0) is false, but + // IsLondon(0+1) is true, so BaseFee must return InitialBaseFee. + config := *params.NonActivatedConfig + config.HomesteadBlock = big.NewInt(0) + config.EIP150Block = big.NewInt(0) + config.EIP155Block = big.NewInt(0) + config.EIP158Block = big.NewInt(0) + config.ByzantiumBlock = big.NewInt(0) + config.ConstantinopleBlock = big.NewInt(0) + config.PetersburgBlock = big.NewInt(0) + config.IstanbulBlock = big.NewInt(0) + config.BerlinBlock = big.NewInt(0) + config.LondonBlock = big.NewInt(1) + db := rawdb.NewMemoryDatabase() + genesis := &core.Genesis{ + Config: &config, + Difficulty: big.NewInt(1), + } + chain, err := core.NewBlockChain(db, genesis, ethash.NewFaker(), nil) + require.NoError(t, err) + defer chain.Stop() + b := &EthAPIBackend{eth: &Ethereum{blockchain: chain}} + got := b.BaseFee(t.Context()) + if got == nil { + t.Fatal("expected InitialBaseFee at London transition block, got nil") + } + expected := new(big.Int).SetUint64(params.InitialBaseFee) + if got.Cmp(expected) != 0 { + t.Fatalf("expected BaseFee %v, got %v", expected, got) + } + }) + t.Run("PreLondon", func(t *testing.T) { // NonActivatedConfig has no LondonBlock — IsLondon always returns false. db := rawdb.NewMemoryDatabase()