From a5a452512f4ad34692ffb47eabccd6e8d29b33ed Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 16 Jun 2026 16:02:44 -0400 Subject: [PATCH 01/11] all: add BAL perf changes --- beacon/engine/ed_codec.go | 7 +- beacon/engine/types.go | 106 +++--- build/checksums.txt | 5 + build/ci.go | 18 + cmd/evm/blockrunner.go | 2 +- cmd/geth/config.go | 23 ++ cmd/geth/main.go | 3 + cmd/utils/flags.go | 23 ++ core/block_validator.go | 13 +- core/blockchain.go | 153 +++++++- core/parallel_state_processor.go | 320 +++++++++++++++++ core/state/bal_state_transition.go | 537 +++++++++++++++++++++++++++++ core/state/database.go | 4 + core/state/database_history.go | 4 + core/state/database_mpt.go | 19 + core/state/database_ubt.go | 4 + core/state/reader.go | 9 + core/state/reader_eip_7928.go | 156 ++++++--- core/state/statedb.go | 82 ++++- core/types.go | 4 +- core/types/bal/bal.go | 135 ++++++++ core/types/bal/bal_encoding.go | 8 +- core/types/bal/bal_reader.go | 196 +++++++++++ core/vm/evm.go | 12 + eth/backend.go | 4 + eth/catalyst/api.go | 6 +- eth/ethconfig/config.go | 5 + tests/block_test.go | 17 +- tests/block_test_util.go | 80 ++++- tests/init_test.go | 7 +- 30 files changed, 1824 insertions(+), 138 deletions(-) create mode 100644 core/parallel_state_processor.go create mode 100644 core/state/bal_state_transition.go create mode 100644 core/types/bal/bal_reader.go diff --git a/beacon/engine/ed_codec.go b/beacon/engine/ed_codec.go index 02a1fd3805..83603616a7 100644 --- a/beacon/engine/ed_codec.go +++ b/beacon/engine/ed_codec.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executableDataMarshaling)(nil) @@ -36,7 +35,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -87,7 +86,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessList *hexutil.Bytes `json:"blockAccessList,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -165,7 +164,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { e.SlotNumber = (*uint64)(dec.SlotNumber) } if dec.BlockAccessList != nil { - e.BlockAccessList = dec.BlockAccessList + e.BlockAccessList = *dec.BlockAccessList } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 980e8f9771..93aeb5527a 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -17,15 +17,18 @@ package engine import ( + "bytes" "fmt" + "github.com/ethereum/go-ethereum/core/types/bal" "math/big" "slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" ) @@ -83,25 +86,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"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *uint64 `json:"blobGasUsed"` + ExcessBlobGas *uint64 `json:"excessBlobGas"` + SlotNumber *uint64 `json:"slotNumber,omitempty"` + BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"` } // JSON type overrides for executableData. @@ -314,13 +317,14 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H } // If Amsterdam is enabled, data.BlockAccessList is always non-nil, - // even for empty blocks with no state transitions. + // even for empty blocks with no state transitions. The wire format is + // the RLP-encoded access list; the header hash is keccak256(rlp). // // If Amsterdam is not enabled yet, blockAccessListHash is expected // to be nil. var blockAccessListHash *common.Hash if data.BlockAccessList != nil { - hash := data.BlockAccessList.Hash() + hash := crypto.Keccak256Hash(data.BlockAccessList) blockAccessListHash = &hash } header := &types.Header{ @@ -347,32 +351,50 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H SlotNumber: data.SlotNumber, BlockAccessListHash: blockAccessListHash, } - return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), nil + body := types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals} + if data.BlockAccessList != nil { + balHash := crypto.Keccak256Hash(data.BlockAccessList) + header.BlockAccessListHash = &balHash + var accessList bal.BlockAccessList + if err := rlp.DecodeBytes(data.BlockAccessList, &accessList); err != nil { + return nil, fmt.Errorf("failed to decode BAL: %w\n", err) + } + block := types.NewBlockWithHeader(header).WithBody(body).WithAccessList(&accessList) + 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(), - BlockAccessList: block.AccessList(), + 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(), + } + // Per Engine API spec (Amsterdam): blockAccessList is the RLP-encoded + // access list, serialized as a hex string. Encode it to bytes here. + if al := block.AccessList(); al != nil { + var buf bytes.Buffer + if err := rlp.Encode(&buf, al); err == nil { + data.BlockAccessList = buf.Bytes() + } } // Add blobs. diff --git a/build/checksums.txt b/build/checksums.txt index 454efa93c4..d6a9f2b803 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 v7.2.0 +# https://github.com/ethereum/execution-specs/releases +# https://github.com/ethereum/execution-specs/releases/download/tests-bal%40v7.2.0 +fc1d9ae174cdd5db789068839999e6f83666cc79f7dac36e973d7616d9a2e2cf fixtures_bal.tar.gz + # version:golang 1.25.10 # https://go.dev/dl/ 20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz diff --git a/build/ci.go b/build/ci.go index 53ade2e1bf..2db6c22616 100644 --- a/build/ci.go +++ b/build/ci.go @@ -160,6 +160,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")) @@ -398,6 +401,7 @@ func doTest(cmdline []string) { // Get test fixtures. if !*short { downloadSpecTestFixtures(csdb, *cachedir) + downloadBALSpecTestFixtures(csdb, *cachedir) } // Configure the toolchain. @@ -463,6 +467,20 @@ func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string return filepath.Join(cachedir, base) } +// downloadBALSpecTestFixtures downloads and extracts the bal-specific execution-spec-tests fixtures. +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/blockrunner.go b/cmd/evm/blockrunner.go index b175f05dc7..d5d957cc40 100644 --- a/cmd/evm/blockrunner.go +++ b/cmd/evm/blockrunner.go @@ -117,7 +117,7 @@ func runBlockTest(ctx *cli.Context, fname string) ([]testResult, error) { test := tests[name] result := &testResult{Name: name, Pass: true} var finalRoot *common.Hash - if err := test.Run(false, rawdb.PathScheme, ctx.Bool(WitnessCrossCheckFlag.Name), tracer, func(res error, chain *core.BlockChain) { + if err := test.Run(false, rawdb.PathScheme, ctx.Bool(WitnessCrossCheckFlag.Name), true, tracer, func(res error, chain *core.BlockChain) { if ctx.Bool(DumpFlag.Name) { if s, _ := chain.State(); s != nil { result.State = dump(s) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 8ac9e7f6bd..437babfb8f 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -20,6 +20,7 @@ import ( "bufio" "errors" "fmt" + "github.com/ethereum/go-ethereum/core/types/bal" "os" "reflect" "runtime" @@ -245,6 +246,28 @@ func makeFullNode(ctx *cli.Context) *node.Node { cfg.Eth.OverrideUBT = &v } + if ctx.IsSet(utils.BALExecutionModeFlag.Name) { + val := ctx.String(utils.BALExecutionModeFlag.Name) + switch val { + case utils.BalExecutionModeOptimized: + cfg.Eth.BALExecutionMode = bal.BALExecutionOptimized + case utils.BalExecutionModeNoBatchIO: + cfg.Eth.BALExecutionMode = bal.BALExecutionNoBatchIO + case utils.BalExecutionModeSequential: + cfg.Eth.BALExecutionMode = bal.BALExecutionSequential + default: + utils.Fatalf("invalid option for --bal.executionmode: %s. acceptable values are full|nobatchio|sequential", val) + } + } + cfg.Eth.BlockingPrefetch = ctx.Bool(utils.BlockingPrefetchFlag.Name) + + prefetchWorkers := ctx.Uint(utils.PrefetchWorkersFlag.Name) + if ctx.IsSet(utils.PrefetchWorkersFlag.Name) && prefetchWorkers == 0 { + prefetchWorkers = uint(runtime.NumCPU()) + log.Warn(fmt.Sprintf("invalid value for --bal.prefetchworkers. got 0. sanitizing to %d", prefetchWorkers)) + } + cfg.Eth.PrefetchWorkers = prefetchWorkers + // Start metrics export if enabled. utils.SetupMetrics(&cfg.Metrics) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 14e3a549cb..09943331f5 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -92,6 +92,9 @@ var ( utils.BinTrieGroupDepthFlag, utils.LightKDFFlag, utils.EthRequiredBlocksFlag, + utils.BALExecutionModeFlag, + utils.PrefetchWorkersFlag, + utils.BlockingPrefetchFlag, utils.CacheFlag, utils.CacheDatabaseFlag, utils.CacheTrieFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index fd9893fe81..bbe4b8a4ff 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -28,6 +28,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" godebug "runtime/debug" "strconv" "strings" @@ -243,6 +244,22 @@ var ( Usage: "Comma separated block number-to-hash mappings to require for peering (=)", Category: flags.EthCategory, } + BALExecutionModeFlag = &cli.StringFlag{ + Name: "bal.executionmode", + Usage: "EIP-7928 block-access-list execution mode (no-op placeholder)", + Category: flags.EthCategory, + } + PrefetchWorkersFlag = &cli.UintFlag{ + Name: "bal.prefetchworkers", + Usage: "The number of concurrent state loading tasks to perform when prefetching BAL state. Default to the number of cpus", + Value: uint(runtime.NumCPU()), + Category: flags.MiscCategory, + } + BlockingPrefetchFlag = &cli.BoolFlag{ + Name: "bal.blockingprefetch", + Usage: "only relevant when executing in parallel with a BAL: if true, the prefetcher will block tx/state-root calculation until all scheduled fetching tasks have completed.", + Category: flags.MiscCategory, + } BloomFilterSizeFlag = &cli.Uint64Flag{ Name: "bloomfilter.size", Usage: "Megabytes of memory allocated to bloom-filter for pruning", @@ -1131,6 +1148,12 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. } ) +const ( + BalExecutionModeOptimized = "full" + BalExecutionModeNoBatchIO = "nobatchio" + BalExecutionModeSequential = "sequential" +) + var ( // TestnetFlags is the flag group of all built-in supported testnets. TestnetFlags = []cli.Flag{ diff --git a/core/block_validator.go b/core/block_validator.go index 962fffb82a..f2df77a27d 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -19,9 +19,9 @@ package core import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" @@ -143,9 +143,14 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { return nil } +type StateRootSource interface { + IntermediateRoot(deleteEmptyObjects bool) common.Hash + Error() error +} + // ValidateState validates the various changes that happen after a state transition, // such as amount of used gas, the receipt roots and the state root itself. -func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult, stateless bool) error { +func (v *BlockValidator) ValidateState(block *types.Block, state StateRootSource, res *ProcessResult, stateless bool) error { if res == nil { return errors.New("nil ProcessResult value") } @@ -201,8 +206,8 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD } // Validate the state root against the received state root and throw // an error if they don't match. - if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { - return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error()) + if root := state.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { + return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, state.Error()) } return nil } diff --git a/core/blockchain.go b/core/blockchain.go index d6edd90133..ea59ff75bd 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "github.com/ethereum/go-ethereum/core/types/bal" "io" "math/big" "runtime" @@ -225,6 +226,10 @@ type BlockChainConfig struct { // Execution configs StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose) EnableWitnessStats bool // Whether trie access statistics collection is enabled + + BALExecutionMode bal.BALExecutionMode + BlockingPrefetch bool + PrefetchWorkers int } // DefaultConfig returns the default config. @@ -365,12 +370,13 @@ type BlockChain struct { stopping atomic.Bool // false if chain is running, true when stopped procInterrupt atomic.Bool // interrupt signaler for block processing - engine consensus.Engine - validator Validator // Block and state validator interface - prefetcher Prefetcher - processor Processor // Block transaction processor interface - logger *tracing.Hooks - stateSizer *state.SizeTracker // State size tracking + engine consensus.Engine + validator Validator // Block and state validator interface + prefetcher Prefetcher + processor Processor // Block transaction processor interface + parallelProcessor ParallelStateProcessor // block processor for use with access lists + logger *tracing.Hooks + stateSizer *state.SizeTracker // State size tracking lastForkReadyAlert time.Time // Last time there was a fork readiness print out slowBlockThreshold time.Duration // Block execution time threshold beyond which detailed statistics will be logged @@ -433,6 +439,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, bc.validator = NewBlockValidator(chainConfig, bc) bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc) bc.processor = NewStateProcessor(bc.hc) + bc.parallelProcessor = *NewParallelStateProcessor(bc.hc, bc.GetVMConfig()) genesisHeader := bc.GetHeaderByNumber(0) if genesisHeader == nil { @@ -1642,7 +1649,7 @@ func (bc *BlockChain) writeKnownBlock(block *types.Block) error { // writeBlockWithState writes block, metadata and corresponding state data to the // database. -func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb *state.StateDB) error { +func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb state.Committer) error { if !bc.HasHeader(block.ParentHash(), block.NumberU64()-1) { return consensus.ErrUnknownAncestor } @@ -1756,7 +1763,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. // writeBlockAndSetHead is the internal implementation of WriteBlockAndSetHead. // This function expects the chain mutex to be held. -func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) { +func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state state.Committer, emitHeadEvent bool) (status WriteStatus, err error) { if err := bc.writeBlockWithState(block, receipts, state); err != nil { return NonStatTy, err } @@ -2111,16 +2118,136 @@ type ExecuteConfig struct { EnableWitnessStats bool } +func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block *types.Block, setHead bool) (procRes *blockProcessingResult, blockEndErr error) { + var ( + startTime = time.Now() + procTime time.Duration + statedb *state.StateDB + ) + + sdb := state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps) + + useAsyncReads := bc.cfg.BALExecutionMode != bal.BALExecutionNoBatchIO + al := block.AccessList() + // Preprocess the access list once for the whole block; the resulting + // structure is read-only and shared by the prefetch reader, the state + // transition and every per-transaction execution reader. + prepared := bal.NewAccessListReader(*al) + prefetchReader, err := sdb.ReaderWithPrefetch(parentRoot, prepared.StorageKeys(useAsyncReads), bc.cfg.PrefetchWorkers, bc.cfg.BlockingPrefetch) + if err != nil { + return nil, err + } + + stateTransition, err := state.NewBALStateTransition(block, prefetchReader, sdb, parentRoot, prepared) + if err != nil { + return nil, err + } + statedb, err = state.NewWithReader(parentRoot, sdb, prefetchReader) + if err != nil { + return nil, err + } + + if bc.logger != nil && bc.logger.OnBlockStart != nil { + bc.logger.OnBlockStart(tracing.BlockEvent{ + Block: block, + Finalized: bc.CurrentFinalBlock(), + Safe: bc.CurrentSafeBlock(), + }) + } + if bc.logger != nil && bc.logger.OnBlockEnd != nil { + defer func() { + bc.logger.OnBlockEnd(blockEndErr) + }() + } + + res, err := bc.parallelProcessor.Process(block, stateTransition, statedb, bc.cfg.VmConfig) + if err != nil { + return nil, err + } + + if err := bc.validator.ValidateState(block, stateTransition, res.ProcessResult, false); err != nil { + return nil, err + } + + procTime = time.Since(startTime) + writeStart := time.Now() + // Write the block to the chain and get the status. + var ( + //wstart = time.Now() + status WriteStatus + ) + if !setHead { + // Don't set the head, only insert the block + err = bc.writeBlockWithState(block, res.ProcessResult.Receipts, stateTransition) + } else { + status, err = bc.writeBlockAndSetHead(block, res.ProcessResult.Receipts, res.ProcessResult.Logs, stateTransition, false) + } + if err != nil { + return nil, err + } + writeTime := time.Since(writeStart) + var stats ExecuteStats + + wc := stateTransition.WrittenCounts() + d := stateTransition.Deletions() + //codeLoaded, codeLoadBytes := prefetchReader.(state.CodeLoadTracker).CodeLoads() + //stats.AccountLoaded = al.UniqueAccountCount() + stats.AccountUpdated = wc.Accounts - d.Accounts + stats.AccountDeleted = d.Accounts + //stats.StorageLoaded = al.UniqueStorageSlotCount() + stats.StorageUpdated = wc.StorageSlots - d.Storage + stats.StorageDeleted = d.Storage + //stats.CodeLoaded = codeLoaded + //stats.CodeLoadBytes = codeLoadBytes + stats.CodeUpdated = wc.Codes + stats.CodeUpdateBytes = wc.CodeBytes + + //stats.ExecWall = res.ExecTime + //stats.PostProcess = res.PostProcessTime + + if m := res.StateTransitionMetrics; m != nil { + stats.AccountHashes = m.AccountUpdate + m.StateUpdate + m.StateHash + stats.AccountCommits = m.AccountCommits + stats.StorageCommits = m.StorageCommits + stats.DatabaseCommit = m.TrieDBCommits + //stats.Prefetch = m.StatePrefetch + } + //stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed + + stats.StateReadCacheStats = prefetchReader.(state.ReaderStater).GetStats() + + elapsed := time.Since(startTime) + 1 // prevent zero division + stats.TotalTime = elapsed + stats.MgasPerSecond = float64(res.ProcessResult.GasUsed) * 1000 / float64(elapsed) + stats.BlockWrite = writeTime + + // TODO: reinstate + //stats.balTransitionStats = res.StateTransitionMetrics + + return &blockProcessingResult{ + usedGas: res.ProcessResult.GasUsed, + procTime: procTime, + status: status, + witness: nil, + stats: &stats, + }, nil +} + // ProcessBlock executes and validates the given block. If there was no error // 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 + blockHasAccessList = block.AccessList() != nil ) + + if blockHasAccessList && bc.cfg.BALExecutionMode != bal.BALExecutionSequential { + return bc.processBlockWithAccessList(parentRoot, block, config.WriteHead) + } defer interrupt.Store(true) // terminate the prefetch at the end if bc.chainConfig.IsUBT(block.Number(), block.Time()) { diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go new file mode 100644 index 0000000000..299d7efd7b --- /dev/null +++ b/core/parallel_state_processor.go @@ -0,0 +1,320 @@ +package core + +import ( + "cmp" + "context" + "fmt" + "runtime" + "slices" + "time" + + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/core/vm" + "golang.org/x/sync/errgroup" +) + +// ProcessResultWithMetrics wraps ProcessResult with timing breakdown for BAL block processing. +type ProcessResultWithMetrics struct { + ProcessResult *ProcessResult + PreProcessTime time.Duration + StateTransitionMetrics *state.BALStateTransitionMetrics + ExecTime time.Duration + PostProcessTime time.Duration +} + +// errResult wraps an error into a new ProcessResultWithMetrics instance +func errResult(err error) *ProcessResultWithMetrics { + return &ProcessResultWithMetrics{ProcessResult: &ProcessResult{Error: err}} +} + +// ParallelStateProcessor is used to execute and verify blocks containing +// access lists. +type ParallelStateProcessor struct { + *StateProcessor + chain *HeaderChain + vmCfg *vm.Config +} + +// NewParallelStateProcessor returns a new ParallelStateProcessor instance. +func NewParallelStateProcessor(chain *HeaderChain, vmConfig *vm.Config) *ParallelStateProcessor { + return &ParallelStateProcessor{ + StateProcessor: NewStateProcessor(chain), + chain: chain, + vmCfg: vmConfig, + } +} + +// execVMConfig returns the subset of the configured VM options that is safe to +// reuse across the parallel per-transaction and post-transaction executions. +// Only the fields explicitly copied here are propagated (mirroring the original +// per-tx behaviour); notably the full caller-supplied config is used only for +// pre-execution in processBlockPreTx. +func (p *ParallelStateProcessor) execVMConfig() vm.Config { + return vm.Config{ + NoBaseFee: p.vmCfg.NoBaseFee, + EnablePreimageRecording: p.vmCfg.EnablePreimageRecording, + ExtraEips: slices.Clone(p.vmCfg.ExtraEips), + } +} + +// called by resultHandler when all transactions have successfully executed. +// performs post-tx state transition (system contracts and withdrawals) +// and calculates the ProcessResult, returning it to be sent on resCh +// by resultHandler +func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, preTxBAL *bal.ConstructionBlockAccessList, accessList *bal.AccessListReader, statedb *state.StateDB, results []txExecResult) *ProcessResultWithMetrics { + tExec := time.Since(tExecStart) + tPostprocessStart := time.Now() + header := block.Header() + + // The post-execution changes are recorded at the BAL index immediately + // following the last transaction. + lastBALIdx := len(block.Transactions()) + 1 + postTxState := statedb.WithReader(state.NewReaderWithAccessList(statedb.Reader(), accessList, lastBALIdx)) + + evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), postTxState, p.chainConfig(), p.execVMConfig()) + + // 1. order the receipts by tx index + // 2. correctly calculate the cumulative gas used per receipt, returning bad block error if it goes over the allowed + slices.SortFunc(results, func(a, b txExecResult) int { + return cmp.Compare(a.receipt.TransactionIndex, b.receipt.TransactionIndex) + }) + + var ( + // Per-dimension cumulative sums for 2D block gas (EIP-8037). + sumRegular uint64 + sumState uint64 + cumulativeReceipt uint64 // cumulative receipt gas (what users pay) + + allLogs []*types.Log + allReceipts []*types.Receipt + ) + for _, result := range results { + sumRegular += result.txRegular + sumState += result.txState + + cumulativeReceipt += result.execGas + result.receipt.CumulativeGasUsed = cumulativeReceipt + allLogs = append(allLogs, result.receipt.Logs...) + allReceipts = append(allReceipts, result.receipt) + } + // Block gas = max(sum_regular, sum_state) per EIP-8037. + blockGasUsed := max(sumRegular, sumState) + if blockGasUsed > header.GasLimit { + return errResult(fmt.Errorf("gas limit exceeded")) + } + + requests, postBAL, err := PostExecution(context.Background(), p.chainConfig(), block.Number(), block.Time(), allLogs, evm, uint32(lastBALIdx)) + if err != nil { + return errResult(err) + } + + p.chain.Engine().Finalize(p.chain, block.Header(), evm.StateDB, block.Body(), uint32(lastBALIdx), postBAL) + + blockAccessList := bal.NewConstructionBlockAccessList() + blockAccessList.Merge(preTxBAL) + blockAccessList.Merge(postBAL) + for _, res := range results { + blockAccessList.Merge(res.blockAccessList) + } + + // TODO: do we move validation to ValidateState? + if block.AccessList().Hash() != blockAccessList.ToEncodingObj().Hash() { + // TODO: expose json string method on encoding block access list and log it here + return errResult(fmt.Errorf("invalid block access list: mismatch between local and remote block access list")) + } + + tPostprocess := time.Since(tPostprocessStart) + + return &ProcessResultWithMetrics{ + ProcessResult: &ProcessResult{ + Receipts: allReceipts, + Requests: requests, + Logs: allLogs, + GasUsed: blockGasUsed, + Bal: blockAccessList, + }, + PostProcessTime: tPostprocess, + ExecTime: tExec, + } +} + +type txExecResult struct { + receipt *types.Receipt + err error // non-EVM error which would render the block invalid + execGas uint64 // gas reported on the receipt (what the user pays) + + // Per-tx dimensional gas for Amsterdam 2D gas accounting (EIP-8037). + txRegular uint64 + txState uint64 + + blockAccessList *bal.ConstructionBlockAccessList +} + +// resultHandler polls until all transactions have finished executing and the +// state root calculation is complete. The result is emitted on resCh. +func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxBAL *bal.ConstructionBlockAccessList, prepared *bal.AccessListReader, statedb *state.StateDB, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { + // 1. if the block has transactions, receive the execution results from all of them and return an error on resCh if any txs err'd + // 2. once all txs are executed, compute the post-tx state transition and produce the ProcessResult sending it on resCh (or an error if the post-tx state didn't match what is reported in the BAL) + var ( + results []txExecResult + cumulativeStateGas uint64 + cumulativeRegularGas uint64 + execErr error + ) + + if numTx := len(block.Transactions()); numTx > 0 { + for completed := 0; completed < numTx; completed++ { + res := <-txResCh + if execErr != nil { + // A block-invalidating result was already seen; keep draining so + // the worker goroutines don't block on their sends. + continue + } + switch { + case res.err != nil: + execErr = res.err + default: + bottleneck := max(cumulativeRegularGas+res.txRegular, cumulativeStateGas+res.txState) + if bottleneck > block.GasLimit() { + execErr = fmt.Errorf("block used too much gas in bottleneck dimension: %d. block gas limit is %d", bottleneck, block.GasLimit()) + continue + } + cumulativeRegularGas += res.txRegular + cumulativeStateGas += res.txState + results = append(results, res) + } + } + + if execErr != nil { + // Drain stateRootCalcResCh so the calcAndVerifyRoot goroutine can exit. + <-stateRootCalcResCh + resCh <- errResult(execErr) + return + } + } + + execResults := p.prepareExecResult(block, tExecStart, preTxBAL, prepared, statedb, results) + rootCalcRes := <-stateRootCalcResCh + + switch { + case execResults.ProcessResult.Error != nil: + resCh <- execResults + case rootCalcRes.err != nil: + resCh <- errResult(rootCalcRes.err) + default: + execResults.StateTransitionMetrics = rootCalcRes.metrics + resCh <- execResults + } +} + +type stateRootCalculationResult struct { + err error + metrics *state.BALStateTransitionMetrics +} + +// calcAndVerifyRoot performs the post-state root hash calculation, verifying +// it against what is reported by the block and returning a result on resCh. +func (p *ParallelStateProcessor) calcAndVerifyRoot(block *types.Block, stateTransition *state.BALStateTransition, resCh chan stateRootCalculationResult) { + root := stateTransition.IntermediateRoot(false) + + res := stateRootCalculationResult{ + metrics: stateTransition.Metrics(), + } + if root != block.Root() { + res.err = fmt.Errorf("state root mismatch. local: %x. remote: %x", root, block.Root()) + } + resCh <- res +} + +// execTx executes a single transaction returning a result which includes state accessed/modified. +func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transaction, balIdx int, db *state.StateDB, signer types.Signer) *txExecResult { + header := block.Header() + evmContext := NewEVMBlockContext(header, p.chain, nil) + evm := vm.NewEVM(evmContext, db, p.chainConfig(), p.execVMConfig()) + + msg, err := TransactionToMessage(tx, signer, header.BaseFee) + if err != nil { + return &txExecResult{err: fmt.Errorf("could not apply tx %d [%v]: %w", balIdx, tx.Hash().Hex(), err)} + } + sender, err := signer.Sender(tx) + if err != nil { + return &txExecResult{err: fmt.Errorf("could not recover sender for tx at bal idx %d: %w", balIdx, err)} + } + + gp := NewGasPool(block.GasLimit()) + // TODO: make precompiled addresses be resolvable from chain config + block + db.Prepare(evm.GetRules(), sender, block.Coinbase(), tx.To(), vm.PrecompiledAddressesCancun, tx.AccessList()) + db.SetTxContext(tx.Hash(), balIdx-1, uint32(balIdx)) + + receipt, txBAL, err := ApplyTransactionWithEVM(msg, gp, db, block.Number(), block.Hash(), evmContext.Time, tx, evm) + if err != nil { + return &txExecResult{err: fmt.Errorf("could not apply tx %d [%v]: %w", balIdx, tx.Hash().Hex(), err)} + } + + return &txExecResult{ + receipt: receipt, + execGas: receipt.GasUsed, + txRegular: gp.cumulativeRegular, + txState: gp.cumulativeState, + blockAccessList: txBAL, + } +} + +func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, parent *types.Header, statedb *state.StateDB, cfg vm.Config) *bal.ConstructionBlockAccessList { + header := block.Header() + evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), statedb, p.chainConfig(), cfg) + return PreExecution(context.Background(), block.BeaconRoot(), parent, p.chainConfig(), evm, block.Number(), block.Time()) +} + +// Process performs EVM execution and state root computation for a block which is known +// to contain an access list. +func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *state.BALStateTransition, statedb *state.StateDB, cfg vm.Config) (*ProcessResultWithMetrics, error) { + header := block.Header() + signer := types.MakeSigner(p.chainConfig(), header.Number, header.Time) + + var ( + resCh = make(chan *ProcessResultWithMetrics) + rootCalcResultCh = make(chan stateRootCalculationResult) + txResCh = make(chan txExecResult) + ) + + // Pre-transaction processing: system-contract updates and the pre-tx BAL. + pStart := time.Now() + startingState := statedb.Copy() + prepared := stateTransition.PreparedAccessList() + preTxBAL := p.processBlockPreTx(block, p.chain.GetHeader(block.ParentHash(), block.NumberU64()), statedb, cfg) + tPreprocess := time.Since(pStart) + + // Execute transactions and the state-root calculation in parallel. + tExecStart := time.Now() + go p.resultHandler(block, preTxBAL, prepared, statedb, tExecStart, txResCh, rootCalcResultCh, resCh) + + // Workers execute transactions concurrently against per-tx state copies. + // Each worker reports completion (and any block-invalidating error) on + // txResCh, which resultHandler drains. Worker errors therefore flow through + // the channel rather than the errgroup, so the group is used purely to bound + // concurrency and Wait() is intentionally not called. + var workers errgroup.Group + workers.SetLimit(runtime.NumCPU()) + for i, tx := range block.Transactions() { + balIdx := i + 1 + prestate := startingState.Copy() + workers.Go(func() error { + prestate = prestate.WithReader(state.NewReaderWithAccessList(statedb.Reader(), prepared, balIdx)) + txResCh <- *p.execTx(block, tx, balIdx, prestate, signer) + return nil + }) + } + + go p.calcAndVerifyRoot(block, stateTransition, rootCalcResultCh) + + res := <-resCh + if res.ProcessResult.Error != nil { + return nil, res.ProcessResult.Error + } + // TODO: remove preprocess metric ? + res.PreProcessTime = tPreprocess + return res, nil +} diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go new file mode 100644 index 0000000000..5cf761d647 --- /dev/null +++ b/core/state/bal_state_transition.go @@ -0,0 +1,537 @@ +package state + +import ( + "slices" + "sync" + "sync/atomic" + "time" + + "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/crypto" + "github.com/ethereum/go-ethereum/trie/trienode" + "github.com/holiman/uint256" + "golang.org/x/sync/errgroup" +) + +// BALStateTransition is responsible for performing the state root update +// and commit for EIP 7928 access-list-containing blocks. An instance of +// this object is only used for a single block. +type BALStateTransition struct { + accessList *bal.AccessListReader + written bal.WrittenCounts + db Database + reader Reader + stateTrie Trie + parentRoot common.Hash + + // the computed state root of the block + rootHash common.Hash + // the state modifications performed by the block + diffs bal.StateMutations + + // a map of common.Address -> *types.StateAccount containing the block + // prestate of all accounts that will be modified + prestates sync.Map + + postStates map[common.Address]*types.StateAccount + // a map of common.Address -> Trie containing the account tries for all + // accounts with mutated storage + tries sync.Map //map[common.Address]Trie + deletions map[common.Address]struct{} + + // Deletion counters; not derivable from the BAL alone (selfdestruct vs + // balance drain is indistinguishable without prestate). + accountDeleted int + storageDeleted atomic.Int64 + + stateUpdate *StateUpdate + + metrics BALStateTransitionMetrics + maxBALIdx int + + err error +} + +func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { + return &s.metrics +} + +// DeletionCounts holds per-block deletion counters for accounts/storage +type DeletionCounts struct { + Accounts int + Storage int +} + +func (s *BALStateTransition) Deletions() DeletionCounts { + return DeletionCounts{ + Accounts: s.accountDeleted, + Storage: int(s.storageDeleted.Load()), + } +} + +type BALStateTransitionMetrics struct { + // trie hashing metrics + AccountUpdate time.Duration + StatePrefetch time.Duration + StateUpdate time.Duration + StateHash time.Duration + + // commit metrics + AccountCommits time.Duration + StorageCommits time.Duration + SnapshotCommits time.Duration + TrieDBCommits time.Duration + TotalCommitTime time.Duration +} + +func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Database, parentRoot common.Hash, prepared *bal.AccessListReader) (*BALStateTransition, error) { + stateTrie, err := db.OpenTrie(parentRoot) + if err != nil { + return nil, err + } + + return &BALStateTransition{ + accessList: prepared, + written: block.AccessList().WrittenCounts(), + db: db, + reader: prefetchReader, + stateTrie: stateTrie, + parentRoot: parentRoot, + rootHash: common.Hash{}, + diffs: make(bal.StateMutations), + prestates: sync.Map{}, + postStates: make(map[common.Address]*types.StateAccount), + tries: sync.Map{}, + deletions: make(map[common.Address]struct{}), + stateUpdate: nil, + maxBALIdx: len(block.Transactions()) + 1, + }, nil +} + +// WrittenCounts returns the cached BAL write counts (computed once per block). +func (s *BALStateTransition) WrittenCounts() bal.WrittenCounts { + return s.written +} + +// PreparedAccessList returns the shared, read-only preprocessed access list for +// the block. It is built once per block and reused by the parallel execution +// readers so the preprocessing is not repeated per transaction. +func (s *BALStateTransition) PreparedAccessList() *bal.AccessListReader { + return s.accessList +} + +func (s *BALStateTransition) Error() error { + return s.err +} + +func (s *BALStateTransition) setError(err error) { + if s.err == nil { + s.err = err + } +} + +// isAccountDeleted checks whether the state account was deleted in this block. Post selfdestruct-removal, +// deletions can only occur if an account which has a balance becomes the target of a CREATE2 initcode +// which calls SENDALL, clearing the account and marking it for deletion. +func isAccountDeleted(prestate *types.StateAccount, mutations bal.AccountMutations) bool { + // TODO: figure out how to simplify this method + if mutations.Code != nil && len(mutations.Code) != 0 { + return false + } + if mutations.Nonce != nil && *mutations.Nonce != 0 { + return false + } + if mutations.StorageWrites != nil && len(mutations.StorageWrites) > 0 { + return false + } + if mutations.Balance != nil { + if mutations.Balance.IsZero() { + if prestate.Nonce != 0 || prestate.Balance.IsZero() || common.BytesToHash(prestate.CodeHash) != types.EmptyCodeHash { + return false + } + // consider an empty account with storage to be deleted, so we don't check root here + return true + } + } + return false +} + +// updateAccount applies the block state mutations to a given account returning +// the updated state account and new code (if the account code changed) +func (s *BALStateTransition) updateAccount(addr common.Address) (*types.StateAccount, []byte) { + a, _ := s.prestates.Load(addr) + acct := a.(*types.StateAccount) + + acct, diff := acct.Copy(), s.diffs[addr] + code := diff.Code + + if diff.Nonce != nil { + acct.Nonce = *diff.Nonce + } + if diff.Balance != nil { + acct.Balance = new(uint256.Int).Set(diff.Balance) + } + if tr, ok := s.tries.Load(addr); ok { + acct.Root = tr.(Trie).Hash() + } + return acct, code +} + +func (s *BALStateTransition) commitAccount(addr common.Address) (*AccountUpdate, *trienode.NodeSet, error) { + op := &AccountUpdate{ + Address: addr, + Data: s.postStates[addr], // TODO: cache the updated state account somewhere + } + var prestate *types.StateAccount + if ps, exist := s.prestates.Load(addr); exist { + op.Origin = ps.(*types.StateAccount) + } + + if s.diffs[addr].Code != nil { + code := ContractCode{ + Hash: crypto.Keccak256Hash(s.diffs[addr].Code), + Blob: s.diffs[addr].Code, + } + if prestate == nil { + code.OriginHash = types.EmptyCodeHash + } else { + code.OriginHash = common.BytesToHash(prestate.CodeHash) + } + op.Code = &code + } + + if len(s.diffs[addr].StorageWrites) == 0 { + return op, nil, nil + } + + op.Storages = make(map[common.Hash]common.Hash) + op.StoragesOriginByHash = make(map[common.Hash]common.Hash) + op.StoragesOriginByKey = make(map[common.Hash]common.Hash) + + for key, value := range s.diffs[addr].StorageWrites { + hash := crypto.Keccak256Hash(key[:]) + op.Storages[hash] = value + origin, err := s.reader.Storage(addr, key) + if err != nil { + return nil, nil, err + } + op.StoragesOriginByHash[hash] = origin + op.StoragesOriginByKey[key] = origin + } + tr, _ := s.tries.Load(addr) + root, nodes := tr.(Trie).Commit(false) + s.postStates[addr].Root = root + return op, nodes, nil +} + +// CommitWithUpdate flushes mutated trie nodes and state accounts to disk. +func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *StateUpdate, error) { + // 1) create a stateUpdate object + // Commit objects to the trie, measuring the elapsed time + var ( + //commitStart = time.Now() + accountTrieNodesUpdated int + accountTrieNodesDeleted int + storageTrieNodesUpdated int + storageTrieNodesDeleted int + + lock sync.Mutex // protect two maps below + nodes = trienode.NewMergedNodeSet() // aggregated trie nodes + updates = make(map[common.Hash]*AccountUpdate, len(s.diffs)) // aggregated account updates + + // merge aggregates the dirty trie nodes into the global set. + // + // Given that some accounts may be destroyed and then recreated within + // the same block, it's possible that a node set with the same owner + // may already exist. In such cases, these two sets are combined, with + // the later one overwriting the previous one if any nodes are modified + // or deleted in both sets. + // + // merge run concurrently across all the state objects and account trie. + merge = func(set *trienode.NodeSet) error { + if set == nil { + return nil + } + lock.Lock() + defer lock.Unlock() + + updates, deletes := set.Size() + if set.Owner == (common.Hash{}) { + accountTrieNodesUpdated += updates + accountTrieNodesDeleted += deletes + } else { + storageTrieNodesUpdated += updates + storageTrieNodesDeleted += deletes + } + return nodes.Merge(set) + } + ) + + destructedPrestates := make(map[common.Address]*types.StateAccount) + s.prestates.Range(func(key, value any) bool { + addr := key.(common.Address) + acct := value.(*types.StateAccount) + destructedPrestates[addr] = acct + return true + }) + + deletes, delNodes, err := handleDestruction(s.db, s.stateTrie, s.parentRoot, noStorageWiping, slices.Values(s.accessList.AllDestructions()), destructedPrestates) + if err != nil { + return common.Hash{}, nil, err + } + for _, set := range delNodes { + if err := merge(set); err != nil { + return common.Hash{}, nil, err + } + } + + // Handle all state updates afterwards, concurrently to one another to shave + // off some milliseconds from the commit operation. Also accumulate the code + // writes to run in parallel with the computations. + var ( + start = time.Now() + root common.Hash + workers errgroup.Group + ) + // Schedule the account trie first since that will be the biggest, so give + // it the most time to crunch. + // + // TODO(karalabe): This account trie commit is *very* heavy. 5-6ms at chain + // heads, which seems excessive given that it doesn't do hashing, it just + // shuffles some data. For comparison, the *hashing* at chain head is 2-3ms. + // We need to investigate what's happening as it seems something's wonky. + // Obviously it's not an end of the world issue, just something the original + // code didn't anticipate for. + workers.Go(func() error { + // Write the account trie changes, measuring the amount of wasted time + newroot, set := s.stateTrie.Commit(true) + root = newroot + + if err := merge(set); err != nil { + return err + } + s.metrics.AccountCommits = time.Since(start) + return nil + }) + + // Schedule each of the storage tries that need to be updated, so they can + // run concurrently to one another. + // + // TODO(karalabe): Experimentally, the account commit takes approximately the + // same time as all the storage commits combined, so we could maybe only have + // 2 threads in total. But that kind of depends on the account commit being + // more expensive than it should be, so let's fix that and revisit this todo. + for addr, _ := range s.diffs { + if _, isDeleted := s.deletions[addr]; isDeleted { + continue + } + + address := addr + // Run the storage updates concurrently to one another + workers.Go(func() error { + // Write any storage changes in the state object to its storage trie + update, set, err := s.commitAccount(address) + if err != nil { + return err + } + + if err := merge(set); err != nil { + return err + } + lock.Lock() + updates[crypto.Keccak256Hash(address[:])] = update + s.metrics.StorageCommits = time.Since(start) // overwrite with the longest storage commit runtime + lock.Unlock() + return nil + }) + } + // Wait for everything to finish and update the metrics + if err := workers.Wait(); err != nil { + return common.Hash{}, nil, err + } + + storageDeleted := s.storageDeleted.Load() + accountUpdatedMeter.Mark(int64(s.written.Accounts - s.accountDeleted)) + storageUpdatedMeter.Mark(int64(s.written.StorageSlots) - storageDeleted) + accountDeletedMeter.Mark(int64(s.accountDeleted)) + storageDeletedMeter.Mark(storageDeleted) + accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated)) + accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted)) + storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated)) + storageTriesDeletedMeter.Mark(int64(storageTrieNodesDeleted)) + + storageKeyType := StorageKeyHashed + if noStorageWiping { + storageKeyType = StorageKeyPlain + } + update := NewStateUpdate(storageKeyType, s.parentRoot, root, block, deletes, updates, nodes) + + if err := s.db.Commit(update); err != nil { + return common.Hash{}, nil, err + } + // TODO: fix the following metrics: + /* + snapshotCommits, trieDBCommits, err := flushStateUpdate(s.db, block, ret) + if err != nil { + return common.Hash{}, nil, err + } + + s.metrics.SnapshotCommits, s.metrics.TrieDBCommits = snapshotCommits, trieDBCommits + s.metrics.TotalCommitTime = time.Since(commitStart) + */ + return root, update, nil +} + +func (s *BALStateTransition) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, error) { + hash, _, err := s.CommitWithUpdate(block, deleteEmptyObjects, noStorageWiping) + return hash, err +} + +// IntermediateRoot applies block state mutations and computes the updated state +// trie root. +func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { + if s.rootHash != (common.Hash{}) { + return s.rootHash + } + + // State root calculation proceeds as follows: + + // 1 (a): load the origin storage values for all slots which were modified during the block (this is needed for computing the stateUpdate) + // 1 (b): update each mutated account, producing the post-block state object by applying the state mutations to the prestate (retrieved in 1a). + // 1 (c): prefetch the intermediate trie nodes of the mutated state set from the account trie. + // + // 2: compute the post-state root of the account trie + // + // Steps 1/2 are performed sequentially, with steps 1a-d performed in parallel + + start := time.Now() + + var wg sync.WaitGroup + + s.diffs = *s.accessList.Mutations(s.maxBALIdx + 1) + + for addr, d := range s.diffs { + wg.Add(1) + address := addr + diff := d + go func() { + defer wg.Done() + + // 1 (b): update each mutated account, producing the post-block state object by applying the state mutations to the prestate (retrieved in 1a). + acct, err := s.reader.Account(address) + if err != nil { + s.setError(err) + return + } + + if acct == nil { + acct = types.NewEmptyStateAccount() + } + s.prestates.Store(address, acct) + + if len(diff.StorageWrites) > 0 { + tr, err := s.db.OpenStorageTrie(s.parentRoot, address, acct.Root, s.stateTrie) + if err != nil { + s.setError(err) + return + } + s.tries.Store(address, tr) + + var ( + updateKeys, updateValues [][]byte + deleteKeys [][]byte + ) + for key, val := range diff.StorageWrites { + if val != (common.Hash{}) { + updateKeys = append(updateKeys, key[:]) + updateValues = append(updateValues, common.TrimLeftZeroes(val[:])) + } else { + deleteKeys = append(deleteKeys, key[:]) + } + } + + if err := tr.UpdateStorageBatch(address, updateKeys, updateValues); err != nil { + s.setError(err) + return + } + + for _, key := range deleteKeys { + if err := tr.DeleteStorage(address, key); err != nil { + s.setError(err) + return + } + } + + hashStart := time.Now() + tr.Hash() + s.metrics.StateHash = time.Since(hashStart) + } + }() + } + + wg.Add(1) + // 1 (c): prefetch the intermediate trie nodes of the mutated state set from the account trie. + go func() { + defer wg.Done() + prefetchStart := time.Now() + var prefetchAddrs []common.Address + for addr, _ := range s.diffs { + prefetchAddrs = append(prefetchAddrs, addr) + } + if err := s.stateTrie.PrefetchAccount(prefetchAddrs); err != nil { + s.setError(err) + return + } + s.metrics.StatePrefetch = time.Since(prefetchStart) + }() + + wg.Wait() + s.metrics.AccountUpdate = time.Since(start) + + // 2: compute the post-state root of the account trie + stateUpdateStart := time.Now() + for mutatedAddr, _ := range s.diffs { + p, _ := s.prestates.Load(mutatedAddr) + prestate := p.(*types.StateAccount) + + isDeleted := isAccountDeleted(prestate, s.diffs[mutatedAddr]) + if isDeleted { + if err := s.stateTrie.DeleteAccount(mutatedAddr); err != nil { + s.setError(err) + return common.Hash{} + } + s.deletions[mutatedAddr] = struct{}{} + s.accountDeleted++ + } else { + acct, code := s.updateAccount(mutatedAddr) + + if code != nil { + codeHash := crypto.Keccak256Hash(code) + acct.CodeHash = codeHash.Bytes() + if err := s.stateTrie.UpdateContractCode(mutatedAddr, codeHash, code); err != nil { + s.setError(err) + return common.Hash{} + } + } + if err := s.stateTrie.UpdateAccount(mutatedAddr, acct, len(code)); err != nil { + s.setError(err) + return common.Hash{} + } + s.postStates[mutatedAddr] = acct + } + } + + s.metrics.StateUpdate = time.Since(stateUpdateStart) + + stateTrieHashStart := time.Now() + s.rootHash = s.stateTrie.Hash() + s.metrics.StateHash = time.Since(stateTrieHashStart) + return s.rootHash +} + +func (s *BALStateTransition) Preimages() map[common.Hash][]byte { + // TODO: implement this + return make(map[common.Hash][]byte) +} diff --git a/core/state/database.go b/core/state/database.go index 1001a4b4cf..273693a2db 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -54,6 +54,10 @@ type Database interface { // Reader returns a state reader associated with the specified state root. Reader(root common.Hash) (Reader, error) + // ReaderWithPrefetch returns a reader which asynchronously fetches block + // access list state in the background. + ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) + // Iteratee returns a state iteratee associated with the specified state root, // through which the account iterator and storage iterator can be created. Iteratee(root common.Hash) (Iteratee, error) diff --git a/core/state/database_history.go b/core/state/database_history.go index fbf4ab5f9c..ddc6d79238 100644 --- a/core/state/database_history.go +++ b/core/state/database_history.go @@ -223,6 +223,10 @@ type HistoricDB struct { codedb *CodeDB } +func (db *HistoricDB) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) { + panic("not implemented") +} + // Type returns the trie type of the underlying database. func (db *HistoricDB) Type() DatabaseType { // TODO(rjl493456442) support UBT in the future diff --git a/core/state/database_mpt.go b/core/state/database_mpt.go index 42c5f2e5ef..5e7d278232 100644 --- a/core/state/database_mpt.go +++ b/core/state/database_mpt.go @@ -185,3 +185,22 @@ func (db *MPTDatabase) Commit(update *StateUpdate) error { func (db *MPTDatabase) Iteratee(root common.Hash) (Iteratee, error) { return newStateIteratee(true, root, db.triedb, db.snap) } + +func (db *MPTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) { + base, err := db.StateReader(stateRoot) + if err != nil { + return nil, err + } + // Construct the state reader with native cache and associated statistics + r := newStateReaderWithStats(newStateReaderWithCache(base)) + + // Construct the state reader with background prefetching + pr := newPrefetchStateReader(r, accessList, threads) + if block { + if err := pr.Wait(); err != nil { + panic("this should unreachable") + } + } + + return newReaderWithPrefetch(db.codedb.Reader(), pr, pr), nil +} diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go index d9b2f07a77..0affdea2ed 100644 --- a/core/state/database_ubt.go +++ b/core/state/database_ubt.go @@ -96,6 +96,10 @@ func (db *UBTDatabase) Reader(stateRoot common.Hash) (Reader, error) { return newReader(db.codedb.Reader(), sr), nil } +func (db *UBTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) { + panic("not implemented") +} + // ReadersWithCacheStats creates a pair of state readers that share the same // underlying state reader and internal state cache, while maintaining separate // statistics respectively. diff --git a/core/state/reader.go b/core/state/reader.go index be07cec0f9..c485af96ed 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -560,6 +560,7 @@ func (r *stateReaderWithStats) GetStateStats() StateReaderStats { type reader struct { ContractCodeReader StateReader + PrefetcherMetricer } // newReader constructs a reader with the supplied code reader and state reader. @@ -570,6 +571,14 @@ func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader { } } +func newReaderWithPrefetch(codeReader ContractCodeReader, stateReader StateReader, metricer PrefetcherMetricer) *reader { + return &reader{ + ContractCodeReader: codeReader, + StateReader: stateReader, + PrefetcherMetricer: metricer, + } +} + // GetCodeStats returns the statistics of code access. func (r *reader) GetCodeStats() ContractCodeReaderStats { if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok { diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index ff315ac5eb..72d9672b19 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -16,14 +16,6 @@ package state -import ( - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" -) - // The EIP27928 reader utilizes a hierarchical architecture to optimize state // access during block execution: // @@ -39,15 +31,13 @@ import ( // This layer provides a "unified view" by merging the pre-transition state // with mutated states from preceding transactions in the block. // -// - Tracking Layer: Finally, the readerTracker wraps the execution reader to -// capture all state reads made during a specific transaction. These individual -// reads are subsequently merged to construct a comprehensive access list -// for the entire block. -// // The architecture can be illustrated by the diagram below: -// + +// [ Block Level Access List ] <────────────────┐ +// ▲ │ (Merge) +// │ │ // ┌──────────────┴──────────────┐ ┌──────────────┴──────────────┐ -// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │ +// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │ (Unified View) // │ (Pre-state + Mutations) │ │ (Pre-state + Mutations) │ // └──────────────┬──────────────┘ └──────────────┬──────────────┘ // │ │ @@ -63,11 +53,16 @@ import ( // │ (State & Contract Code) │ // └─────────────────────────────┘ -// Note: The block producer, which is responsible for generating the block -// along with the block-level access list, does not maintain the internal -// hierarchy (e.g., PrefetchStateReader or ReaderWithBlockLevelAL). -// Instead, it directly utilizes the readerTracker, wrapped around the -// base reader, to construct the access list. +import ( + "sync" + "time" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" +) type fetchTask struct { addr common.Address @@ -78,16 +73,27 @@ func (t *fetchTask) weight() int { return 1 + len(t.slots) } type prefetchStateReader struct { StateReader - tasks []*fetchTask nThreads int done chan struct{} term chan struct{} closeOnce sync.Once + start time.Time + metrics PrefetchMetrics } -// nolint:unused -func newPrefetchStateReader(reader StateReader, accessList map[common.Address][]common.Hash, nThreads int) *prefetchStateReader { +type PrefetchMetrics struct { + // the total amount of time it took to complete the scheduled workload + Elapsed time.Duration +} + +// PrefetcherMetricer is an object that can expose metrics related to the state +// prefetching. +type PrefetcherMetricer interface { + Metrics() PrefetchMetrics +} + +func newPrefetchStateReader(reader StateReader, accessList bal.StorageKeys, nThreads int) *prefetchStateReader { tasks := make([]*fetchTask, 0, len(accessList)) for addr, slots := range accessList { tasks = append(tasks, &fetchTask{ @@ -105,11 +111,16 @@ func newPrefetchStateReaderInternal(reader StateReader, tasks []*fetchTask, nThr nThreads: nThreads, done: make(chan struct{}), term: make(chan struct{}), + start: time.Now(), } go r.prefetch() return r } +func (r *prefetchStateReader) Metrics() PrefetchMetrics { + return r.metrics +} + func (r *prefetchStateReader) Close() { r.closeOnce.Do(func() { close(r.term) @@ -127,7 +138,10 @@ func (r *prefetchStateReader) Wait() error { } func (r *prefetchStateReader) prefetch() { - defer close(r.done) + defer func() { + r.metrics = PrefetchMetrics{time.Since(r.start)} + close(r.done) + }() if len(r.tasks) == 0 { return @@ -196,52 +210,104 @@ func (r *prefetchStateReader) process(start, limit int) { // ReaderWithBlockLevelAccessList provides state access that reflects the // pre-transition state combined with the mutations made by transactions // prior to TxIndex. +// +// It is a cheap, per-transaction view over a shared, read-only +// bal.AccessListReader: constructing one is O(1) and every lookup is an +// allocation-free binary search. type ReaderWithBlockLevelAccessList struct { Reader - AccessList *bal.ConstructionBlockAccessList - TxIndex int + prepared *bal.AccessListReader + TxIndex int } -// NewReaderWithBlockLevelAccessList constructs a reader for accessing states -// with the mutations made by transactions prior to txIndex. -// -// The txIndex refers to the call frame as such: -// - 0 for pre‑execution system contract calls. -// - 1 … n for transactions (in block order). -// - n + 1 for post‑execution system contract calls. -func NewReaderWithBlockLevelAccessList(base Reader, accessList *bal.ConstructionBlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList { +// NewReaderWithAccessList wraps a base reader with a shared, already +// preprocessed access list. This is the cheap constructor used on the hot path: +// the prepared list is built once per block and borrowed by every per-tx reader. +func NewReaderWithAccessList(base Reader, prepared *bal.AccessListReader, txIndex int) *ReaderWithBlockLevelAccessList { return &ReaderWithBlockLevelAccessList{ - Reader: base, - AccessList: accessList, - TxIndex: txIndex, + Reader: base, + prepared: prepared, + TxIndex: txIndex, } } +// NewReaderWithBlockLevelAccessList wraps a base reader with a raw access list, +// preprocessing it on the spot. Prefer NewReaderWithAccessList when the +// prepared list can be built once and shared across multiple readers. +func NewReaderWithBlockLevelAccessList(base Reader, accessList bal.BlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList { + return NewReaderWithAccessList(base, bal.NewAccessListReader(accessList), txIndex) +} + // Account implements Reader, returning the account with the specific address. -func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (*types.StateAccount, error) { - panic("implement me") +func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (acct *types.StateAccount, err error) { + acct, err = r.Reader.Account(addr) + if err != nil { + return nil, err + } + + balance := r.prepared.Balance(addr, r.TxIndex) + code := r.prepared.Code(addr, r.TxIndex) + nonce, hasNonce := r.prepared.Nonce(addr, r.TxIndex) + if balance == nil && code == nil && !hasNonce { + return acct, nil + } + + if acct == nil { + acct = types.NewEmptyStateAccount() + } else { + // the account returned by the underlying reader is a reference + // copy it to avoid mutating the reader's instance + acct = acct.Copy() + } + + // balance and code alias the shared access list; this is safe because the + // EVM never mutates them in place (it replaces the pointer/slice wholesale, + // and the journal clones before stashing). + if balance != nil { + acct.Balance = balance + } + if code != nil { + codeHash := crypto.Keccak256Hash(code) + acct.CodeHash = codeHash[:] + } + if hasNonce { + acct.Nonce = nonce + } + return } // Storage implements Reader, returning the storage slot with the specific // address and slot key. func (r *ReaderWithBlockLevelAccessList) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { - panic("implement me") + if val, ok := r.prepared.StorageAt(addr, slot, r.TxIndex); ok { + return val, nil + } + return r.Reader.Storage(addr, slot) } // Has implements Reader, returning the flag indicating whether the contract // code with specified address and hash exists or not. func (r *ReaderWithBlockLevelAccessList) Has(addr common.Address, codeHash common.Hash) bool { - panic("implement me") + if code := r.prepared.Code(addr, r.TxIndex); code != nil { + return crypto.Keccak256Hash(code) == codeHash + } + return r.Reader.Has(addr, codeHash) } // Code implements Reader, returning the contract code with specified address // and hash. -func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) ([]byte, error) { - panic("implement me") +func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) []byte { + if code := r.prepared.Code(addr, r.TxIndex); code != nil && crypto.Keccak256Hash(code) == codeHash { + return code + } + return r.Reader.Code(addr, codeHash) } // CodeSize implements Reader, returning the contract code size with specified // address and hash. -func (r *ReaderWithBlockLevelAccessList) CodeSize(addr common.Address, codeHash common.Hash) (int, error) { - panic("implement me") +func (r *ReaderWithBlockLevelAccessList) CodeSize(addr common.Address, codeHash common.Hash) int { + if code := r.prepared.Code(addr, r.TxIndex); code != nil && crypto.Keccak256Hash(code) == codeHash { + return len(code) + } + return r.Reader.CodeSize(addr, codeHash) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 09a896a89a..ef67315507 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -21,6 +21,7 @@ import ( "bytes" "errors" "fmt" + "iter" "maps" "slices" "sort" @@ -182,6 +183,13 @@ func New(root common.Hash, db Database) (*StateDB, error) { return NewWithReader(root, db, reader) } +// 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 +} + // NewWithReader creates a new state for the specified state root. Unlike New, // this function accepts an additional Reader which is bound to the given root. func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, error) { @@ -1133,12 +1141,15 @@ func (s *StateDB) clearJournalAndRefund() { // deleteStorage is designed to delete the storage trie of a designated account. func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) { + return deleteStorage(s.db, s.originalRoot, addrHash, root) +} +func deleteStorage(db Database, originalRoot common.Hash, addrHash common.Hash, root common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) { var ( nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil) storages = make(map[common.Hash]common.Hash) // the set for storage mutations (value is nil) storageOrigins = make(map[common.Hash]common.Hash) // the set for tracking the original value of slot ) - iteratee, err := s.db.Iteratee(s.originalRoot) + iteratee, err := db.Iteratee(originalRoot) if err != nil { return nil, nil, nil, err } @@ -1567,3 +1578,72 @@ func (s *StateDB) Witness() *stateless.Witness { func (s *StateDB) AccessEvents() *AccessEvents { return s.accessEvents } + +// handleDestruction processes all destruction markers and deletes the account +// and associated storage slots if necessary. There are four potential scenarios +// as following: +// +// (a) the account was not existent and be marked as destructed +// (b) the account was not existent and be marked as destructed, +// however, it's resurrected later in the same block. +// (c) the account was existent and be marked as destructed +// (d) the account was existent and be marked as destructed, +// however it's resurrected later in the same block. +// +// In case (a), nothing needs be deleted, nil to nil transition can be ignored. +// In case (b), nothing needs be deleted, nil is used as the original value for +// newly created account and storages +// In case (c), **original** account along with its storages should be deleted, +// with their values be tracked as original value. +// In case (d), **original** account along with its storages should be deleted, +// with their values be tracked as original value. +func handleDestruction(db Database, trie Trie, root common.Hash, noStorageWiping bool, destructions iter.Seq[common.Address], prestates map[common.Address]*types.StateAccount) (map[common.Hash]*AccountDelete, []*trienode.NodeSet, error) { + var ( + nodes []*trienode.NodeSet + deletes = make(map[common.Hash]*AccountDelete) + ) + for addr := range destructions { + prestate := prestates[addr] + // The account was non-existent, and it's marked as destructed in the scope + // of block. It can be either case (a) or (b) and will be interpreted as + // null->null state transition. + // - for (a), skip it without doing anything + // - for (b), the resurrected account with nil as original will be handled afterwards + if prestate == nil { + continue + } + // The account was existent, it can be either case (c) or (d). + addrHash := crypto.Keccak256Hash(addr.Bytes()) + op := &AccountDelete{ + Address: addr, + Origin: prestate, + } + deletes[addrHash] = op + + // Short circuit if the origin storage was empty. + if prestate.Root == types.EmptyRootHash || db.TrieDB().IsUBT() { + continue + } + if noStorageWiping { + return nil, nil, fmt.Errorf("unexpected storage wiping, %x", addr) + } + // Remove storage slots belonging to the account. + storages, storagesOrigin, set, err := deleteStorage(db, prestate.Root, addrHash, root) + if err != nil { + return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err) + } + op.Storages = storages + op.StoragesOrigin = storagesOrigin + + // Aggregate the associated trie node changes. + nodes = append(nodes, set) + } + return deletes, nodes, nil +} + +// TODO: find better location for this +type Committer interface { + Commit(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, error) + CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *StateUpdate, error) + Preimages() map[common.Hash][]byte +} diff --git a/core/types.go b/core/types.go index f6f15101e0..98035262b1 100644 --- a/core/types.go +++ b/core/types.go @@ -34,7 +34,7 @@ type Validator interface { ValidateBody(block *types.Block) error // ValidateState validates the given statedb and optionally the process result. - ValidateState(block *types.Block, state *state.StateDB, res *ProcessResult, stateless bool) error + ValidateState(block *types.Block, state StateRootSource, res *ProcessResult, stateless bool) error } // Prefetcher is an interface for pre-caching transaction signatures and state. @@ -63,4 +63,6 @@ type ProcessResult struct { // BAL is only meaningful for post-Amsterdam blocks. Please ensure // fork validation is performed before accessing it. Bal *bal.ConstructionBlockAccessList + + Error error } diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index 2eb5fe93cd..9845916765 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -18,6 +18,7 @@ package bal import ( "bytes" + "encoding/json" "maps" "github.com/ethereum/go-ethereum/common" @@ -223,3 +224,137 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { } return res } + +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 []byte `json:"Code,omitempty"` + StorageWrites StorageMutations `json:"StorageWrites,omitempty"` +} + +// String returns a human-readable JSON representation of the account mutations. +func (a *AccountMutations) String() string { + var res bytes.Buffer + enc := json.NewEncoder(&res) + enc.SetIndent("", " ") + enc.Encode(a) + return res.String() +} + +// Copy returns a deep-copy of the instance. +func (a *AccountMutations) Copy() *AccountMutations { + res := &AccountMutations{ + nil, + nil, + nil, + nil, + } + if a.Nonce != nil { + res.Nonce = new(uint64) + *res.Nonce = *a.Nonce + } + if a.Code != nil { + res.Code = bytes.Clone(a.Code) + } + if a.Balance != nil { + res.Balance = new(uint256.Int).Set(a.Balance) + } + if a.StorageWrites != nil { + res.StorageWrites = maps.Clone(a.StorageWrites) + } + return res +} + +// 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 +} + +type BALExecutionMode int + +const ( + BALExecutionOptimized BALExecutionMode = iota + BALExecutionNoBatchIO + BALExecutionSequential +) + +// WrittenCounts groups per-block aggregate write counts derived from the BAL. +type WrittenCounts struct { + Accounts int + StorageSlots int + Codes int + CodeBytes int +} + +// WrittenCounts walks the BAL once and returns the aggregate write counts. +func (e BlockAccessList) WrittenCounts() WrittenCounts { + var w WrittenCounts + for i := range e { + a := &e[i] + if len(a.StorageChanges) > 0 || len(a.BalanceChanges) > 0 || + len(a.NonceChanges) > 0 || len(a.CodeChanges) > 0 { + w.Accounts++ + } + w.StorageSlots += len(a.StorageChanges) + if n := len(a.CodeChanges); n > 0 { + w.Codes++ + w.CodeBytes += len(a.CodeChanges[n-1].NewCode) + } + } + return w +} + +type StateMutations map[common.Address]AccountMutations + +type StorageKeySet map[common.Hash]struct{} +type StateAccesses map[common.Address]StorageKeySet + +func (s StateAccesses) Eq(other StateAccesses) bool { + if len(s) != len(other) { + return false + } + + for addr, set := range s { + otherSet, ok := other[addr] + if !ok { + return false + } + if !maps.Equal(set, otherSet) { + return false + } + } + return true +} diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index db891b92d0..4822652332 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -400,7 +400,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc obj.SlotChanges = make([]encodingStorageWrite, 0, len(slotWrites)) indices := slices.Collect(maps.Keys(slotWrites)) - slices.SortFunc(indices, cmp.Compare) + slices.Sort(indices) for _, index := range indices { val := slotWrites[index] obj.SlotChanges = append(obj.SlotChanges, encodingStorageWrite{ @@ -420,7 +420,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc // Convert balance changes balanceIndices := slices.Collect(maps.Keys(a.BalanceChanges)) - slices.SortFunc(balanceIndices, cmp.Compare) + slices.Sort(balanceIndices) for _, idx := range balanceIndices { res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{ BlockAccessIndex: idx, @@ -430,7 +430,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc // Convert nonce changes nonceIndices := slices.Collect(maps.Keys(a.NonceChanges)) - slices.SortFunc(nonceIndices, cmp.Compare) + slices.Sort(nonceIndices) for _, idx := range nonceIndices { res.NonceChanges = append(res.NonceChanges, encodingAccountNonce{ BlockAccessIndex: idx, @@ -440,7 +440,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc // Convert code change codeIndices := slices.Collect(maps.Keys(a.CodeChange)) - slices.SortFunc(codeIndices, cmp.Compare) + slices.Sort(codeIndices) for _, idx := range codeIndices { res.CodeChanges = append(res.CodeChanges, encodingCodeChange{ BlockAccessIndex: idx, diff --git a/core/types/bal/bal_reader.go b/core/types/bal/bal_reader.go new file mode 100644 index 0000000000..bb4a97c8d4 --- /dev/null +++ b/core/types/bal/bal_reader.go @@ -0,0 +1,196 @@ +package bal + +import ( + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" +) + +// AccessListReader enables efficient state diff lookups from a block access +// list during block execution. +type AccessListReader struct { + accounts map[common.Address]*preparedAccount +} + +type preparedAccount struct { + storage map[common.Hash]preparedSlot + AccountAccess +} + +type preparedSlot struct { + changes []encodingStorageWrite // borrowed, sorted asc by BlockAccessIndex +} + +// NewAccessListReader instantiates an access list reader. +func NewAccessListReader(list BlockAccessList) *AccessListReader { + accounts := make(map[common.Address]*preparedAccount, len(list)) + for i := range list { + a := list[i] // index; do not range-copy the AccountAccess + pa := &preparedAccount{ + AccountAccess: a, + } + if len(a.StorageChanges) > 0 { + pa.storage = make(map[common.Hash]preparedSlot, len(a.StorageChanges)) + for j := range a.StorageChanges { + sc := &a.StorageChanges[j] + pa.storage[sc.Slot.Bytes32()] = preparedSlot{changes: sc.SlotChanges} + } + } + accounts[a.Address] = pa + } + return &AccessListReader{accounts: accounts} +} + +// lastBefore returns the position of the last element in a slice of n elements +// sorted ascending by BlockAccessIndex whose key is strictly less than idx, or +// -1 if no such element exists. keyAt returns the BlockAccessIndex at position k. +func lastBefore(n int, idx uint32, keyAt func(k int) uint32) int { + // sort.Search returns the smallest position whose key is >= idx; everything + // before it is strictly less than idx, so the answer is that position - 1. + return sort.Search(n, func(k int) bool { return keyAt(k) >= idx }) - 1 +} + +// Balance returns the post-balance in effect immediately before the given block +// access index, or nil if the account's balance was not changed before idx. +// The returned pointer aliases the access list and must not be mutated. +func (p *AccessListReader) Balance(addr common.Address, idx int) *uint256.Int { + a := p.accounts[addr] + if a == nil { + return nil + } + k := lastBefore(len(a.BalanceChanges), uint32(idx), func(i int) uint32 { return a.BalanceChanges[i].BlockAccessIndex }) + if k < 0 { + return nil + } + return a.BalanceChanges[k].PostBalance +} + +// Nonce returns the post-nonce in effect immediately before the given block +// access index. The boolean is false if the nonce was not changed before idx. +func (p *AccessListReader) Nonce(addr common.Address, idx int) (uint64, bool) { + a := p.accounts[addr] + if a == nil { + return 0, false + } + k := lastBefore(len(a.NonceChanges), uint32(idx), func(i int) uint32 { return a.NonceChanges[i].BlockAccessIndex }) + if k < 0 { + return 0, false + } + return a.NonceChanges[k].PostNonce, true +} + +// Code returns the contract code in effect immediately before the given block +// access index, or nil if the code was not changed before idx. The returned +// slice aliases the access list and must not be mutated. +func (p *AccessListReader) Code(addr common.Address, idx int) []byte { + a := p.accounts[addr] + if a == nil { + return nil + } + k := lastBefore(len(a.CodeChanges), uint32(idx), func(i int) uint32 { return a.CodeChanges[i].BlockAccessIndex }) + if k < 0 { + return nil + } + return a.CodeChanges[k].NewCode +} + +// StorageAt returns the post-value of a storage slot immediately before the +// given block access index. The boolean is false if the slot was not written +// before idx. +func (p *AccessListReader) StorageAt(addr common.Address, slot common.Hash, idx int) (common.Hash, bool) { + a := p.accounts[addr] + if a == nil { + return common.Hash{}, false + } + s, ok := a.storage[slot] + if !ok { + return common.Hash{}, false + } + k := lastBefore(len(s.changes), uint32(idx), func(i int) uint32 { return s.changes[i].BlockAccessIndex }) + if k < 0 { + return common.Hash{}, false + } + return s.changes[k].PostValue.Bytes32(), true +} + +// AccountMutations returns the aggregate mutation for an account up until (and +// not including) the given block access list index, or nil if the account was +// not mutated before idx. +func (p *AccessListReader) AccountMutations(addr common.Address, idx int) *AccountMutations { + a := p.accounts[addr] + if a == nil { + return nil + } + res := &AccountMutations{} + if bal := p.Balance(addr, idx); bal != nil { + res.Balance = bal.Clone() + } + if code := p.Code(addr, idx); code != nil { + res.Code = code + } + if nonce, ok := p.Nonce(addr, idx); ok { + res.Nonce = new(uint64) + *res.Nonce = nonce + } + for slot, s := range a.storage { + k := lastBefore(len(s.changes), uint32(idx), func(i int) uint32 { return s.changes[i].BlockAccessIndex }) + if k < 0 { + continue + } + if res.StorageWrites == nil { + res.StorageWrites = make(map[common.Hash]common.Hash) + } + res.StorageWrites[slot] = s.changes[k].PostValue.Bytes32() + } + if res.Code == nil && res.Nonce == nil && len(res.StorageWrites) == 0 && res.Balance == nil { + return nil + } + return res +} + +type StorageKeys map[common.Address][]common.Hash + +// StorageKeys returns the set of accounts and storage keys mutated in the access +// list. If reads is set, the un-mutated accounts/keys are included in the result. +func (p *AccessListReader) StorageKeys(reads bool) (keys StorageKeys) { + keys = make(StorageKeys) + for addr, a := range p.accounts { + for _, storageChange := range a.StorageChanges { + keys[addr] = append(keys[addr], storageChange.Slot.Bytes32()) + } + if !(reads && len(a.StorageReads) > 0) { + continue + } + for _, storageRead := range a.StorageReads { + keys[addr] = append(keys[addr], storageRead.Bytes32()) + } + } + return +} + +// Mutations returns the aggregate state mutations from bal indices [0, idx). +func (p *AccessListReader) Mutations(idx int) *StateMutations { + res := make(StateMutations) + for addr := range p.accounts { + if mut := p.AccountMutations(addr, idx); mut != nil { + res[addr] = *mut + } + } + return &res +} + +// AllDestructions returns all accounts that experienced a destruction, regardless +// of whether they were later resurrected and exist after the block. It excludes +// ephemeral contracts from the result. +func (p *AccessListReader) AllDestructions() (res []common.Address) { + for addr, a := range p.accounts { + for _, nonce := range a.NonceChanges { + if nonce.PostNonce == 0 { + res = append(res, addr) + break + } + } + } + return res +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 15609a0205..0c596489aa 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -261,6 +261,12 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } syscall := isSystemCall(caller) + // EIP-7928: per the Amsterdam spec, delegation resolution happens before + // the value-transfer check, so the delegated-to must appear in the BAL + // even when the call later reverts with ErrInsufficientBalance. Touch the + // target's code here (a no-op for non-delegated accounts) to record it. + evm.resolveCode(addr) + // Fail if we're trying to transfer more than the available balance. if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance @@ -346,6 +352,12 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } + + // EIP-7928: per the Amsterdam spec, delegation resolution happens before + // the value-transfer check, so the delegated-to must appear in the BAL + // even when the call later reverts with ErrInsufficientBalance. + evm.resolveCode(addr) + // Fail if we're trying to transfer more than the available balance if !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance diff --git a/eth/backend.go b/eth/backend.go index 366f911785..1206ce64f3 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -294,6 +294,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } options.Overrides = &overrides + options.BALExecutionMode = config.BALExecutionMode + options.BlockingPrefetch = config.BlockingPrefetch + options.PrefetchWorkers = int(config.PrefetchWorkers) + eth.blockchain, err = core.NewBlockChain(chainDb, config.Genesis, eth.engine, options) if err != nil { return nil, err diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 4b0c3f6ea3..54e5e45c7a 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -804,10 +804,12 @@ func (api *ConsensusAPI) NewPayloadV5(ctx context.Context, params engine.Executa return invalidStatus, paramsErr("nil beaconRoot post-cancun") case executionRequests == nil: return invalidStatus, paramsErr("nil executionRequests post-prague") - case params.SlotNumber == nil: - return invalidStatus, paramsErr("nil slotnumber post-amsterdam") case !api.checkFork(params.Timestamp, forks.Amsterdam): return invalidStatus, unsupportedForkErr("newPayloadV5 must only be called for amsterdam payloads") + case params.SlotNumber == nil: + return invalidStatus, paramsErr("nil slotnumber post-amsterdam") + case params.BlockAccessList == nil: + return invalidStatus, paramsErr("nil block access list post-amsterdam") } requests := convertRequests(executionRequests) if err := validateRequests(requests); err != nil { diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 378bb64a1e..a124092f5d 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -19,6 +19,7 @@ package ethconfig import ( "errors" + "github.com/ethereum/go-ethereum/core/types/bal" "time" "github.com/ethereum/go-ethereum/common" @@ -224,6 +225,10 @@ type Config struct { // RangeLimit restricts the maximum range (end - start) for range queries. RangeLimit uint64 `toml:",omitempty"` + + BALExecutionMode bal.BALExecutionMode + PrefetchWorkers uint + BlockingPrefetch bool } // CreateConsensusEngine creates a consensus engine for the given chain config. diff --git a/tests/block_test.go b/tests/block_test.go index 0f087967bb..d309d81670 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -82,8 +82,17 @@ func TestBlockchain(t *testing.T) { // TestExecutionSpecBlocktests runs the test fixtures from execution-spec-tests. func TestExecutionSpecBlocktests(t *testing.T) { - if !common.FileExist(executionSpecBlockchainTestDir) { - t.Skipf("directory %s does not exist", executionSpecBlockchainTestDir) + testExecutionSpecBlocktests(t, executionSpecBlockchainTestDir) +} + +// TestExecutionSpecBlocktestsBAL runs the BAL release test fixtures from execution-spec-tests. +func TestExecutionSpecBlocktestsBAL(t *testing.T) { + testExecutionSpecBlocktests(t, executionSpecBALBlockchainTestDir) +} + +func testExecutionSpecBlocktests(t *testing.T, testDir string) { + if !common.FileExist(testDir) { + t.Skipf("directory %s does not exist", testDir) } bt := new(testMatcher) @@ -97,7 +106,7 @@ func TestExecutionSpecBlocktests(t *testing.T) { bt.skipLoad(`dynamicAccountOverwriteEmpty_Paris`) bt.skipLoad(`create2collisionStorageParis`) - bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) { + bt.walk(t, testDir, func(t *testing.T, name string, test *BlockTest) { execBlockTest(t, bt, test) }) } @@ -118,7 +127,7 @@ func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) { } for _, snapshot := range snapshotConf { for _, dbscheme := range dbschemeConf { - if err := bt.checkFailure(t, test.Run(snapshot, dbscheme, true, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(snapshot, dbscheme, true, true, nil, nil)); err != nil { t.Errorf("test with config {snapshotter:%v, scheme:%v} failed: %v", snapshot, dbscheme, err) return } diff --git a/tests/block_test_util.go b/tests/block_test_util.go index bece8ae610..ab0c908470 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -113,27 +113,20 @@ type btHeaderMarshaling struct { SlotNumber *math.HexOrDecimal64 } -func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) { - config, ok := Forks[t.json.Network] - if !ok { - return UnsupportedForkError{t.json.Network} - } - +func (t *BlockTest) createTestBlockChain(config *params.ChainConfig, snapshotter bool, scheme string, witness, createAndVerifyBAL bool, tracer *tracing.Hooks) (*core.BlockChain, error) { // import pre accounts & construct test genesis block & state root - // Commit genesis state var ( - gspec = t.genesis(config) db = rawdb.NewMemoryDatabase() tconf = &triedb.Config{ Preimages: true, - IsUBT: gspec.Config.UBTTime != nil && *gspec.Config.UBTTime <= gspec.Timestamp, } ) - if scheme == rawdb.PathScheme || tconf.IsUBT { + if scheme == rawdb.PathScheme { tconf.PathDB = pathdb.Defaults } else { tconf.HashDB = hashdb.Defaults } + gspec := t.genesis(config) // if ttd is not specified, set an arbitrary huge value if gspec.Config.TerminalTotalDifficulty == nil { @@ -142,15 +135,15 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t triedb := triedb.NewDatabase(db, tconf) gblock, err := gspec.Commit(db, triedb, nil) if err != nil { - return err + return nil, err } triedb.Close() // close the db to prevent memory leak if gblock.Hash() != t.json.Genesis.Hash { - return fmt.Errorf("genesis block hash doesn't match test: computed=%x, test=%x", gblock.Hash().Bytes()[:6], t.json.Genesis.Hash[:6]) + return nil, fmt.Errorf("genesis block hash doesn't match test: computed=%x, test=%x", gblock.Hash().Bytes()[:6], t.json.Genesis.Hash[:6]) } if gblock.Root() != t.json.Genesis.StateRoot { - return fmt.Errorf("genesis block state root does not match test: computed=%x, test=%x", gblock.Root().Bytes()[:6], t.json.Genesis.StateRoot[:6]) + return nil, fmt.Errorf("genesis block state root does not match test: computed=%x, test=%x", gblock.Root().Bytes()[:6], t.json.Genesis.StateRoot[:6]) } // Wrap the original engine within the beacon-engine engine := beacon.New(ethash.NewFaker()) @@ -164,12 +157,28 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t Tracer: tracer, }, StatelessSelfValidation: witness, + NoPrefetch: true, + BlockingPrefetch: true, + PrefetchWorkers: 100, // note: this is totally unrelated to NoPrefetch, just for BAL execution } if snapshotter { options.SnapshotLimit = 1 options.SnapshotWait = true } chain, err := core.NewBlockChain(db, gspec, engine, options) + if err != nil { + return nil, err + } + return chain, nil +} + +func (t *BlockTest) Run(snapshotter bool, scheme string, witness, createAndVerifyBAL bool, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) { + config, ok := Forks[t.json.Network] + if !ok { + return UnsupportedForkError{t.json.Network} + } + + chain, err := t.createTestBlockChain(config, snapshotter, scheme, witness, createAndVerifyBAL, tracer) if err != nil { return err } @@ -203,7 +212,50 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t } } } - return t.validateImportedHeaders(chain, validBlocks) + err = t.validateImportedHeaders(chain, validBlocks) + if err != nil { + return err + } + + if createAndVerifyBAL { + newChain, _ := t.createTestBlockChain(config, snapshotter, scheme, witness, createAndVerifyBAL, tracer) + defer newChain.Stop() + + var blocksWithBAL types.Blocks + for i := uint64(1); i <= chain.CurrentBlock().Number.Uint64(); i++ { + block := chain.GetBlockByNumber(i) + if chain.Config().IsAmsterdam(block.Number(), block.Time()) && block.AccessList() == nil { + return fmt.Errorf("block %d missing BAL", block.NumberU64()) + } + blocksWithBAL = append(blocksWithBAL, block) + } + + amt, err := newChain.InsertChain(blocksWithBAL) + if err != nil { + return err + } + _ = amt + newDB, err := newChain.State() + if err != nil { + return err + } + if err = t.validatePostState(newDB); err != nil { + return fmt.Errorf("post state validation failed: %v", err) + } + // Cross-check the snapshot-to-hash against the trie hash + if snapshotter { + if newChain.Snapshots() != nil { + if err := chain.Snapshots().Verify(chain.CurrentBlock().Root); err != nil { + return err + } + } + } + err = t.validateImportedHeaders(newChain, validBlocks) + if err != nil { + return err + } + } + return nil } // Network returns the network/fork name for this test. diff --git a/tests/init_test.go b/tests/init_test.go index b933c9808c..20c9e8e1c9 100644 --- a/tests/init_test.go +++ b/tests/init_test.go @@ -41,9 +41,10 @@ var ( transactionTestDir = filepath.Join(baseDir, "TransactionTests") rlpTestDir = filepath.Join(baseDir, "RLPTests") difficultyTestDir = filepath.Join(baseDir, "BasicTests") - executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") - executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests") - executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests") + executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") + executionSpecBALBlockchainTestDir = filepath.Join(".", "spec-tests-bal", "fixtures", "blockchain_tests") + executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests") + executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests") benchmarksDir = filepath.Join(".", "evm-benchmarks", "benchmarks") ) From 504bb79b7bca64f3d5ffb727b679b328bd0a5f38 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 16 Jun 2026 21:05:50 -0400 Subject: [PATCH 02/11] remove some AI comments and BAL perf option flags --- beacon/engine/types.go | 8 +------- cmd/geth/config.go | 23 ----------------------- cmd/geth/main.go | 3 --- cmd/utils/flags.go | 17 ----------------- core/blockchain.go | 9 ++------- core/state/database.go | 2 +- core/state/database_history.go | 2 +- core/state/database_mpt.go | 10 +++------- core/state/database_ubt.go | 2 +- core/types/bal/bal_reader.go | 5 +---- eth/backend.go | 4 ---- eth/ethconfig/config.go | 5 ----- 12 files changed, 10 insertions(+), 80 deletions(-) diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 93aeb5527a..c06c4b44ab 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -317,11 +317,7 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H } // If Amsterdam is enabled, data.BlockAccessList is always non-nil, - // even for empty blocks with no state transitions. The wire format is - // the RLP-encoded access list; the header hash is keccak256(rlp). - // - // If Amsterdam is not enabled yet, blockAccessListHash is expected - // to be nil. + // even for empty blocks with no state transitions. var blockAccessListHash *common.Hash if data.BlockAccessList != nil { hash := crypto.Keccak256Hash(data.BlockAccessList) @@ -388,8 +384,6 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. ExcessBlobGas: block.ExcessBlobGas(), SlotNumber: block.SlotNumber(), } - // Per Engine API spec (Amsterdam): blockAccessList is the RLP-encoded - // access list, serialized as a hex string. Encode it to bytes here. if al := block.AccessList(); al != nil { var buf bytes.Buffer if err := rlp.Encode(&buf, al); err == nil { diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 437babfb8f..8ac9e7f6bd 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -20,7 +20,6 @@ import ( "bufio" "errors" "fmt" - "github.com/ethereum/go-ethereum/core/types/bal" "os" "reflect" "runtime" @@ -246,28 +245,6 @@ func makeFullNode(ctx *cli.Context) *node.Node { cfg.Eth.OverrideUBT = &v } - if ctx.IsSet(utils.BALExecutionModeFlag.Name) { - val := ctx.String(utils.BALExecutionModeFlag.Name) - switch val { - case utils.BalExecutionModeOptimized: - cfg.Eth.BALExecutionMode = bal.BALExecutionOptimized - case utils.BalExecutionModeNoBatchIO: - cfg.Eth.BALExecutionMode = bal.BALExecutionNoBatchIO - case utils.BalExecutionModeSequential: - cfg.Eth.BALExecutionMode = bal.BALExecutionSequential - default: - utils.Fatalf("invalid option for --bal.executionmode: %s. acceptable values are full|nobatchio|sequential", val) - } - } - cfg.Eth.BlockingPrefetch = ctx.Bool(utils.BlockingPrefetchFlag.Name) - - prefetchWorkers := ctx.Uint(utils.PrefetchWorkersFlag.Name) - if ctx.IsSet(utils.PrefetchWorkersFlag.Name) && prefetchWorkers == 0 { - prefetchWorkers = uint(runtime.NumCPU()) - log.Warn(fmt.Sprintf("invalid value for --bal.prefetchworkers. got 0. sanitizing to %d", prefetchWorkers)) - } - cfg.Eth.PrefetchWorkers = prefetchWorkers - // Start metrics export if enabled. utils.SetupMetrics(&cfg.Metrics) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 09943331f5..14e3a549cb 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -92,9 +92,6 @@ var ( utils.BinTrieGroupDepthFlag, utils.LightKDFFlag, utils.EthRequiredBlocksFlag, - utils.BALExecutionModeFlag, - utils.PrefetchWorkersFlag, - utils.BlockingPrefetchFlag, utils.CacheFlag, utils.CacheDatabaseFlag, utils.CacheTrieFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index bbe4b8a4ff..c733a5fcbb 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -28,7 +28,6 @@ import ( "net/http" "os" "path/filepath" - "runtime" godebug "runtime/debug" "strconv" "strings" @@ -244,22 +243,6 @@ var ( Usage: "Comma separated block number-to-hash mappings to require for peering (=)", Category: flags.EthCategory, } - BALExecutionModeFlag = &cli.StringFlag{ - Name: "bal.executionmode", - Usage: "EIP-7928 block-access-list execution mode (no-op placeholder)", - Category: flags.EthCategory, - } - PrefetchWorkersFlag = &cli.UintFlag{ - Name: "bal.prefetchworkers", - Usage: "The number of concurrent state loading tasks to perform when prefetching BAL state. Default to the number of cpus", - Value: uint(runtime.NumCPU()), - Category: flags.MiscCategory, - } - BlockingPrefetchFlag = &cli.BoolFlag{ - Name: "bal.blockingprefetch", - Usage: "only relevant when executing in parallel with a BAL: if true, the prefetcher will block tx/state-root calculation until all scheduled fetching tasks have completed.", - Category: flags.MiscCategory, - } BloomFilterSizeFlag = &cli.Uint64Flag{ Name: "bloomfilter.size", Usage: "Megabytes of memory allocated to bloom-filter for pruning", diff --git a/core/blockchain.go b/core/blockchain.go index ea59ff75bd..27776da938 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -226,10 +226,6 @@ type BlockChainConfig struct { // Execution configs StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose) EnableWitnessStats bool // Whether trie access statistics collection is enabled - - BALExecutionMode bal.BALExecutionMode - BlockingPrefetch bool - PrefetchWorkers int } // DefaultConfig returns the default config. @@ -2127,13 +2123,12 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * sdb := state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps) - useAsyncReads := bc.cfg.BALExecutionMode != bal.BALExecutionNoBatchIO al := block.AccessList() // Preprocess the access list once for the whole block; the resulting // structure is read-only and shared by the prefetch reader, the state // transition and every per-transaction execution reader. prepared := bal.NewAccessListReader(*al) - prefetchReader, err := sdb.ReaderWithPrefetch(parentRoot, prepared.StorageKeys(useAsyncReads), bc.cfg.PrefetchWorkers, bc.cfg.BlockingPrefetch) + prefetchReader, err := sdb.ReaderWithPrefetch(parentRoot, prepared.StorageKeys()) if err != nil { return nil, err } @@ -2245,7 +2240,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, blockHasAccessList = block.AccessList() != nil ) - if blockHasAccessList && bc.cfg.BALExecutionMode != bal.BALExecutionSequential { + if blockHasAccessList { return bc.processBlockWithAccessList(parentRoot, block, config.WriteHead) } defer interrupt.Store(true) // terminate the prefetch at the end diff --git a/core/state/database.go b/core/state/database.go index 273693a2db..277ff60d3a 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -56,7 +56,7 @@ type Database interface { // ReaderWithPrefetch returns a reader which asynchronously fetches block // access list state in the background. - ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) + ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash) (Reader, error) // Iteratee returns a state iteratee associated with the specified state root, // through which the account iterator and storage iterator can be created. diff --git a/core/state/database_history.go b/core/state/database_history.go index ddc6d79238..d7d051874b 100644 --- a/core/state/database_history.go +++ b/core/state/database_history.go @@ -223,7 +223,7 @@ type HistoricDB struct { codedb *CodeDB } -func (db *HistoricDB) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) { +func (db *HistoricDB) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash) (Reader, error) { panic("not implemented") } diff --git a/core/state/database_mpt.go b/core/state/database_mpt.go index 5e7d278232..228b7eb61b 100644 --- a/core/state/database_mpt.go +++ b/core/state/database_mpt.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" + "runtime" ) // MPTDatabase is an implementation of Database interface for Merkle Patricia Tries. @@ -186,7 +187,7 @@ func (db *MPTDatabase) Iteratee(root common.Hash) (Iteratee, error) { return newStateIteratee(true, root, db.triedb, db.snap) } -func (db *MPTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) { +func (db *MPTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash) (Reader, error) { base, err := db.StateReader(stateRoot) if err != nil { return nil, err @@ -195,12 +196,7 @@ func (db *MPTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[ r := newStateReaderWithStats(newStateReaderWithCache(base)) // Construct the state reader with background prefetching - pr := newPrefetchStateReader(r, accessList, threads) - if block { - if err := pr.Wait(); err != nil { - panic("this should unreachable") - } - } + pr := newPrefetchStateReader(r, accessList, runtime.NumCPU()) return newReaderWithPrefetch(db.codedb.Reader(), pr, pr), nil } diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go index 0affdea2ed..b088927dea 100644 --- a/core/state/database_ubt.go +++ b/core/state/database_ubt.go @@ -96,7 +96,7 @@ func (db *UBTDatabase) Reader(stateRoot common.Hash) (Reader, error) { return newReader(db.codedb.Reader(), sr), nil } -func (db *UBTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) { +func (db *UBTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash) (Reader, error) { panic("not implemented") } diff --git a/core/types/bal/bal_reader.go b/core/types/bal/bal_reader.go index bb4a97c8d4..a08df17797 100644 --- a/core/types/bal/bal_reader.go +++ b/core/types/bal/bal_reader.go @@ -153,15 +153,12 @@ type StorageKeys map[common.Address][]common.Hash // StorageKeys returns the set of accounts and storage keys mutated in the access // list. If reads is set, the un-mutated accounts/keys are included in the result. -func (p *AccessListReader) StorageKeys(reads bool) (keys StorageKeys) { +func (p *AccessListReader) StorageKeys() (keys StorageKeys) { keys = make(StorageKeys) for addr, a := range p.accounts { for _, storageChange := range a.StorageChanges { keys[addr] = append(keys[addr], storageChange.Slot.Bytes32()) } - if !(reads && len(a.StorageReads) > 0) { - continue - } for _, storageRead := range a.StorageReads { keys[addr] = append(keys[addr], storageRead.Bytes32()) } diff --git a/eth/backend.go b/eth/backend.go index 1206ce64f3..366f911785 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -294,10 +294,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } options.Overrides = &overrides - options.BALExecutionMode = config.BALExecutionMode - options.BlockingPrefetch = config.BlockingPrefetch - options.PrefetchWorkers = int(config.PrefetchWorkers) - eth.blockchain, err = core.NewBlockChain(chainDb, config.Genesis, eth.engine, options) if err != nil { return nil, err diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index a124092f5d..378bb64a1e 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -19,7 +19,6 @@ package ethconfig import ( "errors" - "github.com/ethereum/go-ethereum/core/types/bal" "time" "github.com/ethereum/go-ethereum/common" @@ -225,10 +224,6 @@ type Config struct { // RangeLimit restricts the maximum range (end - start) for range queries. RangeLimit uint64 `toml:",omitempty"` - - BALExecutionMode bal.BALExecutionMode - PrefetchWorkers uint - BlockingPrefetch bool } // CreateConsensusEngine creates a consensus engine for the given chain config. From 98f46239426fa85152c412286ac940a864aef681 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 16 Jun 2026 21:14:35 -0400 Subject: [PATCH 03/11] remove old flags and add some comment docs --- cmd/utils/flags.go | 6 ------ core/state/bal_state_transition.go | 9 ++++++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c733a5fcbb..fd9893fe81 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1131,12 +1131,6 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. } ) -const ( - BalExecutionModeOptimized = "full" - BalExecutionModeNoBatchIO = "nobatchio" - BalExecutionModeSequential = "sequential" -) - var ( // TestnetFlags is the flag group of all built-in supported testnets. TestnetFlags = []cli.Flag{ diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 5cf761d647..d2767c0e2b 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -15,9 +15,8 @@ import ( "golang.org/x/sync/errgroup" ) -// BALStateTransition is responsible for performing the state root update -// and commit for EIP 7928 access-list-containing blocks. An instance of -// this object is only used for a single block. +// BALStateTransition computes the state root update and commits a single block +// which contains an access list. type BALStateTransition struct { accessList *bal.AccessListReader written bal.WrittenCounts @@ -54,6 +53,7 @@ type BALStateTransition struct { err error } +// Metrics returns metrics if Commit has previously been called. func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { return &s.metrics } @@ -71,6 +71,7 @@ func (s *BALStateTransition) Deletions() DeletionCounts { } } +// BALStateTransitionMetrics contains metrics from a committed block. type BALStateTransitionMetrics struct { // trie hashing metrics AccountUpdate time.Duration @@ -86,6 +87,7 @@ type BALStateTransitionMetrics struct { TotalCommitTime time.Duration } +// NewBALStateTransition creates a BALStateTransition instance func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Database, parentRoot common.Hash, prepared *bal.AccessListReader) (*BALStateTransition, error) { stateTrie, err := db.OpenTrie(parentRoot) if err != nil { @@ -122,6 +124,7 @@ func (s *BALStateTransition) PreparedAccessList() *bal.AccessListReader { return s.accessList } +// Error returns an error if IntermediateRoot or Commit failed. func (s *BALStateTransition) Error() error { return s.err } From 0fc6d6281c0097ed28200476d441d3172eeae5fe Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 16 Jun 2026 21:19:26 -0400 Subject: [PATCH 04/11] remove cruft --- tests/block_test_util.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/block_test_util.go b/tests/block_test_util.go index ab0c908470..a302504821 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -158,8 +158,6 @@ func (t *BlockTest) createTestBlockChain(config *params.ChainConfig, snapshotter }, StatelessSelfValidation: witness, NoPrefetch: true, - BlockingPrefetch: true, - PrefetchWorkers: 100, // note: this is totally unrelated to NoPrefetch, just for BAL execution } if snapshotter { options.SnapshotLimit = 1 From 861890877af07c8029a682bf702a42b52398db46 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 16 Jun 2026 22:10:12 -0400 Subject: [PATCH 05/11] remove bal devnet 7 fixtures download --- build/checksums.txt | 5 ----- build/ci.go | 18 ------------------ core/state/bal_state_transition.go | 1 - 3 files changed, 24 deletions(-) diff --git a/build/checksums.txt b/build/checksums.txt index d6a9f2b803..454efa93c4 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -5,11 +5,6 @@ # https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz -# version:spec-tests-bal v7.2.0 -# https://github.com/ethereum/execution-specs/releases -# https://github.com/ethereum/execution-specs/releases/download/tests-bal%40v7.2.0 -fc1d9ae174cdd5db789068839999e6f83666cc79f7dac36e973d7616d9a2e2cf fixtures_bal.tar.gz - # version:golang 1.25.10 # https://go.dev/dl/ 20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz diff --git a/build/ci.go b/build/ci.go index 2db6c22616..53ade2e1bf 100644 --- a/build/ci.go +++ b/build/ci.go @@ -160,9 +160,6 @@ 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")) @@ -401,7 +398,6 @@ func doTest(cmdline []string) { // Get test fixtures. if !*short { downloadSpecTestFixtures(csdb, *cachedir) - downloadBALSpecTestFixtures(csdb, *cachedir) } // Configure the toolchain. @@ -467,20 +463,6 @@ func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string return filepath.Join(cachedir, base) } -// downloadBALSpecTestFixtures downloads and extracts the bal-specific execution-spec-tests fixtures. -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/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index d2767c0e2b..29135494e5 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -139,7 +139,6 @@ func (s *BALStateTransition) setError(err error) { // deletions can only occur if an account which has a balance becomes the target of a CREATE2 initcode // which calls SENDALL, clearing the account and marking it for deletion. func isAccountDeleted(prestate *types.StateAccount, mutations bal.AccountMutations) bool { - // TODO: figure out how to simplify this method if mutations.Code != nil && len(mutations.Code) != 0 { return false } From d9f607e8676b2edc2e68f75c3051cbfb2413c0aa Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 17 Jun 2026 14:50:45 -0400 Subject: [PATCH 06/11] ai implementation for preimages --- core/parallel_state_processor.go | 40 +++++++++++- core/state/bal_state_transition.go | 29 ++++++++- core/state/bal_state_transition_test.go | 83 +++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 core/state/bal_state_transition_test.go diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 299d7efd7b..bdc95d19ac 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -8,6 +8,7 @@ import ( "slices" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types/bal" @@ -22,6 +23,12 @@ type ProcessResultWithMetrics struct { StateTransitionMetrics *state.BALStateTransitionMetrics ExecTime time.Duration PostProcessTime time.Duration + + // Preimages holds the SHA3 preimages seen by the VM across the whole block + // (the pre/post-transaction system executions and every transaction). They + // are gathered here so the caller can install them into the state + // transition without the execution helpers needing a reference to it. + Preimages map[common.Hash][]byte } // errResult wraps an error into a new ProcessResultWithMetrics instance @@ -112,6 +119,24 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar p.chain.Engine().Finalize(p.chain, block.Header(), evm.StateDB, block.Body(), uint32(lastBALIdx), postBAL) + // Gather the SHA3 preimages seen across the block. postTxState is a copy of + // the block statedb taken after pre-transaction processing, so its set + // already covers both the pre-tx system execution and the post-tx execution + // (PostExecution + Finalize) performed above; the per-transaction sets are + // carried on each result. The merged set is returned to the caller, which + // owns the state transition and installs them there. + preimages := make(map[common.Hash][]byte) + for hash, preimage := range postTxState.Preimages() { + preimages[hash] = preimage + } + for i := range results { + for hash, preimage := range results[i].preimages { + if _, ok := preimages[hash]; !ok { + preimages[hash] = preimage + } + } + } + blockAccessList := bal.NewConstructionBlockAccessList() blockAccessList.Merge(preTxBAL) blockAccessList.Merge(postBAL) @@ -137,6 +162,7 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar }, PostProcessTime: tPostprocess, ExecTime: tExec, + Preimages: preimages, } } @@ -150,6 +176,10 @@ type txExecResult struct { txState uint64 blockAccessList *bal.ConstructionBlockAccessList + + // preimages holds the SHA3 preimages recorded while executing this + // transaction against its own statedb copy. + preimages map[common.Hash][]byte } // resultHandler polls until all transactions have finished executing and the @@ -303,7 +333,11 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st prestate := startingState.Copy() workers.Go(func() error { prestate = prestate.WithReader(state.NewReaderWithAccessList(statedb.Reader(), prepared, balIdx)) - txResCh <- *p.execTx(block, tx, balIdx, prestate, signer) + res := p.execTx(block, tx, balIdx, prestate, signer) + // Carry this transaction's preimages on the result; they're merged + // into the block-level set in prepareExecResult. + res.preimages = prestate.Preimages() + txResCh <- *res return nil }) } @@ -314,6 +348,10 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st if res.ProcessResult.Error != nil { return nil, res.ProcessResult.Error } + // Install the block's preimages into the state transition so they're + // persisted when the block is committed. Process is the sole owner of the + // state transition, keeping the execution helpers free of it. + stateTransition.AddPreimages(res.Preimages) // TODO: remove preprocess metric ? res.PreProcessTime = tPreprocess return res, nil diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 29135494e5..07b62258c9 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -47,6 +47,12 @@ type BALStateTransition struct { stateUpdate *StateUpdate + // preimages holds the SHA3 preimages seen by the VM across the whole block: + // the pre/post-transaction system executions and every individual + // transaction execution. It is populated once via AddPreimages after the + // parallel execution has been collected. + preimages map[common.Hash][]byte + metrics BALStateTransitionMetrics maxBALIdx int @@ -108,6 +114,7 @@ func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Databas tries: sync.Map{}, deletions: make(map[common.Address]struct{}), stateUpdate: nil, + preimages: make(map[common.Hash][]byte), maxBALIdx: len(block.Transactions()) + 1, }, nil } @@ -533,7 +540,23 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { return s.rootHash } -func (s *BALStateTransition) Preimages() map[common.Hash][]byte { - // TODO: implement this - return make(map[common.Hash][]byte) +// AddPreimages merges a set of SHA3 preimages into the block-level preimage +// set, leaving any hash already present untouched (mirroring the +// first-write-wins behaviour of StateDB.AddPreimage). The parallel processor +// collects the preimages from the per-transaction and pre/post-transaction +// executions and installs them here once execution has completed. +func (s *BALStateTransition) AddPreimages(preimages map[common.Hash][]byte) { + for hash, preimage := range preimages { + if _, ok := s.preimages[hash]; !ok { + s.preimages[hash] = preimage + } + } +} + +// Preimages returns the SHA3 preimages seen by the VM over the course of the +// block. Unlike the sequential processor, which reads them from a single +// statedb, the BAL processor executes the block across several statedbs and +// installs the combined set via AddPreimages. +func (s *BALStateTransition) Preimages() map[common.Hash][]byte { + return s.preimages } diff --git a/core/state/bal_state_transition_test.go b/core/state/bal_state_transition_test.go new file mode 100644 index 0000000000..5ec6ad6e56 --- /dev/null +++ b/core/state/bal_state_transition_test.go @@ -0,0 +1,83 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// newPreimageTransition returns a BALStateTransition with only the preimage +// accumulator initialised, which is all that AddPreimages/Preimages touch. +func newPreimageTransition() *BALStateTransition { + return &BALStateTransition{preimages: make(map[common.Hash][]byte)} +} + +// TestBALStateTransitionPreimagesUnion verifies that AddPreimages folds the +// preimages contributed by the various block executions (the per-transaction +// copies plus the pre/post-tx execution) into a single set returned by +// Preimages. +func TestBALStateTransitionPreimagesUnion(t *testing.T) { + s := newPreimageTransition() + + // Empty/nil inputs are no-ops. + s.AddPreimages(nil) + s.AddPreimages(map[common.Hash][]byte{}) + if got := len(s.Preimages()); got != 0 { + t.Fatalf("expected empty set, got %d entries", got) + } + + s.AddPreimages(map[common.Hash][]byte{ + {0x01}: []byte("pre"), + {0x02}: []byte("tx1"), + {0x03}: []byte("tx2"), + {0x04}: []byte("post"), + }) + + want := map[common.Hash][]byte{ + {0x01}: []byte("pre"), + {0x02}: []byte("tx1"), + {0x03}: []byte("tx2"), + {0x04}: []byte("post"), + } + got := s.Preimages() + if len(got) != len(want) { + t.Fatalf("preimage count: got %d, want %d", len(got), len(want)) + } + for h, v := range want { + if !bytes.Equal(got[h], v) { + t.Errorf("preimage %x: got %q, want %q", h, got[h], v) + } + } +} + +// TestBALStateTransitionPreimagesFirstWriteWins verifies that a preimage for a +// hash already present is not overwritten across successive AddPreimages calls, +// matching StateDB.AddPreimage. +func TestBALStateTransitionPreimagesFirstWriteWins(t *testing.T) { + s := newPreimageTransition() + key := common.Hash{0xaa} + + s.AddPreimages(map[common.Hash][]byte{key: []byte("first")}) + s.AddPreimages(map[common.Hash][]byte{key: []byte("second")}) + + if got := s.Preimages()[key]; !bytes.Equal(got, []byte("first")) { + t.Fatalf("expected first write to win, got %q", got) + } +} From 7fa29797518c698c3bfb58d4b7a23736da8356d7 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 17 Jun 2026 14:51:24 -0400 Subject: [PATCH 07/11] add test case which verifies preimages are accrued during bal execution --- core/bal_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/core/bal_test.go b/core/bal_test.go index 64c2714b16..0371447470 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto" @@ -1453,3 +1454,62 @@ func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) { t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) } } + +// ============================== Preimages ============================== + +// TestBALPreimagesEndToEnd verifies that BALStateTransition.Preimages collects +// the SHA3 preimages observed during transaction execution and that they are +// persisted when a post-Amsterdam block is processed through the parallel BAL +// processor. Because that processor executes each transaction against its own +// statedb copy (separate from the pre/post-tx system executions), the per-tx +// preimages must be folded back into the shared state transition; here a +// transaction runs KECCAK256 over a distinctive byte and we confirm the +// preimage is written to disk on commit. +func TestBALPreimagesEndToEnd(t *testing.T) { + // Runtime code: store 0x42 at memory[0], then KECCAK256 over memory[0:1]. + // PUSH1 0x42 ; PUSH1 0x00 ; MSTORE8 ; PUSH1 0x01 ; PUSH1 0x00 ; KECCAK256 ; POP ; STOP + contract := common.HexToAddress("0xca11ee") + runtime := []byte{0x60, 0x42, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0x20, 0x50, 0x00} + + env := newBALTestEnv(types.GenesisAlloc{ + contract: {Code: runtime, Balance: common.Big0}, + }) + + engine := beacon.New(ethash.NewFaker()) + _, blocks, _ := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, g *BlockGen) { + // Run the EIP-4788 beacon-root system call during generation so the + // generated BAL matches what the processor recomputes on import. + g.SetParentBeaconRoot(common.Hash{}) + g.AddTx(env.tx(0, &contract, big.NewInt(0), 200_000, 0, nil)) + }) + block := blocks[0] + if block.AccessList() == nil { + t.Fatal("expected block to carry an access list") + } + if !env.cfg.IsAmsterdam(block.Number(), block.Time()) { + t.Fatalf("block must be post-Amsterdam: number=%v time=%d", block.Number(), block.Time()) + } + + // Import the block through the parallel BAL processor with preimage + // recording enabled, so the KECCAK256 above is captured. + db := rawdb.NewMemoryDatabase() + cfg := DefaultConfig() + cfg.VmConfig.EnablePreimageRecording = true + chain, err := NewBlockChain(db, env.gspec, engine, cfg) + if err != nil { + t.Fatalf("new blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks); err != nil { + t.Fatalf("insert chain: %v", err) + } + + // The transaction hashed the single byte 0x42; the preimage must have been + // accumulated by the state transition and written to disk on commit. + want := []byte{0x42} + hash := crypto.Keccak256Hash(want) + if got := rawdb.ReadPreimage(db, hash); !bytes.Equal(got, want) { + t.Fatalf("preimage for %x: got %x, want %x", hash, got, want) + } +} From 94bcaa8696744709a06def091a96e4bbb65d17d8 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 17 Jun 2026 15:36:55 -0400 Subject: [PATCH 08/11] cleanup last commit --- core/bal_test.go | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/core/bal_test.go b/core/bal_test.go index 0371447470..ec9e6ccff9 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -1457,15 +1457,9 @@ func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) { // ============================== Preimages ============================== -// TestBALPreimagesEndToEnd verifies that BALStateTransition.Preimages collects -// the SHA3 preimages observed during transaction execution and that they are -// persisted when a post-Amsterdam block is processed through the parallel BAL -// processor. Because that processor executes each transaction against its own -// statedb copy (separate from the pre/post-tx system executions), the per-tx -// preimages must be folded back into the shared state transition; here a -// transaction runs KECCAK256 over a distinctive byte and we confirm the -// preimage is written to disk on commit. -func TestBALPreimagesEndToEnd(t *testing.T) { +// TestBALPreimages tests that preimage tracking works when executing a block +// with an access list. +func TestBALPreimages(t *testing.T) { // Runtime code: store 0x42 at memory[0], then KECCAK256 over memory[0:1]. // PUSH1 0x42 ; PUSH1 0x00 ; MSTORE8 ; PUSH1 0x01 ; PUSH1 0x00 ; KECCAK256 ; POP ; STOP contract := common.HexToAddress("0xca11ee") @@ -1482,14 +1476,6 @@ func TestBALPreimagesEndToEnd(t *testing.T) { g.SetParentBeaconRoot(common.Hash{}) g.AddTx(env.tx(0, &contract, big.NewInt(0), 200_000, 0, nil)) }) - block := blocks[0] - if block.AccessList() == nil { - t.Fatal("expected block to carry an access list") - } - if !env.cfg.IsAmsterdam(block.Number(), block.Time()) { - t.Fatalf("block must be post-Amsterdam: number=%v time=%d", block.Number(), block.Time()) - } - // Import the block through the parallel BAL processor with preimage // recording enabled, so the KECCAK256 above is captured. db := rawdb.NewMemoryDatabase() From b348dd5ae4f191d81cc07c4769d38ebc996fd8a5 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 17 Jun 2026 15:56:07 -0400 Subject: [PATCH 09/11] more cleanup --- core/parallel_state_processor.go | 27 +++----- core/state/bal_state_transition.go | 21 ++----- core/state/bal_state_transition_test.go | 83 ------------------------- 3 files changed, 13 insertions(+), 118 deletions(-) delete mode 100644 core/state/bal_state_transition_test.go diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index bdc95d19ac..0f021bdff0 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -24,10 +24,8 @@ type ProcessResultWithMetrics struct { ExecTime time.Duration PostProcessTime time.Duration - // Preimages holds the SHA3 preimages seen by the VM across the whole block - // (the pre/post-transaction system executions and every transaction). They - // are gathered here so the caller can install them into the state - // transition without the execution helpers needing a reference to it. + // Preimages holds the SHA3 preimages recorded during the execution of + // the block. Preimages map[common.Hash][]byte } @@ -119,12 +117,8 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar p.chain.Engine().Finalize(p.chain, block.Header(), evm.StateDB, block.Body(), uint32(lastBALIdx), postBAL) - // Gather the SHA3 preimages seen across the block. postTxState is a copy of - // the block statedb taken after pre-transaction processing, so its set - // already covers both the pre-tx system execution and the post-tx execution - // (PostExecution + Finalize) performed above; the per-transaction sets are - // carried on each result. The merged set is returned to the caller, which - // owns the state transition and installs them there. + // Gather preimages from block execution. postTxState contains + // preimages from pre/post tx system contract execution. preimages := make(map[common.Hash][]byte) for hash, preimage := range postTxState.Preimages() { preimages[hash] = preimage @@ -177,8 +171,7 @@ type txExecResult struct { blockAccessList *bal.ConstructionBlockAccessList - // preimages holds the SHA3 preimages recorded while executing this - // transaction against its own statedb copy. + // holds preimages recorded during execution of the tx preimages map[common.Hash][]byte } @@ -288,6 +281,7 @@ func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transactio execGas: receipt.GasUsed, txRegular: gp.cumulativeRegular, txState: gp.cumulativeState, + preimages: db.Preimages(), blockAccessList: txBAL, } } @@ -334,9 +328,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st workers.Go(func() error { prestate = prestate.WithReader(state.NewReaderWithAccessList(statedb.Reader(), prepared, balIdx)) res := p.execTx(block, tx, balIdx, prestate, signer) - // Carry this transaction's preimages on the result; they're merged - // into the block-level set in prepareExecResult. - res.preimages = prestate.Preimages() txResCh <- *res return nil }) @@ -348,10 +339,8 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st if res.ProcessResult.Error != nil { return nil, res.ProcessResult.Error } - // Install the block's preimages into the state transition so they're - // persisted when the block is committed. Process is the sole owner of the - // state transition, keeping the execution helpers free of it. - stateTransition.AddPreimages(res.Preimages) + + stateTransition.SetPreimages(res.Preimages) // TODO: remove preprocess metric ? res.PreProcessTime = tPreprocess return res, nil diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 07b62258c9..0c775bdbee 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -49,8 +49,7 @@ type BALStateTransition struct { // preimages holds the SHA3 preimages seen by the VM across the whole block: // the pre/post-transaction system executions and every individual - // transaction execution. It is populated once via AddPreimages after the - // parallel execution has been collected. + // transaction execution. It is populated once via SetPreimages. preimages map[common.Hash][]byte metrics BALStateTransitionMetrics @@ -540,23 +539,13 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { return s.rootHash } -// AddPreimages merges a set of SHA3 preimages into the block-level preimage -// set, leaving any hash already present untouched (mirroring the -// first-write-wins behaviour of StateDB.AddPreimage). The parallel processor -// collects the preimages from the per-transaction and pre/post-transaction -// executions and installs them here once execution has completed. -func (s *BALStateTransition) AddPreimages(preimages map[common.Hash][]byte) { - for hash, preimage := range preimages { - if _, ok := s.preimages[hash]; !ok { - s.preimages[hash] = preimage - } - } +// SetPreimages sets the preimages. +func (s *BALStateTransition) SetPreimages(preimages map[common.Hash][]byte) { + s.preimages = preimages } // Preimages returns the SHA3 preimages seen by the VM over the course of the -// block. Unlike the sequential processor, which reads them from a single -// statedb, the BAL processor executes the block across several statedbs and -// installs the combined set via AddPreimages. +// block. func (s *BALStateTransition) Preimages() map[common.Hash][]byte { return s.preimages } diff --git a/core/state/bal_state_transition_test.go b/core/state/bal_state_transition_test.go deleted file mode 100644 index 5ec6ad6e56..0000000000 --- a/core/state/bal_state_transition_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package state - -import ( - "bytes" - "testing" - - "github.com/ethereum/go-ethereum/common" -) - -// newPreimageTransition returns a BALStateTransition with only the preimage -// accumulator initialised, which is all that AddPreimages/Preimages touch. -func newPreimageTransition() *BALStateTransition { - return &BALStateTransition{preimages: make(map[common.Hash][]byte)} -} - -// TestBALStateTransitionPreimagesUnion verifies that AddPreimages folds the -// preimages contributed by the various block executions (the per-transaction -// copies plus the pre/post-tx execution) into a single set returned by -// Preimages. -func TestBALStateTransitionPreimagesUnion(t *testing.T) { - s := newPreimageTransition() - - // Empty/nil inputs are no-ops. - s.AddPreimages(nil) - s.AddPreimages(map[common.Hash][]byte{}) - if got := len(s.Preimages()); got != 0 { - t.Fatalf("expected empty set, got %d entries", got) - } - - s.AddPreimages(map[common.Hash][]byte{ - {0x01}: []byte("pre"), - {0x02}: []byte("tx1"), - {0x03}: []byte("tx2"), - {0x04}: []byte("post"), - }) - - want := map[common.Hash][]byte{ - {0x01}: []byte("pre"), - {0x02}: []byte("tx1"), - {0x03}: []byte("tx2"), - {0x04}: []byte("post"), - } - got := s.Preimages() - if len(got) != len(want) { - t.Fatalf("preimage count: got %d, want %d", len(got), len(want)) - } - for h, v := range want { - if !bytes.Equal(got[h], v) { - t.Errorf("preimage %x: got %q, want %q", h, got[h], v) - } - } -} - -// TestBALStateTransitionPreimagesFirstWriteWins verifies that a preimage for a -// hash already present is not overwritten across successive AddPreimages calls, -// matching StateDB.AddPreimage. -func TestBALStateTransitionPreimagesFirstWriteWins(t *testing.T) { - s := newPreimageTransition() - key := common.Hash{0xaa} - - s.AddPreimages(map[common.Hash][]byte{key: []byte("first")}) - s.AddPreimages(map[common.Hash][]byte{key: []byte("second")}) - - if got := s.Preimages()[key]; !bytes.Equal(got, []byte("first")) { - t.Fatalf("expected first write to win, got %q", got) - } -} From 6eee5253b8493b9de08b2b5843ea9142244fa4b2 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 17 Jun 2026 16:06:13 -0400 Subject: [PATCH 10/11] removed outdated TODO --- core/state/bal_state_transition.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 0c775bdbee..996a7e0645 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -379,16 +379,6 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b if err := s.db.Commit(update); err != nil { return common.Hash{}, nil, err } - // TODO: fix the following metrics: - /* - snapshotCommits, trieDBCommits, err := flushStateUpdate(s.db, block, ret) - if err != nil { - return common.Hash{}, nil, err - } - - s.metrics.SnapshotCommits, s.metrics.TrieDBCommits = snapshotCommits, trieDBCommits - s.metrics.TotalCommitTime = time.Since(commitStart) - */ return root, update, nil } From c62c486918e3d13b87961fd1278864579c328d02 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Thu, 18 Jun 2026 14:26:17 -0400 Subject: [PATCH 11/11] more cleanups --- core/block_validator.go | 2 ++ core/blockchain.go | 15 +++++---------- core/state/bal_state_transition.go | 12 +----------- core/state/reader_eip_7928.go | 10 +++++++--- tests/init_test.go | 7 +++---- 5 files changed, 18 insertions(+), 28 deletions(-) diff --git a/core/block_validator.go b/core/block_validator.go index f2df77a27d..0b45cb020f 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -19,6 +19,7 @@ package core import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -143,6 +144,7 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { return nil } +// StateRootSource computes a new state root during block execution. type StateRootSource interface { IntermediateRoot(deleteEmptyObjects bool) common.Hash Error() error diff --git a/core/blockchain.go b/core/blockchain.go index 27776da938..c55a38c233 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -21,7 +21,6 @@ import ( "context" "errors" "fmt" - "github.com/ethereum/go-ethereum/core/types/bal" "io" "math/big" "runtime" @@ -32,6 +31,8 @@ import ( "sync/atomic" "time" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/common/mclock" @@ -2124,16 +2125,13 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * sdb := state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps) al := block.AccessList() - // Preprocess the access list once for the whole block; the resulting - // structure is read-only and shared by the prefetch reader, the state - // transition and every per-transaction execution reader. - prepared := bal.NewAccessListReader(*al) - prefetchReader, err := sdb.ReaderWithPrefetch(parentRoot, prepared.StorageKeys()) + reader := bal.NewAccessListReader(*al) + prefetchReader, err := sdb.ReaderWithPrefetch(parentRoot, reader.StorageKeys()) if err != nil { return nil, err } - stateTransition, err := state.NewBALStateTransition(block, prefetchReader, sdb, parentRoot, prepared) + stateTransition, err := state.NewBALStateTransition(block, prefetchReader, sdb, parentRoot, reader) if err != nil { return nil, err } @@ -2202,9 +2200,6 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * if m := res.StateTransitionMetrics; m != nil { stats.AccountHashes = m.AccountUpdate + m.StateUpdate + m.StateHash - stats.AccountCommits = m.AccountCommits - stats.StorageCommits = m.StorageCommits - stats.DatabaseCommit = m.TrieDBCommits //stats.Prefetch = m.StatePrefetch } //stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 996a7e0645..bafaee671c 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -83,13 +83,6 @@ type BALStateTransitionMetrics struct { StatePrefetch time.Duration StateUpdate time.Duration StateHash time.Duration - - // commit metrics - AccountCommits time.Duration - StorageCommits time.Duration - SnapshotCommits time.Duration - TrieDBCommits time.Duration - TotalCommitTime time.Duration } // NewBALStateTransition creates a BALStateTransition instance @@ -190,7 +183,7 @@ func (s *BALStateTransition) updateAccount(addr common.Address) (*types.StateAcc func (s *BALStateTransition) commitAccount(addr common.Address) (*AccountUpdate, *trienode.NodeSet, error) { op := &AccountUpdate{ Address: addr, - Data: s.postStates[addr], // TODO: cache the updated state account somewhere + Data: s.postStates[addr], } var prestate *types.StateAccount if ps, exist := s.prestates.Load(addr); exist { @@ -299,7 +292,6 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b // off some milliseconds from the commit operation. Also accumulate the code // writes to run in parallel with the computations. var ( - start = time.Now() root common.Hash workers errgroup.Group ) @@ -320,7 +312,6 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b if err := merge(set); err != nil { return err } - s.metrics.AccountCommits = time.Since(start) return nil }) @@ -350,7 +341,6 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b } lock.Lock() updates[crypto.Keccak256Hash(address[:])] = update - s.metrics.StorageCommits = time.Since(start) // overwrite with the longest storage commit runtime lock.Unlock() return nil }) diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index 72d9672b19..0b1bfb0874 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -220,9 +220,13 @@ type ReaderWithBlockLevelAccessList struct { TxIndex int } -// NewReaderWithAccessList wraps a base reader with a shared, already -// preprocessed access list. This is the cheap constructor used on the hot path: -// the prepared list is built once per block and borrowed by every per-tx reader. +// NewReaderWithAccessList constructs a reader for accessing states +// with the mutations made by transactions prior to txIndex. +// +// The txIndex refers to the call frame as such: +// - 0 for pre‑execution system contract calls. +// - 1 … n for transactions (in block order). +// - n + 1 for post‑execution system contract calls. func NewReaderWithAccessList(base Reader, prepared *bal.AccessListReader, txIndex int) *ReaderWithBlockLevelAccessList { return &ReaderWithBlockLevelAccessList{ Reader: base, diff --git a/tests/init_test.go b/tests/init_test.go index 20c9e8e1c9..b933c9808c 100644 --- a/tests/init_test.go +++ b/tests/init_test.go @@ -41,10 +41,9 @@ var ( transactionTestDir = filepath.Join(baseDir, "TransactionTests") rlpTestDir = filepath.Join(baseDir, "RLPTests") difficultyTestDir = filepath.Join(baseDir, "BasicTests") - executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") - executionSpecBALBlockchainTestDir = filepath.Join(".", "spec-tests-bal", "fixtures", "blockchain_tests") - executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests") - executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests") + executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") + executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests") + executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests") benchmarksDir = filepath.Join(".", "evm-benchmarks", "benchmarks") )