diff --git a/beacon/engine/types.go b/beacon/engine/types.go index a312fee88a..b29f09118c 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -21,6 +21,8 @@ import ( "math/big" "slices" + "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/core/types" @@ -82,24 +84,25 @@ type payloadAttributesMarshaling struct { // ExecutableData is the data necessary to execute an EL payload. type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom []byte `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number uint64 `json:"blockNumber" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - GasUsed uint64 `json:"gasUsed" gencodec:"required"` - Timestamp uint64 `json:"timestamp" gencodec:"required"` - ExtraData []byte `json:"extraData" gencodec:"required"` - BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions [][]byte `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *uint64 `json:"blobGasUsed"` - ExcessBlobGas *uint64 `json:"excessBlobGas"` - SlotNumber *uint64 `json:"slotNumber,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *uint64 `json:"blobGasUsed"` + ExcessBlobGas *uint64 `json:"excessBlobGas"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList"` + SlotNumber *uint64 `json:"slotNumber,omitempty"` } // JSON type overrides for executableData. @@ -303,6 +306,8 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H requestsHash = &h } + body := types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals} + header := &types.Header{ ParentHash: data.ParentHash, UncleHash: types.EmptyUncleHash, @@ -326,33 +331,40 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H RequestsHash: requestsHash, SlotNumber: data.SlotNumber, } - return types.NewBlockWithHeader(header). - WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), - nil + + if data.BlockAccessList != nil { + balHash := data.BlockAccessList.Hash() + header.BlockAccessListHash = &balHash + block := types.NewBlockWithHeader(header).WithBody(body).WithAccessList(data.BlockAccessList) + return block, nil + } + + 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(), - SlotNumber: block.SlotNumber(), + BlockHash: block.Hash(), + ParentHash: block.ParentHash(), + FeeRecipient: block.Coinbase(), + StateRoot: block.Root(), + Number: block.NumberU64(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + BaseFeePerGas: block.BaseFee(), + Timestamp: block.Time(), + ReceiptsRoot: block.ReceiptHash(), + LogsBloom: block.Bloom().Bytes(), + Transactions: encodeTransactions(block.Transactions()), + Random: block.MixDigest(), + ExtraData: block.Extra(), + Withdrawals: block.Withdrawals(), + BlobGasUsed: block.BlobGasUsed(), + ExcessBlobGas: block.ExcessBlobGas(), + BlockAccessList: block.AccessList(), + SlotNumber: block.SlotNumber(), } // Add blobs. @@ -391,8 +403,9 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. // ExecutionPayloadBody is used in the response to GetPayloadBodiesByHash and GetPayloadBodiesByRange type ExecutionPayloadBody struct { - TransactionData []hexutil.Bytes `json:"transactions"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` + TransactionData []hexutil.Bytes `json:"transactions"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + AccessList *bal.BlockAccessList `json:"blockAccessList"` } // Client identifiers to support ClientVersionV1. diff --git a/build/checksums.txt b/build/checksums.txt index 1832ce41dd..7751011ce1 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 v5.6.1 +# https://github.com/ethereum/execution-spec-tests/releases +# https://github.com/ethereum/execution-spec-tests/releases/download/bal%40v5.6.1 +741530c88f6a48c15184d1504316c02c3a76c2322c410a04b643a85185dc62e9 fixtures_bal.tar.gz + # version:golang 1.25.9 # https://go.dev/dl/ 0ec9ef8ebcea097aac37decae9f09a7218b451cd96be7d6ed513d8e4bcf909cf go1.25.9.src.tar.gz diff --git a/build/ci.go b/build/ci.go index 173288bcdc..d6e3af1ac0 100644 --- a/build/ci.go +++ b/build/ci.go @@ -176,6 +176,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")) @@ -384,6 +387,7 @@ func doTest(cmdline []string) { // Get test fixtures. if !*short { downloadSpecTestFixtures(csdb, *cachedir) + downloadBALSpecTestFixtures(csdb, *cachedir) } // Configure the toolchain. @@ -449,6 +453,19 @@ func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string return filepath.Join(cachedir, base) } +func downloadBALSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string { + ext := ".tar.gz" + base := "fixtures_bal" + archivePath := filepath.Join(cachedir, base+ext) + if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { + log.Fatal(err) + } + if err := build.ExtractArchive(archivePath, executionSpecTestsBALDir); err != nil { + log.Fatal(err) + } + return filepath.Join(cachedir, base) +} + // doCheckGenerate ensures that re-generating generated files does not cause // any mutations in the source file tree. func doCheckGenerate() { diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index f17829ec53..4d456a1e16 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -258,7 +258,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, snapshot = statedb.Snapshot() gp = gaspool.Snapshot() ) - receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) + _, _, receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) if err != nil { statedb.RevertToSnapshot(snapshot) log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err) @@ -327,11 +327,11 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not parse requests logs: %v", err)) } // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil { + if _, _, err := core.ProcessWithdrawalQueue(&requests, evm); err != nil { return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not process withdrawal requests: %v", err)) } // EIP-7251 - if err := core.ProcessConsolidationQueue(&requests, evm); err != nil { + if _, _, err := core.ProcessConsolidationQueue(&requests, evm); err != nil { return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not process consolidation requests: %v", err)) } } diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 72ac75c036..cc5032e1ae 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -21,6 +21,8 @@ import ( "fmt" "math/big" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" @@ -342,18 +344,21 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine and processes withdrawals on top. -func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) (*bal.StateAccessList, *bal.StateMutations) { if !beacon.IsPoSHeader(header) { - beacon.ethone.Finalize(chain, header, state, body) - return + return beacon.ethone.Finalize(chain, header, state, body) } // Withdrawals processing. for _, w := range body.Withdrawals { + // ensure that target account is included as a read in the BAL even if the withdrawal amount is zero + 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)) state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) } + return state.Finalise(true) // No block reward which is issued by consensus layer instead. } diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index ceaec44656..14ebffb63a 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -27,6 +27,8 @@ import ( "sync" "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/common/lru" @@ -573,8 +575,9 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header // Finalize implements consensus.Engine. There is no post-transaction // consensus rules in clique, do nothing here. -func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) (*bal.StateAccessList, *bal.StateMutations) { // No block rewards in PoA, so the state remains as is + return nil, nil } // Authorize injects a private key into the consensus engine to mint new blocks diff --git a/consensus/consensus.go b/consensus/consensus.go index 4ba389292f..5c0a0be6d7 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" ) @@ -84,7 +85,7 @@ type Engine interface { // // Note: The state database might be updated to reflect any consensus rules // that happen at finalization (e.g. block rewards). - Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) + Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) (*bal.StateAccessList, *bal.StateMutations) // Seal generates a new sealing request for the given input block and pushes // the result into the given channel. diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index ee9d9d97d6..6ac37f17d1 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -22,6 +22,8 @@ import ( "math/big" "time" + "github.com/ethereum/go-ethereum/core/types/bal" + mapset "github.com/deckarep/golang-set/v2" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -504,9 +506,10 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine, accumulating the block and uncle rewards. -func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) (*bal.StateAccessList, *bal.StateMutations) { // Accumulate any block and uncle rewards accumulateRewards(chain.Config(), state, header, body.Uncles) + return nil, nil } // SealHash returns the hash of a block prior to it being sealed. diff --git a/core/block_validator.go b/core/block_validator.go index 008444fbbc..30ab81094c 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -111,6 +111,25 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } } + // block access list hash must be present in header after the Amsterdam hard fork + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if block.Header().BlockAccessListHash == nil { + // TODO: verify that this check isn't also done elsewhere + return fmt.Errorf("block access list hash not set in header") + } + // if the block does not come with an access list, we compute the access list locally + // as part of execution and validate against the header's access list hash. + if block.AccessList() != nil { + if *block.Header().BlockAccessListHash != block.AccessList().Hash() { + return fmt.Errorf("access list hash mismatch. local: %x. remote: %x\n", block.AccessList().Hash(), *block.Header().BlockAccessListHash) + } else if err := block.AccessList().Validate(len(block.Transactions()), block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } + } + } else if block.AccessList() != nil { + return fmt.Errorf("block had access list before amsterdam") + } + // Ancestor block must be known. if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { diff --git a/core/blockchain.go b/core/blockchain.go index 296ef6bc16..8f812803d8 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2112,11 +2112,12 @@ type ExecuteConfig struct { // it writes the block and associated state to database. func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, block *types.Block, config ExecuteConfig) (result *blockProcessingResult, blockEndErr error) { var ( - err error - startTime = time.Now() - statedb *state.StateDB - interrupt atomic.Bool - sdb state.Database + err error + startTime = time.Now() + statedb *state.StateDB + interrupt atomic.Bool + sdb state.Database + isAmsterdam = bc.chainConfig.IsAmsterdam(block.Number(), block.Time()) ) defer interrupt.Store(true) // terminate the prefetch at the end @@ -2240,6 +2241,37 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, } vtime := time.Since(vstart) + if isAmsterdam { + computedAccessList := res.AccessList.ToEncodingObj() + computedAccessListHash := computedAccessList.Hash() + + if *block.Header().BlockAccessListHash != computedAccessListHash { + err := fmt.Errorf("block header access list hash mismatch (remote =%x local=%x)", *block.Header().BlockAccessListHash, computedAccessListHash) + bc.reportBadBlock(block, res, err) + return nil, err + } + // note that we don't validate that the computed BAL's size aligns with the gas + // limit here because it should be impossible case if the parameters in 7928 + // are tuned correctly. + + if block.AccessList() == nil { + // attach the computed access list to the block so it gets persisted + // when the block is written to disk + block = block.WithAccessList(computedAccessList) + } + // Failing the access list max size validation should be impossible here + // better safe than sorry. + if err := computedAccessList.ValidateGasLimit(block.GasLimit()); err != nil { + err := fmt.Errorf("block access list validation failed: %v", err) + bc.reportBadBlock(block, res, err) + return nil, err + } else if block.AccessList().Hash() != computedAccessListHash { + err := fmt.Errorf("block access list hash mismatch (remote=%x computed=%x)", block.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 ffeea2054c..5d83d64157 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -20,6 +20,8 @@ import ( "fmt" "math/big" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc" @@ -51,6 +53,8 @@ type BlockGen struct { withdrawals []*types.Withdrawal engine consensus.Engine + + accessList *bal.ConstructionBlockAccessList } // SetCoinbase sets the coinbase of the generated block. @@ -117,11 +121,15 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig) ) b.statedb.SetTxContext(tx.Hash(), len(b.txs)) - receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) + txAccesses, txMut, receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) if err != nil { panic(err) } b.header.GasUsed = b.gasPool.Used() + if b.accessList != nil { + b.accessList.AddTransactionMutations(txMut, len(b.txs)) + b.accessList.AddAccesses(txAccesses) + } // Merge the tx-local access event into the "block-local" one, in order to collect // all values, so that the witness can be built. @@ -325,17 +333,29 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { if err := ParseDepositLogs(&requests, blockLogs, b.cm.config); err != nil { panic(fmt.Sprintf("failed to parse deposit log: %v", err)) } + // TODO: these accesses should be accumulated in the BAL // create EVM for system calls blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) + + mut := new(bal.StateMutations) // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm); err != nil { + withdrawalAccess, withdrawalMut, err := ProcessWithdrawalQueue(&requests, evm) + if err != nil { panic(fmt.Sprintf("could not process withdrawal requests: %v", err)) } + mut.Merge(withdrawalMut) // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm); err != nil { + consolidationAccess, consolidationMut, err := ProcessConsolidationQueue(&requests, evm) + if err != nil { panic(fmt.Sprintf("could not process consolidation requests: %v", err)) } + mut.Merge(consolidationMut) + if b.cm.config.IsAmsterdam(b.header.Number, b.header.Time) { + b.accessList.AddAccesses(withdrawalAccess) + b.accessList.AddAccesses(consolidationAccess) + b.accessList.AddBlockFinalizeMutations(mut) + } } return requests } @@ -364,7 +384,10 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse genblock := func(i int, parent *types.Block, triedb *triedb.Database, statedb *state.StateDB) (*types.Block, types.Receipts) { b := &BlockGen{i: i, cm: cm, parent: parent, statedb: statedb, engine: engine} b.header = cm.makeHeader(parent, statedb, b.engine) - + isAmsterdam := config.IsAmsterdam(b.header.Number, b.header.Time) + if isAmsterdam { + b.accessList = bal.NewConstructionBlockAccessList() + } // Set the difficulty for clique block. The chain maker doesn't have access // to a chain, so the difficulty will be left unset (nil). Set it here to the // correct value. @@ -391,14 +414,32 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse misc.ApplyDAOHardFork(statedb) } + preTxMutations := bal.NewStateMutations() if config.IsPrague(b.header.Number, b.header.Time) || config.IsUBT(b.header.Number, b.header.Time) { // EIP-2935 blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase) blockContext.Random = &common.Hash{} // enable post-merge instruction set evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{}) - ProcessParentBlockHash(b.header.ParentHash, evm) + accesses, mutations := ProcessParentBlockHash(b.header.ParentHash, evm) + if isAmsterdam { + preTxMutations.Merge(mutations) + b.accessList.AddAccesses(accesses) + } + + beaconRoot := common.Hash{} + if b.header.ParentBeaconRoot != nil { + beaconRoot = *b.header.ParentBeaconRoot + } + reads, writes := ProcessBeaconBlockRoot(beaconRoot, evm) + if isAmsterdam { + preTxMutations.Merge(writes) + b.accessList.AddAccesses(reads) + b.accessList.AddBlockInitMutations(preTxMutations) + } } + // TODO: what about the parent beacon root, and post-block system contract (forget what it is rn)? + // Execute any user modifications to the block if gen != nil { gen(i, b) diff --git a/core/chain_makers_test.go b/core/chain_makers_test.go index cc9672199e..5bb05653b8 100644 --- a/core/chain_makers_test.go +++ b/core/chain_makers_test.go @@ -181,6 +181,175 @@ func TestGeneratePOSChain(t *testing.T) { } } +func TestGenerateBALChain(t *testing.T) { + var ( + keyHex = "9c647b8b7c4e7c3490668fb6c11473619db80c93704c70893d3813af4090c39c" + key, _ = crypto.HexToECDSA(keyHex) + address = crypto.PubkeyToAddress(key.PublicKey) // 658bdf435d810c91414ec09147daa6db62406379 + aa = common.Address{0xaa} + bb = common.Address{0xbb} + funds = big.NewInt(0).Mul(big.NewInt(1337), big.NewInt(params.Ether)) + config = *params.MergedTestChainConfig + gspec = &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + address: {Balance: funds}, + params.BeaconRootsAddress: {Code: params.BeaconRootsCode}, + params.WithdrawalQueueAddress: {Code: params.WithdrawalQueueCode}, + params.ConsolidationQueueAddress: {Code: params.ConsolidationQueueCode}, + params.HistoryStorageAddress: {Code: params.HistoryStorageCode}, + }, + BaseFee: big.NewInt(params.InitialBaseFee), + Difficulty: common.Big0, + GasLimit: 5_000_000, + + // TODO: why do I have to set the bal hash here, and why does it trigger an issue with the genesis + // being reported as not present if I omit the following line? + BlockAccessListHash: &common.Hash{}, // TODO: probably bad to initialize this to something other than keccak(nil) but idk + } + gendb = rawdb.NewMemoryDatabase() + db = rawdb.NewMemoryDatabase() + ) + + // init 0xaa with some storage elements + storage := make(map[common.Hash]common.Hash) + storage[common.Hash{0x00}] = common.Hash{0x00} + storage[common.Hash{0x01}] = common.Hash{0x01} + storage[common.Hash{0x02}] = common.Hash{0x02} + storage[common.Hash{0x03}] = common.HexToHash("0303") + gspec.Alloc[aa] = types.Account{ + Balance: common.Big1, + Nonce: 1, + Storage: storage, + Code: common.Hex2Bytes("6042"), + } + gspec.Alloc[bb] = types.Account{ + Balance: common.Big2, + Nonce: 1, + Storage: storage, + Code: common.Hex2Bytes("600154600354"), + } + genesis := gspec.MustCommit(gendb, triedb.NewDatabase(gendb, triedb.HashDefaults)) + engine := beacon.New(ethash.NewFaker()) + + genchain, genreceipts := GenerateChain(gspec.Config, genesis, engine, gendb, 4, func(i int, gen *BlockGen) { + // TODO: I think we can remove SetBeaconRoot entirely + // and provide a different mechanism to set it? + + // gen.SetParentBeaconRoot(common.Hash{byte(i + 1)}) + + if gspec.Config.IsAmsterdam(gen.header.Number, gen.header.Time) { + // TODO: parameterize the slot num + gen.header.SlotNumber = new(uint64) + *gen.header.SlotNumber = gen.header.Number.Uint64() + } + // Add value transfer tx. + tx := types.MustSignNewTx(key, gen.Signer(), &types.LegacyTx{ + Nonce: gen.TxNonce(address), + To: &address, + Value: big.NewInt(1000), + Gas: params.TxGas, + GasPrice: new(big.Int).Add(gen.BaseFee(), common.Big1), + }) + gen.AddTx(tx) + + // Add withdrawals. + if i == 1 { + gen.AddWithdrawal(&types.Withdrawal{ + Validator: 42, + Address: common.Address{0xee}, + Amount: 1337, + }) + gen.AddWithdrawal(&types.Withdrawal{ + Validator: 13, + Address: common.Address{0xee}, + Amount: 1, + }) + } + if i == 3 { + gen.AddWithdrawal(&types.Withdrawal{ + Validator: 42, + Address: common.Address{0xee}, + Amount: 1337, + }) + gen.AddWithdrawal(&types.Withdrawal{ + Validator: 13, + Address: common.Address{0xee}, + Amount: 1, + }) + } + }) + + // Import the chain. This runs all block validation rules. + blockchain, err := NewBlockChain(db, gspec, engine, nil) + if err != nil { + fmt.Printf("err is %v\n", err) + } + defer blockchain.Stop() + + if i, err := blockchain.InsertChain(genchain); err != nil { + t.Fatalf("insert error (block %d): %v\n", genchain[i].NumberU64(), err) + } + + // enforce that withdrawal indexes are monotonically increasing from 0 + var ( + withdrawalIndex uint64 + ) + for i := range genchain { + blocknum := genchain[i].NumberU64() + block := blockchain.GetBlockByNumber(blocknum) + if block == nil { + t.Fatalf("block %d not found", blocknum) + } + + // Verify receipts. + genBlockReceipts := genreceipts[i] + for _, r := range genBlockReceipts { + if r.BlockNumber.Cmp(block.Number()) != 0 { + t.Errorf("receipt has wrong block number %d, want %d", r.BlockNumber, block.Number()) + } + if r.BlockHash != block.Hash() { + t.Errorf("receipt has wrong block hash %v, want %v", r.BlockHash, block.Hash()) + } + + // patch up empty logs list to make DeepEqual below work + if r.Logs == nil { + r.Logs = []*types.Log{} + } + } + blockchainReceipts := blockchain.GetReceiptsByHash(block.Hash()) + if !reflect.DeepEqual(genBlockReceipts, blockchainReceipts) { + t.Fatalf("receipts mismatch\ngenerated: %s\nblockchain: %s", spew.Sdump(genBlockReceipts), spew.Sdump(blockchainReceipts)) + } + + // Verify withdrawals. + if len(block.Withdrawals()) == 0 { + continue + } + for j := 0; j < len(block.Withdrawals()); j++ { + if block.Withdrawals()[j].Index != withdrawalIndex { + t.Fatalf("withdrawal index %d does not equal expected index %d", block.Withdrawals()[j].Index, withdrawalIndex) + } + withdrawalIndex += 1 + } + + // TODO: can we reinstate the following? + /* + // Verify parent beacon root. + want := common.Hash{byte(blocknum)} + if got := block.BeaconRoot(); *got != want { + t.Fatalf("block %d, wrong parent beacon root: got %s, want %s", i, got, want) + } + state, _ := blockchain.State() + idx := block.Time()%8191 + 8191 + got := state.GetState(params.BeaconRootsAddress, common.BigToHash(new(big.Int).SetUint64(idx))) + if got != want { + t.Fatalf("block %d, wrong parent beacon root in state: got %s, want %s", i, got, want) + } + */ + } +} + func ExampleGenerateChain() { var ( key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") diff --git a/core/genesis.go b/core/genesis.go index d77ea10d8c..550f2e43c7 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -67,13 +67,14 @@ 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 - SlotNumber *uint64 `json:"slotNumber"` // EIP-7843 + 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"` + SlotNumber *uint64 `json:"slotNumber"` // EIP-7843 } // copy copies the genesis. @@ -487,18 +488,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/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go index c35f56ee07..d52e1cdab3 100644 --- a/core/rawdb/accessors_chain_test.go +++ b/core/rawdb/accessors_chain_test.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/crypto/keccak" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" - "github.com/holiman/uint256" ) // Tests block header storage and retrieval operations. @@ -906,13 +905,17 @@ func makeTestBAL(t *testing.T) (rlp.RawValue, *bal.BlockAccessList) { t.Helper() cb := bal.NewConstructionBlockAccessList() - addr := common.HexToAddress("0x1111111111111111111111111111111111111111") - cb.AccountRead(addr) - cb.StorageRead(addr, common.BytesToHash([]byte{0x01})) - cb.StorageWrite(0, addr, common.BytesToHash([]byte{0x02}), common.BytesToHash([]byte{0xaa})) - cb.BalanceChange(0, addr, uint256.NewInt(100)) - cb.NonceChange(addr, 0, 1) + /* + TODO MariusVanDerWijden fix after rebase + addr := common.HexToAddress("0x1111111111111111111111111111111111111111") + + cb.AccountRead(addr) + cb.StorageRead(addr, common.BytesToHash([]byte{0x01})) + cb.StorageWrite(0, addr, common.BytesToHash([]byte{0x02}), common.BytesToHash([]byte{0xaa})) + cb.BalanceChange(0, addr, uint256.NewInt(100)) + cb.NonceChange(addr, 0, 1) + */ var buf bytes.Buffer if err := cb.EncodeRLP(&buf); err != nil { t.Fatalf("failed to encode BAL: %v", err) diff --git a/core/state/state_object.go b/core/state/state_object.go index 264dfd920d..33d7b60bee 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -23,6 +23,8 @@ import ( "slices" "time" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -53,6 +55,9 @@ type stateObject struct { origin *types.StateAccount // Account original data without any change applied, nil means it was not existent data types.StateAccount // Account data with all mutations applied in the scope of block + txPreBalance *uint256.Int // the account balance after the last call to finalise + txPreNonce uint64 // the account nonce after the last call to finalise + // Write caches. trie Trie // storage trie, which becomes non-nil on first access code []byte // contract bytecode, which gets set when code is loaded @@ -75,6 +80,9 @@ type stateObject struct { // Cache flags. dirtyCode bool // true if the code was updated + nonFinalizedCode bool // true if the code has been changed in the current transaction + txPrestateCode []byte // set to the value of the code at the beginning of the transaction if it changed in the current transaction + // Flag whether the account was marked as self-destructed. The self-destructed // account is still accessible in the scope of same transaction. selfDestructed bool @@ -107,6 +115,8 @@ func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *s dirtyStorage: make(Storage), pendingStorage: make(Storage), uncommittedStorage: make(Storage), + txPreNonce: acct.Nonce, + txPreBalance: acct.Balance.Clone(), } } @@ -248,20 +258,59 @@ func (s *stateObject) setState(key common.Hash, value common.Hash, origin common // finalise moves all dirty storage slots into the pending area to be hashed or // committed later. It is invoked at the end of every transaction. -func (s *stateObject) finalise() { +func (s *stateObject) finalise() (mutations *bal.AccountMutations) { + mutations = &bal.AccountMutations{} + if s.Balance().Cmp(s.txPreBalance) != 0 { + mutations.Balance = s.Balance() + } + if s.Nonce() != s.txPreNonce { + mutations.Nonce = new(uint64) + *mutations.Nonce = s.Nonce() + } + // include account code changes: created contracts and 7702 delegation authority code changes + if s.nonFinalizedCode { + if s.code == nil { + // code cleared (7702). code must be non-nil in the post to signal that it's part of the diff vs being unchanged. + mutations.Code = []byte{} + } else { + mutations.Code = s.code + } + } + + mutations.StorageWrites = make(map[common.Hash]common.Hash) + slotsToPrefetch := make([]common.Hash, 0, len(s.dirtyStorage)) for key, value := range s.dirtyStorage { if origin, exist := s.uncommittedStorage[key]; exist && origin == value { + // non-parallel-execution: // The slot is reverted to its original value, delete the entry // to avoid thrashing the data structures. + // + // parallel-exec-with-BAL: + // each statedb instance only executes a single transaction so the previous value + // of the slot won't be in uncommittedStorage + txPrestateVal := s.GetCommittedState(key) + if txPrestateVal != value { + mutations.StorageWrites[key] = value + } delete(s.uncommittedStorage, key) } else if exist { + // non-parallel-execution: // The slot is modified to another value and the slot has been - // tracked for commit, do nothing here. + // tracked for commit in uncommittedStorage. + // + // parallel-exec-with-BAL: + // each statedb instance only executes a single transaction so the previous value + // of the slot won't be in uncommittedStorage + mutations.StorageWrites[key] = value } else { // The slot is different from its original value and hasn't been // tracked for commit yet. - s.uncommittedStorage[key] = s.GetCommittedState(key) + // Whether executing parallel with BAL or not, the value of the slot before the execution + // of the current transaction is in originStorage + origin := s.GetCommittedState(key) + mutations.StorageWrites[key] = value + s.uncommittedStorage[key] = origin slotsToPrefetch = append(slotsToPrefetch, key) // Copy needed for closure } // Aggregate the dirty storage slots into the pending area. It might @@ -284,6 +333,18 @@ func (s *stateObject) finalise() { // of the newly-created object as it's no longer eligible for self-destruct // by EIP-6780. For non-newly-created objects, it's a no-op. s.newContract = false + + s.nonFinalizedCode = false + s.txPrestateCode = nil + + // TODO(jwasinger): I had a bug here where i would set both of these to the value of s.data.* and there were no amsterdam-release test failures. need to figure out why. + s.txPreBalance = s.Balance().Clone() + s.txPreNonce = s.Nonce() + + if mutations.Nonce == nil && mutations.Code == nil && mutations.Balance == nil && len(mutations.StorageWrites) == 0 { + return nil + } + return mutations } // updateTrie is responsible for persisting cached storage changes into the @@ -511,6 +572,7 @@ func (s *stateObject) deepCopy(db *StateDB) *stateObject { dirtyCode: s.dirtyCode, selfDestructed: s.selfDestructed, newContract: s.newContract, + txPreBalance: s.txPreBalance.Clone(), } switch s.trie.(type) { @@ -587,13 +649,26 @@ func (s *stateObject) SetCode(codeHash common.Hash, code []byte) (prev []byte) { prev = slices.Clone(s.code) s.db.journal.setCode(s.address, prev) s.setCode(codeHash, code) + return prev } func (s *stateObject) setCode(codeHash common.Hash, code []byte) { + prevCode := s.code + if s.txPrestateCode == nil { + if prevCode == nil { + prevCode = []byte{} + } + s.txPrestateCode = prevCode + } + if !bytes.Equal(code, s.txPrestateCode) { + s.dirtyCode = true + s.nonFinalizedCode = true + } else { + s.nonFinalizedCode = false + } s.code = code s.data.CodeHash = codeHash[:] - s.dirtyCode = true } func (s *stateObject) SetNonce(nonce uint64) { diff --git a/core/state/statedb.go b/core/state/statedb.go index 1858f4758d..d17f947a12 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -27,11 +27,12 @@ import ( "sync/atomic" "time" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -193,6 +194,7 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro journal: newJournal(), accessList: newAccessList(), transientStorage: newTransientStorage(), + stateReadList: bal.NewStateAccessList(), } if db.Type().Is(TypeUBT) { sdb.accessEvents = NewAccessEvents() @@ -200,6 +202,13 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro return sdb, nil } +// WithReader returns a copy of the StateDB instance with the specified reader. +func (s *StateDB) WithReader(reader Reader) *StateDB { + cpy := s.Copy() + cpy.reader = reader + return cpy +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. @@ -799,8 +808,9 @@ func (s *StateDB) LogsForBurnAccounts() []*types.Log { // Finalise finalises the state by removing the destructed objects and clears // the journal as well as the refunds. Finalise, however, will not push any updates // into the tries just yet. Only IntermediateRoot or Commit will do that. -func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { +func (s *StateDB) Finalise(deleteEmptyObjects bool) (accesses *bal.StateAccessList, mutations *bal.StateMutations) { addressesToPrefetch := make([]common.Address, 0, len(s.journal.dirties)) + mutations = bal.NewStateMutations() for addr := range s.journal.dirties { obj, exist := s.stateObjects[addr] if !exist { @@ -822,8 +832,18 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { if _, ok := s.stateObjectsDestruct[obj.address]; !ok { s.stateObjectsDestruct[obj.address] = obj } + // a pre-existing account can only be removed from the state under the following circumstance: + // it had a balance and was the target of a create2 which selfdestructed in the initcode + if !obj.txPreBalance.IsZero() { + mutations.Set(addr, &bal.AccountMutations{ + Balance: uint256.NewInt(0), + }) + } } else { - obj.finalise() + mut := obj.finalise() + if mut != nil { + mutations.Set(addr, mut) + } s.markUpdate(addr) } // At this point, also ship the address off to the precacher. The precacher @@ -838,8 +858,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { } // Invalidate journal because reverting across transactions is not allowed. s.clearJournalAndRefund() - - return s.stateReadList + return s.stateReadList, mutations } // IntermediateRoot computes the current root hash of the state trie. diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index c5faa7c98e..ae6d795cd3 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -21,11 +21,12 @@ import ( "math/big" "sort" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -234,7 +235,7 @@ func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log { return s.inner.LogsForBurnAccounts() } -func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { +func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) (*bal.StateAccessList, *bal.StateMutations) { if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil { // Short circuit if no relevant hooks are set. return s.inner.Finalise(deleteEmptyObjects) diff --git a/core/state_processor.go b/core/state_processor.go index 31d521311e..ace4106db5 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -21,6 +21,8 @@ import ( "fmt" "math/big" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc" @@ -63,13 +65,15 @@ func (p *StateProcessor) chainConfig() *params.ChainConfig { // transactions failed to execute due to insufficient gas it will return an error. func (p *StateProcessor) Process(ctx context.Context, block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) { var ( - config = p.chainConfig() - receipts = make(types.Receipts, 0, len(block.Transactions())) - header = block.Header() - blockHash = block.Hash() - blockNumber = block.Number() - allLogs []*types.Log - gp = NewGasPool(block.GasLimit()) + config = p.chainConfig() + receipts = make(types.Receipts, 0, len(block.Transactions())) + header = block.Header() + blockHash = block.Hash() + blockNumber = block.Number() + allLogs []*types.Log + gp = NewGasPool(block.GasLimit()) + computedAccessList = bal.NewConstructionBlockAccessList() + isAmsterdam = p.chainConfig().IsAmsterdam(block.Number(), block.Time()) ) var tracingStateDB = vm.StateDB(statedb) if hooks := cfg.Tracer; hooks != nil { @@ -90,10 +94,18 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated evm := vm.NewEVM(context, tracingStateDB, config, cfg) if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - ProcessBeaconBlockRoot(*beaconRoot, evm) + accesses, mutations := ProcessBeaconBlockRoot(*beaconRoot, evm) + if isAmsterdam { + computedAccessList.AddBlockInitMutations(mutations) + computedAccessList.AddAccesses(accesses) + } } if config.IsPrague(block.Number(), block.Time()) || config.IsUBT(block.Number(), block.Time()) { - ProcessParentBlockHash(block.ParentHash(), evm) + accesses, mutations := ProcessParentBlockHash(block.ParentHash(), evm) + if isAmsterdam { + computedAccessList.AddBlockInitMutations(mutations) + computedAccessList.AddAccesses(accesses) + } } // Iterate over and process the individual transactions @@ -108,60 +120,81 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated telemetry.Int64Attribute("tx.index", int64(i)), ) - receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) + accesses, mutations, receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) if err != nil { spanEnd(&err) return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) + + if isAmsterdam { + computedAccessList.AddTransactionMutations(mutations, i) + computedAccessList.AddAccesses(accesses) + } spanEnd(nil) } - requests, err := postExecution(ctx, config, block, allLogs, evm) + postMut := bal.NewStateMutations() + postReads, postExecMut, requests, err := postExecution(ctx, config, block, allLogs, evm) if err != nil { return nil, err } + postMut.Merge(postExecMut) + // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) - p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) + eip4895WithdrawalReads, eip4985WithdrawalMuts := p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) + postMut.Merge(eip4985WithdrawalMuts) + if isAmsterdam { + computedAccessList.AddBlockFinalizeMutations(postMut) + computedAccessList.AddAccesses(postReads) + computedAccessList.AddAccesses(eip4895WithdrawalReads) + } return &ProcessResult{ - Receipts: receipts, - Requests: requests, - Logs: allLogs, - GasUsed: gp.Used(), + Receipts: receipts, + Requests: requests, + Logs: allLogs, + GasUsed: gp.Used(), + AccessList: computedAccessList, }, nil } // postExecution processes the post-execution system calls if Prague is enabled. -func postExecution(ctx context.Context, config *params.ChainConfig, block *types.Block, allLogs []*types.Log, evm *vm.EVM) (requests [][]byte, err error) { +func postExecution(ctx context.Context, config *params.ChainConfig, block *types.Block, allLogs []*types.Log, evm *vm.EVM) (accesses *bal.StateAccessList, mutations *bal.StateMutations, requests [][]byte, err error) { _, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution") defer spanEnd(&err) + mutations = bal.NewStateMutations() + accesses = bal.NewStateAccessList() // Read requests if Prague is enabled. if config.IsPrague(block.Number(), block.Time()) { requests = [][]byte{} // EIP-6110 if err := ParseDepositLogs(&requests, allLogs, config); err != nil { - return requests, fmt.Errorf("failed to parse deposit logs: %w", err) + return nil, nil, requests, fmt.Errorf("failed to parse deposit logs: %w", err) } // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm); err != nil { - return requests, fmt.Errorf("failed to process withdrawal queue: %w", err) + if accesses, mutations, err = ProcessWithdrawalQueue(&requests, evm); err != nil { + return nil, nil, requests, fmt.Errorf("failed to process withdrawal queue: %w", err) } // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm); err != nil { - return requests, fmt.Errorf("failed to process consolidation queue: %w", err) + consolidationAccesses, consolidationMutations, err := ProcessConsolidationQueue(&requests, evm) + if err != nil { + return nil, nil, requests, fmt.Errorf("failed to process consolidation queue: %w", err) } + + mutations.Merge(consolidationMutations) + accesses.Merge(consolidationAccesses) } - return requests, nil + return accesses, mutations, requests, nil } // ApplyTransactionWithEVM attempts to apply a transaction to the given state database // and uses the input parameters for its environment similar to ApplyTransaction. However, // this method takes an already created EVM instance as input. -func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, err error) { +func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (accesses *bal.StateAccessList, mutations *bal.StateMutations, receipt *types.Receipt, err error) { if hooks := evm.Config.Tracer; hooks != nil { if hooks.OnTxStart != nil { hooks.OnTxStart(evm.GetVMContext(), tx, msg.From) @@ -173,12 +206,12 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, // Apply the transaction to the current state (included in the env). result, err := ApplyMessage(evm, msg, gp) if err != nil { - return nil, err + return nil, nil, nil, err } // Update the state with pending changes. var root []byte if evm.ChainConfig().IsByzantium(blockNumber) { - evm.StateDB.Finalise(true) + accesses, mutations = evm.StateDB.Finalise(true) } else { root = statedb.IntermediateRoot(evm.ChainConfig().IsEIP158(blockNumber)).Bytes() } @@ -187,7 +220,7 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, if statedb.Database().Type().Is(state.TypeUBT) { statedb.AccessEvents().Merge(evm.AccessEvents) } - return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil + return accesses, mutations, MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil } // MakeReceipt generates the receipt object for a transaction given its execution result. @@ -231,10 +264,10 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b // and uses the input parameters for its environment. It returns the receipt // for the transaction and an error if the transaction failed, // indicating the block was invalid. -func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, error) { +func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*bal.StateAccessList, *bal.StateMutations, *types.Receipt, error) { msg, err := TransactionToMessage(tx, types.MakeSigner(evm.ChainConfig(), header.Number, header.Time), header.BaseFee) if err != nil { - return nil, err + return nil, nil, nil, err } // Create a new context to be used in the EVM environment return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm) @@ -242,7 +275,7 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header * // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root // contract. This method is exported to be used in tests. -func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { +func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) (*bal.StateAccessList, *bal.StateMutations) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -264,12 +297,12 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + return evm.StateDB.Finalise(true) } // ProcessParentBlockHash stores the parent block hash in the history storage contract // as per EIP-2935/7709. -func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { +func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) (*bal.StateAccessList, *bal.StateMutations) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -294,22 +327,22 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + return evm.StateDB.Finalise(true) } // ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract. // It returns the opaque request data returned by the contract. -func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM) error { +func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM) (*bal.StateAccessList, *bal.StateMutations, error) { return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress) } // ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract. // It returns the opaque request data returned by the contract. -func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM) error { +func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM) (*bal.StateAccessList, *bal.StateMutations, error) { return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress) } -func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address) error { +func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address) (*bal.StateAccessList, *bal.StateMutations, error) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -330,19 +363,19 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + accesses, mutations := evm.StateDB.Finalise(true) if err != nil { - return fmt.Errorf("system call failed to execute: %v", err) + return nil, nil, fmt.Errorf("system call failed to execute: %v", err) } if len(ret) == 0 { - return nil // skip empty output + return accesses, mutations, nil // skip empty output } // Append prefixed requestsData to the requests list. requestsData := make([]byte, len(ret)+1) requestsData[0] = requestType copy(requestsData[1:], ret) *requests = append(*requests, requestsData) - return nil + return accesses, mutations, nil } var depositTopic = common.HexToHash("0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5") diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 9f85510269..7b3877b96a 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -469,6 +469,7 @@ func TestEIP8037MaxRegularGasValidation(t *testing.T) { Amsterdam: params.DefaultOsakaBlobConfig, }, } + rules = config.Rules(common.Big0, true, 0) signer = types.LatestSigner(config) key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") ) @@ -486,7 +487,7 @@ func TestEIP8037MaxRegularGasValidation(t *testing.T) { } // Verify that floor data gas exceeds MaxTxGas - floorGas, err := FloorDataGas(largeData) + floorGas, err := FloorDataGas(rules, largeData) if err != nil { t.Fatalf("Failed to calculate floor data gas: %v", err) } diff --git a/core/types.go b/core/types.go index 87bbfcff58..52161df523 100644 --- a/core/types.go +++ b/core/types.go @@ -20,6 +20,8 @@ import ( "context" "sync/atomic" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -54,8 +56,9 @@ type Processor interface { // ProcessResult contains the values computed by Process. type ProcessResult struct { - Receipts types.Receipts - Requests [][]byte - Logs []*types.Log - GasUsed uint64 + Receipts types.Receipts + Requests [][]byte + Logs []*types.Log + GasUsed uint64 + AccessList *bal.ConstructionBlockAccessList } diff --git a/core/types/bal/access_list.go b/core/types/bal/access_list.go index e563fa22e2..3fcdb646f2 100644 --- a/core/types/bal/access_list.go +++ b/core/types/bal/access_list.go @@ -92,3 +92,18 @@ func (s *StateAccessList) Copy() *StateAccessList { } return cpy } + +func (s *StateAccessList) Eq(other StateAccessList) bool { + if len(s.list) != len(other.list) { + return false + } + for addr, accesses := range s.list { + if _, ok := other.list[addr]; !ok { + return false + } + if !maps.Equal(accesses, other.list[addr]) { + return false + } + } + return true +} diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index 86dc8e5426..506b6b514b 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -18,156 +18,331 @@ package bal import ( "bytes" + "encoding/json" "maps" "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" ) -// ConstructionAccountAccess contains post-block account state for mutations as well as +// ConstructionAccountAccesses 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 { +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"` + 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 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"` + NonceChanges map[uint16]uint64 - // CodeChange contains the post-state contract code of an account keyed - // by tx index. - CodeChange map[uint16][]byte `json:"codeChange,omitempty"` + CodeChanges map[uint16][]byte } -// NewConstructionAccountAccess initializes the account access object. -func NewConstructionAccountAccess() *ConstructionAccountAccess { - return &ConstructionAccountAccess{ +func (c *ConstructionAccountAccesses) Copy() (res ConstructionAccountAccesses) { + if c.StorageWrites != nil { + res.StorageWrites = make(map[common.Hash]map[uint16]common.Hash) + for slot, writes := range c.StorageWrites { + res.StorageWrites[slot] = maps.Clone(writes) + } + } + if c.StorageReads != nil { + res.StorageReads = maps.Clone(c.StorageReads) + } + if c.BalanceChanges != nil { + res.BalanceChanges = maps.Clone(c.BalanceChanges) + } + if c.NonceChanges != nil { + res.NonceChanges = maps.Clone(c.NonceChanges) + } + if c.CodeChanges != nil { + res.CodeChanges = maps.Clone(c.CodeChanges) + } + return res +} + +type StateMutations struct { + list map[common.Address]AccountMutations +} + +func NewStateMutations() *StateMutations { + return &StateMutations{make(map[common.Address]AccountMutations)} +} + +func (s StateMutations) String() string { + b, _ := json.MarshalIndent(s, "", " ") + return string(b) +} + +// 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 *StateMutations) Merge(next *StateMutations) { + if next == nil { + return + } + for account, diff := range next.list { + if mut, ok := s.list[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 + } + } + } + s.list[account] = mut + } else { + s.list[account] = *diff.Copy() + } + } +} + +func (s *StateMutations) Eq(other *StateMutations) bool { + if s == nil && other == nil { + return true + } else if s == nil && other != nil { + return false + } else if s != nil && other == nil { + return false + } else if len(s.list) != len(other.list) { + return false + } + + for addr, mut := range s.list { + otherMut, ok := other.list[addr] + if !ok { + return false + } + + if !mut.Eq(&otherMut) { + return false + } + } + + return true +} + +func (s *StateMutations) Set(addr common.Address, mut *AccountMutations) { + s.list[addr] = *mut +} + +type ConstructionBlockAccessList struct { + list map[common.Address]*ConstructionAccountAccesses + transactionCount int +} + +func NewConstructionBlockAccessList() *ConstructionBlockAccessList { + return &ConstructionBlockAccessList{ + make(map[common.Address]*ConstructionAccountAccesses), + 0} +} + +func (c *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { + if c == nil { + return nil + } + res := NewConstructionBlockAccessList() + for addr, accountAccess := range c.list { + aaCopy := accountAccess.Copy() + res.list[addr] = &aaCopy + } + return res +} + +// must be called after all txs are added +func (c *ConstructionBlockAccessList) AddBlockFinalizeMutations(muts *StateMutations) { + c.addMutations(muts, c.transactionCount+1) +} + +func (c *ConstructionBlockAccessList) AddBlockInitMutations(muts *StateMutations) { + c.addMutations(muts, 0) +} + +func (c *ConstructionBlockAccessList) AddTransactionMutations(muts *StateMutations, txIdx int) { + c.transactionCount = max(c.transactionCount, txIdx+1) + c.addMutations(muts, c.transactionCount) +} + +func (c *ConstructionBlockAccessList) addMutations(muts *StateMutations, index int) { + if muts == nil { + return + } + // TO + idx := uint16(index) + for addr, mut := range muts.list { + if _, exist := c.list[addr]; !exist { + c.list[addr] = newConstructionAccountAccesses() + } + if mut.Nonce != nil { + if c.list[addr].NonceChanges == nil { + c.list[addr].NonceChanges = make(map[uint16]uint64) + } + c.list[addr].NonceChanges[idx] = *mut.Nonce + } + if mut.Balance != nil { + if c.list[addr].BalanceChanges == nil { + c.list[addr].BalanceChanges = make(map[uint16]*uint256.Int) + } + c.list[addr].BalanceChanges[idx] = mut.Balance.Clone() + } + if mut.Code != nil { + if c.list[addr].CodeChanges == nil { + c.list[addr].CodeChanges = make(map[uint16][]byte) + } + c.list[addr].CodeChanges[idx] = bytes.Clone(mut.Code) + } + if len(mut.StorageWrites) > 0 { + for key, val := range mut.StorageWrites { + if c.list[addr].StorageWrites[key] == nil { + c.list[addr].StorageWrites[key] = make(map[uint16]common.Hash) + } + c.list[addr].StorageWrites[key][idx] = val + + // delete the key from the tracked reads if it was previously read. + delete(c.list[addr].StorageReads, key) + } + } + } +} + +func (c *ConstructionBlockAccessList) AddAccesses(reads *StateAccessList) { + if reads == nil { + return + } + for addr, addrReads := range reads.list { + if _, ok := c.list[addr]; !ok { + c.list[addr] = newConstructionAccountAccesses() + } + for storageKey, _ := range addrReads { + if c.list[addr].StorageWrites != nil { + if _, ok := c.list[addr].StorageWrites[storageKey]; ok { + continue + } + } + if c.list[addr].StorageReads == nil { + c.list[addr].StorageReads = make(map[common.Hash]struct{}) + } + c.list[addr].StorageReads[storageKey] = struct{}{} + } + } +} + +func newConstructionAccountAccesses() *ConstructionAccountAccesses { + return &ConstructionAccountAccesses{ 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), - CodeChange: make(map[uint16][]byte), + CodeChanges: make(map[uint16][]byte), } } -// 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 +type StorageMutations 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 StorageMutations `json:"StorageWrites,omitempty"` } -// NewConstructionBlockAccessList instantiates an empty access list. -func NewConstructionBlockAccessList() ConstructionBlockAccessList { - return ConstructionBlockAccessList{ - Accounts: make(map[common.Address]*ConstructionAccountAccess), - } +// 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() } -// 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() +// 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 } -// 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 _, 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() - } - // TODO(rjl493456442) is it essential to deep-copy the code? - b.Accounts[address].CodeChange[txIndex] = 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) - } - 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() - } - aaCopy.BalanceChanges = balances - aaCopy.NonceChanges = maps.Clone(aa.NonceChanges) - - codes := make(map[uint16][]byte, len(aa.CodeChange)) - for index, code := range aa.CodeChange { - codes[index] = bytes.Clone(code) - } - aaCopy.CodeChange = codes - res.Accounts[addr] = &aaCopy +// Copy returns a deep copy of the access list +func (e BlockAccessList) Copy() *BlockAccessList { + var res BlockAccessList + for _, accountAccess := range e { + res = append(res, accountAccess.Copy()) } return &res } + +// Eq returns whether the calling instance is equal to the provided one. +func (a *AccountMutations) Eq(other *AccountMutations) bool { + if a.Balance != nil || other.Balance != nil { + if a.Balance == nil || other.Balance == nil { + return false + } + + if !a.Balance.Eq(other.Balance) { + return false + } + } + + if (len(a.Code) != 0 || len(other.Code) != 0) && !bytes.Equal(a.Code, other.Code) { + return false + } + + if a.Nonce != nil || other.Nonce != nil { + if a.Nonce == nil || other.Nonce == nil { + return false + } + + if *a.Nonce != *other.Nonce { + return false + } + } + + if a.StorageWrites != nil || other.StorageWrites != nil { + if !maps.Equal(a.StorageWrites, other.StorageWrites) { + return false + } + } + return true +} diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 6d52c17c83..6d248af39c 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" @@ -28,35 +30,132 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "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() } -// Validate returns an error if the contents of the access list are not ordered +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) EncodedSize() int { + b, err := rlp.EncodeToBytes(e) + if err != nil { + // TODO: proper to crit here? + log.Crit("failed to rlp encode access list", "err", err) + } + return len(b) +} + +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 { +// * the total accounts and storage slots in the access list exceed the protocol max +func (e BlockAccessList) Validate(blockTxCount int, blockGasLimit uint64) error { + if !slices.IsSortedFunc(e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") } - for _, entry := range e.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{}{} + } + // validate individual entries + for _, entry := range e { + if err := entry.validate(blockTxCount); err != nil { return err } } + // check that the total number of items doesn't exceed max + return e.ValidateGasLimit(blockGasLimit) +} + +// ValidateGasLimit checks that the number of BAL items does not exceed the +// block gas limit divided by the per-item cost (EIP-7928). +func (e BlockAccessList) ValidateGasLimit(blockGasLimit uint64) error { + var balItems uint64 + for _, account := range e { + // Count each address as one item + balItems++ + // Count unique storage keys across both reads and writes + uniqueSlots := make(map[common.Hash]struct{}) + for _, sc := range account.StorageChanges { + uniqueSlots[sc.Slot.ToHash()] = struct{}{} + } + for _, sr := range account.StorageReads { + uniqueSlots[sr.ToHash()] = struct{}{} + } + balItems += uint64(len(uniqueSlots)) + } + limit := blockGasLimit / params.GasBlockAccessListItem + if balItems > limit { + return fmt.Errorf("block access list exceeds gas limit: %d items exceeds limit of %d", balItems, limit) + } return nil } @@ -70,53 +169,135 @@ 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") - } - var enc [16]byte - copy(enc[16-len(valBytes):], valBytes[:]) - return enc -} - // 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 (s *EncodedStorage) ToHash() common.Hash { + if s == nil { + return common.Hash{} + } + return s.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 e.Slot == nil { + return errors.New("nil slot key") + } + 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") + for i, access := range e.Accesses { + if access.ValueAfter == nil { + return errors.New("nil storage write post") + } + if i > 0 && e.Accesses[i-1].TxIdx == access.TxIdx { + return errors.New("duplicate storage write index") + } + } + // 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 } // encodingCodeChange contains the runtime bytecode deployed at an address @@ -126,64 +307,120 @@ type encodingCodeChange struct { Code []byte `ssz-max:"300000"` // TODO(rjl493456442) shall we put the limit here? The limit will be increased gradually } -// 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]) - CodeChanges []encodingCodeChange `ssz-max:"300000"` // 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 []encodingCodeChange `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+1 { + return errors.New("highest balance change index beyond what is allowed") + } + // check that the balance values are set and there are no duplicate index entries + for i, balanceChange := range e.BalanceChanges { + if balanceChange.Balance == nil { + return errors.New("nil balance change value") + } + if i > 0 && e.BalanceChanges[i-1].TxIdx == balanceChange.TxIdx { + return errors.New("duplicate index for balance change") + } + } + // 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.NonceChanges) > 0 && int(e.NonceChanges[len(e.NonceChanges)-1].TxIdx) >= blockTxCount+2 { + return errors.New("highest nonce change index beyond what is allowed") + } + for i, nonceChange := range e.NonceChanges { + if i > 0 && nonceChange.TxIdx == e.NonceChanges[i-1].TxIdx { + return errors.New("duplicate index reported in nonce changes") + } + } - // Check the code changes are sorted in order + // 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 encodingCodeChange) int { return cmp.Compare[uint16](a.TxIndex, b.TxIndex) }) { - return errors.New("code changes not in ascending order by tx index") + return errors.New("code changes not in ascending order") } - for _, change := range e.CodeChanges { - // TODO(rjl493456442): This check should be fork-aware, since the limit may - // differ across forks. - if len(change.Code) > params.MaxCodeSize { - return errors.New("code change contained oversized code") + if len(e.CodeChanges) > 0 && int(e.CodeChanges[len(e.CodeChanges)-1].TxIndex) >= blockTxCount+2 { + return errors.New("highest code change index beyond what is allowed") + } + for i, codeChange := range e.CodeChanges { + if i > 0 && codeChange.TxIndex == e.CodeChanges[i-1].TxIndex { + return errors.New("duplicate index reported in code changes") + } + } + + // 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.MaxCodeSizeAmsterdam { + return fmt.Errorf("code change contained oversized code") } } return nil @@ -196,41 +433,35 @@ func (e *AccountAccess) Copy() AccountAccess { StorageReads: slices.Clone(e.StorageReads), BalanceChanges: slices.Clone(e.BalanceChanges), NonceChanges: slices.Clone(e.NonceChanges), - StorageWrites: make([]encodingSlotWrites, 0, len(e.StorageWrites)), - CodeChanges: make([]encodingCodeChange, 0, len(e.CodeChanges)), } - 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), }) } for _, codeChange := range e.CodeChanges { - res.CodeChanges = append(res.CodeChanges, encodingCodeChange{ - TxIndex: codeChange.TxIndex, - Code: bytes.Clone(codeChange.Code), - }) + res.CodeChanges = append(res.CodeChanges, + encodingCodeChange{ + codeChange.TxIndex, + 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, len(a.StorageWrites)), - StorageReads: make([][32]byte, 0, len(a.StorageReads)), - BalanceChanges: make([]encodingBalanceChange, 0, len(a.BalanceChanges)), - NonceChanges: make([]encodingAccountNonce, 0, len(a.NonceChanges)), - CodeChanges: make([]encodingCodeChange, 0, len(a.CodeChange)), + Address: addr, } // Convert write slots @@ -238,7 +469,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)) @@ -248,17 +479,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 @@ -267,7 +498,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]), }) } @@ -282,80 +513,34 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc } // Convert code change - codeIndices := slices.Collect(maps.Keys(a.CodeChange)) - slices.SortFunc(codeIndices, cmp.Compare[uint16]) - for _, idx := range codeIndices { + codeChangeIdxs := slices.Collect(maps.Keys(a.CodeChanges)) + slices.SortFunc(codeChangeIdxs, cmp.Compare[uint16]) + for _, idx := range codeChangeIdxs { res.CodeChanges = append(res.CodeChanges, encodingCodeChange{ - TxIndex: idx, - Code: a.CodeChange[idx], + idx, + bytes.Clone(a.CodeChanges[idx]), }) } 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 { + if c == nil { + return nil + } var addresses []common.Address - for addr := range b.Accounts { + for addr := range c.list { 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.list[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)) - } - - printWithIndent(1, "code changes:") - for _, change := range accountDiff.CodeChanges { - printWithIndent(2, fmt.Sprintf("%d: %x", change.TxIndex, change.Code)) - } - } - return res.String() -} - -// Copy returns a deep copy of the access list -func (e *BlockAccessList) Copy() *BlockAccessList { - cpy := &BlockAccessList{ - Accesses: make([]AccountAccess, 0, len(e.Accesses)), - } - for _, accountAccess := range e.Accesses { - cpy.Accesses = append(cpy.Accesses, accountAccess.Copy()) - } - return cpy -} +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..8d495848c6 --- /dev/null +++ b/core/types/bal/bal_encoding_json.go @@ -0,0 +1,108 @@ +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 640035e30e..f61ebe2675 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.CodeChanges { - _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.TxIndex)) + 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 - // CodeChanges: - var _tmp21 []encodingCodeChange - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp22 encodingCodeChange - { - 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.CodeChanges = _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 []encodingCodeChange + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp20 encodingCodeChange + { + if _, err := dec.List(); err != nil { + return err + } + // TxIndex: + _tmp21, err := dec.Uint16() + if err != nil { + return err + } + _tmp20.TxIndex = _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 58ba639ff0..5d42f3d458 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/internal/testrand" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/uint256" ) @@ -36,9 +37,9 @@ func equalBALs(a *BlockAccessList, b *BlockAccessList) bool { return true } -func makeTestConstructionBAL() *ConstructionBlockAccessList { - return &ConstructionBlockAccessList{ - map[common.Address]*ConstructionAccountAccess{ +func makeTestConstructionBAL() ConstructionBlockAccessList { + return ConstructionBlockAccessList{ + list: map[common.Address]*ConstructionAccountAccesses{ common.BytesToAddress([]byte{0xff, 0xff}): { StorageWrites: map[common.Hash]map[uint16]common.Hash{ common.BytesToHash([]byte{0x01}): { @@ -60,9 +61,7 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { 1: 2, 2: 6, }, - CodeChange: map[uint16][]byte{ - 0: common.Hex2Bytes("deadbeef"), - }, + CodeChanges: map[uint16][]byte{0: common.Hex2Bytes("deadbeef")}, }, common.BytesToAddress([]byte{0xff, 0xff, 0xff}): { StorageWrites: map[common.Hash]map[uint16]common.Hash{ @@ -70,9 +69,6 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { 2: common.BytesToHash([]byte{1, 2, 3, 4, 5, 6}), 3: common.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7, 8}), }, - common.BytesToHash([]byte{0x10}): { - 21: common.BytesToHash([]byte{1, 2, 3, 4, 5}), - }, }, StorageReads: map[common.Hash]struct{}{ common.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7, 8}): {}, @@ -84,11 +80,9 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { NonceChanges: map[uint16]uint64{ 1: 2, }, - CodeChange: map[uint16][]byte{ - 0: common.Hex2Bytes("deadbeef"), - }, }, }, + transactionCount: 1, } } @@ -104,10 +98,12 @@ 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) { + + // TODO (jwasinger): we should have a fuzzer to expand on what this test case does. + if !equalBALs(bal.ToEncodingObj(), &dec) { t.Fatal("decoded BAL doesn't match") } } @@ -115,18 +111,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(), + TxIdx: uint16(i*3 + j), + ValueAfter: NewEncodedStorageFromHash(testrand.Hash()), }) } if sort { @@ -138,7 +134,7 @@ func makeTestAccountAccess(sort bool) AccountAccess { } if sort { slices.SortFunc(storageWrites, func(a, b encodingSlotWrites) int { - return bytes.Compare(a.Slot[:], b.Slot[:]) + return bytes.Compare(a.Slot.inner.Bytes(), b.Slot.inner.Bytes()) }) } @@ -146,15 +142,15 @@ 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[:]) }) } for i := 0; i < 5; i++ { balances = append(balances, encodingBalanceChange{ - TxIdx: uint16(2 * i), - Balance: [16]byte(testrand.Bytes(16)), + TxIdx: uint16(i), + Balance: new(uint256.Int).SetBytes(testrand.Bytes(32)), }) } if sort { @@ -165,7 +161,7 @@ func makeTestAccountAccess(sort bool) AccountAccess { for i := 0; i < 5; i++ { nonces = append(nonces, encodingAccountNonce{ - TxIdx: uint16(2 * i), + TxIdx: uint16(i), Nonce: uint64(i + 100), }) } @@ -175,28 +171,32 @@ 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, CodeChanges: []encodingCodeChange{ { - TxIndex: 100, + TxIndex: 3, Code: testrand.Bytes(256), }, }, } } -func makeTestBAL(sort bool) *BlockAccessList { - list := &BlockAccessList{} +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[:]) }) } @@ -208,28 +208,29 @@ func TestBlockAccessListCopy(t *testing.T) { cpy := list.Copy() cpyCpy := cpy.Copy() - if !reflect.DeepEqual(list, cpy) { + if !reflect.DeepEqual(list, *cpy) { t.Fatal("block access mismatch") } - if !reflect.DeepEqual(cpy, cpyCpy) { + if !reflect.DeepEqual(*cpy, *cpyCpy) { t.Fatal("block access mismatch") } // Make sure the mutations on copy won't affect the origin - for _, aa := range cpyCpy.Accesses { - for i := 0; i < len(aa.StorageReads); i++ { - aa.StorageReads[i] = [32]byte(testrand.Bytes(32)) + for i := range *cpyCpy { + for j := 0; j < len((*cpyCpy)[i].StorageReads); j++ { + (*cpyCpy)[i].StorageReads[j] = NewEncodedStorageFromHash(testrand.Hash()) } } - if !reflect.DeepEqual(list, cpy) { + if !reflect.DeepEqual(list, *cpy) { t.Fatal("block access mismatch") } } func TestBlockAccessListValidation(t *testing.T) { // Validate the block access list after RLP decoding + testBALMaxIndex := 20 enc := makeTestBAL(true) - if err := enc.Validate(); err != nil { + if err := enc.Validate(testBALMaxIndex, params.MaxGasLimit); err != nil { t.Fatalf("Unexpected validation error: %v", err) } var buf bytes.Buffer @@ -241,14 +242,17 @@ func TestBlockAccessListValidation(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("Unexpected RLP-decode error: %v", err) } - if err := dec.Validate(); err != nil { + if err := dec.Validate(testBALMaxIndex, params.MaxGasLimit); err != nil { t.Fatalf("Unexpected validation error: %v", err) } // Validate the derived block access list cBAL := makeTestConstructionBAL() - listB := cBAL.toEncodingObj() - if err := listB.Validate(); err != nil { + listB := cBAL.ToEncodingObj() + if err := listB.Validate(testBALMaxIndex, params.MaxGasLimit); 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/vm/interface.go b/core/vm/interface.go index 487d8002f9..4d75dd46d0 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -98,5 +98,5 @@ type StateDB interface { AccessEvents() *state.AccessEvents // Finalise must be invoked at the end of a transaction - Finalise(bool) *bal.StateAccessList + Finalise(bool) (*bal.StateAccessList, *bal.StateMutations) } diff --git a/eth/protocols/snap/handler_test.go b/eth/protocols/snap/handler_test.go index 3f6a43a059..61b5e6abde 100644 --- a/eth/protocols/snap/handler_test.go +++ b/eth/protocols/snap/handler_test.go @@ -37,12 +37,14 @@ func makeTestBAL(minSize int) *bal.BlockAccessList { n := minSize/33 + 1 // 33 bytes per storage read slot in RLP access := bal.AccountAccess{ Address: common.HexToAddress("0x01"), - StorageReads: make([][32]byte, n), + StorageReads: make([]*bal.EncodedStorage, n), } for i := range access.StorageReads { - binary.BigEndian.PutUint64(access.StorageReads[i][24:], uint64(i)) + var slot common.Hash + binary.BigEndian.PutUint64(slot[24:], uint64(i)) + access.StorageReads[i] = bal.NewEncodedStorageFromHash(slot) } - return &bal.BlockAccessList{Accesses: []bal.AccountAccess{access}} + return &bal.BlockAccessList{access} } // getChainWithBALs creates a minimal test chain with BALs stored for each block. diff --git a/eth/tracers/api.go b/eth/tracers/api.go index b5ddc78a10..1b30472d89 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -1023,7 +1023,7 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor // Call Prepare to clear out the statedb access list statedb.SetTxContext(txctx.TxHash, txctx.TxIndex) - _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) + _, _, _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) if err != nil { return nil, fmt.Errorf("tracing failed: %w", err) } diff --git a/eth/tracers/internal/tracetest/selfdestruct_state_test.go b/eth/tracers/internal/tracetest/selfdestruct_state_test.go index 692c5eb775..20641201c3 100644 --- a/eth/tracers/internal/tracetest/selfdestruct_state_test.go +++ b/eth/tracers/internal/tracetest/selfdestruct_state_test.go @@ -620,7 +620,7 @@ func TestSelfdestructStateTracer(t *testing.T) { } context := core.NewEVMBlockContext(block.Header(), blockchain, nil) evm := vm.NewEVM(context, hookedState, tt.genesis.Config, vm.Config{Tracer: tracer.Hooks()}) - _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) + _, _, _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index d18561760e..1fd41c9426 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -256,6 +256,9 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]*simBlo } func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, parent *types.Header, headers []*types.Header, timeout time.Duration) (*types.Block, []simCallResult, map[common.Hash]common.Address, error) { + if sim.chainConfig.IsAmsterdam(header.Number, header.Time) { + return nil, nil, nil, fmt.Errorf("eth simulate does not yet support Amsterdam") + } // Set header fields that depend only on parent block. // Parent hash is needed for evm.GetHashFn to work. header.ParentHash = parent.Hash() @@ -401,11 +404,11 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, return nil, nil, nil, err } // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil { + if _, _, err := core.ProcessWithdrawalQueue(&requests, evm); err != nil { return nil, nil, nil, err } // EIP-7251 - if err := core.ProcessConsolidationQueue(&requests, evm); err != nil { + if _, _, err := core.ProcessConsolidationQueue(&requests, evm); err != nil { return nil, nil, nil, err } } diff --git a/miner/worker.go b/miner/worker.go index ae5d6c306f..5e97ae3113 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -213,11 +213,11 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, return &newPayloadResult{err: err} } // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, work.evm); err != nil { + if _, _, err := core.ProcessWithdrawalQueue(&requests, work.evm); err != nil { return &newPayloadResult{err: err} } // EIP-7251 consolidations - if err := core.ProcessConsolidationQueue(&requests, work.evm); err != nil { + if _, _, err := core.ProcessConsolidationQueue(&requests, work.evm); err != nil { return &newPayloadResult{err: err} } } @@ -414,7 +414,7 @@ func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (* snap = env.state.Snapshot() gp = env.gasPool.Snapshot() ) - receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) + _, _, receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) if err != nil { env.state.RevertToSnapshot(snap) env.gasPool.Set(gp) diff --git a/params/config.go b/params/config.go index 17508cbf27..05b3ad222a 100644 --- a/params/config.go +++ b/params/config.go @@ -323,14 +323,16 @@ var ( CancunTime: newUint64(0), PragueTime: newUint64(0), OsakaTime: newUint64(0), + AmsterdamTime: newUint64(0), UBTTime: nil, TerminalTotalDifficulty: big.NewInt(0), Ethash: new(EthashConfig), Clique: nil, BlobScheduleConfig: &BlobScheduleConfig{ - Cancun: DefaultCancunBlobConfig, - Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, + Cancun: DefaultCancunBlobConfig, + Prague: DefaultPragueBlobConfig, + Osaka: DefaultOsakaBlobConfig, + Amsterdam: DefaultOsakaBlobConfig, }, } diff --git a/params/protocol_params.go b/params/protocol_params.go index 1203be3b02..e631776a51 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -194,8 +194,7 @@ const ( StorageCreationSize = 32 AuthorizationCreationSize = 23 - // TODO: Add when EIP-7928 is implemented - // GasBlockAccessListItem = 2000 // EIP-7928: gas cost per BAL item for gas limit check + GasBlockAccessListItem = 2000 // EIP-7928: gas cost per BAL item for gas limit check ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation diff --git a/tests/block_test_util.go b/tests/block_test_util.go index bece8ae610..f0b64266a5 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -98,6 +98,7 @@ type btHeader struct { ExcessBlobGas *uint64 ParentBeaconBlockRoot *common.Hash SlotNumber *uint64 + BlockAccessListHash *common.Hash } type btHeaderMarshaling struct { @@ -225,8 +226,10 @@ func (t *BlockTest) genesis(config *params.ChainConfig) *core.Genesis { Coinbase: t.json.Genesis.Coinbase, Alloc: t.json.Pre, BaseFee: t.json.Genesis.BaseFeePerGas, - BlobGasUsed: t.json.Genesis.BlobGasUsed, - ExcessBlobGas: t.json.Genesis.ExcessBlobGas, + BlobGasUsed: t.json.Genesis.BlobGasUsed, + ExcessBlobGas: t.json.Genesis.ExcessBlobGas, + SlotNumber: t.json.Genesis.SlotNumber, + BlockAccessListHash: t.json.Genesis.BlockAccessListHash, } } diff --git a/tests/gen_btheader.go b/tests/gen_btheader.go index eb6d9a8271..b5fd9eec0c 100644 --- a/tests/gen_btheader.go +++ b/tests/gen_btheader.go @@ -39,6 +39,7 @@ func (b btHeader) MarshalJSON() ([]byte, error) { ExcessBlobGas *math.HexOrDecimal64 ParentBeaconBlockRoot *common.Hash SlotNumber *math.HexOrDecimal64 + BlockAccessListHash *common.Hash } var enc btHeader enc.Bloom = b.Bloom @@ -63,6 +64,7 @@ func (b btHeader) MarshalJSON() ([]byte, error) { enc.ExcessBlobGas = (*math.HexOrDecimal64)(b.ExcessBlobGas) enc.ParentBeaconBlockRoot = b.ParentBeaconBlockRoot enc.SlotNumber = (*math.HexOrDecimal64)(b.SlotNumber) + enc.BlockAccessListHash = b.BlockAccessListHash return json.Marshal(&enc) } @@ -91,6 +93,7 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { ExcessBlobGas *math.HexOrDecimal64 ParentBeaconBlockRoot *common.Hash SlotNumber *math.HexOrDecimal64 + BlockAccessListHash *common.Hash } var dec btHeader if err := json.Unmarshal(input, &dec); err != nil { @@ -162,5 +165,8 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { if dec.SlotNumber != nil { b.SlotNumber = (*uint64)(dec.SlotNumber) } + if dec.BlockAccessListHash != nil { + b.BlockAccessListHash = dec.BlockAccessListHash + } return nil } diff --git a/tests/init.go b/tests/init.go index 3db988a993..55dc2cf601 100644 --- a/tests/init.go +++ b/tests/init.go @@ -490,7 +490,7 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, }, }, "OsakaToBPO1AtTime15k": { @@ -519,7 +519,7 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, }, }, "BPO2": { @@ -549,8 +549,8 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, }, }, "BPO1ToBPO2AtTime15k": { @@ -580,8 +580,8 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, }, }, "BPO3": { @@ -612,8 +612,8 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, }, }, @@ -645,8 +645,8 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, }, }, @@ -679,8 +679,8 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, BPO4: params.DefaultBPO4BlobConfig, }, @@ -714,8 +714,8 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, BPO4: params.DefaultBPO4BlobConfig, }, @@ -750,11 +750,44 @@ var Forks = map[string]*params.ChainConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, BPO4: params.DefaultBPO4BlobConfig, - Amsterdam: params.DefaultBPO4BlobConfig, // TODO update when defined + Amsterdam: params.DefaultBPO2BlobConfig, + }, + }, + "BPO2ToAmsterdamAtTime15k": { + 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(15_000), + DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, + BlobScheduleConfig: ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + Osaka: params.DefaultOsakaBlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, + Amsterdam: params.DefaultBPO2BlobConfig, }, }, "Verkle": { @@ -778,18 +811,6 @@ var Forks = map[string]*params.ChainConfig{ }, } -var bpo1BlobConfig = ¶ms.BlobConfig{ - Target: 9, - Max: 14, - UpdateFraction: 8832827, -} - -var bpo2BlobConfig = ¶ms.BlobConfig{ - Target: 14, - Max: 21, - UpdateFraction: 13739630, -} - // AvailableForks returns the set of defined fork names func AvailableForks() []string { var availableForks []string diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 077db64f39..8d7a7c717a 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -82,7 +82,7 @@ func (tt *TransactionTest) Run() error { } // Intrinsic cost // TODO (MariusVanDerWijden): correctly set this for post-amsterdam tests. - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, *rules, 0) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, 0) if err != nil { return }