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