diff --git a/beacon/engine/ed_codec.go b/beacon/engine/ed_codec.go
index 02a1fd380539..83603616a715 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 60d564b8779f..2946f55ccc3d 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 454efa93c478..d6a9f2b803b2 100644
--- a/build/checksums.txt
+++ b/build/checksums.txt
@@ -5,6 +5,11 @@
# https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0
a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz
+# version:spec-tests-bal v7.2.0
+# https://github.com/ethereum/execution-specs/releases
+# https://github.com/ethereum/execution-specs/releases/download/tests-bal%40v7.2.0
+fc1d9ae174cdd5db789068839999e6f83666cc79f7dac36e973d7616d9a2e2cf fixtures_bal.tar.gz
+
# version:golang 1.25.10
# https://go.dev/dl/
20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz
diff --git a/build/ci.go b/build/ci.go
index 173a3280ce16..8948c2230e0e 100644
--- a/build/ci.go
+++ b/build/ci.go
@@ -164,6 +164,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"))
@@ -402,6 +405,7 @@ func doTest(cmdline []string) {
// Get test fixtures.
if !*short {
downloadSpecTestFixtures(csdb, *cachedir)
+ downloadBALSpecTestFixtures(csdb, *cachedir)
}
// Configure the toolchain.
@@ -467,6 +471,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 c6fac5396e0c..070bdf4c5512 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 ca19ae3386da..9eb1bdbf5f0d 100644
--- a/cmd/evm/internal/t8ntool/transaction.go
+++ b/cmd/evm/internal/t8ntool/transaction.go
@@ -133,7 +133,7 @@ func Transaction(ctx *cli.Context) error {
}
// Check intrinsic gas
rules := chainConfig.Rules(common.Big0, true, 0)
- cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
+ cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte)
if err != nil {
r.Error = err
results = append(results, r)
diff --git a/cmd/geth/archivecmd.go b/cmd/geth/archivecmd.go
new file mode 100644
index 000000000000..d6e241974e2c
--- /dev/null
+++ b/cmd/geth/archivecmd.go
@@ -0,0 +1,576 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see .
+
+package main
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "slices"
+ "time"
+
+ "github.com/ethereum/go-ethereum/cmd/utils"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/trie"
+ "github.com/ethereum/go-ethereum/trie/archive"
+ "github.com/ethereum/go-ethereum/triedb/database"
+ "github.com/urfave/cli/v2"
+)
+
+var (
+ // Flags for the archive command
+ archiveOutputFlag = &cli.StringFlag{
+ Name: "output",
+ Usage: "Path to archive output file",
+ Value: "", // Default: /nodearchive
+ }
+ archiveCompactionIntervalFlag = &cli.Uint64Flag{
+ Name: "compaction-interval",
+ Usage: "Run compaction after this many subtrees (0 = disable)",
+ Value: 1000,
+ }
+ archiveDryRunFlag = &cli.BoolFlag{
+ Name: "dry-run",
+ Usage: "Simulate without modifying database",
+ }
+
+ // Commands
+ archiveCheckNodeFlag = &cli.StringFlag{
+ Name: "owner",
+ Usage: "Owner hash (hex) for the trie node to check",
+ }
+ archiveCheckPathFlag = &cli.StringFlag{
+ Name: "path",
+ Usage: "Path (hex nibbles) of the trie node to check",
+ }
+
+ archiveCommand = &cli.Command{
+ Name: "archive",
+ Usage: "Archive state trie nodes to reduce database size",
+ Subcommands: []*cli.Command{
+ archiveGenerateCmd,
+ archiveVerifyCmd,
+ archiveDeleteJournalCmd,
+ archiveCheckNodeCmd,
+ },
+ }
+
+ archiveCheckNodeCmd = &cli.Command{
+ Name: "check-node",
+ Usage: "Check if a specific trie node exists in the raw DB",
+ Action: archiveCheckNode,
+ Flags: slices.Concat([]cli.Flag{
+ archiveCheckNodeFlag,
+ archiveCheckPathFlag,
+ }, utils.NetworkFlags, utils.DatabaseFlags),
+ }
+
+ archiveDeleteJournalCmd = &cli.Command{
+ Name: "delete-journal",
+ Usage: "Delete the pathdb journal to force a clean restart",
+ Action: archiveDeleteJournal,
+ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
+ Description: `
+Deletes the pathdb journal (TrieJournal key and merkle.journal file) from the
+database. This forces geth to restart with a bare disk layer, discarding any
+in-memory diff layers that may be inconsistent with archived state.
+
+Use this after running 'archive generate' if geth was started in between and
+recreated the journal.
+
+Examples:
+ geth archive delete-journal --datadir /path/to/datadir
+ geth archive delete-journal --hoodi
+`,
+ }
+
+ archiveVerifyCmd = &cli.Command{
+ Name: "verify",
+ Usage: "Verify all archived nodes can be correctly resurrected",
+ Action: archiveVerify,
+ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
+ Description: `
+Walks the entire state trie, resolving every expired node from the archive
+file and verifying that the reconstructed subtree hash matches the original.
+Also walks all storage tries referenced by accounts.
+
+The database is opened read-only. No modifications are made.
+
+Examples:
+ geth archive verify --datadir /path/to/datadir
+ geth archive verify --hoodi
+`,
+ }
+
+ archiveGenerateCmd = &cli.Command{
+ Name: "generate",
+ Usage: "Generate archive files from height-3 subtrees",
+ ArgsUsage: "[state-root]",
+ Action: archiveGenerate,
+ Flags: slices.Concat([]cli.Flag{
+ archiveOutputFlag,
+ archiveCompactionIntervalFlag,
+ archiveDryRunFlag,
+ }, utils.NetworkFlags, utils.DatabaseFlags),
+ Description: `
+Walks the state trie of the specified root (or head block) and archives
+subtrees at height 3. Each archived subtree is replaced with an expiredNode
+that references the archive file offset and size.
+
+Height is measured from leaves: leaves=0, parents=1, etc. A height-3 node
+has leaves at most 3 levels below it.
+
+The archiver reads trie nodes directly from the persistent database layer,
+bypassing any in-memory diff layers. This ensures consistency between the
+data it reads and the data it modifies.
+
+Examples:
+ # Archive from the persistent disk state
+ geth archive generate --datadir /path/to/datadir
+
+ # Dry run to see what would be archived
+ geth archive generate --dry-run --datadir /path/to/datadir
+
+ # Custom output and compaction interval
+ geth archive generate --output /path/to/archive --compaction-interval 500
+`,
+ }
+)
+
+// rawDBNodeReader implements database.NodeReader by reading trie nodes directly
+// from the raw key-value database, bypassing pathdb's in-memory diff layers.
+// This ensures the archiver sees the same trie state it modifies.
+type rawDBNodeReader struct {
+ db ethdb.KeyValueReader
+}
+
+func (r *rawDBNodeReader) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) {
+ var blob []byte
+ if owner == (common.Hash{}) {
+ blob = rawdb.ReadAccountTrieNode(r.db, path)
+ } else {
+ blob = rawdb.ReadStorageTrieNode(r.db, owner, path)
+ }
+ // Skip hash verification: the raw DB may contain expiredNode markers
+ // (blob[0] == 0x00) which have different hashes than the original nodes.
+ return blob, nil
+}
+
+// rawDBNodeDatabase implements database.NodeDatabase using direct raw DB reads.
+type rawDBNodeDatabase struct {
+ db ethdb.KeyValueReader
+ root common.Hash
+}
+
+func (d *rawDBNodeDatabase) NodeReader(stateRoot common.Hash) (database.NodeReader, error) {
+ // Only allow reading the persistent disk root state
+ if stateRoot != d.root {
+ return nil, fmt.Errorf("raw DB reader only supports disk root %x, got %x", d.root, stateRoot)
+ }
+ return &rawDBNodeReader{db: d.db}, nil
+}
+
+func archiveGenerate(ctx *cli.Context) error {
+ // 1. Setup node and databases
+ stack, _ := makeConfigNode(ctx)
+ defer stack.Close()
+
+ dryRun := ctx.Bool(archiveDryRunFlag.Name)
+ chaindb := utils.MakeChainDatabase(ctx, stack, dryRun)
+ defer chaindb.Close()
+
+ // Check state scheme - we only support PathDB
+ scheme := cycleCheckScheme(ctx, chaindb)
+ if scheme != rawdb.PathScheme {
+ return fmt.Errorf("archive generation requires path-based state scheme, got: %s", scheme)
+ }
+
+ // 2. Flush diff layers to disk via pathdb. This ensures the raw DB
+ // contains the complete, up-to-date state trie and that state history
+ // entries are properly written to the freezer.
+ trieDB := utils.MakeTrieDatabase(ctx, stack, chaindb, false, dryRun, false)
+ head, hasDiff := trieDB.DiffHead()
+ if hasDiff {
+ log.Info("Flushing diff layers to disk", "head", head)
+ if err := trieDB.Commit(head, true); err != nil {
+ trieDB.Close()
+ return fmt.Errorf("failed to flush diff layers: %w", err)
+ }
+ log.Info("Diff layers flushed successfully")
+ } else {
+ log.Info("No diff layers to flush, disk state is current", "root", head)
+ }
+ // Close triedb — we work directly with raw DB for archival.
+ // We'll re-open it at the end to write a fresh journal.
+ trieDB.Close()
+
+ // 3. Determine the disk state root (now up-to-date after flush).
+ rootBlob := rawdb.ReadAccountTrieNode(chaindb, nil)
+ if len(rootBlob) == 0 {
+ return errors.New("state trie not found in database")
+ }
+ root := crypto.Keccak256Hash(rootBlob)
+ log.Info("Using disk state root", "root", root)
+
+ // Create a raw DB node reader that bypasses pathdb layers
+ nodeDB := &rawDBNodeDatabase{db: chaindb, root: root}
+
+ // 4. Open archive writer (unless dry-run).
+ // The archive file is placed at /geth/nodearchive by default,
+ // matching the path used by ArchivedNodeResolver when reading back.
+ var writer *archive.ArchiveWriter
+ archivePath := ctx.String(archiveOutputFlag.Name)
+ if archivePath == "" {
+ archivePath = filepath.Join(stack.ResolvePath(""), "nodearchive")
+ }
+
+ if !dryRun {
+ var err error
+ writer, err = archive.NewArchiveWriter(archivePath)
+ if err != nil {
+ return fmt.Errorf("failed to open archive file %s: %w", archivePath, err)
+ }
+ defer writer.Close()
+ log.Info("Opened archive file", "path", archivePath)
+ } else {
+ log.Info("Dry run mode - no changes will be made")
+ }
+
+ // 5. Create and run archiver
+ archiver := trie.NewArchiver(
+ chaindb,
+ nodeDB,
+ writer,
+ ctx.Uint64(archiveCompactionIntervalFlag.Name),
+ dryRun,
+ )
+
+ start := time.Now()
+ if err := archiver.ProcessState(root); err != nil {
+ return fmt.Errorf("archive generation failed: %w", err)
+ }
+
+ // 6. Get stats and optionally run final compaction
+ subtrees, leaves, bytesDeleted := archiver.Stats()
+
+ if !dryRun && subtrees > 0 {
+ log.Info("Running final database compaction")
+ if err := chaindb.Compact(nil, nil); err != nil {
+ log.Warn("Final compaction failed", "err", err)
+ }
+ }
+
+ // 7. Re-journal the pathdb state with the current disk root.
+ // After archiving, some trie nodes have been replaced with expired
+ // markers. We re-open pathdb and write a fresh journal (disk layer
+ // only, since all diff layers were flushed in step 2) so that geth
+ // can restart cleanly.
+ if !dryRun {
+ log.Info("Re-journaling pathdb state")
+ freshTrieDB := utils.MakeTrieDatabase(ctx, stack, chaindb, false, false, false)
+ freshRoot := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(chaindb, nil))
+ if err := freshTrieDB.Journal(freshRoot); err != nil {
+ log.Warn("Failed to re-journal pathdb state", "err", err)
+ }
+ freshTrieDB.Close()
+ }
+
+ // 8. Print summary
+ var archiveSize uint64
+ if writer != nil {
+ archiveSize = writer.Offset()
+ }
+
+ log.Info("Archive generation complete",
+ "subtrees", subtrees,
+ "leaves", leaves,
+ "bytesDeleted", bytesDeleted,
+ "archiveSize", archiveSize,
+ "elapsed", common.PrettyDuration(time.Since(start)))
+
+ if dryRun {
+ log.Info("This was a dry run - no changes were made to the database")
+ }
+
+ return nil
+}
+
+func archiveVerify(ctx *cli.Context) error {
+ stack, _ := makeConfigNode(ctx)
+ defer stack.Close()
+
+ // Open database read-only
+ chaindb := utils.MakeChainDatabase(ctx, stack, true)
+ defer chaindb.Close()
+
+ scheme := cycleCheckScheme(ctx, chaindb)
+ if scheme != rawdb.PathScheme {
+ return fmt.Errorf("archive verify requires path-based state scheme, got: %s", scheme)
+ }
+
+ // Set archive data dir so ArchivedNodeResolver can find the file
+ // ResolvePath("") returns the node's data directory (e.g. .ethereum/hoodi/geth),
+ // but ArchivedNodeResolver expects the instance directory (.ethereum/hoodi)
+ // since it appends "geth/nodearchive" itself.
+ archive.ArchiveDataDir = filepath.Dir(stack.ResolvePath(""))
+
+ // Compute disk root
+ rootBlob := rawdb.ReadAccountTrieNode(chaindb, nil)
+ if len(rootBlob) == 0 {
+ return errors.New("state trie not found in database")
+ }
+ root := crypto.Keccak256Hash(rootBlob)
+ log.Info("Verifying archived nodes", "root", root)
+
+ nodeDB := &rawDBNodeDatabase{db: chaindb, root: root}
+
+ // Open account trie
+ accountTrie, err := trie.New(trie.StateTrieID(root), nodeDB)
+ if err != nil {
+ return fmt.Errorf("failed to open account trie: %w", err)
+ }
+
+ var (
+ totalAccounts int
+ totalStorageTries int
+ totalLeaves int
+ totalExpired int
+ totalErrors int
+ start = time.Now()
+ lastLog = time.Now()
+ )
+
+ // Walk the account trie — this resolves all expired nodes and verifies hashes
+ accountStats, err := accountTrie.Walk(func(path []byte, value []byte) error {
+ totalAccounts++
+ if time.Since(lastLog) > 30*time.Second {
+ log.Info("Verification progress",
+ "accounts", totalAccounts,
+ "storageTries", totalStorageTries,
+ "leaves", totalLeaves,
+ "expired", totalExpired,
+ "errors", totalErrors)
+ lastLog = time.Now()
+ }
+
+ // Decode account to check for storage trie
+ var acc types.StateAccount
+ if err := rlp.DecodeBytes(value, &acc); err != nil {
+ log.Warn("Failed to decode account", "err", err)
+ totalErrors++
+ return nil // continue walking
+ }
+ if acc.Root == types.EmptyRootHash {
+ return nil
+ }
+
+ // Open and walk storage trie.
+ // path is hex-nibble encoded (with a 16 terminator from the trie key),
+ // so convert nibble pairs back to the 32-byte account hash.
+ nibbles := path
+ if len(nibbles) > 0 && nibbles[len(nibbles)-1] == 16 {
+ nibbles = nibbles[:len(nibbles)-1]
+ }
+ keyBytes := make([]byte, len(nibbles)/2)
+ for i := 0; i < len(nibbles); i += 2 {
+ keyBytes[i/2] = nibbles[i]<<4 | nibbles[i+1]
+ }
+ accountHash := common.BytesToHash(keyBytes)
+ storageID := trie.StorageTrieID(root, accountHash, acc.Root)
+ storageTrie, err := trie.New(storageID, nodeDB)
+ if err != nil {
+ log.Warn("Failed to open storage trie", "account", accountHash, "err", err)
+ totalErrors++
+ return nil
+ }
+
+ storageStats, err := storageTrie.Walk(func(spath []byte, svalue []byte) error {
+ return nil
+ })
+ if err != nil {
+ log.Warn("Storage trie walk failed", "account", accountHash, "err", err)
+ totalErrors++
+ return nil
+ }
+ totalStorageTries++
+ totalLeaves += storageStats.Leaves
+ totalExpired += storageStats.ExpiredResolved
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("account trie walk failed: %w", err)
+ }
+
+ totalLeaves += accountStats.Leaves
+ totalExpired += accountStats.ExpiredResolved
+
+ log.Info("Archive verification complete",
+ "accounts", totalAccounts,
+ "storageTries", totalStorageTries,
+ "totalLeaves", totalLeaves,
+ "expiredResolved", totalExpired,
+ "errors", totalErrors,
+ "elapsed", common.PrettyDuration(time.Since(start)))
+
+ if totalErrors > 0 {
+ return fmt.Errorf("verification completed with %d errors", totalErrors)
+ }
+ return nil
+}
+
+func archiveDeleteJournal(ctx *cli.Context) error {
+ stack, _ := makeConfigNode(ctx)
+ defer stack.Close()
+
+ chaindb := utils.MakeChainDatabase(ctx, stack, false)
+ defer chaindb.Close()
+
+ // Delete the pathdb journal KV key
+ if err := chaindb.Delete([]byte("TrieJournal")); err != nil {
+ log.Warn("Failed to delete pathdb journal key", "err", err)
+ } else {
+ log.Info("Deleted pathdb journal key (TrieJournal)")
+ }
+
+ // Delete the journal file(s) - check both legacy and current locations
+ for _, dir := range []string{"triedb", ""} {
+ for _, name := range []string{"merkle.journal", "verkle.journal"} {
+ journalFile := filepath.Join(stack.ResolvePath(dir), name)
+ if err := os.Remove(journalFile); err == nil {
+ log.Info("Deleted journal file", "path", journalFile)
+ } else if !os.IsNotExist(err) {
+ log.Warn("Failed to delete journal file", "path", journalFile, "err", err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func archiveCheckNode(ctx *cli.Context) error {
+ stack, _ := makeConfigNode(ctx)
+ defer stack.Close()
+
+ chaindb := utils.MakeChainDatabase(ctx, stack, true)
+ defer chaindb.Close()
+
+ ownerHex := ctx.String(archiveCheckNodeFlag.Name)
+ pathHex := ctx.String(archiveCheckPathFlag.Name)
+
+ if ownerHex == "" {
+ return errors.New("--owner flag is required")
+ }
+
+ owner := common.HexToHash(ownerHex)
+
+ // Parse path: hex nibbles like "08" → []byte{0, 8}
+ var path []byte
+ for _, c := range pathHex {
+ var nibble byte
+ switch {
+ case c >= '0' && c <= '9':
+ nibble = byte(c - '0')
+ case c >= 'a' && c <= 'f':
+ nibble = byte(c-'a') + 10
+ case c >= 'A' && c <= 'F':
+ nibble = byte(c-'A') + 10
+ default:
+ return fmt.Errorf("invalid hex char in path: %c", c)
+ }
+ path = append(path, nibble)
+ }
+
+ log.Info("Checking node in raw DB", "owner", owner, "path", fmt.Sprintf("%x", path))
+
+ // Read the node directly from the raw DB
+ isAccount := owner == (common.Hash{})
+
+ // Check the target path and all prefixes up to root
+ for i := len(path); i >= 0; i-- {
+ subpath := path[:i]
+ var blob []byte
+ if isAccount {
+ blob = rawdb.ReadAccountTrieNode(chaindb, subpath)
+ } else {
+ blob = rawdb.ReadStorageTrieNode(chaindb, owner, subpath)
+ }
+
+ status := "MISSING"
+ details := ""
+ if len(blob) > 0 {
+ if blob[0] == 0x00 {
+ status = "EXPIRED"
+ if len(blob) == 17 {
+ offset := binary.BigEndian.Uint64(blob[1:9])
+ size := binary.BigEndian.Uint64(blob[9:17])
+ details = fmt.Sprintf("offset=%d size=%d", offset, size)
+ }
+ } else {
+ status = fmt.Sprintf("PRESENT (%d bytes, first=0x%02x)", len(blob), blob[0])
+ }
+ }
+ label := "prefix"
+ if i == len(path) {
+ label = "TARGET"
+ }
+ if i == 0 {
+ label = "ROOT"
+ }
+ log.Info("Node check",
+ "label", label,
+ "path", fmt.Sprintf("%x", subpath),
+ "pathLen", i,
+ "status", status,
+ "details", details)
+ }
+
+ // Also check a few child paths to see what's below the target
+ for nibble := byte(0); nibble < 16; nibble++ {
+ childPath := append(append([]byte{}, path...), nibble)
+ var blob []byte
+ if isAccount {
+ blob = rawdb.ReadAccountTrieNode(chaindb, childPath)
+ } else {
+ blob = rawdb.ReadStorageTrieNode(chaindb, owner, childPath)
+ }
+ if len(blob) > 0 {
+ status := fmt.Sprintf("PRESENT (%d bytes, first=0x%02x)", len(blob), blob[0])
+ if blob[0] == 0x00 && len(blob) == 17 {
+ offset := binary.BigEndian.Uint64(blob[1:9])
+ size := binary.BigEndian.Uint64(blob[9:17])
+ status = fmt.Sprintf("EXPIRED offset=%d size=%d", offset, size)
+ }
+ log.Info("Child node", "path", fmt.Sprintf("%x", childPath), "status", status)
+ }
+ }
+
+ return nil
+}
+
+// cycleCheckScheme returns the state scheme for the database.
+// It's a helper to check what scheme is in use.
+func cycleCheckScheme(ctx *cli.Context, db ethdb.Database) string {
+ return rawdb.ReadStateScheme(db)
+}
diff --git a/cmd/geth/config.go b/cmd/geth/config.go
index c02e307bdc49..e6ef7802d98f 100644
--- a/cmd/geth/config.go
+++ b/cmd/geth/config.go
@@ -27,6 +27,8 @@ import (
"strings"
"unicode"
+ "github.com/ethereum/go-ethereum/core/types/bal"
+
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/external"
"github.com/ethereum/go-ethereum/accounts/keystore"
@@ -241,6 +243,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 850e26d1618c..b43675f50fe8 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -95,6 +95,9 @@ var (
utils.BinTrieGroupDepthFlag,
utils.LightKDFFlag,
utils.EthRequiredBlocksFlag,
+ utils.BALExecutionModeFlag,
+ utils.PrefetchWorkersFlag,
+ utils.BlockingPrefetchFlag,
utils.LegacyWhitelistFlag, // deprecated
utils.CacheFlag,
utils.CacheDatabaseFlag,
@@ -254,6 +257,8 @@ func init() {
dumpConfigCommand,
// see dbcmd.go
dbCommand,
+ // See archivecmd.go
+ archiveCommand,
// See cmd/utils/flags_legacy.go
utils.ShowDeprecated,
// See snapshot.go
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index c41cf4ee40e9..66018186740d 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",
@@ -1114,6 +1131,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/bench_test.go b/core/bench_test.go
index 65179c54d457..fe66aeae0d37 100644
--- a/core/bench_test.go
+++ b/core/bench_test.go
@@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
data := make([]byte, nbytes)
return func(i int, gen *BlockGen) {
toaddr := common.Address{}
- cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false, false)
+ cost, _ := IntrinsicGas(data, nil, nil, false, params.Rules{}, params.CostPerStateByte)
signer := gen.Signer()
gasPrice := big.NewInt(0)
if gen.header.BaseFee != nil {
diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go
index b49ac83bb55c..e151a72801d8 100644
--- a/core/bintrie_witness_test.go
+++ b/core/bintrie_witness_test.go
@@ -64,12 +64,12 @@ var (
func TestProcessUBT(t *testing.T) {
var (
code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`)
- intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true, false)
+ intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0)
// A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness
// will not contain that copied data.
// Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985
codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`)
- intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true, false)
+ intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0)
signer = types.LatestSigner(testUBTChainConfig)
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain
@@ -201,7 +201,7 @@ func TestProcessParentBlockHash(t *testing.T) {
if isUBT {
chainConfig = testUBTChainConfig
}
- vmContext := NewEVMBlockContext(header, nil, new(common.Address))
+ vmContext := NewEVMBlockContext(header, &BlockChain{chainConfig: chainConfig}, new(common.Address))
evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{})
ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList())
}
diff --git a/core/block_validator.go b/core/block_validator.go
index 962fffb82a0e..cdfbe1074d9e 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -20,8 +20,9 @@ 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 +144,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 +207,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 7b5a910b7a1c..016d4c39ffac 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -31,6 +31,8 @@ import (
"sync/atomic"
"time"
+ "github.com/ethereum/go-ethereum/core/types/bal"
+
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/common/mclock"
@@ -221,6 +223,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.
@@ -360,12 +366,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
@@ -427,6 +434,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
bc.validator = NewBlockValidator(chainConfig, bc)
bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc)
bc.processor = NewStateProcessor(bc.hc)
+ bc.parallelProcessor = *NewParallelStateProcessor(bc.hc, bc.GetVMConfig())
genesisHeader := bc.GetHeaderByNumber(0)
if genesisHeader == nil {
@@ -1642,7 +1650,7 @@ func (bc *BlockChain) writeKnownBlock(block *types.Block) error {
// writeBlockWithState writes block, metadata and corresponding state data to the
// database.
-func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb *state.StateDB) error {
+func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb state.Committer) error {
if !bc.HasHeader(block.ParentHash(), block.NumberU64()-1) {
return consensus.ErrUnknownAncestor
}
@@ -1756,7 +1764,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
// writeBlockAndSetHead is the internal implementation of WriteBlockAndSetHead.
// This function expects the chain mutex to be held.
-func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) {
+func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state state.Committer, emitHeadEvent bool) (status WriteStatus, err error) {
if err := bc.writeBlockWithState(block, receipts, state); err != nil {
return NonStatTy, err
}
@@ -2111,16 +2119,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()) {
@@ -2301,6 +2429,12 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation
stats.CrossValidation = xvtime // The time spent on stateless cross validation
+ // Attach the computed block access list so it gets persisted alongside the
+ // block. The validator has already verified the hash matches the header.
+ if res.Bal != nil && block.AccessList() == nil {
+ block = block.WithAccessListUnsafe(res.Bal.ToEncodingObj())
+ }
+
// Write the block to the chain and get the status.
var status WriteStatus
if config.WriteState {
diff --git a/core/evm.go b/core/evm.go
index 73e4c01a995f..fdea63e46910 100644
--- a/core/evm.go
+++ b/core/evm.go
@@ -68,18 +68,19 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
}
return vm.BlockContext{
- CanTransfer: CanTransfer,
- Transfer: Transfer,
- GetHash: GetHashFn(header, chain),
- Coinbase: beneficiary,
- BlockNumber: new(big.Int).Set(header.Number),
- Time: header.Time,
- Difficulty: new(big.Int).Set(header.Difficulty),
- BaseFee: baseFee,
- BlobBaseFee: blobBaseFee,
- GasLimit: header.GasLimit,
- Random: random,
- SlotNum: slotNum,
+ CanTransfer: CanTransfer,
+ Transfer: Transfer,
+ GetHash: GetHashFn(header, chain),
+ Coinbase: beneficiary,
+ BlockNumber: new(big.Int).Set(header.Number),
+ Time: header.Time,
+ Difficulty: new(big.Int).Set(header.Difficulty),
+ BaseFee: baseFee,
+ BlobBaseFee: blobBaseFee,
+ GasLimit: header.GasLimit,
+ Random: random,
+ SlotNum: slotNum,
+ CostPerStateByte: params.CostPerStateByte,
}
}
diff --git a/core/gaspool.go b/core/gaspool.go
index 14f5abd93c3b..2fb6416795dc 100644
--- a/core/gaspool.go
+++ b/core/gaspool.go
@@ -27,6 +27,11 @@ type GasPool struct {
remaining uint64
initial uint64
cumulativeUsed uint64
+
+ // After 8037 Block gas used is
+ // max(cumulativeRegular, cumulativeState).
+ cumulativeRegular uint64
+ cumulativeState uint64
}
// NewGasPool initializes the gasPool with the given amount.
@@ -37,9 +42,9 @@ func NewGasPool(amount uint64) *GasPool {
}
}
-// SubGas deducts the given amount from the pool if enough gas is
+// CheckGasLegacy deducts the given amount from the pool if enough gas is
// available and returns an error otherwise.
-func (gp *GasPool) SubGas(amount uint64) error {
+func (gp *GasPool) CheckGasLegacy(amount uint64) error {
if gp.remaining < amount {
return ErrGasLimitReached
}
@@ -47,41 +52,70 @@ func (gp *GasPool) SubGas(amount uint64) error {
return nil
}
-// ReturnGas adds the refunded gas back to the pool and updates
+// CheckGasAmsterdam performs the EIP-8037 per-tx 2D block-inclusion check:
+// the worst-case regular contribution must fit in the regular dimension and
+// the worst-case state contribution must fit in the state dimension
+func (gp *GasPool) CheckGasAmsterdam(regularReservation, stateReservation uint64) error {
+ if gp.initial-gp.cumulativeRegular < regularReservation {
+ return ErrGasLimitReached
+ }
+ if gp.initial-gp.cumulativeState < stateReservation {
+ return ErrGasLimitReached
+ }
+ return nil
+}
+
+// ChargeGasLegacy adds the refunded gas back to the pool and updates
// the cumulative gas usage accordingly.
-func (gp *GasPool) ReturnGas(returned uint64, gasUsed uint64) error {
+func (gp *GasPool) ChargeGasLegacy(returned uint64, gasUsed uint64) error {
if gp.remaining > math.MaxUint64-returned {
return fmt.Errorf("%w: remaining: %d, returned: %d", ErrGasLimitOverflow, gp.remaining, returned)
}
- // The returned gas calculation differs across forks.
- //
- // - Pre-Amsterdam:
- // returned = purchased - remaining (refund included)
- //
- // - Post-Amsterdam:
- // returned = purchased - gasUsed (refund excluded)
+ // returned = purchased - remaining (refund included)
gp.remaining += returned
// gasUsed = max(txGasUsed - gasRefund, calldataFloorGasCost)
- // regardless of Amsterdam is activated or not.
gp.cumulativeUsed += gasUsed
return nil
}
+// ChargeGasAmsterdam calculates the new remaining gas in the pool after the
+// execution of a message. Previously we subtracted and re-added gas to the
+// gaspool. After Amsterdam we only check if we can include the transaction
+// and charge the gaspool at the end.
+func (gp *GasPool) ChargeGasAmsterdam(txRegular, txState, receiptGasUsed uint64) error {
+ gp.cumulativeRegular += txRegular
+ gp.cumulativeState += txState
+ gp.cumulativeUsed += receiptGasUsed
+
+ blockUsed := max(gp.cumulativeRegular, gp.cumulativeState)
+ if gp.initial < blockUsed {
+ return fmt.Errorf("%w: block gas overflow: initial %d, used %d (regular: %d, state: %d)",
+ ErrGasLimitReached, gp.initial, blockUsed, gp.cumulativeRegular, gp.cumulativeState)
+ }
+ // For tx inclusion, we only check if the regular dimension fits.
+ gp.remaining = gp.initial - gp.cumulativeRegular
+ return nil
+}
+
// Gas returns the amount of gas remaining in the pool.
func (gp *GasPool) Gas() uint64 {
return gp.remaining
}
-// CumulativeUsed returns the amount of cumulative consumed gas (refunded included).
+// CumulativeUsed returns the cumulative gas consumed for receipt tracking.
func (gp *GasPool) CumulativeUsed() uint64 {
return gp.cumulativeUsed
}
// Used returns the amount of consumed gas.
func (gp *GasPool) Used() uint64 {
+ if gp.cumulativeRegular > 0 || gp.cumulativeState > 0 {
+ // After 8037 we return max(sum_regular, sum_state)
+ return max(gp.cumulativeRegular, gp.cumulativeState)
+ }
if gp.initial < gp.remaining {
- panic("gas used underflow")
+ panic(fmt.Sprintf("gas used underflow: %v %v", gp.initial, gp.remaining))
}
return gp.initial - gp.remaining
}
@@ -89,9 +123,11 @@ func (gp *GasPool) Used() uint64 {
// Snapshot returns the deep-copied object as the snapshot.
func (gp *GasPool) Snapshot() *GasPool {
return &GasPool{
- initial: gp.initial,
- remaining: gp.remaining,
- cumulativeUsed: gp.cumulativeUsed,
+ initial: gp.initial,
+ remaining: gp.remaining,
+ cumulativeUsed: gp.cumulativeUsed,
+ cumulativeRegular: gp.cumulativeRegular,
+ cumulativeState: gp.cumulativeState,
}
}
@@ -100,6 +136,8 @@ func (gp *GasPool) Set(other *GasPool) {
gp.initial = other.initial
gp.remaining = other.remaining
gp.cumulativeUsed = other.cumulativeUsed
+ gp.cumulativeRegular = other.cumulativeRegular
+ gp.cumulativeState = other.cumulativeState
}
func (gp *GasPool) String() string {
diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go
new file mode 100644
index 000000000000..4c4d09b747de
--- /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 000000000000..5cf761d64733
--- /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 3b1e627f286c..3eebd18bacac 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 fbf4ab5f9c70..ddc6d7923856 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 42c5f2e5efe0..5e7d278232a9 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 16579f6d6a0f..5dbbab4ae15c 100644
--- a/core/state/database_ubt.go
+++ b/core/state/database_ubt.go
@@ -80,6 +80,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 be07cec0f97f..c485af96edc5 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 ff315ac5eb6e..72d9672b19b4 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
+}
+
+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
}
-// nolint:unused
-func newPrefetchStateReader(reader StateReader, accessList map[common.Address][]common.Hash, nThreads int) *prefetchStateReader {
+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/state_object.go b/core/state/state_object.go
index ce456e7668f6..2165fad57ad1 100644
--- a/core/state/state_object.go
+++ b/core/state/state_object.go
@@ -572,6 +572,30 @@ func (s *stateObject) Code() []byte {
return code
}
+// GetCommittedCode returns the contract code committed at the start of the
+// current execution, ignoring any intra-execution SetCode modifications.
+// Used by EIP-7702 authorization application to make refund decisions
+// relative to the originally-committed delegation, matching the spirit of
+// GetCommittedState for storage slots.
+func (s *stateObject) GetCommittedCode() []byte {
+ // The account did not exist at the start of the current execution.
+ if s.origin == nil {
+ return nil
+ }
+ hash := common.BytesToHash(s.origin.CodeHash)
+ if hash == types.EmptyCodeHash {
+ return nil
+ }
+ // If the code has not been touched in this execution, the live cache
+ // already holds the committed code.
+ if !s.dirtyCode {
+ return s.Code()
+ }
+ // Code was modified within the current execution. Reach for the on-disk
+ // blob keyed by the origin hash.
+ return s.db.reader.Code(s.address, hash)
+}
+
// CodeSize returns the size of the contract code associated with this object,
// or zero if none. This method is an almost mirror of Code, but uses a cache
// inside the database to avoid loading codes seen recently.
diff --git a/core/state/statedb.go b/core/state/statedb.go
index 1c49d460206d..79f4f2b530c8 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) {
@@ -397,6 +405,18 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash {
return common.Hash{}
}
+// GetCommittedCode returns the contract code committed at the start of the
+// current execution, ignoring any in-progress SetCode mutations. Returns
+// nil when the account had no code (or did not exist) prior to this
+// execution.
+func (s *StateDB) GetCommittedCode(addr common.Address) []byte {
+ stateObject := s.getStateObject(addr)
+ if stateObject != nil {
+ return stateObject.GetCommittedCode()
+ }
+ return nil
+}
+
// GetState retrieves the value associated with the specific key.
func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash {
stateObject := s.getStateObject(addr)
@@ -1110,12 +1130,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 +1567,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/statedb_hooked.go b/core/state/statedb_hooked.go
index 98d01343a45e..184629ea414c 100644
--- a/core/state/statedb_hooked.go
+++ b/core/state/statedb_hooked.go
@@ -75,6 +75,10 @@ func (s *hookedStateDB) GetCode(addr common.Address) []byte {
return s.inner.GetCode(addr)
}
+func (s *hookedStateDB) GetCommittedCode(addr common.Address) []byte {
+ return s.inner.GetCommittedCode(addr)
+}
+
func (s *hookedStateDB) GetCodeSize(addr common.Address) int {
return s.inner.GetCodeSize(addr)
}
diff --git a/core/state_processor.go b/core/state_processor.go
index 5690a152e7e2..d0ce45078ee8 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -277,9 +277,15 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList
defer tracer.OnSystemCallEnd()
}
}
+ // SYSTEM_MAX_SSTORES_PER_CALL = 16 is the upper bound on the number of
+ // new storage slots a single system call is expected to write.
+ //
+ // This value matches MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the
+ // largest per-block bound across the existing system contracts.
+ stateBudget := params.SystemMaxSStoresPerCall * evm.Context.CostPerStateByte * params.StorageCreationSize
msg := &Message{
From: params.SystemAddress,
- GasLimit: 30_000_000,
+ GasLimit: 30_000_000 + stateBudget,
GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0),
GasTipCap: uint256.NewInt(0),
@@ -290,7 +296,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList
evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress)
- _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
+ _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, stateBudget), common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
@@ -306,9 +312,15 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *
defer tracer.OnSystemCallEnd()
}
}
+ // SYSTEM_MAX_SSTORES_PER_CALL = 16 is the upper bound on the number of
+ // new storage slots a single system call is expected to write.
+ //
+ // This value matches MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the
+ // largest per-block bound across the existing system contracts.
+ stateBudget := params.SystemMaxSStoresPerCall * evm.Context.CostPerStateByte * params.StorageCreationSize
msg := &Message{
From: params.SystemAddress,
- GasLimit: 30_000_000,
+ GasLimit: 30_000_000 + stateBudget,
GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0),
GasTipCap: uint256.NewInt(0),
@@ -319,7 +331,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *
evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress)
- _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
+ _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, stateBudget), common.U2560)
if err != nil {
panic(err)
}
@@ -348,9 +360,15 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E
defer tracer.OnSystemCallEnd()
}
}
+ // SYSTEM_MAX_SSTORES_PER_CALL = 16 is the upper bound on the number of
+ // new storage slots a single system call is expected to write.
+ //
+ // This value matches MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the
+ // largest per-block bound across the existing system contracts.
+ stateBudget := params.SystemMaxSStoresPerCall * evm.Context.CostPerStateByte * params.StorageCreationSize
msg := &Message{
From: params.SystemAddress,
- GasLimit: 30_000_000,
+ GasLimit: 30_000_000 + stateBudget,
GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0),
GasTipCap: uint256.NewInt(0),
@@ -360,7 +378,7 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E
evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex)
evm.StateDB.AddAddressToAccessList(addr)
- ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
+ ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, stateBudget), common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
diff --git a/core/state_transition.go b/core/state_transition.go
index 51c583689254..fdd4b7d66358 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
+ "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
@@ -68,13 +69,27 @@ func (result *ExecutionResult) Revert() []byte {
}
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
-func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860, isAmsterdam bool) (vm.GasCosts, error) {
+func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation bool, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) {
// Set the starting gas for the raw transaction
- var gas uint64
- if isContractCreation && isHomestead {
- gas = params.TxGasContractCreation
+ var gas vm.GasCosts
+ if isContractCreation && rules.IsHomestead {
+ if rules.IsAmsterdam {
+ gas.RegularGas = params.TxGas + params.CreateGasAmsterdam
+ gas.StateGas = params.AccountCreationSize * costPerStateByte
+ } else {
+ gas.RegularGas = params.TxGasContractCreation
+ }
} else {
- gas = params.TxGas
+ gas.RegularGas = params.TxGas
+ }
+ // Add gas for authorizations
+ if authList != nil {
+ if rules.IsAmsterdam {
+ gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas
+ gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte
+ } else {
+ gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas
+ }
}
dataLen := uint64(len(data))
// Bump the required gas by the amount of transactional data
@@ -85,59 +100,56 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
// Make sure we don't exceed uint64 for all data combinations
nonZeroGas := params.TxDataNonZeroGasFrontier
- if isEIP2028 {
+ if rules.IsIstanbul {
nonZeroGas = params.TxDataNonZeroGasEIP2028
}
- if (math.MaxUint64-gas)/nonZeroGas < nz {
+ if (math.MaxUint64-gas.RegularGas)/nonZeroGas < nz {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += nz * nonZeroGas
+ gas.RegularGas += nz * nonZeroGas
- if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
+ if (math.MaxUint64-gas.RegularGas)/params.TxDataZeroGas < z {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += z * params.TxDataZeroGas
+ gas.RegularGas += z * params.TxDataZeroGas
- if isContractCreation && isEIP3860 {
+ if isContractCreation && rules.IsShanghai {
lenWords := toWordSize(dataLen)
- if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords {
+ if (math.MaxUint64-gas.RegularGas)/params.InitCodeWordGas < lenWords {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += lenWords * params.InitCodeWordGas
+ gas.RegularGas += lenWords * params.InitCodeWordGas
}
}
if accessList != nil {
addresses := uint64(len(accessList))
storageKeys := uint64(accessList.StorageKeys())
- if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses {
+ if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += addresses * params.TxAccessListAddressGas
- if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys {
+ gas.RegularGas += addresses * params.TxAccessListAddressGas
+ if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += storageKeys * params.TxAccessListStorageKeyGas
+ gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas
// EIP-7981: access list data is charged in addition to the base charge.
- if isAmsterdam {
+ if rules.IsAmsterdam {
const (
addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
)
- if (math.MaxUint64-gas)/addressCost < addresses {
+ if (math.MaxUint64-gas.RegularGas)/addressCost < addresses {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += addresses * addressCost
- if (math.MaxUint64-gas)/storageKeyCost < storageKeys {
+ gas.RegularGas += addresses * addressCost
+ if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += storageKeys * storageKeyCost
+ gas.RegularGas += storageKeys * storageKeyCost
}
}
- if authList != nil {
- gas += uint64(len(authList)) * params.CallNewAccountGas
- }
- return vm.GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623).
@@ -315,7 +327,7 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err
return newStateTransition(evm, msg, gp).execute()
}
-// stateTransition represents a state transition.
+// StateTransition represents a state transition.
//
// == The State Transitioning Model
//
@@ -337,7 +349,7 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err
//
// 5. Run Script section
// 6. Derive new state root
-type stateTransition struct {
+type StateTransition struct {
gp *GasPool
msg *Message
initialBudget vm.GasBudget
@@ -347,8 +359,8 @@ type stateTransition struct {
}
// newStateTransition initialises and returns a new state transition object.
-func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition {
- return &stateTransition{
+func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *StateTransition {
+ return &StateTransition{
gp: gp,
evm: evm,
msg: msg,
@@ -357,14 +369,32 @@ func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition
}
// to returns the recipient of the message.
-func (st *stateTransition) to() common.Address {
+func (st *StateTransition) to() common.Address {
if st.msg == nil || st.msg.To == nil /* contract creation */ {
return common.Address{}
}
return *st.msg.To
}
-func (st *stateTransition) buyGas() error {
+// buyGas pre-pays gas from the sender's balance and initializes the
+// transaction's gas budget. It is invoked at the tail of preCheck.
+//
+// The balance requirement is the worst-case ETH the tx may need to lock
+// up: `msg.GasLimit × max(msg.GasPrice, msg.GasFeeCap) + msg.Value`,
+// plus `blobGas × msg.BlobGasFeeCap` under Cancun. Insufficient balance
+// returns ErrInsufficientFunds. After the check, the sender is actually
+// debited `msg.GasLimit × msg.GasPrice` (plus `blobGas × blobBaseFee`
+// under Cancun), the cap-vs-tip differential is settled at tx end.
+//
+// The gas budget is seeded into both `initialBudget` (frozen snapshot
+// for tx-end accounting) and `gasRemaining` (live running balance):
+//
+// - Pre-Amsterdam: one-dimensional regular budget equal to
+// `msg.GasLimit`; the state-gas reservoir is zero.
+// - 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 {
mgval := new(uint256.Int).SetUint64(st.msg.GasLimit)
_, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice)
if overflow {
@@ -416,23 +446,43 @@ func (st *stateTransition) buyGas() error {
if have, want := st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) < 0 {
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want)
}
- if err := st.gp.SubGas(st.msg.GasLimit); err != nil {
- return err
+
+ // After Amsterdam we limit the regular gas to 16M, the data gas to the transaction limit
+ limit := st.msg.GasLimit
+ if st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) {
+ limit = min(st.msg.GasLimit, params.MaxTxGas)
}
+ st.initialBudget = vm.NewGasBudget(limit, st.msg.GasLimit-limit)
+ st.gasRemaining = st.initialBudget.Copy()
if st.evm.Config.Tracer.HasGasHook() {
- empty := vm.GasBudget{}
- initial := vm.NewGasBudget(st.msg.GasLimit)
- st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance)
+ st.evm.Config.Tracer.EmitGasChange(tracing.Gas{}, st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance)
}
- st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit)
- st.initialBudget = st.gasRemaining.Copy()
-
+ // Deduct the gas cost from the sender's balance
st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy)
return nil
}
-func (st *stateTransition) preCheck() error {
+// preCheck performs all pre-execution validation that does not require
+// the EVM to run, then ends by calling buyGas to lock in the gas budget.
+// It returns a consensus error if any of the following fail:
+//
+// - Sender nonce matches state and is not at 2^64-1 (EIP-2681).
+// - EIP-7825 per-tx gas-limit cap on Osaka chains pre-Amsterdam
+// (the cap also bounds the regular dimension after Amsterdam, but
+// it is enforced there via the two-dimensional budget in buyGas).
+// - EIP-3607 sender-is-EOA, allowing accounts whose only code is an
+// EIP-7702 delegation designator.
+// - EIP-1559 fee-cap, tip-cap and base-fee constraints (London+).
+// - Blob-tx structural checks: non-nil `To`, non-empty hash list,
+// valid KZG versioned hashes, count below `BlobTxMaxBlobs` (Osaka+).
+// - Blob fee-cap not below the current blob base fee (Cancun+).
+// - EIP-7702 set-code-tx shape: non-nil `To` and non-empty
+// authorization list.
+//
+// The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass
+// subsets of these checks for simulation paths (eth_call, eth_estimateGas).
+func (st *StateTransition) preCheck() error {
// Only check transactions that are not fake
msg := st.msg
if !msg.SkipNonceChecks {
@@ -449,11 +499,13 @@ func (st *stateTransition) preCheck() error {
msg.From.Hex(), stNonce)
}
}
- isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
- isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time)
+ var (
+ isOsaka = st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
+ isAmsterdam = st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time)
+ )
if !msg.SkipTransactionChecks {
// Verify tx gas limit does not exceed EIP-7825 cap.
- if isOsaka && !isAmsterdam && msg.GasLimit > params.MaxTxGas {
+ if !isAmsterdam && isOsaka && msg.GasLimit > params.MaxTxGas {
return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
}
// Make sure the sender is an EOA
@@ -527,28 +579,72 @@ func (st *stateTransition) preCheck() error {
return st.buyGas()
}
-// execute will transition the state by applying the current message and
-// returning the evm execution result with following fields.
+// reserveBlockGasBudget checks if the remaining gas budget in the block pool is
+// sufficient for including this transaction.
+func (st *StateTransition) reserveBlockGasBudget(rules params.Rules, gasLimit uint64, intrinsicCost vm.GasCosts) error {
+ var err error
+ if rules.IsAmsterdam {
+ // EIP-8037 per-tx 2D block-inclusion check. For each dimension,
+ // the worst-case contribution is tx.gas minus the other
+ // dimension's intrinsic (capped at MaxTxGas for the regular
+ // dimension).
+ regularReservation := gasLimit
+ if regularReservation > intrinsicCost.StateGas {
+ regularReservation -= intrinsicCost.StateGas
+ } else {
+ regularReservation = 0
+ }
+ regularReservation = min(regularReservation, params.MaxTxGas)
+
+ stateReservation := gasLimit
+ if stateReservation > intrinsicCost.RegularGas {
+ stateReservation -= intrinsicCost.RegularGas
+ } else {
+ stateReservation = 0
+ }
+ err = st.gp.CheckGasAmsterdam(regularReservation, stateReservation)
+ } else {
+ err = st.gp.CheckGasLegacy(gasLimit)
+ }
+ return err
+}
+
+// execute transitions the state by applying the current message and
+// returns the EVM execution result with the following fields:
//
-// - used gas: total gas used (including gas being refunded)
-// - returndata: the returned data from evm
-// - concrete execution error: various EVM errors which abort the execution, e.g.
-// ErrOutOfGas, ErrExecutionReverted
+// - used gas: total gas used, including gas refunded
+// - peak used gas: maximum gas used before applying refunds
+// - returndata: data returned by the EVM
+// - execution error: EVM-level errors that abort execution, such as
+// ErrOutOfGas or ErrExecutionReverted
//
-// However if any consensus issue encountered, return the error directly with
-// nil evm execution result.
-func (st *stateTransition) execute() (*ExecutionResult, error) {
- // First check this message satisfies all consensus rules before
- // applying the message. The rules include these clauses
+// If a consensus error is encountered, it is returned directly with a
+// nil EVM execution result.
+func (st *StateTransition) execute() (*ExecutionResult, error) {
+ // The state-transition pipeline below runs in stages. Each stage may
+ // abort with a consensus error before the EVM is invoked:
+ //
+ // 1. preCheck: nonce, fee-cap, blob and EIP-7702 structural
+ // checks; ends by calling buyGas to debit the
+ // sender and seed the two-dimensional gas budget
+ // (EIP-8037).
+ // 2. Intrinsic: charges the intrinsic regular + state cost from
+ // the running budget with overflow detection.
+ // 3. Block pool: per-dimension inclusion reservation against the
+ // block gas pool (two-dimensional after Amsterdam,
+ // EIP-8037).
+ // 4. Floor pre: EIP-7623 calldata floor must fit in the gas allowance.
+ // 5. Top-call: run the top-most call, ensuring sender can cover
+ // the value transfer of the top call frame; init-code
+ // size respects the cap.
//
- // 1. the nonce of the message caller is correct
- // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice)
- // 3. the amount of gas required is available in the block
- // 4. the purchased gas is enough to cover intrinsic usage
- // 5. there is no overflow when calculating intrinsic gas
- // 6. caller has enough balance to cover asset transfer for **topmost** call
-
- // Check clauses 1-3, buy gas if everything is correct
+ // After the EVM has run, the result path applies EIP-8037 state-gas
+ // refunds, the EIP-3529 regular-refund cap, and the EIP-7623 scalar
+ // floor (`tx_gas_used = max(tx_gas_used_after_refund, floor)`),
+ // returns leftover gas to the sender, settles the block pool and
+ // pays the coinbase tip.
+
+ // Stage 1: validate the message and pre-pay gas.
if err := st.preCheck(); err != nil {
return nil, err
}
@@ -559,8 +655,11 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
contractCreation = msg.To == nil
floorDataGas uint64
)
- // Check clauses 4-5, subtract intrinsic gas if everything is correct
- cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
+
+ // Stage 2: 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.
+ cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte)
if err != nil {
return nil, err
}
@@ -571,14 +670,33 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
if st.evm.Config.Tracer.HasGasHook() {
st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas)
}
- // Gas limit suffices for the floor data cost (EIP-7623)
+
+ // Stage 3: reserve this tx's share of the block gas pool. Under
+ // Amsterdam this is a two-dimensional per-tx inclusion check; before
+ // Amsterdam it is a single scalar subtraction.
+ if err := st.reserveBlockGasBudget(rules, msg.GasLimit, cost); err != nil {
+ return nil, err
+ }
+
+ // Stage 4: 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)
if err != nil {
return nil, err
}
+ // Make sure the transaction has sufficient gas allowance to
+ // pay the floor cost.
if msg.GasLimit < floorDataGas {
- return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas)
+ return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, msg.GasLimit, floorDataGas)
+ }
+ // In Amsterdam, the transaction gas limit is allowed to exceed
+ // params.MaxTxGas, but the calldata floor cost is capped by it.
+ if rules.IsAmsterdam {
+ if max(cost.RegularGas, floorDataGas) > params.MaxTxGas {
+ return nil, fmt.Errorf("%w: regular intrisic cost %v, floor: %v", ErrFloorDataGas, cost.RegularGas, floorDataGas)
+ }
}
}
@@ -590,7 +708,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
}
- // Check clause 6
+ // Stage 5: top-call affordability, the sender must still be able
+ // to cover the value transfer of the top frame after gas pre-pay.
value := msg.Value
if value == nil {
value = new(uint256.Int)
@@ -617,18 +736,15 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
)
if contractCreation {
- ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value)
+ var result vm.GasBudget
+ ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
+ 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)
// Apply EIP-7702 authorizations.
- if msg.SetCodeAuthorizations != nil {
- for _, auth := range msg.SetCodeAuthorizations {
- // Note errors are ignored, we simply skip invalid authorizations here.
- st.applyAuthorization(&auth)
- }
- }
+ st.applyAuthorizations(rules, msg.SetCodeAuthorizations)
// Perform convenience warming of sender's delegation target. Although the
// sender is already warmed in Prepare(..), it's possible a delegation to
@@ -638,43 +754,81 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok {
st.state.AddAddressToAccessList(addr)
}
-
// Execute the transaction's call.
- ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value)
+ var result vm.GasBudget
+ ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value)
+ st.gasRemaining.Absorb(result)
+ }
+ // If this was a failed contract creation, refund the account creation costs.
+ if rules.IsAmsterdam {
+ if vmerr != nil && contractCreation {
+ refund := params.AccountCreationSize * st.evm.Context.CostPerStateByte
+ st.gasRemaining.RefundState(refund)
+ }
}
// Record the gas used excluding gas refunds. This value represents the actual
// gas allowance required to complete execution.
peakGasUsed := st.gasUsed()
+ peakRegular := st.gasRemaining.UsedRegularGas
// Compute refund counter, capped to a refund quotient.
- st.gasRemaining.Refund(st.calcRefund())
+ st.gasRemaining.RefundRegular(st.calcRefund())
if rules.IsPrague {
- // After EIP-7623: Data-heavy transactions pay the floor gas.
+ // EIP-7623 floor: tx_gas_used_after_refund = max(used, calldata_floor).
+ // Drain the leftover gas budget — regular first, then state — to bring
+ // gasUsed up to the floor. State must be drained too because a failed
+ // contract-creation top-level refund (line ~770) can move otherwise-spent
+ // gas back into the state reservoir, leaving RegularGas too small to
+ // satisfy the floor on its own.
if used := st.gasUsed(); used < floorDataGas {
- prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used})
+ prior := st.gasRemaining
+ need := floorDataGas - used
+ if take := min(need, st.gasRemaining.RegularGas); take > 0 {
+ st.gasRemaining.RegularGas -= take
+ st.gasRemaining.UsedRegularGas += take
+ need -= take
+ }
+ if take := min(need, st.gasRemaining.StateGas); take > 0 {
+ st.gasRemaining.StateGas -= take
+ st.gasRemaining.UsedStateGas += int64(take)
+ need -= take
+ }
+ if need > 0 {
+ return nil, fmt.Errorf("insufficient gas for floor cost, remaining: %d", need)
+ }
if st.evm.Config.Tracer.HasGasHook() {
st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor)
}
}
- if peakGasUsed < floorDataGas {
- peakGasUsed = floorDataGas
- }
+ peakGasUsed = max(peakGasUsed, floorDataGas)
+ peakRegular = max(peakRegular, floorDataGas)
}
- // Return gas to the user
- st.returnGas()
- // Return gas to the gas pool
+ returned := st.returnGas()
if rules.IsAmsterdam {
- // Refund is excluded for returning
- err = st.gp.ReturnGas(st.initialBudget.RegularGas-peakGasUsed, st.gasUsed())
+ // EIP-8037: 2D gas accounting for Amsterdam.
+ // st.gasRemaining.UsedRegularGas / UsedStateGas already include both
+ // the intrinsic charge (from st.gasRemaining.Charge(cost) above) and
+ // the per-frame exec contributions absorbed from evm.Call / evm.Create.
+ //
+ // UsedStateGas should never become negative in the top-most frame, since
+ // state gas refunds only occur when state creation is reverted within the
+ // same transaction, while clearing pre-existing state is never refunded.
+ var txState uint64
+ if st.gasRemaining.UsedStateGas >= 0 {
+ txState = uint64(st.gasRemaining.UsedStateGas)
+ } else {
+ log.Error("Negative top-most frame state gas usage", "amount", st.gasRemaining.UsedStateGas)
+ }
+ if err := st.gp.ChargeGasAmsterdam(peakRegular, txState, st.gasUsed()); err != nil {
+ return nil, err
+ }
} else {
- // Refund is included for returning
- err = st.gp.ReturnGas(st.gasRemaining.RegularGas, st.gasUsed())
- }
- if err != nil {
- return nil, err
+ if err = st.gp.ChargeGasLegacy(returned, st.gasUsed()); err != nil {
+ return nil, err
+ }
}
effectiveTip := msg.GasPrice
if rules.IsLondon {
@@ -713,7 +867,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
// validateAuthorization validates an EIP-7702 authorization against the state.
-func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) {
+func (st *StateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) {
// Verify chain ID is null or equal to current chain ID.
if !auth.ChainID.IsZero() && auth.ChainID.CmpBig(st.evm.ChainConfig().ChainID) != 0 {
return authority, ErrAuthorizationWrongChainID
@@ -743,35 +897,114 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio
return authority, nil
}
+// applyAuthorizations applies every EIP-7702 code delegation in the tx and,
+// under EIP-8037, reconciles the per-authority creation budget so that the
+// total AuthorizationCreationSize state gas charged matches the net change
+// in on-disk delegation bytes.
+//
+// Invalid authorizations are silently skipped (their auth-base intrinsic
+// state gas remains charged, matching the pre-existing behavior).
+func (st *StateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) {
+ if len(auths) == 0 {
+ return
+ }
+ // Under EIP-8037 each authority can be billed at most one
+ // AuthorizationCreationSize. applyAuthorization records authorities it
+ // has billed; we reconcile after the loop by refunding any creation that
+ // was billed but whose final delegation state in this tx ended up empty
+ // (e.g., 0→a→0).
+ var billed map[common.Address]struct{}
+ if rules.IsAmsterdam {
+ billed = make(map[common.Address]struct{})
+ }
+ for _, auth := range auths {
+ // Errors are ignored — invalid authorizations are simply skipped.
+ st.applyAuthorization(rules, &auth, billed)
+ }
+ // End-of-loop reconciliation: a billed creation whose authority is no
+ // longer delegated wrote zero net bytes to disk, so the auth-base
+ // intrinsic state gas should not be retained.
+ for authority := range billed {
+ if _, isDelegated := types.ParseDelegation(st.state.GetCode(authority)); !isDelegated {
+ st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte)
+ }
+ }
+}
+
// applyAuthorization applies an EIP-7702 code delegation to the state.
-func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error {
+//
+// authBilledCreations, when non-nil, tracks the set of authorities for which
+// this tx has been billed one AuthorizationCreationSize charge (the per-
+// authority "first creation" budget). The caller is expected to do an
+// end-of-loop pass over this set and refund any entry whose final delegation
+// state ended up empty.
+func (st *StateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, authBilledCreations map[common.Address]struct{}) error {
authority, err := st.validateAuthorization(auth)
if err != nil {
return err
}
-
// If the account already exists in state, refund the new account cost
// charged in the intrinsic calculation.
if st.state.Exist(authority) {
- st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas)
+ if rules.IsAmsterdam {
+ // EIP-8037: refund account creation state gas to the reservoir.
+ st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
+ } else {
+ st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas)
+ }
+ }
+ prevDelegation, isDelegated := types.ParseDelegation(st.state.GetCode(authority))
+ if rules.IsAmsterdam {
+ // EIP-8037: refund the auth-base state gas unless this auth is the
+ // first one in this tx to write delegation bytes to an authority
+ // whose committed code was empty. Refund when ANY of:
+ //
+ // - the authority was already delegated at the start of the tx
+ // (the 23 bytes are already accounted for in committed state,
+ // and any auth against it just re-writes them);
+ //
+ // - the auth is no-op / clearing (auth.Address == 0) — no bytes
+ // are written in this step at all;
+ //
+ // - we have already billed a creation for this authority in
+ // this tx (per-authority creation budget is 1).
+ //
+ // Modeling it this way mirrors the SSTORE "reset to original"
+ // pattern (EIP-2200 / EIP-3529) and avoids both the undercount in
+ // a→0→b (committed had delegation, second auth missed the refund)
+ // and the overcount in 0→a→0→c (each later auth was previously
+ // billed as a fresh creation). The remaining 0→a→0 case — a
+ // creation is billed and then undone within the same auth list —
+ // is handled by the caller's end-of-loop adjustment over
+ // authBilledCreations.
+ _, committedDelegated := types.ParseDelegation(st.state.GetCommittedCode(authority))
+ _, alreadyBilled := authBilledCreations[authority]
+ if committedDelegated || alreadyBilled || auth.Address == (common.Address{}) {
+ st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte)
+ } else {
+ authBilledCreations[authority] = struct{}{}
+ }
}
// Update nonce and account code.
st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization)
+
+ // Delegation to zero address means clear.
if auth.Address == (common.Address{}) {
- // Delegation to zero address means clear.
- st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear)
+ if isDelegated {
+ st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear)
+ }
return nil
}
-
- // Otherwise install delegation to auth.Address.
- st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization)
-
+ // Install delegation to auth.Address if the delegation changed
+ if !isDelegated || auth.Address != prevDelegation {
+ st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization)
+ }
return nil
}
// calcRefund computes refund counter, capped to a refund quotient.
-func (st *stateTransition) calcRefund() vm.GasBudget {
+func (st *StateTransition) calcRefund() uint64 {
var refund uint64
if !st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
// Before EIP-3529: refunds were capped to gasUsed / 2
@@ -789,29 +1022,28 @@ func (st *stateTransition) calcRefund() vm.GasBudget {
st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxRefunds)
}
- return vm.NewGasBudget(refund)
+ return refund
}
-// returnGas returns ETH for remaining gas,
-// exchanged at the original rate.
-func (st *stateTransition) returnGas() {
- remaining := uint256.NewInt(st.gasRemaining.RegularGas)
+// returnGas returns ETH for remaining gas, exchanged at the original rate.
+func (st *StateTransition) returnGas() uint64 {
+ gas := st.gasRemaining.RegularGas + st.gasRemaining.StateGas
+ remaining := uint256.NewInt(gas)
remaining.Mul(remaining, st.msg.GasPrice)
st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn)
- if st.gasRemaining.RegularGas > 0 && st.evm.Config.Tracer.HasGasHook() {
- after := st.gasRemaining
- after.RegularGas = 0
- st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned)
+ if !st.gasRemaining.IsZero() && st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), tracing.Gas{}, tracing.GasChangeTxLeftOverReturned)
}
+ return gas
}
// gasUsed returns the amount of gas used up by the state transition.
-func (st *stateTransition) gasUsed() uint64 {
+func (st *StateTransition) gasUsed() uint64 {
return st.gasRemaining.Used(st.initialBudget)
}
// blobGasUsed returns the amount of blob gas used by the message.
-func (st *stateTransition) blobGasUsed() uint64 {
+func (st *StateTransition) blobGasUsed() uint64 {
return uint64(len(st.msg.BlobHashes) * params.BlobTxBlobGasPerBlob)
}
diff --git a/core/state_transition_test.go b/core/state_transition_test.go
index 8aab016123e6..be2de7f5114e 100644
--- a/core/state_transition_test.go
+++ b/core/state_transition_test.go
@@ -155,50 +155,50 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028 bool
isEIP3860 bool
isAmsterdam bool
- want uint64
+ want vm.GasCosts
}{
{
name: "frontier/empty-call",
- want: params.TxGas,
+ want: vm.GasCosts{RegularGas: params.TxGas},
},
{
name: "frontier/contract-creation-pre-homestead",
creation: true,
isHomestead: false,
// pre-homestead, contract creation still uses TxGas
- want: params.TxGas,
+ want: vm.GasCosts{RegularGas: params.TxGas},
},
{
name: "homestead/contract-creation",
creation: true,
isHomestead: true,
- want: params.TxGasContractCreation,
+ want: vm.GasCosts{RegularGas: params.TxGasContractCreation},
},
{
name: "frontier/non-zero-data",
data: bytes.Repeat([]byte{0xff}, 100),
// 100 nz bytes * 68 (frontier)
- want: params.TxGas + 100*params.TxDataNonZeroGasFrontier,
+ want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasFrontier},
},
{
name: "istanbul/non-zero-data",
data: bytes.Repeat([]byte{0xff}, 100),
isEIP2028: true,
// 100 nz bytes * 16 (post-EIP2028)
- want: params.TxGas + 100*params.TxDataNonZeroGasEIP2028,
+ want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasEIP2028},
},
{
name: "istanbul/zero-data",
data: bytes.Repeat([]byte{0x00}, 100),
isEIP2028: true,
// 100 zero bytes * 4
- want: params.TxGas + 100*params.TxDataZeroGas,
+ want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataZeroGas},
},
{
name: "istanbul/mixed-data",
data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...),
isEIP2028: true,
- want: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028,
+ want: vm.GasCosts{RegularGas: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028},
},
{
name: "shanghai/init-code-word-gas",
@@ -208,7 +208,7 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true,
isEIP3860: true,
// TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2
- want: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas,
+ want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas},
},
{
name: "shanghai/init-code-non-multiple-of-32",
@@ -217,7 +217,7 @@ func TestIntrinsicGas(t *testing.T) {
isHomestead: true,
isEIP2028: true,
isEIP3860: true,
- want: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas,
+ want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas},
},
{
name: "berlin/access-list",
@@ -227,7 +227,7 @@ func TestIntrinsicGas(t *testing.T) {
},
isEIP2028: true,
// 2 addrs * 2400 + 3 keys * 1900
- want: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas,
+ want: vm.GasCosts{RegularGas: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas},
},
{
name: "amsterdam/access-list-extra-cost",
@@ -238,9 +238,9 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true,
isAmsterdam: true,
// base access-list charge + EIP-7981 extra
- want: params.TxGas +
+ want: vm.GasCosts{RegularGas: params.TxGas +
2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas +
- 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost,
+ 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost},
},
{
name: "prague/auth-list",
@@ -250,8 +250,54 @@ func TestIntrinsicGas(t *testing.T) {
{Address: addr1},
},
isEIP2028: true,
- // 3 auths * 25000
- want: params.TxGas + 3*params.CallNewAccountGas,
+ // 3 auths * 25000 (pre-Amsterdam: CallNewAccountGas per auth tuple)
+ want: vm.GasCosts{RegularGas: params.TxGas + 3*params.CallNewAccountGas},
+ },
+ {
+ name: "amsterdam/contract-creation-empty",
+ creation: true,
+ isHomestead: true,
+ isEIP2028: true,
+ isAmsterdam: true,
+ // EIP-8037: creation regular gas is TxGas + CreateGasAmsterdam (not TxGasContractCreation),
+ // and account-creation cost is moved to state gas.
+ want: vm.GasCosts{
+ RegularGas: params.TxGas + params.CreateGasAmsterdam,
+ StateGas: params.AccountCreationSize * params.CostPerStateByte,
+ },
+ },
+ {
+ name: "amsterdam/contract-creation-init-code",
+ data: bytes.Repeat([]byte{0x00}, 64), // 2 words of init code
+ creation: true,
+ isHomestead: true,
+ isEIP2028: true,
+ isEIP3860: true, // Shanghai gates init-code word gas
+ isAmsterdam: true,
+ want: vm.GasCosts{
+ RegularGas: params.TxGas + params.CreateGasAmsterdam +
+ 64*params.TxDataZeroGas + 2*params.InitCodeWordGas,
+ StateGas: params.AccountCreationSize * params.CostPerStateByte,
+ },
+ },
+ {
+ name: "amsterdam/contract-creation-with-access-list",
+ data: bytes.Repeat([]byte{0xff}, 32), // 1 word of non-zero init code
+ accessList: types.AccessList{
+ {Address: addr1, StorageKeys: []common.Hash{key1}},
+ },
+ creation: true,
+ isHomestead: true,
+ isEIP2028: true,
+ isEIP3860: true,
+ isAmsterdam: true,
+ want: vm.GasCosts{
+ RegularGas: params.TxGas + params.CreateGasAmsterdam +
+ 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas +
+ 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
+ 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost,
+ StateGas: params.AccountCreationSize * params.CostPerStateByte,
+ },
},
{
name: "amsterdam/combined",
@@ -264,23 +310,34 @@ func TestIntrinsicGas(t *testing.T) {
},
isEIP2028: true,
isAmsterdam: true,
- want: params.TxGas +
- 100*params.TxDataNonZeroGasEIP2028 +
- 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
- 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost +
- 1*params.CallNewAccountGas,
+ // EIP-8037 splits the auth-tuple charge into regular + state gas:
+ // regular: TxAuthTupleRegularGas (7500) per auth
+ // state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth
+ want: vm.GasCosts{
+ RegularGas: params.TxGas +
+ 100*params.TxDataNonZeroGasEIP2028 +
+ 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
+ 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost +
+ 1*params.TxAuthTupleRegularGas,
+ StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte,
+ },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ rules := params.Rules{
+ IsHomestead: tt.isHomestead,
+ IsIstanbul: tt.isEIP2028,
+ IsShanghai: tt.isEIP3860,
+ IsAmsterdam: tt.isAmsterdam,
+ }
got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList,
- tt.creation, tt.isHomestead, tt.isEIP2028, tt.isEIP3860, tt.isAmsterdam)
+ tt.creation, rules, params.CostPerStateByte)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
- want := vm.GasCosts{RegularGas: tt.want}
- if got != want {
- t.Fatalf("gas mismatch: got %+v, want %+v", got, want)
+ if got != tt.want {
+ t.Fatalf("gas mismatch: got %+v, want %+v", got, tt.want)
}
})
}
diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go
index 6ea3f7ebbfa9..72cbc8a29816 100644
--- a/core/tracing/hooks.go
+++ b/core/tracing/hooks.go
@@ -472,6 +472,10 @@ const (
// transaction data. This change will always be a negative change.
GasChangeTxDataFloor GasChangeReason = 19
+ // GasChangeStateGasRefund represents the amount of pre-charged state gas
+ // refunded back to the state reservoir.
+ GasChangeStateGasRefund GasChangeReason = 20
+
// GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as
// it will be "manually" tracked by a direct emit of the gas change event.
GasChangeIgnored GasChangeReason = 0xFF
diff --git a/core/txpool/validation.go b/core/txpool/validation.go
index c87bba31ac48..5f8463729bb9 100644
--- a/core/txpool/validation.go
+++ b/core/txpool/validation.go
@@ -125,7 +125,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
}
// Ensure the transaction has more gas than the bare minimum needed to cover
// the transaction metadata
- intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
+ intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte)
if err != nil {
return err
}
@@ -138,9 +138,18 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
if err != nil {
return err
}
+ // Make sure the transaction has sufficient gas allowance to
+ // pay the floor cost.
if tx.Gas() < floorDataGas {
return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrFloorDataGas, tx.Gas(), floorDataGas)
}
+ // In Amsterdam, the transaction gas limit is allowed to exceed
+ // params.MaxTxGas, but the calldata floor cost is capped by it.
+ if rules.IsAmsterdam {
+ if max(intrGas.RegularGas, floorDataGas) > params.MaxTxGas {
+ return fmt.Errorf("%w: regular intrisic cost %v, floor: %v", core.ErrFloorDataGas, intrGas.RegularGas, floorDataGas)
+ }
+ }
}
// Ensure the gasprice is high enough to cover the requirement of the calling pool
if tx.GasTipCapIntCmp(opts.MinTip) < 0 {
diff --git a/core/types.go b/core/types.go
index edbfc43db3ee..32f22bc53400 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 2eb5fe93cdde..9845916765da 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 612d2f877772..26d5aaf7d712 100644
--- a/core/types/bal/bal_encoding.go
+++ b/core/types/bal/bal_encoding.go
@@ -395,7 +395,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{
@@ -415,7 +415,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,
@@ -425,7 +425,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,
@@ -435,7 +435,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 000000000000..bb4a97c8d474
--- /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 45c879c80f20..11172af24cae 100644
--- a/core/vm/contract.go
+++ b/core/vm/contract.go
@@ -42,6 +42,8 @@ type Contract struct {
IsDeployment bool
IsSystemCall bool
+ // Gas carries the unified gas state for this frame: running balance,
+ // reservoir, and per-frame usage accumulators. See GasBudget.
Gas GasBudget
value *uint256.Int
}
@@ -113,7 +115,6 @@ func (c *Contract) GetOp(n uint64) OpCode {
if n < uint64(len(c.Code)) {
return OpCode(c.Code[n])
}
-
return STOP
}
@@ -125,9 +126,23 @@ func (c *Contract) Caller() common.Address {
return c.caller
}
-// UseGas attempts the use gas and subtracts it and returns true on success
-func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) {
- prior, ok := c.Gas.Charge(cost)
+// chargeRegular deducts regular gas only, with tracer integration.
+// Returns false on OOG. Delegates the arithmetic to GasBudget.ChargeRegular.
+func (c *Contract) chargeRegular(r uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool {
+ prior, ok := c.Gas.ChargeRegular(r)
+ if !ok {
+ return false
+ }
+ if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
+ }
+ return true
+}
+
+// chargeState deducts state gas (spilling into regular when the reservoir is
+// exhausted), with tracer integration. Returns false on OOG.
+func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool {
+ prior, ok := c.Gas.ChargeState(s)
if !ok {
return false
}
@@ -137,15 +152,42 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G
return true
}
-// RefundGas refunds the leftover gas budget back to the contract.
-func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) {
- prior, changed := c.Gas.Refund(refund)
- if !changed {
- return
+// refundState refunds the pre-charged state gas back to state reservoir.
+func (c *Contract) refundState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
+ prior := c.Gas
+ c.Gas.RefundState(s)
+
+ if s != 0 && logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
+ }
+}
+
+// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas
+// state. Thin wrapper around GasBudget.Absorb with tracer integration.
+func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) {
+ prior := c.Gas
+ c.Gas.Absorb(child)
+ if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
+}
+
+// forwardGas drains `regular` regular gas and the entire state reservoir
+// from this contract's running budget and returns the initial GasBudget for
+// a child frame. The caller's UsedRegularGas is bumped by the forwarded
+// amount so that the absorb-on-return path correctly reclaims the unused
+// portion. Thin wrapper around GasBudget.Forward with tracer integration.
+//
+// Caller must ensure `regular` is no larger than the running balance (the
+// opcode's dynamic gas table is expected to validate that before invoking
+// the opcode handler).
+func (c *Contract) forwardGas(regular uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) GasBudget {
+ prior := c.Gas
+ child := c.Gas.Forward(regular)
if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
+ return child
}
// Address returns the contracts address
diff --git a/core/vm/contracts.go b/core/vm/contracts.go
index 71cfdbc52773..6908ffeba190 100644
--- a/core/vm/contracts.go
+++ b/core/vm/contracts.go
@@ -264,9 +264,8 @@ func ActivePrecompiles(rules params.Rules) []common.Address {
// - any error that occurred
func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, gas GasBudget, logger *tracing.Hooks, rules params.Rules) (ret []byte, remaining GasBudget, err error) {
gasCost := p.RequiredGas(input)
- prior, ok := gas.Charge(GasCosts{RegularGas: gasCost})
+ prior, ok := gas.ChargeRegular(gasCost)
if !ok {
- gas.Exhaust()
return nil, gas, ErrOutOfGas
}
if logger.HasGasHook() {
diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go
index 988cdb91f222..4d28df6a6a17 100644
--- a/core/vm/contracts_fuzz_test.go
+++ b/core/vm/contracts_fuzz_test.go
@@ -37,7 +37,7 @@ func FuzzPrecompiledContracts(f *testing.F) {
return
}
inWant := string(input)
- RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas), nil, params.Rules{})
+ RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas, 0), nil, params.Rules{})
if inHave := string(input); inWant != inHave {
t.Errorf("Precompiled %v modified input data", a)
}
diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go
index e7841c8552e7..c6975bd0a684 100644
--- a/core/vm/contracts_test.go
+++ b/core/vm/contracts_test.go
@@ -100,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) {
in := common.Hex2Bytes(test.Input)
gas := p.RequiredGas(in)
t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) {
- if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}); err != nil {
+ if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}); err != nil {
t.Error(err)
} else if common.Bytes2Hex(res) != test.Expected {
t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res))
@@ -122,7 +122,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) {
gas := test.Gas - 1
t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) {
- _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{})
+ _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{})
if err.Error() != "out of gas" {
t.Errorf("Expected error [out of gas], got [%v]", err)
}
@@ -139,7 +139,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing
in := common.Hex2Bytes(test.Input)
gas := p.RequiredGas(in)
t.Run(test.Name, func(t *testing.T) {
- _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{})
+ _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{})
if err.Error() != test.ExpectedError {
t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err)
}
@@ -170,7 +170,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) {
start := time.Now()
for bench.Loop() {
copy(data, in)
- res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas), nil, params.Rules{})
+ res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas, 0), nil, params.Rules{})
}
elapsed := uint64(time.Since(start))
if elapsed < 1 {
diff --git a/core/vm/eips.go b/core/vm/eips.go
index 33af8fd4fd9d..0fbf27519a62 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -43,6 +43,7 @@ var activators = map[int]func(*JumpTable){
7939: enable7939,
8024: enable8024,
7843: enable7843,
+ 8037: enable8037,
}
// EnableEIP enables the given EIP on the config.
@@ -377,7 +378,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er
code := evm.StateDB.GetCode(addr)
paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64())
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -403,7 +404,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// advanced past this boundary.
contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(GasCosts{RegularGas: wanted}, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.chargeRegular(wanted, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -430,7 +431,7 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc {
if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall {
contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -590,3 +591,14 @@ func enable7843(jt *JumpTable) {
maxStack: maxStack(0, 1),
}
}
+
+// enable8037 enables the multidimensional-metering as specified in EIP-8037.
+func enable8037(jt *JumpTable) {
+ jt[CREATE].constantGas = params.CreateGasAmsterdam
+ jt[CREATE].dynamicGas = gasCreateEip8037
+ jt[CREATE2].constantGas = params.CreateGasAmsterdam
+ jt[CREATE2].dynamicGas = gasCreate2Eip8037
+ jt[CALL].dynamicGas = gasCallEIP8037
+ jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037
+ jt[SSTORE].dynamicGas = gasSStore8037
+}
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 832306b9a0cd..22f5c7ebe392 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -67,6 +67,8 @@ type BlockContext struct {
BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price)
Random *common.Hash // Provides information for PREVRANDAO
SlotNum uint64 // Provides information for SLOTNUM
+
+ CostPerStateByte uint64 // CostPerByte for new state after EIP-8037
}
// TxContext provides the EVM with information about a transaction.
@@ -245,23 +247,29 @@ func isSystemCall(caller common.Address) bool {
// parameters. It also handles any necessary value transfer required and takse
// the necessary steps to create accounts and reverses the state in case of an
// execution error or failed value transfer.
-func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
+func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) {
// Capture the tracer start/end events in debug mode
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, CALL, caller, addr, input, gas.RegularGas, value.ToBig())
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
+ evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig())
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
- return nil, gas, ErrDepth
+ return nil, gas.Preserved(), ErrDepth
}
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
+ return nil, gas.Preserved(), ErrInsufficientBalance
}
snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr)
@@ -275,16 +283,15 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
// hash leaf to the access list, then account creation will proceed unimpaired.
// Thus, only pay for the creation of the code hash leaf here.
wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false)
- if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok {
+ if _, ok := gas.ChargeRegular(wgas); !ok {
evm.StateDB.RevertToSnapshot(snapshot)
- gas.Exhaust()
- return nil, gas, ErrOutOfGas
+ return nil, gas.ExitHalt(), ErrOutOfGas
}
}
if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() {
// Calling a non-existing account, don't do anything.
- return nil, gas, nil
+ return nil, gas.Preserved(), nil
}
evm.StateDB.CreateAccount(addr)
}
@@ -311,22 +318,20 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
gas = contract.Gas
}
}
- // When an error was returned by the EVM or when setting the creation code
- // above we revert to the snapshot and consume any gas remaining. Additionally,
- // when we're in homestead this also counts for code storage gas errors.
+
+ // Calculate the remaining gas at the end of frame
+ exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
+
+ // Drain the leftover regular gas if unexceptional halt occurs
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
}
- // TODO: consider clearing up unused snapshots:
- //} else {
- // evm.StateDB.DiscardSnapshot(snapshot)
}
- return ret, gas, err
+ return ret, exitGas, err
}
// CallCode executes the contract associated with the addr with the given input
@@ -336,24 +341,27 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
//
// CallCode differs from Call in the sense that it executes the given address'
// code with the caller as context.
-func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
+func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas.RegularGas, value.ToBig())
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
+ evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas, value.ToBig())
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
- return nil, gas, ErrDepth
+ return nil, gas.Preserved(), 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
- // Note although it's noop to transfer X ether to caller itself. But
- // if caller doesn't have enough balance, it would be an error to allow
- // over-charging itself. So the check here is necessary.
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
- return nil, gas, ErrInsufficientBalance
+ return nil, gas.Preserved(), ErrInsufficientBalance
}
var snapshot = evm.StateDB.Snapshot()
@@ -368,16 +376,20 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
}
+
+ // Calculate the remaining gas at the end of frame
+ exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
+
+ // Drain the leftover regular gas if unexceptional halt occurs
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
}
}
- return ret, gas, err
+ return ret, exitGas, err
}
// DelegateCall executes the contract associated with the addr with the given input
@@ -385,18 +397,18 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
//
// DelegateCall differs from CallCode in the sense that it executes the given address'
// code with the caller as context and the caller is set to the caller of the caller.
-func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
+func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
// DELEGATECALL inherits value from parent call
- evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas.RegularGas, value.ToBig())
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
+ evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas, value.ToBig())
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
- return nil, gas, ErrDepth
+ return nil, gas.Preserved(), ErrDepth
}
var snapshot = evm.StateDB.Snapshot()
@@ -404,41 +416,42 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else {
- // Initialise a new contract and make initialise the delegate values
- //
- // Note: The value refers to the original value from the parent call.
contract := NewContract(originCaller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
}
+
+ // Calculate the remaining gas at the end of frame
+ exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
+
+ // Drain the leftover regular gas if unexceptional halt occurs
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
}
}
- return ret, gas, err
+ return ret, exitGas, err
}
// StaticCall executes the contract associated with the addr with the given input
// as parameters while disallowing any modifications to the state during the call.
// Opcodes that attempt to perform such modifications will result in exceptions
// instead of performing the modifications.
-func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, leftOverGas GasBudget, err error) {
+func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, result GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas.RegularGas, nil)
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
+ evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas, nil)
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
- return nil, gas, ErrDepth
+ return nil, gas.Preserved(), ErrDepth
}
// We take a snapshot here. This is a bit counter-intuitive, and could probably be skipped.
// However, even a staticcall is considered a 'touch'. On mainnet, static calls were introduced
@@ -456,58 +469,59 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else {
- // Initialise a new contract and set the code that is to be used by the EVM.
- // The contract is a scoped environment for this execution context only.
contract := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
-
- // When an error was returned by the EVM or when setting the creation code
- // above we revert to the snapshot and consume any gas remaining. Additionally
- // when we're in Homestead this also counts for code storage gas errors.
ret, err = evm.Run(contract, input, true)
gas = contract.Gas
}
+
+ // Calculate the remaining gas at the end of frame
+ exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
}
}
- return ret, gas, err
+ return ret, exitGas, err
}
// create creates a new contract using code as deployment code.
-func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas GasBudget, err error) {
- if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, typ, caller, address, code, gas.RegularGas, value.ToBig())
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
- }
+func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, err error) {
// Depth check execution. Fail if we're trying to execute above the
// limit.
+ var nonce uint64
if evm.depth > int(params.CallCreateDepth) {
- return nil, common.Address{}, gas, ErrDepth
+ err = ErrDepth
+ } else if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
+ err = ErrInsufficientBalance
+ } else {
+ nonce = evm.StateDB.GetNonce(caller)
+ if nonce+1 < nonce {
+ err = ErrNonceUintOverflow
+ }
}
- if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
- return nil, common.Address{}, gas, ErrInsufficientBalance
+ if err == nil {
+ evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator)
+ }
+ if evm.Config.Tracer != nil {
+ evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig())
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
- nonce := evm.StateDB.GetNonce(caller)
- if nonce+1 < nonce {
- return nil, common.Address{}, gas, ErrNonceUintOverflow
+ if err != nil {
+ return nil, common.Address{}, gas.Preserved(), err
}
- evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator)
// 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 {
- gas.Exhaust()
- return nil, common.Address{}, gas, ErrOutOfGas
+ return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas
}
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
@@ -528,11 +542,13 @@ 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()
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
- return nil, common.Address{}, gas, ErrContractAddressCollision
+ // EIP-8037 collision rule: the state reservoir is fully preserved on
+ // address collision while regular gas is burnt.
+ return nil, common.Address{}, halt, ErrContractAddressCollision
}
// Create a new account on the state only if the object was not present.
// It might be possible the contract code is deployed to a pre-existent
@@ -554,8 +570,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 {
- gas.Exhaust()
- return nil, common.Address{}, gas, ErrOutOfGas
+ return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas
}
prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
if evm.Config.Tracer.HasGasHook() {
@@ -574,13 +589,23 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
contract.IsDeployment = true
ret, 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)
if err != ErrExecutionReverted {
- contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution)
+ }
}
+ return ret, address, exit, err
}
- return ret, address, contract.Gas, err
+ // Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved).
+ // Both packaged as a success-form GasBudget.
+ return ret, address, contract.Gas.ExitSuccess(), err
}
// initNewContract runs a new contract's creation code, performs checks on the
@@ -591,27 +616,43 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
return ret, err
}
- // Check whether the max code size has been exceeded, assign err if the case.
- if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
- return ret, 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
}
-
- if !evm.chainRules.IsEIP4762 {
- createDataGas := uint64(len(ret)) * params.CreateDataGas
- if !contract.UseGas(GasCosts{RegularGas: createDataGas}, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
+ 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
+ }
+ if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
+ return ret, 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
+ }
+ // Charge regular gas (hash cost) before state gas (code-deposit cost).
+ regularCost := toWordSize(uint64(len(ret))) * params.Keccak256WordGas
+ if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
+ return ret, ErrCodeStoreOutOfGas
+ }
+ stateCost := uint64(len(ret)) * evm.Context.CostPerStateByte
+ if !contract.chargeState(stateCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
return ret, ErrCodeStoreOutOfGas
}
} else {
- consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas)
- contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
- if len(ret) > 0 && (consumed < wanted) {
+ createDataCost := uint64(len(ret)) * params.CreateDataGas
+ if !contract.chargeRegular(createDataCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
return ret, ErrCodeStoreOutOfGas
}
+ if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
+ return ret, err
+ }
}
if len(ret) > 0 {
@@ -621,7 +662,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
}
// Create creates a new contract using code as deployment code.
-func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) {
+func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) {
contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller))
return evm.create(caller, code, gas, value, contractAddr, CREATE)
}
@@ -630,7 +671,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value
//
// The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:]
// instead of the usual sender-and-nonce-hash as the address where the contract is initialized at.
-func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) {
+func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) {
inithash := crypto.Keccak256Hash(code)
contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:])
return evm.create(caller, code, gas, endowment, contractAddr, CREATE2)
@@ -668,22 +709,20 @@ func (evm *EVM) resolveCodeHash(addr common.Address) common.Hash {
// ChainConfig returns the environment's chain configuration
func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig }
-func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas uint64, value *big.Int) {
+func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas GasBudget, value *big.Int) {
tracer := evm.Config.Tracer
if tracer.OnEnter != nil {
- tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value)
+ tracer.OnEnter(depth, byte(typ), from, to, input, startGas.RegularGas, value)
}
if tracer.HasGasHook() {
- initial := NewGasBudget(startGas)
- tracer.EmitGasChange(tracing.Gas{}, initial.AsTracing(), tracing.GasChangeCallInitialBalance)
+ tracer.EmitGasChange(tracing.Gas{}, startGas.AsTracing(), tracing.GasChangeCallInitialBalance)
}
}
-func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) {
+func (evm *EVM) captureEnd(depth int, startGas GasBudget, leftOverGas GasBudget, ret []byte, err error) {
tracer := evm.Config.Tracer
- if leftOverGas != 0 && tracer.HasGasHook() {
- leftover := NewGasBudget(leftOverGas)
- tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned)
+ if !leftOverGas.IsZero() && tracer.HasGasHook() {
+ tracer.EmitGasChange(leftOverGas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned)
}
var reverted bool
if err != nil {
@@ -693,7 +732,7 @@ func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret [
reverted = false
}
if tracer.OnExit != nil {
- tracer.OnExit(depth, ret, startGas-leftOverGas, VMErrorFromErr(err), reverted)
+ tracer.OnExit(depth, ret, startGas.RegularGas-leftOverGas.RegularGas, VMErrorFromErr(err), reverted)
}
}
diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go
index 046311f9cc1f..7ce9e7ca8d8e 100644
--- a/core/vm/gas_table.go
+++ b/core/vm/gas_table.go
@@ -291,10 +291,19 @@ var (
gasMLoad = pureMemoryGascost
gasMStore8 = pureMemoryGascost
gasMStore = pureMemoryGascost
- gasCreate = pureMemoryGascost
)
+func gasCreate(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ if evm.readOnly {
+ return GasCosts{}, ErrWriteProtection
+ }
+ return pureMemoryGascost(evm, contract, stack, mem, memorySize)
+}
+
func gasCreate2(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
@@ -313,6 +322,9 @@ func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memoryS
}
func gasCreateEip3860(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
@@ -331,7 +343,11 @@ func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
}
return GasCosts{RegularGas: gas}, nil
}
+
func gasCreate2Eip3860(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
@@ -384,17 +400,17 @@ var (
gasStaticCall = makeCallVariantGasCost(gasStaticCallIntrinsic)
)
-func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
+func makeCallVariantGasCost(intrinsicFunc intrinsicGasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
- evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.back(0))
+ evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic, stack.back(0))
if err != nil {
return GasCosts{}, err
}
- gas, overflow := math.SafeAdd(intrinsic.RegularGas, evm.callGasTemp)
+ gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp)
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
@@ -402,19 +418,19 @@ func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
}
}
-func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
transfersValue = !stack.back(2).IsZero()
address = common.Address(stack.back(1).Bytes20())
)
if evm.readOnly && transfersValue {
- return GasCosts{}, ErrWriteProtection
+ return 0, ErrWriteProtection
}
// Stateless check
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
- return GasCosts{}, err
+ return 0, err
}
var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 {
@@ -422,12 +438,12 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
}
var overflow bool
if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
- return GasCosts{}, ErrGasUintOverflow
+ return 0, ErrGasUintOverflow
}
// Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps.
if contract.Gas.RegularGas < gas {
- return GasCosts{}, ErrOutOfGas
+ return 0, ErrOutOfGas
}
// Stateful check
var stateGas uint64
@@ -439,56 +455,87 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
stateGas += params.CallNewAccountGas
}
if gas, overflow = math.SafeAdd(gas, stateGas); overflow {
- return GasCosts{}, ErrGasUintOverflow
+ return 0, ErrGasUintOverflow
}
- return GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
-func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+// regularGasCall8037 is the intrinsic gas calculator for CALL in Amsterdam.
+// It computes memory expansion + value transfer gas but excludes new account
+// creation, which is handled as state gas by the wrapper.
+func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ var (
+ gas uint64
+ transfersValue = !stack.back(2).IsZero()
+ )
+ if evm.readOnly && transfersValue {
+ return 0, ErrWriteProtection
+ }
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
- return GasCosts{}, err
+ return 0, err
+ }
+ var transferGas uint64
+ if transfersValue && !evm.chainRules.IsEIP4762 {
+ transferGas = params.CallValueTransferGas
+ }
+ var overflow bool
+ if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
+ return 0, ErrGasUintOverflow
+ }
+ return gas, nil
+}
+
+func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ memoryGas, err := memoryGasCost(mem, memorySize)
+ if err != nil {
+ return 0, err
}
var (
- gas uint64
- overflow bool
+ gas uint64
+ overflow bool
+ transfersValue = !stack.back(2).IsZero()
)
- if stack.back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 {
- gas += params.CallValueTransferGas
+ if transfersValue {
+ if !evm.chainRules.IsEIP4762 {
+ gas += params.CallValueTransferGas
+ }
}
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
- return GasCosts{}, ErrGasUintOverflow
+ return 0, ErrGasUintOverflow
}
- return GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
-func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
- return GasCosts{}, err
+ return 0, err
}
- return GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
-func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
- return GasCosts{}, err
+ return 0, err
}
- return GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
}
-
var gas uint64
// EIP150 homestead gas reprice fork:
if evm.chainRules.IsEIP150 {
gas = params.SelfdestructGasEIP150
- var address = common.Address(stack.back(0).Bytes20())
+ if gas > contract.Gas.RegularGas {
+ return GasCosts{RegularGas: gas}, nil
+ }
+ var address = common.Address(stack.back(0).Bytes20())
if evm.chainRules.IsEIP158 {
// if empty and transfers value
if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
@@ -504,3 +551,174 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
}
return GasCosts{RegularGas: gas}, nil
}
+
+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
+ words := (size + 31) / 32
+ wordGas := params.InitCodeWordGas * words
+ stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte
+ return GasCosts{
+ RegularGas: gas + wordGas,
+ StateGas: stateGas,
+ }, nil
+}
+
+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
+ words := (size + 31) / 32
+
+ // CREATE2 charges both InitCodeWordGas (EIP-3860) and Keccak256WordGas
+ // (for address hashing).
+ wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * words
+ stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte
+ return GasCosts{
+ RegularGas: gas + wordGas,
+ StateGas: stateGas,
+ }, nil
+}
+
+// stateGasCall8037 is the stateful gas calculator for CALL in Amsterdam (EIP-8037).
+// It only returns the state-dependent gas (account creation as state gas).
+// Memory gas, transfer gas, and callGas are handled by gasCallStateless and
+// makeCallVariantGasCall.
+func stateGasCall8037(evm *EVM, contract *Contract, stack *Stack) (uint64, error) {
+ var (
+ gas uint64
+ transfersValue = !stack.back(2).IsZero()
+ address = common.Address(stack.back(1).Bytes20())
+ )
+ // TODO(rjl, marius), can EIP8037 implicitly means the EIP158 is also activated?
+ // It's technically possible to skip the EIP158 but very unlikely in practice.
+ if evm.chainRules.IsEIP158 {
+ if transfersValue && evm.StateDB.Empty(address) {
+ gas += params.AccountCreationSize * evm.Context.CostPerStateByte
+ }
+ } else if !evm.StateDB.Exist(address) {
+ gas += params.AccountCreationSize * evm.Context.CostPerStateByte
+ }
+ return gas, nil
+}
+
+func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ if evm.readOnly {
+ return GasCosts{}, ErrWriteProtection
+ }
+ var (
+ gas GasCosts
+ address = common.Address(stack.peek().Bytes20())
+ )
+ if !evm.StateDB.AddressInAccessList(address) {
+ // If the caller cannot afford the cost, this change will be rolled back
+ evm.StateDB.AddAddressToAccessList(address)
+ gas.RegularGas = params.ColdAccountAccessCostEIP2929
+ }
+ // Check we have enough regular gas before we add the address to the BAL
+ if contract.Gas.RegularGas < gas.RegularGas {
+ return gas, nil
+ }
+ // if empty and transfers value
+ if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
+ gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte
+ }
+ return gas, nil
+}
+
+func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ if evm.readOnly {
+ return GasCosts{}, ErrWriteProtection
+ }
+ // If we fail the minimum gas availability invariant, fail (0)
+ if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 {
+ return GasCosts{}, errors.New("not enough gas for reentrancy sentry")
+ }
+ // Gas sentry honoured, do the actual gas calculation based on the stored value
+ var (
+ y, x = stack.back(1), stack.peek()
+ slot = common.Hash(x.Bytes32())
+ current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot)
+ cost GasCosts
+ )
+ // Check slot presence in the access list
+ if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
+ 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())
+
+ if current == value { // noop (1)
+ // EIP 2200 original clause:
+ // return params.SloadGasEIP2200, nil
+ return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS
+ }
+ if original == current {
+ if original == (common.Hash{}) { // create slot (2.1.1)
+ return GasCosts{
+ RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929,
+ StateGas: params.StorageCreationSize * evm.Context.CostPerStateByte,
+ }, nil
+ }
+ if value == (common.Hash{}) { // delete slot (2.1.2b)
+ evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529)
+ }
+ // EIP-2200 original clause:
+ // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2)
+ return GasCosts{RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929}, nil // write existing slot (2.1.2)
+ }
+ if original != (common.Hash{}) {
+ if current == (common.Hash{}) { // recreate slot (2.2.1.1)
+ evm.StateDB.SubRefund(params.SstoreClearsScheduleRefundEIP3529)
+ } else if value == (common.Hash{}) { // delete slot (2.2.1.2)
+ evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529)
+ }
+ }
+ if original == value {
+ if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1)
+ // EIP-8037 point (2): refund state gas directly to the reservoir
+ // at the SSTORE restoration point (0→x→0 in same tx); not to the
+ // refund counter, which is capped at gas_used/5.
+ contract.Gas.RefundState(params.StorageCreationSize * evm.Context.CostPerStateByte)
+
+ // Regular portion of the refund still goes through the refund counter.
+ evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929)
+ } else { // reset to original existing slot (2.2.2.2)
+ // EIP 2200 Original clause:
+ // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200)
+ // - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST)
+ // - SLOAD_GAS redefined as WARM_STORAGE_READ_COST
+ // Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST
+ evm.StateDB.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929)
+ }
+ }
+ // EIP-2200 original clause:
+ //return params.SloadGasEIP2200, nil // dirty update (2.2)
+ return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2)
+}
diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go
index 16ce651a7d32..803ef3110180 100644
--- a/core/vm/gas_table_test.go
+++ b/core/vm/gas_table_test.go
@@ -97,12 +97,12 @@ func TestEIP2200(t *testing.T) {
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
}
evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})
- initialGas := NewGasBudget(tt.gaspool)
- _, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int))
+ initialGas := NewGasBudget(tt.gaspool, 0)
+ _, result, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int))
if !errors.Is(err, tt.failure) {
t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure)
}
- if used := leftOver.Used(initialGas); used != tt.used {
+ if used := result.Used(initialGas); used != tt.used {
t.Errorf("test %d: gas used mismatch: have %v, want %v", i, used, tt.used)
}
if refund := evm.StateDB.GetRefund(); refund != tt.refund {
@@ -157,12 +157,12 @@ func TestCreateGas(t *testing.T) {
}
evm := NewEVM(vmctx, statedb, chainConfig, config)
- initialGas := NewGasBudget(uint64(testGas))
- ret, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int))
+ initialGas := NewGasBudget(uint64(testGas), 0)
+ ret, result, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int))
if err != nil {
return false
}
- gasUsed = leftOver.Used(initialGas)
+ gasUsed = result.Used(initialGas)
if len(ret) != 32 {
t.Fatalf("test %d: expected 32 bytes returned, have %d", i, len(ret))
}
diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go
index ed938ae41f9a..0ae36ba12661 100644
--- a/core/vm/gascosts.go
+++ b/core/vm/gascosts.go
@@ -22,9 +22,8 @@ import (
"github.com/ethereum/go-ethereum/core/tracing"
)
-// GasCosts denotes a vector of gas costs in the
-// multidimensional metering paradigm. It represents the cost
-// charged by an individual operation.
+// GasCosts denotes a vector of gas costs in the multidimensional metering
+// paradigm. It represents the cost charged by an individual operation.
type GasCosts struct {
RegularGas uint64
StateGas uint64
@@ -40,67 +39,274 @@ func (g GasCosts) String() string {
return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas)
}
-// GasBudget denotes a vector of remaining gas allowances available
-// for EVM execution in the multidimensional metering paradigm.
-// Unlike GasCosts which represents the price of an operation,
-// GasBudget tracks how much gas is left to spend.
+// GasBudget is the unified gas-state structure used throughout the EVM.
+// It carries two pairs of fields:
+//
+// - RegularGas / StateGas: the running balance during execution, or the
+// leftover balance the caller must absorb after a sub-call.
+// - UsedRegularGas / UsedStateGas: per-frame accumulators tracking gross
+// consumption. UsedStateGas is signed so it can be decremented by inline
+// state-gas refunds (e.g., SSTORE 0->A->0).
+//
+// The same struct serves three roles:
+//
+// - During execution: Charge / ChargeRegular / ChargeState / RefundState
+// and RefundRegular mutate the running balance and the usage accumulators
+// in lockstep.
+//
+// - At frame exit: Preserved / ExitSuccess / ExitRevert / ExitHalt /
+// Exit produce a new GasBudget in "leftover" form that packages
+// the result for the caller.
+//
+// - At absorption: the caller's Absorb method merges the child's leftover
+// budget into its own running budget.
type GasBudget struct {
- RegularGas uint64 // The leftover gas for execution and state gas usage
- StateGas uint64 // The state gas reservoir
+ RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb)
+ StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb)
+ UsedRegularGas uint64 // gross regular gas consumed in this frame
+ UsedStateGas int64 // signed net state-gas consumed in this frame
}
-// NewGasBudget creates a GasBudget with the given initial regular gas allowance.
-func NewGasBudget(gas uint64) GasBudget {
- return GasBudget{RegularGas: gas}
+// NewGasBudget initializes a fresh GasBudget for execution / forwarding,
+// with both usage accumulators set to zero.
+func NewGasBudget(regular, state uint64) GasBudget {
+ return GasBudget{RegularGas: regular, StateGas: state}
}
-// Used returns the amount of regular gas consumed so far.
+// Used returns the total scalar gas consumed relative to an initial budget
+// (= (initial.regular + initial.state) − (current.regular + current.state)).
+// This is the payment scalar (EIP-8037's tx_gas_used_before_refund).
+//
+// TODO(rjl493456442) the total used gas can be calculated via g.UsedRegularGas
+// and g.UsedStateGas.
func (g GasBudget) Used(initial GasBudget) uint64 {
- return initial.RegularGas - g.RegularGas
-}
-
-// Exhaust sets all remaining gas to zero, preserving the initial amount
-// for usage tracking.
-func (g *GasBudget) Exhaust() {
- g.RegularGas = 0
- g.StateGas = 0
+ return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas)
}
-func (g *GasBudget) Copy() GasBudget {
- return GasBudget{RegularGas: g.RegularGas, StateGas: g.StateGas}
+// Copy returns a deep copy of the budget.
+func (g GasBudget) Copy() GasBudget {
+ return g
}
-// String returns a visual representation of the gas budget vector.
+// String returns a visual representation of the budget.
func (g GasBudget) String() string {
- return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas)
+ return fmt.Sprintf("<%v,%v,used=<%v,%v>>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas)
}
-// CanAfford reports whether the budget has sufficient gas to cover the cost.
+// CanAfford reports whether the running balance can cover the given cost.
+// State-gas charges that exceed the reservoir spill into regular gas.
func (g GasBudget) CanAfford(cost GasCosts) bool {
- return g.RegularGas >= cost.RegularGas
+ if g.RegularGas < cost.RegularGas {
+ return false
+ }
+ if cost.StateGas > g.StateGas {
+ spillover := cost.StateGas - g.StateGas
+ if spillover > g.RegularGas-cost.RegularGas {
+ return false
+ }
+ }
+ return true
}
-// Charge deducts the given gas cost from the budget. It returns the
-// pre-charge budget and false if the budget does not have sufficient
-// gas to cover the cost.
+// Charge deducts a combined regular+state cost from the running balance and
+// updates the usage accumulators. State-gas in excess of the reservoir spills
+// into regular_gas.
func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
prior := *g
- if g.RegularGas < cost.RegularGas {
+ if !g.CanAfford(cost) {
return prior, false
}
+ // Charge regular gas
g.RegularGas -= cost.RegularGas
- return prior, true
-}
+ g.UsedRegularGas += cost.RegularGas
-// Refund adds the given gas budget back. It returns the pre-refund budget
-// and whether the budget was actually changed.
-func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) {
- prior := *g
- g.RegularGas += other.RegularGas
- return prior, g.RegularGas != prior.RegularGas
+ // Charge state gas
+ if cost.StateGas > g.StateGas {
+ spillover := cost.StateGas - g.StateGas
+ g.StateGas = 0
+ g.RegularGas -= spillover
+ } else {
+ g.StateGas -= cost.StateGas
+ }
+ g.UsedStateGas += int64(cost.StateGas)
+ return prior, true
}
// AsTracing converts the GasBudget into the tracing-facing Gas vector.
func (g GasBudget) AsTracing() tracing.Gas {
return tracing.Gas{Regular: g.RegularGas, State: g.StateGas}
}
+
+// ChargeRegular is a convenience that deducts a regular-only cost.
+func (g *GasBudget) ChargeRegular(r uint64) (GasBudget, bool) {
+ return g.Charge(GasCosts{RegularGas: r})
+}
+
+// ChargeState is a convenience that deducts a state-only cost (spills to
+// regular when the reservoir is exhausted). Returns false on OOG.
+func (g *GasBudget) ChargeState(s uint64) (GasBudget, bool) {
+ return g.Charge(GasCosts{StateGas: s})
+}
+
+// IsZero returns an indicator if the gas budget has been exhausted.
+func (g *GasBudget) IsZero() bool {
+ return g.RegularGas == 0 && g.StateGas == 0
+}
+
+// RefundState applies an inline state-gas refund (e.g., SSTORE 0->A->0).
+// The reservoir is credited and the signed usage counter is decremented
+// in lockstep, preserving the per-frame invariant:
+//
+// StateGas + UsedStateGas == initialStateGas + spillover_so_far
+//
+// which the revert path relies on for the correct gross refund.
+func (g *GasBudget) RefundState(s uint64) {
+ g.StateGas += s
+ g.UsedStateGas -= int64(s)
+}
+
+// RefundRegular applies an inline regular-gas refund.
+func (g *GasBudget) RefundRegular(s uint64) {
+ g.RegularGas += s
+ g.UsedRegularGas -= s
+}
+
+// Forward drains `regular` regular gas and the entire state reservoir from
+// the parent's running budget and returns the initial GasBudget for a child
+// frame.
+//
+// Used by frame boundaries where the regular forward has NOT been pre-
+// deducted: tx-level dispatch (state_transition) and CREATE / CREATE2. The
+// CALL family pre-deducts the forward via the dynamic gas table for tracer-
+// reporting reasons and therefore constructs its child budget directly.
+//
+// Caller must ensure `regular` does not exceed the running balance and
+// apply any EIP-150 1/64 retention before calling Forward.
+func (g *GasBudget) Forward(regular uint64) GasBudget {
+ g.RegularGas -= regular
+
+ child := GasBudget{
+ RegularGas: regular,
+ StateGas: g.StateGas,
+ }
+ g.StateGas = 0
+ return child
+}
+
+// ForwardAll forwards the parent's full remaining budget (both regular and
+// state) to a child frame. Equivalent to Forward(g.RegularGas) — used at
+// the tx boundary where there is no 1/64 retention.
+func (g *GasBudget) ForwardAll() GasBudget {
+ return g.Forward(g.RegularGas)
+}
+
+// ============================================================================
+// Exit-form constructors. These take a post-execution running budget and
+// produce a new GasBudget in "leftover form" — the value the caller should
+// absorb to update its own state.
+// ============================================================================
+
+// Preserved produces a leftover form with the running balance preserved and
+// usage zeroed. Use this for pre-execution validation failures (depth,
+// balance, EIP-158 zero-value-to-nonexistent) where no execution actually
+// occurred.
+func (g GasBudget) Preserved() GasBudget {
+ return GasBudget{
+ RegularGas: g.RegularGas,
+ StateGas: g.StateGas,
+ // UsedRegularGas / UsedStateGas stay at zero.
+ }
+}
+
+// ExitSuccess produces the leftover form for a successful frame. Inline
+// state-gas refunds have already been folded into StateGas / UsedStateGas
+// during execution; the running budget IS the exit budget on success.
+func (g GasBudget) ExitSuccess() GasBudget {
+ return g
+}
+
+// ExitRevert produces the leftover form for a REVERT exit. Per EIP-8037,
+// the gross state-gas charged in the failing subtree is refunded to the
+// caller's reservoir via the signed-counter formula
+//
+// leftover.StateGas = StateGas + UsedStateGas
+//
+// and UsedStateGas is zeroed because the state effect is routed through
+// StateGas. UsedRegularGas is unchanged.
+func (g GasBudget) ExitRevert() GasBudget {
+ reservoir := int64(g.StateGas) + g.UsedStateGas
+ if reservoir < 0 {
+ // Defensive: invariant guarantees non-negativity. Clamping prevents
+ // a hypothetical bug from sign-extending to a huge uint64.
+ reservoir = 0
+ }
+ return GasBudget{
+ RegularGas: g.RegularGas,
+ StateGas: uint64(reservoir),
+ UsedRegularGas: g.UsedRegularGas,
+ UsedStateGas: 0,
+ }
+}
+
+// ExitHalt produces the leftover form for an exceptional halt. Remaining
+// regular gas is burned into UsedRegularGas; the state dimension follows the
+// same revert-style formula as ExitRevert because the spec routes both halt
+// and revert through incorporate_child_on_error:
+//
+// parent.state_gas_left = parent + child.state_gas_used + child.state_gas_left
+//
+// which in our model means returning `StateGas + UsedStateGas` to the parent
+// and zeroing the per-frame counter.
+func (g GasBudget) ExitHalt() GasBudget {
+ reservoir := int64(g.StateGas) + g.UsedStateGas
+ if reservoir < 0 {
+ reservoir = 0
+ }
+ return GasBudget{
+ RegularGas: 0,
+ StateGas: uint64(reservoir),
+ UsedRegularGas: g.UsedRegularGas + g.RegularGas,
+ UsedStateGas: 0,
+ }
+}
+
+// Exit dispatches on err to the appropriate exit-form constructor
+// for the post-evm.Run path:
+//
+// - err == nil → ExitSuccess
+// - err == ErrExecutionReverted → ExitRevert
+// - any other err → ExitHalt
+//
+// Soft validation failures (occurring BEFORE evm.Run) should call Preserved
+// directly instead of going through this dispatcher.
+func (g GasBudget) Exit(err error) GasBudget {
+ switch {
+ case err == nil:
+ return g.ExitSuccess()
+ case err == ErrExecutionReverted:
+ return g.ExitRevert()
+ default:
+ return g.ExitHalt()
+ }
+}
+
+// Absorb merges a sub-call's leftover GasBudget into this (caller's) running
+// budget.
+//
+// - RegularGas: the child's leftover regular gas flows back to the caller.
+// - UsedRegularGas: the child's own gross regular-gas consumption is added
+// to the caller's accumulator. Combined with the caller having NOT
+// pre-bumped UsedRegularGas by the forwarded amount, this matches the
+// spec's escrow_subcall_regular_gas + incorporate_child_* pattern: only
+// opcode charges count towards regular_gas_used, never state-gas
+// spillover or escrowed forwards.
+// - StateGas: the reservoir is overwritten by the child's leftover (spec's
+// incorporate_child_on_success / on_error formula for state_gas_left).
+// - UsedStateGas: the child's signed net state-gas usage is added to the
+// caller's accumulator (spec's incorporate child gas).
+func (g *GasBudget) Absorb(child GasBudget) {
+ g.RegularGas += child.RegularGas
+ g.UsedRegularGas += child.UsedRegularGas
+ g.StateGas = child.StateGas
+ g.UsedStateGas += child.UsedStateGas
+}
diff --git a/core/vm/instructions.go b/core/vm/instructions.go
index 4b05092cc799..417c7efab91b 100644
--- a/core/vm/instructions.go
+++ b/core/vm/instructions.go
@@ -647,25 +647,21 @@ func opSwap16(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- if evm.readOnly {
- return nil, ErrWriteProtection
- }
var (
value = scope.Stack.pop()
offset, size = scope.Stack.pop(), scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
- gas = scope.Contract.Gas.RegularGas
+ forward = scope.Contract.Gas.RegularGas
)
if evm.chainRules.IsEIP150 {
- gas -= gas / 64
+ forward -= forward / 64
}
// reuse size int for stackvalue
stackvalue := size
- scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
-
- res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudget(gas), &value)
+ child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
+ res, addr, result, suberr := evm.Create(scope.Contract.Address(), input, child, &value)
// Push item on the stack based on the returned error. If the ruleset is
// homestead we must check for CodeStoreOutOfGasError (homestead only
// rule) and treat as an error, if the ruleset is frontier we must
@@ -679,8 +675,10 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
scope.Stack.push(&stackvalue)
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
-
+ scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ 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
return res, nil
@@ -690,24 +688,21 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- if evm.readOnly {
- return nil, ErrWriteProtection
- }
var (
endowment = scope.Stack.pop()
offset, size = scope.Stack.pop(), scope.Stack.pop()
salt = scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
- gas = scope.Contract.Gas.RegularGas
+ forward = scope.Contract.Gas.RegularGas
)
// Apply EIP150
- gas -= gas / 64
- scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
+ forward -= forward / 64
+
// reuse size int for stackvalue
stackvalue := size
- res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudget(gas),
- &endowment, &salt)
+ child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
+ res, addr, result, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt)
// Push item on the stack based on the returned error.
if suberr != nil {
stackvalue.Clear()
@@ -715,8 +710,13 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
stackvalue.SetBytes(addr.Bytes())
}
scope.Stack.push(&stackvalue)
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ // If the creation frame reverts or halts exceptionally, the charged state-gas
+ // is refilled back to the state reservoir in Amsterdam.
+ if evm.chainRules.IsAmsterdam && suberr != nil {
+ scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeStateGasRefund)
+ }
if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer
return res, nil
@@ -743,7 +743,18 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if !value.IsZero() {
gas += params.CallStipend
}
- ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value)
+ // Escrow pattern (spec: escrow_subcall_regular_gas): undo the full
+ // forwarded gas — including the value-transfer stipend — from
+ // UsedRegularGas. The stipend is a free loan to the child and must
+ // not count toward the caller's regular gas usage; the child's own
+ // opcode charges are re-added via Absorb on return.
+ scope.Contract.Gas.UsedRegularGas -= gas
+
+ // Regular gas for the forward was already pre-deducted by the dynamic
+ // gas table (see makeCallVariantGasCallEIP*); only the state reservoir
+ // needs to be handed off to the child here.
+ childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas)
+ ret, result, err := evm.Call(scope.Contract.Address(), toAddr, args, childBudget, &value)
if err != nil {
temp.Clear()
@@ -751,11 +762,16 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
temp.SetOne()
}
stack.push(&temp)
+
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
+ scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ // EIP-8037: CALL does not refund the new-account state_gas on
+ // ErrDepth/ErrInsufficientBalance — the spec only reclaims the forwarded
+ // reservoir; the state_gas charged for new account creation stays in
+ // state_gas_used (see Amsterdam spec call() / generic_call()).
evm.returnData = ret
return ret, nil
@@ -776,8 +792,12 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if !value.IsZero() {
gas += params.CallStipend
}
+ // Escrow pattern: undo the full forwarded gas (including stipend) from
+ // UsedRegularGas; the child's own opcode charges come back via Absorb.
+ scope.Contract.Gas.UsedRegularGas -= gas
- ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value)
+ childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas)
+ ret, result, err := evm.CallCode(scope.Contract.Address(), toAddr, args, childBudget, &value)
if err != nil {
temp.Clear()
} else {
@@ -788,7 +808,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@@ -806,7 +826,10 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Get arguments from the memory.
args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64())
- ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudget(gas), scope.Contract.value)
+ // Undo the forwarded gas from UsedRegularGas to prevent double-charging.
+ scope.Contract.Gas.UsedRegularGas -= gas
+ childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas)
+ ret, result, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, childBudget, scope.Contract.value)
if err != nil {
temp.Clear()
} else {
@@ -816,8 +839,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(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@@ -835,7 +857,10 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Get arguments from the memory.
args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64())
- ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudget(gas))
+ // Undo the forwarded gas from UsedRegularGas to prevent double-charging.
+ scope.Contract.Gas.UsedRegularGas -= gas
+ childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas)
+ ret, result, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, childBudget)
if err != nil {
temp.Clear()
} else {
@@ -846,7 +871,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
- scope.Contract.RefundGas(returnGas, 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/interface.go b/core/vm/interface.go
index a9938c2a2873..dc897f0c69e3 100644
--- a/core/vm/interface.go
+++ b/core/vm/interface.go
@@ -42,6 +42,11 @@ type StateDB interface {
GetCodeHash(common.Address) common.Hash
GetCode(common.Address) []byte
+ // GetCommittedCode returns the contract code at the start of the current
+ // execution, ignoring any in-progress SetCode mutations. Returns nil when
+ // the account had no code prior to this execution.
+ GetCommittedCode(common.Address) []byte
+
// SetCode sets the new code for the address, and returns the previous code, if any.
SetCode(common.Address, []byte, tracing.CodeChangeReason) []byte
GetCodeSize(common.Address) int
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 399432724772..ca33670163a9 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -174,7 +174,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// associated costs.
contractAddr := contract.Address()
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas)
- contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
+ contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -192,10 +192,8 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
// for tracing: this gas consumption event is emitted below in the debug section.
- if contract.Gas.RegularGas < cost {
+ if !contract.chargeRegular(cost, nil, tracing.GasChangeIgnored) {
return nil, ErrOutOfGas
- } else {
- contract.Gas.RegularGas -= cost
}
// All ops with a dynamic memory usage also has a dynamic gas cost.
@@ -224,11 +222,13 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- // for tracing: this gas consumption event is emitted below in the debug section.
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ // EIP-8037: charge regular gas before state gas. The state charge
+ // is a no-op when dynamicCost.StateGas == 0 (e.g., pre-Amsterdam).
+ if !contract.chargeRegular(dynamicCost.RegularGas, nil, tracing.GasChangeIgnored) {
+ return nil, ErrOutOfGas
+ }
+ if !contract.chargeState(dynamicCost.StateGas, nil, tracing.GasChangeIgnored) {
return nil, ErrOutOfGas
- } else {
- contract.Gas.RegularGas -= dynamicCost.RegularGas
}
}
diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go
index 868cb12d04e3..42530b83b72f 100644
--- a/core/vm/interpreter_test.go
+++ b/core/vm/interpreter_test.go
@@ -55,7 +55,7 @@ func TestLoopInterrupt(t *testing.T) {
timeout := make(chan bool)
go func(evm *EVM) {
- _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64), new(uint256.Int))
+ _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64, 0), new(uint256.Int))
errChannel <- err
}(evm)
@@ -85,7 +85,7 @@ func BenchmarkInterpreter(b *testing.B) {
value = uint256.NewInt(0)
stack = newStackForTesting()
mem = NewMemory()
- contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas), nil)
+ contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas, 0), nil)
)
stack.push(uint256.NewInt(123))
stack.push(uint256.NewInt(123))
diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go
index 82fc43ec1327..5cc5e34cedb2 100644
--- a/core/vm/jump_table.go
+++ b/core/vm/jump_table.go
@@ -23,10 +23,14 @@ import (
)
type (
- executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error)
- gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64
+ executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error)
+ gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64
+ intrinsicGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64
// memorySizeFunc returns the required size, and whether the operation overflowed a uint64
memorySizeFunc func(*Stack) (size uint64, overflow bool)
+
+ regularGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error)
+ stateGasFunc func(*EVM, *Contract, *Stack) (uint64, error)
)
type operation struct {
@@ -97,6 +101,7 @@ func newAmsterdamInstructionSet() JumpTable {
instructionSet := newOsakaInstructionSet()
enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode)
enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE)
+ enable8037(&instructionSet) // EIP-8037 (State creation gas cost increase)
return validate(instructionSet)
}
diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go
index 86ac262a9349..67056ecb65d2 100644
--- a/core/vm/operations_acl.go
+++ b/core/vm/operations_acl.go
@@ -168,7 +168,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
evm.StateDB.AddAddressToAccessList(addr)
// Charge the remaining difference here already, to correctly calculate available
// gas for call
- if !contract.UseGas(GasCosts{RegularGas: coldCost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.chargeRegular(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
@@ -245,6 +245,10 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
return GasCosts{}, ErrOutOfGas
}
}
+ if gas > contract.Gas.RegularGas {
+ return GasCosts{RegularGas: gas}, nil
+ }
+
// if empty and transfers value
if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
gas += params.CreateBySelfdestructGas
@@ -262,6 +266,8 @@ var (
gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic)
gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic)
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic)
+
+ innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(regularGasCall8037, stateGasCall8037)
)
func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
@@ -276,7 +282,15 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
}
-func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
+func gasCallEIP8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ transfersValue := !stack.back(2).IsZero()
+ if evm.readOnly && transfersValue {
+ return GasCosts{}, ErrWriteProtection
+ }
+ return innerGasCallEIP8037(evm, contract, stack, mem, memorySize)
+}
+
+func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
eip2929Cost uint64
@@ -295,7 +309,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// Charge the remaining difference here already, to correctly calculate
// available gas for call
- if !contract.UseGas(GasCosts{RegularGas: eip2929Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
@@ -312,7 +326,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps.
// It's an essential safeguard before any stateful check.
- if contract.Gas.RegularGas < intrinsicCost.RegularGas {
+ if contract.Gas.RegularGas < intrinsicCost {
return GasCosts{}, ErrOutOfGas
}
@@ -324,13 +338,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
evm.StateDB.AddAddressToAccessList(target)
eip7702Cost = params.ColdAccountAccessCostEIP2929
}
- if !contract.UseGas(GasCosts{RegularGas: eip7702Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
// Calculate the gas budget for the nested call. The costs defined by
// EIP-2929 and EIP-7702 have already been applied.
- evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.back(0))
+ evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost, stack.back(0))
if err != nil {
return GasCosts{}, err
}
@@ -339,6 +353,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// part of the dynamic gas. This will ensure it is correctly reported to
// tracers.
contract.Gas.RegularGas += eip2929Cost + eip7702Cost
+ // Undo the RegularGasUsed increments from the direct UseGas charges,
+ // since this gas will be re-charged via the returned cost.
+ contract.Gas.UsedRegularGas -= eip2929Cost
+ contract.Gas.UsedRegularGas -= eip7702Cost
// Aggregate the gas costs from all components, including EIP-2929, EIP-7702,
// the CALL opcode itself, and the cost incurred by nested calls.
@@ -349,7 +367,93 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
return GasCosts{}, ErrGasUintOverflow
}
- if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost.RegularGas); overflow {
+ if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow {
+ return GasCosts{}, ErrGasUintOverflow
+ }
+ if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
+ return GasCosts{}, ErrGasUintOverflow
+ }
+ return GasCosts{RegularGas: totalCost}, nil
+ }
+}
+
+// makeCallVariantGasCallEIP8037 creates a call gas function for Amsterdam (EIP-8037).
+// It extends the EIP-7702 pattern with state gas handling and GasUsed tracking.
+// intrinsicFunc computes the regular gas (memory + transfer, no new account creation).
+// stateGasFunc computes the state gas (new account creation as state gas).
+func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc) gasFunc {
+ return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ var (
+ eip2929Cost uint64
+ eip7702Cost uint64
+ addr = common.Address(stack.back(1).Bytes20())
+ )
+ // EIP-2929 cold access check.
+ if !evm.StateDB.AddressInAccessList(addr) {
+ evm.StateDB.AddAddressToAccessList(addr)
+ eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
+ if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ return GasCosts{}, ErrOutOfGas
+ }
+ }
+
+ // Compute regular cost (memory + transfer, no new account creation).
+ regularCost, err := regularFunc(evm, contract, stack, mem, memorySize)
+ if err != nil {
+ return GasCosts{}, err
+ }
+
+ // Charge intrinsic cost directly (regular gas). This must happen
+ // BEFORE state gas to prevent reservoir inflation, and also serves
+ // as the OOG guard before stateful operations.
+ if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallOpCode) {
+ return GasCosts{}, ErrOutOfGas
+ }
+
+ // EIP-7702 delegation check.
+ if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
+ if evm.StateDB.AddressInAccessList(target) {
+ eip7702Cost = params.WarmStorageReadCostEIP2929
+ } else {
+ evm.StateDB.AddAddressToAccessList(target)
+ eip7702Cost = params.ColdAccountAccessCostEIP2929
+ }
+ if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ return GasCosts{}, ErrOutOfGas
+ }
+ }
+
+ // Compute and charge state gas (new account creation) AFTER regular gas.
+ stateGas, err := stateGasFunc(evm, contract, stack)
+ if err != nil {
+ return GasCosts{}, err
+ }
+ if stateGas > 0 {
+ if _, ok := contract.Gas.ChargeState(stateGas); !ok {
+ return GasCosts{}, ErrOutOfGas
+ }
+ }
+
+ // Calculate the gas budget for the nested call (63/64 rule).
+ evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, 0, stack.back(0))
+ if err != nil {
+ return GasCosts{}, err
+ }
+
+ // Temporarily undo direct regular charges for tracer reporting.
+ // The interpreter will charge the returned totalCost.
+ contract.Gas.RegularGas += eip2929Cost + eip7702Cost + regularCost
+ contract.Gas.UsedRegularGas -= eip2929Cost + eip7702Cost + regularCost
+
+ // Aggregate total cost.
+ var (
+ overflow bool
+ totalCost uint64
+ )
+ if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
+ return GasCosts{}, ErrGasUintOverflow
+ }
+ if totalCost, overflow = math.SafeAdd(totalCost, regularCost); overflow {
return GasCosts{}, ErrGasUintOverflow
}
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go
index 4fafdf3a5049..34dec1bd4a7d 100644
--- a/core/vm/runtime/runtime.go
+++ b/core/vm/runtime/runtime.go
@@ -144,15 +144,15 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) {
// set the receiver's (the executing contract) code for execution.
cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified)
// Call the code with the given configuration.
- ret, leftOverGas, err := vmenv.Call(
+ ret, result, err := vmenv.Call(
cfg.Origin,
common.BytesToAddress([]byte("contract")),
input,
- vm.NewGasBudget(cfg.GasLimit),
+ vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err)
}
return ret, cfg.State, err
}
@@ -179,16 +179,16 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) {
// - reset transient storage(eip 1153)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil)
// Call the code with the given configuration.
- code, address, leftOverGas, err := vmenv.Create(
+ code, address, result, err := vmenv.Create(
cfg.Origin,
input,
- vm.NewGasBudget(cfg.GasLimit),
+ vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err)
}
- return code, address, leftOverGas.RegularGas, err
+ return code, address, result.RegularGas, err
}
// Call executes the code given by the contract's address. It will return the
@@ -213,15 +213,15 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er
statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil)
// Call the code with the given configuration.
- ret, leftOverGas, err := vmenv.Call(
+ ret, result, err := vmenv.Call(
cfg.Origin,
address,
input,
- vm.NewGasBudget(cfg.GasLimit),
+ vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err)
}
- return ret, leftOverGas.RegularGas, err
+ return ret, result.RegularGas, err
}
diff --git a/eth/backend.go b/eth/backend.go
index af8b04bda665..fe80d113ccf8 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -280,6 +280,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 b31185a40f90..52038fde3b66 100644
--- a/eth/catalyst/api.go
+++ b/eth/catalyst/api.go
@@ -770,10 +770,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 b51b78e199ad..2a7e66de110f 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"
@@ -216,6 +217,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/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go
index 6570d735755c..2fefa46492d6 100644
--- a/eth/tracers/js/tracer_test.go
+++ b/eth/tracers/js/tracer_test.go
@@ -55,7 +55,7 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo
gasLimit uint64 = 31000
startGas uint64 = 10000
value = uint256.NewInt(0)
- contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas), nil)
+ contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas, 0), nil)
)
evm.SetTxContext(vmctx.txCtx)
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0}
diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go
index decdf588e133..73868d22e031 100644
--- a/eth/tracers/logger/logger_test.go
+++ b/eth/tracers/logger/logger_test.go
@@ -47,7 +47,7 @@ func TestStoreCapture(t *testing.T) {
var (
logger = NewStructLogger(nil)
evm = vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()})
- contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000), nil)
+ contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000, 0), nil)
)
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)}
var index common.Hash
diff --git a/node/node.go b/node/node.go
index 56ecd7d52278..3beade3ed1c9 100644
--- a/node/node.go
+++ b/node/node.go
@@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
+ "github.com/ethereum/go-ethereum/trie/archive"
"github.com/gofrs/flock"
)
@@ -85,6 +86,7 @@ func New(conf *Config) (*Node, error) {
return nil, err
}
conf.DataDir = absdatadir
+ archive.ArchiveDataDir = absdatadir
}
if conf.Logger == nil {
conf.Logger = log.New()
diff --git a/params/protocol_params.go b/params/protocol_params.go
index 3e36b8354751..69e10fa5d9db 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -88,6 +88,7 @@ const (
LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas.
CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction.
Create2Gas uint64 = 32000 // Once per CREATE2 operation
+ CreateGasAmsterdam uint64 = 9000 // Regular gas portion of CREATE in Amsterdam (EIP-8037); state gas is charged separately.
CreateNGasEip4762 uint64 = 1000 // Once per CREATEn operations post-verkle
SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation.
MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL.
@@ -100,6 +101,7 @@ const (
TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list
TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list
TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702
+ TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037
// These have been changed during the course of the chain
CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction.
@@ -196,6 +198,12 @@ const (
// the bound has a small safety margin for system-contract accesses that
// don't consume block gas.
BALItemCost uint64 = 2000
+
+ AccountCreationSize = 120
+ StorageCreationSize = 64
+ AuthorizationCreationSize = 23
+ CostPerStateByte = 1530
+ SystemMaxSStoresPerCall = 16
)
// Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation
diff --git a/tests/block_test.go b/tests/block_test.go
index 0f087967bb68..d309d8167046 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 bece8ae61064..ab0c9084706f 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 b933c9808cda..20c9e8e1c90b 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/state_test.go b/tests/state_test.go
index cf1d4bce4c90..f6ae90c21f48 100644
--- a/tests/state_test.go
+++ b/tests/state_test.go
@@ -325,10 +325,10 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.StartTimer()
start := time.Now()
- initialGas := vm.NewGasBudget(msg.GasLimit)
+ initialGas := vm.NewGasBudget(msg.GasLimit, 0)
// Execute the message.
- _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value)
+ _, result, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value)
if err != nil {
b.Error(err)
return
@@ -337,7 +337,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.StopTimer()
elapsed += uint64(time.Since(start))
refund += state.StateDB.GetRefund()
- gasUsed += leftOverGas.Used(initialGas)
+ gasUsed += result.Used(initialGas)
state.StateDB.RevertToSnapshot(snapshot)
}
diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go
index 572c109f1ea4..91f7d6c3ec68 100644
--- a/tests/transaction_test_util.go
+++ b/tests/transaction_test_util.go
@@ -80,8 +80,8 @@ func (tt *TransactionTest) Run() error {
if err != nil {
return
}
- // Intrinsic gas
- cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
+ // Intrinsic cost
+ cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte)
if err != nil {
return
}
diff --git a/trie/archive/archive.go b/trie/archive/archive.go
new file mode 100644
index 000000000000..d4f4fb23825d
--- /dev/null
+++ b/trie/archive/archive.go
@@ -0,0 +1,95 @@
+// Copyright 2026 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 archive
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// ResolverFn is a callback to resolve expired nodes from an archive file.
+// Given an offset and size, it returns the serialized node data from the archive.
+type ResolverFn func(offset, size uint64) ([]*Record, error)
+
+// OffsetSize is the size of the file offset in bytes.
+const OffsetSize = 8
+
+var (
+ EmptyArchiveRecord = errors.New("empty record") // The archive contained a size-zero record.
+ ErrNoResolver = errors.New("no archive resolver set for expired node") // An expired node is accessed without a resolver.
+)
+
+// Record contains an archive file record. It is not the most optimal
+// structure, since any modification to it will need to be overwritten.
+type Record struct {
+ Path []byte
+ Value []byte
+}
+
+// ArchiveDataDir is the data directory where the archive file is stored.
+var ArchiveDataDir string
+
+// ArchivedNodeResolver takes a buffer containing the archive data
+// held by an expiring node (an offset and a size) and returns a
+// list of records, which is a list of serialized leaf nodes. The
+// caller knows the context (MPT, binary trie) and is responsible
+// for decoding the nodes.
+func ArchivedNodeResolver(offset, size uint64) ([]*Record, error) {
+ file, err := os.Open(filepath.Join(ArchiveDataDir, "geth", "nodearchive"))
+ if err != nil {
+ return nil, fmt.Errorf("error opening archive file: %w", err)
+ }
+ defer file.Close()
+
+ o, err := file.Seek(int64(offset), io.SeekStart)
+ if err != nil {
+ return nil, fmt.Errorf("error seeking into archive file: %w", err)
+ }
+ if uint64(o) != offset {
+ return nil, fmt.Errorf("invalid offset: want %d, got %d", offset, o)
+ }
+
+ data := make([]byte, size)
+ if _, err := io.ReadFull(file, data); err != nil {
+ return nil, fmt.Errorf("error reading data from archive: %w", err)
+ }
+
+ var records []*Record
+ stream := rlp.NewStream(bytes.NewReader(data), uint64(len(data)))
+ for len(data) > 0 {
+ _, size, err := stream.Kind()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("error getting rlp kind from archive data: %w", err)
+ }
+ var record Record
+ err = stream.Decode(&record)
+ if err != nil {
+ return nil, fmt.Errorf("error decoding rlp record from archive data (offset=%d, size=%d): %w", offset, size, err)
+ }
+ records = append(records, &record)
+ }
+ return records, nil
+}
diff --git a/trie/archive/writer.go b/trie/archive/writer.go
new file mode 100644
index 000000000000..98b4ecce4b7f
--- /dev/null
+++ b/trie/archive/writer.go
@@ -0,0 +1,92 @@
+// Copyright 2026 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 archive
+
+import (
+ "os"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// ArchiveWriter is an append-only writer for archive files.
+// It writes RLP-encoded records to a file and tracks the current offset.
+type ArchiveWriter struct {
+ file *os.File
+ offset uint64
+ mu sync.Mutex
+}
+
+// NewArchiveWriter creates a new archive writer that appends to the given file.
+// If the file exists, it will be opened in append mode and writing continues
+// from the current end of file. If it doesn't exist, it will be created.
+func NewArchiveWriter(path string) (*ArchiveWriter, error) {
+ file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return nil, err
+ }
+ info, err := file.Stat()
+ if err != nil {
+ file.Close()
+ return nil, err
+ }
+ return &ArchiveWriter{
+ file: file,
+ offset: uint64(info.Size()),
+ }, nil
+}
+
+// WriteSubtree writes all records belonging to a subtree and returns
+// the starting offset and total size of the written data.
+// This is the atomic unit of archival - all records for a subtree are
+// written together and can be retrieved together using the returned
+// offset and size.
+func (w *ArchiveWriter) WriteSubtree(records []*Record) (offset uint64, size uint64, err error) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ startOffset := w.offset
+ for _, rec := range records {
+ encoded, err := rlp.EncodeToBytes(rec)
+ if err != nil {
+ return 0, 0, err
+ }
+ if _, err := w.file.Write(encoded); err != nil {
+ return 0, 0, err
+ }
+ w.offset += uint64(len(encoded))
+ }
+ return startOffset, w.offset - startOffset, nil
+}
+
+// Sync flushes the file to disk. This should be called after writing
+// a subtree and before modifying the database to ensure crash consistency.
+func (w *ArchiveWriter) Sync() error {
+ return w.file.Sync()
+}
+
+// Close closes the archive file.
+func (w *ArchiveWriter) Close() error {
+ return w.file.Close()
+}
+
+// Offset returns the current write offset in the file.
+func (w *ArchiveWriter) Offset() uint64 {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ return w.offset
+}
diff --git a/trie/archiver.go b/trie/archiver.go
new file mode 100644
index 000000000000..34d3bd89b8f4
--- /dev/null
+++ b/trie/archiver.go
@@ -0,0 +1,601 @@
+// Copyright 2026 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 trie
+
+import (
+ "encoding/binary"
+ "fmt"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/trie/archive"
+ "github.com/ethereum/go-ethereum/triedb/database"
+)
+
+// subtreeInfo holds information about a subtree to be archived.
+// It contains all the data needed to write the subtree to an archive
+// and replace it with an expiredNode in the database.
+type subtreeInfo struct {
+ path []byte // Hex-encoded path to subtree root
+ owner common.Hash // Zero for account trie, account hash for storage
+ height int // Height of subtree (from leaves)
+ leaves []*archive.Record // All leaf records (relative path + encoded node)
+ nodePaths [][]byte // Paths of all nodes to delete
+ rootHash common.Hash // Hash of the original subtree root (for verification)
+}
+
+// Archiver handles the archival process of trie nodes.
+// It walks the state trie, identifies subtrees at height 3,
+// archives their leaf data, and replaces them with expiredNode markers.
+//
+// The archiver uses a streaming approach: it walks the trie using a
+// NodeIterator, probes each node's height via bounded raw DB reads,
+// and archives subtrees immediately when found. This keeps memory
+// usage proportional to the iterator stack depth + the current subtree
+// being processed, rather than loading the entire trie into memory.
+type Archiver struct {
+ db ethdb.Database
+ triedb database.NodeDatabase
+ writer *archive.ArchiveWriter
+ compactionInterval uint64
+ dryRun bool
+ stateRoot common.Hash
+
+ // Progress tracking
+ subtreesArchived uint64
+ bytesDeleted uint64
+ leavesArchived uint64
+ lastCompaction uint64
+}
+
+// NewArchiver creates a new archiver instance.
+//
+// Parameters:
+// - db: The underlying key-value database
+// - triedb: The trie database for reading nodes
+// - writer: Archive file writer (can be nil for dry run)
+// - compactionInterval: Run compaction after this many subtrees (0 = disable)
+// - dryRun: If true, don't modify the database
+func NewArchiver(db ethdb.Database, triedb database.NodeDatabase,
+ writer *archive.ArchiveWriter, compactionInterval uint64, dryRun bool) *Archiver {
+ return &Archiver{
+ db: db,
+ triedb: triedb,
+ writer: writer,
+ compactionInterval: compactionInterval,
+ dryRun: dryRun,
+ }
+}
+
+// ProcessState archives subtrees from the given state root.
+// It processes storage tries first, then the account trie.
+func (a *Archiver) ProcessState(root common.Hash) error {
+ a.stateRoot = root
+
+ accountTrie, err := New(StateTrieID(root), a.triedb)
+ if err != nil {
+ return fmt.Errorf("failed to open account trie: %w", err)
+ }
+
+ log.Info("Processing storage tries")
+ iter, err := accountTrie.NodeIterator(nil)
+ if err != nil {
+ return fmt.Errorf("failed to create account iterator: %w", err)
+ }
+
+ kvIter := NewIterator(iter)
+ for kvIter.Next() {
+ // Decode the account to check for storage
+ var acc types.StateAccount
+ if err := rlp.DecodeBytes(kvIter.Value, &acc); err != nil {
+ log.Warn("Failed to decode account", "err", err)
+ continue
+ }
+ if acc.Root == types.EmptyRootHash {
+ continue
+ }
+
+ // Process this account's storage trie
+ accountHash := common.BytesToHash(kvIter.Key)
+ storageID := StorageTrieID(root, accountHash, acc.Root)
+ storageTrie, err := New(storageID, a.triedb)
+ if err != nil {
+ log.Warn("Failed to open storage trie", "account", accountHash, "err", err)
+ continue
+ }
+
+ if err := a.processTrie(accountHash, storageTrie); err != nil {
+ log.Warn("Failed to process storage trie", "account", accountHash, "err", err)
+ }
+ }
+
+ if kvIter.Err != nil {
+ return fmt.Errorf("account iteration error: %w", kvIter.Err)
+ }
+
+ log.Info("Processing account trie", "root", root)
+ if err := a.processTrie(common.Hash{}, accountTrie); err != nil {
+ return fmt.Errorf("failed to process account trie: %w", err)
+ }
+
+ return nil
+}
+
+// processTrie finds and archives all height-3 subtrees in the trie using
+// a streaming approach. It walks the trie with a NodeIterator, probes each
+// node's height via bounded raw DB reads, and archives subtrees immediately.
+//
+// Memory usage is O(iterator_stack_depth + current_subtree_size) instead of
+// O(entire_trie) as with the previous recursive approach.
+func (a *Archiver) processTrie(owner common.Hash, t *Trie) error {
+ if t.root == nil {
+ return nil
+ }
+
+ iter, err := t.NodeIterator(nil)
+ if err != nil {
+ return fmt.Errorf("failed to create node iterator: %w", err)
+ }
+
+ var (
+ lastLog = time.Now()
+ found uint64
+ )
+
+ for iter.Next(true) {
+ if iter.Leaf() {
+ continue
+ }
+
+ // Progress logging
+ if time.Since(lastLog) > 30*time.Second {
+ log.Info("Scanning trie for subtrees",
+ "owner", owner,
+ "path", common.Bytes2Hex(iter.Path()),
+ "found", found,
+ "archived", a.subtreesArchived)
+ lastLog = time.Now()
+ }
+
+ path := copyBytes(iter.Path())
+ hash := iter.Hash()
+ if hash == (common.Hash{}) {
+ // Embedded node (no hash), skip — it will be part of a
+ // parent subtree.
+ continue
+ }
+
+ // Probe subtree height via bounded raw DB reads.
+ // This does NOT load the trie into memory — it reads blobs from
+ // the DB, decodes them, computes height, and discards them.
+ height := a.probeHeight(owner, path, hash, 3)
+ if height != 3 {
+ // Too small to archive; the iterator will visit children.
+ // Too tall — descend into children to find height-3 subtrees.
+ continue
+ }
+
+ // height == 3: collect and archive this subtree immediately.
+ info := a.collectSubtree(owner, path, hash)
+ if info == nil {
+ continue
+ }
+ found++
+
+ if err := a.archiveSubtree(info); err != nil {
+ log.Warn("Failed to archive subtree", "path", common.Bytes2Hex(path), "err", err)
+ continue
+ }
+ a.subtreesArchived++
+ a.leavesArchived += uint64(len(info.leaves))
+
+ if err := a.maybeCompact(); err != nil {
+ log.Warn("Compaction failed", "err", err)
+ }
+
+ // Skip children — they're now archived.
+ // We call Next(false) to move past the subtree without descending.
+ iter.Next(false)
+ }
+
+ if iter.Error() != nil {
+ return fmt.Errorf("iterator error: %w", iter.Error())
+ }
+
+ log.Info("Found subtrees to archive", "owner", owner, "count", found)
+ return nil
+}
+
+// probeHeight computes the height of a node by reading from the raw DB.
+// It stops early once height exceeds maxHeight (returns maxHeight+1).
+// The decoded nodes are not retained — they are discarded after inspection.
+//
+// Height is measured from leaves: leaves=0, their parents=1, etc.
+func (a *Archiver) probeHeight(owner common.Hash, path []byte, hash common.Hash, maxHeight int) int {
+ blob := a.readNodeBlob(owner, path)
+ if len(blob) == 0 {
+ return 0
+ }
+
+ // Already expired — skip.
+ if blob[0] == expiredNodeMarker {
+ return -1
+ }
+
+ n, err := decodeNodeUnsafe(hash[:], blob)
+ if err != nil {
+ return 0
+ }
+
+ return a.nodeHeight(n, path, owner, maxHeight)
+}
+
+// nodeHeight computes the height of a decoded node, bounded by maxHeight.
+// Returns maxHeight+1 early if the subtree is taller than maxHeight.
+func (a *Archiver) nodeHeight(n node, path []byte, owner common.Hash, maxHeight int) int {
+ switch n := n.(type) {
+ case nil:
+ return 0
+
+ case valueNode:
+ return 0
+
+ case *shortNode:
+ childPath := append(append([]byte{}, path...), n.Key...)
+ switch child := n.Val.(type) {
+ case valueNode:
+ return 1 // shortNode → leaf
+ case hashNode:
+ if maxHeight <= 1 {
+ return maxHeight + 1
+ }
+ childHeight := a.probeHeight(owner, childPath, common.BytesToHash(child), maxHeight-1)
+ if childHeight < 0 {
+ return -1 // expired child
+ }
+ return childHeight + 1
+ default:
+ // Inline node
+ childHeight := a.nodeHeight(child, childPath, owner, maxHeight-1)
+ if childHeight < 0 {
+ return -1
+ }
+ return childHeight + 1
+ }
+
+ case *fullNode:
+ if maxHeight <= 0 {
+ // No depth budget left: a fullNode always has at least one
+ // child, so its height is >= 1, i.e. already > maxHeight.
+ return maxHeight + 1
+ }
+ maxH := 0
+ for i, child := range n.Children[:16] {
+ if child == nil {
+ continue
+ }
+ childPath := append(append([]byte{}, path...), byte(i))
+ var childHeight int
+ switch c := child.(type) {
+ case valueNode:
+ childHeight = 0
+ case hashNode:
+ childHeight = a.probeHeight(owner, childPath, common.BytesToHash(c), maxHeight-1)
+ default:
+ childHeight = a.nodeHeight(c, childPath, owner, maxHeight-1)
+ }
+ if childHeight < 0 {
+ continue // expired child, skip
+ }
+ h := childHeight + 1
+ if h > maxH {
+ maxH = h
+ }
+ if maxH > maxHeight {
+ return maxHeight + 1
+ }
+ }
+ return maxH
+
+ case hashNode:
+ return a.probeHeight(owner, path, common.BytesToHash(n), maxHeight)
+
+ case *expiredNode:
+ return -1
+ }
+ return 0
+}
+
+// collectSubtree reads a height-3 subtree from the raw DB and collects its
+// leaves and node paths for archival. The subtree is bounded (height ≤ 3),
+// so memory usage is limited.
+func (a *Archiver) collectSubtree(owner common.Hash, path []byte, hash common.Hash) *subtreeInfo {
+ blob := a.readNodeBlob(owner, path)
+ if len(blob) == 0 {
+ return nil
+ }
+ if blob[0] == expiredNodeMarker {
+ return nil
+ }
+
+ n, err := decodeNodeUnsafe(hash[:], blob)
+ if err != nil {
+ log.Warn("Failed to decode node for collection", "path", common.Bytes2Hex(path), "err", err)
+ return nil
+ }
+
+ info := &subtreeInfo{
+ path: copyBytes(path),
+ owner: owner,
+ rootHash: hash,
+ }
+
+ leaves, nodePaths, height, err := a.collectNodeLeaves(n, path, nil, owner)
+ if err != nil {
+ log.Warn("Failed to collect subtree leaves", "path", common.Bytes2Hex(path), "err", err)
+ return nil
+ }
+
+ info.height = height
+ info.leaves = leaves
+ info.nodePaths = append([][]byte{copyBytes(path)}, nodePaths...)
+ return info
+}
+
+// collectNodeLeaves recursively collects all leaves and node paths in a
+// bounded subtree. relPath is the path relative to the subtree root.
+// Returns (leaves, nodePaths, height, error).
+func (a *Archiver) collectNodeLeaves(n node, absPath, relPath []byte, owner common.Hash) ([]*archive.Record, [][]byte, int, error) {
+ switch n := n.(type) {
+ case nil:
+ return nil, nil, 0, nil
+
+ case valueNode:
+ return []*archive.Record{{
+ Path: copyBytes(relPath),
+ Value: []byte(n),
+ }}, nil, 0, nil
+
+ case *shortNode:
+ childAbsPath := append(append([]byte{}, absPath...), n.Key...)
+ var childNode node
+ switch c := n.Val.(type) {
+ case hashNode:
+ resolved, err := a.resolveRawNode(owner, childAbsPath, common.BytesToHash(c))
+ if err != nil {
+ return nil, nil, 0, fmt.Errorf("resolve shortNode child at %s: %w", common.Bytes2Hex(childAbsPath), err)
+ }
+ childNode = resolved
+ default:
+ childNode = c
+ }
+
+ // Pass nil relPath to child — we prepend the key ourselves
+ leaves, nodePaths, height, err := a.collectNodeLeaves(childNode, childAbsPath, nil, owner)
+ if err != nil {
+ return nil, nil, 0, err
+ }
+
+ // Prepend [relPath + extension key] to leaf relative paths
+ prefix := append(append([]byte{}, relPath...), n.Key...)
+ for _, leaf := range leaves {
+ leaf.Path = append(append([]byte{}, prefix...), leaf.Path...)
+ }
+
+ return leaves, append([][]byte{copyBytes(absPath)}, nodePaths...), height + 1, nil
+
+ case *fullNode:
+ var (
+ allLeaves []*archive.Record
+ allPaths [][]byte
+ maxHeight int
+ )
+ for i, child := range n.Children[:16] {
+ if child == nil {
+ continue
+ }
+ childAbsPath := append(append([]byte{}, absPath...), byte(i))
+
+ var childNode node
+ switch c := child.(type) {
+ case hashNode:
+ resolved, err := a.resolveRawNode(owner, childAbsPath, common.BytesToHash(c))
+ if err != nil {
+ return nil, nil, 0, fmt.Errorf("resolve fullNode child[%x] at %s: %w", i, common.Bytes2Hex(childAbsPath), err)
+ }
+ childNode = resolved
+ default:
+ childNode = c
+ }
+
+ // Pass nil relPath to child — we prepend the index ourselves
+ leaves, nodePaths, height, err := a.collectNodeLeaves(childNode, childAbsPath, nil, owner)
+ if err != nil {
+ return nil, nil, 0, err
+ }
+
+ // Prepend [relPath + branch index] to leaf relative paths
+ prefix := append(append([]byte{}, relPath...), byte(i))
+ for _, leaf := range leaves {
+ leaf.Path = append(append([]byte{}, prefix...), leaf.Path...)
+ }
+
+ allLeaves = append(allLeaves, leaves...)
+ allPaths = append(allPaths, nodePaths...)
+ h := height + 1
+ if h > maxHeight {
+ maxHeight = h
+ }
+ }
+ return allLeaves, allPaths, maxHeight, nil
+
+ case hashNode:
+ resolved, err := a.resolveRawNode(owner, absPath, common.BytesToHash(n))
+ if err != nil {
+ return nil, nil, 0, err
+ }
+ return a.collectNodeLeaves(resolved, absPath, relPath, owner)
+
+ case *expiredNode:
+ return nil, nil, 0, nil
+ }
+ return nil, nil, 0, nil
+}
+
+// readNodeBlob reads a trie node blob directly from the raw key-value
+// database, bypassing pathdb layers.
+func (a *Archiver) readNodeBlob(owner common.Hash, path []byte) []byte {
+ if owner == (common.Hash{}) {
+ return rawdb.ReadAccountTrieNode(a.db, path)
+ }
+ return rawdb.ReadStorageTrieNode(a.db, owner, path)
+}
+
+// resolveRawNode reads and decodes a trie node directly from the raw DB.
+// Unlike resolveNode, this does NOT use the trie database (no caching,
+// no diff layers). The decoded node is ephemeral and will be GC'd after use.
+func (a *Archiver) resolveRawNode(owner common.Hash, path []byte, hash common.Hash) (node, error) {
+ blob := a.readNodeBlob(owner, path)
+ if len(blob) == 0 {
+ return nil, fmt.Errorf("node not found: owner=%s path=%s", owner, common.Bytes2Hex(path))
+ }
+ if blob[0] == expiredNodeMarker {
+ return &expiredNode{}, nil
+ }
+ return decodeNodeUnsafe(hash[:], blob)
+}
+
+// archiveSubtree writes leaves to archive and replaces subtree with expiredNode.
+func (a *Archiver) archiveSubtree(info *subtreeInfo) error {
+ if a.dryRun {
+ log.Info("Would archive subtree",
+ "path", common.Bytes2Hex(info.path),
+ "owner", info.owner,
+ "height", info.height,
+ "leaves", len(info.leaves),
+ "nodes", len(info.nodePaths))
+ return nil
+ }
+
+ // 1. Write to archive file
+ offset, size, err := a.writer.WriteSubtree(info.leaves)
+ if err != nil {
+ return fmt.Errorf("failed to write subtree to archive: %w", err)
+ }
+
+ // 2. Sync to ensure durability before modifying DB
+ if err := a.writer.Sync(); err != nil {
+ return fmt.Errorf("failed to sync archive: %w", err)
+ }
+
+ // 3. Verify archive round-trip: reconstruct trie from records and
+ // check that the hash matches the original subtree root. This
+ // catches any data corruption before we delete the original nodes.
+ if info.rootHash != (common.Hash{}) {
+ reconstructed, err := archiveRecordsToNode(info.leaves)
+ if err != nil {
+ return fmt.Errorf("archive verification failed: cannot reconstruct trie from records: %w", err)
+ }
+ h := newHasher(false)
+ gotHash := common.BytesToHash(h.hash(reconstructed, true))
+ returnHasherToPool(h)
+ if gotHash != info.rootHash {
+ return fmt.Errorf("archive verification failed: hash mismatch at path %s owner %s: got %s want %s (leaves=%d offset=%d size=%d)",
+ common.Bytes2Hex(info.path), info.owner, gotHash, info.rootHash,
+ len(info.leaves), offset, size)
+ }
+ }
+
+ // 4. Batch database operations
+ batch := a.db.NewBatch()
+
+ // Delete all nodes in subtree (except the root which we'll overwrite)
+ for _, nodePath := range info.nodePaths[1:] { // Skip first (root)
+ if info.owner == (common.Hash{}) {
+ rawdb.DeleteAccountTrieNode(batch, nodePath)
+ } else {
+ rawdb.DeleteStorageTrieNode(batch, info.owner, nodePath)
+ }
+ a.bytesDeleted += uint64(len(nodePath))
+ }
+
+ // Write expiredNode at subtree root
+ expiredBlob := encodeExpiredNodeBlob(offset, size)
+ if info.owner == (common.Hash{}) {
+ rawdb.WriteAccountTrieNode(batch, info.path, expiredBlob)
+ } else {
+ rawdb.WriteStorageTrieNode(batch, info.owner, info.path, expiredBlob)
+ }
+
+ if err := batch.Write(); err != nil {
+ return fmt.Errorf("failed to write batch: %w", err)
+ }
+
+ log.Debug("Archived subtree",
+ "path", common.Bytes2Hex(info.path),
+ "owner", info.owner,
+ "leaves", len(info.leaves),
+ "offset", offset,
+ "size", size)
+
+ return nil
+}
+
+// maybeCompact runs database compaction if the threshold is reached.
+func (a *Archiver) maybeCompact() error {
+ if a.compactionInterval == 0 {
+ return nil
+ }
+ if a.subtreesArchived-a.lastCompaction >= a.compactionInterval {
+ log.Info("Running database compaction", "subtrees", a.subtreesArchived)
+ if err := a.db.Compact(nil, nil); err != nil {
+ return err
+ }
+ a.lastCompaction = a.subtreesArchived
+ }
+ return nil
+}
+
+// encodeExpiredNodeBlob creates the raw bytes for an expiredNode.
+// Format: 1-byte marker (0x00) + 8-byte offset + 8-byte size = 17 bytes
+func encodeExpiredNodeBlob(offset, size uint64) []byte {
+ buf := make([]byte, 1+2*archive.OffsetSize) // 17 bytes
+ buf[0] = expiredNodeMarker // 0x00
+ binary.BigEndian.PutUint64(buf[1:], offset)
+ binary.BigEndian.PutUint64(buf[1+archive.OffsetSize:], size)
+ return buf
+}
+
+// Stats returns archival statistics.
+func (a *Archiver) Stats() (subtrees, leaves, bytesDeleted uint64) {
+ return a.subtreesArchived, a.leavesArchived, a.bytesDeleted
+}
+
+// copyBytes returns a copy of the given byte slice.
+func copyBytes(b []byte) []byte {
+ if b == nil {
+ return nil
+ }
+ c := make([]byte, len(b))
+ copy(c, b)
+ return c
+}
diff --git a/trie/archiver_test.go b/trie/archiver_test.go
new file mode 100644
index 000000000000..938081ae6d5c
--- /dev/null
+++ b/trie/archiver_test.go
@@ -0,0 +1,81 @@
+// Copyright 2026 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 trie
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+)
+
+// TestProbeHeightFullNodeBranch is a regression test for an over-estimate bug in
+// nodeHeight's *fullNode case. The previous guard
+//
+// if maxH+1 > maxHeight { return maxHeight + 1 }
+//
+// fired once the running max height reached maxHeight via one child, so the next
+// hashNode child made a genuine height-3 branch report as height 4. Combined with
+// the "archive only height == 3" predicate, dense branch-heavy tries (notably the
+// account trie, whose height-3 nodes are all multi-child branches) archived
+// nothing. Pre-fix probeHeight returns 4 here; post-fix it must return 3.
+//
+// The trie built below has a height-3 ROOT branch:
+//
+// root (branch @ nibble0, children at 0 and 1) height 3
+// ├─ child0 (branch @ nibble1) ├─ leafA ├─ leafB height 2
+// └─ child1 (branch @ nibble1) ├─ leafC ├─ leafD height 2
+//
+// Values are 40 bytes so leaves are NOT embedded; root's children are hashNodes,
+// which is exactly what exercises the buggy branch of nodeHeight.
+func TestProbeHeightFullNodeBranch(t *testing.T) {
+ big := func(b byte) []byte { return bytes.Repeat([]byte{b}, 40) }
+
+ tr := NewEmpty(nil)
+ keys := [][]byte{
+ {0x00, 0x11, 0x11, 0x11}, // nibbles 0,0,...
+ {0x01, 0x22, 0x22, 0x22}, // nibbles 0,1,...
+ {0x10, 0x33, 0x33, 0x33}, // nibbles 1,0,...
+ {0x11, 0x44, 0x44, 0x44}, // nibbles 1,1,...
+ }
+ for i, k := range keys {
+ if err := tr.Update(k, big(byte('A'+i))); err != nil {
+ t.Fatalf("update %x: %v", k, err)
+ }
+ }
+ root, nodes := tr.Commit(false)
+ if nodes == nil {
+ t.Fatal("nil node set after commit")
+ }
+
+ // Persist the committed nodes under account-trie path keys so the archiver's
+ // raw-DB reader (readNodeBlob -> ReadAccountTrieNode) can resolve them.
+ raw := rawdb.NewMemoryDatabase()
+ for path, n := range nodes.Nodes {
+ if n == nil || len(n.Blob) == 0 { // skip deletions
+ continue
+ }
+ rawdb.WriteAccountTrieNode(raw, []byte(path), n.Blob)
+ }
+ a := &Archiver{db: raw}
+
+ if got := a.probeHeight(common.Hash{}, nil, root, 3); got != 3 {
+ t.Fatalf("probeHeight(height-3 root branch) = %d, want 3 "+
+ "(a height-3 branch must be detected as height 3, not over-estimated)", got)
+ }
+}
diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go
index e3436e3df1bf..3d236174d444 100644
--- a/trie/bintrie/trie.go
+++ b/trie/bintrie/trie.go
@@ -419,3 +419,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/committer.go b/trie/committer.go
index 2a2142e0ffaa..7ea4e690cf45 100644
--- a/trie/committer.go
+++ b/trie/committer.go
@@ -79,6 +79,8 @@ func (c *committer) commit(path []byte, n node, parallel bool) node {
return cn
case hashNode:
return cn
+ case *expiredNode:
+ return cn
default:
// nil, valuenode shouldn't be committed
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
diff --git a/trie/expired_node.go b/trie/expired_node.go
new file mode 100644
index 000000000000..ce622daa03bd
--- /dev/null
+++ b/trie/expired_node.go
@@ -0,0 +1,262 @@
+// Copyright 2026 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 trie
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "time"
+
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/trie/archive"
+)
+
+// expiredNodeMarker is a special marker byte to identify expired nodes.
+// Using 0x00 as a marker since valid MPT nodes are always RLP lists (starting with 0xc0+).
+const expiredNodeMarker = 0x00
+
+// expiredNode represents a node whose data has been archived.
+// It stores the file offset and size of the archived data.
+type expiredNode struct {
+ offset uint64
+ size uint64
+ cachedHash hashNode
+ archiveResolver archive.ResolverFn
+}
+
+func (n *expiredNode) cache() (hashNode, bool) {
+ return n.cachedHash, n.cachedHash == nil
+}
+
+func (n *expiredNode) encode(w rlp.EncoderBuffer) {
+ var buf [1 + 2*archive.OffsetSize]byte
+ buf[0] = expiredNodeMarker
+ binary.BigEndian.PutUint64(buf[1:], n.offset)
+ binary.BigEndian.PutUint64(buf[1+archive.OffsetSize:], n.size)
+ w.Write(buf[:])
+}
+
+func (n *expiredNode) fstring(ind string) string {
+ return fmt.Sprintf(" ", n.offset, n.size)
+}
+
+// Offset returns the archive file offset for this expired node.
+func (n *expiredNode) Offset() uint64 {
+ return n.offset
+}
+
+// SetArchiveResolver sets the resolver function for this expired node.
+func (n *expiredNode) SetArchiveResolver(resolver archive.ResolverFn) {
+ n.archiveResolver = resolver
+}
+
+// resolveExpiredNodeData resolves an expired node from the archive, verifies
+// the reconstructed subtree hash, and stamps the cached hash onto the root.
+// Returns an error if the archive data is corrupted (hash mismatch).
+func resolveExpiredNodeData(n *expiredNode) (node, error) {
+ start := time.Now()
+ records, err := archive.ArchivedNodeResolver(n.offset, n.size)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve expired node: %w", err)
+ }
+ resolved, err := archiveRecordsToNode(records)
+ if err != nil {
+ return nil, fmt.Errorf("failed to rebuild expired node from archive: %w", err)
+ }
+ depth := subtreeDepth(resolved)
+ log.Debug("Resurrected expired node from archive",
+ "offset", n.offset, "archiveBytes", n.size,
+ "records", len(records), "depth", depth,
+ "elapsed", time.Since(start))
+ // Verify hash integrity: if the original hash is known, check that the
+ // reconstructed subtree produces the same hash. A mismatch means the
+ // archive is corrupted (e.g. missing leaves due to unresolvable hashNodes
+ // during archival) and any data from it is unreliable.
+ if n.cachedHash != nil {
+ h := newHasher(false)
+ gotHash := h.hash(resolved, true)
+ returnHasherToPool(h)
+ if !bytes.Equal(gotHash, n.cachedHash) {
+ return nil, fmt.Errorf("expired node hash mismatch at offset=%d size=%d: archive data is corrupted (expected %x got %x, %d records)",
+ n.offset, n.size, []byte(n.cachedHash), gotHash, len(records))
+ }
+ // Stamp the original hash onto the resolved subtree root so the
+ // hasher returns it directly instead of re-computing.
+ switch nn := resolved.(type) {
+ case *fullNode:
+ nn.flags.hash = n.cachedHash
+ case *shortNode:
+ nn.flags.hash = n.cachedHash
+ }
+ }
+ // Mark the entire resolved subtree as dirty. This is critical for
+ // correctness with pathdb's diff layer model: when a trie with expired
+ // nodes is modified and committed, the committer only captures dirty
+ // nodes into the NodeSet (which becomes the diff layer). Without this
+ // marking, resolved-but-unmodified sibling nodes within the subtree
+ // would exist nowhere — not in any diff layer (they're clean) and not
+ // in the raw DB (the archiver deleted them). Subsequent trie accesses
+ // from higher diff layers would fall through to the disk layer, find
+ // nothing, and produce MissingNodeError.
+ //
+ // For read-only tries (only get operations, no commit), this dirty
+ // marking is harmless — the nodes are discarded when the trie is GC'd.
+ markSubtreeDirty(resolved)
+ return resolved, nil
+}
+
+// subtreeDepth returns the maximum depth of a trie subtree.
+func subtreeDepth(n node) int {
+ switch n := n.(type) {
+ case *fullNode:
+ max := 0
+ for _, child := range &n.Children {
+ if child != nil {
+ if d := subtreeDepth(child); d > max {
+ max = d
+ }
+ }
+ }
+ return 1 + max
+ case *shortNode:
+ return 1 + subtreeDepth(n.Val)
+ default:
+ return 0
+ }
+}
+
+// markSubtreeDirty recursively marks all fullNode and shortNode in the
+// subtree as dirty, preserving any cached hashes. This ensures the
+// committer will capture them in the NodeSet during trie commit.
+func markSubtreeDirty(n node) {
+ switch n := n.(type) {
+ case *fullNode:
+ n.flags.dirty = true
+ for _, child := range n.Children[:16] {
+ if child != nil {
+ markSubtreeDirty(child)
+ }
+ }
+ case *shortNode:
+ n.flags.dirty = true
+ markSubtreeDirty(n.Val)
+ }
+ // valueNode, hashNode, nil: no flags to mark
+}
+
+func archiveRecordsToNode(records []*archive.Record) (node, error) {
+ if len(records) == 0 {
+ return nil, archive.EmptyArchiveRecord
+ }
+
+ // Build the trie incrementally from nil to produce the canonical
+ // MPT structure. Starting with a fullNode would be wrong when the
+ // original subtree root was a shortNode (shared prefix).
+ var root node
+ for i, record := range records {
+ if err := validateRecordPath(record.Path); err != nil {
+ return nil, err
+ }
+
+ key, err := normalizeRecordKey(record.Path)
+ if err != nil {
+ return nil, err
+ }
+ if len(key) < 1 {
+ return nil, fmt.Errorf("empty key in record #%d", i)
+ }
+ root, err = insertTrieNode(root, key, valueNode(record.Value))
+ if err != nil {
+ return nil, err
+ }
+ }
+ return root, nil
+}
+
+func validateRecordPath(path []byte) error {
+ for i, b := range path {
+ if b > 16 {
+ return fmt.Errorf("invalid nibble in record path: %d", b)
+ }
+ if b == 16 && i != len(path)-1 {
+ return fmt.Errorf("terminator nibble in middle of record path")
+ }
+ }
+ return nil
+}
+
+// normalizeRecordKey ensures the record path is a hex-nibble key suitable for
+// leaf insertion by guaranteeing a single terminator nibble and preserving any
+// already-terminated path. Empty paths are normalized to a sole terminator.
+func normalizeRecordKey(path []byte) ([]byte, error) {
+ if len(path) == 0 {
+ return []byte{16}, nil
+ }
+ if hasTerm(path) {
+ return path, nil
+ }
+ key := append([]byte{}, path...)
+ key = append(key, 16)
+ return key, nil
+}
+
+func insertTrieNode(n node, key []byte, value node) (node, error) {
+ if len(key) == 0 {
+ return value, nil
+ }
+ switch n := n.(type) {
+ case *shortNode:
+ matchlen := prefixLen(key, n.Key)
+ if matchlen == len(n.Key) {
+ nn, err := insertTrieNode(n.Val, key[matchlen:], value)
+ if err != nil {
+ return nil, err
+ }
+ return &shortNode{Key: n.Key, Val: nn}, nil
+ }
+ branch := &fullNode{}
+ var err error
+ branch.Children[n.Key[matchlen]], err = insertTrieNode(nil, n.Key[matchlen+1:], n.Val)
+ if err != nil {
+ return nil, err
+ }
+ branch.Children[key[matchlen]], err = insertTrieNode(nil, key[matchlen+1:], value)
+ if err != nil {
+ return nil, err
+ }
+ if matchlen == 0 {
+ return branch, nil
+ }
+ return &shortNode{Key: key[:matchlen], Val: branch}, nil
+
+ case *fullNode:
+ child, err := insertTrieNode(n.Children[key[0]], key[1:], value)
+ if err != nil {
+ return nil, err
+ }
+ n.Children[key[0]] = child
+ return n, nil
+
+ case nil:
+ return &shortNode{Key: key, Val: value}, nil
+
+ default:
+ return nil, fmt.Errorf("invalid node type in trie insert: %T", n)
+ }
+}
diff --git a/trie/expired_node_test.go b/trie/expired_node_test.go
new file mode 100644
index 000000000000..40b3b056b415
--- /dev/null
+++ b/trie/expired_node_test.go
@@ -0,0 +1,601 @@
+// Copyright 2026 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 trie
+
+import (
+ "bytes"
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/trie/archive"
+)
+
+// setupTestArchive creates a temporary archive directory with an archive file
+// containing the given records, and configures archive.ArchiveDataDir to point
+// to it. It returns the offset and size of the written data, and a cleanup function.
+func setupTestArchive(t *testing.T, records []*archive.Record) (offset, size uint64, cleanup func()) {
+ t.Helper()
+ tmpDir := t.TempDir()
+ gethDir := filepath.Join(tmpDir, "geth")
+ if err := os.MkdirAll(gethDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+
+ writer, err := archive.NewArchiveWriter(filepath.Join(gethDir, "nodearchive"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ offset, size, err = writer.WriteSubtree(records)
+ if err != nil {
+ writer.Close()
+ t.Fatal(err)
+ }
+ writer.Close()
+
+ oldDir := archive.ArchiveDataDir
+ archive.ArchiveDataDir = tmpDir
+
+ return offset, size, func() {
+ archive.ArchiveDataDir = oldDir
+ }
+}
+
+func TestExpiredNodeEncodeDecode(t *testing.T) {
+ testCases := []struct {
+ offset uint64
+ size uint64
+ }{
+ {0, 0},
+ {1, 100},
+ {255, 1024},
+ {256, 4096},
+ {1 << 16, 1 << 20},
+ {1 << 32, 1 << 32},
+ {1<<64 - 1, 1<<64 - 1},
+ }
+
+ for _, tc := range testCases {
+ original := &expiredNode{offset: tc.offset, size: tc.size}
+
+ w := rlp.NewEncoderBuffer(nil)
+ original.encode(w)
+ encoded := w.ToBytes()
+ w.Flush()
+
+ decoded, err := decodeNodeUnsafe(nil, encoded)
+ if err != nil {
+ t.Fatalf("failed to decode expired node with offset %d, size %d: %v", tc.offset, tc.size, err)
+ }
+
+ expNode, ok := decoded.(*expiredNode)
+ if !ok {
+ t.Fatalf("decoded node is not an expired node, got %T", decoded)
+ }
+
+ if expNode.offset != original.offset {
+ t.Errorf("offset mismatch: got %d, want %d", expNode.offset, original.offset)
+ }
+ if expNode.size != original.size {
+ t.Errorf("size mismatch: got %d, want %d", expNode.size, original.size)
+ }
+ }
+}
+
+func TestExpiredNodeEncodedFormat(t *testing.T) {
+ node := &expiredNode{offset: 0x0102030405060708, size: 0x1112131415161718}
+
+ w := rlp.NewEncoderBuffer(nil)
+ node.encode(w)
+ encoded := w.ToBytes()
+ w.Flush()
+
+ expected := []byte{
+ 0x00,
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+ }
+ if !bytes.Equal(encoded, expected) {
+ t.Errorf("encoded format mismatch: got %x, want %x", encoded, expected)
+ }
+}
+
+func TestExpiredNodeFstring(t *testing.T) {
+ node := &expiredNode{offset: 12345, size: 6789}
+ s := node.fstring("")
+ if s != " " {
+ t.Errorf("fstring mismatch: got %q", s)
+ }
+}
+
+func TestExpiredNodeCache(t *testing.T) {
+ node := &expiredNode{offset: 100}
+ hash, dirty := node.cache()
+ if hash != nil {
+ t.Error("expected nil hash from expired node cache")
+ }
+ if !dirty {
+ t.Error("expected dirty=true from expired node cache")
+ }
+}
+
+func TestExpiredNodeInvalidLength(t *testing.T) {
+ invalidCases := [][]byte{
+ {0x00},
+ {0x00, 0x01},
+ {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
+ {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
+ {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11},
+ }
+
+ for _, buf := range invalidCases {
+ _, err := decodeNodeUnsafe(nil, buf)
+ if err == nil {
+ t.Errorf("expected error for buffer length %d, got nil", len(buf))
+ }
+ }
+}
+
+func TestExpiredNodeNoArchiveFile(t *testing.T) {
+ // When no archive file exists, Get should return an error
+ tmpDir := t.TempDir()
+ gethDir := filepath.Join(tmpDir, "geth")
+ if err := os.MkdirAll(gethDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+
+ oldDir := archive.ArchiveDataDir
+ archive.ArchiveDataDir = tmpDir
+ defer func() { archive.ArchiveDataDir = oldDir }()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: 100, size: 50}
+
+ _, err := tr.Get([]byte("key"))
+ if err == nil {
+ t.Error("expected error when archive file doesn't exist")
+ }
+}
+
+func TestExpiredNodeWithResolver(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("testvalue")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ val, err := tr.Get([]byte{0x12})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if string(val) != "testvalue" {
+ t.Errorf("value mismatch: got %q, want %q", val, "testvalue")
+ }
+}
+
+func TestExpiredNodeCopy(t *testing.T) {
+ original := &expiredNode{
+ offset: 12345,
+ size: 6789,
+ archiveResolver: archive.ArchivedNodeResolver,
+ }
+
+ copied := copyNode(original)
+ copiedExp, ok := copied.(*expiredNode)
+ if !ok {
+ t.Fatalf("copied node is not an expired node, got %T", copied)
+ }
+
+ if copiedExp.offset != original.offset {
+ t.Errorf("offset mismatch: got %d, want %d", copiedExp.offset, original.offset)
+ }
+
+ if copiedExp.size != original.size {
+ t.Errorf("size mismatch: got %d, want %d", copiedExp.size, original.size)
+ }
+
+ if copiedExp.archiveResolver == nil {
+ t.Error("archive resolver was not copied")
+ }
+}
+
+func TestArchiveRecordsToNodeEmpty(t *testing.T) {
+ _, err := archiveRecordsToNode([]*archive.Record{})
+ if !errors.Is(err, archive.EmptyArchiveRecord) {
+ t.Errorf("expected EmptyArchiveRecord error, got %v", err)
+ }
+
+ _, err = archiveRecordsToNode(nil)
+ if !errors.Is(err, archive.EmptyArchiveRecord) {
+ t.Errorf("expected EmptyArchiveRecord error for nil slice, got %v", err)
+ }
+}
+
+func TestArchiveRecordsToNodeMultiple(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 16}, Value: []byte("value1")},
+ {Path: []byte{0x02, 16}, Value: []byte("value2")},
+ }
+
+ node, err := archiveRecordsToNode(records)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ fn, ok := node.(*fullNode)
+ if !ok {
+ t.Fatalf("expected fullNode, got %T", node)
+ }
+
+ if fn.Children[0x01] == nil {
+ t.Error("expected child at index 0x01")
+ }
+ if fn.Children[0x02] == nil {
+ t.Error("expected child at index 0x02")
+ }
+}
+
+func TestExpiredNodeGetMultipleRecords(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
+ {Path: []byte{0x04, 0x05, 16}, Value: []byte("value2")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ val, err := tr.Get([]byte{0x12})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if string(val) != "value1" {
+ t.Errorf("value mismatch: got %q, want %q", val, "value1")
+ }
+
+ tr2 := NewEmpty(nil)
+ tr2.root = &expiredNode{offset: offset, size: size}
+
+ val2, err := tr2.Get([]byte{0x45})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if string(val2) != "value2" {
+ t.Errorf("value mismatch: got %q, want %q", val2, "value2")
+ }
+}
+
+func TestExpiredNodeGetKeyNotFound(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ val, err := tr.Get([]byte{0xff, 0xff})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if val != nil {
+ t.Errorf("expected nil value for non-existent key, got %q", val)
+ }
+}
+
+func TestExpiredNodeGetPathMismatch(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("testvalue")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ val, err := tr.Get([]byte{0x19})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if val != nil {
+ t.Errorf("expected nil value when leaf key doesn't match, got %q", val)
+ }
+}
+
+func TestExpiredNodeInsert(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("existing")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ err := tr.Update([]byte{0x45}, []byte("newvalue"))
+ if err != nil {
+ t.Fatalf("unexpected error on insert: %v", err)
+ }
+
+ val, err := tr.Get([]byte{0x45})
+ if err != nil {
+ t.Fatalf("unexpected error on get: %v", err)
+ }
+ if string(val) != "newvalue" {
+ t.Errorf("value mismatch: got %q, want %q", val, "newvalue")
+ }
+}
+
+func TestExpiredNodeUpdate(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("oldvalue")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ err := tr.Update([]byte{0x12}, []byte("newvalue"))
+ if err != nil {
+ t.Fatalf("unexpected error on update: %v", err)
+ }
+
+ val, err := tr.Get([]byte{0x12})
+ if err != nil {
+ t.Fatalf("unexpected error on get: %v", err)
+ }
+ if string(val) != "newvalue" {
+ t.Errorf("value mismatch: got %q, want %q", val, "newvalue")
+ }
+}
+
+func TestExpiredNodeDelete(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
+ {Path: []byte{0x04, 0x05, 16}, Value: []byte("value2")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ err := tr.Delete([]byte{0x12})
+ if err != nil {
+ t.Fatalf("unexpected error on delete: %v", err)
+ }
+
+ val, err := tr.Get([]byte{0x12})
+ if err != nil {
+ t.Fatalf("unexpected error on get after delete: %v", err)
+ }
+ if val != nil {
+ t.Errorf("expected nil after delete, got %q", val)
+ }
+
+ val2, err := tr.Get([]byte{0x45})
+ if err != nil {
+ t.Fatalf("unexpected error getting other key: %v", err)
+ }
+ if string(val2) != "value2" {
+ t.Errorf("other value should still exist: got %q, want %q", val2, "value2")
+ }
+}
+
+func TestTrieCopyPreservesArchiveResolver(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("testvalue")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ trCopy := tr.Copy()
+
+ val, err := trCopy.Get([]byte{0x12})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if string(val) != "testvalue" {
+ t.Errorf("value mismatch: got %q, want %q", val, "testvalue")
+ }
+}
+
+func TestWalkWithExpiredNodes(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
+ {Path: []byte{0x04, 0x05, 16}, Value: []byte("value2")},
+ {Path: []byte{0x07, 0x08, 16}, Value: []byte("value3")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ var leaves []string
+ stats, err := tr.Walk(func(path []byte, value []byte) error {
+ leaves = append(leaves, string(value))
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("Walk failed: %v", err)
+ }
+ if stats.Leaves != 3 {
+ t.Errorf("expected 3 leaves, got %d", stats.Leaves)
+ }
+ if stats.ExpiredResolved != 1 {
+ t.Errorf("expected 1 expired resolved, got %d", stats.ExpiredResolved)
+ }
+ // Verify all values were visited
+ expected := map[string]bool{"value1": true, "value2": true, "value3": true}
+ for _, leaf := range leaves {
+ if !expected[leaf] {
+ t.Errorf("unexpected leaf value: %q", leaf)
+ }
+ delete(expected, leaf)
+ }
+ if len(expected) > 0 {
+ t.Errorf("missing leaves: %v", expected)
+ }
+}
+
+func TestWalkEmptyTrie(t *testing.T) {
+ tr := NewEmpty(nil)
+ stats, err := tr.Walk(func(path []byte, value []byte) error {
+ t.Error("callback should not be called for empty trie")
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("Walk failed: %v", err)
+ }
+ if stats.Leaves != 0 || stats.ExpiredResolved != 0 {
+ t.Errorf("expected zero stats for empty trie, got leaves=%d expired=%d", stats.Leaves, stats.ExpiredResolved)
+ }
+}
+
+func TestWalkCallbackError(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ testErr := errors.New("test error")
+ _, err := tr.Walk(func(path []byte, value []byte) error {
+ return testErr
+ })
+ if !errors.Is(err, testErr) {
+ t.Fatalf("expected test error, got %v", err)
+ }
+}
+
+// TestExpiredNodeResolvedSubtreeDirty verifies that when an expired node is
+// resolved and a sibling leaf is modified, the commit captures ALL resolved
+// nodes (not just the modified path). Without this fix, resolved-but-unmodified
+// nodes would be lost: not in the diff layer (clean) and not in the raw DB
+// (deleted by archiver).
+func TestExpiredNodeResolvedSubtreeDirty(t *testing.T) {
+ // Use large values (>32 bytes) so leaf nodes are NOT embedded in
+ // their parent. This matches production storage tries where
+ // intermediate nodes are large enough to be stored independently.
+ bigVal1 := bytes.Repeat([]byte("A"), 40)
+ bigVal2 := bytes.Repeat([]byte("B"), 40)
+
+ // Create an archive with records under different branches.
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: bigVal1},
+ {Path: []byte{0x04, 0x05, 16}, Value: bigVal2},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ // Insert a value that goes through one branch of the resolved subtree.
+ // This modifies path [1, ...] but leaves path [4, ...] unmodified.
+ if err := tr.Update([]byte{0x12}, bytes.Repeat([]byte("C"), 40)); err != nil {
+ t.Fatalf("Update failed: %v", err)
+ }
+
+ // Commit the trie. The NodeSet should be non-nil because we modified data.
+ _, nodes := tr.Commit(false)
+ if nodes == nil {
+ t.Fatal("expected non-nil NodeSet after modifying expired subtree")
+ }
+
+ // The resolved-but-unmodified sibling (path [4, 5]) should also be
+ // captured in the NodeSet, because markSubtreeDirty ensures all resolved
+ // nodes are dirty. Count the nodes to verify.
+ nodeCount := len(nodes.Nodes)
+ // We expect at least 3 nodes: the root, the modified branch, and the
+ // sibling branch. The exact count depends on trie structure.
+ if nodeCount < 3 {
+ t.Errorf("expected at least 3 nodes in NodeSet (root + modified + sibling), got %d", nodeCount)
+ }
+}
+
+// TestMarkSubtreeDirty verifies that markSubtreeDirty correctly sets the dirty
+// flag on all nodes in a subtree while preserving cached hashes.
+func TestMarkSubtreeDirty(t *testing.T) {
+ // Build a small trie structure
+ leaf1 := &shortNode{Key: []byte{1, 16}, Val: valueNode("v1")}
+ leaf2 := &shortNode{Key: []byte{2, 16}, Val: valueNode("v2")}
+ branch := &fullNode{}
+ branch.Children[1] = leaf1
+ branch.Children[2] = leaf2
+
+ // Set hash but not dirty (as if loaded from DB)
+ branch.flags = nodeFlag{hash: hashNode("testhash"), dirty: false}
+ leaf1.flags = nodeFlag{hash: hashNode("hash1"), dirty: false}
+ leaf2.flags = nodeFlag{hash: hashNode("hash2"), dirty: false}
+
+ markSubtreeDirty(branch)
+
+ // All nodes should be dirty
+ if !branch.flags.dirty {
+ t.Error("branch should be dirty")
+ }
+ if !leaf1.flags.dirty {
+ t.Error("leaf1 should be dirty")
+ }
+ if !leaf2.flags.dirty {
+ t.Error("leaf2 should be dirty")
+ }
+
+ // Hashes should be preserved
+ if !bytes.Equal(branch.flags.hash, hashNode("testhash")) {
+ t.Error("branch hash should be preserved")
+ }
+ if !bytes.Equal(leaf1.flags.hash, hashNode("hash1")) {
+ t.Error("leaf1 hash should be preserved")
+ }
+ if !bytes.Equal(leaf2.flags.hash, hashNode("hash2")) {
+ t.Error("leaf2 hash should be preserved")
+ }
+}
+
+func TestExpiredNodeGetNode(t *testing.T) {
+ records := []*archive.Record{
+ {Path: []byte{0x01, 0x02, 16}, Value: []byte("testvalue")},
+ }
+ offset, size, cleanup := setupTestArchive(t, records)
+ defer cleanup()
+
+ tr := NewEmpty(nil)
+ tr.root = &expiredNode{offset: offset, size: size}
+
+ _, _, err := tr.GetNode(hexToCompact([]byte{0x01, 0x02}))
+ if err != nil && err.Error() != "non-consensus node" {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
diff --git a/trie/hasher.go b/trie/hasher.go
index a2a1f5b662c2..d4376e12e26e 100644
--- a/trie/hasher.go
+++ b/trie/hasher.go
@@ -18,6 +18,7 @@ package trie
import (
"bytes"
+ "encoding/binary"
"fmt"
"sync"
@@ -97,6 +98,22 @@ func (h *hasher) hash(n node, force bool) []byte {
// hash nodes don't have children, so they're left as were
return n
+ case *expiredNode:
+ // Return the original subtree hash that was cached when the
+ // expired node was decoded. The parent node references this
+ // hash, so we must return the same value to keep the Merkle
+ // root consistent.
+ if n.cachedHash != nil {
+ return n.cachedHash
+ }
+ // Fallback: hash the marker blob (should not happen in practice
+ // because decodeNodeUnsafe always provides the hash).
+ var buf [1 + 2*8]byte // 17 bytes
+ buf[0] = expiredNodeMarker
+ binary.BigEndian.PutUint64(buf[1:], n.offset)
+ binary.BigEndian.PutUint64(buf[9:], n.size)
+ return h.hashData(buf[:])
+
default:
panic(fmt.Errorf("unexpected node type, %T", n))
}
@@ -214,6 +231,12 @@ func (h *hasher) proofHash(original node) []byte {
return bytes.Clone(h.encodeShortNode(n))
case *fullNode:
return bytes.Clone(h.encodeFullNode(n))
+ case *expiredNode:
+ var buf [1 + 2*8]byte
+ buf[0] = expiredNodeMarker
+ binary.BigEndian.PutUint64(buf[1:], n.offset)
+ binary.BigEndian.PutUint64(buf[9:], n.size)
+ return buf[:]
default:
panic(fmt.Errorf("unexpected node type, %T", original))
}
diff --git a/trie/node.go b/trie/node.go
index 70221160488b..0034521d78f8 100644
--- a/trie/node.go
+++ b/trie/node.go
@@ -18,12 +18,15 @@ package trie
import (
"bytes"
+ "encoding/binary"
"fmt"
"io"
"strings"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/trie/archive"
)
var indices = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "[17]"}
@@ -157,6 +160,15 @@ func decodeNodeUnsafe(hash, buf []byte) (node, error) {
if len(buf) == 0 {
return nil, io.ErrUnexpectedEOF
}
+ if buf[0] == expiredNodeMarker {
+ if len(buf) != 1+2*archive.OffsetSize {
+ return nil, fmt.Errorf("invalid expired node length: %d", len(buf))
+ }
+ offset := binary.BigEndian.Uint64(buf[1:])
+ size := binary.BigEndian.Uint64(buf[1+archive.OffsetSize:])
+ log.Debug("Decoded expired node", "offset", offset, "size", size, "hash", common.BytesToHash(hash))
+ return &expiredNode{offset: offset, size: size, cachedHash: hashNode(hash), archiveResolver: archive.ArchivedNodeResolver}, nil
+ }
elems, _, err := rlp.SplitList(buf)
if err != nil {
return nil, fmt.Errorf("decode error: %v", err)
diff --git a/trie/proof.go b/trie/proof.go
index 58075daf9b11..5be05c6f8132 100644
--- a/trie/proof.go
+++ b/trie/proof.go
@@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/trie/archive"
)
// Prove constructs a merkle proof for key. The result contains all encoded nodes
@@ -78,6 +79,16 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
// clean cache or the database, they are all in their own
// copy and safe to use unsafe decoder.
tn = mustDecodeNodeUnsafe(n, blob)
+ case *expiredNode:
+ records, err := archive.ArchivedNodeResolver(n.offset, n.size)
+ if err != nil {
+ return fmt.Errorf("failed to resolve expired node in proof: %w", err)
+ }
+ resolved, err := archiveRecordsToNode(records)
+ if err != nil {
+ return fmt.Errorf("failed to rebuild expired node in proof: %w", err)
+ }
+ tn = resolved
default:
panic(fmt.Sprintf("%T: invalid node: %v", tn, tn))
}
@@ -617,6 +628,8 @@ func get(tn node, key []byte, skipResolved bool) ([]byte, node) {
}
case hashNode:
return key, n
+ case *expiredNode:
+ return key, n
case nil:
return key, nil
case valueNode:
diff --git a/trie/secure_trie.go b/trie/secure_trie.go
index 4d03ca45f011..f2176310d002 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 04122d1384f9..042fa468bfa5 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 3e5511be9ed2..d939e804e371 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 1ef2c2f1a66e..9cc6e9157e4a 100644
--- a/trie/trie.go
+++ b/trie/trie.go
@@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/trie/archive"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb/database"
"golang.org/x/sync/errgroup"
@@ -57,6 +58,10 @@ type Trie struct {
// reader is the handler trie can retrieve nodes from.
reader *Reader
+ // archiveResolver is an optional callback to resolve expired nodes from
+ // an archive file.
+ archiveResolver archive.ResolverFn
+
// Various tracers for capturing the modifications to trie
opTracer *opTracer
prevalueTracer *PrevalueTracer
@@ -70,17 +75,23 @@ func (t *Trie) newFlag() nodeFlag {
// Copy returns a copy of Trie.
func (t *Trie) Copy() *Trie {
return &Trie{
- root: copyNode(t.root),
- owner: t.owner,
- committed: t.committed,
- unhashed: t.unhashed,
- uncommitted: t.uncommitted,
- reader: t.reader,
- opTracer: t.opTracer.copy(),
- prevalueTracer: t.prevalueTracer.Copy(),
+ root: copyNode(t.root),
+ owner: t.owner,
+ committed: t.committed,
+ unhashed: t.unhashed,
+ uncommitted: t.uncommitted,
+ reader: t.reader,
+ archiveResolver: t.archiveResolver,
+ opTracer: t.opTracer.copy(),
+ prevalueTracer: t.prevalueTracer.Copy(),
}
}
+// SetArchiveResolver sets the archive resolver callback for expired nodes.
+func (t *Trie) SetArchiveResolver(resolver archive.ResolverFn) {
+ t.archiveResolver = resolver
+}
+
// New creates the trie instance with provided trie id and the read-only
// database. The state specified by trie id must be available, otherwise
// an error will be returned. The trie root specified by trie id can be
@@ -218,6 +229,14 @@ func (t *Trie) get(origNode node, key []byte, pos int) (value []byte, newnode no
}
value, newnode, _, err := t.get(child, key, pos)
return value, newnode, true, err
+ case *expiredNode:
+ log.Debug("Resolving expired node in get()", "owner", t.owner, "offset", n.offset, "size", n.size, "pos", pos)
+ newnode, err := resolveExpiredNodeData(n)
+ if err != nil {
+ return nil, n, false, err
+ }
+ value, _, _, err = t.get(newnode, key, pos)
+ return value, newnode, true, err
default:
panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode))
}
@@ -352,6 +371,14 @@ func (t *Trie) getNode(origNode node, path []byte, pos int) (item []byte, newnod
item, newnode, resolved, err := t.getNode(child, path, pos)
return item, newnode, resolved + 1, err
+ case *expiredNode:
+ rn, err := resolveExpiredNodeData(n)
+ if err != nil {
+ return nil, n, 0, err
+ }
+ item, newnode, resolvedCount, err := t.getNode(rn, path, pos)
+ return item, newnode, resolvedCount + 1, err
+
default:
panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode))
}
@@ -475,11 +502,86 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
}
return true, nn, nil
+ case *expiredNode:
+ log.Debug("Resolving expired node in insert()", "owner", t.owner, "offset", n.offset, "size", n.size)
+ rn, err := resolveExpiredNodeData(n)
+ if err != nil {
+ return false, nil, err
+ }
+ dirty, nn, err := t.insert(rn, prefix, key, value)
+ if !dirty || err != nil {
+ return false, rn, err
+ }
+ return true, nn, nil
+
default:
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
}
}
+// 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) {
@@ -636,6 +738,18 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) {
}
return true, nn, nil
+ case *expiredNode:
+ log.Debug("Resolving expired node in delete()", "owner", t.owner, "offset", n.offset, "size", n.size)
+ rn, err := resolveExpiredNodeData(n)
+ if err != nil {
+ return false, nil, err
+ }
+ dirty, nn, err := t.delete(rn, prefix, key)
+ if !dirty || err != nil {
+ return false, rn, err
+ }
+ return true, nn, nil
+
default:
panic(fmt.Sprintf("%T: invalid node: %v (%v)", n, n, key))
}
@@ -666,14 +780,24 @@ func copyNode(n node) node {
}
case hashNode:
return n
+ case *expiredNode:
+ return &expiredNode{
+ offset: n.offset,
+ size: n.size,
+ cachedHash: common.CopyBytes(n.cachedHash),
+ archiveResolver: n.archiveResolver,
+ }
default:
panic(fmt.Sprintf("%T: unknown node type", n))
}
}
func (t *Trie) resolve(n node, prefix []byte) (node, error) {
- if n, ok := n.(hashNode); ok {
+ switch n := n.(type) {
+ case hashNode:
return t.resolveAndTrack(n, prefix)
+ case *expiredNode:
+ return resolveExpiredNodeData(n)
}
return n, nil
}
@@ -784,6 +908,58 @@ func (t *Trie) Witness() map[string][]byte {
return t.prevalueTracer.Values()
}
+// WalkStats holds statistics from a Walk traversal.
+type WalkStats struct {
+ Leaves int // Number of leaf nodes visited
+ ExpiredResolved int // Number of expired nodes resolved from archive
+}
+
+// Walk recursively traverses the trie, resolving all nodes including
+// hashNodes and expiredNodes. It calls fn for each leaf found.
+// This triggers hash verification for expired nodes via cachedHash.
+func (t *Trie) Walk(fn func(path []byte, value []byte) error) (WalkStats, error) {
+ return t.walk(t.root, nil, fn)
+}
+
+func (t *Trie) walk(n node, path []byte, fn func([]byte, []byte) error) (WalkStats, error) {
+ switch n := n.(type) {
+ case *shortNode:
+ return t.walk(n.Val, append(append([]byte{}, path...), n.Key...), fn)
+ case *fullNode:
+ var stats WalkStats
+ for i, child := range n.Children[:16] {
+ if child != nil {
+ childStats, err := t.walk(child, append(append([]byte{}, path...), byte(i)), fn)
+ if err != nil {
+ return stats, err
+ }
+ stats.Leaves += childStats.Leaves
+ stats.ExpiredResolved += childStats.ExpiredResolved
+ }
+ }
+ return stats, nil
+ case hashNode:
+ resolved, err := t.resolveAndTrack(n, path)
+ if err != nil {
+ return WalkStats{}, err
+ }
+ return t.walk(resolved, path, fn)
+ case *expiredNode:
+ resolved, err := resolveExpiredNodeData(n)
+ if err != nil {
+ return WalkStats{}, err
+ }
+ childStats, err := t.walk(resolved, path, fn)
+ childStats.ExpiredResolved++
+ return childStats, err
+ case valueNode:
+ return WalkStats{Leaves: 1}, fn(path, []byte(n))
+ case nil:
+ return WalkStats{}, nil
+ }
+ return WalkStats{}, nil
+}
+
// reset drops the referenced root node and cleans all internal state.
func (t *Trie) reset() {
t.root = nil
diff --git a/trie/trie_test.go b/trie/trie_test.go
index 3661933e2281..949f381f0786 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())
+ }
+}
diff --git a/triedb/database.go b/triedb/database.go
index ef95169df1ae..71b578367bba 100644
--- a/triedb/database.go
+++ b/triedb/database.go
@@ -399,6 +399,28 @@ func (db *Database) Disk() ethdb.Database {
return db.disk
}
+// DiffHead returns the root hash of the topmost diff layer in pathdb.
+// If there are no diff layers or the backend is not pathdb, it returns
+// the zero hash and false.
+func (db *Database) DiffHead() (common.Hash, bool) {
+ pdb, ok := db.backend.(*pathdb.Database)
+ if !ok {
+ return common.Hash{}, false
+ }
+ return pdb.DiffHead()
+}
+
+// DisableStateHistory closes and disables the state history freezer.
+// This is used by the archiver to bypass state history writes during
+// diff layer flushing when state history may have gaps.
+func (db *Database) DisableStateHistory() {
+ pdb, ok := db.backend.(*pathdb.Database)
+ if !ok {
+ return
+ }
+ pdb.DisableStateHistory()
+}
+
// SnapshotCompleted returns the indicator if the snapshot is completed.
func (db *Database) SnapshotCompleted() bool {
pdb, ok := db.backend.(*pathdb.Database)
diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go
index e52949c93e87..ba606552df1e 100644
--- a/triedb/pathdb/database.go
+++ b/triedb/pathdb/database.go
@@ -318,6 +318,30 @@ func (db *Database) Update(root common.Hash, parentRoot common.Hash, block uint6
return db.tree.cap(root, maxDiffLayers)
}
+// DiffHead returns the root hash of the topmost diff layer. If there are no
+// diff layers (only the disk layer), it returns the disk layer root and false.
+func (db *Database) DiffHead() (common.Hash, bool) {
+ db.lock.RLock()
+ defer db.lock.RUnlock()
+
+ return db.tree.diffHead()
+}
+
+// DisableStateHistory closes and disables the state history freezer. This is
+// used by the archiver to bypass state history writes during diff layer flushing,
+// since the archiver only needs trie nodes committed to disk and state history
+// may have gaps from unclean shutdowns that prevent sequential appends.
+func (db *Database) DisableStateHistory() {
+ db.lock.Lock()
+ defer db.lock.Unlock()
+
+ if db.stateFreezer != nil {
+ db.stateFreezer.Close()
+ db.stateFreezer = nil
+ log.Info("Disabled state history freezer")
+ }
+}
+
// Commit traverses downwards the layer tree from a specified layer with the
// provided state root and all the layers below are flattened downwards. It
// can be used alone and mostly for test purposes.
diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go
index 7f5b0e35bac6..7751bbe29dcd 100644
--- a/triedb/pathdb/history.go
+++ b/triedb/pathdb/history.go
@@ -278,9 +278,17 @@ func truncateFromHead(store ethdb.AncientStore, typ historyType, nhead uint64) (
return 0, err
}
// Ensure that the truncation target falls within the valid range.
- if ohead < nhead || nhead < otail {
+ if nhead < otail {
return 0, fmt.Errorf("%w, %s, tail: %d, head: %d, target: %d", errHeadTruncationOutOfRange, typ, otail, ohead, nhead)
}
+ // If the target is ahead of the current head, there's nothing to truncate.
+ // This can happen after unclean shutdowns where the state history was not
+ // fully written.
+ if ohead < nhead {
+ log.Warn("State history shorter than target, nothing to truncate",
+ "type", typ.String(), "head", ohead, "target", nhead)
+ return 0, nil
+ }
// Short circuit if nothing to truncate.
if ohead == nhead {
return 0, nil
diff --git a/triedb/pathdb/history_state_test.go b/triedb/pathdb/history_state_test.go
index 4046fb96400a..845d58b256fb 100644
--- a/triedb/pathdb/history_state_test.go
+++ b/triedb/pathdb/history_state_test.go
@@ -244,8 +244,8 @@ func TestTruncateOutOfRange(t *testing.T) {
target uint64
expErr error
}{
- {0, head, nil}, // nothing to delete
- {0, head + 1, errHeadTruncationOutOfRange},
+ {0, head, nil}, // nothing to delete
+ {0, head + 1, nil}, // gracefully handled after unclean shutdown
{0, tail - 1, errHeadTruncationOutOfRange},
{1, tail, nil}, // nothing to delete
{1, head + 1, errTailTruncationOutOfRange},
diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go
index b20e40bd0516..99fd23a2a1a1 100644
--- a/triedb/pathdb/layertree.go
+++ b/triedb/pathdb/layertree.go
@@ -31,6 +31,7 @@ import (
// of the referenced layer by themselves.
type layerTree struct {
base *diskLayer
+ head common.Hash // Root hash of the topmost layer (diff or disk)
layers map[common.Hash]layer
// descendants is a two-dimensional map where the keys represent
@@ -59,6 +60,7 @@ func (tree *layerTree) init(head layer) {
defer tree.lock.Unlock()
current := head
+ tree.head = head.rootHash()
tree.layers = make(map[common.Hash]layer)
tree.descendants = make(map[common.Hash]map[common.Hash]struct{})
@@ -76,6 +78,18 @@ func (tree *layerTree) init(head layer) {
tree.lookup = newLookup(head, tree.isDescendant)
}
+// diffHead returns the root hash of the topmost diff layer. If there are no
+// diff layers, returns the disk layer root and false.
+func (tree *layerTree) diffHead() (common.Hash, bool) {
+ tree.lock.RLock()
+ defer tree.lock.RUnlock()
+
+ if _, ok := tree.layers[tree.head].(*diffLayer); ok {
+ return tree.head, true
+ }
+ return tree.base.rootHash(), false
+}
+
// get retrieves a layer belonging to the given state root.
func (tree *layerTree) get(root common.Hash) layer {
tree.lock.RLock()
diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go
index e087ef26edbd..845667b578f7 100644
--- a/triedb/pathdb/reader.go
+++ b/triedb/pathdb/reader.go
@@ -69,7 +69,7 @@ func (r *reader) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte,
return nil, err
}
// Error out if the local one is inconsistent with the target.
- if !r.noHashCheck && got != hash {
+ if !r.noHashCheck && (len(blob) > 0 && blob[0] != 0) && got != hash {
// Location is always available even if the node
// is not found.
switch loc.loc {