go-ethereum/rollup/tracing/tracing.go
2024-06-14 15:02:00 +03:00

604 lines
20 KiB
Go

package tracing
import (
"bytes"
"errors"
"fmt"
"runtime"
"sync"
"time"
"github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/common/hexutil"
"github.com/scroll-tech/go-ethereum/consensus"
"github.com/scroll-tech/go-ethereum/core"
"github.com/scroll-tech/go-ethereum/core/rawdb"
"github.com/scroll-tech/go-ethereum/core/state"
"github.com/scroll-tech/go-ethereum/core/types"
"github.com/scroll-tech/go-ethereum/core/vm"
"github.com/scroll-tech/go-ethereum/eth/tracers"
_ "github.com/scroll-tech/go-ethereum/eth/tracers/native"
"github.com/scroll-tech/go-ethereum/ethdb"
"github.com/scroll-tech/go-ethereum/log"
"github.com/scroll-tech/go-ethereum/metrics"
"github.com/scroll-tech/go-ethereum/params"
"github.com/scroll-tech/go-ethereum/rollup/fees"
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
"github.com/scroll-tech/go-ethereum/rollup/withdrawtrie"
)
var (
getTxResultTimer = metrics.NewRegisteredTimer("rollup/tracing/get_tx_result", nil)
getTxResultApplyMessageTimer = metrics.NewRegisteredTimer("rollup/tracing/get_tx_result/apply_message", nil)
getTxResultZkTrieBuildTimer = metrics.NewRegisteredTimer("rollup/tracing/get_tx_result/zk_trie_build", nil)
getTxResultTracerResultTimer = metrics.NewRegisteredTimer("rollup/tracing/get_tx_result/tracer_result", nil)
feedTxToTracerTimer = metrics.NewRegisteredTimer("rollup/tracing/feed_tx_to_tracer", nil)
fillBlockTraceTimer = metrics.NewRegisteredTimer("rollup/tracing/fill_block_trace", nil)
)
// TracerWrapper implements ScrollTracerWrapper interface
type TracerWrapper struct{}
// NewTracerWrapper TracerWrapper creates a new TracerWrapper
func NewTracerWrapper() *TracerWrapper {
return &TracerWrapper{}
}
// CreateTraceEnvAndGetBlockTrace wraps the whole block tracing logic for a block
func (tw *TracerWrapper) CreateTraceEnvAndGetBlockTrace(chainConfig *params.ChainConfig, chainContext core.ChainContext, engine consensus.Engine, chaindb ethdb.Database, statedb *state.StateDB, parent *types.Block, block *types.Block, commitAfterApply bool) (*types.BlockTrace, error) {
traceEnv, err := CreateTraceEnv(chainConfig, chainContext, engine, chaindb, statedb, parent, block, commitAfterApply)
if err != nil {
return nil, err
}
return traceEnv.GetBlockTrace(block)
}
type TraceEnv struct {
logConfig *vm.LogConfig
commitAfterApply bool
chainConfig *params.ChainConfig
coinbase common.Address
// rMu lock is used to protect txs executed in parallel.
signer types.Signer
state *state.StateDB
blockCtx vm.BlockContext
// pMu lock is used to protect Proofs' read and write mutual exclusion,
// since txs are executed in parallel, so this lock is required.
pMu sync.Mutex
// sMu is required because of txs are executed in parallel,
// this lock is used to protect StorageTrace's read and write mutual exclusion.
sMu sync.Mutex
*types.StorageTrace
TxStorageTraces []*types.StorageTrace
// zktrie tracer is used for zktrie storage to build additional deletion proof
ZkTrieTracer map[string]state.ZktrieProofTracer
ExecutionResults []*types.ExecutionResult
// StartL1QueueIndex is the next L1 message queue index that this block can process.
// Example: If the parent block included QueueIndex=9, then StartL1QueueIndex will
// be 10.
StartL1QueueIndex uint64
}
// Context is the same as Context in eth/tracers/tracers.go
type Context struct {
BlockHash common.Hash
TxIndex int
TxHash common.Hash
}
// txTraceTask is the same as txTraceTask in eth/tracers/api.go
type txTraceTask struct {
statedb *state.StateDB
index int
}
func CreateTraceEnvHelper(chainConfig *params.ChainConfig, logConfig *vm.LogConfig, blockCtx vm.BlockContext, startL1QueueIndex uint64, coinbase common.Address, statedb *state.StateDB, rootBefore common.Hash, block *types.Block, commitAfterApply bool) *TraceEnv {
return &TraceEnv{
logConfig: logConfig,
commitAfterApply: commitAfterApply,
chainConfig: chainConfig,
coinbase: coinbase,
signer: types.MakeSigner(chainConfig, block.Number()),
state: statedb,
blockCtx: blockCtx,
StorageTrace: &types.StorageTrace{
RootBefore: rootBefore,
RootAfter: block.Root(),
Proofs: make(map[string][]hexutil.Bytes),
StorageProofs: make(map[string]map[string][]hexutil.Bytes),
},
ZkTrieTracer: make(map[string]state.ZktrieProofTracer),
ExecutionResults: make([]*types.ExecutionResult, block.Transactions().Len()),
TxStorageTraces: make([]*types.StorageTrace, block.Transactions().Len()),
StartL1QueueIndex: startL1QueueIndex,
}
}
func CreateTraceEnv(chainConfig *params.ChainConfig, chainContext core.ChainContext, engine consensus.Engine, chaindb ethdb.Database, statedb *state.StateDB, parent *types.Block, block *types.Block, commitAfterApply bool) (*TraceEnv, error) {
var coinbase common.Address
var err error
if chainConfig.Scroll.FeeVaultEnabled() {
coinbase = *chainConfig.Scroll.FeeVaultAddress
} else {
coinbase, err = engine.Author(block.Header())
if err != nil {
log.Warn("recover coinbase in CreateTraceEnv fail. using zero-address", "err", err, "blockNumber", block.Header().Number, "headerHash", block.Header().Hash())
}
}
// Collect start queue index, we should always have this value for blocks
// that have been executed.
// FIXME: This value will be incorrect on the signer, since we reuse this
// DB entry to signal which index the worker should continue from.
// Example: Ledger A <-- B <-- C. Block `A` contains up to `QueueIndex=9`.
// For block `B`, the worker skips 10 messages and includes 0.
// `ReadFirstQueueIndexNotInL2Block(B)` will then return `20` on the
// signer to avoid re-processing the same 10 transactions again for
// block `C`.
// `ReadFirstQueueIndexNotInL1Block(B)` will return the correct value
// `10` on follower nodes.
startL1QueueIndex := rawdb.ReadFirstQueueIndexNotInL2Block(chaindb, parent.Hash())
if startL1QueueIndex == nil {
log.Error("missing FirstQueueIndexNotInL2Block for block during trace call", "number", parent.NumberU64(), "hash", parent.Hash())
return nil, fmt.Errorf("missing FirstQueueIndexNotInL2Block for block during trace call: hash=%v, parentHash=%vv", block.Hash(), parent.Hash())
}
env := CreateTraceEnvHelper(
chainConfig,
&vm.LogConfig{
DisableStorage: true,
DisableStack: true,
EnableMemory: false,
EnableReturnData: true,
},
core.NewEVMBlockContext(block.Header(), chainContext, chainConfig, nil),
*startL1QueueIndex,
coinbase,
statedb,
parent.Root(),
block,
commitAfterApply,
)
key := coinbase.String()
if _, exist := env.Proofs[key]; !exist {
proof, err := env.state.GetProof(coinbase)
if err != nil {
log.Error("Proof for coinbase not available", "coinbase", coinbase, "error", err)
// but we still mark the proofs map with nil array
}
env.Proofs[key] = types.WrapProof(proof)
}
return env, nil
}
func (env *TraceEnv) GetBlockTrace(block *types.Block) (*types.BlockTrace, error) {
// Execute all the transaction contained within the block concurrently
var (
txs = block.Transactions()
pend = new(sync.WaitGroup)
jobs = make(chan *txTraceTask, len(txs))
errCh = make(chan error, 1)
)
threads := runtime.NumCPU()
if threads > len(txs) {
threads = len(txs)
}
for th := 0; th < threads; th++ {
pend.Add(1)
go func() {
defer func(t time.Time) {
pend.Done()
getTxResultTimer.Update(time.Since(t))
}(time.Now())
// Fetch and execute the next transaction trace tasks
for task := range jobs {
if err := env.getTxResult(task.statedb, task.index, block); err != nil {
select {
case errCh <- err:
default:
}
log.Error(
"failed to trace tx",
"txHash", txs[task.index].Hash().String(),
"blockHash", block.Hash().String(),
"blockNumber", block.NumberU64(),
"err", err,
)
}
}
}()
}
// Feed the transactions into the tracers and return
var failed error
common.WithTimer(feedTxToTracerTimer, func() {
for i, tx := range txs {
// Send the trace task over for execution
jobs <- &txTraceTask{statedb: env.state.Copy(), index: i}
// Generate the next state snapshot fast without tracing
msg, _ := tx.AsMessage(env.signer, block.BaseFee())
env.state.SetTxContext(tx.Hash(), i)
vmenv := vm.NewEVM(env.blockCtx, core.NewEVMTxContext(msg), env.state, env.chainConfig, vm.Config{})
l1DataFee, err := fees.CalculateL1DataFee(tx, env.state, env.chainConfig, block.Number())
if err != nil {
failed = err
break
}
if _, err = core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), l1DataFee); err != nil {
failed = err
break
}
if env.commitAfterApply {
env.state.Finalise(vmenv.ChainConfig().IsEIP158(block.Number()))
}
}
})
close(jobs)
pend.Wait()
// after all tx has been traced, collect "deletion proof" for zktrie
for _, tracer := range env.ZkTrieTracer {
delProofs, err := tracer.GetDeletionProofs()
if err != nil {
log.Error("deletion proof failure", "error", err)
} else {
for _, proof := range delProofs {
env.DeletionProofs = append(env.DeletionProofs, proof)
}
}
}
// build dummy per-tx deletion proof
for _, txStorageTrace := range env.TxStorageTraces {
if txStorageTrace != nil {
txStorageTrace.DeletionProofs = env.DeletionProofs
}
}
// If execution failed in between, abort
select {
case err := <-errCh:
return nil, err
default:
if failed != nil {
return nil, failed
}
}
return env.fillBlockTrace(block)
}
func (env *TraceEnv) getTxResult(state *state.StateDB, index int, block *types.Block) error {
tx := block.Transactions()[index]
msg, _ := tx.AsMessage(env.signer, block.BaseFee())
from, _ := types.Sender(env.signer, tx)
to := tx.To()
txctx := &Context{
BlockHash: block.TxHash(),
TxIndex: index,
TxHash: tx.Hash(),
}
sender := &types.AccountWrapper{
Address: from,
Nonce: state.GetNonce(from),
Balance: (*hexutil.Big)(state.GetBalance(from)),
KeccakCodeHash: state.GetKeccakCodeHash(from),
PoseidonCodeHash: state.GetPoseidonCodeHash(from),
CodeSize: state.GetCodeSize(from),
}
var receiver *types.AccountWrapper
if to != nil {
receiver = &types.AccountWrapper{
Address: *to,
Nonce: state.GetNonce(*to),
Balance: (*hexutil.Big)(state.GetBalance(*to)),
KeccakCodeHash: state.GetKeccakCodeHash(*to),
PoseidonCodeHash: state.GetPoseidonCodeHash(*to),
CodeSize: state.GetCodeSize(*to),
}
}
txContext := core.NewEVMTxContext(msg)
tracerContext := tracers.Context{
BlockHash: block.Hash(),
TxIndex: index,
TxHash: tx.Hash(),
}
callTracer, err := tracers.New("callTracer", &tracerContext)
if err != nil {
return fmt.Errorf("failed to create callTracer: %w", err)
}
prestateTracer, err := tracers.New("prestateTracer", &tracerContext)
if err != nil {
return fmt.Errorf("failed to create prestateTracer: %w", err)
}
applyMessageStart := time.Now()
structLogger := vm.NewStructLogger(env.logConfig)
tracer := NewMuxTracer(structLogger, callTracer, prestateTracer)
// Run the transaction with tracing enabled.
vmenv := vm.NewEVM(env.blockCtx, txContext, state, env.chainConfig, vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true})
// Call Prepare to clear out the statedb access list
state.SetTxContext(txctx.TxHash, txctx.TxIndex)
// Computes the new state by applying the given message.
l1DataFee, err := fees.CalculateL1DataFee(tx, state, env.chainConfig, block.Number())
if err != nil {
return err
}
result, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), l1DataFee)
if err != nil {
getTxResultApplyMessageTimer.UpdateSince(applyMessageStart)
return err
}
getTxResultApplyMessageTimer.UpdateSince(applyMessageStart)
// If the result contains a revert reason, return it.
returnVal := result.Return()
if len(result.Revert()) > 0 {
returnVal = result.Revert()
}
createdAcc := structLogger.CreatedAccount()
var after []*types.AccountWrapper
if to == nil {
if createdAcc == nil {
return errors.New("unexpected tx: address for created contract unavailable")
}
to = &createdAcc.Address
}
// collect affected account after tx being applied
for _, acc := range []common.Address{from, *to, env.coinbase} {
after = append(after, &types.AccountWrapper{
Address: acc,
Nonce: state.GetNonce(acc),
Balance: (*hexutil.Big)(state.GetBalance(acc)),
KeccakCodeHash: state.GetKeccakCodeHash(acc),
PoseidonCodeHash: state.GetPoseidonCodeHash(acc),
CodeSize: state.GetCodeSize(acc),
})
}
txStorageTrace := &types.StorageTrace{
Proofs: make(map[string][]hexutil.Bytes),
StorageProofs: make(map[string]map[string][]hexutil.Bytes),
}
// still we have no state root for per tx, only set the head and tail
if index == 0 {
txStorageTrace.RootBefore = state.GetRootHash()
}
if index == len(block.Transactions())-1 {
txStorageTrace.RootAfter = block.Root()
}
// merge required proof data
proofAccounts := structLogger.UpdatedAccounts()
proofAccounts[vmenv.FeeRecipient()] = struct{}{}
for addr := range proofAccounts {
addrStr := addr.String()
env.pMu.Lock()
checkedProof, existed := env.Proofs[addrStr]
if existed {
txStorageTrace.Proofs[addrStr] = checkedProof
}
env.pMu.Unlock()
if existed {
continue
}
proof, err := state.GetProof(addr)
if err != nil {
log.Error("Proof not available", "address", addrStr, "error", err)
// but we still mark the proofs map with nil array
}
wrappedProof := types.WrapProof(proof)
env.pMu.Lock()
env.Proofs[addrStr] = wrappedProof
txStorageTrace.Proofs[addrStr] = wrappedProof
env.pMu.Unlock()
}
zkTrieBuildStart := time.Now()
proofStorages := structLogger.UpdatedStorages()
for addr, keys := range proofStorages {
if _, existed := txStorageTrace.StorageProofs[addr.String()]; !existed {
txStorageTrace.StorageProofs[addr.String()] = make(map[string][]hexutil.Bytes)
}
env.sMu.Lock()
trie, err := state.GetStorageTrieForProof(addr)
if err != nil {
// but we still continue to next address
log.Error("Storage trie not available", "error", err, "address", addr)
env.sMu.Unlock()
continue
}
zktrieTracer := state.NewProofTracer(trie)
env.sMu.Unlock()
for key, values := range keys {
addrStr := addr.String()
keyStr := key.String()
isDelete := bytes.Equal(values.Bytes(), common.Hash{}.Bytes())
txm := txStorageTrace.StorageProofs[addrStr]
env.sMu.Lock()
m, existed := env.StorageProofs[addrStr]
if !existed {
m = make(map[string][]hexutil.Bytes)
env.StorageProofs[addrStr] = m
}
if zktrieTracer.Available() && !env.ZkTrieTracer[addrStr].Available() {
env.ZkTrieTracer[addrStr] = state.NewProofTracer(trie)
}
if proof, existed := m[keyStr]; existed {
txm[keyStr] = proof
// still need to touch tracer for deletion
if isDelete && zktrieTracer.Available() {
env.ZkTrieTracer[addrStr].MarkDeletion(key)
}
env.sMu.Unlock()
continue
}
env.sMu.Unlock()
var proof [][]byte
var err error
if zktrieTracer.Available() {
proof, err = state.GetSecureTrieProof(zktrieTracer, key)
} else {
proof, err = state.GetSecureTrieProof(trie, key)
}
if err != nil {
log.Error("Storage proof not available", "error", err, "address", addrStr, "key", keyStr)
// but we still mark the proofs map with nil array
}
wrappedProof := types.WrapProof(proof)
env.sMu.Lock()
txm[keyStr] = wrappedProof
m[keyStr] = wrappedProof
if zktrieTracer.Available() {
if isDelete {
zktrieTracer.MarkDeletion(key)
}
env.ZkTrieTracer[addrStr].Merge(zktrieTracer)
}
env.sMu.Unlock()
}
}
getTxResultZkTrieBuildTimer.UpdateSince(zkTrieBuildStart)
tracerResultTimer := time.Now()
callTrace, err := callTracer.GetResult()
if err != nil {
return fmt.Errorf("failed to get callTracer result: %w", err)
}
prestate, err := prestateTracer.GetResult()
if err != nil {
return fmt.Errorf("failed to get prestateTracer result: %w", err)
}
getTxResultTracerResultTimer.UpdateSince(tracerResultTimer)
env.ExecutionResults[index] = &types.ExecutionResult{
From: sender,
To: receiver,
AccountCreated: createdAcc,
AccountsAfter: after,
L1DataFee: (*hexutil.Big)(result.L1DataFee),
Gas: result.UsedGas,
Failed: result.Failed(),
ReturnValue: fmt.Sprintf("%x", returnVal),
StructLogs: vm.FormatLogs(structLogger.StructLogs()),
CallTrace: callTrace,
Prestate: prestate,
}
env.TxStorageTraces[index] = txStorageTrace
return nil
}
// fillBlockTrace content after all the txs are finished running.
func (env *TraceEnv) fillBlockTrace(block *types.Block) (*types.BlockTrace, error) {
defer func(t time.Time) {
fillBlockTraceTimer.Update(time.Since(t))
}(time.Now())
statedb := env.state
txs := make([]*types.TransactionData, block.Transactions().Len())
for i, tx := range block.Transactions() {
txs[i] = types.NewTransactionData(tx, block.NumberU64(), env.chainConfig)
}
intrinsicStorageProofs := map[common.Address][]common.Hash{
rcfg.L2MessageQueueAddress: {rcfg.WithdrawTrieRootSlot},
rcfg.L1GasPriceOracleAddress: {
rcfg.L1BaseFeeSlot,
rcfg.OverheadSlot,
rcfg.ScalarSlot,
rcfg.L1BlobBaseFeeSlot,
rcfg.CommitScalarSlot,
rcfg.BlobScalarSlot,
rcfg.IsCurieSlot,
},
}
for addr, storages := range intrinsicStorageProofs {
if _, existed := env.Proofs[addr.String()]; !existed {
if proof, err := statedb.GetProof(addr); err != nil {
log.Error("Proof for intrinstic address not available", "error", err, "address", addr)
} else {
env.Proofs[addr.String()] = types.WrapProof(proof)
}
}
if _, existed := env.StorageProofs[addr.String()]; !existed {
env.StorageProofs[addr.String()] = make(map[string][]hexutil.Bytes)
}
for _, slot := range storages {
if _, existed := env.StorageProofs[addr.String()][slot.String()]; !existed {
if trie, err := statedb.GetStorageTrieForProof(addr); err != nil {
log.Error("Storage proof for intrinstic address not available", "error", err, "address", addr)
} else if proof, _ := statedb.GetSecureTrieProof(trie, slot); err != nil {
log.Error("Get storage proof for intrinstic address failed", "error", err, "address", addr, "slot", slot)
} else {
env.StorageProofs[addr.String()][slot.String()] = types.WrapProof(proof)
}
}
}
}
var chainID uint64
if env.chainConfig.ChainID != nil {
chainID = env.chainConfig.ChainID.Uint64()
}
blockTrace := &types.BlockTrace{
ChainID: chainID,
Version: params.ArchiveVersion(params.CommitHash),
Coinbase: &types.AccountWrapper{
Address: env.coinbase,
Nonce: statedb.GetNonce(env.coinbase),
Balance: (*hexutil.Big)(statedb.GetBalance(env.coinbase)),
KeccakCodeHash: statedb.GetKeccakCodeHash(env.coinbase),
PoseidonCodeHash: statedb.GetPoseidonCodeHash(env.coinbase),
CodeSize: statedb.GetCodeSize(env.coinbase),
},
Header: block.Header(),
StorageTrace: env.StorageTrace,
ExecutionResults: env.ExecutionResults,
TxStorageTraces: env.TxStorageTraces,
Transactions: txs,
StartL1QueueIndex: env.StartL1QueueIndex,
}
for i, tx := range block.Transactions() {
evmTrace := env.ExecutionResults[i]
// Contract is created.
if tx.To() == nil {
evmTrace.ByteCode = hexutil.Encode(tx.Data())
} else { // contract call be included at this case, specially fallback call's data is empty.
evmTrace.ByteCode = hexutil.Encode(statedb.GetCode(*tx.To()))
// Get tx.to address's code hash.
codeHash := statedb.GetPoseidonCodeHash(*tx.To())
evmTrace.PoseidonCodeHash = &codeHash
}
}
blockTrace.WithdrawTrieRoot = withdrawtrie.ReadWTRSlot(rcfg.L2MessageQueueAddress, env.state)
return blockTrace, nil
}