From c5cc9c3e6f8ba80981027b2c33a900b787175506 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Mon, 29 Sep 2025 14:44:19 +0800 Subject: [PATCH] all: implement eip-7928: block-level access lists --- beacon/engine/gen_ed.go | 75 +- beacon/engine/types.go | 126 ++-- build/checksums.txt | 5 + build/ci.go | 19 +- consensus/beacon/consensus.go | 16 +- consensus/clique/clique.go | 6 +- consensus/consensus.go | 6 +- consensus/ethash/consensus.go | 5 +- consensus/misc/eip4844/eip4844.go | 2 + core/block_access_list_tracer.go | 114 +++ core/block_validator.go | 12 + core/blockchain.go | 34 + core/chain_makers.go | 2 +- core/gen_genesis.go | 66 +- core/genesis.go | 39 +- core/state/statedb_hooked.go | 33 + core/state_processor.go | 4 + core/state_transition.go | 21 +- core/tracing/hooks.go | 223 ++++-- core/tracing/journal.go | 30 +- core/tracing/journal_test.go | 9 +- core/types/bal/bal.go | 697 +++++++++++++++---- core/types/bal/bal_encoding.go | 386 ++++++---- core/types/bal/bal_encoding_json.go | 107 +++ core/types/bal/bal_encoding_rlp_generated.go | 395 +++++------ core/types/bal/bal_test.go | 55 +- core/types/bal_blocks_test.go | 32 + core/types/block.go | 21 +- core/types/gen_header_json.go | 92 +-- core/types/gen_header_rlp.go | 20 +- core/vm/gas_table.go | 111 ++- core/vm/jump_table_export.go | 2 + core/vm/operations_acl.go | 185 ++--- eth/api_backend.go | 20 + eth/catalyst/api.go | 45 +- internal/ethapi/api.go | 15 + internal/ethapi/backend.go | 1 + internal/ethapi/simulate.go | 2 +- internal/web3ext/web3ext.go | 17 +- miner/worker.go | 45 +- params/config.go | 6 + tests/block_test.go | 19 +- tests/block_test_util.go | 30 +- tests/gen_btheader.go | 5 + tests/init.go | 32 + tests/init_test.go | 23 +- 46 files changed, 2292 insertions(+), 918 deletions(-) create mode 100644 core/block_access_list_tracer.go create mode 100644 core/types/bal/bal_encoding_json.go create mode 100644 core/types/bal_blocks_test.go diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index 6893d64a16..94ef5c7f19 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,23 +18,24 @@ 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"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -58,29 +60,31 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.Withdrawals = e.Withdrawals enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) + enc.BlockAccessList = e.BlockAccessList 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"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -154,5 +158,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.ExcessBlobGas != nil { e.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) } + if dec.BlockAccessList != nil { + e.BlockAccessList = dec.BlockAccessList + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index da9b6568f2..f634f7864f 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -18,6 +18,7 @@ package engine import ( "fmt" + "github.com/ethereum/go-ethereum/core/types/bal" "math/big" "slices" @@ -50,6 +51,7 @@ var ( // ExecutionPayloadV3 has the syntax of ExecutionPayloadV2 and appends the new // fields: blobGasUsed and excessBlobGas. PayloadV3 PayloadVersion = 0x3 + PayloadV4 PayloadVersion = 0x4 ) //go:generate go run github.com/fjl/gencodec -type PayloadAttributes -field-override payloadAttributesMarshaling -out gen_blockparams.go @@ -73,23 +75,24 @@ 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"` + 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"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList"` } // JSON type overrides for executableData. @@ -292,54 +295,61 @@ 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, + var blockAccessListHash *common.Hash + body := types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals, AccessList: data.BlockAccessList} + if body.AccessList != nil { + balHash := body.AccessList.Hash() + blockAccessListHash = &balHash } - 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, + BlockAccessListHash: blockAccessListHash, + } + return types.NewBlockWithHeader(header).WithBody(body), 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(), + 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(), + BlockAccessList: block.Body().AccessList, } // Add blobs. diff --git a/build/checksums.txt b/build/checksums.txt index 98ee3a91ef..fdf4453ec5 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -5,6 +5,11 @@ # https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz +# version:spec-tests-bal v3.0.1 +# https://github.com/ethereum/execution-spec-tests/releases +# https://github.com/ethereum/execution-spec-tests/releases/download/bal%40v3.0.1 +57d0f109f0557ec33d6ecd6cbd77b55415a658aefe583c4035157e1021ae512a fixtures_bal.tar.gz + # version:golang 1.25.1 # https://go.dev/dl/ d010c109cee94d80efe681eab46bdea491ac906bf46583c32e9f0dbb0bd1a594 go1.25.1.src.tar.gz diff --git a/build/ci.go b/build/ci.go index abb7c4997f..5e2729eebd 100644 --- a/build/ci.go +++ b/build/ci.go @@ -174,6 +174,9 @@ var ( // This is where the tests should be unpacked. executionSpecTestsDir = "tests/spec-tests" + + // This is where the bal-specific release of the tests should be unpacked. + executionSpecTestsBALDir = "tests/spec-tests-bal" ) var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin")) @@ -381,7 +384,8 @@ func doTest(cmdline []string) { // Get test fixtures. if !*short { - downloadSpecTestFixtures(csdb, *cachedir) + downloadMainnetTestFixtures(csdb, *cachedir) + downloadBALSpecTestFixtures(csdb, *cachedir) } // Configure the toolchain. @@ -433,10 +437,8 @@ func doTest(cmdline []string) { } } -// downloadSpecTestFixtures downloads and extracts the execution-spec-tests fixtures. -func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string { +func downloadTestFixtures(csdb *download.ChecksumDB, cachedir, base string) string { ext := ".tar.gz" - base := "fixtures_develop" archivePath := filepath.Join(cachedir, base+ext) if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { log.Fatal(err) @@ -447,6 +449,15 @@ func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string return filepath.Join(cachedir, base) } +// downloadMainnetTestFixtures downloads and extracts the execution-spec-tests fixtures. +func downloadMainnetTestFixtures(csdb *download.ChecksumDB, cachedir string) string { + return downloadTestFixtures(csdb, cachedir, "fixtures") +} + +func downloadBALSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string { + return downloadTestFixtures(csdb, cachedir, "fixtures_bal") +} + // doCheckGenerate ensures that re-generating generated files does not cause // any mutations in the source file tree. func doCheckGenerate() { diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index eed27407a5..c984b355d8 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -272,6 +272,10 @@ func (beacon *Beacon) verifyHeader(chain consensus.ChainHeaderReader, header, pa return err } } + // Verify the block access list hash is set for post-Amsterdam blocks + if chain.Config().IsAmsterdam(header.Number, header.Time) && header.BlockAccessListHash == nil { + return fmt.Errorf("block access list hash must be set post-Amsterdam") + } return nil } @@ -333,6 +337,10 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. } // Withdrawals processing. for _, w := range body.Withdrawals { + // always read the target of the withdrawal regardless of the amount + // because it must be included in the block access list. + state.GetBalance(w.Address) + // Convert amount from gwei to wei. amount := new(uint256.Int).SetUint64(w.Amount) amount = amount.Mul(amount, uint256.NewInt(params.GWei)) @@ -343,9 +351,9 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. // FinalizeAndAssemble implements consensus.Engine, setting the final state and // assembling the block. -func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) { +func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, onFinalization func()) (*types.Block, error) { if !beacon.IsPoSHeader(header) { - return beacon.ethone.FinalizeAndAssemble(chain, header, state, body, receipts) + return beacon.ethone.FinalizeAndAssemble(chain, header, state, body, receipts, onFinalization) } shanghai := chain.Config().IsShanghai(header.Number, header.Time) if shanghai { @@ -364,6 +372,10 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea // Assign the final state root to header. header.Root = state.IntermediateRoot(true) + if onFinalization != nil { + onFinalization() + } + // Assemble the final block. return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)), nil } diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index a6f02c8c2b..4a33848fd5 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -579,7 +579,7 @@ func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Heade // FinalizeAndAssemble implements consensus.Engine, ensuring no uncles are set, // nor block rewards given, and returns the final block. -func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) { +func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, onFinalize func()) (*types.Block, error) { if len(body.Withdrawals) > 0 { return nil, errors.New("clique does not support withdrawals") } @@ -589,6 +589,10 @@ func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header * // Assign the final state root to header. header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) + if onFinalize != nil { + onFinalize() + } + // Assemble and return the final block for sealing. return types.NewBlock(header, &types.Body{Transactions: body.Transactions}, receipts, trie.NewStackTrie(nil)), nil } diff --git a/consensus/consensus.go b/consensus/consensus.go index a68351f7ff..e0168d25e1 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -88,11 +88,13 @@ type Engine interface { Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) // FinalizeAndAssemble runs any post-transaction state modifications (e.g. block - // rewards or process withdrawals) and assembles the final block. + // rewards or process withdrawals) and assembles the final block. onFinalize is + // an optional callback that is called immediately before the block hash is + // computed and embedded in the block header. // // Note: The block header and state database might be updated to reflect any // consensus rules that happen at finalization (e.g. block rewards). - FinalizeAndAssemble(chain ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) + FinalizeAndAssemble(chain ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, onFinalization func()) (*types.Block, error) // 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 376cbac8c0..f5a6f52612 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -511,7 +511,7 @@ func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types. // FinalizeAndAssemble implements consensus.Engine, accumulating the block and // uncle rewards, setting the final state and assembling the block. -func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) { +func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, onFinalize func()) (*types.Block, error) { if len(body.Withdrawals) > 0 { return nil, errors.New("ethash does not support withdrawals") } @@ -521,6 +521,9 @@ func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea // Assign the final state root to header. header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) + if onFinalize != nil { + onFinalize() + } // Header seems complete, assemble into a block and return return types.NewBlock(header, &types.Body{Transactions: body.Transactions, Uncles: body.Uncles}, receipts, trie.NewStackTrie(nil)), nil } diff --git a/consensus/misc/eip4844/eip4844.go b/consensus/misc/eip4844/eip4844.go index 47b54d2e85..5d7c98ec84 100644 --- a/consensus/misc/eip4844/eip4844.go +++ b/consensus/misc/eip4844/eip4844.go @@ -69,6 +69,8 @@ func latestBlobConfig(cfg *params.ChainConfig, time uint64) *BlobConfig { bc = s.BPO4 case cfg.IsBPO3(london, time) && s.BPO3 != nil: bc = s.BPO3 + case cfg.IsAmsterdam(london, time) && s.Amsterdam != nil: + bc = s.Amsterdam case cfg.IsBPO2(london, time) && s.BPO2 != nil: bc = s.BPO2 case cfg.IsBPO1(london, time) && s.BPO1 != nil: diff --git a/core/block_access_list_tracer.go b/core/block_access_list_tracer.go new file mode 100644 index 0000000000..3f35ef5b0e --- /dev/null +++ b/core/block_access_list_tracer.go @@ -0,0 +1,114 @@ +package core + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/holiman/uint256" + "math/big" +) + +// BlockAccessListTracer gathers state accesses/mutations during the execution +// of a block. It is used for construction block access lists. +// +// TODO: enable the BlockAccessListTracer to wrap another tracer. +type BlockAccessListTracer struct { + builder *bal.AccessListBuilder + + // the access list index that changes are currently being recorded into + balIdx uint16 + + // the number of system calls that have been invoked, used when building + // an access list to determine if the system calls being executed are + // before/after the block transactions. + sysCallCount int +} + +// NewBlockAccessListTracer returns an BlockAccessListTracer and a set of hooks +func NewBlockAccessListTracer() (*BlockAccessListTracer, *tracing.Hooks) { + balTracer := &BlockAccessListTracer{ + builder: bal.NewAccessListBuilder(), + } + hooks := &tracing.Hooks{ + OnBlockFinalization: balTracer.OnBlockFinalization, + OnTxEnd: balTracer.TxEndHook, + OnEnter: balTracer.OnEnter, + OnExit: balTracer.OnExit, + OnCodeChangeV2: balTracer.OnCodeChange, + OnBalanceChange: balTracer.OnBalanceChange, + OnNonceChangeV2: balTracer.OnNonceChange, + OnStorageChange: balTracer.OnStorageChange, + OnStorageRead: balTracer.OnStorageRead, + OnAccountRead: balTracer.OnAcountRead, + OnSelfDestructChange: balTracer.OnSelfDestruct, + OnSystemCallEnd: balTracer.OnSystemCallEnd, + } + wrappedHooks, _ := tracing.WrapWithJournal(hooks) + return balTracer, wrappedHooks +} + +// AccessList returns the constructed access list. +// It is assumed that this is only called after all the block state changes +// have been executed and the block has been finalized. +func (a *BlockAccessListTracer) AccessList() *bal.ConstructionBlockAccessList { + return &a.builder.FinalizedAccesses +} + +func (a *BlockAccessListTracer) OnSystemCallEnd() { + // finalize the post-block changes in OnBlockFinalization to account for + // the EIP-4895 withdrawals which occur after the last system contracts + // are executed. + a.sysCallCount++ + if a.sysCallCount == 2 { + a.builder.FinaliseIdxChanges(a.balIdx) + a.balIdx++ + } +} + +func (a *BlockAccessListTracer) TxEndHook(receipt *types.Receipt, err error) { + a.builder.FinaliseIdxChanges(a.balIdx) + a.balIdx++ +} + +func (a *BlockAccessListTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + a.builder.EnterScope() +} + +func (a *BlockAccessListTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + a.builder.ExitScope(reverted) +} + +func (a *BlockAccessListTracer) OnCodeChange(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) { + a.builder.CodeChange(addr, prevCode, code) +} + +func (a *BlockAccessListTracer) OnSelfDestruct(addr common.Address) { + a.builder.SelfDestruct(addr) +} + +func (a *BlockAccessListTracer) OnBlockFinalization() { + a.builder.FinaliseIdxChanges(a.balIdx) +} + +func (a *BlockAccessListTracer) OnBalanceChange(addr common.Address, prevBalance, newBalance *big.Int, _ tracing.BalanceChangeReason) { + newU256 := new(uint256.Int).SetBytes(newBalance.Bytes()) + prevU256 := new(uint256.Int).SetBytes(prevBalance.Bytes()) + a.builder.BalanceChange(addr, prevU256, newU256) +} + +func (a *BlockAccessListTracer) OnNonceChange(addr common.Address, prev uint64, new uint64, reason tracing.NonceChangeReason) { + a.builder.NonceChange(addr, prev, new) +} + +func (a *BlockAccessListTracer) OnStorageRead(addr common.Address, key common.Hash) { + a.builder.StorageRead(addr, key) +} + +func (a *BlockAccessListTracer) OnAcountRead(addr common.Address) { + a.builder.AccountRead(addr) +} + +func (a *BlockAccessListTracer) OnStorageChange(addr common.Address, slot common.Hash, prev common.Hash, new common.Hash) { + a.builder.StorageWrite(addr, slot, prev, new) +} diff --git a/core/block_validator.go b/core/block_validator.go index 008444fbbc..f44eb9067b 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -111,6 +111,18 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } } + // check that the block access list, if present: + // * hashes to the same value reported in the block header + // * is well-formed + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if block.Body().AccessList != nil { + if *block.Header().BlockAccessListHash != block.Body().AccessList.Hash() { + return fmt.Errorf("access list hash mismatch. local: %x. remote: %x\n", block.Body().AccessList.Hash(), *block.Header().BlockAccessListHash) + } else if err := block.Body().AccessList.Validate(len(block.Transactions())); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } + } + } // Ancestor block must be known. if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { diff --git a/core/blockchain.go b/core/blockchain.go index 8741b8b937..2710d73a3c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2173,6 +2173,17 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s }() } + // BAL Tracer used for creating BALs in ProcessBlock in testing path only + var balTracer *BlockAccessListTracer + + isAmsterdam := bc.chainConfig.IsAmsterdam(block.Number(), block.Time()) + // Process block using the parent state as reference point + if isAmsterdam { + balTracer, bc.cfg.VmConfig.Tracer = NewBlockAccessListTracer() + defer func() { + bc.cfg.VmConfig.Tracer = nil + }() + } // Process block using the parent state as reference point pstart := time.Now() res, err := bc.processor.Process(block, statedb, bc.cfg.VmConfig) @@ -2182,6 +2193,13 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s } ptime := time.Since(pstart) + if isAmsterdam { + balTracer.OnBlockFinalization() + } + + // unset the BAL-creation tracer (dirty) + bc.cfg.VmConfig.Tracer = nil + vstart := time.Now() if err := bc.validator.ValidateState(block, statedb, res, false); err != nil { bc.reportBadBlock(block, res, err) @@ -2189,6 +2207,22 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s } vtime := time.Since(vstart) + if isAmsterdam { + computedAccessList := balTracer.AccessList().ToEncodingObj() + computedAccessListHash := computedAccessList.Hash() + + if *block.Header().BlockAccessListHash != computedAccessListHash { + err := fmt.Errorf("block header access list hash mismatch with computed (header=%x computed=%x)", *block.Header().BlockAccessListHash, computedAccessListHash) + bc.reportBadBlock(block, res, err) + return nil, err + } + if block.Body().AccessList != nil && block.Body().AccessList.Hash() != computedAccessListHash { + err := fmt.Errorf("block access list hash mismatch (remote=%x computed=%x)", block.Body().AccessList.Hash(), computedAccessListHash) + bc.reportBadBlock(block, res, err) + return nil, err + } + } + // If witnesses was generated and stateless self-validation requested, do // that now. Self validation should *never* run in production, it's more of // a tight integration to enable running *all* consensus tests through the diff --git a/core/chain_makers.go b/core/chain_makers.go index 7ce86b14e9..d7b97e65a7 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -409,7 +409,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse } body := types.Body{Transactions: b.txs, Uncles: b.uncles, Withdrawals: b.withdrawals} - block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, &body, b.receipts) + block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, &body, b.receipts, nil) if err != nil { panic(err) } diff --git a/core/gen_genesis.go b/core/gen_genesis.go index 2028f98edc..d3e7313eae 100644 --- a/core/gen_genesis.go +++ b/core/gen_genesis.go @@ -19,21 +19,22 @@ var _ = (*genesisSpecMarshaling)(nil) // MarshalJSON marshals as JSON. func (g Genesis) MarshalJSON() ([]byte, error) { type Genesis struct { - Config *params.ChainConfig `json:"config"` - Nonce math.HexOrDecimal64 `json:"nonce"` - Timestamp math.HexOrDecimal64 `json:"timestamp"` - ExtraData hexutil.Bytes `json:"extraData"` - GasLimit math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"` - Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"` - Mixhash common.Hash `json:"mixHash"` - Coinbase common.Address `json:"coinbase"` - Alloc map[common.UnprefixedAddress]types.Account `json:"alloc" gencodec:"required"` - Number math.HexOrDecimal64 `json:"number"` - GasUsed math.HexOrDecimal64 `json:"gasUsed"` - ParentHash common.Hash `json:"parentHash"` - BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"` - ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"` - BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"` + Config *params.ChainConfig `json:"config"` + Nonce math.HexOrDecimal64 `json:"nonce"` + Timestamp math.HexOrDecimal64 `json:"timestamp"` + ExtraData hexutil.Bytes `json:"extraData"` + GasLimit math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"` + Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"` + Mixhash common.Hash `json:"mixHash"` + Coinbase common.Address `json:"coinbase"` + Alloc map[common.UnprefixedAddress]types.Account `json:"alloc" gencodec:"required"` + Number math.HexOrDecimal64 `json:"number"` + GasUsed math.HexOrDecimal64 `json:"gasUsed"` + ParentHash common.Hash `json:"parentHash"` + BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"` + ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"` + BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` } var enc Genesis enc.Config = g.Config @@ -56,27 +57,29 @@ func (g Genesis) MarshalJSON() ([]byte, error) { enc.BaseFee = (*math.HexOrDecimal256)(g.BaseFee) enc.ExcessBlobGas = (*math.HexOrDecimal64)(g.ExcessBlobGas) enc.BlobGasUsed = (*math.HexOrDecimal64)(g.BlobGasUsed) + enc.BlockAccessListHash = g.BlockAccessListHash return json.Marshal(&enc) } // UnmarshalJSON unmarshals from JSON. func (g *Genesis) UnmarshalJSON(input []byte) error { type Genesis struct { - Config *params.ChainConfig `json:"config"` - Nonce *math.HexOrDecimal64 `json:"nonce"` - Timestamp *math.HexOrDecimal64 `json:"timestamp"` - ExtraData *hexutil.Bytes `json:"extraData"` - GasLimit *math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"` - Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"` - Mixhash *common.Hash `json:"mixHash"` - Coinbase *common.Address `json:"coinbase"` - Alloc map[common.UnprefixedAddress]types.Account `json:"alloc" gencodec:"required"` - Number *math.HexOrDecimal64 `json:"number"` - GasUsed *math.HexOrDecimal64 `json:"gasUsed"` - ParentHash *common.Hash `json:"parentHash"` - BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"` - ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"` - BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"` + Config *params.ChainConfig `json:"config"` + Nonce *math.HexOrDecimal64 `json:"nonce"` + Timestamp *math.HexOrDecimal64 `json:"timestamp"` + ExtraData *hexutil.Bytes `json:"extraData"` + GasLimit *math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"` + Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"` + Mixhash *common.Hash `json:"mixHash"` + Coinbase *common.Address `json:"coinbase"` + Alloc map[common.UnprefixedAddress]types.Account `json:"alloc" gencodec:"required"` + Number *math.HexOrDecimal64 `json:"number"` + GasUsed *math.HexOrDecimal64 `json:"gasUsed"` + ParentHash *common.Hash `json:"parentHash"` + BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"` + ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"` + BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` } var dec Genesis if err := json.Unmarshal(input, &dec); err != nil { @@ -133,5 +136,8 @@ func (g *Genesis) UnmarshalJSON(input []byte) error { if dec.BlobGasUsed != nil { g.BlobGasUsed = (*uint64)(dec.BlobGasUsed) } + if dec.BlockAccessListHash != nil { + g.BlockAccessListHash = dec.BlockAccessListHash + } return nil } diff --git a/core/genesis.go b/core/genesis.go index 983ad4c3cb..c086038253 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -67,12 +67,13 @@ type Genesis struct { // These fields are used for consensus tests. Please don't use them // in actual genesis blocks. - Number uint64 `json:"number"` - GasUsed uint64 `json:"gasUsed"` - ParentHash common.Hash `json:"parentHash"` - BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559 - ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844 - BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844 + Number uint64 `json:"number"` + GasUsed uint64 `json:"gasUsed"` + ParentHash common.Hash `json:"parentHash"` + BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559 + ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844 + BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844 + BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` // EIP-7928 } // copy copies the genesis. @@ -122,6 +123,7 @@ func ReadGenesis(db ethdb.Database) (*Genesis, error) { genesis.BaseFee = genesisHeader.BaseFee genesis.ExcessBlobGas = genesisHeader.ExcessBlobGas genesis.BlobGasUsed = genesisHeader.BlobGasUsed + genesis.BlockAccessListHash = genesisHeader.BlockAccessListHash return &genesis, nil } @@ -485,18 +487,19 @@ func (g *Genesis) ToBlock() *types.Block { // toBlockWithRoot constructs the genesis block with the given genesis state root. func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { head := &types.Header{ - Number: new(big.Int).SetUint64(g.Number), - Nonce: types.EncodeNonce(g.Nonce), - Time: g.Timestamp, - ParentHash: g.ParentHash, - Extra: g.ExtraData, - GasLimit: g.GasLimit, - GasUsed: g.GasUsed, - BaseFee: g.BaseFee, - Difficulty: g.Difficulty, - MixDigest: g.Mixhash, - Coinbase: g.Coinbase, - Root: root, + Number: new(big.Int).SetUint64(g.Number), + Nonce: types.EncodeNonce(g.Nonce), + Time: g.Timestamp, + ParentHash: g.ParentHash, + Extra: g.ExtraData, + GasLimit: g.GasLimit, + GasUsed: g.GasUsed, + BaseFee: g.BaseFee, + Difficulty: g.Difficulty, + MixDigest: g.Mixhash, + Coinbase: g.Coinbase, + BlockAccessListHash: g.BlockAccessListHash, + Root: root, } if g.GasLimit == 0 { head.GasLimit = params.GenesisGasLimit diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 48794a3f41..b4968790dc 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -59,22 +59,37 @@ func (s *hookedStateDB) IsNewContract(addr common.Address) bool { } func (s *hookedStateDB) GetBalance(addr common.Address) *uint256.Int { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.GetBalance(addr) } func (s *hookedStateDB) GetNonce(addr common.Address) uint64 { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.GetNonce(addr) } func (s *hookedStateDB) GetCodeHash(addr common.Address) common.Hash { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.GetCodeHash(addr) } func (s *hookedStateDB) GetCode(addr common.Address) []byte { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.GetCode(addr) } func (s *hookedStateDB) GetCodeSize(addr common.Address) int { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.GetCodeSize(addr) } @@ -91,14 +106,23 @@ func (s *hookedStateDB) GetRefund() uint64 { } func (s *hookedStateDB) GetStateAndCommittedState(addr common.Address, hash common.Hash) (common.Hash, common.Hash) { + if s.hooks.OnStorageRead != nil { + s.hooks.OnStorageRead(addr, hash) + } return s.inner.GetStateAndCommittedState(addr, hash) } func (s *hookedStateDB) GetState(addr common.Address, hash common.Hash) common.Hash { + if s.hooks.OnStorageRead != nil { + s.hooks.OnStorageRead(addr, hash) + } return s.inner.GetState(addr, hash) } func (s *hookedStateDB) GetStorageRoot(addr common.Address) common.Hash { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.GetStorageRoot(addr) } @@ -111,14 +135,23 @@ func (s *hookedStateDB) SetTransientState(addr common.Address, key, value common } func (s *hookedStateDB) HasSelfDestructed(addr common.Address) bool { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.HasSelfDestructed(addr) } func (s *hookedStateDB) Exist(addr common.Address) bool { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.Exist(addr) } func (s *hookedStateDB) Empty(addr common.Address) bool { + if s.hooks.OnAccountRead != nil { + s.hooks.OnAccountRead(addr) + } return s.inner.Empty(addr) } diff --git a/core/state_processor.go b/core/state_processor.go index b4b22e4318..7b1dc747e9 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -130,6 +130,10 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) + if hooks := cfg.Tracer; hooks != nil && hooks.OnBlockFinalization != nil { + hooks.OnBlockFinalization() + } + return &ProcessResult{ Receipts: receipts, Requests: requests, diff --git a/core/state_transition.go b/core/state_transition.go index bf5ac07636..934ff4fbf1 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -19,9 +19,6 @@ package core import ( "bytes" "fmt" - "math" - "math/big" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" @@ -29,6 +26,8 @@ import ( "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" + "math" + "math/big" ) // ExecutionResult includes all output after executing given evm @@ -557,6 +556,10 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } else { fee := new(uint256.Int).SetUint64(st.gasUsed()) fee.Mul(fee, effectiveTipU256) + + // always read the coinbase account to include it in the BAL (TODO check this is actually part of the spec) + st.state.GetBalance(st.evm.Context.Coinbase) + st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee) // add the coinbase to the witness iff the fee is greater than 0 @@ -617,16 +620,22 @@ func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) } + prevDelegation, isDelegated := types.ParseDelegation(st.state.GetCode(authority)) + // Update nonce and account code. st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) if auth.Address == (common.Address{}) { // Delegation to zero address means clear. - st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) + if isDelegated { + st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) + } return nil } - // Otherwise install delegation to auth.Address. - st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) + // install delegation to auth.Address if the delegation changed + if !isDelegated || auth.Address != prevDelegation { + st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) + } return nil } diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index c85abe6482..75a277be40 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -178,7 +178,6 @@ type ( CloseHook = func() // BlockStartHook is called before executing `block`. - // `td` is the total difficulty prior to `block`. BlockStartHook = func(event BlockEvent) // BlockEndHook is called after executing a block. @@ -192,24 +191,25 @@ type ( // GenesisBlockHook is called when the genesis block is being processed. GenesisBlockHook = func(genesis *types.Block, alloc types.GenesisAlloc) - // OnSystemCallStartHook is called when a system call is about to be executed. Today, - // this hook is invoked when the EIP-4788 system call is about to be executed to set the - // beacon block root. + // OnSystemCallStartHook is called when a system call is about to be executed. + // Today, this hook is invoked when the EIP-4788 system call is about to be + // executed to set the beacon block root. // - // After this hook, the EVM call tracing will happened as usual so you will receive a `OnEnter/OnExit` - // as well as state hooks between this hook and the `OnSystemCallEndHook`. + // After this hook, the EVM call tracing will happened as usual so you will + // receive a `OnEnter/OnExit` as well as state hooks between this hook and + // the `OnSystemCallEndHook`. // - // Note that system call happens outside normal transaction execution, so the `OnTxStart/OnTxEnd` hooks - // will not be invoked. + // Note that system call happens outside normal transaction execution, so + // the `OnTxStart/OnTxEnd` hooks will not be invoked. OnSystemCallStartHook = func() - // OnSystemCallStartHookV2 is called when a system call is about to be executed. Refer - // to `OnSystemCallStartHook` for more information. + // OnSystemCallStartHookV2 is called when a system call is about to be executed. + // Refer to `OnSystemCallStartHook` for more information. OnSystemCallStartHookV2 = func(vm *VMContext) - // OnSystemCallEndHook is called when a system call has finished executing. Today, - // this hook is invoked when the EIP-4788 system call is about to be executed to set the - // beacon block root. + // OnSystemCallEndHook is called when a system call has finished executing. + // Today, this hook is invoked when the EIP-4788 system call is about to be + // executed to set the beacon block root. OnSystemCallEndHook = func() // StateUpdateHook is called after state is committed for a block. @@ -239,9 +239,17 @@ type ( // StorageChangeHook is called when the storage of an account changes. StorageChangeHook = func(addr common.Address, slot common.Hash, prev, new common.Hash) + SelfDestructHook = func(address common.Address) + // LogHook is called when a log is emitted. LogHook = func(log *types.Log) + // AccountReadHook is called when the account is accessed. + AccountReadHook = func(addr common.Address) + + // StorageReadHook is called when the storage slot is accessed. + StorageReadHook = func(addr common.Address, slot common.Hash) + // BlockHashReadHook is called when EVM reads the blockhash of a block. BlockHashReadHook = func(blockNumber uint64, hash common.Hash) ) @@ -255,6 +263,7 @@ type Hooks struct { OnOpcode OpcodeHook OnFault FaultHook OnGasChange GasChangeHook + // Chain events OnBlockchainInit BlockchainInitHook OnClose CloseHook @@ -266,14 +275,23 @@ type Hooks struct { OnSystemCallStartV2 OnSystemCallStartHookV2 OnSystemCallEnd OnSystemCallEndHook OnStateUpdate StateUpdateHook - // State events - OnBalanceChange BalanceChangeHook - OnNonceChange NonceChangeHook - OnNonceChangeV2 NonceChangeHookV2 - OnCodeChange CodeChangeHook - OnCodeChangeV2 CodeChangeHookV2 - OnStorageChange StorageChangeHook - OnLog LogHook + + OnBlockFinalization func() // called after post-tx system contracts and consensus finalization are invoked + + // State mutation events + OnBalanceChange BalanceChangeHook + OnNonceChange NonceChangeHook + OnNonceChangeV2 NonceChangeHookV2 + OnCodeChange CodeChangeHook + OnCodeChangeV2 CodeChangeHookV2 + OnStorageChange StorageChangeHook + OnLog LogHook + OnSelfDestructChange SelfDestructHook + + // State access events + OnAccountRead AccountReadHook + OnStorageRead StorageReadHook + // Block hash read OnBlockHashRead BlockHashReadHook } @@ -290,57 +308,74 @@ const ( // Issuance // BalanceIncreaseRewardMineUncle is a reward for mining an uncle block. BalanceIncreaseRewardMineUncle BalanceChangeReason = 1 + // BalanceIncreaseRewardMineBlock is a reward for mining a block. BalanceIncreaseRewardMineBlock BalanceChangeReason = 2 + // BalanceIncreaseWithdrawal is ether withdrawn from the beacon chain. BalanceIncreaseWithdrawal BalanceChangeReason = 3 + // BalanceIncreaseGenesisBalance is ether allocated at the genesis block. BalanceIncreaseGenesisBalance BalanceChangeReason = 4 // Transaction fees - // BalanceIncreaseRewardTransactionFee is the transaction tip increasing block builder's balance. + // BalanceIncreaseRewardTransactionFee is the transaction tip increasing + // block builder's balance. BalanceIncreaseRewardTransactionFee BalanceChangeReason = 5 + // BalanceDecreaseGasBuy is spent to purchase gas for execution a transaction. // Part of this gas will be burnt as per EIP-1559 rules. BalanceDecreaseGasBuy BalanceChangeReason = 6 + // BalanceIncreaseGasReturn is ether returned for unused gas at the end of execution. BalanceIncreaseGasReturn BalanceChangeReason = 7 // DAO fork // BalanceIncreaseDaoContract is ether sent to the DAO refund contract. BalanceIncreaseDaoContract BalanceChangeReason = 8 - // BalanceDecreaseDaoAccount is ether taken from a DAO account to be moved to the refund contract. + + // BalanceDecreaseDaoAccount is ether taken from a DAO account to be moved + // to the refund contract. BalanceDecreaseDaoAccount BalanceChangeReason = 9 // BalanceChangeTransfer is ether transferred via a call. // it is a decrease for the sender and an increase for the recipient. BalanceChangeTransfer BalanceChangeReason = 10 + // BalanceChangeTouchAccount is a transfer of zero value. It is only there to // touch-create an account. BalanceChangeTouchAccount BalanceChangeReason = 11 - // BalanceIncreaseSelfdestruct is added to the recipient as indicated by a selfdestructing account. + // BalanceIncreaseSelfdestruct is added to the recipient as indicated by a + // selfdestructing account. BalanceIncreaseSelfdestruct BalanceChangeReason = 12 + // BalanceDecreaseSelfdestruct is deducted from a contract due to self-destruct. BalanceDecreaseSelfdestruct BalanceChangeReason = 13 + // BalanceDecreaseSelfdestructBurn is ether that is sent to an already self-destructed // account within the same tx (captured at end of tx). // Note it doesn't account for a self-destruct which appoints itself as recipient. BalanceDecreaseSelfdestructBurn BalanceChangeReason = 14 - // BalanceChangeRevert is emitted when the balance is reverted back to a previous value due to call failure. - // It is only emitted when the tracer has opted in to use the journaling wrapper (WrapWithJournal). + // BalanceChangeRevert is emitted when the balance is reverted back to a + // previous value due to call failure. + // + // It is only emitted when the tracer has opted in to use the journaling + // wrapper (WrapWithJournal). BalanceChangeRevert BalanceChangeReason = 15 ) // GasChangeReason is used to indicate the reason for a gas change, useful // for tracing and reporting. // -// There is essentially two types of gas changes, those that can be emitted once per transaction -// and those that can be emitted on a call basis, so possibly multiple times per transaction. +// There is essentially two types of gas changes, those that can be emitted +// once per transaction and those that can be emitted on a call basis, so possibly +// multiple times per transaction. // -// They can be recognized easily by their name, those that start with `GasChangeTx` are emitted -// once per transaction, while those that start with `GasChangeCall` are emitted on a call basis. +// They can be recognized easily by their name, those that start with `GasChangeTx` +// are emitted once per transaction, while those that start with `GasChangeCall` +// are emitted on a call basis. type GasChangeReason byte //go:generate go run golang.org/x/tools/cmd/stringer -type=GasChangeReason -trimprefix=GasChange -output gen_gas_change_reason_stringer.go @@ -348,61 +383,100 @@ type GasChangeReason byte const ( GasChangeUnspecified GasChangeReason = 0 - // GasChangeTxInitialBalance is the initial balance for the call which will be equal to the gasLimit of the call. There is only - // one such gas change per transaction. + // GasChangeTxInitialBalance is the initial balance for the call which will + // be equal to the gasLimit of the call. There is only one such gas change + // per transaction. GasChangeTxInitialBalance GasChangeReason = 1 - // GasChangeTxIntrinsicGas is the amount of gas that will be charged for the intrinsic cost of the transaction, there is - // always exactly one of those per transaction. + + // GasChangeTxIntrinsicGas is the amount of gas that will be charged for the + // intrinsic cost of the transaction, there is always exactly one of those + // per transaction. GasChangeTxIntrinsicGas GasChangeReason = 2 - // GasChangeTxRefunds is the sum of all refunds which happened during the tx execution (e.g. storage slot being cleared) - // this generates an increase in gas. There is at most one of such gas change per transaction. + + // GasChangeTxRefunds is the sum of all refunds which happened during the tx + // execution (e.g. storage slot being cleared). this generates an increase in + // gas. There is at most one of such gas change per transaction. GasChangeTxRefunds GasChangeReason = 3 - // GasChangeTxLeftOverReturned is the amount of gas left over at the end of transaction's execution that will be returned - // to the chain. This change will always be a negative change as we "drain" left over gas towards 0. If there was no gas - // left at the end of execution, no such even will be emitted. The returned gas's value in Wei is returned to caller. - // There is at most one of such gas change per transaction. + + // GasChangeTxLeftOverReturned is the amount of gas left over at the end of + // transaction's execution that will be returned to the chain. This change + // will always be a negative change as we "drain" left over gas towards 0. + // If there was no gas left at the end of execution, no such even will be + // emitted. The returned gas's value in Wei is returned to caller. There is + // at most one of such gas change per transaction. GasChangeTxLeftOverReturned GasChangeReason = 4 - // GasChangeCallInitialBalance is the initial balance for the call which will be equal to the gasLimit of the call. There is only - // one such gas change per call. + // GasChangeCallInitialBalance is the initial balance for the call which + // will be equal to the gasLimit of the call. There is only one such gas + // change per call. GasChangeCallInitialBalance GasChangeReason = 5 - // GasChangeCallLeftOverReturned is the amount of gas left over that will be returned to the caller, this change will always - // be a negative change as we "drain" left over gas towards 0. If there was no gas left at the end of execution, no such even - // will be emitted. + + // GasChangeCallLeftOverReturned is the amount of gas left over that will + // be returned to the caller, this change will always be a negative change + // as we "drain" left over gas towards 0. If there was no gas left at the + // end of execution, no such even will be emitted. GasChangeCallLeftOverReturned GasChangeReason = 6 - // GasChangeCallLeftOverRefunded is the amount of gas that will be refunded to the call after the child call execution it - // executed completed. This value is always positive as we are giving gas back to the you, the left over gas of the child. - // If there was no gas left to be refunded, no such even will be emitted. + + // GasChangeCallLeftOverRefunded is the amount of gas that will be refunded + // to the call after the child call execution it executed completed. This + // value is always positive as we are giving gas back to the you, the left over + // gas of the child. If there was no gas left to be refunded, no such event + // will be emitted. GasChangeCallLeftOverRefunded GasChangeReason = 7 - // GasChangeCallContractCreation is the amount of gas that will be burned for a CREATE. + + // GasChangeCallContractCreation is the amount of gas that will be burned + // for a CREATE. GasChangeCallContractCreation GasChangeReason = 8 - // GasChangeCallContractCreation2 is the amount of gas that will be burned for a CREATE2. + + // GasChangeCallContractCreation2 is the amount of gas that will be burned + // for a CREATE2. GasChangeCallContractCreation2 GasChangeReason = 9 - // GasChangeCallCodeStorage is the amount of gas that will be charged for code storage. + + // GasChangeCallCodeStorage is the amount of gas that will be charged for + // code storage. GasChangeCallCodeStorage GasChangeReason = 10 - // GasChangeCallOpCode is the amount of gas that will be charged for an opcode executed by the EVM, exact opcode that was - // performed can be check by `OnOpcode` handling. + + // GasChangeCallOpCode is the amount of gas that will be charged for an opcode + // executed by the EVM, exact opcode that was performed can be check by + // `OnOpcode` handling. GasChangeCallOpCode GasChangeReason = 11 - // GasChangeCallPrecompiledContract is the amount of gas that will be charged for a precompiled contract execution. + + // GasChangeCallPrecompiledContract is the amount of gas that will be charged + // for a precompiled contract execution. GasChangeCallPrecompiledContract GasChangeReason = 12 - // GasChangeCallStorageColdAccess is the amount of gas that will be charged for a cold storage access as controlled by EIP2929 rules. + + // GasChangeCallStorageColdAccess is the amount of gas that will be charged + // for a cold storage access as controlled by EIP2929 rules. GasChangeCallStorageColdAccess GasChangeReason = 13 - // GasChangeCallFailedExecution is the burning of the remaining gas when the execution failed without a revert. + + // GasChangeCallFailedExecution is the burning of the remaining gas when the + // execution failed without a revert. GasChangeCallFailedExecution GasChangeReason = 14 - // GasChangeWitnessContractInit flags the event of adding to the witness during the contract creation initialization step. + + // GasChangeWitnessContractInit flags the event of adding to the witness + // during the contract creation initialization step. GasChangeWitnessContractInit GasChangeReason = 15 - // GasChangeWitnessContractCreation flags the event of adding to the witness during the contract creation finalization step. + + // GasChangeWitnessContractCreation flags the event of adding to the witness + // during the contract creation finalization step. GasChangeWitnessContractCreation GasChangeReason = 16 - // GasChangeWitnessCodeChunk flags the event of adding one or more contract code chunks to the witness. + + // GasChangeWitnessCodeChunk flags the event of adding one or more contract + // code chunks to the witness. GasChangeWitnessCodeChunk GasChangeReason = 17 - // GasChangeWitnessContractCollisionCheck flags the event of adding to the witness when checking for contract address collision. + + // GasChangeWitnessContractCollisionCheck flags the event of adding to the + // witness when checking for contract address collision. GasChangeWitnessContractCollisionCheck GasChangeReason = 18 - // GasChangeTxDataFloor is the amount of extra gas the transaction has to pay to reach the minimum gas requirement for the - // transaction data. This change will always be a negative change. + + // GasChangeTxDataFloor is the amount of extra gas the transaction has to + // pay to reach the minimum gas requirement for the transaction data. + // This change will always be a negative change. GasChangeTxDataFloor GasChangeReason = 19 - // GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as - // it will be "manually" tracked by a direct emit of the gas change event. + // GasChangeIgnored is a special value that can be used to indicate that + // the gas change should be ignored as it will be "manually" tracked by + // a direct emit of the gas change event. GasChangeIgnored GasChangeReason = 0xFF ) @@ -426,11 +500,12 @@ const ( // NonceChangeNewContract is the nonce change of a newly created contract. NonceChangeNewContract NonceChangeReason = 4 - // NonceChangeTransaction is the nonce change due to a EIP-7702 authorization. + // NonceChangeAuthorization is the nonce change due to a EIP-7702 authorization. NonceChangeAuthorization NonceChangeReason = 5 - // NonceChangeRevert is emitted when the nonce is reverted back to a previous value due to call failure. - // It is only emitted when the tracer has opted in to use the journaling wrapper (WrapWithJournal). + // NonceChangeRevert is emitted when the nonce is reverted back to a previous + // value due to call failure. It is only emitted when the tracer has opted in + // to use the journaling wrapper (WrapWithJournal). NonceChangeRevert NonceChangeReason = 6 // NonceChangeSelfdestruct is emitted when the nonce is reset to zero due to a self-destruct @@ -445,22 +520,26 @@ type CodeChangeReason byte const ( CodeChangeUnspecified CodeChangeReason = 0 - // CodeChangeContractCreation is when a new contract is deployed via CREATE/CREATE2 operations. + // CodeChangeContractCreation is when a new contract is deployed via + // CREATE/CREATE2 operations. CodeChangeContractCreation CodeChangeReason = 1 - // CodeChangeGenesis is when contract code is set during blockchain genesis or initial setup. + // CodeChangeGenesis is when contract code is set during blockchain genesis + // or initial setup. CodeChangeGenesis CodeChangeReason = 2 // CodeChangeAuthorization is when code is set via EIP-7702 Set Code Authorization. CodeChangeAuthorization CodeChangeReason = 3 - // CodeChangeAuthorizationClear is when EIP-7702 delegation is cleared by setting to zero address. + // CodeChangeAuthorizationClear is when EIP-7702 delegation is cleared by + // setting to zero address. CodeChangeAuthorizationClear CodeChangeReason = 4 // CodeChangeSelfDestruct is when contract code is cleared due to self-destruct. CodeChangeSelfDestruct CodeChangeReason = 5 - // CodeChangeRevert is emitted when the code is reverted back to a previous value due to call failure. - // It is only emitted when the tracer has opted in to use the journaling wrapper (WrapWithJournal). + // CodeChangeRevert is emitted when the code is reverted back to a previous + // value due to call failure. It is only emitted when the tracer has opted + // in to use the journaling wrapper (WrapWithJournal). CodeChangeRevert CodeChangeReason = 6 ) diff --git a/core/tracing/journal.go b/core/tracing/journal.go index 62a70d6c27..87d1d2ad7d 100644 --- a/core/tracing/journal.go +++ b/core/tracing/journal.go @@ -42,7 +42,9 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) { return nil, errors.New("wrapping nil tracer") } // No state change to journal, return the wrapped hooks as is - if hooks.OnBalanceChange == nil && hooks.OnNonceChange == nil && hooks.OnNonceChangeV2 == nil && hooks.OnCodeChange == nil && hooks.OnCodeChangeV2 == nil && hooks.OnStorageChange == nil { + if hooks.OnBalanceChange == nil && hooks.OnNonceChange == nil && hooks.OnNonceChangeV2 == nil && + hooks.OnCodeChange == nil && hooks.OnCodeChangeV2 == nil && hooks.OnStorageChange == nil { + // TODO(sina) hooks.OnLog should also be handled here return hooks, nil } if hooks.OnNonceChange != nil && hooks.OnNonceChangeV2 != nil { @@ -56,11 +58,14 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) { wrapped := *hooks // Create journal - j := &journal{hooks: hooks} + j := &journal{ + hooks: hooks, + } // Scope hooks need to be re-implemented. wrapped.OnTxEnd = j.OnTxEnd wrapped.OnEnter = j.OnEnter wrapped.OnExit = j.OnExit + // Wrap state change hooks. if hooks.OnBalanceChange != nil { wrapped.OnBalanceChange = j.OnBalanceChange @@ -69,6 +74,7 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) { // Regardless of which hook version is used in the tracer, // the journal will want to capture the nonce change reason. wrapped.OnNonceChangeV2 = j.OnNonceChangeV2 + // A precaution to ensure EVM doesn't call both hooks. wrapped.OnNonceChange = nil } @@ -81,7 +87,6 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) { if hooks.OnStorageChange != nil { wrapped.OnStorageChange = j.OnStorageChange } - return &wrapped, nil } @@ -148,7 +153,11 @@ func (j *journal) OnExit(depth int, output []byte, gasUsed uint64, err error, re } func (j *journal) OnBalanceChange(addr common.Address, prev, new *big.Int, reason BalanceChangeReason) { - j.entries = append(j.entries, balanceChange{addr: addr, prev: prev, new: new}) + j.entries = append(j.entries, balanceChange{ + addr: addr, + prev: prev, + new: new, + }) if j.hooks.OnBalanceChange != nil { j.hooks.OnBalanceChange(addr, prev, new, reason) } @@ -158,7 +167,11 @@ func (j *journal) OnNonceChangeV2(addr common.Address, prev, new uint64, reason // When a contract is created, the nonce of the creator is incremented. // This change is not reverted when the creation fails. if reason != NonceChangeContractCreator { - j.entries = append(j.entries, nonceChange{addr: addr, prev: prev, new: new}) + j.entries = append(j.entries, nonceChange{ + addr: addr, + prev: prev, + new: new, + }) } if j.hooks.OnNonceChangeV2 != nil { j.hooks.OnNonceChangeV2(addr, prev, new, reason) @@ -194,7 +207,12 @@ func (j *journal) OnCodeChangeV2(addr common.Address, prevCodeHash common.Hash, } func (j *journal) OnStorageChange(addr common.Address, slot common.Hash, prev, new common.Hash) { - j.entries = append(j.entries, storageChange{addr: addr, slot: slot, prev: prev, new: new}) + j.entries = append(j.entries, storageChange{ + addr: addr, + slot: slot, + prev: prev, + new: new, + }) if j.hooks.OnStorageChange != nil { j.hooks.OnStorageChange(addr, slot, prev, new) } diff --git a/core/tracing/journal_test.go b/core/tracing/journal_test.go index e00447f5f3..9f9fa27565 100644 --- a/core/tracing/journal_test.go +++ b/core/tracing/journal_test.go @@ -63,7 +63,7 @@ func (t *testTracer) OnCodeChangeV2(addr common.Address, prevCodeHash common.Has } func (t *testTracer) OnStorageChange(addr common.Address, slot common.Hash, prev common.Hash, new common.Hash) { - t.t.Logf("OnStorageCodeChange(%v, %v, %v -> %v)", addr, slot, prev, new) + t.t.Logf("OnStorageChange(%v, %v, %v -> %v)", addr, slot, prev, new) if t.storage == nil { t.storage = make(map[common.Hash]common.Hash) } @@ -76,7 +76,12 @@ func (t *testTracer) OnStorageChange(addr common.Address, slot common.Hash, prev func TestJournalIntegration(t *testing.T) { tr := &testTracer{t: t} - wr, err := WrapWithJournal(&Hooks{OnBalanceChange: tr.OnBalanceChange, OnNonceChange: tr.OnNonceChange, OnCodeChange: tr.OnCodeChange, OnStorageChange: tr.OnStorageChange}) + wr, err := WrapWithJournal(&Hooks{ + OnBalanceChange: tr.OnBalanceChange, + OnNonceChange: tr.OnNonceChange, + OnCodeChange: tr.OnCodeChange, + OnStorageChange: tr.OnStorageChange, + }) if err != nil { t.Fatalf("failed to wrap test tracer: %v", err) } diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index fca54f7681..283749316e 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -1,4 +1,4 @@ -// Copyright 2025 The go-ethereum Authors +// 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 @@ -18,165 +18,602 @@ package bal import ( "bytes" - "maps" - + "encoding/json" "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" + "maps" ) +// idxAccessListBuilder is responsible for producing the state accesses and +// reads recorded within the scope of a single index in the access list. +type idxAccessListBuilder struct { + // the prestate values for any state modified at this index. this is state + // values before the execution of this index. + prestates map[common.Address]*accountIdxPrestate + + // a stack which maintains a set of state mutations/reads for each EVM + // execution frame. Entering a frame appends a constructionAccountAccess + // and terminating a frame pops the last entry and merges it into the + // constructionAccountAccess of the parent frame. + accesses []map[common.Address]*constructionAccountAccess +} + +func newAccessListBuilder() *idxAccessListBuilder { + return &idxAccessListBuilder{ + make(map[common.Address]*accountIdxPrestate), + []map[common.Address]*constructionAccountAccess{ + make(map[common.Address]*constructionAccountAccess), + }, + } +} + +func (c *idxAccessListBuilder) storageRead(address common.Address, key common.Hash) { + if _, ok := c.accesses[len(c.accesses)-1][address]; !ok { + c.accesses[len(c.accesses)-1][address] = &constructionAccountAccess{} + } + acctAccesses := c.accesses[len(c.accesses)-1][address] + acctAccesses.StorageRead(key) +} + +func (c *idxAccessListBuilder) accountRead(address common.Address) { + if _, ok := c.accesses[len(c.accesses)-1][address]; !ok { + c.accesses[len(c.accesses)-1][address] = &constructionAccountAccess{} + } +} + +func (c *idxAccessListBuilder) storageWrite(address common.Address, key, prevVal, newVal common.Hash) { + if _, ok := c.prestates[address]; !ok { + c.prestates[address] = &accountIdxPrestate{} + } + if c.prestates[address].storage == nil { + c.prestates[address].storage = make(map[common.Hash]common.Hash) + } + if _, ok := c.prestates[address].storage[key]; !ok { + c.prestates[address].storage[key] = prevVal + } + + if _, ok := c.accesses[len(c.accesses)-1][address]; !ok { + c.accesses[len(c.accesses)-1][address] = &constructionAccountAccess{} + } + acctAccesses := c.accesses[len(c.accesses)-1][address] + acctAccesses.StorageWrite(key, newVal) +} + +func (c *idxAccessListBuilder) balanceChange(address common.Address, prev, cur *uint256.Int) { + if _, ok := c.prestates[address]; !ok { + c.prestates[address] = &accountIdxPrestate{} + } + if c.prestates[address].balance == nil { + c.prestates[address].balance = prev + } + if _, ok := c.accesses[len(c.accesses)-1][address]; !ok { + c.accesses[len(c.accesses)-1][address] = &constructionAccountAccess{} + } + acctAccesses := c.accesses[len(c.accesses)-1][address] + acctAccesses.BalanceChange(cur) +} + +func (c *idxAccessListBuilder) codeChange(address common.Address, prev, cur []byte) { + // auth unset and selfdestruct pass code change as 'nil' + // however, internally in the access list accumulation of state changes, + // a nil field on an account means that it was never modified in the block. + if cur == nil { + cur = []byte{} + } + + if _, ok := c.prestates[address]; !ok { + c.prestates[address] = &accountIdxPrestate{} + } + if c.prestates[address].code == nil { + if prev == nil { + prev = []byte{} + } + c.prestates[address].code = prev + } + if _, ok := c.accesses[len(c.accesses)-1][address]; !ok { + c.accesses[len(c.accesses)-1][address] = &constructionAccountAccess{} + } + acctAccesses := c.accesses[len(c.accesses)-1][address] + + acctAccesses.CodeChange(cur) +} + +// selfDestruct is invoked when an account which has been created and invoked +// SENDALL in the same transaction is removed as part of transaction finalization. +// +// Any storage accesses/modifications performed at the contract during execution +// of the current call are retained in the block access list as state reads. +func (c *idxAccessListBuilder) selfDestruct(address common.Address) { + access := c.accesses[len(c.accesses)-1][address] + if len(access.storageMutations) != 0 && access.storageReads == nil { + access.storageReads = make(map[common.Hash]struct{}) + } + for key, _ := range access.storageMutations { + access.storageReads[key] = struct{}{} + } + access.storageMutations = nil +} + +func (c *idxAccessListBuilder) nonceChange(address common.Address, prev, cur uint64) { + if _, ok := c.prestates[address]; !ok { + c.prestates[address] = &accountIdxPrestate{} + } + if c.prestates[address].nonce == nil { + c.prestates[address].nonce = &prev + } + if _, ok := c.accesses[len(c.accesses)-1][address]; !ok { + c.accesses[len(c.accesses)-1][address] = &constructionAccountAccess{} + } + acctAccesses := c.accesses[len(c.accesses)-1][address] + acctAccesses.NonceChange(cur) +} + +// enterScope is called after a new EVM call frame has been entered. +func (c *idxAccessListBuilder) enterScope() { + c.accesses = append(c.accesses, make(map[common.Address]*constructionAccountAccess)) +} + +// exitCall is called after an EVM call scope terminates. If the call scope +// terminates with an error: +// * the scope's state accesses are added to the calling scope's access list +// * mutated accounts/storage are added into the calling scope's access list as state accesses +func (c *idxAccessListBuilder) exitCall(evmErr bool) { + childAccessList := c.accesses[len(c.accesses)-1] + parentAccessList := c.accesses[len(c.accesses)-2] + + for addr, childAccess := range childAccessList { + if _, ok := parentAccessList[addr]; ok { + } else { + parentAccessList[addr] = &constructionAccountAccess{} + } + if evmErr { + // all storage writes in the child scope are converted into reads + // if there were no storage writes, the account is reported in the BAL as a read (if it wasn't already in the BAL and/or mutated previously) + parentAccessList[addr].MergeReads(childAccess) + } else { + parentAccessList[addr].Merge(childAccess) + } + } + + c.accesses = c.accesses[:len(c.accesses)-1] +} + +// finalise returns the net state mutations at the access list index as well as +// state which was accessed. The idxAccessListBuilder instance must be discarded +// after calling finalise. +func (a *idxAccessListBuilder) finalise() (*StateDiff, StateAccesses) { + diff := &StateDiff{make(map[common.Address]*AccountMutations)} + stateAccesses := make(StateAccesses) + + for addr, access := range a.accesses[0] { + // remove any reported mutations from the access list with no net difference vs the index prestate value + if access.nonce != nil && *a.prestates[addr].nonce == *access.nonce { + access.nonce = nil + } + if access.balance != nil && a.prestates[addr].balance.Eq(access.balance) { + access.balance = nil + } + if access.code != nil && bytes.Equal(access.code, a.prestates[addr].code) { + access.code = nil + } + if access.storageMutations != nil { + for key, val := range access.storageMutations { + if a.prestates[addr].storage[key] == val { + delete(access.storageMutations, key) + access.storageReads[key] = struct{}{} + } + } + if len(access.storageMutations) == 0 { + access.storageMutations = nil + } + } + + // if the account has no net mutations against the index prestate, only include + // it in the state read set + if len(access.code) == 0 && access.nonce == nil && access.balance == nil && len(access.storageMutations) == 0 { + stateAccesses[addr] = make(map[common.Hash]struct{}) + if access.storageReads != nil { + stateAccesses[addr] = access.storageReads + } + continue + } + + stateAccesses[addr] = access.storageReads + diff.Mutations[addr] = &AccountMutations{ + Balance: access.balance, + Nonce: access.nonce, + Code: access.code, + StorageWrites: access.storageMutations, + } + } + + return diff, stateAccesses +} + +// FinaliseIdxChanges records all pending state mutations/accesses in the +// access list at the given index. The set of pending state mutations/accesse are +// then emptied. +func (c *AccessListBuilder) FinaliseIdxChanges(idx uint16) { + pendingDiff, pendingAccesses := c.idxBuilder.finalise() + c.idxBuilder = newAccessListBuilder() + + for addr, pendingAcctDiff := range pendingDiff.Mutations { + finalizedAcctChanges, ok := c.FinalizedAccesses[addr] + if !ok { + finalizedAcctChanges = &ConstructionAccountAccesses{} + c.FinalizedAccesses[addr] = finalizedAcctChanges + } + + if pendingAcctDiff.Nonce != nil { + if finalizedAcctChanges.NonceChanges == nil { + finalizedAcctChanges.NonceChanges = make(map[uint16]uint64) + } + finalizedAcctChanges.NonceChanges[idx] = *pendingAcctDiff.Nonce + } + if pendingAcctDiff.Balance != nil { + if finalizedAcctChanges.BalanceChanges == nil { + finalizedAcctChanges.BalanceChanges = make(map[uint16]*uint256.Int) + } + finalizedAcctChanges.BalanceChanges[idx] = pendingAcctDiff.Balance + } + if pendingAcctDiff.Code != nil { + if finalizedAcctChanges.CodeChanges == nil { + finalizedAcctChanges.CodeChanges = make(map[uint16]CodeChange) + } + finalizedAcctChanges.CodeChanges[idx] = CodeChange{idx, pendingAcctDiff.Code} + } + if pendingAcctDiff.StorageWrites != nil { + if finalizedAcctChanges.StorageWrites == nil { + finalizedAcctChanges.StorageWrites = make(map[common.Hash]map[uint16]common.Hash) + } + for key, val := range pendingAcctDiff.StorageWrites { + if _, ok := finalizedAcctChanges.StorageWrites[key]; !ok { + finalizedAcctChanges.StorageWrites[key] = make(map[uint16]common.Hash) + } + finalizedAcctChanges.StorageWrites[key][idx] = val + + // if any of the newly-mutated storage slots were previously + // accessed, they must be removed from the accessed state set. + // + // TODO: commenting this 'if' results in no test failures. + // double-check that this edge-case was fixed by a future + // release of the eest BAL tests. + if _, ok := finalizedAcctChanges.StorageReads[key]; ok { + delete(finalizedAcctChanges.StorageReads, key) + } + } + } + } + // record pending accesses in the BAL access set unless they were + // already written in a previous index + for addr, pendingAccountAccesses := range pendingAccesses { + finalizedAcctAccesses, ok := c.FinalizedAccesses[addr] + if !ok { + finalizedAcctAccesses = &ConstructionAccountAccesses{} + c.FinalizedAccesses[addr] = finalizedAcctAccesses + } + + for key := range pendingAccountAccesses { + if _, ok := finalizedAcctAccesses.StorageWrites[key]; ok { + continue + } + if finalizedAcctAccesses.StorageReads == nil { + finalizedAcctAccesses.StorageReads = make(map[common.Hash]struct{}) + } + finalizedAcctAccesses.StorageReads[key] = struct{}{} + } + } + c.lastFinalizedMutations = pendingDiff + c.lastFinalizedAccesses = pendingAccesses +} + +func (c *AccessListBuilder) StorageRead(address common.Address, key common.Hash) { + c.idxBuilder.storageRead(address, key) +} +func (c *AccessListBuilder) AccountRead(address common.Address) { + c.idxBuilder.accountRead(address) +} +func (c *AccessListBuilder) StorageWrite(address common.Address, key, prevVal, newVal common.Hash) { + c.idxBuilder.storageWrite(address, key, prevVal, newVal) +} +func (c *AccessListBuilder) BalanceChange(address common.Address, prev, cur *uint256.Int) { + c.idxBuilder.balanceChange(address, prev, cur) +} +func (c *AccessListBuilder) NonceChange(address common.Address, prev, cur uint64) { + c.idxBuilder.nonceChange(address, prev, cur) +} +func (c *AccessListBuilder) CodeChange(address common.Address, prev, cur []byte) { + c.idxBuilder.codeChange(address, prev, cur) +} +func (c *AccessListBuilder) SelfDestruct(address common.Address) { + c.idxBuilder.selfDestruct(address) +} + +func (c *AccessListBuilder) EnterScope() { + c.idxBuilder.enterScope() +} +func (c *AccessListBuilder) ExitScope(executionErr bool) { + c.idxBuilder.exitCall(executionErr) +} + // CodeChange contains the runtime bytecode deployed at an address and the // transaction index where the deployment took place. type CodeChange struct { - TxIndex uint16 - Code []byte `json:"code,omitempty"` + TxIdx uint16 + Code []byte `json:"code,omitempty"` } -// ConstructionAccountAccess contains post-block account state for mutations as well as -// all storage keys that were read during execution. It is used when building block -// access list during execution. -type ConstructionAccountAccess struct { +// ConstructionAccountAccesses contains post-block account state for mutated +// values as well as all storage keys that were read during execution. +// It is used when building block access list during execution. +type ConstructionAccountAccesses struct { // StorageWrites is the post-state values of an account's storage slots - // that were modified in a block, keyed by the slot key and the tx index - // where the modification occurred. - StorageWrites map[common.Hash]map[uint16]common.Hash `json:"storageWrites,omitempty"` + // that were modified in a block, keyed by the slot key and the bal indices + // where modifications occurred. + StorageWrites map[common.Hash]map[uint16]common.Hash // StorageReads is the set of slot keys that were accessed during block // execution. // - // Storage slots which are both read and written (with changed values) + // storage slots which are both read and written (with changed values) // appear only in StorageWrites. - StorageReads map[common.Hash]struct{} `json:"storageReads,omitempty"` + StorageReads map[common.Hash]struct{} - // BalanceChanges contains the post-transaction balances of an account, - // keyed by transaction indices where it was changed. - BalanceChanges map[uint16]*uint256.Int `json:"balanceChanges,omitempty"` + // BalanceChanges contains the post-state balances of an account, + // keyed by bal indices where it was changed. + BalanceChanges map[uint16]*uint256.Int // NonceChanges contains the post-state nonce values of an account keyed - // by tx index. - NonceChanges map[uint16]uint64 `json:"nonceChanges,omitempty"` + // by bal indices where it changed. + NonceChanges map[uint16]uint64 - // CodeChange is only set for contract accounts which were deployed in - // the block. - CodeChange *CodeChange `json:"codeChange,omitempty"` + // NonceChanges contains the post-state code values of an account keyed + // by bal indices where it changed. + CodeChanges map[uint16]CodeChange } -// NewConstructionAccountAccess initializes the account access object. -func NewConstructionAccountAccess() *ConstructionAccountAccess { - return &ConstructionAccountAccess{ - StorageWrites: make(map[common.Hash]map[uint16]common.Hash), - StorageReads: make(map[common.Hash]struct{}), - BalanceChanges: make(map[uint16]*uint256.Int), - NonceChanges: make(map[uint16]uint64), - } +// constructionAccountAccess contains fields for an account which were modified +// during execution of the current access list index. +// It also accumulates a set of storage slots which were accessed but not +// modified. +type constructionAccountAccess struct { + code []byte + nonce *uint64 + balance *uint256.Int + + storageMutations map[common.Hash]common.Hash + storageReads map[common.Hash]struct{} } -// ConstructionBlockAccessList contains post-block modified state and some state accessed -// in execution (account addresses and storage keys). -type ConstructionBlockAccessList struct { - Accounts map[common.Address]*ConstructionAccountAccess -} - -// NewConstructionBlockAccessList instantiates an empty access list. -func NewConstructionBlockAccessList() ConstructionBlockAccessList { - return ConstructionBlockAccessList{ - Accounts: make(map[common.Address]*ConstructionAccountAccess), +// Merge adds the accesses/mutations from other into the calling instance. If +func (c *constructionAccountAccess) Merge(other *constructionAccountAccess) { + if other.code != nil { + c.code = other.code } -} - -// AccountRead records the address of an account that has been read during execution. -func (b *ConstructionBlockAccessList) AccountRead(addr common.Address) { - if _, ok := b.Accounts[addr]; !ok { - b.Accounts[addr] = NewConstructionAccountAccess() + if other.nonce != nil { + c.nonce = other.nonce } -} - -// StorageRead records a storage key read during execution. -func (b *ConstructionBlockAccessList) StorageRead(address common.Address, key common.Hash) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() + if other.balance != nil { + c.balance = other.balance } - if _, ok := b.Accounts[address].StorageWrites[key]; ok { - return - } - b.Accounts[address].StorageReads[key] = struct{}{} -} - -// StorageWrite records the post-transaction value of a mutated storage slot. -// The storage slot is removed from the list of read slots. -func (b *ConstructionBlockAccessList) StorageWrite(txIdx uint16, address common.Address, key, value common.Hash) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() - } - if _, ok := b.Accounts[address].StorageWrites[key]; !ok { - b.Accounts[address].StorageWrites[key] = make(map[uint16]common.Hash) - } - b.Accounts[address].StorageWrites[key][txIdx] = value - - delete(b.Accounts[address].StorageReads, key) -} - -// CodeChange records the code of a newly-created contract. -func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex uint16, code []byte) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() - } - b.Accounts[address].CodeChange = &CodeChange{ - TxIndex: txIndex, - Code: bytes.Clone(code), - } -} - -// NonceChange records tx post-state nonce of any contract-like accounts whose -// nonce was incremented. -func (b *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx uint16, postNonce uint64) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() - } - b.Accounts[address].NonceChanges[txIdx] = postNonce -} - -// BalanceChange records the post-transaction balance of an account whose -// balance changed. -func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint16, address common.Address, balance *uint256.Int) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() - } - b.Accounts[address].BalanceChanges[txIdx] = balance.Clone() -} - -// PrettyPrint returns a human-readable representation of the access list -func (b *ConstructionBlockAccessList) PrettyPrint() string { - enc := b.toEncodingObj() - return enc.PrettyPrint() -} - -// Copy returns a deep copy of the access list. -func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { - res := NewConstructionBlockAccessList() - for addr, aa := range b.Accounts { - var aaCopy ConstructionAccountAccess - - slotWrites := make(map[common.Hash]map[uint16]common.Hash, len(aa.StorageWrites)) - for key, m := range aa.StorageWrites { - slotWrites[key] = maps.Clone(m) + if other.storageMutations != nil { + if c.storageMutations == nil { + c.storageMutations = make(map[common.Hash]common.Hash) } - aaCopy.StorageWrites = slotWrites - aaCopy.StorageReads = maps.Clone(aa.StorageReads) - - balances := make(map[uint16]*uint256.Int, len(aa.BalanceChanges)) - for index, balance := range aa.BalanceChanges { - balances[index] = balance.Clone() + for key, val := range other.storageMutations { + c.storageMutations[key] = val + delete(c.storageReads, key) } - aaCopy.BalanceChanges = balances - aaCopy.NonceChanges = maps.Clone(aa.NonceChanges) + } + if other.storageReads != nil { + if c.storageReads == nil { + c.storageReads = make(map[common.Hash]struct{}) + } + // TODO: if the state was mutated in the caller, don't add it to the caller's reads. + // need to have a test case for this, verify it fails in the current state, and then fix this bug. + for key, val := range other.storageReads { + c.storageReads[key] = val + } + } +} - if aa.CodeChange != nil { - aaCopy.CodeChange = &CodeChange{ - TxIndex: aa.CodeChange.TxIndex, - Code: bytes.Clone(aa.CodeChange.Code), +// MergeReads merges accesses from a reverted execution from: +// * any reads/writes from the reverted frame which weren't mutated +// in the current frame, are merged into the current frame as reads. +func (c *constructionAccountAccess) MergeReads(other *constructionAccountAccess) { + if other.storageMutations != nil { + if c.storageReads == nil { + c.storageReads = make(map[common.Hash]struct{}) + } + for key, _ := range other.storageMutations { + if _, ok := c.storageMutations[key]; ok { + continue } + c.storageReads[key] = struct{}{} + } + } + if other.storageReads != nil { + if c.storageReads == nil { + c.storageReads = make(map[common.Hash]struct{}) + } + for key := range other.storageReads { + if _, ok := c.storageMutations[key]; ok { + continue + } + c.storageReads[key] = struct{}{} + } + } +} + +func (c *constructionAccountAccess) StorageRead(key common.Hash) { + if c.storageReads == nil { + c.storageReads = make(map[common.Hash]struct{}) + } + if _, ok := c.storageMutations[key]; !ok { + c.storageReads[key] = struct{}{} + } +} + +func (c *constructionAccountAccess) StorageWrite(key, newVal common.Hash) { + if c.storageMutations == nil { + c.storageMutations = make(map[common.Hash]common.Hash) + } + c.storageMutations[key] = newVal + // a key can be first read and later written, but it must only show up + // in either read or write sets, not both. + // + // the caller should not + // call StorageRead on a slot that was already written + delete(c.storageReads, key) +} + +func (c *constructionAccountAccess) BalanceChange(cur *uint256.Int) { + c.balance = cur +} + +func (c *constructionAccountAccess) CodeChange(cur []byte) { + c.code = cur +} + +func (c *constructionAccountAccess) NonceChange(cur uint64) { + c.nonce = &cur +} + +type ConstructionBlockAccessList map[common.Address]*ConstructionAccountAccesses + +// AccessListBuilder is used to build an EIP-7928 block access list +type AccessListBuilder struct { + FinalizedAccesses ConstructionBlockAccessList + + idxBuilder *idxAccessListBuilder + + lastFinalizedMutations *StateDiff + lastFinalizedAccesses StateAccesses +} + +// NewAccessListBuilder instantiates an empty access list. +func NewAccessListBuilder() *AccessListBuilder { + return &AccessListBuilder{ + make(map[common.Address]*ConstructionAccountAccesses), + newAccessListBuilder(), + nil, + nil, + } +} + +// FinalizedIdxChanges returns the state mutations and accesses recorded in the latest +// access list index that was finalized. +func (c *AccessListBuilder) FinalizedIdxChanges() (*StateDiff, StateAccesses) { + return c.lastFinalizedMutations, c.lastFinalizedAccesses +} + +// StateDiff contains state mutations occuring over one or more access list +// index. +type StateDiff struct { + Mutations map[common.Address]*AccountMutations `json:"Mutations,omitempty"` +} + +// StateAccesses contains a set of accounts/storage that were accessed during the +// execution of one or more access list indices. +type StateAccesses map[common.Address]map[common.Hash]struct{} + +// Merge combines adds the accesses from other into s. +func (s *StateAccesses) Merge(other StateAccesses) { + for addr, accesses := range other { + if _, ok := (*s)[addr]; !ok { + (*s)[addr] = make(map[common.Hash]struct{}) + } + for slot := range accesses { + (*s)[addr][slot] = struct{}{} + } + } +} + +// accountIdxPrestate contains partial account state before the execution at an +// index. nil values indicate that a state value is not resolved. +type accountIdxPrestate struct { + balance *uint256.Int + nonce *uint64 + code ContractCode + storage map[common.Hash]common.Hash +} + +// AccountMutations contains mutations that were made to an account across +// one or more access list indices. +type AccountMutations struct { + Balance *uint256.Int `json:"Balance,omitempty"` + Nonce *uint64 `json:"Nonce,omitempty"` + Code ContractCode `json:"Code,omitempty"` + StorageWrites map[common.Hash]common.Hash `json:"StorageWrites,omitempty"` +} + +// String returns a human-readable JSON representation of the account mutations. +func (a *AccountMutations) String() string { + var res bytes.Buffer + enc := json.NewEncoder(&res) + enc.SetIndent("", " ") + enc.Encode(a) + return res.String() +} + +// Copy returns a deep-copy of the instance. +func (a *AccountMutations) Copy() *AccountMutations { + res := &AccountMutations{ + nil, + nil, + nil, + nil, + } + if a.Nonce != nil { + res.Nonce = new(uint64) + *res.Nonce = *a.Nonce + } + if a.Code != nil { + res.Code = bytes.Clone(a.Code) + } + if a.Balance != nil { + res.Balance = new(uint256.Int).Set(a.Balance) + } + if a.StorageWrites != nil { + res.StorageWrites = maps.Clone(a.StorageWrites) + } + return res +} + +// String returns the state diff as a formatted JSON string. +func (s *StateDiff) String() string { + var res bytes.Buffer + enc := json.NewEncoder(&res) + enc.SetIndent("", " ") + enc.Encode(s) + return res.String() +} + +// Merge merges the state changes present in next into the caller. After, +// the state of the caller is the aggregate diff through next. +func (s *StateDiff) Merge(next *StateDiff) { + for account, diff := range next.Mutations { + if mut, ok := s.Mutations[account]; ok { + if diff.Balance != nil { + mut.Balance = diff.Balance + } + if diff.Code != nil { + mut.Code = diff.Code + } + if diff.Nonce != nil { + mut.Nonce = diff.Nonce + } + if len(diff.StorageWrites) > 0 { + if mut.StorageWrites == nil { + mut.StorageWrites = maps.Clone(diff.StorageWrites) + } else { + for key, val := range diff.StorageWrites { + mut.StorageWrites[key] = val + } + } + } + } else { + s.Mutations[account] = diff.Copy() } - res.Accounts[addr] = &aaCopy } - return &res } diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 24dfafa083..5c878fd3d8 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -19,6 +19,8 @@ package bal import ( "bytes" "cmp" + "encoding/hex" + "encoding/json" "errors" "fmt" "io" @@ -33,27 +35,86 @@ import ( "github.com/holiman/uint256" ) -//go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen -out bal_encoding_rlp_generated.go -type BlockAccessList -decoder +//go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen -out bal_encoding_rlp_generated.go -type AccountAccess -decoder // These are objects used as input for the access list encoding. They mirror // the spec format. -// BlockAccessList is the encoding format of ConstructionBlockAccessList. -type BlockAccessList struct { - Accesses []AccountAccess `ssz-max:"300000"` +// BlockAccessList is the encoding format of AccessListBuilder. +type BlockAccessList []AccountAccess + +func (e BlockAccessList) EncodeRLP(_w io.Writer) error { + w := rlp.NewEncoderBuffer(_w) + l := w.List() + for _, access := range e { + access.EncodeRLP(w) + } + w.ListEnd(l) + return w.Flush() } +func (e *BlockAccessList) DecodeRLP(dec *rlp.Stream) error { + if _, err := dec.List(); err != nil { + return err + } + *e = (*e)[:0] + for dec.MoreDataInList() { + var access AccountAccess + if err := access.DecodeRLP(dec); err != nil { + return err + } + *e = append(*e, access) + } + dec.ListEnd() + return nil +} + +func (e *BlockAccessList) JSONString() string { + res, _ := json.MarshalIndent(e.StringableRepresentation(), "", " ") + return string(res) +} + +// StringableRepresentation returns an instance of the block access list +// which can be converted to a human-readable JSON representation. +func (e *BlockAccessList) StringableRepresentation() interface{} { + res := []AccountAccess{} + for _, aa := range *e { + res = append(res, aa) + } + return &res +} + +func (e *BlockAccessList) String() string { + var res bytes.Buffer + enc := json.NewEncoder(&res) + enc.SetIndent("", " ") + // TODO: check error + enc.Encode(e) + return res.String() +} + +// TODO: check that no fields are nil in Validate (unless it's valid for them to be nil) // 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() error { - if !slices.IsSortedFunc(e.Accesses, func(a, b AccountAccess) int { +func (e BlockAccessList) Validate(blockTxCount int) 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.Accesses { - if err := entry.validate(); err != nil { + // check that the accounts are unique + addrs := make(map[common.Address]struct{}) + for _, acct := range e { + addr := acct.Address + if _, ok := addrs[addr]; ok { + return fmt.Errorf("duplicate account in block access list: %x", addr) + } + addrs[addr] = struct{}{} + } + + for _, entry := range e { + if err := entry.validate(blockTxCount); err != nil { return err } } @@ -70,106 +131,228 @@ func (e *BlockAccessList) Hash() common.Hash { // under reasonable conditions. panic(err) } + /* + bal, err := json.MarshalIndent(e.StringableRepresentation(), "", " ") + if err != nil { + panic(err) + } + */ return crypto.Keccak256Hash(enc.Bytes()) } -// encodeBalance encodes the provided balance into 16-bytes. -func encodeBalance(val *uint256.Int) [16]byte { - valBytes := val.Bytes() - if len(valBytes) > 16 { - panic("can't encode value that is greater than 16 bytes in size") +// Copy returns a deep copy of the access list +func (e BlockAccessList) Copy() (res BlockAccessList) { + for _, accountAccess := range e { + res = append(res, accountAccess.Copy()) } - var enc [16]byte - copy(enc[16-len(valBytes):], valBytes[:]) - return enc + return } // encodingBalanceChange is the encoding format of BalanceChange. type encodingBalanceChange struct { - TxIdx uint16 `ssz-size:"2"` - Balance [16]byte `ssz-size:"16"` + TxIdx uint16 `json:"txIndex"` + Balance *uint256.Int `json:"balance"` } // encodingAccountNonce is the encoding format of NonceChange. type encodingAccountNonce struct { - TxIdx uint16 `ssz-size:"2"` - Nonce uint64 `ssz-size:"8"` + TxIdx uint16 `json:"txIndex"` + Nonce uint64 `json:"nonce"` } // encodingStorageWrite is the encoding format of StorageWrites. type encodingStorageWrite struct { - TxIdx uint16 - ValueAfter [32]byte `ssz-size:"32"` + TxIdx uint16 `json:"txIndex"` + ValueAfter *EncodedStorage `json:"valueAfter"` +} + +// EncodedStorage can represent either a storage key or value +type EncodedStorage struct { + inner *uint256.Int +} + +var _ rlp.Encoder = &EncodedStorage{} +var _ rlp.Decoder = &EncodedStorage{} + +func (e *EncodedStorage) ToHash() common.Hash { + if e == nil { + return common.Hash{} + } + return e.inner.Bytes32() +} + +func newEncodedStorageFromHash(hash common.Hash) *EncodedStorage { + return &EncodedStorage{ + new(uint256.Int).SetBytes(hash[:]), + } +} + +func (s *EncodedStorage) UnmarshalJSON(b []byte) error { + var str string + if err := json.Unmarshal(b, &str); err != nil { + return err + } + + str = strings.TrimLeft(str, "0x") + if len(str) == 0 { + return nil + } + + if len(str)%2 == 1 { + str = "0" + str + } + + val, err := hex.DecodeString(str) + if err != nil { + return err + } + + if len(val) > 32 { + return fmt.Errorf("storage key/value cannot be greater than 32 bytes") + } + + // TODO: check is s == nil ?? should be programmer error + + *s = EncodedStorage{ + inner: new(uint256.Int).SetBytes(val), + } + return nil +} + +func (s EncodedStorage) MarshalJSON() ([]byte, error) { + return json.Marshal(s.inner.Hex()) +} + +func (s *EncodedStorage) EncodeRLP(_w io.Writer) error { + return s.inner.EncodeRLP(_w) +} + +func (s *EncodedStorage) DecodeRLP(dec *rlp.Stream) error { + if s == nil { + *s = EncodedStorage{} + } + s.inner = uint256.NewInt(0) + return dec.ReadUint256(s.inner) } // encodingStorageWrite is the encoding format of SlotWrites. type encodingSlotWrites struct { - Slot [32]byte `ssz-size:"32"` - Accesses []encodingStorageWrite `ssz-max:"300000"` + Slot *EncodedStorage `json:"slot"` + Accesses []encodingStorageWrite `json:"accesses"` } // validate returns an instance of the encoding-representation slot writes in // working representation. -func (e *encodingSlotWrites) validate() error { - if slices.IsSortedFunc(e.Accesses, func(a, b encodingStorageWrite) int { +func (e *encodingSlotWrites) validate(blockTxCount int) error { + if !slices.IsSortedFunc(e.Accesses, func(a, b encodingStorageWrite) int { return cmp.Compare[uint16](a.TxIdx, b.TxIdx) }) { - return nil + return errors.New("storage write tx indices not in order") } - return errors.New("storage write tx indices not in order") + // TODO: add test that covers there are actually storage modifications here + // if there aren't, it should be a bad block + if len(e.Accesses) == 0 { + return fmt.Errorf("empty storage writes") + } else if int(e.Accesses[len(e.Accesses)-1].TxIdx) >= blockTxCount+2 { + return fmt.Errorf("storage access reported index higher than allowed") + } + return nil } -// AccountAccess is the encoding format of ConstructionAccountAccess. +// AccountAccess is the encoding format of ConstructionAccountAccesses. type AccountAccess struct { - Address [20]byte `ssz-size:"20"` // 20-byte Ethereum address - StorageWrites []encodingSlotWrites `ssz-max:"300000"` // Storage changes (slot -> [tx_index -> new_value]) - StorageReads [][32]byte `ssz-max:"300000"` // Read-only storage keys - BalanceChanges []encodingBalanceChange `ssz-max:"300000"` // Balance changes ([tx_index -> post_balance]) - NonceChanges []encodingAccountNonce `ssz-max:"300000"` // Nonce changes ([tx_index -> new_nonce]) - Code []CodeChange `ssz-max:"1"` // Code changes ([tx_index -> new_code]) + Address common.Address `json:"address,omitempty"` // 20-byte Ethereum address + StorageChanges []encodingSlotWrites `json:"storageChanges,omitempty"` // EncodedStorage changes (slot -> [tx_index -> new_value]) + StorageReads []*EncodedStorage `json:"storageReads,omitempty"` // Read-only storage keys + BalanceChanges []encodingBalanceChange `json:"balanceChanges,omitempty"` // Balance changes ([tx_index -> post_balance]) + NonceChanges []encodingAccountNonce `json:"nonceChanges,omitempty"` // Nonce changes ([tx_index -> new_nonce]) + CodeChanges []CodeChange `json:"code,omitempty"` // CodeChanges changes ([tx_index -> new_code]) } // 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() error { +func (e *AccountAccess) validate(blockTxCount int) error { // Check the storage write slots are sorted in order - if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { - return bytes.Compare(a.Slot[:], b.Slot[:]) + if !slices.IsSortedFunc(e.StorageChanges, func(a, b encodingSlotWrites) int { + aHash, bHash := a.Slot.ToHash(), b.Slot.ToHash() + return bytes.Compare(aHash[:], bHash[:]) }) { return errors.New("storage writes slots not in lexicographic order") } - for _, write := range e.StorageWrites { - if err := write.validate(); err != nil { + for _, write := range e.StorageChanges { + if err := write.validate(blockTxCount); err != nil { return err } } + readKeys := make(map[common.Hash]struct{}) + writeKeys := make(map[common.Hash]struct{}) + for _, readKey := range e.StorageReads { + if _, ok := readKeys[readKey.ToHash()]; ok { + return errors.New("duplicate read key") + } + readKeys[readKey.ToHash()] = struct{}{} + } + for _, write := range e.StorageChanges { + writeKey := write.Slot + if _, ok := writeKeys[writeKey.ToHash()]; ok { + return errors.New("duplicate write key") + } + writeKeys[writeKey.ToHash()] = struct{}{} + } + + for readKey := range readKeys { + if _, ok := writeKeys[readKey]; ok { + return errors.New("storage key reported in both read/write sets") + } + } // Check the storage read slots are sorted in order - if !slices.IsSortedFunc(e.StorageReads, func(a, b [32]byte) int { - return bytes.Compare(a[:], b[:]) + if !slices.IsSortedFunc(e.StorageReads, func(a, b *EncodedStorage) int { + aHash, bHash := a.ToHash(), b.ToHash() + return bytes.Compare(aHash[:], bHash[:]) }) { return errors.New("storage read slots not in lexicographic order") } // Check the balance changes are sorted in order + // and that none of them report an index above what is allowed if !slices.IsSortedFunc(e.BalanceChanges, func(a, b encodingBalanceChange) int { return cmp.Compare[uint16](a.TxIdx, b.TxIdx) }) { return errors.New("balance changes not in ascending order by tx index") } + if len(e.BalanceChanges) > 0 && int(e.BalanceChanges[len(e.BalanceChanges)-1].TxIdx) > blockTxCount+2 { + return errors.New("highest balance change index beyond what is allowed") + } // Check the nonce changes are sorted in order + // and that none of them report an index above what is allowed if !slices.IsSortedFunc(e.NonceChanges, func(a, b encodingAccountNonce) int { return cmp.Compare[uint16](a.TxIdx, b.TxIdx) }) { return errors.New("nonce changes not in ascending order by tx index") } + if len(e.CodeChanges) > 0 && int(e.NonceChanges[len(e.NonceChanges)-1].TxIdx) >= blockTxCount+2 { + return errors.New("highest nonce change index beyond what is allowed") + } - // Convert code change - if len(e.Code) == 1 { - if len(e.Code[0].Code) > params.MaxCodeSize { - return errors.New("code change contained oversized code") + // TODO: contact testing team to add a test case which has the code changes out of order, + // as it wasn't checked here previously + if !slices.IsSortedFunc(e.CodeChanges, func(a, b CodeChange) int { + return cmp.Compare[uint16](a.TxIdx, b.TxIdx) + }) { + return errors.New("code changes not in ascending order") + } + if len(e.CodeChanges) > 0 && int(e.CodeChanges[len(e.CodeChanges)-1].TxIdx) >= blockTxCount+2 { + return errors.New("highest code change index beyond what is allowed") + } + + // validate that code changes could plausibly be correct (none exceed + // max code size of a contract) + for _, codeChange := range e.CodeChanges { + if len(codeChange.Code) > params.MaxCodeSize { + return fmt.Errorf("code change contained oversized code") } } return nil @@ -183,40 +366,39 @@ func (e *AccountAccess) Copy() AccountAccess { BalanceChanges: slices.Clone(e.BalanceChanges), NonceChanges: slices.Clone(e.NonceChanges), } - for _, storageWrite := range e.StorageWrites { - res.StorageWrites = append(res.StorageWrites, encodingSlotWrites{ + for _, storageWrite := range e.StorageChanges { + res.StorageChanges = append(res.StorageChanges, encodingSlotWrites{ Slot: storageWrite.Slot, Accesses: slices.Clone(storageWrite.Accesses), }) } - if len(e.Code) == 1 { - res.Code = []CodeChange{ - { - e.Code[0].TxIndex, - bytes.Clone(e.Code[0].Code), - }, - } + for _, codeChange := range e.CodeChanges { + res.CodeChanges = append(res.CodeChanges, + CodeChange{ + codeChange.TxIdx, + bytes.Clone(codeChange.Code), + }) } return res } // EncodeRLP returns the RLP-encoded access list -func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error { - return b.toEncodingObj().EncodeRLP(wr) +func (c ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error { + return c.ToEncodingObj().EncodeRLP(wr) } var _ rlp.Encoder = &ConstructionBlockAccessList{} -// toEncodingObj creates an instance of the ConstructionAccountAccess of the type that is +// toEncodingObj creates an instance of the ConstructionAccountAccesses of the type that is // used as input for the encoding. -func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAccess { +func (a *ConstructionAccountAccesses) toEncodingObj(addr common.Address) AccountAccess { res := AccountAccess{ Address: addr, - StorageWrites: make([]encodingSlotWrites, 0), - StorageReads: make([][32]byte, 0), + StorageChanges: make([]encodingSlotWrites, 0), + StorageReads: make([]*EncodedStorage, 0), BalanceChanges: make([]encodingBalanceChange, 0), NonceChanges: make([]encodingAccountNonce, 0), - Code: nil, + CodeChanges: make([]CodeChange, 0), } // Convert write slots @@ -224,7 +406,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc slices.SortFunc(writeSlots, common.Hash.Cmp) for _, slot := range writeSlots { var obj encodingSlotWrites - obj.Slot = slot + obj.Slot = newEncodedStorageFromHash(slot) slotWrites := a.StorageWrites[slot] obj.Accesses = make([]encodingStorageWrite, 0, len(slotWrites)) @@ -234,17 +416,17 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc for _, index := range indices { obj.Accesses = append(obj.Accesses, encodingStorageWrite{ TxIdx: index, - ValueAfter: slotWrites[index], + ValueAfter: newEncodedStorageFromHash(slotWrites[index]), }) } - res.StorageWrites = append(res.StorageWrites, obj) + res.StorageChanges = append(res.StorageChanges, obj) } // Convert read slots readSlots := slices.Collect(maps.Keys(a.StorageReads)) slices.SortFunc(readSlots, common.Hash.Cmp) for _, slot := range readSlots { - res.StorageReads = append(res.StorageReads, slot) + res.StorageReads = append(res.StorageReads, newEncodedStorageFromHash(slot)) } // Convert balance changes @@ -253,7 +435,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc for _, idx := range balanceIndices { res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{ TxIdx: idx, - Balance: encodeBalance(a.BalanceChanges[idx]), + Balance: new(uint256.Int).Set(a.BalanceChanges[idx]), }) } @@ -268,77 +450,31 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc } // Convert code change - if a.CodeChange != nil { - res.Code = []CodeChange{ - { - a.CodeChange.TxIndex, - bytes.Clone(a.CodeChange.Code), - }, - } + codeChangeIdxs := slices.Collect(maps.Keys(a.CodeChanges)) + slices.SortFunc(codeChangeIdxs, cmp.Compare[uint16]) + for _, idx := range codeChangeIdxs { + res.CodeChanges = append(res.CodeChanges, CodeChange{ + idx, + bytes.Clone(a.CodeChanges[idx].Code), + }) } 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 (c ConstructionBlockAccessList) ToEncodingObj() *BlockAccessList { var addresses []common.Address - for addr := range b.Accounts { + for addr := range c { addresses = append(addresses, addr) } slices.SortFunc(addresses, common.Address.Cmp) var res BlockAccessList for _, addr := range addresses { - res.Accesses = append(res.Accesses, b.Accounts[addr].toEncodingObj(addr)) + res = append(res, c[addr].toEncodingObj(addr)) } return &res } -func (e *BlockAccessList) PrettyPrint() string { - var res bytes.Buffer - printWithIndent := func(indent int, text string) { - fmt.Fprintf(&res, "%s%s\n", strings.Repeat(" ", indent), text) - } - for _, accountDiff := range e.Accesses { - printWithIndent(0, fmt.Sprintf("%x:", accountDiff.Address)) - - printWithIndent(1, "storage writes:") - for _, sWrite := range accountDiff.StorageWrites { - printWithIndent(2, fmt.Sprintf("%x:", sWrite.Slot)) - for _, access := range sWrite.Accesses { - printWithIndent(3, fmt.Sprintf("%d: %x", access.TxIdx, access.ValueAfter)) - } - } - - printWithIndent(1, "storage reads:") - for _, slot := range accountDiff.StorageReads { - printWithIndent(2, fmt.Sprintf("%x", slot)) - } - - printWithIndent(1, "balance changes:") - for _, change := range accountDiff.BalanceChanges { - balance := new(uint256.Int).SetBytes(change.Balance[:]).String() - printWithIndent(2, fmt.Sprintf("%d: %s", change.TxIdx, balance)) - } - - printWithIndent(1, "nonce changes:") - for _, change := range accountDiff.NonceChanges { - printWithIndent(2, fmt.Sprintf("%d: %d", change.TxIdx, change.Nonce)) - } - - if len(accountDiff.Code) > 0 { - printWithIndent(1, "code:") - printWithIndent(2, fmt.Sprintf("%d: %x", accountDiff.Code[0].TxIndex, accountDiff.Code[0].Code)) - } - } - return res.String() -} - -// Copy returns a deep copy of the access list -func (e *BlockAccessList) Copy() (res BlockAccessList) { - for _, accountAccess := range e.Accesses { - res.Accesses = append(res.Accesses, accountAccess.Copy()) - } - return -} +type ContractCode []byte diff --git a/core/types/bal/bal_encoding_json.go b/core/types/bal/bal_encoding_json.go new file mode 100644 index 0000000000..bf25345b5e --- /dev/null +++ b/core/types/bal/bal_encoding_json.go @@ -0,0 +1,107 @@ +package bal + +import ( + "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rlp" +) + +func (c *ContractCode) MarshalJSON() ([]byte, error) { + hexStr := fmt.Sprintf("%x", *c) + return json.Marshal(hexStr) +} +func (e encodingBalanceChange) MarshalJSON() ([]byte, error) { + type Alias encodingBalanceChange + return json.Marshal(&struct { + TxIdx string `json:"txIndex"` + *Alias + }{ + TxIdx: fmt.Sprintf("0x%x", e.TxIdx), + Alias: (*Alias)(&e), + }) +} + +func (e *encodingBalanceChange) UnmarshalJSON(data []byte) error { + type Alias encodingBalanceChange + aux := &struct { + TxIdx string `json:"txIndex"` + *Alias + }{ + Alias: (*Alias)(e), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + if len(aux.TxIdx) >= 2 && aux.TxIdx[:2] == "0x" { + if _, err := fmt.Sscanf(aux.TxIdx, "0x%x", &e.TxIdx); err != nil { + return err + } + } + return nil +} +func (e encodingAccountNonce) MarshalJSON() ([]byte, error) { + type Alias encodingAccountNonce + return json.Marshal(&struct { + TxIdx string `json:"txIndex"` + Nonce string `json:"nonce"` + *Alias + }{ + TxIdx: fmt.Sprintf("0x%x", e.TxIdx), + Nonce: fmt.Sprintf("0x%x", e.Nonce), + Alias: (*Alias)(&e), + }) +} + +func (e *encodingAccountNonce) UnmarshalJSON(data []byte) error { + type Alias encodingAccountNonce + aux := &struct { + TxIdx string `json:"txIndex"` + Nonce string `json:"nonce"` + *Alias + }{ + Alias: (*Alias)(e), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + if len(aux.TxIdx) >= 2 && aux.TxIdx[:2] == "0x" { + if _, err := fmt.Sscanf(aux.TxIdx, "0x%x", &e.TxIdx); err != nil { + return err + } + } + if len(aux.Nonce) >= 2 && aux.Nonce[:2] == "0x" { + if _, err := fmt.Sscanf(aux.Nonce, "0x%x", &e.Nonce); err != nil { + return err + } + } + return nil +} + +// UnmarshalJSON implements json.Unmarshaler to decode from RLP hex bytes +func (b *BlockAccessList) UnmarshalJSON(input []byte) error { + // Handle both hex string and object formats + var hexBytes hexutil.Bytes + if err := json.Unmarshal(input, &hexBytes); err == nil { + // It's a hex string, decode from RLP + return rlp.DecodeBytes(hexBytes, b) + } + + // Otherwise try to unmarshal as structured JSON + var tmp []AccountAccess + if err := json.Unmarshal(input, &tmp); err != nil { + return err + } + *b = BlockAccessList(tmp) + return nil +} + +// MarshalJSON implements json.Marshaler to encode as RLP hex bytes +func (b BlockAccessList) MarshalJSON() ([]byte, error) { + // Encode to RLP then to hex + rlpBytes, err := rlp.EncodeToBytes(b) + if err != nil { + return nil, err + } + return json.Marshal(hexutil.Bytes(rlpBytes)) +} diff --git a/core/types/bal/bal_encoding_rlp_generated.go b/core/types/bal/bal_encoding_rlp_generated.go index 0d52395329..832996b4f0 100644 --- a/core/types/bal/bal_encoding_rlp_generated.go +++ b/core/types/bal/bal_encoding_rlp_generated.go @@ -2,275 +2,260 @@ package bal +import "github.com/ethereum/go-ethereum/common" import "github.com/ethereum/go-ethereum/rlp" +import "github.com/holiman/uint256" import "io" -func (obj *BlockAccessList) EncodeRLP(_w io.Writer) error { +func (obj *AccountAccess) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) _tmp0 := w.List() + w.WriteBytes(obj.Address[:]) _tmp1 := w.List() - for _, _tmp2 := range obj.Accesses { + for _, _tmp2 := range obj.StorageChanges { _tmp3 := w.List() - w.WriteBytes(_tmp2.Address[:]) + if err := _tmp2.Slot.EncodeRLP(w); err != nil { + return err + } _tmp4 := w.List() - for _, _tmp5 := range _tmp2.StorageWrites { + for _, _tmp5 := range _tmp2.Accesses { _tmp6 := w.List() - w.WriteBytes(_tmp5.Slot[:]) - _tmp7 := w.List() - for _, _tmp8 := range _tmp5.Accesses { - _tmp9 := w.List() - w.WriteUint64(uint64(_tmp8.TxIdx)) - w.WriteBytes(_tmp8.ValueAfter[:]) - w.ListEnd(_tmp9) + w.WriteUint64(uint64(_tmp5.TxIdx)) + if err := _tmp5.ValueAfter.EncodeRLP(w); err != nil { + return err } - w.ListEnd(_tmp7) w.ListEnd(_tmp6) } w.ListEnd(_tmp4) - _tmp10 := w.List() - for _, _tmp11 := range _tmp2.StorageReads { - w.WriteBytes(_tmp11[:]) - } - w.ListEnd(_tmp10) - _tmp12 := w.List() - for _, _tmp13 := range _tmp2.BalanceChanges { - _tmp14 := w.List() - w.WriteUint64(uint64(_tmp13.TxIdx)) - w.WriteBytes(_tmp13.Balance[:]) - w.ListEnd(_tmp14) - } - w.ListEnd(_tmp12) - _tmp15 := w.List() - for _, _tmp16 := range _tmp2.NonceChanges { - _tmp17 := w.List() - w.WriteUint64(uint64(_tmp16.TxIdx)) - w.WriteUint64(_tmp16.Nonce) - w.ListEnd(_tmp17) - } - w.ListEnd(_tmp15) - _tmp18 := w.List() - for _, _tmp19 := range _tmp2.Code { - _tmp20 := w.List() - w.WriteUint64(uint64(_tmp19.TxIndex)) - w.WriteBytes(_tmp19.Code) - w.ListEnd(_tmp20) - } - w.ListEnd(_tmp18) w.ListEnd(_tmp3) } w.ListEnd(_tmp1) + _tmp7 := w.List() + for _, _tmp8 := range obj.StorageReads { + if err := _tmp8.EncodeRLP(w); err != nil { + return err + } + } + w.ListEnd(_tmp7) + _tmp9 := w.List() + for _, _tmp10 := range obj.BalanceChanges { + _tmp11 := w.List() + w.WriteUint64(uint64(_tmp10.TxIdx)) + if _tmp10.Balance == nil { + w.Write(rlp.EmptyString) + } else { + w.WriteUint256(_tmp10.Balance) + } + w.ListEnd(_tmp11) + } + w.ListEnd(_tmp9) + _tmp12 := w.List() + for _, _tmp13 := range obj.NonceChanges { + _tmp14 := w.List() + w.WriteUint64(uint64(_tmp13.TxIdx)) + w.WriteUint64(_tmp13.Nonce) + w.ListEnd(_tmp14) + } + w.ListEnd(_tmp12) + _tmp15 := w.List() + for _, _tmp16 := range obj.CodeChanges { + _tmp17 := w.List() + w.WriteUint64(uint64(_tmp16.TxIdx)) + w.WriteBytes(_tmp16.Code) + w.ListEnd(_tmp17) + } + w.ListEnd(_tmp15) w.ListEnd(_tmp0) return w.Flush() } -func (obj *BlockAccessList) DecodeRLP(dec *rlp.Stream) error { - var _tmp0 BlockAccessList +func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { + var _tmp0 AccountAccess { if _, err := dec.List(); err != nil { return err } - // Accesses: - var _tmp1 []AccountAccess + // Address: + var _tmp1 common.Address + if err := dec.ReadBytes(_tmp1[:]); err != nil { + return err + } + _tmp0.Address = _tmp1 + // StorageChanges: + var _tmp2 []encodingSlotWrites if _, err := dec.List(); err != nil { return err } for dec.MoreDataInList() { - var _tmp2 AccountAccess + var _tmp3 encodingSlotWrites { if _, err := dec.List(); err != nil { return err } - // Address: - var _tmp3 [20]byte - if err := dec.ReadBytes(_tmp3[:]); err != nil { + // Slot: + _tmp4 := new(EncodedStorage) + if err := _tmp4.DecodeRLP(dec); err != nil { return err } - _tmp2.Address = _tmp3 - // StorageWrites: - var _tmp4 []encodingSlotWrites + _tmp3.Slot = _tmp4 + // Accesses: + var _tmp5 []encodingStorageWrite if _, err := dec.List(); err != nil { return err } for dec.MoreDataInList() { - var _tmp5 encodingSlotWrites - { - if _, err := dec.List(); err != nil { - return err - } - // Slot: - var _tmp6 [32]byte - if err := dec.ReadBytes(_tmp6[:]); err != nil { - return err - } - _tmp5.Slot = _tmp6 - // Accesses: - var _tmp7 []encodingStorageWrite - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp8 encodingStorageWrite - { - if _, err := dec.List(); err != nil { - return err - } - // TxIdx: - _tmp9, err := dec.Uint16() - if err != nil { - return err - } - _tmp8.TxIdx = _tmp9 - // ValueAfter: - var _tmp10 [32]byte - if err := dec.ReadBytes(_tmp10[:]); err != nil { - return err - } - _tmp8.ValueAfter = _tmp10 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp7 = append(_tmp7, _tmp8) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp5.Accesses = _tmp7 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp4 = append(_tmp4, _tmp5) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.StorageWrites = _tmp4 - // StorageReads: - var _tmp11 [][32]byte - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp12 [32]byte - if err := dec.ReadBytes(_tmp12[:]); err != nil { - return err - } - _tmp11 = append(_tmp11, _tmp12) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.StorageReads = _tmp11 - // BalanceChanges: - var _tmp13 []encodingBalanceChange - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp14 encodingBalanceChange + var _tmp6 encodingStorageWrite { if _, err := dec.List(); err != nil { return err } // TxIdx: - _tmp15, err := dec.Uint16() + _tmp7, err := dec.Uint16() if err != nil { return err } - _tmp14.TxIdx = _tmp15 - // Balance: - var _tmp16 [16]byte - if err := dec.ReadBytes(_tmp16[:]); err != nil { + _tmp6.TxIdx = _tmp7 + // ValueAfter: + _tmp8 := new(EncodedStorage) + if err := _tmp8.DecodeRLP(dec); err != nil { return err } - _tmp14.Balance = _tmp16 + _tmp6.ValueAfter = _tmp8 if err := dec.ListEnd(); err != nil { return err } } - _tmp13 = append(_tmp13, _tmp14) + _tmp5 = append(_tmp5, _tmp6) } if err := dec.ListEnd(); err != nil { return err } - _tmp2.BalanceChanges = _tmp13 - // NonceChanges: - var _tmp17 []encodingAccountNonce - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp18 encodingAccountNonce - { - if _, err := dec.List(); err != nil { - return err - } - // TxIdx: - _tmp19, err := dec.Uint16() - if err != nil { - return err - } - _tmp18.TxIdx = _tmp19 - // Nonce: - _tmp20, err := dec.Uint64() - if err != nil { - return err - } - _tmp18.Nonce = _tmp20 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp17 = append(_tmp17, _tmp18) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.NonceChanges = _tmp17 - // Code: - var _tmp21 []CodeChange - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp22 CodeChange - { - if _, err := dec.List(); err != nil { - return err - } - // TxIndex: - _tmp23, err := dec.Uint16() - if err != nil { - return err - } - _tmp22.TxIndex = _tmp23 - // Code: - _tmp24, err := dec.Bytes() - if err != nil { - return err - } - _tmp22.Code = _tmp24 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp21 = append(_tmp21, _tmp22) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.Code = _tmp21 + _tmp3.Accesses = _tmp5 if err := dec.ListEnd(); err != nil { return err } } - _tmp1 = append(_tmp1, _tmp2) + _tmp2 = append(_tmp2, _tmp3) } if err := dec.ListEnd(); err != nil { return err } - _tmp0.Accesses = _tmp1 + _tmp0.StorageChanges = _tmp2 + // StorageReads: + var _tmp9 []*EncodedStorage + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + _tmp10 := new(EncodedStorage) + if err := _tmp10.DecodeRLP(dec); err != nil { + return err + } + _tmp9 = append(_tmp9, _tmp10) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.StorageReads = _tmp9 + // BalanceChanges: + var _tmp11 []encodingBalanceChange + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp12 encodingBalanceChange + { + if _, err := dec.List(); err != nil { + return err + } + // TxIdx: + _tmp13, err := dec.Uint16() + if err != nil { + return err + } + _tmp12.TxIdx = _tmp13 + // Balance: + var _tmp14 uint256.Int + if err := dec.ReadUint256(&_tmp14); err != nil { + return err + } + _tmp12.Balance = &_tmp14 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp11 = append(_tmp11, _tmp12) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.BalanceChanges = _tmp11 + // NonceChanges: + var _tmp15 []encodingAccountNonce + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp16 encodingAccountNonce + { + if _, err := dec.List(); err != nil { + return err + } + // TxIdx: + _tmp17, err := dec.Uint16() + if err != nil { + return err + } + _tmp16.TxIdx = _tmp17 + // Nonce: + _tmp18, err := dec.Uint64() + if err != nil { + return err + } + _tmp16.Nonce = _tmp18 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp15 = append(_tmp15, _tmp16) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.NonceChanges = _tmp15 + // CodeChanges: + var _tmp19 []CodeChange + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp20 CodeChange + { + if _, err := dec.List(); err != nil { + return err + } + // TxIdx: + _tmp21, err := dec.Uint16() + if err != nil { + return err + } + _tmp20.TxIdx = _tmp21 + // Code: + _tmp22, err := dec.Bytes() + if err != nil { + return err + } + _tmp20.Code = _tmp22 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp19 = append(_tmp19, _tmp20) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.CodeChanges = _tmp19 if err := dec.ListEnd(); err != nil { return err } diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 29414e414e..b359300c84 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -36,9 +36,9 @@ func equalBALs(a *BlockAccessList, b *BlockAccessList) bool { return true } -func makeTestConstructionBAL() *ConstructionBlockAccessList { - return &ConstructionBlockAccessList{ - map[common.Address]*ConstructionAccountAccess{ +func makeTestConstructionBAL() *AccessListBuilder { + return &AccessListBuilder{ + FinalizedAccesses: map[common.Address]*ConstructionAccountAccesses{ common.BytesToAddress([]byte{0xff, 0xff}): { StorageWrites: map[common.Hash]map[uint16]common.Hash{ common.BytesToHash([]byte{0x01}): { @@ -60,10 +60,10 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { 1: 2, 2: 6, }, - CodeChange: &CodeChange{ - TxIndex: 0, - Code: common.Hex2Bytes("deadbeef"), - }, + CodeChanges: map[uint16]CodeChange{0: { + TxIdx: 0, + Code: common.Hex2Bytes("deadbeef"), + }}, }, common.BytesToAddress([]byte{0xff, 0xff, 0xff}): { StorageWrites: map[common.Hash]map[uint16]common.Hash{ @@ -102,10 +102,10 @@ func TestBALEncoding(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 10000000)); 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 !equalBALs(bal.toEncodingObj(), &dec) { + if !equalBALs(bal.ToEncodingObj(), &dec) { t.Fatal("decoded BAL doesn't match") } } @@ -113,18 +113,18 @@ func TestBALEncoding(t *testing.T) { func makeTestAccountAccess(sort bool) AccountAccess { var ( storageWrites []encodingSlotWrites - storageReads [][32]byte + storageReads []common.Hash balances []encodingBalanceChange nonces []encodingAccountNonce ) for i := 0; i < 5; i++ { slot := encodingSlotWrites{ - Slot: testrand.Hash(), + Slot: newEncodedStorageFromHash(testrand.Hash()), } for j := 0; j < 3; j++ { slot.Accesses = append(slot.Accesses, encodingStorageWrite{ TxIdx: uint16(2 * j), - ValueAfter: testrand.Hash(), + ValueAfter: newEncodedStorageFromHash(testrand.Hash()), }) } if sort { @@ -144,7 +144,7 @@ func makeTestAccountAccess(sort bool) AccountAccess { storageReads = append(storageReads, testrand.Hash()) } if sort { - slices.SortFunc(storageReads, func(a, b [32]byte) int { + slices.SortFunc(storageReads, func(a, b common.Hash) int { return bytes.Compare(a[:], b[:]) }) } @@ -152,7 +152,7 @@ func makeTestAccountAccess(sort bool) AccountAccess { for i := 0; i < 5; i++ { balances = append(balances, encodingBalanceChange{ TxIdx: uint16(2 * i), - Balance: [16]byte(testrand.Bytes(16)), + Balance: new(uint256.Int).SetBytes(testrand.Bytes(32)), }) } if sort { @@ -173,16 +173,20 @@ func makeTestAccountAccess(sort bool) AccountAccess { }) } + var encodedStorageReads []EncodedStorage + for _, slot := range storageReads { + encodedStorageReads = append(encodedStorageReads, newEncodedStorageFromHash(slot)) + } return AccountAccess{ Address: [20]byte(testrand.Bytes(20)), - StorageWrites: storageWrites, - StorageReads: storageReads, + StorageChanges: storageWrites, + StorageReads: encodedStorageReads, BalanceChanges: balances, NonceChanges: nonces, - Code: []CodeChange{ + CodeChanges: []CodeChange{ { - TxIndex: 100, - Code: testrand.Bytes(256), + TxIdx: 100, + Code: testrand.Bytes(256), }, }, } @@ -191,10 +195,10 @@ func makeTestAccountAccess(sort bool) AccountAccess { func makeTestBAL(sort bool) BlockAccessList { list := BlockAccessList{} for i := 0; i < 5; i++ { - list.Accesses = append(list.Accesses, makeTestAccountAccess(sort)) + list = append(list, makeTestAccountAccess(sort)) } if sort { - slices.SortFunc(list.Accesses, func(a, b AccountAccess) int { + slices.SortFunc(list, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) } @@ -214,9 +218,9 @@ func TestBlockAccessListCopy(t *testing.T) { } // Make sure the mutations on copy won't affect the origin - for _, aa := range cpyCpy.Accesses { + for _, aa := range cpyCpy { for i := 0; i < len(aa.StorageReads); i++ { - aa.StorageReads[i] = [32]byte(testrand.Bytes(32)) + aa.StorageReads[i] = testrand.Bytes(32) } } if !reflect.DeepEqual(list, cpy) { @@ -245,8 +249,11 @@ func TestBlockAccessListValidation(t *testing.T) { // Validate the derived block access list cBAL := makeTestConstructionBAL() - listB := cBAL.toEncodingObj() + listB := cBAL.ToEncodingObj() if err := listB.Validate(); err != nil { t.Fatalf("Unexpected validation error: %v", err) } } + +// BALReader test ideas +// * BAL which doesn't have any pre-tx system contracts should return an empty state diff at idx 0 diff --git a/core/types/bal_blocks_test.go b/core/types/bal_blocks_test.go new file mode 100644 index 0000000000..9500cf8ca7 --- /dev/null +++ b/core/types/bal_blocks_test.go @@ -0,0 +1,32 @@ +package types + +import ( + "bytes" + "fmt" + "github.com/ethereum/go-ethereum/rlp" + "io" + "os" + "testing" +) + +func TestBALDecoding(t *testing.T) { + var ( + err error + data []byte + ) + data, err = os.ReadFile("blocks_bal_one.rlp") + if err != nil { + t.Fatalf("error opening file: %v", err) + } + reader := bytes.NewReader(data) + stream := rlp.NewStream(reader, 0) + var blocks Block + for i := 0; err == nil; i++ { + fmt.Printf("decode %d\n", i) + err = stream.Decode(&blocks) + if err != nil && err != io.EOF { + t.Fatalf("error decoding blocks: %v", err) + } + fmt.Printf("block number is %d\n", blocks.NumberU64()) + } +} diff --git a/core/types/block.go b/core/types/block.go index c52c05a4c7..af254403fa 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -28,6 +28,8 @@ import ( "sync/atomic" "time" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rlp" @@ -98,6 +100,9 @@ type Header struct { // RequestsHash was added by EIP-7685 and is ignored in legacy headers. RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + + // BlockAccessListHash was added by EIP-7928 and is ignored in legacy headers. + BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` } // field type overrides for gencodec @@ -175,7 +180,8 @@ func (h *Header) EmptyReceipts() bool { type Body struct { Transactions []*Transaction Uncles []*Header - Withdrawals []*Withdrawal `rlp:"optional"` + Withdrawals []*Withdrawal `rlp:"optional"` + AccessList *bal.BlockAccessList `rlp:"-,nil"` } // Block represents an Ethereum block. @@ -200,6 +206,7 @@ type Block struct { uncles []*Header transactions Transactions withdrawals Withdrawals + accessList *bal.BlockAccessList // caches hash atomic.Pointer[common.Hash] @@ -277,6 +284,12 @@ func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher ListHasher b.withdrawals = slices.Clone(withdrawals) } + if body.AccessList != nil { + balHash := body.AccessList.Hash() + b.header.BlockAccessListHash = &balHash + b.accessList = body.AccessList + } + return b } @@ -344,7 +357,7 @@ func (b *Block) EncodeRLP(w io.Writer) error { // Body returns the non-header content of the block. // Note the returned data is not an independent copy. func (b *Block) Body() *Body { - return &Body{b.transactions, b.uncles, b.withdrawals} + return &Body{b.transactions, b.uncles, b.withdrawals, b.accessList} } // Accessors for body data. These do not return a copy because the content @@ -490,6 +503,10 @@ func (b *Block) WithBody(body Body) *Block { uncles: make([]*Header, len(body.Uncles)), withdrawals: slices.Clone(body.Withdrawals), } + if body.AccessList != nil { + balCopy := body.AccessList.Copy() + block.accessList = &balCopy + } for i := range body.Uncles { block.uncles[i] = CopyHeader(body.Uncles[i]) } diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index 0af12500bd..66856bba36 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -16,28 +16,29 @@ var _ = (*headerMarshaling)(nil) // MarshalJSON marshals as JSON. func (h Header) MarshalJSON() ([]byte, error) { type Header struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase common.Address `json:"miner"` - Root common.Hash `json:"stateRoot" gencodec:"required"` - TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` - Number *hexutil.Big `json:"number" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` - Extra hexutil.Bytes `json:"extraData" gencodec:"required"` - MixDigest common.Hash `json:"mixHash"` - Nonce BlockNonce `json:"nonce"` - BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` - WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` - ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` - RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` - Hash common.Hash `json:"hash"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` + Hash common.Hash `json:"hash"` } var enc Header enc.ParentHash = h.ParentHash @@ -61,6 +62,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.ExcessBlobGas = (*hexutil.Uint64)(h.ExcessBlobGas) enc.ParentBeaconRoot = h.ParentBeaconRoot enc.RequestsHash = h.RequestsHash + enc.BlockAccessListHash = h.BlockAccessListHash enc.Hash = h.Hash() return json.Marshal(&enc) } @@ -68,27 +70,28 @@ func (h Header) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (h *Header) UnmarshalJSON(input []byte) error { type Header struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase *common.Address `json:"miner"` - Root *common.Hash `json:"stateRoot" gencodec:"required"` - TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom *Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` - Number *hexutil.Big `json:"number" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` - MixDigest *common.Hash `json:"mixHash"` - Nonce *BlockNonce `json:"nonce"` - BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` - WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` - ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` - RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase *common.Address `json:"miner"` + Root *common.Hash `json:"stateRoot" gencodec:"required"` + TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom *Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest *common.Hash `json:"mixHash"` + Nonce *BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -169,5 +172,8 @@ func (h *Header) UnmarshalJSON(input []byte) error { if dec.RequestsHash != nil { h.RequestsHash = dec.RequestsHash } + if dec.BlockAccessListHash != nil { + h.BlockAccessListHash = dec.BlockAccessListHash + } return nil } diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_rlp.go index c79aa8a250..e68e8927c2 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -43,7 +43,8 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { _tmp4 := obj.ExcessBlobGas != nil _tmp5 := obj.ParentBeaconRoot != nil _tmp6 := obj.RequestsHash != nil - if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 { + _tmp7 := obj.BlockAccessListHash != nil + if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.BaseFee == nil { w.Write(rlp.EmptyString) } else { @@ -53,41 +54,48 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBigInt(obj.BaseFee) } } - if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 { + if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.WithdrawalsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.WithdrawalsHash[:]) } } - if _tmp3 || _tmp4 || _tmp5 || _tmp6 { + if _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.BlobGasUsed == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.BlobGasUsed)) } } - if _tmp4 || _tmp5 || _tmp6 { + if _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.ExcessBlobGas == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.ExcessBlobGas)) } } - if _tmp5 || _tmp6 { + if _tmp5 || _tmp6 || _tmp7 { if obj.ParentBeaconRoot == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.ParentBeaconRoot[:]) } } - if _tmp6 { + if _tmp6 || _tmp7 { if obj.RequestsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.RequestsHash[:]) } } + if _tmp7 { + if obj.BlockAccessListHash == nil { + w.Write([]byte{0x80}) + } else { + w.WriteBytes(obj.BlockAccessListHash[:]) + } + } w.ListEnd(_tmp0) return w.Flush() } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 23a2cbbf4d..48670da6d3 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -374,7 +374,33 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor return gas, nil } -func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + var ( + gas uint64 + transfersValue = !stack.Back(2).IsZero() + ) + + if transfersValue { + if evm.readOnly { + return 0, ErrWriteProtection + } else if !evm.chainRules.IsEIP4762 { + gas += params.CallValueTransferGas + } + } + + memoryGas, err := memoryGasCost(mem, memorySize) + if err != nil { + return 0, err + } + var overflow bool + if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { + return 0, ErrGasUintOverflow + } + + return gas, nil +} + +func gasCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( gas uint64 transfersValue = !stack.Back(2).IsZero() @@ -391,15 +417,22 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize } else if !evm.StateDB.Exist(address) { gas += params.CallNewAccountGas } - if transfersValue && !evm.chainRules.IsEIP4762 { - gas += params.CallValueTransferGas - } - memoryGas, err := memoryGasCost(mem, memorySize) + + return gas, nil +} +func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + stateless, err := gasCallStateless(evm, contract, stack, mem, memorySize) if err != nil { return 0, err } - var overflow bool - if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { + + stateful, err := gasCallStateful(evm, contract, stack, mem, memorySize) + if err != nil { + return 0, err + } + + gas, overflow := math.SafeAdd(stateless, stateful) + if overflow { return 0, ErrGasUintOverflow } @@ -410,25 +443,39 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { return 0, ErrGasUintOverflow } - return gas, nil } -func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasCallCodeStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return 0, nil +} + +func gasCallCodeStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { memoryGas, err := memoryGasCost(mem, memorySize) if err != nil { return 0, err } var ( - gas uint64 - overflow bool + gas uint64 + overflow bool + transfersValue = !stack.Back(2).IsZero() ) - if stack.Back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 { + if transfersValue && !evm.chainRules.IsEIP4762 { gas += params.CallValueTransferGas } if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { return 0, ErrGasUintOverflow } + return gas, nil +} + +func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + var overflow bool + gas, err := gasCallCodeStateless(evm, contract, stack, mem, memorySize) + if err != nil { + return 0, err + } + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) if err != nil { return 0, err @@ -440,10 +487,16 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory } func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - gas, err := memoryGasCost(mem, memorySize) + var ( + err error + gas uint64 + ) + + gas, err = gasDelegateCallStateless(evm, contract, stack, mem, memorySize) if err != nil { return 0, err } + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) if err != nil { return 0, err @@ -455,11 +508,36 @@ func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me return gas, nil } -func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasDelegateCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return 0, nil +} + +func gasDelegateCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { return 0, err } + return gas, nil +} + +func gasStaticCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return 0, err + } + return gas, nil +} + +func gasStaticCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return 0, nil +} + +func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + gas, err := gasStaticCallStateless(evm, contract, stack, mem, memorySize) + if err != nil { + return 0, err + } + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) if err != nil { return 0, err @@ -477,11 +555,16 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me } var gas uint64 + // EIP150 homestead gas reprice fork: if evm.chainRules.IsEIP150 { gas = params.SelfdestructGasEIP150 var address = common.Address(stack.Back(0).Bytes20()) + if gas > contract.Gas { + return gas, nil + } + if evm.chainRules.IsEIP158 { // if empty and transfers value if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { diff --git a/core/vm/jump_table_export.go b/core/vm/jump_table_export.go index 89a2ebf6f4..dc63e633bd 100644 --- a/core/vm/jump_table_export.go +++ b/core/vm/jump_table_export.go @@ -28,6 +28,8 @@ func LookupInstructionSet(rules params.Rules) (JumpTable, error) { switch { case rules.IsVerkle: return newCancunInstructionSet(), errors.New("verkle-fork not defined yet") + case rules.IsAmsterdam: + return newPragueInstructionSet(), nil case rules.IsOsaka: return newOsakaInstructionSet(), nil case rules.IsPrague: diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index ce394d9384..716a82ab97 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -155,50 +155,12 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem return 0, nil } -func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc { - return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - addr := common.Address(stack.Back(addressPosition).Bytes20()) - // Check slot presence in the access list - warmAccess := evm.StateDB.AddressInAccessList(addr) - // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so - // the cost to charge for cold access, if any, is Cold - Warm - coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 - if !warmAccess { - evm.StateDB.AddAddressToAccessList(addr) - // Charge the remaining difference here already, to correctly calculate available - // gas for call - if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { - return 0, ErrOutOfGas - } - } - // Now call the old calculator, which takes into account - // - create new account - // - transfer value - // - memory expansion - // - 63/64ths rule - gas, err := oldCalculator(evm, contract, stack, mem, memorySize) - if warmAccess || err != nil { - return gas, err - } - // In case of a cold access, we temporarily add the cold charge back, and also - // add it to the returned gas. By adding it to the return, it will be charged - // outside of this function, as part of the dynamic gas, and that will make it - // also become correctly reported to tracers. - contract.Gas += coldCost - - var overflow bool - if gas, overflow = math.SafeAdd(gas, coldCost); overflow { - return 0, ErrGasUintOverflow - } - return gas, nil - } -} - var ( - gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall, 1) - gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCall, 1) - gasStaticCallEIP2929 = makeCallVariantGasCallEIP2929(gasStaticCall, 1) - gasCallCodeEIP2929 = makeCallVariantGasCallEIP2929(gasCallCode, 1) + // TODO: we can use the same functions already defined above for the 7702 gas handlers + gasCallEIP2929 = makeCallVariantGasCall(gasCallStateless, gasCallStateful) + gasDelegateCallEIP2929 = makeCallVariantGasCall(gasDelegateCallStateless, gasDelegateCallStateful) + gasStaticCallEIP2929 = makeCallVariantGasCall(gasStaticCallStateless, gasStaticCallStateful) + gasCallCodeEIP2929 = makeCallVariantGasCall(gasCallCodeStateless, gasCallCodeStateful) gasSelfdestructEIP2929 = makeSelfdestructGasFn(true) // gasSelfdestructEIP3529 implements the changes in EIP-3529 (no refunds) gasSelfdestructEIP3529 = makeSelfdestructGasFn(false) @@ -243,6 +205,10 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { return 0, ErrOutOfGas } } + if contract.Gas < gas { + return gas, nil + } + // if empty and transfers value if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { gas += params.CreateBySelfdestructGas @@ -256,33 +222,25 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { } var ( - innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCall) - gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCall) - gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCall) - gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCode) + gasCallEIP7702 = makeCallVariantGasCall(gasCallStateful, gasCallStateless) + gasDelegateCallEIP7702 = makeCallVariantGasCall(gasDelegateCallStateful, gasDelegateCallStateless) + gasStaticCallEIP7702 = makeCallVariantGasCall(gasStaticCallStateful, gasStaticCallStateless) + gasCallCodeEIP7702 = makeCallVariantGasCall(gasCallCodeStateful, gasCallCodeStateless) ) -func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - // Return early if this call attempts to transfer value in a static context. - // Although it's checked in `gasCall`, EIP-7702 loads the target's code before - // to determine if it is resolving a delegation. This could incorrectly record - // the target in the block access list (BAL) if the call later fails. - transfersValue := !stack.Back(2).IsZero() - if evm.readOnly && transfersValue { - return 0, ErrWriteProtection - } - return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) -} - -func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { +func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( - total uint64 // total dynamic gas used - addr = common.Address(stack.Back(1).Bytes20()) + eip150BaseGas uint64 // gas used for memory expansion, transfer costs -> input to the 63/64 bounding + eip7702Gas uint64 + eip2929Gas uint64 + addr = common.Address(stack.Back(1).Bytes20()) + overflow bool + err error ) // Check slot presence in the access list - if !evm.StateDB.AddressInAccessList(addr) { + if evm.chainRules.IsEIP2929 && !evm.StateDB.AddressInAccessList(addr) { evm.StateDB.AddAddressToAccessList(addr) // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so // the cost to charge for cold access, if any, is Cold - Warm @@ -292,44 +250,87 @@ func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return 0, ErrOutOfGas } - total += coldCost + eip2929Gas = coldCost } - - // Check if code is a delegation and if so, charge for resolution. - if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok { - var cost uint64 - if evm.StateDB.AddressInAccessList(target) { - cost = params.WarmStorageReadCostEIP2929 - } else { - evm.StateDB.AddAddressToAccessList(target) - cost = params.ColdAccountAccessCostEIP2929 - } - if !contract.UseGas(cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { - return 0, ErrOutOfGas - } - total += cost - } - - // Now call the old calculator, which takes into account - // - create new account - // - transfer value - // - memory expansion - // - 63/64ths rule - old, err := oldCalculator(evm, contract, stack, mem, memorySize) + eip150BaseGas, err = oldCalculatorStateless(evm, contract, stack, mem, memorySize) if err != nil { - return old, err + return 0, err } + // ensure the portion of the call cost which doesn't depend on state lookups + // is covered by the provided gas + if contract.Gas < eip150BaseGas { + return 0, ErrOutOfGas + } + + oldStateful, err := oldCalculatorStateful(evm, contract, stack, mem, memorySize) + if err != nil { + return oldStateful, err + } + + // this should cause BAL test failures if uncommented + baseCost, overflow := math.SafeAdd(eip150BaseGas, oldStateful) + if overflow { + return 0, ErrGasUintOverflow + } else if contract.Gas < baseCost { + return 0, ErrOutOfGas + } + + if eip150BaseGas, overflow = math.SafeAdd(eip150BaseGas, oldStateful); overflow { + return 0, ErrOutOfGas + } + + if evm.chainRules.IsPrague { + // Check if code is a delegation and if so, charge for resolution. + if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok { + if evm.StateDB.AddressInAccessList(target) { + eip7702Gas = params.WarmStorageReadCostEIP2929 + } else { + evm.StateDB.AddAddressToAccessList(target) + eip7702Gas = params.ColdAccountAccessCostEIP2929 + } + if !contract.UseGas(eip7702Gas, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + return 0, ErrOutOfGas + } + } + } + + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, eip150BaseGas, stack.Back(0)) + if err != nil { + return 0, err + } + + // TODO: it's not clear what happens if there is enough gas to cover the stateless component + // but not enough to cover the whole call: do all the state reads happen in this case, and + // we fail at the very end? + // Temporarily add the gas charge back to the contract and return value. By // adding it to the return, it will be charged outside of this function, as // part of the dynamic gas. This will ensure it is correctly reported to // tracers. - contract.Gas += total - - var overflow bool - if total, overflow = math.SafeAdd(old, total); overflow { + contract.Gas, overflow = math.SafeAdd(contract.Gas, eip2929Gas) + if overflow { return 0, ErrGasUintOverflow } - return total, nil + contract.Gas, overflow = math.SafeAdd(contract.Gas, eip7702Gas) + if overflow { + return 0, ErrGasUintOverflow + } + + var totalCost uint64 + totalCost, overflow = math.SafeAdd(eip2929Gas, eip7702Gas) + if overflow { + return 0, ErrGasUintOverflow + } + totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp) + if overflow { + return 0, ErrGasUintOverflow + } + totalCost, overflow = math.SafeAdd(totalCost, eip150BaseGas) + if overflow { + return 0, ErrGasUintOverflow + } + + return totalCost, nil } } diff --git a/eth/api_backend.go b/eth/api_backend.go index 3f826b7861..2656395b22 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -19,6 +19,7 @@ package eth import ( "context" "errors" + "fmt" "math/big" "time" @@ -499,3 +500,22 @@ func (b *EthAPIBackend) RPCTxSyncDefaultTimeout() time.Duration { func (b *EthAPIBackend) RPCTxSyncMaxTimeout() time.Duration { return b.eth.config.TxSyncMaxTimeout } + +// GetBlockAccessList returns a block access list for the given number/hash +// or nil if one does not exist. +func (b *EthAPIBackend) BlockAccessListByNumberOrHash(number rpc.BlockNumberOrHash) (interface{}, error) { + var block *types.Block + if num := number.BlockNumber; num != nil { + block = b.eth.blockchain.GetBlockByNumber(uint64(num.Int64())) + } else if hash := number.BlockHash; hash != nil { + block = b.eth.blockchain.GetBlockByHash(*hash) + } + + if block == nil { + return nil, fmt.Errorf("block not found") + } + if block.Body().AccessList == nil { + return nil, nil + } + return block.Body().AccessList.StringableRepresentation(), nil +} diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index e6ecf4ff6a..844780692d 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -198,9 +198,13 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV3(update engine.ForkchoiceStateV1, pa return engine.STATUS_INVALID, attributesErr("missing withdrawals") case params.BeaconRoot == nil: return engine.STATUS_INVALID, attributesErr("missing beacon root") - case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): + case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5, forks.Amsterdam): return engine.STATUS_INVALID, unsupportedForkErr("fcuV3 must only be called for cancun/prague/osaka payloads") } + + if api.checkFork(params.Timestamp, forks.Amsterdam) { + return api.forkchoiceUpdated(update, params, engine.PayloadV4, false) + } } // TODO(matt): the spec requires that fcu is applied when called on a valid // hash, even if params are wrong. To do this we need to split up @@ -455,11 +459,23 @@ func (api *ConsensusAPI) GetPayloadV5(payloadID engine.PayloadID) (*engine.Execu }) } +// GetPayloadV6 returns a cached payload by id. +func (api *ConsensusAPI) GetPayloadV6(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { + if !payloadID.Is(engine.PayloadV4) { + return nil, engine.UnsupportedFork + } + return api.getPayload(payloadID, + false, + []engine.PayloadVersion{engine.PayloadV4}, + nil) +} + // getPayload will retrieve the specified payload and verify it conforms to the // endpoint's allowed payload versions and forks. // // Note passing nil `forks`, `versions` disables the respective check. func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool, versions []engine.PayloadVersion, forks []forks.Fork) (*engine.ExecutionPayloadEnvelope, error) { + log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID) if versions != nil && !payloadID.Is(versions...) { return nil, engine.UnsupportedFork @@ -697,6 +713,33 @@ func (api *ConsensusAPI) NewPayloadV4(params engine.ExecutableData, versionedHas return api.newPayload(params, versionedHashes, beaconRoot, requests, false) } +// NewPayloadV5 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. +func (api *ConsensusAPI) NewPayloadV5(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes) (engine.PayloadStatusV1, error) { + switch { + case params.Withdrawals == nil: + return invalidStatus, paramsErr("nil withdrawals post-shanghai") + case params.ExcessBlobGas == nil: + return invalidStatus, paramsErr("nil excessBlobGas post-cancun") + case params.BlobGasUsed == nil: + return invalidStatus, paramsErr("nil blobGasUsed post-cancun") + case versionedHashes == nil: + return invalidStatus, paramsErr("nil versionedHashes post-cancun") + case beaconRoot == nil: + return invalidStatus, paramsErr("nil beaconRoot post-cancun") + case executionRequests == nil: + return invalidStatus, paramsErr("nil executionRequests post-prague") + case params.BlockAccessList == nil: + return invalidStatus, paramsErr("nil block access list post-amsterdam") + case !api.checkFork(params.Timestamp, forks.Prague, forks.Osaka, forks.Amsterdam): + return invalidStatus, unsupportedForkErr("newPayloadV5 must only be called for amsterdam payloads") + } + requests := convertRequests(executionRequests) + if err := validateRequests(requests); err != nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) + } + return api.newPayload(params, versionedHashes, beaconRoot, requests, false) +} + func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, witness bool) (engine.PayloadStatusV1, error) { // The locking here is, strictly, not required. Without these locks, this can happen: // diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index b0a79295f5..a8a0f71d0c 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -967,6 +967,9 @@ func RPCMarshalBlock(block *types.Block, inclTx bool, fullTx bool, config *param if block.Withdrawals() != nil { fields["withdrawals"] = block.Withdrawals() } + if block.Body().AccessList != nil { + fields["accessList"] = block.Body().AccessList + } return fields } @@ -1336,6 +1339,18 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH } } +// BlockAccessListByBlockNumber returns a block access list for the given block number +// or nil if one does not exist. +func (api *BlockChainAPI) BlockAccessListByBlockNumber(number rpc.BlockNumber) (interface{}, error) { + return api.b.BlockAccessListByNumberOrHash(rpc.BlockNumberOrHash{BlockNumber: &number}) +} + +// BlockAccessListByBlockHash returns a block access list for the given block hash +// or nil if one does not exist. +func (api *BlockChainAPI) BlockAccessListByBlockHash(hash common.Hash) (interface{}, error) { + return api.b.BlockAccessListByNumberOrHash(rpc.BlockNumberOrHash{BlockHash: &hash}) +} + // TransactionAPI exposes methods for reading and creating transaction data. type TransactionAPI struct { b Backend diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index af3d592b82..222a0da479 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -74,6 +74,7 @@ type Backend interface { GetEVM(ctx context.Context, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockCtx *vm.BlockContext) *vm.EVM SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription + BlockAccessListByNumberOrHash(number rpc.BlockNumberOrHash) (interface{}, error) // Transaction pool API SendTx(ctx context.Context, signedTx *types.Transaction) error diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 3c08061313..deaaafadb0 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -347,7 +347,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } blockBody := &types.Body{Transactions: txes, Withdrawals: *block.BlockOverrides.Withdrawals} chainHeadReader := &simChainHeadReader{ctx, sim.b} - b, err := sim.b.Engine().FinalizeAndAssemble(chainHeadReader, header, sim.state, blockBody, receipts) + b, err := sim.b.Engine().FinalizeAndAssemble(chainHeadReader, header, sim.state, blockBody, receipts, nil) if err != nil { return nil, nil, nil, err } diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 0aedffe230..788a3c5cc2 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -474,6 +474,11 @@ web3._extend({ params: 1, inputFormatter: [null], }), + new web3._extend.Method({ + name: 'getEncodedBlockAccessList', + call: 'debug_getEncodedBlockAccessList', + params: 1 + }), ], properties: [] }); @@ -605,7 +610,17 @@ web3._extend({ name: 'config', call: 'eth_config', params: 0, - }) + }), + new web3._extend.Method({ + name: 'getBlockAccessListByBlockNumber', + call: 'eth_blockAccessListByBlockNumber', + params: 1, + }), + new web3._extend.Method({ + name: 'getBlockAccessListByBlockHash', + call: 'eth_blockAccessListByBlockHash', + params: 1, + }), ], properties: [ new web3._extend.Property({ diff --git a/miner/worker.go b/miner/worker.go index 45d7073ed7..dfd701f743 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -19,6 +19,7 @@ package miner import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/core/tracing" "math/big" "sync/atomic" "time" @@ -70,7 +71,8 @@ type environment struct { sidecars []*types.BlobTxSidecar blobs int - witness *stateless.Witness + witness *stateless.Witness + alTracer *core.BlockAccessListTracer } // txFits reports whether the transaction fits into the block size limit. @@ -144,6 +146,9 @@ func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPay } } body := types.Body{Transactions: work.txs, Withdrawals: genParam.withdrawals} + if work.alTracer != nil { + body.AccessList = work.alTracer.AccessList().ToEncodingObj() + } allLogs := make([]*types.Log, 0) for _, r := range work.receipts { @@ -172,10 +177,24 @@ func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPay work.header.RequestsHash = &reqHash } - block, err := miner.engine.FinalizeAndAssemble(miner.chain, work.header, work.state, &body, work.receipts) + // set the block access list on the body after the block has finished executing + // but before the header hash is computed (in FinalizeAndAssemble). + // + // I considered trying to instantiate the beacon consensus engine with a tracer. + // however, the BAL tracer instance is used once per block, while the engine object + // lives for the entire time the client is running. + onBlockFinalization := func() { + if miner.chainConfig.IsAmsterdam(work.header.Number, work.header.Time) { + work.alTracer.OnBlockFinalization() + body.AccessList = work.alTracer.AccessList().ToEncodingObj() + } + } + + block, err := miner.engine.FinalizeAndAssemble(miner.chain, work.header, work.state, &body, work.receipts, onBlockFinalization) if err != nil { return &newPayloadResult{err: err} } + return &newPayloadResult{ block: block, fees: totalFees(block, work.receipts), @@ -266,13 +285,15 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir if miner.chainConfig.IsPrague(header.Number, header.Time) { core.ProcessParentBlockHash(header.ParentHash, env.evm) } + // TODO: verify that we can make blocks that correctly record the pre-tx system calls + // TODO ^ comprehensive miner unit tests return env, nil } // makeEnv creates a new environment for the sealing block. func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address, witness bool) (*environment, error) { // Retrieve the parent state to execute on top. - state, err := miner.chain.StateAt(parent.Root) + sdb, err := miner.chain.StateAt(parent.Root) if err != nil { return nil, err } @@ -281,17 +302,27 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase if err != nil { return nil, err } - state.StartPrefetcher("miner", bundle, nil) + sdb.StartPrefetcher("miner", bundle, nil) + } + var alTracer *core.BlockAccessListTracer + var hooks *tracing.Hooks + var hookedState vm.StateDB = sdb + var vmConfig vm.Config + if miner.chainConfig.IsAmsterdam(header.Number, header.Time) { + alTracer, hooks = core.NewBlockAccessListTracer() + hookedState = state.NewHookedState(sdb, hooks) + vmConfig.Tracer = hooks } // Note the passed coinbase may be different with header.Coinbase. return &environment{ signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time), - state: state, + state: sdb, size: uint64(header.Size()), coinbase: coinbase, header: header, - witness: state.Witness(), - evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}), + witness: sdb.Witness(), + evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), hookedState, miner.chainConfig, vmConfig), + alTracer: alTracer, }, nil } diff --git a/params/config.go b/params/config.go index c5dfad1cd3..1f2e44e523 100644 --- a/params/config.go +++ b/params/config.go @@ -236,6 +236,8 @@ var ( Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, Osaka: DefaultOsakaBlobConfig, + BPO1: DefaultBPO1BlobConfig, + BPO2: DefaultBPO2BlobConfig, }, } @@ -1172,6 +1174,8 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork { // BlobConfig returns the blob config associated with the provided fork. func (c *ChainConfig) BlobConfig(fork forks.Fork) *BlobConfig { switch fork { + case forks.Amsterdam: + return c.BlobScheduleConfig.Amsterdam case forks.BPO5: return c.BlobScheduleConfig.BPO5 case forks.BPO4: @@ -1217,6 +1221,8 @@ func (c *ChainConfig) ActiveSystemContracts(time uint64) map[string]common.Addre // the fork isn't defined or isn't a time-based fork. func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 { switch { + case fork == forks.Amsterdam: + return c.AmsterdamTime case fork == forks.BPO5: return c.BPO5Time case fork == forks.BPO4: diff --git a/tests/block_test.go b/tests/block_test.go index c718b304b6..ff8e5bba0b 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -74,10 +74,9 @@ func TestBlockchain(t *testing.T) { // which run natively, so there's no reason to run them here. } -// TestExecutionSpecBlocktests runs the test fixtures from execution-spec-tests. -func TestExecutionSpecBlocktests(t *testing.T) { - if !common.FileExist(executionSpecBlockchainTestDir) { - t.Skipf("directory %s does not exist", executionSpecBlockchainTestDir) +func testExecutionSpecBlocktests(t *testing.T, testDir string) { + if !common.FileExist(testDir) { + t.Skipf("directory %s does not exist", testDir) } bt := new(testMatcher) @@ -85,11 +84,21 @@ func TestExecutionSpecBlocktests(t *testing.T) { bt.skipLoad(".*prague/eip7251_consolidations/test_system_contract_deployment.json") bt.skipLoad(".*prague/eip7002_el_triggerable_withdrawals/test_system_contract_deployment.json") - bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) { + bt.walk(t, testDir, func(t *testing.T, name string, test *BlockTest) { execBlockTest(t, bt, test) }) } +// TestExecutionSpecBlocktests runs the test fixtures from execution-spec-tests. +func TestExecutionSpecBlocktests(t *testing.T) { + testExecutionSpecBlocktests(t, executionSpecBlockchainTestDir) +} + +// TestExecutionSpecBlocktestsBAL runs the BAL release test fixtures from execution-spec-tests. +func TestExecutionSpecBlocktestsBAL(t *testing.T) { + testExecutionSpecBlocktests(t, executionSpecBALBlockchainTestDir) +} + func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) { // Define all the different flag combinations we should run the tests with, // picking only one for short tests. diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 4f6ab65c1a..9782901f9f 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -97,6 +97,7 @@ type btHeader struct { BlobGasUsed *uint64 ExcessBlobGas *uint64 ParentBeaconBlockRoot *common.Hash + BlockAccessListHash *common.Hash } type btHeaderMarshaling struct { @@ -211,20 +212,21 @@ func (t *BlockTest) Network() string { func (t *BlockTest) genesis(config *params.ChainConfig) *core.Genesis { return &core.Genesis{ - Config: config, - Nonce: t.json.Genesis.Nonce.Uint64(), - Timestamp: t.json.Genesis.Timestamp, - ParentHash: t.json.Genesis.ParentHash, - ExtraData: t.json.Genesis.ExtraData, - GasLimit: t.json.Genesis.GasLimit, - GasUsed: t.json.Genesis.GasUsed, - Difficulty: t.json.Genesis.Difficulty, - Mixhash: t.json.Genesis.MixHash, - Coinbase: t.json.Genesis.Coinbase, - Alloc: t.json.Pre, - BaseFee: t.json.Genesis.BaseFeePerGas, - BlobGasUsed: t.json.Genesis.BlobGasUsed, - ExcessBlobGas: t.json.Genesis.ExcessBlobGas, + Config: config, + Nonce: t.json.Genesis.Nonce.Uint64(), + Timestamp: t.json.Genesis.Timestamp, + ParentHash: t.json.Genesis.ParentHash, + ExtraData: t.json.Genesis.ExtraData, + GasLimit: t.json.Genesis.GasLimit, + GasUsed: t.json.Genesis.GasUsed, + Difficulty: t.json.Genesis.Difficulty, + Mixhash: t.json.Genesis.MixHash, + Coinbase: t.json.Genesis.Coinbase, + Alloc: t.json.Pre, + BaseFee: t.json.Genesis.BaseFeePerGas, + BlobGasUsed: t.json.Genesis.BlobGasUsed, + ExcessBlobGas: t.json.Genesis.ExcessBlobGas, + BlockAccessListHash: t.json.Genesis.BlockAccessListHash, } } diff --git a/tests/gen_btheader.go b/tests/gen_btheader.go index 80ad89e03b..4ddd2195c2 100644 --- a/tests/gen_btheader.go +++ b/tests/gen_btheader.go @@ -38,6 +38,7 @@ func (b btHeader) MarshalJSON() ([]byte, error) { BlobGasUsed *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64 ParentBeaconBlockRoot *common.Hash + BlockAccessListHash *common.Hash } var enc btHeader enc.Bloom = b.Bloom @@ -88,6 +89,7 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { BlobGasUsed *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64 ParentBeaconBlockRoot *common.Hash + BlockAccessListHash *common.Hash } var dec btHeader if err := json.Unmarshal(input, &dec); err != nil { @@ -156,5 +158,8 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { if dec.ParentBeaconBlockRoot != nil { b.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot } + if dec.BlockAccessListHash != nil { + b.BlockAccessListHash = dec.BlockAccessListHash + } return nil } diff --git a/tests/init.go b/tests/init.go index d10b47986c..4c3268c41c 100644 --- a/tests/init.go +++ b/tests/init.go @@ -493,6 +493,38 @@ var Forks = map[string]*params.ChainConfig{ BPO1: bpo1BlobConfig, }, }, + "Amsterdam": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0), + ShanghaiTime: u64(0), + CancunTime: u64(0), + PragueTime: u64(0), + OsakaTime: u64(0), + BPO1Time: u64(0), + BPO2Time: u64(0), + AmsterdamTime: u64(0), + DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, + BlobScheduleConfig: ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + Osaka: params.DefaultOsakaBlobConfig, + BPO1: bpo1BlobConfig, + BPO2: bpo2BlobConfig, + }, + }, "OsakaToBPO1AtTime15k": { ChainID: big.NewInt(1), HomesteadBlock: big.NewInt(0), diff --git a/tests/init_test.go b/tests/init_test.go index b933c9808c..2f2e4101f8 100644 --- a/tests/init_test.go +++ b/tests/init_test.go @@ -34,17 +34,18 @@ import ( ) var ( - baseDir = filepath.Join(".", "testdata") - blockTestDir = filepath.Join(baseDir, "BlockchainTests") - stateTestDir = filepath.Join(baseDir, "GeneralStateTests") - legacyStateTestDir = filepath.Join(baseDir, "LegacyTests", "Constantinople", "GeneralStateTests") - transactionTestDir = filepath.Join(baseDir, "TransactionTests") - rlpTestDir = filepath.Join(baseDir, "RLPTests") - difficultyTestDir = filepath.Join(baseDir, "BasicTests") - executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") - executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests") - executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests") - benchmarksDir = filepath.Join(".", "evm-benchmarks", "benchmarks") + baseDir = filepath.Join(".", "testdata") + blockTestDir = filepath.Join(baseDir, "BlockchainTests") + stateTestDir = filepath.Join(baseDir, "GeneralStateTests") + legacyStateTestDir = filepath.Join(baseDir, "LegacyTests", "Constantinople", "GeneralStateTests") + transactionTestDir = filepath.Join(baseDir, "TransactionTests") + rlpTestDir = filepath.Join(baseDir, "RLPTests") + difficultyTestDir = filepath.Join(baseDir, "BasicTests") + executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") + executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests") + executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests") + benchmarksDir = filepath.Join(".", "evm-benchmarks", "benchmarks") + executionSpecBALBlockchainTestDir = filepath.Join(".", "spec-tests-bal", "fixtures", "blockchain_tests") ) func readJSON(reader io.Reader, value interface{}) error {