From ccc047b71ef500d7a54dc684ad219f09e3aa83c5 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Tue, 21 Apr 2026 10:03:39 +0200 Subject: [PATCH 1/4] core: set empty BlockAccessListHash on Amsterdam genesis --- core/genesis.go | 3 +++ core/types/hashes.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/core/genesis.go b/core/genesis.go index 550f2e43c7..ca6e112b3c 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -556,6 +556,9 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { if head.SlotNumber == nil { head.SlotNumber = new(uint64) } + if head.BlockAccessListHash == nil { + head.BlockAccessListHash = &types.EmptyBlockAccessListHash + } } } return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil)) diff --git a/core/types/hashes.go b/core/types/hashes.go index db8912a66f..5800ab68c3 100644 --- a/core/types/hashes.go +++ b/core/types/hashes.go @@ -43,6 +43,10 @@ var ( // EmptyRequestsHash is the known hash of an empty request set, sha256(""). EmptyRequestsHash = common.HexToHash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + // EmptyBlockAccessListHash is the known hash of an empty block access list, + // keccak256(rlp(empty list)) = keccak256(0xc0). + EmptyBlockAccessListHash = common.HexToHash("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") + // EmptyBinaryHash is the known hash of an empty binary trie. EmptyBinaryHash = common.Hash{} ) From 16ef6da19034f0b0024898eae98aedf266e4aac2 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 22 Apr 2026 23:52:49 +0200 Subject: [PATCH 2/4] beacon/engine: regenerate gen_ed.go to include BlockAccessList ExecutableData.BlockAccessList was added to the struct in types.go but gen_ed.go was never regenerated, so MarshalJSON/UnmarshalJSON silently dropped the field. engine_getPayloadV6 responses were emitted without blockAccessList, which Prysm rejected with "missing required field 'blockAccessList' for ExecutionPayload", preventing any block production post-genesis. Regenerated via `go generate ./beacon/engine/`. --- beacon/engine/gen_ed.go | 79 ++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index c733b3f350..ce9669c826 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executableDataMarshaling)(nil) @@ -17,24 +18,25 @@ var _ = (*executableDataMarshaling)(nil) // MarshalJSON marshals as JSON. func (e ExecutableData) MarshalJSON() ([]byte, error) { type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -59,6 +61,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.Withdrawals = e.Withdrawals enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) + enc.BlockAccessList = e.BlockAccessList enc.SlotNumber = (*hexutil.Uint64)(e.SlotNumber) return json.Marshal(&enc) } @@ -66,24 +69,25 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (e *ExecutableData) UnmarshalJSON(input []byte) error { type ExecutableData struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random *common.Hash `json:"prevRandao" gencodec:"required"` - Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash *common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -157,6 +161,9 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.ExcessBlobGas != nil { e.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) } + if dec.BlockAccessList != nil { + e.BlockAccessList = dec.BlockAccessList + } if dec.SlotNumber != nil { e.SlotNumber = (*uint64)(dec.SlotNumber) } From 9190a0826db9c2d8933f3e67ce91e77a20ae85eb Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Tue, 21 Apr 2026 16:01:39 +0200 Subject: [PATCH 3/4] core/txpool: report actual 110% threshold in intrinsic gas error When a chain has gasCostPerStateByte != 0 (EIP-8037), the txpool admission requires tx.Gas() >= ceil(intrGas.RegularGas * 10/9), but the error message reported intrGas.RegularGas as the "minimum needed", which is the unscaled value below the real threshold. This is confusing: the user sees e.g. "gas 21000, minimum needed 21000" for a simple transfer and assumes the comparison is broken, when in fact the pool wants 23334. Compute the threshold once and report it in the error. --- core/txpool/validation.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 1b65d0c80f..284fc062fc 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -134,8 +134,8 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types // We require transactions to pay for 110% of intrinsic gas in order to // prevent situations where a change in gas limit invalidates a lot // of transactions in the txpool - if tx.Gas() < (intrGas.RegularGas*10)/9 { - return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), intrGas.RegularGas) + if minGas := (intrGas.RegularGas * 10) / 9; tx.Gas() < minGas { + return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), minGas) } } if tx.Gas() < intrGas.RegularGas { From 0053f339ee96c008ff1b3145d8025bff19d16f76 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 22 Apr 2026 14:00:35 +0200 Subject: [PATCH 4/4] miner: supply a slot number when synthesising pending block post-Amsterdam getPending builds the pending block on demand via generateWork, but after EIP-7843 (#33589) prepareWork rejects the call unless generateParams.slotNum is non-nil once Amsterdam is active: if miner.chainConfig.IsAmsterdam(header.Number, header.Time) { if genParams.slotNum == nil { return nil, errors.New("no slot number set post-amsterdam") } header.SlotNumber = genParams.slotNum } getPending never populated slotNum, so on any Amsterdam-activated chain eth_getBalance(addr, "pending") (and every other RPC that resolves through the pending state) fails with "pending state is not available", breaking faucets and nonce trackers that poll pending. Fix by synthesising a slot number from the parent header when available, mirroring how the Shanghai branch above already conditionally populates withdrawals. The pending block is empty post-merge so the exact slot value is not user-visible; SlotNumber+1 (or zero) is sufficient to satisfy the prepareWork invariant. Observed on a bal-devnet-3 Geth build: every eth_getBalance(...,"pending") returned -32000 "pending state is not available" until the faucet was repointed at a non-Geth EL. Other clients (Nethermind, Besu, Reth, Erigon, Ethrex) serve pending on the same chain without issue. --- miner/miner.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/miner/miner.go b/miner/miner.go index 0ff0237a08..921200bcaa 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -151,12 +151,24 @@ func (miner *Miner) getPending() *newPayloadResult { return cached } var ( - timestamp = uint64(time.Now().Unix()) - withdrawal types.Withdrawals + timestamp = uint64(time.Now().Unix()) + childNumber = new(big.Int).Add(header.Number, big.NewInt(1)) + withdrawal types.Withdrawals + slotNum *uint64 ) - if miner.chainConfig.IsShanghai(new(big.Int).Add(header.Number, big.NewInt(1)), timestamp) { + if miner.chainConfig.IsShanghai(childNumber, timestamp) { withdrawal = []*types.Withdrawal{} } + // Post-Amsterdam, prepareWork requires a slot number (EIP-7843). The pending + // block is synthetic and has no canonical slot, so derive one from the parent + // when available and fall back to zero otherwise. + if miner.chainConfig.IsAmsterdam(childNumber, timestamp) { + var n uint64 + if header.SlotNumber != nil { + n = *header.SlotNumber + 1 + } + slotNum = &n + } ret := miner.generateWork(context.Background(), &generateParams{ timestamp: timestamp, @@ -166,6 +178,7 @@ func (miner *Miner) getPending() *newPayloadResult { random: common.Hash{}, withdrawals: withdrawal, beaconRoot: nil, + slotNum: slotNum, noTxs: false, }, false) // we will never make a witness for a pending block if ret.err != nil {