064f37d6f6
Here we add a Go API for running tracing plugins within the main block import process. As an advanced user of geth, you can now create a Go file in eth/tracers/live/, and within that file register your custom tracer implementation. Then recompile geth and select your tracer on the command line. Hooks defined in the tracer will run whenever a block is processed. The hook system is defined in package core/tracing. It uses a struct with callbacks, instead of requiring an interface, for several reasons: - We plan to keep this API stable long-term. The core/tracing hook API does not depend on on deep geth internals. - There are a lot of hooks, and tracers will only need some of them. Using a struct allows you to implement only the hooks you want to actually use. All existing tracers in eth/tracers/native have been rewritten to use the new hook system. This change breaks compatibility with the vm.EVMLogger interface that we used to have. If you are a user of vm.EVMLogger, please migrate to core/tracing, and sorry for breaking your stuff. But we just couldn't have both the old and new tracing APIs coexist in the EVM. --------- Co-authored-by: Matthieu Vachon <matthieu.o.vachon@gmail.com> Co-authored-by: Delweng <delweng@gmail.com> Co-authored-by: Martin HS <martin@swende.se>
368 lines
12 KiB
Go
368 lines
12 KiB
Go
// Copyright 2020 The go-ethereum Authors
|
|
// This file is part of go-ethereum.
|
|
//
|
|
// go-ethereum is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// go-ethereum is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package t8ntool
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
|
|
"github.com/ethereum/go-ethereum/core/state"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/core/vm"
|
|
"github.com/ethereum/go-ethereum/eth/tracers"
|
|
"github.com/ethereum/go-ethereum/eth/tracers/logger"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/ethereum/go-ethereum/params"
|
|
"github.com/ethereum/go-ethereum/tests"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
const (
|
|
ErrorEVM = 2
|
|
ErrorConfig = 3
|
|
ErrorMissingBlockhash = 4
|
|
|
|
ErrorJson = 10
|
|
ErrorIO = 11
|
|
ErrorRlp = 12
|
|
|
|
stdinSelector = "stdin"
|
|
)
|
|
|
|
type NumberedError struct {
|
|
errorCode int
|
|
err error
|
|
}
|
|
|
|
func NewError(errorCode int, err error) *NumberedError {
|
|
return &NumberedError{errorCode, err}
|
|
}
|
|
|
|
func (n *NumberedError) Error() string {
|
|
return fmt.Sprintf("ERROR(%d): %v", n.errorCode, n.err.Error())
|
|
}
|
|
|
|
func (n *NumberedError) ExitCode() int {
|
|
return n.errorCode
|
|
}
|
|
|
|
// compile-time conformance test
|
|
var (
|
|
_ cli.ExitCoder = (*NumberedError)(nil)
|
|
)
|
|
|
|
type input struct {
|
|
Alloc types.GenesisAlloc `json:"alloc,omitempty"`
|
|
Env *stEnv `json:"env,omitempty"`
|
|
Txs []*txWithKey `json:"txs,omitempty"`
|
|
TxRlp string `json:"txsRlp,omitempty"`
|
|
}
|
|
|
|
func Transition(ctx *cli.Context) error {
|
|
var getTracer = func(txIndex int, txHash common.Hash) (*tracers.Tracer, io.WriteCloser, error) { return nil, nil, nil }
|
|
|
|
baseDir, err := createBasedir(ctx)
|
|
if err != nil {
|
|
return NewError(ErrorIO, fmt.Errorf("failed creating output basedir: %v", err))
|
|
}
|
|
|
|
if ctx.Bool(TraceFlag.Name) { // JSON opcode tracing
|
|
// Configure the EVM logger
|
|
logConfig := &logger.Config{
|
|
DisableStack: ctx.Bool(TraceDisableStackFlag.Name),
|
|
EnableMemory: ctx.Bool(TraceEnableMemoryFlag.Name),
|
|
EnableReturnData: ctx.Bool(TraceEnableReturnDataFlag.Name),
|
|
Debug: true,
|
|
}
|
|
getTracer = func(txIndex int, txHash common.Hash) (*tracers.Tracer, io.WriteCloser, error) {
|
|
traceFile, err := os.Create(filepath.Join(baseDir, fmt.Sprintf("trace-%d-%v.jsonl", txIndex, txHash.String())))
|
|
if err != nil {
|
|
return nil, nil, NewError(ErrorIO, fmt.Errorf("failed creating trace-file: %v", err))
|
|
}
|
|
logger := logger.NewJSONLogger(logConfig, traceFile)
|
|
tracer := &tracers.Tracer{
|
|
Hooks: logger,
|
|
// jsonLogger streams out result to file.
|
|
GetResult: func() (json.RawMessage, error) { return nil, nil },
|
|
Stop: func(err error) {},
|
|
}
|
|
return tracer, traceFile, nil
|
|
}
|
|
} else if ctx.IsSet(TraceTracerFlag.Name) {
|
|
var config json.RawMessage
|
|
if ctx.IsSet(TraceTracerConfigFlag.Name) {
|
|
config = []byte(ctx.String(TraceTracerConfigFlag.Name))
|
|
}
|
|
getTracer = func(txIndex int, txHash common.Hash) (*tracers.Tracer, io.WriteCloser, error) {
|
|
traceFile, err := os.Create(filepath.Join(baseDir, fmt.Sprintf("trace-%d-%v.json", txIndex, txHash.String())))
|
|
if err != nil {
|
|
return nil, nil, NewError(ErrorIO, fmt.Errorf("failed creating trace-file: %v", err))
|
|
}
|
|
tracer, err := tracers.DefaultDirectory.New(ctx.String(TraceTracerFlag.Name), nil, config)
|
|
if err != nil {
|
|
return nil, nil, NewError(ErrorConfig, fmt.Errorf("failed instantiating tracer: %w", err))
|
|
}
|
|
return tracer, traceFile, nil
|
|
}
|
|
}
|
|
// We need to load three things: alloc, env and transactions. May be either in
|
|
// stdin input or in files.
|
|
// Check if anything needs to be read from stdin
|
|
var (
|
|
prestate Prestate
|
|
txIt txIterator // txs to apply
|
|
allocStr = ctx.String(InputAllocFlag.Name)
|
|
|
|
envStr = ctx.String(InputEnvFlag.Name)
|
|
txStr = ctx.String(InputTxsFlag.Name)
|
|
inputData = &input{}
|
|
)
|
|
// Figure out the prestate alloc
|
|
if allocStr == stdinSelector || envStr == stdinSelector || txStr == stdinSelector {
|
|
decoder := json.NewDecoder(os.Stdin)
|
|
if err := decoder.Decode(inputData); err != nil {
|
|
return NewError(ErrorJson, fmt.Errorf("failed unmarshalling stdin: %v", err))
|
|
}
|
|
}
|
|
if allocStr != stdinSelector {
|
|
if err := readFile(allocStr, "alloc", &inputData.Alloc); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
prestate.Pre = inputData.Alloc
|
|
|
|
// Set the block environment
|
|
if envStr != stdinSelector {
|
|
var env stEnv
|
|
if err := readFile(envStr, "env", &env); err != nil {
|
|
return err
|
|
}
|
|
inputData.Env = &env
|
|
}
|
|
prestate.Env = *inputData.Env
|
|
|
|
vmConfig := vm.Config{}
|
|
// Construct the chainconfig
|
|
var chainConfig *params.ChainConfig
|
|
if cConf, extraEips, err := tests.GetChainConfig(ctx.String(ForknameFlag.Name)); err != nil {
|
|
return NewError(ErrorConfig, fmt.Errorf("failed constructing chain configuration: %v", err))
|
|
} else {
|
|
chainConfig = cConf
|
|
vmConfig.ExtraEips = extraEips
|
|
}
|
|
// Set the chain id
|
|
chainConfig.ChainID = big.NewInt(ctx.Int64(ChainIDFlag.Name))
|
|
|
|
if txIt, err = loadTransactions(txStr, inputData, prestate.Env, chainConfig); err != nil {
|
|
return err
|
|
}
|
|
if err := applyLondonChecks(&prestate.Env, chainConfig); err != nil {
|
|
return err
|
|
}
|
|
if err := applyShanghaiChecks(&prestate.Env, chainConfig); err != nil {
|
|
return err
|
|
}
|
|
if err := applyMergeChecks(&prestate.Env, chainConfig); err != nil {
|
|
return err
|
|
}
|
|
if err := applyCancunChecks(&prestate.Env, chainConfig); err != nil {
|
|
return err
|
|
}
|
|
// Run the test and aggregate the result
|
|
s, result, body, err := prestate.Apply(vmConfig, chainConfig, txIt, ctx.Int64(RewardFlag.Name), getTracer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Dump the execution result
|
|
collector := make(Alloc)
|
|
s.DumpToCollector(collector, nil)
|
|
return dispatchOutput(ctx, baseDir, result, collector, body)
|
|
}
|
|
|
|
func applyLondonChecks(env *stEnv, chainConfig *params.ChainConfig) error {
|
|
if !chainConfig.IsLondon(big.NewInt(int64(env.Number))) {
|
|
return nil
|
|
}
|
|
// Sanity check, to not `panic` in state_transition
|
|
if env.BaseFee != nil {
|
|
// Already set, base fee has precedent over parent base fee.
|
|
return nil
|
|
}
|
|
if env.ParentBaseFee == nil || env.Number == 0 {
|
|
return NewError(ErrorConfig, errors.New("EIP-1559 config but missing 'currentBaseFee' in env section"))
|
|
}
|
|
env.BaseFee = eip1559.CalcBaseFee(chainConfig, &types.Header{
|
|
Number: new(big.Int).SetUint64(env.Number - 1),
|
|
BaseFee: env.ParentBaseFee,
|
|
GasUsed: env.ParentGasUsed,
|
|
GasLimit: env.ParentGasLimit,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func applyShanghaiChecks(env *stEnv, chainConfig *params.ChainConfig) error {
|
|
if !chainConfig.IsShanghai(big.NewInt(int64(env.Number)), env.Timestamp) {
|
|
return nil
|
|
}
|
|
if env.Withdrawals == nil {
|
|
return NewError(ErrorConfig, errors.New("Shanghai config but missing 'withdrawals' in env section"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyMergeChecks(env *stEnv, chainConfig *params.ChainConfig) error {
|
|
isMerged := chainConfig.TerminalTotalDifficulty != nil && chainConfig.TerminalTotalDifficulty.BitLen() == 0
|
|
if !isMerged {
|
|
// pre-merge: If difficulty was not provided by caller, we need to calculate it.
|
|
if env.Difficulty != nil {
|
|
// already set
|
|
return nil
|
|
}
|
|
switch {
|
|
case env.ParentDifficulty == nil:
|
|
return NewError(ErrorConfig, errors.New("currentDifficulty was not provided, and cannot be calculated due to missing parentDifficulty"))
|
|
case env.Number == 0:
|
|
return NewError(ErrorConfig, errors.New("currentDifficulty needs to be provided for block number 0"))
|
|
case env.Timestamp <= env.ParentTimestamp:
|
|
return NewError(ErrorConfig, fmt.Errorf("currentDifficulty cannot be calculated -- currentTime (%d) needs to be after parent time (%d)",
|
|
env.Timestamp, env.ParentTimestamp))
|
|
}
|
|
env.Difficulty = calcDifficulty(chainConfig, env.Number, env.Timestamp,
|
|
env.ParentTimestamp, env.ParentDifficulty, env.ParentUncleHash)
|
|
return nil
|
|
}
|
|
// post-merge:
|
|
// - random must be supplied
|
|
// - difficulty must be zero
|
|
switch {
|
|
case env.Random == nil:
|
|
return NewError(ErrorConfig, errors.New("post-merge requires currentRandom to be defined in env"))
|
|
case env.Difficulty != nil && env.Difficulty.BitLen() != 0:
|
|
return NewError(ErrorConfig, errors.New("post-merge difficulty must be zero (or omitted) in env"))
|
|
}
|
|
env.Difficulty = nil
|
|
return nil
|
|
}
|
|
|
|
func applyCancunChecks(env *stEnv, chainConfig *params.ChainConfig) error {
|
|
if !chainConfig.IsCancun(big.NewInt(int64(env.Number)), env.Timestamp) {
|
|
env.ParentBeaconBlockRoot = nil // un-set it if it has been set too early
|
|
return nil
|
|
}
|
|
// Post-cancun
|
|
// We require EIP-4788 beacon root to be set in the env
|
|
if env.ParentBeaconBlockRoot == nil {
|
|
return NewError(ErrorConfig, errors.New("post-cancun env requires parentBeaconBlockRoot to be set"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Alloc map[common.Address]types.Account
|
|
|
|
func (g Alloc) OnRoot(common.Hash) {}
|
|
|
|
func (g Alloc) OnAccount(addr *common.Address, dumpAccount state.DumpAccount) {
|
|
if addr == nil {
|
|
return
|
|
}
|
|
balance, _ := new(big.Int).SetString(dumpAccount.Balance, 0)
|
|
var storage map[common.Hash]common.Hash
|
|
if dumpAccount.Storage != nil {
|
|
storage = make(map[common.Hash]common.Hash)
|
|
for k, v := range dumpAccount.Storage {
|
|
storage[k] = common.HexToHash(v)
|
|
}
|
|
}
|
|
genesisAccount := types.Account{
|
|
Code: dumpAccount.Code,
|
|
Storage: storage,
|
|
Balance: balance,
|
|
Nonce: dumpAccount.Nonce,
|
|
}
|
|
g[*addr] = genesisAccount
|
|
}
|
|
|
|
// saveFile marshals the object to the given file
|
|
func saveFile(baseDir, filename string, data interface{}) error {
|
|
b, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err))
|
|
}
|
|
location := filepath.Join(baseDir, filename)
|
|
if err = os.WriteFile(location, b, 0644); err != nil {
|
|
return NewError(ErrorIO, fmt.Errorf("failed writing output: %v", err))
|
|
}
|
|
log.Info("Wrote file", "file", location)
|
|
return nil
|
|
}
|
|
|
|
// dispatchOutput writes the output data to either stderr or stdout, or to the specified
|
|
// files
|
|
func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, alloc Alloc, body hexutil.Bytes) error {
|
|
stdOutObject := make(map[string]interface{})
|
|
stdErrObject := make(map[string]interface{})
|
|
dispatch := func(baseDir, fName, name string, obj interface{}) error {
|
|
switch fName {
|
|
case "stdout":
|
|
stdOutObject[name] = obj
|
|
case "stderr":
|
|
stdErrObject[name] = obj
|
|
case "":
|
|
// don't save
|
|
default: // save to file
|
|
if err := saveFile(baseDir, fName, obj); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := dispatch(baseDir, ctx.String(OutputAllocFlag.Name), "alloc", alloc); err != nil {
|
|
return err
|
|
}
|
|
if err := dispatch(baseDir, ctx.String(OutputResultFlag.Name), "result", result); err != nil {
|
|
return err
|
|
}
|
|
if err := dispatch(baseDir, ctx.String(OutputBodyFlag.Name), "body", body); err != nil {
|
|
return err
|
|
}
|
|
if len(stdOutObject) > 0 {
|
|
b, err := json.MarshalIndent(stdOutObject, "", " ")
|
|
if err != nil {
|
|
return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err))
|
|
}
|
|
os.Stdout.Write(b)
|
|
os.Stdout.WriteString("\n")
|
|
}
|
|
if len(stdErrObject) > 0 {
|
|
b, err := json.MarshalIndent(stdErrObject, "", " ")
|
|
if err != nil {
|
|
return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err))
|
|
}
|
|
os.Stderr.Write(b)
|
|
os.Stderr.WriteString("\n")
|
|
}
|
|
return nil
|
|
}
|