go-ethereum/eth/tracers/api_blocktrace.go
Ho c913b3e2f1
fix(zktrie): fix deletion proofs and collect them in commiting phase (#263)
* new deletion proof

* complete tracer and test

* extend proof for parallel tracing

* integrating into blocktrace

* lint

* deduplication of deletion proofs

* fix an issue on marking deletion

* fixs since last review

* Update version.go

---------

Co-authored-by: HAOYUatHZ <37070449+HAOYUatHZ@users.noreply.github.com>
Co-authored-by: Ubuntu <ubuntu@ip-10-2-9-63.us-west-2.compute.internal>
2023-05-08 15:40:54 +08:00

447 lines
14 KiB
Go

package tracers
import (
"bytes"
"context"
"errors"
"fmt"
"runtime"
"sync"
"github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/common/hexutil"
"github.com/scroll-tech/go-ethereum/core"
"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/log"
"github.com/scroll-tech/go-ethereum/params"
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
"github.com/scroll-tech/go-ethereum/rollup/withdrawtrie"
"github.com/scroll-tech/go-ethereum/rpc"
"github.com/scroll-tech/go-ethereum/trie/zkproof"
)
type TraceBlock interface {
GetBlockTraceByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, config *TraceConfig) (trace *types.BlockTrace, err error)
}
type traceEnv struct {
config *TraceConfig
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
// zktrie tracer is used for zktrie storage to build additional deletion proof
zkTrieTracer map[string]state.ZktrieProofTracer
executionResults []*types.ExecutionResult
}
// GetBlockTraceByNumberOrHash replays the block and returns the structured BlockTrace by hash or number.
func (api *API) GetBlockTraceByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, config *TraceConfig) (trace *types.BlockTrace, err error) {
var block *types.Block
if number, ok := blockNrOrHash.Number(); ok {
block, err = api.blockByNumber(ctx, number)
}
if hash, ok := blockNrOrHash.Hash(); ok {
block, err = api.blockByHash(ctx, hash)
}
if err != nil {
return nil, err
}
if block.NumberU64() == 0 {
return nil, errors.New("genesis is not traceable")
}
if config == nil {
config = &TraceConfig{
LogConfig: &vm.LogConfig{
EnableMemory: false,
EnableReturnData: true,
},
}
} else if config.Tracer != nil {
config.Tracer = nil
log.Warn("Tracer params is unsupported")
}
// create current execution environment.
env, err := api.createTraceEnv(ctx, config, block)
if err != nil {
return nil, err
}
return api.getBlockTrace(block, env)
}
// Make trace environment for current block.
func (api *API) createTraceEnv(ctx context.Context, config *TraceConfig, block *types.Block) (*traceEnv, error) {
parent, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(block.NumberU64()-1), block.ParentHash())
if err != nil {
return nil, err
}
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, true)
if err != nil {
return nil, err
}
// get coinbase
var coinbase common.Address
if api.backend.ChainConfig().Scroll.FeeVaultEnabled() {
coinbase = *api.backend.ChainConfig().Scroll.FeeVaultAddress
} else {
coinbase, err = api.backend.Engine().Author(block.Header())
if err != nil {
return nil, err
}
}
env := &traceEnv{
config: config,
coinbase: coinbase,
signer: types.MakeSigner(api.backend.ChainConfig(), block.Number()),
state: statedb,
blockCtx: core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil),
StorageTrace: &types.StorageTrace{
RootBefore: parent.Root(),
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()),
}
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
}
wrappedProof := make([]hexutil.Bytes, len(proof))
for i, bt := range proof {
wrappedProof[i] = bt
}
env.Proofs[key] = wrappedProof
}
return env, nil
}
func (api *API) getBlockTrace(block *types.Block, env *traceEnv) (*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 pend.Done()
// Fetch and execute the next transaction trace tasks
for task := range jobs {
if err := api.getTxResult(env, task.statedb, task.index, block); err != nil {
select {
case errCh <- err:
default:
}
log.Error("failed to trace tx", "txHash", txs[task.index].Hash().String())
}
}
}()
}
// Feed the transactions into the tracers and return
var failed error
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.Prepare(tx.Hash(), i)
vmenv := vm.NewEVM(env.blockCtx, core.NewEVMTxContext(msg), env.state, api.backend.ChainConfig(), vm.Config{})
if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())); err != nil {
failed = err
break
}
// Finalize the state so any modifications are written to the trie
// Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect
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)
}
}
}
// If execution failed in between, abort
select {
case err := <-errCh:
return nil, err
default:
if failed != nil {
return nil, failed
}
}
return api.fillBlockTrace(env, block)
}
func (api *API) getTxResult(env *traceEnv, 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),
}
}
tracer := vm.NewStructLogger(env.config.LogConfig)
// Run the transaction with tracing enabled.
vmenv := vm.NewEVM(env.blockCtx, core.NewEVMTxContext(msg), state, api.backend.ChainConfig(), vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true})
// Call Prepare to clear out the statedb access list
state.Prepare(txctx.TxHash, txctx.TxIndex)
// Computes the new state by applying the given message.
result, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()))
if err != nil {
return fmt.Errorf("tracing failed: %w", err)
}
// If the result contains a revert reason, return it.
returnVal := result.Return()
if len(result.Revert()) > 0 {
returnVal = result.Revert()
}
createdAcc := tracer.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),
})
}
// merge required proof data
proofAccounts := tracer.UpdatedAccounts()
for addr := range proofAccounts {
addrStr := addr.String()
env.pMu.Lock()
_, existed := env.Proofs[addrStr]
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 := make([]hexutil.Bytes, len(proof))
for i, bt := range proof {
wrappedProof[i] = bt
}
env.pMu.Lock()
env.Proofs[addrStr] = wrappedProof
env.pMu.Unlock()
}
proofStorages := tracer.UpdatedStorages()
for addr, keys := range proofStorages {
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())
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] = zktrieTracer
}
} else if _, existed := m[keyStr]; existed {
// 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 := make([]hexutil.Bytes, len(proof))
for i, bt := range proof {
wrappedProof[i] = bt
}
env.sMu.Lock()
m[keyStr] = wrappedProof
if zktrieTracer.Available() {
if isDelete {
zktrieTracer.MarkDeletion(key)
}
env.zkTrieTracer[addrStr].Merge(zktrieTracer)
}
env.sMu.Unlock()
}
}
var l1Fee uint64
if result.L1Fee != nil {
l1Fee = result.L1Fee.Uint64()
}
env.executionResults[index] = &types.ExecutionResult{
From: sender,
To: receiver,
AccountCreated: createdAcc,
AccountsAfter: after,
L1Fee: l1Fee,
Gas: result.UsedGas,
Failed: result.Failed(),
ReturnValue: fmt.Sprintf("%x", returnVal),
StructLogs: vm.FormatLogs(tracer.StructLogs()),
}
return nil
}
// Fill blockTrace content after all the txs are finished running.
func (api *API) fillBlockTrace(env *traceEnv, block *types.Block) (*types.BlockTrace, error) {
statedb := env.state
txs := make([]*types.TransactionData, block.Transactions().Len())
for i, tx := range block.Transactions() {
txs[i] = types.NewTransactionData(tx, block.NumberU64(), api.backend.ChainConfig())
}
blockTrace := &types.BlockTrace{
ChainID: api.backend.ChainConfig().ChainID.Uint64(),
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,
Transactions: txs,
}
for i, tx := range block.Transactions() {
evmTrace := env.executionResults[i]
// probably a Contract Call
if len(tx.Data()) != 0 && tx.To() != nil {
evmTrace.ByteCode = hexutil.Encode(statedb.GetCode(*tx.To()))
// Get tx.to address's code hash.
codeHash := statedb.GetPoseidonCodeHash(*tx.To())
evmTrace.PoseidonCodeHash = &codeHash
} else if tx.To() == nil { // Contract is created.
evmTrace.ByteCode = hexutil.Encode(tx.Data())
}
}
// only zktrie model has the ability to get `mptwitness`.
if api.backend.ChainConfig().Scroll.ZktrieEnabled() {
if err := zkproof.FillBlockTraceForMPTWitness(zkproof.MPTWitnessType(api.backend.CacheConfig().MPTWitness), blockTrace); err != nil {
log.Error("fill mpt witness fail", "error", err)
}
}
blockTrace.WithdrawTrieRoot = withdrawtrie.ReadWTRSlot(rcfg.L2MessageQueueAddress, env.state)
return blockTrace, nil
}