go-ethereum/core/parallel_state_processor.go

371 lines
12 KiB
Go

package core
import (
"cmp"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc"
"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"
"github.com/ethereum/go-ethereum/params"
"golang.org/x/sync/errgroup"
"slices"
"time"
)
// ProcessResultWithMetrics wraps ProcessResult with some metrics that are
// emitted when executing blocks containing access lists.
type ProcessResultWithMetrics struct {
ProcessResult *ProcessResult
// the time it took to load modified prestate accounts from disk and instantiate statedbs for execution
PreProcessTime time.Duration
// the time it took to validate the block post transaction execution and state root calculation
PostProcessTime time.Duration
// the time it took to hash the state root, including intermediate node reads
RootCalcTime time.Duration
// the time that it took to load the prestate for accounts that were updated as part of
// the state root update
PrestateLoadTime time.Duration
// the time it took to execute all txs in the block
ExecTime time.Duration
}
// 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(config *params.ChainConfig, chain *HeaderChain, cfg *vm.Config) ParallelStateProcessor {
res := NewStateProcessor(config, chain)
return ParallelStateProcessor{
res,
cfg,
}
}
// 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, allStateReads *bal.StateAccesses, tExecStart time.Time, postTxState *state.StateDB, receipts types.Receipts) *ProcessResultWithMetrics {
tExec := time.Since(tExecStart)
var requests [][]byte
tPostprocessStart := time.Now()
header := block.Header()
postTxState.SetAccessListIndex(len(block.Transactions()) + 1)
var tracingStateDB = vm.StateDB(postTxState)
if hooks := p.vmCfg.Tracer; hooks != nil {
tracingStateDB = state.NewHookedState(postTxState, hooks)
}
context := NewEVMBlockContext(header, p.chain, nil)
evm := vm.NewEVM(context, tracingStateDB, p.config, *p.vmCfg)
// 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(receipts, func(a, b *types.Receipt) int {
return cmp.Compare(a.TransactionIndex, b.TransactionIndex)
})
var cumulativeGasUsed uint64
var allLogs []*types.Log
for _, receipt := range receipts {
receipt.CumulativeGasUsed = cumulativeGasUsed + receipt.GasUsed
cumulativeGasUsed += receipt.GasUsed
if receipt.CumulativeGasUsed > header.GasLimit {
return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{Error: fmt.Errorf("gas limit exceeded")},
}
}
allLogs = append(allLogs, receipt.Logs...)
}
computedDiff := &bal.StateDiff{make(map[common.Address]*bal.AccountState)}
computedAccesses := make(bal.StateAccesses)
// Read requests if Prague is enabled.
if p.config.IsPrague(block.Number(), block.Time()) {
requests = [][]byte{}
// EIP-6110
if err := ParseDepositLogs(&requests, allLogs, p.config); err != nil {
return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{Error: err},
}
}
// EIP-7002
diff, accesses, err := ProcessWithdrawalQueue(&requests, evm)
if err != nil {
return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{Error: err},
}
}
computedDiff = diff
computedAccesses = *accesses
// EIP-7251
diff, accesses, err = ProcessConsolidationQueue(&requests, evm)
if err != nil {
return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{Error: err},
}
}
computedDiff.Merge(diff)
computedAccesses.Merge(*accesses)
}
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
p.chain.engine.Finalize(p.chain, header, tracingStateDB, block.Body())
// invoke Finalise so that withdrawals are accounted for in the state diff
postTxState.Finalise(true)
if err := postTxState.BlockAccessList().ValidateStateDiff(len(block.Transactions())+1, computedDiff); err != nil {
return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{Error: err},
}
}
allStateReads.Merge(computedAccesses)
if err := postTxState.BlockAccessList().ValidateStateReads(*allStateReads); err != nil {
return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{Error: err},
}
}
tPostprocess := time.Since(tPostprocessStart)
return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{
Receipts: receipts,
Requests: requests,
Logs: allLogs,
GasUsed: cumulativeGasUsed,
},
PostProcessTime: tPostprocess,
ExecTime: tExec,
}
}
type txExecResult struct {
idx int // transaction index
receipt *types.Receipt
err error // non-EVM error which would render the block invalid
mutations *bal.StateDiff
stateReads *bal.StateAccesses
}
// 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, preTxStateReads *bal.StateAccesses, postTxState *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 receipts []*types.Receipt
gp := new(GasPool)
gp.SetGas(block.GasLimit())
var execErr error
var numTxComplete int
allReads := make(bal.StateAccesses)
allReads.Merge(*preTxStateReads)
if len(block.Transactions()) > 0 {
loop:
for {
select {
case res := <-txResCh:
if execErr == nil {
if res.err != nil {
execErr = res.err
} else {
if err := gp.SubGas(res.receipt.GasUsed); err != nil {
execErr = err
} else {
receipts = append(receipts, res.receipt)
allReads.Merge(*res.stateReads)
}
}
}
numTxComplete++
if numTxComplete == len(block.Transactions()) {
break loop
}
}
}
if execErr != nil {
resCh <- &ProcessResultWithMetrics{ProcessResult: &ProcessResult{Error: execErr}}
return
}
}
execResults := p.prepareExecResult(block, &allReads, tExecStart, postTxState, receipts)
rootCalcRes := <-stateRootCalcResCh
if execResults.ProcessResult.Error != nil {
resCh <- execResults
} else if rootCalcRes.err != nil {
resCh <- &ProcessResultWithMetrics{ProcessResult: &ProcessResult{Error: rootCalcRes.err}}
} else {
execResults.RootCalcTime = rootCalcRes.rootCalcTime
execResults.PrestateLoadTime = rootCalcRes.prestateLoadTime
resCh <- execResults
}
}
type stateRootCalculationResult struct {
err error
prestateLoadTime time.Duration
rootCalcTime time.Duration
root common.Hash
}
// 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(preState *state.StateDB, block *types.Block, resCh chan stateRootCalculationResult) {
// calculate and apply the block state modifications
root, prestateLoadTime, rootCalcTime := preState.BlockAccessList().StateRoot(preState)
res := stateRootCalculationResult{
root: root,
prestateLoadTime: prestateLoadTime,
rootCalcTime: rootCalcTime,
}
if root != block.Root() {
res.err = fmt.Errorf("state root mismatch. local: %x. remote: %x", root, block.Root())
}
resCh <- res
}
// execTx executes single transaction returning a result which includes state accessed/modified
func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transaction, idx int, db *state.StateDB, signer types.Signer) *txExecResult {
header := block.Header()
balTracer, hooks := NewBlockAccessListTracer()
tracingStateDB := state.NewHookedState(db, hooks)
context := NewEVMBlockContext(header, p.chain, nil)
evm := vm.NewEVM(context, tracingStateDB, p.config, *p.vmCfg)
msg, err := TransactionToMessage(tx, signer, header.BaseFee)
if err != nil {
err = fmt.Errorf("could not apply tx %d [%v]: %w", idx, tx.Hash().Hex(), err)
return &txExecResult{err: err}
}
sender, _ := types.Sender(signer, tx)
db.SetTxSender(sender)
db.SetTxContext(tx.Hash(), idx)
db.SetAccessListIndex(idx + 1)
evm.StateDB = db
gp := new(GasPool)
gp.SetGas(block.GasLimit())
var gasUsed uint64
mutatedState, accessedState, receipt, err := ApplyTransactionWithEVM(msg, gp, db, block.Number(), block.Hash(), context.Time, tx, &gasUsed, evm)
if err != nil {
err := fmt.Errorf("could not apply tx %d [%v]: %w", idx, tx.Hash().Hex(), err)
return &txExecResult{err: err}
}
if err := db.BlockAccessList().ValidateStateDiff(idx+1, balTracer.AccessList().DiffAt(uint16(idx)+1)); err != nil {
return &txExecResult{err: err}
}
return &txExecResult{
idx: idx,
receipt: receipt,
mutations: mutatedState,
stateReads: accessedState,
}
}
// 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, statedb *state.StateDB, cfg vm.Config) (*ProcessResultWithMetrics, error) {
fmt.Println("start ParallelProcess")
var (
header = block.Header()
resCh = make(chan *ProcessResultWithMetrics)
signer = types.MakeSigner(p.config, header.Number, header.Time)
)
txResCh := make(chan txExecResult)
pStart := time.Now()
var (
tPreprocess time.Duration // time to create a set of prestates for parallel transaction execution
tExecStart time.Time
rootCalcResultCh = make(chan stateRootCalculationResult)
)
// Mutate the block and state according to any hard-fork specs
if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 {
misc.ApplyDAOHardFork(statedb)
}
var (
context vm.BlockContext
)
alReader := state.NewBALReader(block, statedb)
statedb.SetBlockAccessList(alReader)
// Apply pre-execution system calls.
var tracingStateDB = vm.StateDB(statedb)
if hooks := cfg.Tracer; hooks != nil {
tracingStateDB = state.NewHookedState(statedb, hooks)
}
context = NewEVMBlockContext(header, p.chain, nil)
evm := vm.NewEVM(context, tracingStateDB, p.config, cfg)
// validate the correctness of pre-transaction execution state changes
computedPreTxDiff := &bal.StateDiff{make(map[common.Address]*bal.AccountState)}
preTxStateReads := make(bal.StateAccesses)
if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
diff, reads := ProcessBeaconBlockRoot(*beaconRoot, evm)
computedPreTxDiff.Merge(diff)
preTxStateReads.Merge(*reads)
}
if p.config.IsPrague(block.Number(), block.Time()) || p.config.IsVerkle(block.Number(), block.Time()) {
diff, reads := ProcessParentBlockHash(block.ParentHash(), evm)
computedPreTxDiff.Merge(diff)
preTxStateReads.Merge(*reads)
}
if err := statedb.BlockAccessList().ValidateStateDiff(0, computedPreTxDiff); err != nil {
return nil, err
}
// compute the post-tx state prestate (before applying final block system calls and eip-4895 withdrawals)
// the post-tx state transition is verified by resultHandler
postTxState := statedb.Copy()
tPreprocess = time.Since(pStart)
// execute transactions and state root calculation in parallel
tExecStart = time.Now()
go p.resultHandler(block, &preTxStateReads, postTxState, tExecStart, txResCh, rootCalcResultCh, resCh)
var workers errgroup.Group
startingState := statedb.Copy()
for i, tx := range block.Transactions() {
tx := tx
i := i
workers.Go(func() error {
res := p.execTx(block, tx, i, startingState.Copy(), signer)
txResCh <- *res
return nil
})
}
go p.calcAndVerifyRoot(statedb, block, rootCalcResultCh)
res := <-resCh
if res.ProcessResult.Error != nil {
return nil, res.ProcessResult.Error
}
res.PreProcessTime = tPreprocess
// res.PreProcessLoadTime = tPreprocessLoad
return res, nil
}