all: add BAL perf changes

This commit is contained in:
Jared Wasinger 2026-06-16 16:02:44 -04:00
parent 4d2181aa41
commit a5a452512f
30 changed files with 1824 additions and 138 deletions

View file

@ -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
}

View file

@ -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"
)
@ -101,7 +104,7 @@ type ExecutableData struct {
BlobGasUsed *uint64 `json:"blobGasUsed"`
ExcessBlobGas *uint64 `json:"excessBlobGas"`
SlotNumber *uint64 `json:"slotNumber,omitempty"`
BlockAccessList *bal.BlockAccessList `json:"blockAccessList,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,7 +351,18 @@ 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
@ -372,7 +387,14 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.
BlobGasUsed: block.BlobGasUsed(),
ExcessBlobGas: block.ExcessBlobGas(),
SlotNumber: block.SlotNumber(),
BlockAccessList: block.AccessList(),
}
// 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.

View file

@ -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

View file

@ -160,6 +160,9 @@ var (
// This is where the tests should be unpacked.
executionSpecTestsDir = "tests/spec-tests"
// This is where the bal-specific release of the tests should be unpacked.
executionSpecTestsBALDir = "tests/spec-tests-bal"
)
var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin"))
@ -398,6 +401,7 @@ func doTest(cmdline []string) {
// Get test fixtures.
if !*short {
downloadSpecTestFixtures(csdb, *cachedir)
downloadBALSpecTestFixtures(csdb, *cachedir)
}
// Configure the toolchain.
@ -463,6 +467,20 @@ func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string
return filepath.Join(cachedir, base)
}
// downloadBALSpecTestFixtures downloads and extracts the bal-specific execution-spec-tests fixtures.
func downloadBALSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string {
ext := ".tar.gz"
base := "fixtures_bal"
archivePath := filepath.Join(cachedir, base+ext)
if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil {
log.Fatal(err)
}
if err := build.ExtractArchive(archivePath, executionSpecTestsBALDir); err != nil {
log.Fatal(err)
}
return filepath.Join(cachedir, base)
}
// doCheckGenerate ensures that re-generating generated files does not cause
// any mutations in the source file tree.
func doCheckGenerate() {

View file

@ -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)

View file

@ -20,6 +20,7 @@ import (
"bufio"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/core/types/bal"
"os"
"reflect"
"runtime"
@ -245,6 +246,28 @@ func makeFullNode(ctx *cli.Context) *node.Node {
cfg.Eth.OverrideUBT = &v
}
if ctx.IsSet(utils.BALExecutionModeFlag.Name) {
val := ctx.String(utils.BALExecutionModeFlag.Name)
switch val {
case utils.BalExecutionModeOptimized:
cfg.Eth.BALExecutionMode = bal.BALExecutionOptimized
case utils.BalExecutionModeNoBatchIO:
cfg.Eth.BALExecutionMode = bal.BALExecutionNoBatchIO
case utils.BalExecutionModeSequential:
cfg.Eth.BALExecutionMode = bal.BALExecutionSequential
default:
utils.Fatalf("invalid option for --bal.executionmode: %s. acceptable values are full|nobatchio|sequential", val)
}
}
cfg.Eth.BlockingPrefetch = ctx.Bool(utils.BlockingPrefetchFlag.Name)
prefetchWorkers := ctx.Uint(utils.PrefetchWorkersFlag.Name)
if ctx.IsSet(utils.PrefetchWorkersFlag.Name) && prefetchWorkers == 0 {
prefetchWorkers = uint(runtime.NumCPU())
log.Warn(fmt.Sprintf("invalid value for --bal.prefetchworkers. got 0. sanitizing to %d", prefetchWorkers))
}
cfg.Eth.PrefetchWorkers = prefetchWorkers
// Start metrics export if enabled.
utils.SetupMetrics(&cfg.Metrics)

View file

@ -92,6 +92,9 @@ var (
utils.BinTrieGroupDepthFlag,
utils.LightKDFFlag,
utils.EthRequiredBlocksFlag,
utils.BALExecutionModeFlag,
utils.PrefetchWorkersFlag,
utils.BlockingPrefetchFlag,
utils.CacheFlag,
utils.CacheDatabaseFlag,
utils.CacheTrieFlag,

View file

@ -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 (<number>=<hash>)",
Category: flags.EthCategory,
}
BALExecutionModeFlag = &cli.StringFlag{
Name: "bal.executionmode",
Usage: "EIP-7928 block-access-list execution mode (no-op placeholder)",
Category: flags.EthCategory,
}
PrefetchWorkersFlag = &cli.UintFlag{
Name: "bal.prefetchworkers",
Usage: "The number of concurrent state loading tasks to perform when prefetching BAL state. Default to the number of cpus",
Value: uint(runtime.NumCPU()),
Category: flags.MiscCategory,
}
BlockingPrefetchFlag = &cli.BoolFlag{
Name: "bal.blockingprefetch",
Usage: "only relevant when executing in parallel with a BAL: if true, the prefetcher will block tx/state-root calculation until all scheduled fetching tasks have completed.",
Category: flags.MiscCategory,
}
BloomFilterSizeFlag = &cli.Uint64Flag{
Name: "bloomfilter.size",
Usage: "Megabytes of memory allocated to bloom-filter for pruning",
@ -1131,6 +1148,12 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server.
}
)
const (
BalExecutionModeOptimized = "full"
BalExecutionModeNoBatchIO = "nobatchio"
BalExecutionModeSequential = "sequential"
)
var (
// TestnetFlags is the flag group of all built-in supported testnets.
TestnetFlags = []cli.Flag{

View file

@ -19,9 +19,9 @@ package core
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
@ -143,9 +143,14 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
return nil
}
type StateRootSource interface {
IntermediateRoot(deleteEmptyObjects bool) common.Hash
Error() error
}
// ValidateState validates the various changes that happen after a state transition,
// such as amount of used gas, the receipt roots and the state root itself.
func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult, stateless bool) error {
func (v *BlockValidator) ValidateState(block *types.Block, state StateRootSource, res *ProcessResult, stateless bool) error {
if res == nil {
return errors.New("nil ProcessResult value")
}
@ -201,8 +206,8 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
}
// Validate the state root against the received state root and throw
// an error if they don't match.
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error())
if root := state.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, state.Error())
}
return nil
}

View file

@ -21,6 +21,7 @@ import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/core/types/bal"
"io"
"math/big"
"runtime"
@ -225,6 +226,10 @@ type BlockChainConfig struct {
// Execution configs
StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose)
EnableWitnessStats bool // Whether trie access statistics collection is enabled
BALExecutionMode bal.BALExecutionMode
BlockingPrefetch bool
PrefetchWorkers int
}
// DefaultConfig returns the default config.
@ -369,6 +374,7 @@ type BlockChain struct {
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
@ -433,6 +439,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
bc.validator = NewBlockValidator(chainConfig, bc)
bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc)
bc.processor = NewStateProcessor(bc.hc)
bc.parallelProcessor = *NewParallelStateProcessor(bc.hc, bc.GetVMConfig())
genesisHeader := bc.GetHeaderByNumber(0)
if genesisHeader == nil {
@ -1642,7 +1649,7 @@ func (bc *BlockChain) writeKnownBlock(block *types.Block) error {
// writeBlockWithState writes block, metadata and corresponding state data to the
// database.
func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb *state.StateDB) error {
func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb state.Committer) error {
if !bc.HasHeader(block.ParentHash(), block.NumberU64()-1) {
return consensus.ErrUnknownAncestor
}
@ -1756,7 +1763,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
// writeBlockAndSetHead is the internal implementation of WriteBlockAndSetHead.
// This function expects the chain mutex to be held.
func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) {
func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state state.Committer, emitHeadEvent bool) (status WriteStatus, err error) {
if err := bc.writeBlockWithState(block, receipts, state); err != nil {
return NonStatTy, err
}
@ -2111,6 +2118,121 @@ 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) {
@ -2120,7 +2242,12 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
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()) {

View file

@ -0,0 +1,320 @@
package core
import (
"cmp"
"context"
"fmt"
"runtime"
"slices"
"time"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"golang.org/x/sync/errgroup"
)
// ProcessResultWithMetrics wraps ProcessResult with timing breakdown for BAL block processing.
type ProcessResultWithMetrics struct {
ProcessResult *ProcessResult
PreProcessTime time.Duration
StateTransitionMetrics *state.BALStateTransitionMetrics
ExecTime time.Duration
PostProcessTime time.Duration
}
// errResult wraps an error into a new ProcessResultWithMetrics instance
func errResult(err error) *ProcessResultWithMetrics {
return &ProcessResultWithMetrics{ProcessResult: &ProcessResult{Error: err}}
}
// ParallelStateProcessor is used to execute and verify blocks containing
// access lists.
type ParallelStateProcessor struct {
*StateProcessor
chain *HeaderChain
vmCfg *vm.Config
}
// NewParallelStateProcessor returns a new ParallelStateProcessor instance.
func NewParallelStateProcessor(chain *HeaderChain, vmConfig *vm.Config) *ParallelStateProcessor {
return &ParallelStateProcessor{
StateProcessor: NewStateProcessor(chain),
chain: chain,
vmCfg: vmConfig,
}
}
// execVMConfig returns the subset of the configured VM options that is safe to
// reuse across the parallel per-transaction and post-transaction executions.
// Only the fields explicitly copied here are propagated (mirroring the original
// per-tx behaviour); notably the full caller-supplied config is used only for
// pre-execution in processBlockPreTx.
func (p *ParallelStateProcessor) execVMConfig() vm.Config {
return vm.Config{
NoBaseFee: p.vmCfg.NoBaseFee,
EnablePreimageRecording: p.vmCfg.EnablePreimageRecording,
ExtraEips: slices.Clone(p.vmCfg.ExtraEips),
}
}
// called by resultHandler when all transactions have successfully executed.
// performs post-tx state transition (system contracts and withdrawals)
// and calculates the ProcessResult, returning it to be sent on resCh
// by resultHandler
func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, preTxBAL *bal.ConstructionBlockAccessList, accessList *bal.AccessListReader, statedb *state.StateDB, results []txExecResult) *ProcessResultWithMetrics {
tExec := time.Since(tExecStart)
tPostprocessStart := time.Now()
header := block.Header()
// The post-execution changes are recorded at the BAL index immediately
// following the last transaction.
lastBALIdx := len(block.Transactions()) + 1
postTxState := statedb.WithReader(state.NewReaderWithAccessList(statedb.Reader(), accessList, lastBALIdx))
evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), postTxState, p.chainConfig(), p.execVMConfig())
// 1. order the receipts by tx index
// 2. correctly calculate the cumulative gas used per receipt, returning bad block error if it goes over the allowed
slices.SortFunc(results, func(a, b txExecResult) int {
return cmp.Compare(a.receipt.TransactionIndex, b.receipt.TransactionIndex)
})
var (
// Per-dimension cumulative sums for 2D block gas (EIP-8037).
sumRegular uint64
sumState uint64
cumulativeReceipt uint64 // cumulative receipt gas (what users pay)
allLogs []*types.Log
allReceipts []*types.Receipt
)
for _, result := range results {
sumRegular += result.txRegular
sumState += result.txState
cumulativeReceipt += result.execGas
result.receipt.CumulativeGasUsed = cumulativeReceipt
allLogs = append(allLogs, result.receipt.Logs...)
allReceipts = append(allReceipts, result.receipt)
}
// Block gas = max(sum_regular, sum_state) per EIP-8037.
blockGasUsed := max(sumRegular, sumState)
if blockGasUsed > header.GasLimit {
return errResult(fmt.Errorf("gas limit exceeded"))
}
requests, postBAL, err := PostExecution(context.Background(), p.chainConfig(), block.Number(), block.Time(), allLogs, evm, uint32(lastBALIdx))
if err != nil {
return errResult(err)
}
p.chain.Engine().Finalize(p.chain, block.Header(), evm.StateDB, block.Body(), uint32(lastBALIdx), postBAL)
blockAccessList := bal.NewConstructionBlockAccessList()
blockAccessList.Merge(preTxBAL)
blockAccessList.Merge(postBAL)
for _, res := range results {
blockAccessList.Merge(res.blockAccessList)
}
// TODO: do we move validation to ValidateState?
if block.AccessList().Hash() != blockAccessList.ToEncodingObj().Hash() {
// TODO: expose json string method on encoding block access list and log it here
return errResult(fmt.Errorf("invalid block access list: mismatch between local and remote block access list"))
}
tPostprocess := time.Since(tPostprocessStart)
return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{
Receipts: allReceipts,
Requests: requests,
Logs: allLogs,
GasUsed: blockGasUsed,
Bal: blockAccessList,
},
PostProcessTime: tPostprocess,
ExecTime: tExec,
}
}
type txExecResult struct {
receipt *types.Receipt
err error // non-EVM error which would render the block invalid
execGas uint64 // gas reported on the receipt (what the user pays)
// Per-tx dimensional gas for Amsterdam 2D gas accounting (EIP-8037).
txRegular uint64
txState uint64
blockAccessList *bal.ConstructionBlockAccessList
}
// resultHandler polls until all transactions have finished executing and the
// state root calculation is complete. The result is emitted on resCh.
func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxBAL *bal.ConstructionBlockAccessList, prepared *bal.AccessListReader, statedb *state.StateDB, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) {
// 1. if the block has transactions, receive the execution results from all of them and return an error on resCh if any txs err'd
// 2. once all txs are executed, compute the post-tx state transition and produce the ProcessResult sending it on resCh (or an error if the post-tx state didn't match what is reported in the BAL)
var (
results []txExecResult
cumulativeStateGas uint64
cumulativeRegularGas uint64
execErr error
)
if numTx := len(block.Transactions()); numTx > 0 {
for completed := 0; completed < numTx; completed++ {
res := <-txResCh
if execErr != nil {
// A block-invalidating result was already seen; keep draining so
// the worker goroutines don't block on their sends.
continue
}
switch {
case res.err != nil:
execErr = res.err
default:
bottleneck := max(cumulativeRegularGas+res.txRegular, cumulativeStateGas+res.txState)
if bottleneck > block.GasLimit() {
execErr = fmt.Errorf("block used too much gas in bottleneck dimension: %d. block gas limit is %d", bottleneck, block.GasLimit())
continue
}
cumulativeRegularGas += res.txRegular
cumulativeStateGas += res.txState
results = append(results, res)
}
}
if execErr != nil {
// Drain stateRootCalcResCh so the calcAndVerifyRoot goroutine can exit.
<-stateRootCalcResCh
resCh <- errResult(execErr)
return
}
}
execResults := p.prepareExecResult(block, tExecStart, preTxBAL, prepared, statedb, results)
rootCalcRes := <-stateRootCalcResCh
switch {
case execResults.ProcessResult.Error != nil:
resCh <- execResults
case rootCalcRes.err != nil:
resCh <- errResult(rootCalcRes.err)
default:
execResults.StateTransitionMetrics = rootCalcRes.metrics
resCh <- execResults
}
}
type stateRootCalculationResult struct {
err error
metrics *state.BALStateTransitionMetrics
}
// calcAndVerifyRoot performs the post-state root hash calculation, verifying
// it against what is reported by the block and returning a result on resCh.
func (p *ParallelStateProcessor) calcAndVerifyRoot(block *types.Block, stateTransition *state.BALStateTransition, resCh chan stateRootCalculationResult) {
root := stateTransition.IntermediateRoot(false)
res := stateRootCalculationResult{
metrics: stateTransition.Metrics(),
}
if root != block.Root() {
res.err = fmt.Errorf("state root mismatch. local: %x. remote: %x", root, block.Root())
}
resCh <- res
}
// execTx executes a single transaction returning a result which includes state accessed/modified.
func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transaction, balIdx int, db *state.StateDB, signer types.Signer) *txExecResult {
header := block.Header()
evmContext := NewEVMBlockContext(header, p.chain, nil)
evm := vm.NewEVM(evmContext, db, p.chainConfig(), p.execVMConfig())
msg, err := TransactionToMessage(tx, signer, header.BaseFee)
if err != nil {
return &txExecResult{err: fmt.Errorf("could not apply tx %d [%v]: %w", balIdx, tx.Hash().Hex(), err)}
}
sender, err := signer.Sender(tx)
if err != nil {
return &txExecResult{err: fmt.Errorf("could not recover sender for tx at bal idx %d: %w", balIdx, err)}
}
gp := NewGasPool(block.GasLimit())
// TODO: make precompiled addresses be resolvable from chain config + block
db.Prepare(evm.GetRules(), sender, block.Coinbase(), tx.To(), vm.PrecompiledAddressesCancun, tx.AccessList())
db.SetTxContext(tx.Hash(), balIdx-1, uint32(balIdx))
receipt, txBAL, err := ApplyTransactionWithEVM(msg, gp, db, block.Number(), block.Hash(), evmContext.Time, tx, evm)
if err != nil {
return &txExecResult{err: fmt.Errorf("could not apply tx %d [%v]: %w", balIdx, tx.Hash().Hex(), err)}
}
return &txExecResult{
receipt: receipt,
execGas: receipt.GasUsed,
txRegular: gp.cumulativeRegular,
txState: gp.cumulativeState,
blockAccessList: txBAL,
}
}
func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, parent *types.Header, statedb *state.StateDB, cfg vm.Config) *bal.ConstructionBlockAccessList {
header := block.Header()
evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), statedb, p.chainConfig(), cfg)
return PreExecution(context.Background(), block.BeaconRoot(), parent, p.chainConfig(), evm, block.Number(), block.Time())
}
// Process performs EVM execution and state root computation for a block which is known
// to contain an access list.
func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *state.BALStateTransition, statedb *state.StateDB, cfg vm.Config) (*ProcessResultWithMetrics, error) {
header := block.Header()
signer := types.MakeSigner(p.chainConfig(), header.Number, header.Time)
var (
resCh = make(chan *ProcessResultWithMetrics)
rootCalcResultCh = make(chan stateRootCalculationResult)
txResCh = make(chan txExecResult)
)
// Pre-transaction processing: system-contract updates and the pre-tx BAL.
pStart := time.Now()
startingState := statedb.Copy()
prepared := stateTransition.PreparedAccessList()
preTxBAL := p.processBlockPreTx(block, p.chain.GetHeader(block.ParentHash(), block.NumberU64()), statedb, cfg)
tPreprocess := time.Since(pStart)
// Execute transactions and the state-root calculation in parallel.
tExecStart := time.Now()
go p.resultHandler(block, preTxBAL, prepared, statedb, tExecStart, txResCh, rootCalcResultCh, resCh)
// Workers execute transactions concurrently against per-tx state copies.
// Each worker reports completion (and any block-invalidating error) on
// txResCh, which resultHandler drains. Worker errors therefore flow through
// the channel rather than the errgroup, so the group is used purely to bound
// concurrency and Wait() is intentionally not called.
var workers errgroup.Group
workers.SetLimit(runtime.NumCPU())
for i, tx := range block.Transactions() {
balIdx := i + 1
prestate := startingState.Copy()
workers.Go(func() error {
prestate = prestate.WithReader(state.NewReaderWithAccessList(statedb.Reader(), prepared, balIdx))
txResCh <- *p.execTx(block, tx, balIdx, prestate, signer)
return nil
})
}
go p.calcAndVerifyRoot(block, stateTransition, rootCalcResultCh)
res := <-resCh
if res.ProcessResult.Error != nil {
return nil, res.ProcessResult.Error
}
// TODO: remove preprocess metric ?
res.PreProcessTime = tPreprocess
return res, nil
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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

View file

@ -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
}

View file

@ -96,6 +96,10 @@ func (db *UBTDatabase) Reader(stateRoot common.Hash) (Reader, error) {
return newReader(db.codedb.Reader(), sr), nil
}
func (db *UBTDatabase) ReaderWithPrefetch(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) {
panic("not implemented")
}
// ReadersWithCacheStats creates a pair of state readers that share the same
// underlying state reader and internal state cache, while maintaining separate
// statistics respectively.

View file

@ -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 {

View file

@ -16,14 +16,6 @@
package state
import (
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
)
// The EIP27928 reader utilizes a hierarchical architecture to optimize state
// access during block execution:
//
@ -39,15 +31,13 @@ import (
// This layer provides a "unified view" by merging the pre-transition state
// with mutated states from preceding transactions in the block.
//
// - Tracking Layer: Finally, the readerTracker wraps the execution reader to
// capture all state reads made during a specific transaction. These individual
// reads are subsequently merged to construct a comprehensive access list
// for the entire block.
//
// The architecture can be illustrated by the diagram below:
//
// [ Block Level Access List ] <────────────────┐
// ▲ │ (Merge)
// │ │
// ┌──────────────┴──────────────┐ ┌──────────────┴──────────────┐
// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │
// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │ (Unified View)
// │ (Pre-state + Mutations) │ │ (Pre-state + Mutations) │
// └──────────────┬──────────────┘ └──────────────┬──────────────┘
// │ │
@ -63,11 +53,16 @@ import (
// │ (State & Contract Code) │
// └─────────────────────────────┘
// Note: The block producer, which is responsible for generating the block
// along with the block-level access list, does not maintain the internal
// hierarchy (e.g., PrefetchStateReader or ReaderWithBlockLevelAL).
// Instead, it directly utilizes the readerTracker, wrapped around the
// base reader, to construct the access list.
import (
"sync"
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
)
type fetchTask struct {
addr common.Address
@ -78,16 +73,27 @@ func (t *fetchTask) weight() int { return 1 + len(t.slots) }
type prefetchStateReader struct {
StateReader
tasks []*fetchTask
nThreads int
done chan struct{}
term chan struct{}
closeOnce sync.Once
start time.Time
metrics PrefetchMetrics
}
// nolint:unused
func newPrefetchStateReader(reader StateReader, accessList map[common.Address][]common.Hash, nThreads int) *prefetchStateReader {
type PrefetchMetrics struct {
// the total amount of time it took to complete the scheduled workload
Elapsed time.Duration
}
// PrefetcherMetricer is an object that can expose metrics related to the state
// prefetching.
type PrefetcherMetricer interface {
Metrics() PrefetchMetrics
}
func newPrefetchStateReader(reader StateReader, accessList bal.StorageKeys, nThreads int) *prefetchStateReader {
tasks := make([]*fetchTask, 0, len(accessList))
for addr, slots := range accessList {
tasks = append(tasks, &fetchTask{
@ -105,11 +111,16 @@ func newPrefetchStateReaderInternal(reader StateReader, tasks []*fetchTask, nThr
nThreads: nThreads,
done: make(chan struct{}),
term: make(chan struct{}),
start: time.Now(),
}
go r.prefetch()
return r
}
func (r *prefetchStateReader) Metrics() PrefetchMetrics {
return r.metrics
}
func (r *prefetchStateReader) Close() {
r.closeOnce.Do(func() {
close(r.term)
@ -127,7 +138,10 @@ func (r *prefetchStateReader) Wait() error {
}
func (r *prefetchStateReader) prefetch() {
defer close(r.done)
defer func() {
r.metrics = PrefetchMetrics{time.Since(r.start)}
close(r.done)
}()
if len(r.tasks) == 0 {
return
@ -196,52 +210,104 @@ func (r *prefetchStateReader) process(start, limit int) {
// ReaderWithBlockLevelAccessList provides state access that reflects the
// pre-transition state combined with the mutations made by transactions
// prior to TxIndex.
//
// It is a cheap, per-transaction view over a shared, read-only
// bal.AccessListReader: constructing one is O(1) and every lookup is an
// allocation-free binary search.
type ReaderWithBlockLevelAccessList struct {
Reader
AccessList *bal.ConstructionBlockAccessList
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 preexecution system contract calls.
// - 1 … n for transactions (in block order).
// - n + 1 for postexecution 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,
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)
}

View file

@ -21,6 +21,7 @@ import (
"bytes"
"errors"
"fmt"
"iter"
"maps"
"slices"
"sort"
@ -182,6 +183,13 @@ func New(root common.Hash, db Database) (*StateDB, error) {
return NewWithReader(root, db, reader)
}
// WithReader returns a copy of the statedb instance with the specified reader.
func (s *StateDB) WithReader(reader Reader) *StateDB {
cpy := s.Copy()
cpy.reader = reader
return cpy
}
// NewWithReader creates a new state for the specified state root. Unlike New,
// this function accepts an additional Reader which is bound to the given root.
func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, error) {
@ -1133,12 +1141,15 @@ func (s *StateDB) clearJournalAndRefund() {
// deleteStorage is designed to delete the storage trie of a designated account.
func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) {
return deleteStorage(s.db, s.originalRoot, addrHash, root)
}
func deleteStorage(db Database, originalRoot common.Hash, addrHash common.Hash, root common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) {
var (
nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
storages = make(map[common.Hash]common.Hash) // the set for storage mutations (value is nil)
storageOrigins = make(map[common.Hash]common.Hash) // the set for tracking the original value of slot
)
iteratee, err := s.db.Iteratee(s.originalRoot)
iteratee, err := db.Iteratee(originalRoot)
if err != nil {
return nil, nil, nil, err
}
@ -1567,3 +1578,72 @@ func (s *StateDB) Witness() *stateless.Witness {
func (s *StateDB) AccessEvents() *AccessEvents {
return s.accessEvents
}
// handleDestruction processes all destruction markers and deletes the account
// and associated storage slots if necessary. There are four potential scenarios
// as following:
//
// (a) the account was not existent and be marked as destructed
// (b) the account was not existent and be marked as destructed,
// however, it's resurrected later in the same block.
// (c) the account was existent and be marked as destructed
// (d) the account was existent and be marked as destructed,
// however it's resurrected later in the same block.
//
// In case (a), nothing needs be deleted, nil to nil transition can be ignored.
// In case (b), nothing needs be deleted, nil is used as the original value for
// newly created account and storages
// In case (c), **original** account along with its storages should be deleted,
// with their values be tracked as original value.
// In case (d), **original** account along with its storages should be deleted,
// with their values be tracked as original value.
func handleDestruction(db Database, trie Trie, root common.Hash, noStorageWiping bool, destructions iter.Seq[common.Address], prestates map[common.Address]*types.StateAccount) (map[common.Hash]*AccountDelete, []*trienode.NodeSet, error) {
var (
nodes []*trienode.NodeSet
deletes = make(map[common.Hash]*AccountDelete)
)
for addr := range destructions {
prestate := prestates[addr]
// The account was non-existent, and it's marked as destructed in the scope
// of block. It can be either case (a) or (b) and will be interpreted as
// null->null state transition.
// - for (a), skip it without doing anything
// - for (b), the resurrected account with nil as original will be handled afterwards
if prestate == nil {
continue
}
// The account was existent, it can be either case (c) or (d).
addrHash := crypto.Keccak256Hash(addr.Bytes())
op := &AccountDelete{
Address: addr,
Origin: prestate,
}
deletes[addrHash] = op
// Short circuit if the origin storage was empty.
if prestate.Root == types.EmptyRootHash || db.TrieDB().IsUBT() {
continue
}
if noStorageWiping {
return nil, nil, fmt.Errorf("unexpected storage wiping, %x", addr)
}
// Remove storage slots belonging to the account.
storages, storagesOrigin, set, err := deleteStorage(db, prestate.Root, addrHash, root)
if err != nil {
return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err)
}
op.Storages = storages
op.StoragesOrigin = storagesOrigin
// Aggregate the associated trie node changes.
nodes = append(nodes, set)
}
return deletes, nodes, nil
}
// TODO: find better location for this
type Committer interface {
Commit(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, error)
CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *StateUpdate, error)
Preimages() map[common.Hash][]byte
}

View file

@ -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
}

View file

@ -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
}

View file

@ -400,7 +400,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
obj.SlotChanges = make([]encodingStorageWrite, 0, len(slotWrites))
indices := slices.Collect(maps.Keys(slotWrites))
slices.SortFunc(indices, cmp.Compare)
slices.Sort(indices)
for _, index := range indices {
val := slotWrites[index]
obj.SlotChanges = append(obj.SlotChanges, encodingStorageWrite{
@ -420,7 +420,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
// Convert balance changes
balanceIndices := slices.Collect(maps.Keys(a.BalanceChanges))
slices.SortFunc(balanceIndices, cmp.Compare)
slices.Sort(balanceIndices)
for _, idx := range balanceIndices {
res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{
BlockAccessIndex: idx,
@ -430,7 +430,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
// Convert nonce changes
nonceIndices := slices.Collect(maps.Keys(a.NonceChanges))
slices.SortFunc(nonceIndices, cmp.Compare)
slices.Sort(nonceIndices)
for _, idx := range nonceIndices {
res.NonceChanges = append(res.NonceChanges, encodingAccountNonce{
BlockAccessIndex: idx,
@ -440,7 +440,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
// Convert code change
codeIndices := slices.Collect(maps.Keys(a.CodeChange))
slices.SortFunc(codeIndices, cmp.Compare)
slices.Sort(codeIndices)
for _, idx := range codeIndices {
res.CodeChanges = append(res.CodeChanges, encodingCodeChange{
BlockAccessIndex: idx,

View file

@ -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
}

View file

@ -261,6 +261,12 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
syscall := isSystemCall(caller)
// EIP-7928: per the Amsterdam spec, delegation resolution happens before
// the value-transfer check, so the delegated-to must appear in the BAL
// even when the call later reverts with ErrInsufficientBalance. Touch the
// target's code here (a no-op for non-delegated accounts) to record it.
evm.resolveCode(addr)
// Fail if we're trying to transfer more than the available balance.
if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
@ -346,6 +352,12 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
// EIP-7928: per the Amsterdam spec, delegation resolution happens before
// the value-transfer check, so the delegated-to must appear in the BAL
// even when the call later reverts with ErrInsufficientBalance.
evm.resolveCode(addr)
// Fail if we're trying to transfer more than the available balance
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance

View file

@ -294,6 +294,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
}
options.Overrides = &overrides
options.BALExecutionMode = config.BALExecutionMode
options.BlockingPrefetch = config.BlockingPrefetch
options.PrefetchWorkers = int(config.PrefetchWorkers)
eth.blockchain, err = core.NewBlockChain(chainDb, config.Genesis, eth.engine, options)
if err != nil {
return nil, err

View file

@ -804,10 +804,12 @@ func (api *ConsensusAPI) NewPayloadV5(ctx context.Context, params engine.Executa
return invalidStatus, paramsErr("nil beaconRoot post-cancun")
case executionRequests == nil:
return invalidStatus, paramsErr("nil executionRequests post-prague")
case params.SlotNumber == nil:
return invalidStatus, paramsErr("nil slotnumber post-amsterdam")
case !api.checkFork(params.Timestamp, forks.Amsterdam):
return invalidStatus, unsupportedForkErr("newPayloadV5 must only be called for amsterdam payloads")
case params.SlotNumber == nil:
return invalidStatus, paramsErr("nil slotnumber post-amsterdam")
case params.BlockAccessList == nil:
return invalidStatus, paramsErr("nil block access list post-amsterdam")
}
requests := convertRequests(executionRequests)
if err := validateRequests(requests); err != nil {

View file

@ -19,6 +19,7 @@ package ethconfig
import (
"errors"
"github.com/ethereum/go-ethereum/core/types/bal"
"time"
"github.com/ethereum/go-ethereum/common"
@ -224,6 +225,10 @@ type Config struct {
// RangeLimit restricts the maximum range (end - start) for range queries.
RangeLimit uint64 `toml:",omitempty"`
BALExecutionMode bal.BALExecutionMode
PrefetchWorkers uint
BlockingPrefetch bool
}
// CreateConsensusEngine creates a consensus engine for the given chain config.

View file

@ -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
}

View file

@ -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.

View file

@ -42,6 +42,7 @@ var (
rlpTestDir = filepath.Join(baseDir, "RLPTests")
difficultyTestDir = filepath.Join(baseDir, "BasicTests")
executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests")
executionSpecBALBlockchainTestDir = filepath.Join(".", "spec-tests-bal", "fixtures", "blockchain_tests")
executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests")
executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests")
benchmarksDir = filepath.Join(".", "evm-benchmarks", "benchmarks")