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..e6f4d3857e 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.3.1 +# https://github.com/ethereum/execution-specs/releases +# https://github.com/ethereum/execution-specs/releases/download/tests-bal%40v7.3.1 +3c9bd8799a506a96f74162863efdf5eaa00226e645db6523346fdb7c5ba0bf62 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 c6fac5396e..070bdf4c55 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/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 9eb1bdbf5f..6167cb28a6 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -147,7 +147,7 @@ func Transaction(ctx *cli.Context) error { } // For Prague txs, validate the floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations()))) if err != nil { r.Error = err results = append(results, r) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index ab9a336349..d40ee08587 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" @@ -241,6 +242,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 5e90164aaa..80a65e5fe3 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -91,6 +91,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 a248d4fa8a..8e9842d62c 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", @@ -1120,6 +1137,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 ecb33ce0b8..eaad5d2d8a 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 { @@ -1660,7 +1667,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 } @@ -1774,7 +1781,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 } @@ -2129,16 +2136,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..4c4d09b747 --- /dev/null +++ b/core/parallel_state_processor.go @@ -0,0 +1,318 @@ +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 + vmCfg *vm.Config +} + +// NewParallelStateProcessor returns a new ParallelStateProcessor instance. +func NewParallelStateProcessor(chain *HeaderChain, vmConfig *vm.Config) *ParallelStateProcessor { + return &ParallelStateProcessor{ + StateProcessor: NewStateProcessor(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, 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(), block.ParentHash(), 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, 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 3b1e627f28..3eebd18bac 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) @@ -107,12 +111,18 @@ type Trie interface { // in the trie with provided address. UpdateAccount(address common.Address, account *types.StateAccount, codeLen int) error + // UpdateAccountBatch attempts to update a list accounts in the batch manner. + UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLengths []int) error + // UpdateStorage associates key with value in the trie. If value has length zero, // any existing value is deleted from the trie. The value bytes must not be modified // by the caller while they are stored in the trie. If a node was not found in the // database, a trie.MissingNodeError is returned. UpdateStorage(addr common.Address, key, value []byte) error + // UpdateStorageBatch attempts to update a list storages in the batch manner. + UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error + // DeleteAccount abstracts an account deletion from the trie. DeleteAccount(address common.Address) 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 1c49d46020..4d4e41bd67 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) { @@ -1110,12 +1118,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 } @@ -1544,3 +1555,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/state_transition.go b/core/state_transition.go index dac8123530..e55e95fd78 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -151,8 +151,11 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set return gas, nil } -// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). -func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) { +// FloorDataGas computes the minimum gas required for a transaction based on its +// data tokens (EIP-7623). On Amsterdam it also includes the EIP-8131 per-auth +// tx-content floor and the EIP-8279 per-auth Block Access List floor, which +// together form the static floor seed extended at runtime by EIP-8279. +func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList, numAuths uint64) (uint64, error) { var ( tokens uint64 tokenCost uint64 @@ -203,7 +206,23 @@ func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) return 0, ErrGasUintOverflow } // Minimum gas required for a transaction based on its data tokens (EIP-7623). - return params.TxGas + tokens*tokenCost, nil + floor := params.TxGas + tokens*tokenCost + + // EIP-8131 / EIP-8279: each EIP-7702 authorization contributes a static + // per-auth floor. EIP-8131 prices the 101-byte authorization tuple + // (FloorCostPerAuth) and EIP-8279 adds the worst-case BAL bytes the auth + // writes when applied (BALBytesPerAuthorization at FloorGasPerByte). The + // per-auth BAL term is folded into the static floor because set_delegation + // runs outside the EVM's out-of-gas handler and cannot extend the floor at + // runtime. + if rules.IsAmsterdam && numAuths > 0 { + const perAuth = params.FloorCostPerAuth + params.BALBytesPerAuthorization*params.FloorGasPerByte + if (math.MaxUint64-floor)/perAuth < numAuths { + return 0, ErrGasUintOverflow + } + floor += numAuths * perAuth + } + return floor, nil } // toWordSize returns the ceiled word size required for init code payment calculation. @@ -349,11 +368,17 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err // 5. Run Script section // 6. Derive new state root type stateTransition struct { - gp *GasPool - msg *Message - gasRemaining vm.GasBudget - state vm.StateDB - evm *vm.EVM + gp *GasPool + msg *Message + gasRemaining vm.GasBudget + initReservoir uint64 // initial state-gas reservoir carved out of GasLimit (EIP-8037) + state vm.StateDB + evm *vm.EVM + + // floorGas is the EIP-8279 Block Access List floor accumulator, seeded with + // the static floor and extended at runtime via the EVM. It is nil before + // Amsterdam. settleGas reads its final value to apply the receipt floor. + floorGas *vm.FloorGasAccumulator } // newStateTransition initialises and returns a new state transition object. @@ -392,7 +417,7 @@ func (st *stateTransition) to() common.Address { // - Amsterdam+ (EIP-8037): two-dimensional budget. Regular gas is // capped at `MaxTxGas` (EIP-7825, 16_777_216); any excess from // `msg.GasLimit` above that cap becomes the state-gas reservoir. -func (st *stateTransition) buyGas() error { +func (st *stateTransition) buyGas(intrinsic vm.GasCosts) error { mgval := new(uint256.Int).SetUint64(st.msg.GasLimit) _, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice) if overflow { @@ -446,10 +471,20 @@ func (st *stateTransition) buyGas() error { } isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) - // Reserve the gas budget in the block gas pool + // Reserve the gas budget in the block gas pool. This block-inclusion check + // must run before the sender's balance is debited below, so it cannot be + // deferred past buyGas. var err error if isAmsterdam { - err = st.gp.CheckGasAmsterdam(min(st.msg.GasLimit, params.MaxTxGas), st.msg.GasLimit) + // EIP-8037 per-tx 2D block-inclusion check (fork.py): the worst-case + // regular contribution is min(MaxTxGas, tx.gas - intrinsic.state) and + // the worst-case state contribution is tx.gas - intrinsic.regular. + // Each dimension subtracts the other's intrinsic counterpart. The + // intrinsic gas is computed once by execute() and passed in, so it is + // shared with the charge below rather than recomputed. + regularReservation := min(st.msg.GasLimit-min(st.msg.GasLimit, intrinsic.StateGas), params.MaxTxGas) + stateReservation := st.msg.GasLimit - min(st.msg.GasLimit, intrinsic.RegularGas) + err = st.gp.CheckGasAmsterdam(regularReservation, stateReservation) } else { err = st.gp.CheckGasLegacy(st.msg.GasLimit) } @@ -462,7 +497,8 @@ func (st *stateTransition) buyGas() error { if isAmsterdam { limit = min(st.msg.GasLimit, params.MaxTxGas) } - st.gasRemaining = vm.NewGasBudget(limit, st.msg.GasLimit-limit) + st.initReservoir = st.msg.GasLimit - limit + st.gasRemaining = vm.NewGasBudget(limit, st.initReservoir) if st.evm.Config.Tracer.HasGasHook() { st.evm.Config.Tracer.EmitGasChange(tracing.Gas{}, st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance) @@ -491,7 +527,7 @@ func (st *stateTransition) buyGas() error { // // The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass // subsets of these checks for simulation paths (eth_call, eth_estimateGas). -func (st *stateTransition) preCheck() error { +func (st *stateTransition) preCheck(intrinsic vm.GasCosts) error { // Only check transactions that are not fake msg := st.msg if !msg.SkipNonceChecks { @@ -585,7 +621,7 @@ func (st *stateTransition) preCheck() error { return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) } } - return st.buyGas() + return st.buyGas(intrinsic) } // execute transitions the state by applying the current message and @@ -600,14 +636,10 @@ func (st *stateTransition) preCheck() error { // If a consensus error is encountered, it is returned directly with a // nil EVM execution result. func (st *stateTransition) execute() (*ExecutionResult, error) { - // Validate the message and pre-pay gas. - if err := st.preCheck(); err != nil { - return nil, err - } - - // Charge intrinsic gas (with overflow detection inside IntrinsicGas). - // Under Amsterdam the cost is two-dimensional and Charge debits both - // regular and state in one step. + // Compute the intrinsic gas once up front. It is a pure function of the + // message and rules (no state access), and is needed both by the EIP-8037 + // block-inclusion check in preCheck/buyGas and by the intrinsic charge + // below, so it is computed here and threaded through. var ( msg = st.msg rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time) @@ -618,6 +650,14 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if err != nil { return nil, err } + + // Validate the message and pre-pay gas. + if err := st.preCheck(cost); err != nil { + return nil, err + } + + // Charge intrinsic gas. Under Amsterdam the cost is two-dimensional and + // Charge debits both regular and state in one step. prior, sufficient := st.gasRemaining.Charge(cost) if !sufficient { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas) @@ -629,7 +669,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Validate the EIP-7623 calldata floor against the gas limit. The floor inflates // the total gas usage at tx end, so the gas limit must be sufficient to cover that. if rules.IsPrague { - floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) + floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList, uint64(len(msg.SetCodeAuthorizations))) if err != nil { return nil, err } @@ -645,6 +685,15 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } + // EIP-8279: seed the per-transaction Block Access List floor accumulator + // with the static floor and bound it by the transaction gas limit. The + // accumulator is extended at runtime as opcodes contribute BAL bytes; at + // settlement the receipt gas becomes max(execution_gas, floor_gas_used). + if rules.IsAmsterdam { + st.floorGas = vm.NewFloorGasAccumulator(floorDataGas, msg.GasLimit) + st.evm.SetFloorGas(st.floorGas) + } + if rules.IsEIP4762 { st.evm.AccessEvents.AddTxOrigin(msg.From) @@ -674,11 +723,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret []byte vmerr error // vm errors do not effect consensus and are therefore not assigned to err result vm.GasBudget - - // Capture the forwarded regular-gas amount BEFORE ForwardAll consumes - // it, so Absorb can back out state-gas spillover from UsedRegularGas - // per EIP-8037. - forwarded = st.gasRemaining.RegularGas ) if contractCreation { // Check whether the init code size has been exceeded. @@ -687,13 +731,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Execute the transaction's creation. ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result, forwarded) - - // If the contract creation failed, refund the account-creation state - // gas pre-charged in IntrinsicGas. - if rules.IsAmsterdam && vmerr != nil { - st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) - } + st.gasRemaining.Absorb(result) } else { // Increment the nonce for the next transaction. st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) @@ -711,7 +749,30 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Execute the transaction's call. ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result, forwarded) + st.gasRemaining.Absorb(result) + } + + // EIP-8037 (fork.py:1086): on any transaction error, the state gas + // consumed during *execution* is discarded — those state changes are + // reverted, so the charge is restored to the reservoir and not counted + // toward block_state_gas_used. The intrinsic state gas (CREATE new-account + // and EIP-7702 authorization charges) is tracked separately by the spec and + // is NOT discarded here; the CREATE new-account portion is refunded above + // via its dedicated RefundState. The frame-level Exit forms already refund + // state gas on a reverting/halting sub-call, but a top-level frame that + // ends in a code-deposit halt (or any other tx-level vmerr) can leave + // accumulated execution UsedStateGas that must be discarded here. + if rules.IsAmsterdam && vmerr != nil { + executionStateGas := st.gasRemaining.UsedStateGas - int64(cost.StateGas) + if executionStateGas > 0 { + st.gasRemaining.RefundState(uint64(executionStateGas)) + } + // Additionally, a failed CREATE transaction refunds the intrinsic + // account-creation state gas pre-charged in IntrinsicGas (fork.py:1093: + // when tx.to is Bytes0 the NEW_ACCOUNT charge is added to state_refund). + if contractCreation { + st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + } } // Settle down the gas usage and refund the ETH back if any remaining @@ -786,6 +847,7 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g if st.gasRemaining.UsedStateGas < 0 { return 0, 0, fmt.Errorf("negative topmost frame state gas usage, %d", st.gasRemaining.UsedStateGas) } + txStateGas := uint64(st.gasRemaining.UsedStateGas) // EIP-8037: @@ -808,20 +870,35 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g gasLeft += refund gasUsed = gasUsedBeforeRefund - refund - // EIP-7623: tx_gas_used = max(tx_gas_used_after_refund, calldata_floor). + // EIP-8279: the effective floor is the runtime accumulator (seeded with the + // static floorDataGas and extended by the BAL bytes opcodes contributed). It + // is always >= floorDataGas when the accumulator is active (Amsterdam); the + // max keeps the pre-Amsterdam / accumulator-less path on the static floor. + floorGas := floorDataGas + if st.floorGas != nil { + floorGas = max(floorGas, st.floorGas.FloorGasUsed()) + } + + // EIP-7623: tx_gas_used = max(tx_gas_used_after_refund, floor). peakUsed = gasUsedBeforeRefund - if rules.IsPrague && gasUsed < floorDataGas { - diff := floorDataGas - gasUsed + if rules.IsPrague && gasUsed < floorGas { + diff := floorGas - gasUsed if st.evm.Config.Tracer.HasGasHook() { st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: gasLeft}, tracing.Gas{Regular: gasLeft - diff}, tracing.GasChangeTxDataFloor) } gasLeft -= diff - gasUsed = floorDataGas - peakUsed = max(peakUsed, floorDataGas) + gasUsed = floorGas + peakUsed = max(peakUsed, floorGas) } if rules.IsAmsterdam { - if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil { + // EIP-7623/7976: the calldata floor applies to the block-level regular + // gas dimension as well, mirroring its effect on the receipt gas. The + // spec accumulates max(tx_regular_gas, floor) into block_gas_used, so the + // block must never count fewer regular units than the floor the sender + // was charged. EIP-8279 widens the floor to include BAL byte costs. + blockRegularGas := max(txRegularGas, floorGas) + if err = st.gp.ChargeGasAmsterdam(blockRegularGas, txStateGas, gasUsed); err != nil { return 0, 0, err } } else { @@ -881,14 +958,17 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio // once, and only when the account did not exist before the tx // // - the delegation-indicator portion (AuthorizationCreationSize × CPSB) is -// charged at most once, and only when the authority ends the tx delegated -// having started it undelegated. -func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]bool) error { +// refunded when this auth writes no new indicator bytes (the authority is +// already delegated, or the auth clears the delegation). +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization) error { authority, err := st.validateAuthorization(auth) if err != nil { - if rules.IsAmsterdam { - st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) - } + // EIP-8037 (spec apply_authorization): an invalid authorization is + // skipped without any state-gas refund. The per-auth intrinsic state + // charge ((NEW_ACCOUNT + AUTH_BASE) * CPSB) was levied for every + // authorization in the list regardless of validity, and only a + // successfully-applied authorization that avoids creating new state + // earns a refund below. Invalid auths therefore pay in full. return err } prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority)) @@ -898,29 +978,20 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) } } else { + // EIP-8037 (spec apply_authorization): refund the per-auth intrinsic + // state charge for state that does not actually get newly created. + // + // - NEW_ACCOUNT is refunded when the authority account already exists + // (account_exists), since no new account is created. if st.state.Exist(authority) { st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) } - authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte - - preDelegated, ok := delegates[authority] - if !ok { - preDelegated = curDelegated - delegates[authority] = preDelegated - } - if auth.Address == (common.Address{}) { - // Clearing writes no indicator, refill this auth's state charge. - st.gasRemaining.RefundState(authBase) - - // The indicator was created by an earlier auth within the same - // transaction, refill the state charge as it's no longer justified. - if curDelegated && !preDelegated { - st.gasRemaining.RefundState(authBase) - } - } else if curDelegated || preDelegated { - // The 23-byte slot is already occupied, overwriting it writes no - // new bytes, refill the state charge. - st.gasRemaining.RefundState(authBase) + // - AUTH_BASE is refunded when no new delegation-indicator bytes are + // written: either the authority already carries code/delegation + // (code_hash != EMPTY, i.e. curDelegated) or this auth clears the + // delegation (auth.address == 0). Exactly one refund per auth. + if curDelegated || auth.Address == (common.Address{}) { + st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte) } } @@ -943,9 +1014,8 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se // applyAuthorizations applies an EIP-7702 code delegation to the state. func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) { - preDelegated := make(map[common.Address]bool) for _, auth := range auths { - st.applyAuthorization(rules, &auth, preDelegated) + st.applyAuthorization(rules, &auth) } } diff --git a/core/state_transition_test.go b/core/state_transition_test.go index be2de7f511..0a3220f1f7 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -123,7 +123,7 @@ func TestFloorDataGas(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rules := params.Rules{IsAmsterdam: tt.amsterdam} - got, err := FloorDataGas(rules, tt.data, tt.accessList) + got, err := FloorDataGas(rules, tt.data, tt.accessList, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 3b30dd30ef..08c66cc392 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -134,7 +134,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction can cover floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations()))) if err != nil { return err } 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 522192cee8..fc2d6f553f 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/contract.go b/core/vm/contract.go index 9155e9f84a..8b4114b84d 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -153,9 +153,9 @@ func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.G } // refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state. -func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) { +func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) { prior := c.Gas - c.Gas.Absorb(child, forwarded) + c.Gas.Absorb(child) if logger.HasGasHook() && reason != tracing.GasChangeIgnored { logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } diff --git a/core/vm/eips.go b/core/vm/eips.go index f8473e65e8..cf3ff9c209 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -588,6 +588,8 @@ func enable7843(jt *JumpTable) { func enable8037(jt *JumpTable) { jt[CREATE].constantGas = params.CreateGasAmsterdam jt[CREATE2].constantGas = params.CreateGasAmsterdam + jt[CREATE].dynamicGas = gasCreateEip8037 + jt[CREATE2].dynamicGas = gasCreate2Eip8037 jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037 jt[SSTORE].dynamicGas = gasSStore8037 } diff --git a/core/vm/evm.go b/core/vm/evm.go index 50d9e8ab0c..9dd6e069cd 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -131,6 +131,13 @@ type EVM struct { returnData []byte // Last CALL's return data for subsequent reuse arena *stackArena + + // floorGas is the per-transaction EIP-8279 Block Access List byte-floor + // accumulator. It is set by the state transition at the start of each + // transaction and extended at runtime as opcodes contribute BAL bytes. It + // is nil before EIP-8279 (Amsterdam) or in contexts without BAL + // construction, in which case the runtime extensions are no-ops. + floorGas *FloorGasAccumulator } // NewEVM constructs an EVM instance with the supplied block context, state @@ -222,6 +229,26 @@ func (evm *EVM) SetTxContext(txCtx TxContext) { evm.TxContext = txCtx } +// SetFloorGas installs the per-transaction EIP-8279 floor accumulator. It is +// called by the state transition once the static floor seed and gas limit are +// known. Passing nil disables runtime floor extensions for the transaction. +func (evm *EVM) SetFloorGas(acc *FloorGasAccumulator) { + evm.floorGas = acc +} + +// FloorGas returns the active EIP-8279 floor accumulator, or nil if none is set. +func (evm *EVM) FloorGas() *FloorGasAccumulator { + return evm.floorGas +} + +// extendFloor extends the EIP-8279 floor accumulator by numBytes BAL bytes. It +// is a no-op when no accumulator is installed (pre-Amsterdam or BAL-less +// contexts). The returned error, when non-nil, is ErrOutOfGas and MUST abort +// the operation before the matching BAL byte is inserted. +func (evm *EVM) extendFloor(numBytes uint64) error { + return evm.floorGas.extendFloor(numBytes) +} + // Cancel cancels any running EVM operation. This may be called concurrently and // it's safe to be called multiple times. func (evm *EVM) Cancel() { @@ -261,11 +288,17 @@ 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 } - snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas + snapshot := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { @@ -279,7 +312,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) if _, ok := gas.ChargeRegular(wgas); !ok { evm.StateDB.RevertToSnapshot(snapshot) - return nil, gas.ExitHalt(reservoir), ErrOutOfGas + return nil, gas.ExitHalt(), ErrOutOfGas } } @@ -289,16 +322,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } evm.StateDB.CreateAccount(addr) } - if evm.chainRules.IsAmsterdam && !value.IsZero() && evm.StateDB.Empty(addr) { - prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte) - if !ok { - evm.StateDB.RevertToSnapshot(snapshot) - return nil, gas.ExitHalt(reservoir), ErrOutOfGas - } - if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation) - } - } // Perform the value transfer only in non-syscall mode. // Calling this is required even for zero-value transfers, // to ensure the state clearing mechanism is applied. @@ -324,7 +347,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, reservoir) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -356,11 +379,17 @@ 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 } - snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas + snapshot := evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -375,7 +404,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, reservoir) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -406,7 +435,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } - snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas + snapshot := evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -419,7 +448,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, reservoir) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -453,7 +482,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b // after all empty accounts were deleted, so this is not required. However, if we omit this, // then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json. // We could change this, but for now it's left for legacy reasons - snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas + snapshot := evm.StateDB.Snapshot() // We do an AddBalance of zero here, just in order to trigger a touch. // This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium, @@ -471,7 +500,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, reservoir) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { @@ -509,20 +538,28 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value } // Increment the caller's nonce after passing all validations evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) - reservoir := gas.StateGas // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas}) if !ok { - return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas } if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) } } + // EIP-8279: an opcode-level CREATE/CREATE2 records the deployed address in + // the BAL. The top-level creation transaction's contract address is part of + // the implicit per-tx bytes covered by TX_BASE headroom, so only nested + // creations extend the floor here. + if evm.depth > 0 { + if err = evm.extendFloor(params.BALBytesPerAddress); err != nil { + return nil, common.Address{}, gas.ExitHalt(), err + } + } // We add this to the access list _before_ taking a snapshot. Even if the // creation fails, the access-list change should not be rolled back. if evm.chainRules.IsEIP2929 { @@ -537,7 +574,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) { - halt := gas.ExitHalt(reservoir) + halt := gas.ExitHalt() if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution) } @@ -551,18 +588,9 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value snapshot := evm.StateDB.Snapshot() if !evm.StateDB.Exist(address) { evm.StateDB.CreateAccount(address) - - if evm.chainRules.IsAmsterdam && evm.depth > 0 { - // Only charge state gas if we are not doing a create transaction. - // Prevents double charging with IntrinsicGas. - prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte) - if !ok { - return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas - } - if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation) - } - } + // EIP-8037: the account-creation state gas is charged before the + // opcode runs (gasCreateEip8037) for CREATE/CREATE2 opcodes, and in + // IntrinsicGas for creation transactions, so there is no charge here. } // CreateContract means that regardless of whether the account previously existed // in the state trie or not, it _now_ becomes created as a _contract_ account. @@ -570,6 +598,21 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value // acts inside that account. evm.StateDB.CreateContract(address) + // EIP-8279: an opcode-level CREATE/CREATE2 sets the new contract's nonce + // (and, with a non-zero endowment, its balance), both recorded in the BAL. + // The floor is extended after the collision check, before the state + // mutation. The top-level creation transaction's nonce is covered by + // TX_BASE headroom, so only nested creations extend the floor here. + if evm.depth > 0 { + if err = evm.extendFloor(params.BALBytesPerNonce); err != nil { + return nil, common.Address{}, gas.ExitHalt(), err + } + if !value.IsZero() { + if err = evm.extendFloor(params.BALBytesPerBalance); err != nil { + return nil, common.Address{}, gas.ExitHalt(), err + } + } + } if evm.chainRules.IsEIP158 { evm.StateDB.SetNonce(address, 1, tracing.NonceChangeNewContract) } @@ -577,7 +620,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) if consumed < wanted { - return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas } prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) if evm.Config.Tracer.HasGasHook() { @@ -595,14 +638,23 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value contract.SetCallCode(common.Hash{}, code) contract.IsDeployment = true - ret, err = evm.initNewContract(contract, address) + var depositHalt bool + ret, depositHalt, err = evm.initNewContract(contract, address) // Special case: ErrCodeStoreOutOfGas pre-Homestead does NOT roll back // state and gas is preserved (i.e., treated as success). if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) - exit := contract.Gas.Exit(err, reservoir) + // EIP-8037: a code-deposit halt (initcode body succeeded, deposit step + // failed) keeps the state gas the body consumed for discard at the tx + // level, rather than refunding the reservoir like a mid-execution halt. + var exit GasBudget + if depositHalt && evm.chainRules.IsAmsterdam { + exit = contract.Gas.ExitCodeDepositHalt() + } else { + exit = contract.Gas.Exit(err) + } if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution) @@ -617,54 +669,70 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value // initNewContract runs a new contract's creation code, performs checks on the // resulting code that is to be deployed, and consumes necessary gas. -func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]byte, error) { - ret, err := evm.Run(contract, nil, false) +// +// The returned depositHalt flag is true when the initcode body itself ran to +// completion successfully but a subsequent code-deposit check failed (oversized +// code, 0xEF prefix, or insufficient gas for the hash/deposit charge). Under +// EIP-8037 this halt is metered differently from a mid-execution halt: the +// state gas consumed by the (successful) body is kept rather than refunded. +func (evm *EVM) initNewContract(contract *Contract, address common.Address) (ret []byte, depositHalt bool, err error) { + ret, err = evm.Run(contract, nil, false) if err != nil { - return ret, err + return ret, false, err } // Check prefix before gas calculation. // Reject code starting with 0xEF if EIP-3541 is enabled. if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { - return ret, ErrInvalidCode + return ret, true, ErrInvalidCode } if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas) contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) if len(ret) > 0 && (consumed < wanted) { - return ret, ErrCodeStoreOutOfGas + return ret, true, ErrCodeStoreOutOfGas } if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { - return ret, err + return ret, true, err } } else if evm.chainRules.IsAmsterdam { // Check max code size BEFORE charging gas so over-max code // does not consume state gas (which would inflate tx_state). if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { - return ret, err + return ret, true, err } // Charge regular gas (hash cost) before state gas. regularCost := toWordSize(uint64(len(ret))) * params.Keccak256WordGas if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { - return ret, ErrCodeStoreOutOfGas + return ret, true, ErrCodeStoreOutOfGas } // Charge state gas (code-deposit) afterwards. stateCost := uint64(len(ret)) * evm.Context.CostPerStateByte if !contract.chargeState(stateCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { - return ret, ErrCodeStoreOutOfGas + return ret, true, ErrCodeStoreOutOfGas } } else { createDataCost := uint64(len(ret)) * params.CreateDataGas if !contract.chargeRegular(createDataCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { - return ret, ErrCodeStoreOutOfGas + return ret, true, ErrCodeStoreOutOfGas } if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { - return ret, err + return ret, true, err + } + } + // EIP-8279: a successful opcode-level CREATE/CREATE2 deploy records the + // deployed code in the BAL. Extend the floor by the code length before + // set_code. A top-level creation transaction's deployed code is bounded by + // the calldata floor on its init code, so only nested creations extend the + // floor here. + if evm.depth > 0 && len(ret) > 0 { + if err := evm.extendFloor(uint64(len(ret))); err != nil { + return ret, true, err } } if len(ret) > 0 { evm.StateDB.SetCode(address, ret, tracing.CodeChangeContractCreation) } - return ret, nil + return ret, false, nil } // Create creates a new contract using code as deployment code. diff --git a/core/vm/floor.go b/core/vm/floor.go new file mode 100644 index 0000000000..b8a6388e97 --- /dev/null +++ b/core/vm/floor.go @@ -0,0 +1,76 @@ +// 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 vm + +import "github.com/ethereum/go-ethereum/params" + +// FloorGasAccumulator implements the per-transaction floor accumulator defined +// by EIP-8279 (Block Access List Byte Floor). It is an internal counter on the +// execution environment, seeded with the EIP-8131 static floor and extended at +// runtime by FloorGasPerByte for every byte an opcode adds to the EIP-7928 +// Block Access List. +// +// The accumulator is not part of the signed transaction, is not RLP-encoded, +// gossiped, or persisted; no gas is reserved or deducted from the execution +// budget. It is checked against the transaction's gas limit only to ensure the +// sender can pay the floor if it ends up binding, and at transaction end the +// receipt gas is settled as max(execution_gas_used, floor_gas_used). +type FloorGasAccumulator struct { + floorGasUsed uint64 // accumulated floor gas (static seed + runtime extensions) + gasLimit uint64 // tx.gas; the accumulator must never climb past this +} + +// NewFloorGasAccumulator returns an accumulator seeded with the static floor +// and bounded by the transaction gas limit. +func NewFloorGasAccumulator(staticFloor, gasLimit uint64) *FloorGasAccumulator { + return &FloorGasAccumulator{floorGasUsed: staticFloor, gasLimit: gasLimit} +} + +// FloorGasUsed returns the current value of the floor accumulator. +func (f *FloorGasAccumulator) FloorGasUsed() uint64 { + if f == nil { + return 0 + } + return f.floorGasUsed +} + +// extendFloor extends the floor accumulator by numBytes BAL bytes, each priced +// at params.FloorGasPerByte. It MUST be called BEFORE the matching BAL +// insertion or state mutation: if the new floor would exceed the transaction +// gas limit it returns ErrOutOfGas, which aborts the operation before any +// unpaid BAL byte exists. A nil accumulator (pre-EIP-8279, or contexts without +// BAL construction) is a no-op. +func (f *FloorGasAccumulator) extendFloor(numBytes uint64) error { + if f == nil { + return nil + } + // numBytes is bounded by deployed-code length in the worst case; guard the + // multiplication against overflow before checking against the gas limit. + if numBytes > (^uint64(0))/params.FloorGasPerByte { + return ErrOutOfGas + } + extension := numBytes * params.FloorGasPerByte + if f.floorGasUsed > f.gasLimit-min(f.gasLimit, extension) { + return ErrOutOfGas + } + newFloor := f.floorGasUsed + extension + if newFloor > f.gasLimit { + return ErrOutOfGas + } + f.floorGasUsed = newFloor + return nil +} diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 550375c9c0..d640acf216 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/params" ) @@ -367,6 +368,62 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, return GasCosts{RegularGas: gas}, nil } +// gasCreateEip8037 is the CREATE gas calculator for Amsterdam. It charges the +// account-creation cost as state gas (EIP-8037) here, before the opcode runs, +// so the 63/64 gas-forwarding split sees the post-charge regular gas. The +// charge is refunded to the reservoir in opCreate on any failure path that +// does not create an account. +func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return GasCosts{}, err + } + size, overflow := stack.back(2).Uint64WithOverflow() + if overflow { + return GasCosts{}, ErrGasUintOverflow + } + if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil { + return GasCosts{}, err + } + // Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow. + wordGas := params.InitCodeWordGas * ((size + 31) / 32) + stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte + return GasCosts{ + RegularGas: gas + wordGas, + StateGas: stateGas, + }, nil +} + +// gasCreate2Eip8037 is the CREATE2 gas calculator for Amsterdam. See +// gasCreateEip8037; CREATE2 additionally charges Keccak256WordGas for hashing +// the init code. +func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return GasCosts{}, err + } + size, overflow := stack.back(2).Uint64WithOverflow() + if overflow { + return GasCosts{}, ErrGasUintOverflow + } + if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil { + return GasCosts{}, err + } + // Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow. + wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * ((size + 31) / 32) + stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte + return GasCosts{ + RegularGas: gas + wordGas, + StateGas: stateGas, + }, nil +} + func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { expByteLen := uint64((stack.back(1).BitLen() + 7) / 8) @@ -446,6 +503,34 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m return 0, ErrOutOfGas } // Stateful check + if evm.chainRules.IsAmsterdam { + // EIP-8279: a CALL transferring non-zero value to a different account + // records a balance change for the recipient in the BAL. Extend the + // floor by the balance bytes before the transfer. A self-call moves no + // value out of the executing account and adds no balance bytes. + if transfersValue && address != contract.Address() { + if err := evm.extendFloor(params.BALBytesPerBalance); err != nil { + return 0, err + } + } + // EIP-8037: the cost of creating a new account via a value-bearing + // CALL is metered as state gas (NEW_ACCOUNT * CostPerStateByte), + // not the legacy regular CallNewAccountGas. It drains the state + // reservoir, spilling into regular gas only when the reservoir is + // exhausted, mirroring the spec's inline charge_state_gas in + // system.call. + if transfersValue && evm.StateDB.Empty(address) { + stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte + regularAfterCall := contract.Gas.RegularGas - gas + if stateGas > contract.Gas.StateGas && stateGas-contract.Gas.StateGas > regularAfterCall { + return 0, ErrOutOfGas + } + if !contract.chargeState(stateGas, evm.Config.Tracer, tracing.GasChangeAccountCreation) { + return 0, ErrOutOfGas + } + } + return gas, nil + } var stateGas uint64 if evm.chainRules.IsEIP158 { if transfersValue && evm.StateDB.Empty(address) { @@ -528,6 +613,10 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory address = common.Address(stack.peek().Bytes20()) ) if !evm.StateDB.AddressInAccessList(address) { + // EIP-8279: a cold beneficiary access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddAddressToAccessList(address) gas.RegularGas = params.ColdAccountAccessCostEIP2929 @@ -536,6 +625,14 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory if contract.Gas.RegularGas < gas.RegularGas { return gas, ErrOutOfGas } + // EIP-8279: SELFDESTRUCT moving a non-zero balance to a different beneficiary + // records the beneficiary's balance change in the BAL. A self-targeted + // SELFDESTRUCT moves no value out and adds no balance bytes. + if address != contract.Address() && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + if err := evm.extendFloor(params.BALBytesPerBalance); err != nil { + return GasCosts{}, err + } + } // Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist // in the current state yet still be considered non-existent by EIP-161 if its // nonce, balance, and code are all zero. Such accounts can appear temporarily @@ -565,12 +662,29 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo ) // Check slot presence in the access list if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + // EIP-8279: a cold SSTORE adds the storage key to the BAL. Extend the + // floor before the slot is recorded; an out-of-gas here aborts the + // opcode before the unpaid BAL byte exists. + if err := evm.extendFloor(params.BALBytesPerStorageKey); err != nil { + return GasCosts{}, err + } cost = GasCosts{RegularGas: params.ColdSloadCostEIP2929} // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } value := common.Hash(y.Bytes32()) + // EIP-8279: an SSTORE that changes the slot's current value contributes a + // post-value to the BAL. Charge the value bytes whenever the value differs, + // mirroring the BAL StorageWrite. This may over-charge when the same slot is + // written more than once in a transaction (the BAL records only one final + // post-value per slot), which is safe: it never under-charges. + if current != value { + if err := evm.extendFloor(params.BALBytesPerStorageValue); err != nil { + return GasCosts{}, err + } + } + if current == value { // noop (1) // EIP 2200 original clause: // return params.SloadGasEIP2200, nil diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index b1756ab5fe..f4469804f0 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -226,9 +226,11 @@ func (g GasBudget) ExitRevert() GasBudget { // ExitHalt produces the leftover for an exceptional halt. // -// - state_gas_reservoir is reset back to its value at the start of the child frame -// - the gas_left initially given to the child is consumed (set to zero) -func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget { +// Per the updated EIP-8037, only the regular gas_left is burned (folded into +// UsedRegularGas); the entire state-gas reservoir — including any portion that +// spilled into the regular pool during execution — is refunded to the caller's +// reservoir rather than reclassified as burned regular gas. +func (g GasBudget) ExitHalt() GasBudget { reservoir := int64(g.StateGas) + g.UsedStateGas if reservoir < 0 { // Reservoir should never be negative. By construction it equals @@ -237,21 +239,34 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget { reservoir = 0 log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas) } - // The portion of state gas charged from regular gas is also burned - // together with the regular gas, rather than being returned to the - // parent's state-gas reservoir. - var spilled uint64 - if uint64(reservoir) > initStateReservoir { - spilled = uint64(reservoir) - initStateReservoir - } return GasBudget{ RegularGas: 0, - StateGas: initStateReservoir, - UsedRegularGas: g.UsedRegularGas + g.RegularGas + spilled, + StateGas: uint64(reservoir), + UsedRegularGas: g.UsedRegularGas + g.RegularGas, UsedStateGas: 0, } } +// ExitCodeDepositHalt produces the leftover for a CREATE/CREATE2 frame whose +// initcode body ran to completion but then failed during the code-deposit step +// (oversized code, 0xEF prefix, or insufficient gas for the hash/deposit +// charge). Per the spec's process_create_message exception handler, this path +// differs from a mid-execution ExceptionalHalt: only the remaining regular gas +// is burned, while the state gas the (successful) body consumed is KEPT in +// UsedStateGas rather than refunded to the reservoir. The state changes are +// reverted by the caller, but the state-gas accounting is propagated upward so +// the top-level tx settlement can discard it as the state dimension (rather +// than folding it back into the combined balance, which would wrongly deflate +// the regular dimension). +func (g GasBudget) ExitCodeDepositHalt() GasBudget { + return GasBudget{ + RegularGas: 0, + StateGas: g.StateGas, + UsedRegularGas: g.UsedRegularGas + g.RegularGas, + UsedStateGas: g.UsedStateGas, + } +} + // Exit dispatches on err to the appropriate exit-form constructor // for the post-evm.Run path: // @@ -261,33 +276,24 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget { // // Soft validation failures (occurring BEFORE evm.Run) should call Preserved // directly instead of going through this dispatcher. -func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget { +func (g GasBudget) Exit(err error) GasBudget { switch { case err == nil: return g.ExitSuccess() case err == ErrExecutionReverted: return g.ExitRevert() default: - return g.ExitHalt(initStateReservoir) + return g.ExitHalt() } } // Absorb merges a sub-call's leftover GasBudget into this (caller's) running -// budget. Additionally, it does an EIP-8037 spillover correction: -// state-gas that spilled into the regular pool inside the child frame is -// excluded from the UsedRegularGas. -// -// spillover = forwarded - child.RegularGas - child.UsedRegularGas -// -// forwarded is the regular-gas amount that was passed to the child at call -// entry (i.e., the regular initial of the child's GasBudget). -func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) { - spillover := forwarded - child.RegularGas - child.UsedRegularGas - - g.UsedRegularGas -= child.RegularGas +// budget. Under the updated EIP-8037, state-gas no longer spills into the +// child's burned regular gas on halt, so the child's UsedRegularGas can be +// folded in directly without a spillover correction. +func (g *GasBudget) Absorb(child GasBudget) { g.RegularGas += child.RegularGas + g.UsedRegularGas += child.UsedRegularGas g.StateGas = child.StateGas g.UsedStateGas += child.UsedStateGas - - g.UsedRegularGas -= spillover } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 209457f670..d80833abf9 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -661,7 +661,14 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Stack.push(&stackvalue) // Refund the leftover gas back to current frame - scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + + // EIP-8037: no account was created on any failure path, so refund the + // account-creation state gas charged before the opcode ran (gasCreateEip8037) + // back to the reservoir. + if evm.chainRules.IsAmsterdam && suberr != nil { + scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte) + } if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -695,7 +702,14 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Stack.push(&stackvalue) // Refund the leftover gas back to current frame - scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + + // EIP-8037: no account was created on any failure path, so refund the + // account-creation state gas charged before the opcode ran (gasCreate2Eip8037) + // back to the reservoir. + if evm.chainRules.IsAmsterdam && suberr != nil { + scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte) + } if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -740,7 +754,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -776,7 +790,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -808,7 +822,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -841,7 +855,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 2206fb95fa..c9d99584e7 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -103,6 +103,12 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me slot := common.Hash(loc.Bytes32()) // Check slot presence in the access list if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + // EIP-8279: a cold SLOAD adds the storage key to the BAL. Extend the + // floor before the slot is recorded; an out-of-gas here aborts the + // opcode before the unpaid BAL byte exists. + if err := evm.extendFloor(params.BALBytesPerStorageKey); err != nil { + return GasCosts{}, err + } // If the caller cannot afford the cost, this change will be rolled back // If he does afford it, we can skip checking the same thing later on, during execution evm.StateDB.AddSlotToAccessList(contract.Address(), slot) @@ -126,6 +132,10 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo addr := common.Address(stack.peek().Bytes20()) // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { + // EIP-8279: a cold account access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } evm.StateDB.AddAddressToAccessList(addr) var overflow bool // We charge (cold-warm), since 'warm' is already charged as constantGas @@ -148,6 +158,10 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem addr := common.Address(stack.peek().Bytes20()) // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { + // EIP-8279: a cold account access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddAddressToAccessList(addr) // The warm storage read cost is already charged as constantGas @@ -165,6 +179,10 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g // the cost to charge for cold access, if any, is Cold - Warm coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 if !warmAccess { + // EIP-8279: a cold account access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } evm.StateDB.AddAddressToAccessList(addr) // Charge the remaining difference here already, to correctly calculate available // gas for call @@ -286,6 +304,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { // Perform EIP-2929 checks (stateless), checking address presence // in the accessList and charge the cold access accordingly. if !evm.StateDB.AddressInAccessList(addr) { + // EIP-8279: a cold account access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } evm.StateDB.AddAddressToAccessList(addr) // The WarmStorageReadCostEIP2929 (100) is already deducted in the form @@ -321,6 +343,11 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { if evm.StateDB.AddressInAccessList(target) { eip7702Cost = params.WarmStorageReadCostEIP2929 } else { + // EIP-8279: resolving a cold delegation target adds its address + // to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } evm.StateDB.AddAddressToAccessList(target) eip7702Cost = params.ColdAccountAccessCostEIP2929 } diff --git a/eth/backend.go b/eth/backend.go index 7438486696..7867f9b5ef 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -291,6 +291,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 99a0fa89f7..b5dada60e2 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" @@ -221,6 +222,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/params/protocol_params.go b/params/protocol_params.go index 69e10fa5d9..6725db91f2 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -103,6 +103,28 @@ const ( TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037 + // FloorCostPerAuth is the per-authorization tx-content floor contribution + // defined by EIP-8131: an EIP-7702 authorization tuple is 101 bytes, charged + // at the floor rate (101 * TxCostFloorPerToken7976 * TxTokenPerNonZeroByte). + FloorCostPerAuth uint64 = 101 * TxCostFloorPerToken7976 * TxTokenPerNonZeroByte // 6464 + + // EIP-8279: Block Access List Byte Floor. Each byte an opcode adds to the + // EIP-7928 Block Access List extends the transaction's floor accumulator by + // FloorGasPerByte gas, charged at runtime before the BAL grows. + FloorGasPerByte uint64 = TxCostFloorPerToken7976 * TxTokenPerNonZeroByte // 64: per-byte floor rate (EIP-7976) + BALBytesPerAddress uint64 = 20 // BAL bytes for an account address + BALBytesPerStorageKey uint64 = 32 // BAL bytes for a storage key + BALBytesPerStorageValue uint64 = 32 // BAL bytes for a storage post-value + BALBytesPerBalance uint64 = 32 // BAL bytes for a balance change + BALBytesPerNonce uint64 = 8 // BAL bytes for a nonce change + BALDelegationCodeBytes uint64 = 23 // EIP-7702 delegation marker length + // BALBytesPerAuthorization is the worst-case BAL contribution an EIP-7702 + // authorization adds when it is applied: the authority address, the + // delegation marker written to its code, and its nonce change. It is folded + // into the static floor seed since set_delegation runs outside the EVM's + // out-of-gas handler. + BALBytesPerAuthorization uint64 = BALBytesPerAddress + BALDelegationCodeBytes + BALBytesPerNonce // 51 + // These have been changed during the course of the chain CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. CallGasEIP150 uint64 = 700 // Static portion of gas for CALL-derivates after EIP 150 (Tangerine) 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") ) diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 91f7d6c3ec..bc6c369e37 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -92,7 +92,7 @@ func (tt *TransactionTest) Run() error { if rules.IsPrague { var floorDataGas uint64 - floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations()))) if err != nil { return } diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index 0d0c0e0e70..ac9b19a2e1 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -451,3 +451,11 @@ func (t *BinaryTrie) PrefetchStorage(addr common.Address, keys [][]byte) error { func (t *BinaryTrie) Witness() map[string][]byte { return t.tracer.Values() } + +func (t *BinaryTrie) UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error { + panic("not implemented") +} + +func (t *BinaryTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, _ []int) error { + panic("not implemented") +} diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 4d03ca45f0..f2176310d0 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -210,6 +210,29 @@ func (t *StateTrie) UpdateStorage(_ common.Address, key, value []byte) error { return nil } +// UpdateStorageBatch attempts to update a list storages in the batch manner. +func (t *StateTrie) UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error { + var ( + hkeys = make([][]byte, 0, len(keys)) + evals = make([][]byte, 0, len(values)) + ) + for _, key := range keys { + hk := crypto.Keccak256(key) + if t.preimages != nil { + t.secKeyCache[common.Hash(hk)] = key + } + hkeys = append(hkeys, hk) + } + for _, val := range values { + data, err := rlp.EncodeToBytes(val) + if err != nil { + return err + } + evals = append(evals, data) + } + return t.trie.UpdateBatch(hkeys, evals) +} + // UpdateAccount will abstract the write of an account to the secure trie. func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccount, _ int) error { hk := crypto.Keccak256(address.Bytes()) @@ -226,6 +249,29 @@ func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccoun return nil } +// UpdateAccountBatch attempts to update a list accounts in the batch manner. +func (t *StateTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, _ []int) error { + var ( + hkeys = make([][]byte, 0, len(addresses)) + values = make([][]byte, 0, len(accounts)) + ) + for _, addr := range addresses { + hk := crypto.Keccak256(addr.Bytes()) + if t.preimages != nil { + t.secKeyCache[common.Hash(hk)] = addr.Bytes() + } + hkeys = append(hkeys, hk) + } + for _, acc := range accounts { + data, err := rlp.EncodeToBytes(acc) + if err != nil { + return err + } + values = append(values, data) + } + return t.trie.UpdateBatch(hkeys, values) +} + func (t *StateTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error { return nil } diff --git a/trie/tracer.go b/trie/tracer.go index 04122d1384..042fa468bf 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -33,12 +33,10 @@ import ( // while the latter is inserted/deleted in order to follow the rule of trie. // This tool can track all of them no matter the node is embedded in its // parent or not, but valueNode is never tracked. -// -// Note opTracer is not thread-safe, callers should be responsible for handling -// the concurrency issues by themselves. type opTracer struct { inserts map[string]struct{} deletes map[string]struct{} + lock sync.RWMutex } // newOpTracer initializes the tracer for capturing trie changes. @@ -53,6 +51,9 @@ func newOpTracer() *opTracer { // in the deletion set (resurrected node), then just wipe it from // the deletion set as it's "untouched". func (t *opTracer) onInsert(path []byte) { + t.lock.Lock() + defer t.lock.Unlock() + if _, present := t.deletes[string(path)]; present { delete(t.deletes, string(path)) return @@ -64,6 +65,9 @@ func (t *opTracer) onInsert(path []byte) { // in the addition set, then just wipe it from the addition set // as it's untouched. func (t *opTracer) onDelete(path []byte) { + t.lock.Lock() + defer t.lock.Unlock() + if _, present := t.inserts[string(path)]; present { delete(t.inserts, string(path)) return @@ -73,12 +77,18 @@ func (t *opTracer) onDelete(path []byte) { // reset clears the content tracked by tracer. func (t *opTracer) reset() { + t.lock.Lock() + defer t.lock.Unlock() + clear(t.inserts) clear(t.deletes) } // copy returns a deep copied tracer instance. func (t *opTracer) copy() *opTracer { + t.lock.RLock() + defer t.lock.RUnlock() + return &opTracer{ inserts: maps.Clone(t.inserts), deletes: maps.Clone(t.deletes), @@ -87,6 +97,9 @@ func (t *opTracer) copy() *opTracer { // deletedList returns a list of node paths which are deleted from the trie. func (t *opTracer) deletedList() [][]byte { + t.lock.RLock() + defer t.lock.RUnlock() + paths := make([][]byte, 0, len(t.deletes)) for path := range t.deletes { paths = append(paths, []byte(path)) diff --git a/trie/transitiontrie/transition.go b/trie/transitiontrie/transition.go index 3e5511be9e..d939e804e3 100644 --- a/trie/transitiontrie/transition.go +++ b/trie/transitiontrie/transition.go @@ -144,6 +144,19 @@ func (t *TransitionTrie) UpdateStorage(address common.Address, key []byte, value return t.overlay.UpdateStorage(address, key, v) } +// UpdateStorageBatch attempts to update a list storages in the batch manner. +func (t *TransitionTrie) UpdateStorageBatch(address common.Address, keys [][]byte, values [][]byte) error { + if len(keys) != len(values) { + return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values)) + } + for i, key := range keys { + if err := t.UpdateStorage(address, key, values[i]); err != nil { + return err + } + } + return nil +} + // UpdateAccount abstract an account write to the trie. func (t *TransitionTrie) UpdateAccount(addr common.Address, account *types.StateAccount, codeLen int) error { // NOTE: before the rebase, this was saving the state root, so that OpenStorageTrie @@ -152,6 +165,22 @@ func (t *TransitionTrie) UpdateAccount(addr common.Address, account *types.State return t.overlay.UpdateAccount(addr, account, codeLen) } +// UpdateAccountBatch attempts to update a list accounts in the batch manner. +func (t *TransitionTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLens []int) error { + if len(addresses) != len(accounts) { + return fmt.Errorf("address and accounts length mismatch: %d != %d", len(addresses), len(accounts)) + } + if len(addresses) != len(codeLens) { + return fmt.Errorf("address and code length mismatch: %d != %d", len(addresses), len(codeLens)) + } + for i, addr := range addresses { + if err := t.UpdateAccount(addr, accounts[i], codeLens[i]); err != nil { + return err + } + } + return nil +} + // DeleteStorage removes any existing value for key from the trie. If a node was not // found in the database, a trie.MissingNodeError is returned. func (t *TransitionTrie) DeleteStorage(addr common.Address, key []byte) error { diff --git a/trie/trie.go b/trie/trie.go index 1ef2c2f1a6..7e69a90823 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -480,6 +480,69 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error } } +// UpdateBatch updates a batch of entries concurrently. +func (t *Trie) UpdateBatch(keys [][]byte, values [][]byte) error { + // Short circuit if the trie is already committed and unusable. + if t.committed { + return ErrCommitted + } + if len(keys) != len(values) { + return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values)) + } + // Insert the entries sequentially if there are not too many + // trie nodes in the trie. + fn, ok := t.root.(*fullNode) + if !ok || len(keys) < 4 { // TODO(rjl493456442) the parallelism threshold should be twisted + for i, key := range keys { + err := t.Update(key, values[i]) + if err != nil { + return err + } + } + return nil + } + var ( + ikeys = make(map[byte][][]byte) + ivals = make(map[byte][][]byte) + eg errgroup.Group + ) + for i, key := range keys { + hkey := keybytesToHex(key) + ikeys[hkey[0]] = append(ikeys[hkey[0]], hkey) + ivals[hkey[0]] = append(ivals[hkey[0]], values[i]) + } + if len(keys) > 0 { + fn.flags = t.newFlag() + } + for pos, ks := range ikeys { + eg.Go(func() error { + vs := ivals[pos] + for i, k := range ks { + if len(vs[i]) != 0 { + _, n, err := t.insert(fn.Children[pos], []byte{pos}, k[1:], valueNode(vs[i])) + if err != nil { + return err + } + fn.Children[pos] = n + } else { + _, n, err := t.delete(fn.Children[pos], []byte{pos}, k[1:]) + if err != nil { + return err + } + fn.Children[pos] = n + } + } + return nil + }) + } + if err := eg.Wait(); err != nil { + return err + } + t.unhashed += len(keys) + t.uncommitted += len(keys) + return nil +} + // MustDelete is a wrapper of Delete and will omit any encountered error but // just print out an error message. func (t *Trie) MustDelete(key []byte) { diff --git a/trie/trie_test.go b/trie/trie_test.go index 3661933e22..949f381f07 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1580,3 +1580,57 @@ func BenchmarkTrieSeqPrefetch(b *testing.B) { } } } + +func TestUpdateBatch(t *testing.T) { + testUpdateBatch(t, []kv{ + {k: []byte("do"), v: []byte("verb")}, + {k: []byte("ether"), v: []byte("wookiedoo")}, + {k: []byte("horse"), v: []byte("stallion")}, + {k: []byte("shaman"), v: []byte("horse")}, + {k: []byte("doge"), v: []byte("coin")}, + {k: []byte("dog"), v: []byte("puppy")}, + }) + + var entries []kv + for i := 0; i < 256; i++ { + entries = append(entries, kv{k: testrand.Bytes(32), v: testrand.Bytes(32)}) + } + testUpdateBatch(t, entries) +} + +func testUpdateBatch(t *testing.T, entries []kv) { + var ( + base = NewEmpty(nil) + keys [][]byte + vals [][]byte + ) + for _, entry := range entries { + base.Update(entry.k, entry.v) + keys = append(keys, entry.k) + vals = append(vals, entry.v) + } + for i := 0; i < 10; i++ { + k, v := testrand.Bytes(32), testrand.Bytes(32) + base.Update(k, v) + keys = append(keys, k) + vals = append(vals, v) + } + + cmp := NewEmpty(nil) + if err := cmp.UpdateBatch(keys, vals); err != nil { + t.Fatalf("Failed to update batch, %v", err) + } + + // Traverse the original tree, the changes made on the copy one shouldn't + // affect the old one + for _, key := range keys { + v1, _ := base.Get(key) + v2, _ := cmp.Get(key) + if !bytes.Equal(v1, v2) { + t.Errorf("Unexpected data, key: %v, want: %v, got: %v", key, v1, v2) + } + } + if base.Hash() != cmp.Hash() { + t.Errorf("Hash mismatch: want %x, got %x", base.Hash(), cmp.Hash()) + } +}