diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go
index c733b3f350..02a1fd3805 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"`
+ SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
+ BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
}
var enc ExecutableData
enc.ParentHash = e.ParentHash
@@ -60,30 +62,32 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) {
enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed)
enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas)
enc.SlotNumber = (*hexutil.Uint64)(e.SlotNumber)
+ enc.BlockAccessList = e.BlockAccessList
return json.Marshal(&enc)
}
// 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"`
+ SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
+ BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
}
var dec ExecutableData
if err := json.Unmarshal(input, &dec); err != nil {
@@ -160,5 +164,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error {
if dec.SlotNumber != nil {
e.SlotNumber = (*uint64)(dec.SlotNumber)
}
+ if dec.BlockAccessList != nil {
+ e.BlockAccessList = dec.BlockAccessList
+ }
return nil
}
diff --git a/beacon/engine/types.go b/beacon/engine/types.go
index 9b0b186df7..5c31ee4e98 100644
--- a/beacon/engine/types.go
+++ b/beacon/engine/types.go
@@ -24,6 +24,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"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
)
@@ -82,24 +83,25 @@ type payloadAttributesMarshaling struct {
// ExecutableData is the data necessary to execute an EL payload.
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 []byte `json:"logsBloom" gencodec:"required"`
- Random common.Hash `json:"prevRandao" gencodec:"required"`
- Number uint64 `json:"blockNumber" gencodec:"required"`
- GasLimit uint64 `json:"gasLimit" gencodec:"required"`
- GasUsed uint64 `json:"gasUsed" gencodec:"required"`
- Timestamp uint64 `json:"timestamp" gencodec:"required"`
- ExtraData []byte `json:"extraData" gencodec:"required"`
- BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
- BlockHash common.Hash `json:"blockHash" gencodec:"required"`
- Transactions [][]byte `json:"transactions" gencodec:"required"`
- Withdrawals []*types.Withdrawal `json:"withdrawals"`
- BlobGasUsed *uint64 `json:"blobGasUsed"`
- ExcessBlobGas *uint64 `json:"excessBlobGas"`
- SlotNumber *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 []byte `json:"logsBloom" gencodec:"required"`
+ Random common.Hash `json:"prevRandao" gencodec:"required"`
+ Number uint64 `json:"blockNumber" gencodec:"required"`
+ GasLimit uint64 `json:"gasLimit" gencodec:"required"`
+ GasUsed uint64 `json:"gasUsed" gencodec:"required"`
+ Timestamp uint64 `json:"timestamp" gencodec:"required"`
+ ExtraData []byte `json:"extraData" gencodec:"required"`
+ BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
+ BlockHash common.Hash `json:"blockHash" gencodec:"required"`
+ Transactions [][]byte `json:"transactions" gencodec:"required"`
+ Withdrawals []*types.Withdrawal `json:"withdrawals"`
+ BlobGasUsed *uint64 `json:"blobGasUsed"`
+ ExcessBlobGas *uint64 `json:"excessBlobGas"`
+ SlotNumber *uint64 `json:"slotNumber,omitempty"`
+ BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
}
// JSON type overrides for executableData.
@@ -303,56 +305,66 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H
requestsHash = &h
}
- header := &types.Header{
- ParentHash: data.ParentHash,
- UncleHash: types.EmptyUncleHash,
- Coinbase: data.FeeRecipient,
- Root: data.StateRoot,
- TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)),
- ReceiptHash: data.ReceiptsRoot,
- Bloom: types.BytesToBloom(data.LogsBloom),
- Difficulty: common.Big0,
- Number: new(big.Int).SetUint64(data.Number),
- GasLimit: data.GasLimit,
- GasUsed: data.GasUsed,
- Time: data.Timestamp,
- BaseFee: data.BaseFeePerGas,
- Extra: data.ExtraData,
- MixDigest: data.Random,
- WithdrawalsHash: withdrawalsRoot,
- ExcessBlobGas: data.ExcessBlobGas,
- BlobGasUsed: data.BlobGasUsed,
- ParentBeaconRoot: beaconRoot,
- RequestsHash: requestsHash,
- SlotNumber: data.SlotNumber,
+ // If Amsterdam is enabled, data.BlockAccessList is always non-nil,
+ // even for empty blocks with no state transitions.
+ //
+ // If Amsterdam is not enabled yet, blockAccessListHash is expected
+ // to be nil.
+ var blockAccessListHash *common.Hash
+ if data.BlockAccessList != nil {
+ hash := data.BlockAccessList.Hash()
+ blockAccessListHash = &hash
}
- return types.NewBlockWithHeader(header).
- WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}),
- nil
+ header := &types.Header{
+ ParentHash: data.ParentHash,
+ UncleHash: types.EmptyUncleHash,
+ Coinbase: data.FeeRecipient,
+ Root: data.StateRoot,
+ TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)),
+ ReceiptHash: data.ReceiptsRoot,
+ Bloom: types.BytesToBloom(data.LogsBloom),
+ Difficulty: common.Big0,
+ Number: new(big.Int).SetUint64(data.Number),
+ GasLimit: data.GasLimit,
+ GasUsed: data.GasUsed,
+ Time: data.Timestamp,
+ BaseFee: data.BaseFeePerGas,
+ Extra: data.ExtraData,
+ MixDigest: data.Random,
+ WithdrawalsHash: withdrawalsRoot,
+ ExcessBlobGas: data.ExcessBlobGas,
+ BlobGasUsed: data.BlobGasUsed,
+ ParentBeaconRoot: beaconRoot,
+ RequestsHash: requestsHash,
+ SlotNumber: data.SlotNumber,
+ BlockAccessListHash: blockAccessListHash,
+ }
+ return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), nil
}
// BlockToExecutableData constructs the ExecutableData structure by filling the
// fields from the given block. It assumes the given block is post-merge block.
func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.BlobTxSidecar, requests [][]byte) *ExecutionPayloadEnvelope {
data := &ExecutableData{
- BlockHash: block.Hash(),
- ParentHash: block.ParentHash(),
- FeeRecipient: block.Coinbase(),
- StateRoot: block.Root(),
- Number: block.NumberU64(),
- GasLimit: block.GasLimit(),
- GasUsed: block.GasUsed(),
- BaseFeePerGas: block.BaseFee(),
- Timestamp: block.Time(),
- ReceiptsRoot: block.ReceiptHash(),
- LogsBloom: block.Bloom().Bytes(),
- Transactions: encodeTransactions(block.Transactions()),
- Random: block.MixDigest(),
- ExtraData: block.Extra(),
- Withdrawals: block.Withdrawals(),
- BlobGasUsed: block.BlobGasUsed(),
- ExcessBlobGas: block.ExcessBlobGas(),
- SlotNumber: block.SlotNumber(),
+ BlockHash: block.Hash(),
+ ParentHash: block.ParentHash(),
+ FeeRecipient: block.Coinbase(),
+ StateRoot: block.Root(),
+ Number: block.NumberU64(),
+ GasLimit: block.GasLimit(),
+ GasUsed: block.GasUsed(),
+ BaseFeePerGas: block.BaseFee(),
+ Timestamp: block.Time(),
+ ReceiptsRoot: block.ReceiptHash(),
+ LogsBloom: block.Bloom().Bytes(),
+ Transactions: encodeTransactions(block.Transactions()),
+ Random: block.MixDigest(),
+ ExtraData: block.Extra(),
+ Withdrawals: block.Withdrawals(),
+ BlobGasUsed: block.BlobGasUsed(),
+ ExcessBlobGas: block.ExcessBlobGas(),
+ SlotNumber: block.SlotNumber(),
+ BlockAccessList: block.AccessList(),
}
// Add blobs.
diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index a2de58ad46..043e675494 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -35,6 +35,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto/keccak"
"github.com/ethereum/go-ethereum/ethdb"
@@ -172,6 +173,9 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
includedTxs types.Transactions
blobGasUsed = uint64(0)
receipts = make(types.Receipts, 0)
+
+ // TODO return blockAccessList as a part of result
+ blockAccessList = bal.NewConstructionBlockAccessList()
)
vmContext := vm.BlockContext{
CanTransfer: core.CanTransfer,
@@ -231,14 +235,14 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
}
evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig)
if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
+ core.ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList)
}
if pre.Env.BlockHashes != nil && chainConfig.IsPrague(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) {
var (
prevNumber = pre.Env.Number - 1
prevHash = pre.Env.BlockHashes[math.HexOrDecimal64(prevNumber)]
)
- core.ProcessParentBlockHash(prevHash, evm)
+ core.ProcessParentBlockHash(prevHash, evm, blockAccessList)
}
for i := 0; txIt.Next(); i++ {
tx, err := txIt.Tx()
@@ -271,11 +275,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
}
}
statedb.SetTxContext(tx.Hash(), len(receipts), uint32(len(receipts)+1))
+
var (
snapshot = statedb.Snapshot()
gp = gaspool.Snapshot()
)
- receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm)
+ receipt, bal, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm)
if err != nil {
statedb.RevertToSnapshot(snapshot)
log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err)
@@ -292,6 +297,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
}
blobGasUsed += txBlobGas
receipts = append(receipts, receipt)
+ blockAccessList.Merge(bal)
}
statedb.IntermediateRoot(chainConfig.IsEIP158(vmContext.BlockNumber))
@@ -336,10 +342,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
for _, receipt := range receipts {
allLogs = append(allLogs, receipt.Logs...)
}
- requests, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1))
+ requests, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1))
if err != nil {
return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err))
}
+ blockAccessList.Merge(bal)
+
// Commit block
root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber), chainConfig.IsCancun(vmContext.BlockNumber, vmContext.Time))
if err != nil {
diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go
index 72ac75c036..4237418e73 100644
--- a/consensus/beacon/consensus.go
+++ b/consensus/beacon/consensus.go
@@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
@@ -342,9 +343,9 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H
}
// Finalize implements consensus.Engine and processes withdrawals on top.
-func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) {
+func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) {
if !beacon.IsPoSHeader(header) {
- beacon.ethone.Finalize(chain, header, state, body)
+ beacon.ethone.Finalize(chain, header, state, body, blockAccessIndex, bal)
return
}
// Withdrawals processing.
@@ -352,7 +353,20 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.
// Convert amount from gwei to wei.
amount := new(uint256.Int).SetUint64(w.Amount)
amount = amount.Mul(amount, uint256.NewInt(params.GWei))
- state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal)
+ prev := state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal)
+
+ // Populate the block-level accessList if Amsterdam is enabled
+ if chain.Config().IsAmsterdam(header.Number, header.Time) {
+ if w.Amount == 0 {
+ // Zero amount withdrawal, account is accessed potential
+ // without state changes.
+ bal.AccountRead(w.Address)
+ } else {
+ // Non-zero amount withdrawal, account is accessed with
+ // a balance change.
+ bal.BalanceChange(blockAccessIndex, w.Address, new(uint256.Int).Add(&prev, amount))
+ }
+ }
}
// No block reward which is issued by consensus layer instead.
}
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go
index ceaec44656..f44afde241 100644
--- a/consensus/clique/clique.go
+++ b/consensus/clique/clique.go
@@ -34,6 +34,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/keccak"
@@ -573,7 +574,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header
// Finalize implements consensus.Engine. There is no post-transaction
// consensus rules in clique, do nothing here.
-func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) {
+func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) {
// No block rewards in PoA, so the state remains as is
}
diff --git a/consensus/consensus.go b/consensus/consensus.go
index 4ba389292f..e4f7b7a6a1 100644
--- a/consensus/consensus.go
+++ b/consensus/consensus.go
@@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
)
@@ -79,12 +80,12 @@ type Engine interface {
// rules of a particular engine. The changes are executed inline.
Prepare(chain ChainHeaderReader, header *types.Header) error
- // Finalize runs any post-transaction state modifications (e.g. block rewards
- // or process withdrawals) but does not assemble the block.
+ // Finalize runs any post-transaction consensus-specific state modifications
+ // (e.g. block rewards or process withdrawals) but does not assemble the block.
//
// Note: The state database might be updated to reflect any consensus rules
// that happen at finalization (e.g. block rewards).
- Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body)
+ Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList)
// Seal generates a new sealing request for the given input block and pushes
// the result into the given channel.
diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go
index ee9d9d97d6..21adc9d279 100644
--- a/consensus/ethash/consensus.go
+++ b/consensus/ethash/consensus.go
@@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto/keccak"
"github.com/ethereum/go-ethereum/params"
@@ -504,7 +505,7 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H
}
// Finalize implements consensus.Engine, accumulating the block and uncle rewards.
-func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) {
+func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) {
// Accumulate any block and uncle rewards
accumulateRewards(chain.Config(), state, header, body.Uncles)
}
diff --git a/core/bal_test.go b/core/bal_test.go
new file mode 100644
index 0000000000..f0b9dc6443
--- /dev/null
+++ b/core/bal_test.go
@@ -0,0 +1,1319 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package core
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "maps"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/consensus/beacon"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
+)
+
+// EIP-7928 BAL inclusion tests.
+//
+// Each test exercises a single rule from the spec and asserts both presence
+// and absence in the resulting block access list.
+
+// balChainConfig returns a MergedTestChainConfig clone with Amsterdam active from genesis.
+func balChainConfig() *params.ChainConfig {
+ cfg := *params.MergedTestChainConfig
+ cfg.AmsterdamTime = new(uint64)
+ blob := *cfg.BlobScheduleConfig
+ blob.Amsterdam = blob.Osaka
+ cfg.BlobScheduleConfig = &blob
+ return &cfg
+}
+
+// balTestEnv bundles common identities used across the tests.
+type balTestEnv struct {
+ cfg *params.ChainConfig
+ signer types.Signer
+ key *ecdsa.PrivateKey
+ from common.Address
+ gspec *Genesis
+}
+
+// newBALTestEnv builds an Amsterdam chain config, funds a sender and pre-deploys
+// the EIP-7928 system contracts. Extra accounts can be merged into Alloc.
+func newBALTestEnv(extra types.GenesisAlloc) *balTestEnv {
+ cfg := balChainConfig()
+ key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ from := crypto.PubkeyToAddress(key.PublicKey)
+
+ alloc := types.GenesisAlloc{
+ from: {Balance: newGwei(1_000_000_000)},
+ params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0},
+ params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},
+ params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0},
+ params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0},
+ }
+ maps.Copy(alloc, extra)
+ return &balTestEnv{
+ cfg: cfg,
+ signer: types.LatestSigner(cfg),
+ key: key,
+ from: from,
+ gspec: &Genesis{Config: cfg, Alloc: alloc},
+ }
+}
+
+// run generates exactly one Amsterdam block and returns its BAL.
+func (e *balTestEnv) run(t *testing.T, gen func(*BlockGen)) (*bal.BlockAccessList, types.Receipts) {
+ t.Helper()
+ engine := beacon.New(ethash.NewFaker())
+ _, blocks, receipts := GenerateChainWithGenesis(e.gspec, engine, 1, func(_ int, b *BlockGen) {
+ gen(b)
+ })
+ if blocks[0].AccessList() == nil {
+ t.Fatal("expected non-nil block access list")
+ }
+ return blocks[0].AccessList(), receipts[0]
+}
+
+// --- assertion helpers ---
+
+func findAccount(b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess {
+ for i := range *b {
+ if (*b)[i].Address == addr {
+ return &(*b)[i]
+ }
+ }
+ return nil
+}
+
+func hasSlotIn(slots []*uint256.Int, key common.Hash) bool {
+ want := new(uint256.Int).SetBytes(key[:])
+ for _, s := range slots {
+ if s.Cmp(want) == 0 {
+ return true
+ }
+ }
+ return false
+}
+
+func hasStorageWrite(b *bal.BlockAccessList, addr common.Address, key common.Hash) bool {
+ aa := findAccount(b, addr)
+ if aa == nil {
+ return false
+ }
+ want := new(uint256.Int).SetBytes(key[:])
+ for _, w := range aa.StorageWrites {
+ if w.Slot.Cmp(want) == 0 {
+ return true
+ }
+ }
+ return false
+}
+
+func assertPresent(t *testing.T, b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess {
+ t.Helper()
+ aa := findAccount(b, addr)
+ if aa == nil {
+ t.Fatalf("address %x missing from BAL\n%s", addr, b.PrettyPrint())
+ }
+ return aa
+}
+
+func assertAbsent(t *testing.T, b *bal.BlockAccessList, addr common.Address) {
+ t.Helper()
+ if findAccount(b, addr) != nil {
+ t.Fatalf("address %x must NOT be in BAL\n%s", addr, b.PrettyPrint())
+ }
+}
+
+func assertEmpty(t *testing.T, aa *bal.AccountAccess) {
+ t.Helper()
+ if len(aa.StorageWrites) != 0 || len(aa.StorageReads) != 0 ||
+ len(aa.BalanceChanges) != 0 || len(aa.NonceChanges) != 0 || len(aa.CodeChanges) != 0 {
+ t.Fatalf("expected empty change set for %x, got %+v", aa.Address, aa)
+ }
+}
+
+// --- tx builders ---
+
+func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction {
+ return types.MustSignNewTx(e.key, e.signer, &types.DynamicFeeTx{
+ ChainID: e.cfg.ChainID,
+ Nonce: nonce,
+ To: to,
+ Value: value,
+ Gas: gas,
+ GasFeeCap: newGwei(10),
+ GasTipCap: newGwei(tipGwei),
+ Data: data,
+ })
+}
+
+// ============================== Account inclusion ==============================
+
+// TestBALTxSenderAndRecipient: a value transfer records balance+nonce for sender
+// and a balance entry for the recipient.
+func TestBALTxSenderAndRecipient(t *testing.T) {
+ to := common.HexToAddress("0xc0ffee")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &to, big.NewInt(1000), params.TxGas, 0, nil))
+ })
+
+ sender := assertPresent(t, b, env.from)
+ if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("sender nonce not bumped: %+v", sender.NonceChanges)
+ }
+ if len(sender.BalanceChanges) == 0 {
+ t.Fatalf("sender missing balance change")
+ }
+ recipient := assertPresent(t, b, to)
+ if len(recipient.BalanceChanges) != 1 || recipient.BalanceChanges[0].Balance.Uint64() != 1000 {
+ t.Fatalf("recipient balance: %+v", recipient.BalanceChanges)
+ }
+}
+
+// TestBALZeroValueRecipient: a tx with value 0 still lists the recipient,
+// but without a balance entry.
+func TestBALZeroValueRecipient(t *testing.T) {
+ to := common.HexToAddress("0x0123456789abcdef")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil))
+ })
+
+ r := assertPresent(t, b, to)
+ if len(r.BalanceChanges) != 0 {
+ t.Fatalf("zero-value recipient should have no balance entry: %+v", r.BalanceChanges)
+ }
+}
+
+// TestBALEmptyBlockExcludesCoinbase: an empty block (no txs, no withdrawals)
+// never touches the coinbase, so it must NOT appear in the BAL — the zero
+// block reward alone does not trigger inclusion.
+func TestBALEmptyBlockExcludesCoinbase(t *testing.T) {
+ coinbase := common.Address{0xc0}
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ // SetCoinbase initialises b.bal but does not record any access.
+ g.SetCoinbase(coinbase)
+ })
+ assertAbsent(t, b, coinbase)
+}
+
+// TestBALCoinbaseTipCapturesBalance: positive priority fee credits coinbase
+// and the balance change appears in the BAL.
+func TestBALCoinbaseTipCapturesBalance(t *testing.T) {
+ coinbase := common.Address{0xc0}
+ to := common.HexToAddress("0xabba")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.SetCoinbase(coinbase)
+ g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 2 /* gwei tip */, nil))
+ })
+
+ cb := assertPresent(t, b, coinbase)
+ if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].Balance.Sign() == 0 {
+ t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges)
+ }
+}
+
+// TestBALSystemAddressExcluded: SYSTEM_ADDRESS (0xff…fe) is not in the BAL
+// for a regular block.
+func TestBALSystemAddressExcluded(t *testing.T) {
+ to := common.HexToAddress("0xabba")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil))
+ })
+ assertAbsent(t, b, params.SystemAddress)
+}
+
+// TestBALSystemAddressIncludedWhenTouched: SYSTEM_ADDRESS becomes a regular
+// account in the BAL once it experiences state access (here: receives value).
+func TestBALSystemAddressIncludedWhenTouched(t *testing.T) {
+ sys := params.SystemAddress
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &sys, big.NewInt(1000), params.TxGas, 0, nil))
+ })
+
+ aa := assertPresent(t, b, sys)
+ if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 1000 {
+ t.Fatalf("system-address balance change missing: %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALPrecompileInvokedFromContractIncluded: a precompile that is invoked
+// indirectly — via STATICCALL from a regular contract — must still appear in
+// the BAL with no balance entry.
+func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) {
+ identity := common.BytesToAddress([]byte{0x04})
+ caller := common.HexToAddress("0xca11")
+ // PUSH1 0 (retSize) PUSH1 0 (retOff) PUSH1 0 (argsSize) PUSH1 0 (argsOff)
+ // PUSH20 0x04 GAS STATICCALL POP STOP
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
+ code = append(code, identity.Bytes()...)
+ code = append(code, 0x5a, 0xfa, 0x50, 0x00)
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, identity)
+ if len(aa.BalanceChanges) != 0 {
+ t.Fatalf("precompile invoked via STATICCALL must not record balance: %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALPrecompileCalledNoValueIncluded: a tx targeting the identity precompile
+// with zero value lists the precompile but records no balance entry.
+func TestBALPrecompileCalledNoValueIncluded(t *testing.T) {
+ identity := common.BytesToAddress([]byte{0x04})
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &identity, big.NewInt(0), 50_000, 0, []byte{0xde, 0xad}))
+ })
+
+ aa := assertPresent(t, b, identity)
+ if len(aa.BalanceChanges) != 0 {
+ t.Fatalf("precompile must not record balance change: %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALPrecompileValueTransferRecordsBalance: a precompile receives ETH only
+// in the form of a value transfer — the balance entry is then recorded.
+func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) {
+ identity := common.BytesToAddress([]byte{0x04})
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &identity, big.NewInt(5), 50_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, identity)
+ if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 5 {
+ t.Fatalf("precompile balance change wrong: %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALBalanceProbeOnNonExistent: BALANCE against a never-allocated address
+// still adds it to the BAL with an empty change set.
+func TestBALBalanceProbeOnNonExistent(t *testing.T) {
+ probe := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ caller := common.HexToAddress("0xc1")
+ code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
+ code = append(code, 0x31, 0x50, 0x00) // BALANCE, POP, STOP
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, probe))
+}
+
+// TestBALExtCodeSizeProbeOnNonExistent: EXTCODESIZE against a never-allocated
+// address adds it to the BAL with an empty change set.
+func TestBALExtCodeSizeProbeOnNonExistent(t *testing.T) {
+ probe := common.HexToAddress("0xcafecafecafecafecafecafecafecafecafecafe")
+ caller := common.HexToAddress("0xc1")
+ code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
+ code = append(code, 0x3b, 0x50, 0x00) // EXTCODESIZE, POP, STOP
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, probe))
+}
+
+// TestBALExtCodeHashProbeOnNonExistent: EXTCODEHASH against a never-allocated
+// address adds it to the BAL with an empty change set.
+func TestBALExtCodeHashProbeOnNonExistent(t *testing.T) {
+ probe := common.HexToAddress("0xfacefacefacefacefacefacefacefacefacefacE")
+ caller := common.HexToAddress("0xc1")
+ code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
+ code = append(code, 0x3f, 0x50, 0x00) // EXTCODEHASH, POP, STOP
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, probe))
+}
+
+// TestBALExtCodeCopyProbeOnNonExistent: EXTCODECOPY against a never-allocated
+// address adds it to the BAL with an empty change set.
+func TestBALExtCodeCopyProbeOnNonExistent(t *testing.T) {
+ probe := common.HexToAddress("0xfeedfeedfeedfeedfeedfeedfeedfeedfeedfeed")
+ caller := common.HexToAddress("0xc1")
+ // PUSH1 0 (length) PUSH1 0 (codeOffset) PUSH1 0 (destOffset)
+ // PUSH20 probe EXTCODECOPY STOP
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
+ code = append(code, probe.Bytes()...)
+ code = append(code, 0x3c, 0x00) // EXTCODECOPY, STOP
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, probe))
+}
+
+// TestBALAccessListNotAutoPromoted: an EIP-2930 access-list entry that is
+// never actually touched must NOT appear in the BAL.
+func TestBALAccessListNotAutoPromoted(t *testing.T) {
+ to := common.HexToAddress("0xabba")
+ dormant := common.HexToAddress("0xd0d0")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ tx := types.MustSignNewTx(env.key, env.signer, &types.DynamicFeeTx{
+ ChainID: env.cfg.ChainID,
+ Nonce: 0,
+ To: &to,
+ Value: big.NewInt(0),
+ Gas: params.TxGas + 4000,
+ GasFeeCap: newGwei(10),
+ GasTipCap: newGwei(0),
+ AccessList: types.AccessList{{Address: dormant, StorageKeys: nil}},
+ })
+ g.AddTx(tx)
+ })
+
+ assertAbsent(t, b, dormant)
+}
+
+// ============================== CALL family ==============================
+
+// makeStubCaller emits a single CALL-family op against `target` then STOPs,
+// with zero call data and discarded return data.
+//
+// op = 0xf1 (CALL) / 0xf2 (CALLCODE):
+// stack = retSize, retOff, argsSize, argsOff, value, addr, gas
+// op = 0xf4 (DELEGATECALL) / 0xfa (STATICCALL):
+// stack = retSize, retOff, argsSize, argsOff, addr, gas
+func makeStubCaller(op byte, target common.Address) []byte {
+ // retSize, retOff, argsSize, argsOff = 0
+ prelude := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00}
+ if op == 0xf1 || op == 0xf2 { // CALL/CALLCODE need an extra value=0
+ prelude = append(prelude, 0x60, 0x00)
+ }
+ prelude = append(prelude, 0x73) // PUSH20
+ prelude = append(prelude, target.Bytes()...)
+ prelude = append(prelude, 0x5a) // GAS
+ prelude = append(prelude, op)
+ prelude = append(prelude, 0x50, 0x00) // POP, STOP
+ return prelude
+}
+
+// TestBALCallTargetWithEmptyChangeSet: a zero-value CALL to an existing
+// contract that has no state changes lists the target with empty entries.
+func TestBALCallTargetWithEmptyChangeSet(t *testing.T) {
+ target := common.HexToAddress("0xbabe")
+ env := newBALTestEnv(types.GenesisAlloc{
+ target: {Code: []byte{0x00}, Balance: common.Big0}, // STOP
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &target, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, target))
+}
+
+// TestBALCallCodeTargetIncluded: CALLCODE puts the target in the BAL with an
+// empty change set (CALLCODE executes target's code in the caller's storage
+// context, so the target itself records no state changes).
+func TestBALCallCodeTargetIncluded(t *testing.T) {
+ target := common.HexToAddress("0xdeed")
+ caller := common.HexToAddress("0xca11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeStubCaller(0xf2 /* CALLCODE */, target), Balance: common.Big0},
+ target: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ assertPresent(t, b, caller)
+ assertEmpty(t, assertPresent(t, b, target))
+}
+
+// TestBALDelegateCallTargetIncluded: DELEGATECALL puts both caller and target
+// in the BAL even when neither produces state changes.
+func TestBALDelegateCallTargetIncluded(t *testing.T) {
+ target := common.HexToAddress("0xdeed")
+ caller := common.HexToAddress("0xca11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeStubCaller(0xf4 /* DELEGATECALL */, target), Balance: common.Big0},
+ target: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ assertPresent(t, b, caller)
+ assertEmpty(t, assertPresent(t, b, target))
+}
+
+// TestBALStaticCallTargetIncluded: STATICCALL puts the target in the BAL with
+// no balance entry recorded.
+func TestBALStaticCallTargetIncluded(t *testing.T) {
+ target := common.HexToAddress("0xdeed")
+ caller := common.HexToAddress("0xca11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeStubCaller(0xfa /* STATICCALL */, target), Balance: common.Big0},
+ target: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ assertPresent(t, b, caller)
+ assertEmpty(t, assertPresent(t, b, target))
+}
+
+// ============================== Revert behaviour ==============================
+
+// TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still
+// records the touched contract in the BAL with an empty change set.
+func TestBALRevertedTxStillIncluded(t *testing.T) {
+ reverter := common.HexToAddress("0xbeef")
+ // PUSH1 0 PUSH1 0 REVERT
+ revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
+ env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, reverter))
+}
+
+// TestBALSenderRecordedOnRevert: even when the top-level call reverts, the
+// sender's final nonce and balance MUST be recorded.
+func TestBALSenderRecordedOnRevert(t *testing.T) {
+ reverter := common.HexToAddress("0xbeef")
+ revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
+ env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ sender := assertPresent(t, b, env.from)
+ if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("sender nonce must be bumped even on revert: %+v", sender.NonceChanges)
+ }
+ if len(sender.BalanceChanges) == 0 {
+ t.Fatalf("sender balance change (gas paid) must be present on revert")
+ }
+}
+
+// ============================== Storage inclusion ==============================
+
+// TestBALStorageWriteRecorded: SSTORE places the slot in storage_changes and
+// keeps it out of storage_reads.
+func TestBALStorageWriteRecorded(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x01))
+ // PUSH1 0x42 PUSH1 0x01 SSTORE STOP
+ code := []byte{0x60, 0x42, 0x60, 0x01, 0x55, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasStorageWrite(b, contract, slot) {
+ t.Fatalf("expected slot 0x01 in storage_changes\n%s", b.PrettyPrint())
+ }
+ if hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("slot 0x01 must NOT appear in storage_reads")
+ }
+}
+
+// TestBALStorageSloadOnly: SLOAD without a write puts the slot in storage_reads.
+func TestBALStorageSloadOnly(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x07))
+ // PUSH1 0x07 SLOAD POP STOP
+ code := []byte{0x60, 0x07, 0x54, 0x50, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("expected slot in storage_reads\n%s", b.PrettyPrint())
+ }
+ if hasStorageWrite(b, contract, slot) {
+ t.Fatalf("slot must NOT appear in storage_changes")
+ }
+}
+
+// TestBALStorageReadThenWriteOnlyInWrites: SLOAD followed by SSTORE on the
+// same slot drops the slot from storage_reads (write-wins invariant).
+func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x05))
+ // PUSH1 5 SLOAD POP PUSH1 0x42 PUSH1 5 SSTORE STOP
+ code := []byte{
+ 0x60, 0x05, 0x54, 0x50,
+ 0x60, 0x42, 0x60, 0x05, 0x55,
+ 0x00,
+ }
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasStorageWrite(b, contract, slot) {
+ t.Fatalf("slot must be in storage_changes\n%s", b.PrettyPrint())
+ }
+ if hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("slot must NOT appear in storage_reads (write-wins)\n%s", b.PrettyPrint())
+ }
+}
+
+// TestBALNoOpSSTOREDemotesToRead: an SSTORE whose value equals the committed
+// value lands the slot in storage_reads only.
+func TestBALNoOpSSTOREDemotesToRead(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x09))
+ // SSTORE(0x09, 0x42) — slot pre-state is 0x42, so the write is a no-op.
+ code := []byte{0x60, 0x42, 0x60, 0x09, 0x55, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{
+ contract: {
+ Code: code,
+ Balance: common.Big0,
+ Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))},
+ },
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("no-op SSTORE should leave slot in storage_reads\n%s", b.PrettyPrint())
+ }
+ if hasStorageWrite(b, contract, slot) {
+ t.Fatalf("no-op SSTORE must NOT register a write")
+ }
+}
+
+// TestBALStorageWriteZeroIsAWrite: writing 0 to a non-zero slot is still a
+// state change and lands in storage_changes.
+func TestBALStorageWriteZeroIsAWrite(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x03))
+ // PUSH1 0 PUSH1 3 SSTORE STOP
+ code := []byte{0x60, 0x00, 0x60, 0x03, 0x55, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{
+ contract: {
+ Code: code,
+ Balance: common.Big0,
+ Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))},
+ },
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasStorageWrite(b, contract, slot) {
+ t.Fatalf("SSTORE to zero must record a write\n%s", b.PrettyPrint())
+ }
+ for _, w := range aa.StorageWrites {
+ if w.Slot.Uint64() == 0x03 {
+ if len(w.Accesses) != 1 || !w.Accesses[0].ValueAfter.IsZero() {
+ t.Fatalf("expected post-value 0 for slot 0x03, got %+v", w.Accesses)
+ }
+ }
+ }
+}
+
+// ============================== CREATE / contract deployment ==============================
+
+// TestBALCreateDeploysCode: a successful contract-creation tx records the new
+// address with nonce 0→1, a balance entry (value transferred), and a code entry.
+func TestBALCreateDeploysCode(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // Init: deploy runtime [0x00] (single STOP byte).
+ // PUSH1 0 PUSH1 0 MSTORE8 PUSH1 1 PUSH1 0 RETURN
+ init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3}
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(7), 200_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ aa := assertPresent(t, b, created)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 1 || !bytes.Equal(aa.CodeChanges[0].Code, []byte{0x00}) {
+ t.Fatalf("expected code [0x00], got %+v", aa.CodeChanges)
+ }
+ if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 7 {
+ t.Fatalf("expected balance 7, got %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALCreateEmptyRuntimeNoCodeEntry: when init code returns 0 bytes the
+// new address is still listed with nonce 0→1 but no code entry.
+func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // Init: PUSH1 0 PUSH1 0 RETURN → returns 0 bytes
+ init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ aa := assertPresent(t, b, created)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("empty runtime must NOT record a code entry, got %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALCreateInitRevertEmptyChangeSet: when init code reverts, the would-be
+// contract address is in the BAL with an empty change set.
+func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // PUSH1 0 PUSH1 0 REVERT
+ init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ assertEmpty(t, assertPresent(t, b, created))
+}
+
+// TestBALCreateInitOOGEmptyChangeSet: init code that runs out of gas leaves
+// the deployed address in the BAL with an empty change set.
+func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG.
+ init := []byte{0x5b, 0x60, 0x00, 0x56}
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 60_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ assertEmpty(t, assertPresent(t, b, created))
+}
+
+// TestBALCreateAddressCollisionStillIncluded: when CREATE targets an address
+// that already holds a contract, the deployment fails but the address was
+// probed during execution and MUST appear in the BAL with an empty change set.
+func TestBALCreateAddressCollisionStillIncluded(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // For a top-level CREATE tx the deployed address is CreateAddress(sender, 0).
+ // Pre-allocate a contract at that address to provoke ErrContractAddressCollision.
+ collide := crypto.CreateAddress(env.from, 0)
+ env.gspec.Alloc[collide] = types.Account{
+ Nonce: 1,
+ Code: []byte{0x00},
+ Balance: common.Big0,
+ }
+
+ // Init code doesn't matter — execution never starts.
+ init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ })
+
+ aa := assertPresent(t, b, collide)
+ // The address must be present but the pre-existing nonce/code MUST NOT
+ // be overwritten by the failed creation.
+ if len(aa.NonceChanges) != 0 {
+ t.Fatalf("collision must not bump nonce: %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("collision must not write code: %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALInEVMCreatePreAccessAbortDestinationExcluded: if a CREATE frame
+// aborts BEFORE the destination is read from state (here: the caller has 0
+// balance and CREATE requests value > 0, tripping evm.create's CanTransfer
+// check before GetCodeHash), the would-be address MUST NOT appear in the
+// BAL — only "if target account is accessed" qualifies for inclusion.
+func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) {
+ factory := common.HexToAddress("0xfac4")
+ // PUSH1 0 (length) PUSH1 0 (offset) PUSH1 1 (value) CREATE POP STOP
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0xf0, 0x50, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{
+ factory: {Code: code, Balance: common.Big0, Nonce: 1}, // factory has no balance
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &factory, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ // The address that WOULD have been deployed had the create succeeded.
+ wouldBeDest := crypto.CreateAddress(factory, 1)
+ assertAbsent(t, b, wouldBeDest)
+
+ // The factory itself is in BAL (it ran), but its nonce MUST NOT have been
+ // bumped because evm.create returned before the SetNonce call.
+ aa := assertPresent(t, b, factory)
+ if len(aa.NonceChanges) != 0 {
+ t.Fatalf("factory nonce must not be bumped on pre-access abort: %+v", aa.NonceChanges)
+ }
+}
+
+// TestBALInEVMCreateDeploysContract: a CREATE issued by an existing contract
+// (not a top-level CREATE tx) records the deployed address in the BAL.
+func TestBALInEVMCreateDeploysContract(t *testing.T) {
+ factory := common.HexToAddress("0xfac4")
+ // Factory code:
+ // Write 5-byte init code (0x60 0x00 0x60 0x00 0xf3) into memory starting at offset 0.
+ // Then CREATE(value=0, offset=0, length=5).
+ //
+ // Layout: store the init code as a single 32-byte word at offset 0 via MSTORE
+ // with leftmost 27 bytes garbage, then call CREATE with offset = 27, length = 5.
+ initBlob := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
+ var word [32]byte
+ copy(word[32-len(initBlob):], initBlob)
+ code := []byte{0x7f} // PUSH32
+ code = append(code, word[:]...)
+ code = append(code, 0x60, 0x00, 0x52) // PUSH1 0, MSTORE
+ // CREATE expects [value, offset, length] with value on bottom of stack.
+ code = append(code,
+ 0x60, 0x05, // PUSH1 5 (length)
+ 0x60, 0x1b, // PUSH1 27 (offset)
+ 0x60, 0x00, // PUSH1 0 (value)
+ 0xf0, // CREATE
+ 0x00, // STOP (discard result)
+ )
+
+ env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &factory, big.NewInt(0), 300_000, 0, nil))
+ })
+
+ // Deployed address depends on the factory's nonce at the moment of CREATE,
+ // which is the factory's genesis nonce (1).
+ deployed := crypto.CreateAddress(factory, 1)
+ aa := assertPresent(t, b, deployed)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("deployed contract nonce: %+v", aa.NonceChanges)
+ }
+}
+
+// ============================== SELFDESTRUCT ==============================
+
+// TestBALSelfDestructBeneficiaryWithZeroBalance: SELFDESTRUCT to a fresh
+// beneficiary when the destructing account has 0 balance — both addresses are
+// listed with empty change sets (no balance entry).
+func TestBALSelfDestructBeneficiaryWithZeroBalance(t *testing.T) {
+ beneficiary := common.HexToAddress("0xbeefbeef")
+ env := newBALTestEnv(nil)
+ // Init code performs SELFDESTRUCT to beneficiary inside the constructor,
+ // so EIP-6780's same-tx requirement is satisfied. The destructing account
+ // starts with balance 0 because the creation tx sends 0 value.
+ // PUSH20 SELFDESTRUCT
+ init := append([]byte{0x73}, beneficiary.Bytes()...)
+ init = append(init, 0xff)
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ ben := assertPresent(t, b, beneficiary)
+ if len(ben.BalanceChanges) != 0 {
+ t.Fatalf("zero-value SELFDESTRUCT must not credit beneficiary: %+v", ben.BalanceChanges)
+ }
+ cc := assertPresent(t, b, created)
+ if len(cc.BalanceChanges) != 0 {
+ t.Fatalf("destructing contract must not record a balance entry: %+v", cc.BalanceChanges)
+ }
+}
+
+// TestBALSelfDestructBeneficiaryWithValueTransfer: SELFDESTRUCT from a freshly
+// created contract that received positive value — beneficiary records the
+// credit; destructing account's balance entry is omitted because its
+// pre-transaction balance was 0.
+func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) {
+ beneficiary := common.HexToAddress("0xbeefbeef")
+ env := newBALTestEnv(nil)
+ // Init code: PUSH20 SELFDESTRUCT
+ init := append([]byte{0x73}, beneficiary.Bytes()...)
+ init = append(init, 0xff)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(100), 200_000, 0, init))
+ })
+
+ ben := assertPresent(t, b, beneficiary)
+ if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 100 {
+ t.Fatalf("beneficiary balance must be credited with 100: %+v", ben.BalanceChanges)
+ }
+}
+
+// TestBALSelfDestructPreExistingContract: SELFDESTRUCT on a pre-existing
+// contract with positive balance records balance→0 for the contract and the
+// corresponding credit on the beneficiary. EIP-6780 means the contract is
+// only credited and not deleted, but its balance moves regardless.
+func TestBALSelfDestructPreExistingContract(t *testing.T) {
+ suicidal := common.HexToAddress("0x5e1f")
+ beneficiary := common.HexToAddress("0xbeefbeef")
+ // PUSH20 SELFDESTRUCT
+ code := append([]byte{0x73}, beneficiary.Bytes()...)
+ code = append(code, 0xff)
+
+ env := newBALTestEnv(types.GenesisAlloc{
+ suicidal: {Code: code, Balance: big.NewInt(50)},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, suicidal)
+ if len(aa.BalanceChanges) != 1 || !aa.BalanceChanges[0].Balance.IsZero() {
+ t.Fatalf("suicidal contract balance should drop to 0: %+v", aa.BalanceChanges)
+ }
+ ben := assertPresent(t, b, beneficiary)
+ if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 50 {
+ t.Fatalf("beneficiary should receive 50: %+v", ben.BalanceChanges)
+ }
+}
+
+// ============================== Mid-tx balance round-trip ==============================
+
+// TestBALMidTxBalanceRoundTrip: when an address's balance changes during a
+// transaction but returns to its pre-transaction value, the address is still
+// listed in the BAL but MUST NOT have a balance entry.
+func TestBALMidTxBalanceRoundTrip(t *testing.T) {
+ bouncer := common.HexToAddress("0xb0unce")
+ // On receiving value, the bouncer immediately CALLs CALLER with CALLVALUE
+ // and zero data. Net effect: bouncer.balance returns to its pre-tx value.
+ //
+ // PUSH1 0 (retSize)
+ // PUSH1 0 (retOff)
+ // PUSH1 0 (argsSize)
+ // PUSH1 0 (argsOff)
+ // CALLVALUE
+ // CALLER
+ // GAS
+ // CALL
+ // POP
+ // STOP
+ code := []byte{
+ 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00,
+ 0x34, // CALLVALUE
+ 0x33, // CALLER
+ 0x5a, // GAS
+ 0xf1, // CALL
+ 0x50, // POP
+ 0x00, // STOP
+ }
+ env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 200_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, bouncer)
+ if len(aa.BalanceChanges) != 0 {
+ t.Fatalf("mid-tx round-trip must not record a balance entry: %+v", aa.BalanceChanges)
+ }
+}
+
+// ============================== System contracts (pre/post-execution) ==============================
+
+// TestBALSystemContractsPresent: per EIP-7928, "System contract addresses
+// accessed during pre/post-execution" MUST be included in the BAL. That
+// means all four of the post-merge system contracts touched by every
+// Amsterdam block:
+//
+// - EIP-4788 beacon roots (pre-execution, when ParentBeaconRoot is set)
+// - EIP-2935 history storage (pre-execution)
+// - EIP-7002 withdrawal queue (post-execution)
+// - EIP-7251 consolidation queue (post-execution)
+func TestBALSystemContractsPresent(t *testing.T) {
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ // SetCoinbase initialises b.bal; SetParentBeaconRoot triggers EIP-4788.
+ g.SetCoinbase(common.Address{0xc0})
+ g.SetParentBeaconRoot(common.Hash{0xbe, 0xac})
+ })
+
+ for _, sys := range []struct {
+ name string
+ addr common.Address
+ }{
+ {"BeaconRoots (4788)", params.BeaconRootsAddress},
+ {"HistoryStorage (2935)", params.HistoryStorageAddress},
+ {"WithdrawalQueue (7002)", params.WithdrawalQueueAddress},
+ {"ConsolidationQueue (7251)", params.ConsolidationQueueAddress},
+ } {
+ if findAccount(b, sys.addr) == nil {
+ t.Errorf("%s (%x) MUST appear in BAL but is missing\n%s", sys.name, sys.addr, b.PrettyPrint())
+ }
+ }
+}
+
+// ============================== Withdrawals ==============================
+
+// TestBALWithdrawalZeroAmountIncluded: a withdrawal with amount 0 still puts
+// the recipient in the BAL (with no balance entry).
+func TestBALWithdrawalZeroAmountIncluded(t *testing.T) {
+ recipient := common.HexToAddress("0xdada")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.SetCoinbase(common.Address{0xc0})
+ g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 0})
+ })
+
+ r := assertPresent(t, b, recipient)
+ if len(r.BalanceChanges) != 0 {
+ t.Fatalf("zero-amount withdrawal must not record balance: %+v", r.BalanceChanges)
+ }
+}
+
+// TestBALWithdrawalNonZeroAmountRecordsBalance: a positive-amount withdrawal
+// records a balance change for the recipient.
+func TestBALWithdrawalNonZeroAmountRecordsBalance(t *testing.T) {
+ recipient := common.HexToAddress("0xdada")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.SetCoinbase(common.Address{0xc0})
+ g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 7})
+ })
+
+ r := assertPresent(t, b, recipient)
+ if len(r.BalanceChanges) != 1 || r.BalanceChanges[0].Balance.Sign() == 0 {
+ t.Fatalf("withdrawal balance change missing: %+v", r.BalanceChanges)
+ }
+}
+
+// ============================== EIP-7702 authority ==============================
+
+// TestBALAuthorityIncludedOnSetCodeTx: the authority of an EIP-7702 set-code
+// transaction is added to the BAL once its delegation is loaded, recording
+// both the nonce bump and the delegation-pointer code entry.
+func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ delegate := common.HexToAddress("0xdeadbeef")
+
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegate,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{
+ ChainID: uint256.MustFromBig(env.cfg.ChainID),
+ Nonce: 0,
+ To: env.from,
+ Value: new(uint256.Int),
+ Gas: 200_000,
+ GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
+ GasTipCap: new(uint256.Int),
+ AuthList: []types.SetCodeAuthorization{auth},
+ })
+ g.AddTx(tx)
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.NonceChanges) == 0 {
+ t.Fatalf("authority nonce should be bumped by delegation: %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) == 0 {
+ t.Fatalf("authority code (delegation pointer) should be recorded: %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALDelegationTargetNotIncludedOnAuthOnly: the EIP-7702 delegation target
+// MUST NOT appear in the BAL when only the authorization is installed and the
+// target is never loaded as an execution target.
+func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ delegate := common.HexToAddress("0xdeadbeef") // never accessed
+
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegate,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{
+ ChainID: uint256.MustFromBig(env.cfg.ChainID),
+ Nonce: 0,
+ To: env.from, // tx.to is an EOA with no code: delegate is never called
+ Value: new(uint256.Int),
+ Gas: 200_000,
+ GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
+ GasTipCap: new(uint256.Int),
+ AuthList: []types.SetCodeAuthorization{auth},
+ })
+ g.AddTx(tx)
+ })
+
+ assertAbsent(t, b, delegate)
+}
+
+// newSetCodeTx is a small constructor used by the multi-auth tests below.
+func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction {
+ t.Helper()
+ tx, err := types.SignTx(types.NewTx(&types.SetCodeTx{
+ ChainID: uint256.MustFromBig(e.cfg.ChainID),
+ Nonce: nonce,
+ To: to,
+ Value: new(uint256.Int),
+ Gas: 400_000,
+ GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
+ GasTipCap: new(uint256.Int),
+ AuthList: auths,
+ }), e.signer, e.key)
+ if err != nil {
+ t.Fatalf("sign SetCodeTx: %v", err)
+ }
+ return tx
+}
+
+// TestBALAuthFailedBeforeLoadExcluded: an EIP-7702 auth whose ChainID check
+// fails returns before the authority is loaded, so the authority address
+// MUST NOT appear in the BAL.
+func TestBALAuthFailedBeforeLoadExcluded(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.NewInt(999), // wrong chain → fails ChainID check (pre-load)
+ Address: common.HexToAddress("0xdeadbeef"),
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth}))
+ })
+
+ assertAbsent(t, b, authority)
+}
+
+// TestBALAuthFailedAfterLoadEmptyChangeSet: an EIP-7702 auth that fails the
+// nonce check happens AFTER the authority's code is loaded (and the address
+// added to accessed_addresses), so the authority MUST appear in the BAL —
+// but with no nonce or code change.
+func TestBALAuthFailedAfterLoadEmptyChangeSet(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+
+ // The authority's actual nonce is 0; supplying auth.Nonce=99 makes
+ // validation fail only after the code has been loaded.
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: common.HexToAddress("0xdeadbeef"),
+ Nonce: 99,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth}))
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.NonceChanges) != 0 {
+ t.Fatalf("failed auth must not bump nonce: %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("failed auth must not record a code change: %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALMultipleAuthsOnlyLoadedIncluded: a SetCode tx with a mix of valid and
+// pre-load-failed auths lists only the loaded authorities in the BAL.
+func TestBALMultipleAuthsOnlyLoadedIncluded(t *testing.T) {
+ env := newBALTestEnv(nil)
+ goodKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ badKey, _ := crypto.HexToECDSA("0303030303030303030303030303030303030303030303030303003030303030")
+ good := crypto.PubkeyToAddress(goodKey.PublicKey)
+ bad := crypto.PubkeyToAddress(badKey.PublicKey)
+ delegate := common.HexToAddress("0xdeadbeef")
+
+ goodAuth, err := types.SignSetCode(goodKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegate,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign good auth: %v", err)
+ }
+ badAuth, err := types.SignSetCode(badKey, types.SetCodeAuthorization{
+ ChainID: *uint256.NewInt(999), // fails before load
+ Address: delegate,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign bad auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{goodAuth, badAuth}))
+ })
+
+ assertPresent(t, b, good) // loaded → in BAL
+ assertAbsent(t, b, bad) // never loaded → not in BAL
+}
+
+// TestBALAuthCodeRoundTripNoCodeEntry: two auths on the same authority that
+// (1) install a delegation and (2) clear it again. Final code equals pre-tx
+// code (empty), so the BAL records only the cumulative nonce bump and NO
+// code change.
+func TestBALAuthCodeRoundTripNoCodeEntry(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ delegateA := common.HexToAddress("0xa11ce")
+
+ auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegateA, // empty → A
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth1: %v", err)
+ }
+ auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: common.Address{}, // delegation to zero clears the code (A → empty)
+ Nonce: 1,
+ })
+ if err != nil {
+ t.Fatalf("sign auth2: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2}))
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 {
+ t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("code round-trip (empty→A→empty) must NOT record a code change: %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALAuthCodeOverwrittenFinalRecorded: two auths on the same authority
+// switching delegation A → B record exactly one code change carrying the
+// final delegation pointer (B), not the intermediate value.
+func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ delegateA := common.HexToAddress("0xa11ce")
+ delegateB := common.HexToAddress("0xb0b0b0")
+
+ auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegateA,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth1: %v", err)
+ }
+ auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegateB,
+ Nonce: 1,
+ })
+ if err != nil {
+ t.Fatalf("sign auth2: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2}))
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.CodeChanges) != 1 {
+ t.Fatalf("expected exactly 1 code change (final), got %+v", aa.CodeChanges)
+ }
+ want := types.AddressToDelegation(delegateB)
+ if !bytes.Equal(aa.CodeChanges[0].Code, want) {
+ t.Fatalf("final code mismatch: want %x, got %x", want, aa.CodeChanges[0].Code)
+ }
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 {
+ t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges)
+ }
+}
diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go
index 5f6239e4fa..b49ac83bb5 100644
--- a/core/bintrie_witness_test.go
+++ b/core/bintrie_witness_test.go
@@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
@@ -202,7 +203,7 @@ func TestProcessParentBlockHash(t *testing.T) {
}
vmContext := NewEVMBlockContext(header, nil, new(common.Address))
evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{})
- ProcessParentBlockHash(header.ParentHash, evm)
+ ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList())
}
// Read block hashes for block 0 .. num-1
for i := 0; i < num; i++ {
diff --git a/core/block_validator.go b/core/block_validator.go
index 008444fbbc..4086a2ead7 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -111,6 +111,28 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
}
}
+ // Block access list hash must be present in header after the
+ // Amsterdam hard fork.
+ if v.config.IsAmsterdam(block.Number(), block.Time()) {
+ if block.Header().BlockAccessListHash == nil {
+ return errors.New("block access list hash not set in header")
+ }
+ // If the block does not include an access list, compute it locally during
+ // execution and validate it against the access list hash in the header.
+ //
+ // If the block includes an attached access list, validate it directly here.
+ if block.AccessList() != nil {
+ computed := block.AccessList().Hash()
+ if *block.Header().BlockAccessListHash != computed {
+ return fmt.Errorf("access list hash mismatch, computed: %x, remote: %x", computed, *block.Header().BlockAccessListHash)
+ } else if err := block.AccessList().Validate(block.GasLimit()); err != nil {
+ return fmt.Errorf("invalid block access list: %v", err)
+ }
+ }
+ } else if block.Header().BlockAccessListHash != nil || block.AccessList() != nil {
+ return errors.New("block had access list before Amsterdam")
+ }
+
// Ancestor block must be known.
if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) {
if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) {
@@ -160,6 +182,23 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
} else if res.Requests != nil {
return errors.New("block has requests before prague fork")
}
+ // Verify Block-level accessList once Amsterdam is enabled
+ if v.config.IsAmsterdam(block.Number(), block.Time()) {
+ if res.Bal == nil {
+ return errors.New("block access list is not available in amsterdam")
+ }
+ if block.Header().BlockAccessListHash == nil {
+ return errors.New("block access list hash not set in header")
+ }
+ enc := res.Bal.ToEncodingObj()
+ local, remote := enc.Hash(), *block.Header().BlockAccessListHash
+ if local != remote {
+ return fmt.Errorf("access list hash mismatch, local: %x, remote: %x", local, remote)
+ }
+ if err := enc.Validate(block.GasLimit()); err != nil {
+ return fmt.Errorf("invalid block access list: %v", err)
+ }
+ }
// Validate the state root against the received state root and throw
// an error if they don't match.
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
diff --git a/core/chain_makers.go b/core/chain_makers.go
index cfd6302794..2e856b5161 100644
--- a/core/chain_makers.go
+++ b/core/chain_makers.go
@@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/params"
@@ -50,6 +51,7 @@ type BlockGen struct {
receipts []*types.Receipt
uncles []*types.Header
withdrawals []*types.Withdrawal
+ bal *bal.ConstructionBlockAccessList
engine consensus.Engine
}
@@ -99,7 +101,7 @@ func (b *BlockGen) Difficulty() *big.Int {
func (b *BlockGen) SetParentBeaconRoot(root common.Hash) {
b.header.ParentBeaconRoot = &root
blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
- ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{}))
+ ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{}), b.bal)
}
// addTx adds a transaction to the generated block. If no coinbase has
@@ -118,7 +120,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti
evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig)
)
b.statedb.SetTxContext(tx.Hash(), len(b.txs), uint32(len(b.txs)+1))
- receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx)
+ receipt, bal, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx)
if err != nil {
panic(err)
}
@@ -134,6 +136,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti
if b.header.BlobGasUsed != nil {
*b.header.BlobGasUsed += receipt.BlobGasUsed
}
+ b.bal.Merge(bal)
}
// AddTx adds a transaction to the generated block. If no coinbase has
@@ -304,10 +307,11 @@ func (b *BlockGen) OffsetTime(seconds int64) {
// ConsensusLayerRequests returns the EIP-7685 requests which have accumulated so far.
func (b *BlockGen) ConsensusLayerRequests() [][]byte {
- return b.collectRequests(true)
+ requests, _ := b.collectRequests(true)
+ return requests
}
-func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) {
+func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte, bal *bal.ConstructionBlockAccessList) {
statedb := b.statedb
if readonly {
// The system contracts clear themselves on a system-initiated read.
@@ -323,11 +327,11 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) {
blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{})
- requests, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1))
+ requests, bal, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1))
if err != nil {
panic(fmt.Sprintf("failed to run post-execution: %v", err))
}
- return requests
+ return requests, bal
}
// GenerateChain creates a chain of n blocks. The first block's
@@ -354,6 +358,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
genblock := func(i int, parent *types.Block, triedb *triedb.Database, statedb *state.StateDB) (*types.Block, types.Receipts) {
b := &BlockGen{i: i, cm: cm, parent: parent, statedb: statedb, engine: engine}
b.header = cm.makeHeader(parent, statedb, b.engine)
+ b.bal = bal.NewConstructionBlockAccessList()
// Set the difficulty for clique block. The chain maker doesn't have access
// to a chain, so the difficulty will be left unset (nil). Set it here to the
@@ -386,7 +391,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase)
blockContext.Random = &common.Hash{} // enable post-merge instruction set
evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{})
- ProcessParentBlockHash(b.header.ParentHash, evm)
+ ProcessParentBlockHash(b.header.ParentHash, evm, b.bal)
}
// Execute any user modifications to the block
@@ -394,11 +399,12 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
gen(i, b)
}
- requests := b.collectRequests(false)
+ requests, bal := b.collectRequests(false)
if requests != nil {
reqHash := types.CalcRequestsHash(requests)
b.header.RequestsHash = &reqHash
}
+ b.bal.Merge(bal)
body := types.Body{
Transactions: b.txs,
@@ -414,8 +420,11 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
body.Withdrawals = make([]*types.Withdrawal, 0)
}
}
+ // Apply the consensus-specific post-transaction changes
+ b.engine.Finalize(cm, b.header, statedb, &body, uint32(len(b.txs)+1), b.bal)
+
// Assemble the block for delivery.
- block := AssembleBlock(b.engine, cm, b.header, statedb, &body, b.receipts)
+ block := AssembleBlock(cm, b.header, statedb, &body, b.receipts, b.bal)
// Write state changes to db
root, err := statedb.Commit(b.header.Number.Uint64(), config.IsEIP158(b.header.Number), config.IsCancun(b.header.Number, b.header.Time))
diff --git a/core/genesis.go b/core/genesis.go
index 6a0affa52e..e1c67e57c2 100644
--- a/core/genesis.go
+++ b/core/genesis.go
@@ -555,6 +555,7 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block {
if head.SlotNumber == nil {
head.SlotNumber = new(uint64)
}
+ head.BlockAccessListHash = &types.EmptyBlockAccessListHash
}
}
return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil))
diff --git a/core/state_processor.go b/core/state_processor.go
index 13466b7815..5690a152e7 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/telemetry"
@@ -81,13 +82,16 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
misc.ApplyDAOHardFork(tracingStateDB)
}
var (
- context = NewEVMBlockContext(header, p.chain, nil)
- signer = types.MakeSigner(config, header.Number, header.Time)
- evm = vm.NewEVM(context, tracingStateDB, config, cfg)
+ context = NewEVMBlockContext(header, p.chain, nil)
+ signer = types.MakeSigner(config, header.Number, header.Time)
+ evm = vm.NewEVM(context, tracingStateDB, config, cfg)
+ blockAccessList = bal.NewConstructionBlockAccessList()
)
defer evm.Release()
+
// Run the pre-execution system calls
- PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time())
+ blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time()))
+
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
msg, err := TransactionToMessage(tx, signer, header.BaseFee)
@@ -99,76 +103,92 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
telemetry.StringAttribute("tx.hash", tx.Hash().Hex()),
telemetry.Int64Attribute("tx.index", int64(i)),
)
-
- receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm)
+ receipt, bal, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm)
if err != nil {
spanEnd(&err)
return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
+ blockAccessList.Merge(bal)
spanEnd(nil)
}
- requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1))
+ requests, bal, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1))
if err != nil {
return nil, err
}
- // Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
- p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body())
+ blockAccessList.Merge(bal)
+
+ // Finalize the block, applying any consensus engine specific extras
+ // (e.g. block rewards).
+ //
+ // TODO(rjl493456442) integrate it into the PostExecution.
+ p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body(), uint32(len(block.Transactions())+1), blockAccessList)
return &ProcessResult{
Receipts: receipts,
Requests: requests,
Logs: allLogs,
GasUsed: gp.Used(),
+ Bal: blockAccessList,
}, nil
}
// PreExecution processes pre-execution system calls.
-func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) {
+func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) *bal.ConstructionBlockAccessList {
_, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution")
defer spanEnd(nil)
+ var blockAccessList *bal.ConstructionBlockAccessList
+ if config.IsAmsterdam(number, time) {
+ blockAccessList = bal.NewConstructionBlockAccessList()
+ }
// EIP-4788
if beaconRoot != nil {
- ProcessBeaconBlockRoot(*beaconRoot, evm)
+ ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList)
}
// EIP-2935
if config.IsPrague(number, time) || config.IsUBT(number, time) {
- ProcessParentBlockHash(parent, evm)
+ ProcessParentBlockHash(parent, evm, blockAccessList)
}
+ return blockAccessList
}
// PostExecution processes post-execution system calls when Prague is enabled.
// If Prague is not activated, it returns null requests to differentiate from
// empty requests.
-func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, err error) {
+func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, blockAccessList *bal.ConstructionBlockAccessList, err error) {
_, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution")
defer spanEnd(&err)
+ if config.IsAmsterdam(number, time) {
+ blockAccessList = bal.NewConstructionBlockAccessList()
+ }
// Read requests if Prague is enabled.
if config.IsPrague(number, time) {
+ rules := config.Rules(number, true, time) // IsMerge is always true
+
requests = [][]byte{}
// EIP-6110
if err := ParseDepositLogs(&requests, allLogs, config); err != nil {
- return nil, fmt.Errorf("failed to parse deposit logs: %w", err)
+ return nil, nil, fmt.Errorf("failed to parse deposit logs: %w", err)
}
// EIP-7002
- if err := ProcessWithdrawalQueue(&requests, evm, blockAccessIndex); err != nil {
- return nil, fmt.Errorf("failed to process withdrawal queue: %w", err)
+ if err := ProcessWithdrawalQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil {
+ return nil, nil, fmt.Errorf("failed to process withdrawal queue: %w", err)
}
// EIP-7251
- if err := ProcessConsolidationQueue(&requests, evm, blockAccessIndex); err != nil {
- return nil, fmt.Errorf("failed to process consolidation queue: %w", err)
+ if err := ProcessConsolidationQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil {
+ return nil, nil, fmt.Errorf("failed to process consolidation queue: %w", err)
}
}
- return requests, nil
+ return requests, blockAccessList, nil
}
// ApplyTransactionWithEVM attempts to apply a transaction to the given state database
// and uses the input parameters for its environment similar to ApplyTransaction. However,
// this method takes an already created EVM instance as input.
-func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, err error) {
+func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, bal *bal.ConstructionBlockAccessList, err error) {
if hooks := evm.Config.Tracer; hooks != nil {
if hooks.OnTxStart != nil {
hooks.OnTxStart(evm.GetVMContext(), tx, msg.From)
@@ -180,12 +200,12 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB,
// Apply the transaction to the current state (included in the env).
result, err := ApplyMessage(evm, msg, gp)
if err != nil {
- return nil, err
+ return nil, nil, err
}
// Update the state with pending changes.
var root []byte
if evm.ChainConfig().IsByzantium(blockNumber) {
- evm.StateDB.Finalise(true)
+ bal = evm.StateDB.Finalise(true)
} else {
root = statedb.IntermediateRoot(evm.ChainConfig().IsEIP158(blockNumber)).Bytes()
}
@@ -194,7 +214,7 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB,
if statedb.Database().Type().Is(state.TypeUBT) {
statedb.AccessEvents().Merge(evm.AccessEvents)
}
- return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil
+ return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), bal, nil
}
// MakeReceipt generates the receipt object for a transaction given its execution result.
@@ -239,10 +259,10 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b
// and uses the input parameters for its environment. It returns the receipt
// for the transaction and an error if the transaction failed,
// indicating the block was invalid.
-func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, error) {
+func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) {
msg, err := TransactionToMessage(tx, types.MakeSigner(evm.ChainConfig(), header.Number, header.Time), header.BaseFee)
if err != nil {
- return nil, err
+ return nil, nil, err
}
// Create a new context to be used in the EVM environment
return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm)
@@ -250,7 +270,7 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *
// ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root
// contract. This method is exported to be used in tests.
-func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) {
+func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())
if tracer.OnSystemCallEnd != nil {
@@ -267,18 +287,19 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) {
Data: beaconRoot[:],
}
evm.SetTxContext(NewEVMTxContext(msg))
+ evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress)
_, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
- evm.StateDB.Finalise(true)
+ blockAccessList.Merge(evm.StateDB.Finalise(true))
}
// ProcessParentBlockHash stores the parent block hash in the history storage contract
// as per EIP-2935/7709.
-func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
+func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())
if tracer.OnSystemCallEnd != nil {
@@ -295,6 +316,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
Data: prevHash.Bytes(),
}
evm.SetTxContext(NewEVMTxContext(msg))
+ evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress)
_, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
@@ -304,22 +326,22 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
- evm.StateDB.Finalise(true)
+ blockAccessList.Merge(evm.StateDB.Finalise(true))
}
// ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract.
// It returns the opaque request data returned by the contract.
-func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error {
- return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex)
+func ProcessWithdrawalQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
+ return processRequestsSystemCall(requests, rules, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex, blockAccessList)
}
// ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract.
// It returns the opaque request data returned by the contract.
-func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error {
- return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex)
+func ProcessConsolidationQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
+ return processRequestsSystemCall(requests, rules, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex, blockAccessList)
}
-func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32) error {
+func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())
if tracer.OnSystemCallEnd != nil {
@@ -335,16 +357,19 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte
To: &addr,
}
evm.SetTxContext(NewEVMTxContext(msg))
+ evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex)
evm.StateDB.AddAddressToAccessList(addr)
ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
- evm.StateDB.Finalise(true)
+ bal := evm.StateDB.Finalise(true)
if err != nil {
return fmt.Errorf("system call failed to execute: %v", err)
}
+ blockAccessList.Merge(bal)
+
if len(ret) == 0 {
return nil // skip empty output
}
@@ -387,8 +412,16 @@ func onSystemCallStart(tracer *tracing.Hooks, ctx *tracing.VMContext) {
// AssembleBlock finalizes the state and assembles the block with provided
// body and receipts.
-func AssembleBlock(engine consensus.Engine, chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) *types.Block {
- engine.Finalize(chain, header, state, body)
+func AssembleBlock(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, blockAccessList *bal.ConstructionBlockAccessList) *types.Block {
+ // Assign the post-transition state root
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
- return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))
+
+ if !chain.Config().IsAmsterdam(header.Number, header.Time) {
+ return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))
+ }
+ // Assign the BlockAccessListHash if Amsterdam has been enabled
+ bal := blockAccessList.ToEncodingObj()
+ balHash := bal.Hash()
+ header.BlockAccessListHash = &balHash
+ return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)).WithAccessListUnsafe(bal)
}
diff --git a/core/state_transition.go b/core/state_transition.go
index 0a6994505d..51c5836892 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -608,7 +608,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
- // - reset transient storage(eip 1153)
+ // - reset transient storage(EIP-1153)
+ // - enable block-level accessList construction (EIP-7928)
st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList)
var (
diff --git a/core/types.go b/core/types.go
index 87bbfcff58..edbfc43db3 100644
--- a/core/types.go
+++ b/core/types.go
@@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
)
@@ -58,4 +59,8 @@ type ProcessResult struct {
Requests [][]byte
Logs []*types.Log
GasUsed uint64
+
+ // BAL is only meaningful for post-Amsterdam blocks. Please ensure
+ // fork validation is performed before accessing it.
+ Bal *bal.ConstructionBlockAccessList
}
diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go
index 9cbc1faeb9..2eb5fe93cd 100644
--- a/core/types/bal/bal.go
+++ b/core/types/bal/bal.go
@@ -138,10 +138,62 @@ func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint32, address common
// PrettyPrint returns a human-readable representation of the access list
func (b *ConstructionBlockAccessList) PrettyPrint() string {
- enc := b.toEncodingObj()
+ enc := b.ToEncodingObj()
return enc.PrettyPrint()
}
+// Merge applies other on top of the local block access list. For colliding
+// entries (a (slot, txIdx) write or a txIdx-keyed balance/nonce/code change),
+// the value from other wins, matching the semantics of applying the local
+// effects first and then other's. Storage reads are unioned; any slot
+// written by either side is dropped from StorageReads.
+//
+// Typically each list covers its own tx index, so txIdx-level collisions are
+// not expected; the exception is pre/post-transition system calls, which
+// share a single tx index. In that case callers must pass block-accessList
+// in order strictly.
+//
+// other is referenced (not deep copied), after the call both lists share
+// inner maps and other must not be mutated.
+func (b *ConstructionBlockAccessList) Merge(other *ConstructionBlockAccessList) {
+ if other == nil {
+ return
+ }
+ for addr, otherAcc := range other.Accounts {
+ acc, ok := b.Accounts[addr]
+ if !ok {
+ b.Accounts[addr] = otherAcc
+ continue
+ }
+ for key, writes := range otherAcc.StorageWrites {
+ existing, ok := acc.StorageWrites[key]
+ if !ok {
+ acc.StorageWrites[key] = writes
+ } else {
+ for txIdx, value := range writes {
+ existing[txIdx] = value
+ }
+ }
+ delete(acc.StorageReads, key)
+ }
+ for key := range otherAcc.StorageReads {
+ if _, ok := acc.StorageWrites[key]; ok {
+ continue
+ }
+ acc.StorageReads[key] = struct{}{}
+ }
+ for txIdx, balance := range otherAcc.BalanceChanges {
+ acc.BalanceChanges[txIdx] = balance
+ }
+ for txIdx, nonce := range otherAcc.NonceChanges {
+ acc.NonceChanges[txIdx] = nonce
+ }
+ for txIdx, code := range otherAcc.CodeChange {
+ acc.CodeChange[txIdx] = code
+ }
+ }
+}
+
// Copy returns a deep copy of the access list.
func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList {
res := NewConstructionBlockAccessList()
diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go
index 03f97f3809..399f9db7c0 100644
--- a/core/types/bal/bal_encoding.go
+++ b/core/types/bal/bal_encoding.go
@@ -78,17 +78,43 @@ func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error {
// Validate returns an error if the contents of the access list are not ordered
// according to the spec or any code changes are contained which exceed protocol
// max code size.
-func (e *BlockAccessList) Validate(rules params.Rules) error {
+func (e *BlockAccessList) Validate(blockGasLimit uint64) error {
if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int {
return bytes.Compare(a.Address[:], b.Address[:])
}) {
return errors.New("block access list accounts not in lexicographic order")
}
for _, entry := range *e {
- if err := entry.validate(rules); err != nil {
+ if err := entry.validate(); err != nil {
return err
}
}
+ return e.ValidateSize(blockGasLimit)
+}
+
+// itemCount returns the number of items in the BAL for EIP-7928 size-constraint
+// purposes: the count of distinct addresses plus every storage key (writes +
+// reads) carried by those accounts. A storage slot is counted once regardless
+// of how many transactions wrote to it.
+func (e *BlockAccessList) itemCount() uint64 {
+ count := uint64(len(*e)) // distinct addresses
+ for i := range *e {
+ count += uint64(len((*e)[i].StorageWrites)) + uint64(len((*e)[i].StorageReads))
+ }
+ return count
+}
+
+// ValidateSize returns an error if the BAL violates the EIP-7928 size
+// constraint for the given block gas limit:
+//
+// itemCount() <= blockGasLimit / params.BALItemCost
+func (e *BlockAccessList) ValidateSize(blockGasLimit uint64) error {
+ items := e.itemCount()
+ limit := blockGasLimit / params.BALItemCost
+ if items > limit {
+ return fmt.Errorf("block access list exceeds size constraint: items=%d, limit=%d (block gas limit %d / %d)",
+ items, limit, blockGasLimit, params.BALItemCost)
+ }
return nil
}
@@ -159,7 +185,7 @@ type AccountAccess struct {
// validate converts the account accesses out of encoding format.
// If any of the keys in the encoding object are not ordered according to the
// spec, an error is returned.
-func (e *AccountAccess) validate(rules params.Rules) error {
+func (e *AccountAccess) validate() error {
// Check the storage write slots are sorted in order
if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int {
return a.Slot.Cmp(b.Slot)
@@ -200,14 +226,7 @@ func (e *AccountAccess) validate(rules params.Rules) error {
return errors.New("code changes not in ascending order by tx index")
}
for _, change := range e.CodeChanges {
- var sizeLimit int
- switch {
- case rules.IsAmsterdam:
- sizeLimit = params.MaxCodeSizeAmsterdam
- default:
- sizeLimit = params.MaxCodeSize
- }
- if len(change.Code) > sizeLimit {
+ if len(change.Code) > params.MaxCodeSizeAmsterdam {
return errors.New("code change contained oversized code")
}
}
@@ -257,7 +276,7 @@ func (e *AccountAccess) Copy() AccountAccess {
// EncodeRLP returns the RLP-encoded access list
func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error {
- return b.toEncodingObj().EncodeRLP(wr)
+ return b.ToEncodingObj().EncodeRLP(wr)
}
var _ rlp.Encoder = &ConstructionBlockAccessList{}
@@ -340,9 +359,9 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
return res
}
-// toEncodingObj returns an instance of the access list expressed as the type
+// ToEncodingObj returns an instance of the access list expressed as the type
// which is used as input for the encoding/decoding.
-func (b *ConstructionBlockAccessList) toEncodingObj() *BlockAccessList {
+func (b *ConstructionBlockAccessList) ToEncodingObj() *BlockAccessList {
var addresses []common.Address
for addr := range b.Accounts {
addresses = append(addresses, addr)
diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go
index 32a0292f2e..2b6a3c194e 100644
--- a/core/types/bal/bal_test.go
+++ b/core/types/bal/bal_test.go
@@ -19,6 +19,7 @@ package bal
import (
"bytes"
"cmp"
+ "math"
"reflect"
"slices"
"testing"
@@ -98,14 +99,65 @@ func TestBALEncoding(t *testing.T) {
if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil {
t.Fatalf("decoding failed: %v\n", err)
}
- if dec.Hash() != bal.toEncodingObj().Hash() {
+ if dec.Hash() != bal.ToEncodingObj().Hash() {
t.Fatalf("encoded block hash doesn't match decoded")
}
- if !reflect.DeepEqual(bal.toEncodingObj(), &dec) {
+ if !reflect.DeepEqual(bal.ToEncodingObj(), &dec) {
t.Fatal("decoded BAL doesn't match")
}
}
+func TestConstructionBALMerge(t *testing.T) {
+ var (
+ addrA = common.BytesToAddress([]byte{0xAA})
+ addrB = common.BytesToAddress([]byte{0xBB})
+ slot1 = common.BytesToHash([]byte{0x01})
+ slot2 = common.BytesToHash([]byte{0x02})
+ slot3 = common.BytesToHash([]byte{0x03})
+ )
+ a := NewConstructionBlockAccessList()
+ a.StorageWrite(1, addrA, slot1, common.BytesToHash([]byte{0x11}))
+ a.StorageRead(addrA, slot2) // demoted by other's write below
+ a.BalanceChange(1, addrA, uint256.NewInt(100))
+ a.NonceChange(addrA, 1, 7)
+
+ b := NewConstructionBlockAccessList()
+ b.StorageWrite(2, addrA, slot1, common.BytesToHash([]byte{0x22})) // same slot, disjoint txIdx
+ b.StorageWrite(2, addrA, slot2, common.BytesToHash([]byte{0x33}))
+ b.StorageRead(addrA, slot3)
+ b.BalanceChange(2, addrA, uint256.NewInt(200))
+ b.NonceChange(addrA, 2, 8)
+ b.CodeChange(addrB, 2, []byte{0xde, 0xad}) // account only in other
+
+ a.Merge(b)
+
+ accA := a.Accounts[addrA]
+ wantWrites := map[common.Hash]map[uint32]common.Hash{
+ slot1: {1: common.BytesToHash([]byte{0x11}), 2: common.BytesToHash([]byte{0x22})},
+ slot2: {2: common.BytesToHash([]byte{0x33})},
+ }
+ if !reflect.DeepEqual(accA.StorageWrites, wantWrites) {
+ t.Fatalf("storage writes mismatch: got %v, want %v", accA.StorageWrites, wantWrites)
+ }
+ wantReads := map[common.Hash]struct{}{slot3: {}}
+ if !reflect.DeepEqual(accA.StorageReads, wantReads) {
+ t.Fatalf("storage reads mismatch: got %v, want %v", accA.StorageReads, wantReads)
+ }
+ if accA.BalanceChanges[1].Uint64() != 100 || accA.BalanceChanges[2].Uint64() != 200 {
+ t.Fatalf("balance changes mismatch: %v", accA.BalanceChanges)
+ }
+ if accA.NonceChanges[1] != 7 || accA.NonceChanges[2] != 8 {
+ t.Fatalf("nonce changes mismatch: %v", accA.NonceChanges)
+ }
+ accB, ok := a.Accounts[addrB]
+ if !ok {
+ t.Fatal("account only present in other was not adopted")
+ }
+ if !bytes.Equal(accB.CodeChange[2], []byte{0xde, 0xad}) {
+ t.Fatalf("code change for adopted account missing: %x", accB.CodeChange[2])
+ }
+}
+
func makeTestAccountAccess(sort bool) AccountAccess {
var (
storageWrites []encodingSlotWrites
@@ -231,10 +283,82 @@ func TestBlockAccessListCopy(t *testing.T) {
}
}
+func TestBlockAccessListItemCount(t *testing.T) {
+ empty := &BlockAccessList{}
+ if got := empty.itemCount(); got != 0 {
+ t.Fatalf("empty BAL item count: got %d, want 0", got)
+ }
+
+ addr1 := [20]byte(testrand.Bytes(20))
+ addr2 := [20]byte(testrand.Bytes(20))
+ one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) }
+ bal := &BlockAccessList{
+ AccountAccess{
+ Address: addr1,
+ StorageWrites: []encodingSlotWrites{
+ {Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}, {TxIdx: 1, ValueAfter: one()}}},
+ {Slot: one()},
+ },
+ StorageReads: []*uint256.Int{one()},
+ },
+ AccountAccess{Address: addr2}, // address-only, no slots
+ }
+ // 2 addresses + 2 write-slots + 1 read-slot = 5 items.
+ // (Multiple TxIdx writes to the same slot count as ONE item.)
+ if got := bal.itemCount(); got != 5 {
+ t.Fatalf("item count: got %d, want 5", got)
+ }
+}
+
+func TestBlockAccessListValidateSize(t *testing.T) {
+ // Build a BAL with exactly 30 items: 3 addresses, each with 9 storage
+ // slots (some writes, some reads). 3 + 9*3 = 30.
+ one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) }
+ bal := make(BlockAccessList, 3)
+ for i := range bal {
+ bal[i].Address = [20]byte(testrand.Bytes(20))
+ for j := 0; j < 5; j++ {
+ bal[i].StorageWrites = append(bal[i].StorageWrites, encodingSlotWrites{
+ Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}},
+ })
+ }
+ for j := 0; j < 4; j++ {
+ bal[i].StorageReads = append(bal[i].StorageReads, one())
+ }
+ }
+ if got := bal.itemCount(); got != 30 {
+ t.Fatalf("setup: item count = %d, want 30", got)
+ }
+
+ // limit = blockGasLimit / BALItemCost.
+ // 30 items requires limit >= 30, i.e. gasLimit >= 30 * 2000 = 60_000.
+ tests := []struct {
+ name string
+ gasLimit uint64
+ expectError bool
+ }{
+ {"exactly at limit", 30 * params.BALItemCost, false},
+ {"well above limit", 60_000_000, false},
+ {"one below limit", 30*params.BALItemCost - 1, true},
+ {"zero gas limit", 0, true},
+ }
+ for _, tc := range tests {
+ err := bal.ValidateSize(tc.gasLimit)
+ if (err != nil) != tc.expectError {
+ t.Errorf("%s: got err=%v, expectError=%v", tc.name, err, tc.expectError)
+ }
+ }
+
+ // Empty BAL is always valid (even with 0 gas limit).
+ if err := (&BlockAccessList{}).ValidateSize(0); err != nil {
+ t.Fatalf("empty BAL must pass any limit: %v", err)
+ }
+}
+
func TestBlockAccessListValidation(t *testing.T) {
// Validate the block access list after RLP decoding
enc := makeTestBAL(true)
- if err := enc.Validate(params.Rules{}); err != nil {
+ if err := enc.Validate(math.MaxUint64); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
var buf bytes.Buffer
@@ -246,14 +370,14 @@ func TestBlockAccessListValidation(t *testing.T) {
if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil {
t.Fatalf("Unexpected RLP-decode error: %v", err)
}
- if err := dec.Validate(params.Rules{}); err != nil {
+ if err := dec.Validate(math.MaxUint64); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
// Validate the derived block access list
cBAL := makeTestConstructionBAL()
- listB := cBAL.toEncodingObj()
- if err := listB.Validate(params.Rules{}); err != nil {
+ listB := cBAL.ToEncodingObj()
+ if err := listB.Validate(math.MaxUint64); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
}
diff --git a/core/types/block.go b/core/types/block.go
index ea576ed232..0856845a4e 100644
--- a/core/types/block.go
+++ b/core/types/block.go
@@ -413,8 +413,9 @@ func (b *Block) BaseFee() *big.Int {
return new(big.Int).Set(b.header.BaseFee)
}
-func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot }
-func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash }
+func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot }
+func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash }
+func (b *Block) BlockAccessListHash() *common.Hash { return b.header.BlockAccessListHash }
func (b *Block) ExcessBlobGas() *uint64 {
var excessBlobGas *uint64
diff --git a/core/types/hashes.go b/core/types/hashes.go
index db8912a66f..541681e4db 100644
--- a/core/types/hashes.go
+++ b/core/types/hashes.go
@@ -43,6 +43,9 @@ 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 accessList, keccak256(rlp.encode([])).
+ EmptyBlockAccessListHash = common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")
+
// EmptyBinaryHash is the known hash of an empty binary trie.
EmptyBinaryHash = common.Hash{}
)
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 9fe6faa3a2..832306b9a0 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -709,3 +709,8 @@ func (evm *EVM) GetVMContext() *tracing.VMContext {
StateDB: evm.StateDB,
}
}
+
+// GetRules returns the chain rules used throughout the EVM execution.
+func (evm *EVM) GetRules() params.Rules {
+ return evm.chainRules
+}
diff --git a/eth/tracers/api.go b/eth/tracers/api.go
index 0df02388b3..88132b4b63 100644
--- a/eth/tracers/api.go
+++ b/eth/tracers/api.go
@@ -1018,7 +1018,7 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor
// Call Prepare to clear out the statedb access list
statedb.SetTxContext(txctx.TxHash, txctx.TxIndex, uint32(txctx.TxIndex+1))
- _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm)
+ _, _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm)
if err != nil {
return nil, fmt.Errorf("tracing failed: %w", err)
}
diff --git a/eth/tracers/internal/tracetest/selfdestruct_state_test.go b/eth/tracers/internal/tracetest/selfdestruct_state_test.go
index 692c5eb775..39067e8efc 100644
--- a/eth/tracers/internal/tracetest/selfdestruct_state_test.go
+++ b/eth/tracers/internal/tracetest/selfdestruct_state_test.go
@@ -620,7 +620,7 @@ func TestSelfdestructStateTracer(t *testing.T) {
}
context := core.NewEVMBlockContext(block.Header(), blockchain, nil)
evm := vm.NewEVM(context, hookedState, tt.genesis.Config, vm.Config{Tracer: tracer.Hooks()})
- _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm)
+ _, _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm)
if err != nil {
t.Fatalf("failed to execute transaction: %v", err)
}
diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go
index fa2ff2c32b..8462194b1d 100644
--- a/internal/ethapi/simulate.go
+++ b/internal/ethapi/simulate.go
@@ -33,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/internal/ethapi/override"
"github.com/ethereum/go-ethereum/params"
@@ -292,9 +293,10 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
gp = core.NewGasPool(blockContext.GasLimit)
blobGasUsed uint64
- txes = make([]*types.Transaction, len(block.Calls))
- callResults = make([]simCallResult, len(block.Calls))
- receipts = make([]*types.Receipt, len(block.Calls))
+ txes = make([]*types.Transaction, len(block.Calls))
+ callResults = make([]simCallResult, len(block.Calls))
+ receipts = make([]*types.Receipt, len(block.Calls))
+ blockAccessList = bal.NewConstructionBlockAccessList()
// Block hash will be repaired after execution.
tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), blockContext.Time, common.Hash{}, common.Hash{}, 0)
@@ -313,13 +315,14 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
}
evm := vm.NewEVM(blockContext, tracingStateDB, sim.chainConfig, *vmConfig)
defer evm.Release()
+
// It is possible to override precompiles with EVM bytecode, or
// move them to another address.
if precompiles != nil {
evm.SetPrecompiles(precompiles)
}
// Run pre-execution system calls
- core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time)
+ blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time))
var allLogs []*types.Log
for i, call := range block.Calls {
@@ -350,7 +353,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
// Update the state with pending changes.
var root []byte
if sim.chainConfig.IsByzantium(blockContext.BlockNumber) {
- tracingStateDB.Finalise(true)
+ blockAccessList.Merge(tracingStateDB.Finalise(true))
} else {
root = sim.state.IntermediateRoot(sim.chainConfig.IsEIP158(blockContext.BlockNumber)).Bytes()
}
@@ -391,7 +394,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
}
// Process EIP-7685 requests
- requests, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1))
+ requests, bal, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1))
if err != nil {
return nil, nil, nil, err
}
@@ -399,6 +402,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
reqHash := types.CalcRequestsHash(requests)
header.RequestsHash = &reqHash
}
+ blockAccessList.Merge(bal)
blockBody := &types.Body{
Transactions: txes,
@@ -411,8 +415,11 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
}
chainHeadReader := &simChainHeadReader{ctx, sim.b}
+ // Apply the consensus-specific post-transaction changes
+ sim.b.Engine().Finalize(chainHeadReader, header, sim.state, blockBody, uint32(len(block.Calls)+1), blockAccessList)
+
// Assemble the block
- b := core.AssembleBlock(sim.b.Engine(), chainHeadReader, header, sim.state, blockBody, receipts)
+ b := core.AssembleBlock(chainHeadReader, header, sim.state, blockBody, receipts, blockAccessList)
repairLogs(callResults, b.Hash())
return b, callResults, senders, nil
diff --git a/miner/worker.go b/miner/worker.go
index 1ecee96688..21bc95cf92 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/internal/telemetry"
"github.com/ethereum/go-ethereum/log"
@@ -71,6 +72,7 @@ type environment struct {
receipts []*types.Receipt
sidecars []*types.BlobTxSidecar
blobs int
+ bal *bal.ConstructionBlockAccessList
witness *stateless.Witness
}
@@ -208,7 +210,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
}
// Collect consensus-layer requests if Prague is enabled.
- requests, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1))
+ requests, bal, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1))
if err != nil {
return &newPayloadResult{err: err}
}
@@ -216,9 +218,14 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
reqHash := types.CalcRequestsHash(requests)
work.header.RequestsHash = &reqHash
}
+ work.bal.Merge(bal)
+
+ // Apply the consensus-specific post-transaction changes
+ miner.engine.Finalize(miner.chain, work.header, work.state, &body, uint32(work.tcount+1), work.bal)
+
// Assemble the block for delivery.
_, _, assembleSpanEnd := telemetry.StartSpan(ctx, "miner.AssembleBlock")
- block := core.AssembleBlock(miner.engine, miner.chain, work.header, work.state, &body, work.receipts)
+ block := core.AssembleBlock(miner.chain, work.header, work.state, &body, work.receipts, work.bal)
assembleSpanEnd(nil)
return &newPayloadResult{
@@ -318,7 +325,7 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams,
return nil, err
}
// Run pre-execution system calls
- core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time)
+ env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time))
return env, nil
}
@@ -337,6 +344,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
}
}
state.StartPrefetcher("miner", bundle)
+
// Note the passed coinbase may be different with header.Coinbase.
return &environment{
signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time),
@@ -345,6 +353,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
coinbase: coinbase,
gasPool: core.NewGasPool(header.GasLimit),
header: header,
+ bal: bal.NewConstructionBlockAccessList(),
witness: state.Witness(),
evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}),
}, nil
@@ -356,7 +365,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx
if tx.Type() == types.BlobTxType {
return miner.commitBlobTransaction(env, tx)
}
- receipt, err := miner.applyTransaction(env, tx)
+ receipt, bal, err := miner.applyTransaction(env, tx)
if err != nil {
return err
}
@@ -364,6 +373,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx
env.receipts = append(env.receipts, receipt)
env.size += tx.Size()
env.tcount++
+ env.bal.Merge(bal)
return nil
}
@@ -380,7 +390,7 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio
if env.blobs+len(sc.Blobs) > maxBlobs {
return errors.New("max data blobs reached")
}
- receipt, err := miner.applyTransaction(env, tx)
+ receipt, bal, err := miner.applyTransaction(env, tx)
if err != nil {
return err
}
@@ -392,23 +402,24 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio
env.size += txNoBlob.Size()
*env.header.BlobGasUsed += receipt.BlobGasUsed
env.tcount++
+ env.bal.Merge(bal)
return nil
}
// applyTransaction runs the transaction. If execution fails, state and gas pool are reverted.
-func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) {
+func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) {
var (
snap = env.state.Snapshot()
gp = env.gasPool.Snapshot()
)
- receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx)
+ receipt, bal, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx)
if err != nil {
env.state.RevertToSnapshot(snap)
env.gasPool.Set(gp)
- return nil, err
+ return nil, nil, err
}
env.header.GasUsed = env.gasPool.Used()
- return receipt, nil
+ return receipt, bal, nil
}
func (miner *Miner) commitTransactions(ctx context.Context, env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error {
diff --git a/params/protocol_params.go b/params/protocol_params.go
index 9da275c486..3e36b83547 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -186,6 +186,16 @@ const (
HistoryServeWindow = 8191 // Number of blocks to serve historical block hashes for, EIP-2935.
MaxBlockSize = 8_388_608 // maximum size of an RLP-encoded block
+
+ // BALItemCost is the gas-cost divisor for the EIP-7928 block access list
+ // size constraint: bal_items <= block_gas_limit / BALItemCost, where
+ // bal_items counts every distinct address in the BAL plus every storage
+ // key (writes + reads) carried by those accounts.
+ //
+ // The value (2000) is set deliberately below COLD_SLOAD_COST (2100) so
+ // the bound has a small safety margin for system-contract accesses that
+ // don't consume block gas.
+ BALItemCost uint64 = 2000
)
// Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation