mirror of https://github.com/ethereum/go-ethereum
cmd/evm: refactor handling output-files for `t8n` (#30854)
As part of trying to make the inputs and outputs of the evm subcommands more streamlined and aligned, this PR modifies how `evm t8n` manages output-files. Previously, we do a kind of wonky thing where between each transaction, we invoke a `getTracer` closure. In that closure, we create a new output-file, a tracer, and then make the tracer stream output to the file. We also fiddle a bit to ensure that the file becomes properly closed. It is a kind of hacky solution we have in place. This PR changes it, so that from the execution-pipeline point of view, we have just a regular tracer. No fiddling with re-setting it or closing files. That particular tracer, however, is a bit special: it takes care of creating new files per transaction (in the tx-start-hook) and closing (on tx-end-hook). Also instantiating the right type of underlying tracer, which can be a json-logger or a custom tracer. --------- Co-authored-by: Gary Rong <garyrong0905@gmail.com>pull/31056/head
parent
e25cedf16d
commit
2bf4a8ff73
@ -0,0 +1,152 @@ |
||||
// Copyright 2024 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" |
||||
"fmt" |
||||
"io" |
||||
"math/big" |
||||
"os" |
||||
"path/filepath" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/tracing" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/eth/tracers" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
// fileWritingTracer wraps either a tracer or a logger. On tx start,
|
||||
// it instantiates a tracer/logger, creates a new file to direct output to,
|
||||
// and on tx end it closes the file.
|
||||
type fileWritingTracer struct { |
||||
txIndex int // transaction counter
|
||||
inner *tracing.Hooks // inner hooks
|
||||
destination io.WriteCloser // the currently open file (if any)
|
||||
baseDir string // baseDir to write output-files to
|
||||
suffix string // suffix is the suffix to use when creating files
|
||||
|
||||
// for custom tracing
|
||||
getResult func() (json.RawMessage, error) |
||||
} |
||||
|
||||
func (l *fileWritingTracer) Write(p []byte) (n int, err error) { |
||||
if l.destination != nil { |
||||
return l.destination.Write(p) |
||||
} |
||||
log.Warn("Tracer wrote to non-existing output") |
||||
// It is tempting to return an error here, however, the json encoder
|
||||
// will no retry writing to an io.Writer once it has returned an error once.
|
||||
// Therefore, we must squash the error.
|
||||
return n, nil |
||||
} |
||||
|
||||
// newFileWriter creates a set of hooks which wraps inner hooks (typically a logger),
|
||||
// and writes the output to a file, one file per transaction.
|
||||
func newFileWriter(baseDir string, innerFn func(out io.Writer) *tracing.Hooks) *tracing.Hooks { |
||||
t := &fileWritingTracer{ |
||||
baseDir: baseDir, |
||||
suffix: "jsonl", |
||||
} |
||||
t.inner = innerFn(t) // instantiate the inner tracer
|
||||
return t.hooks() |
||||
} |
||||
|
||||
// newResultWriter creates a set of hooks wraps and invokes an underlying tracer,
|
||||
// and writes the result (getResult-output) to file, one per transaction.
|
||||
func newResultWriter(baseDir string, tracer *tracers.Tracer) *tracing.Hooks { |
||||
t := &fileWritingTracer{ |
||||
baseDir: baseDir, |
||||
getResult: tracer.GetResult, |
||||
inner: tracer.Hooks, |
||||
suffix: "json", |
||||
} |
||||
return t.hooks() |
||||
} |
||||
|
||||
// OnTxStart creates a new output-file specific for this transaction, and invokes
|
||||
// the inner OnTxStart handler.
|
||||
func (l *fileWritingTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { |
||||
// Open a new file, or print a warning log if it's failed
|
||||
fname := filepath.Join(l.baseDir, fmt.Sprintf("trace-%d-%v.%v", l.txIndex, tx.Hash().String(), l.suffix)) |
||||
traceFile, err := os.Create(fname) |
||||
if err != nil { |
||||
log.Warn("Failed creating trace-file", "err", err) |
||||
} else { |
||||
log.Info("Created tracing-file", "path", fname) |
||||
l.destination = traceFile |
||||
} |
||||
if l.inner != nil && l.inner.OnTxStart != nil { |
||||
l.inner.OnTxStart(env, tx, from) |
||||
} |
||||
} |
||||
|
||||
// OnTxEnd writes result (if getResult exist), closes any currently open output-file,
|
||||
// and invokes the inner OnTxEnd handler.
|
||||
func (l *fileWritingTracer) OnTxEnd(receipt *types.Receipt, err error) { |
||||
if l.inner != nil && l.inner.OnTxEnd != nil { |
||||
l.inner.OnTxEnd(receipt, err) |
||||
} |
||||
if l.getResult != nil && l.destination != nil { |
||||
if result, err := l.getResult(); result != nil { |
||||
json.NewEncoder(l.destination).Encode(result) |
||||
} else { |
||||
log.Warn("Error obtaining tracer result", "err", err) |
||||
} |
||||
l.destination.Close() |
||||
l.destination = nil |
||||
} |
||||
l.txIndex++ |
||||
} |
||||
|
||||
func (l *fileWritingTracer) hooks() *tracing.Hooks { |
||||
return &tracing.Hooks{ |
||||
OnTxStart: l.OnTxStart, |
||||
OnTxEnd: l.OnTxEnd, |
||||
OnEnter: func(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { |
||||
if l.inner != nil && l.inner.OnEnter != nil { |
||||
l.inner.OnEnter(depth, typ, from, to, input, gas, value) |
||||
} |
||||
}, |
||||
OnExit: func(depth int, output []byte, gasUsed uint64, err error, reverted bool) { |
||||
if l.inner != nil && l.inner.OnExit != nil { |
||||
l.inner.OnExit(depth, output, gasUsed, err, reverted) |
||||
} |
||||
}, |
||||
OnOpcode: func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { |
||||
if l.inner != nil && l.inner.OnOpcode != nil { |
||||
l.inner.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) |
||||
} |
||||
}, |
||||
OnFault: func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, depth int, err error) { |
||||
if l.inner != nil && l.inner.OnFault != nil { |
||||
l.inner.OnFault(pc, op, gas, cost, scope, depth, err) |
||||
} |
||||
}, |
||||
OnSystemCallStart: func() { |
||||
if l.inner != nil && l.inner.OnSystemCallStart != nil { |
||||
l.inner.OnSystemCallStart() |
||||
} |
||||
}, |
||||
OnSystemCallEnd: func() { |
||||
if l.inner != nil && l.inner.OnSystemCallEnd != nil { |
||||
l.inner.OnSystemCallEnd() |
||||
} |
||||
}, |
||||
} |
||||
} |
@ -1 +1,11 @@ |
||||
This test does some EVM execution, and can be used to test the tracers and trace-outputs. |
||||
This test should yield three output-traces, in separate files |
||||
|
||||
For example: |
||||
``` |
||||
[user@work evm]$ go run . t8n --input.alloc ./testdata/31/alloc.json --input.txs ./testdata/31/txs.json --input.env ./testdata/31/env.json --state.fork Cancun --output.basedir /tmp --trace |
||||
INFO [12-06|09:53:32.123] Created tracing-file path=/tmp/trace-0-0x88f5fbd1524731a81e49f637aa847543268a5aaf2a6b32a69d2c6d978c45dcfb.jsonl |
||||
INFO [12-06|09:53:32.124] Created tracing-file path=/tmp/trace-1-0x03a7b0a91e61a170d64ea94b8263641ef5a8bbdb10ac69f466083a6789c77fb8.jsonl |
||||
INFO [12-06|09:53:32.125] Created tracing-file path=/tmp/trace-2-0xd96e0ce6418ee3360e11d3c7b6886f5a9a08f7ef183da72c23bb3b2374530128.jsonl |
||||
``` |
||||
|
||||
|
@ -1 +1 @@ |
||||
"hello world" |
||||
"hello world 1" |
||||
|
@ -0,0 +1 @@ |
||||
"hello world 2" |
@ -0,0 +1,6 @@ |
||||
{"pc":0,"op":96,"gas":"0x13498","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"refund":0,"opName":"PUSH1"} |
||||
{"pc":2,"op":96,"gas":"0x13495","gasCost":"0x3","memSize":0,"stack":["0x40"],"depth":1,"refund":0,"opName":"PUSH1"} |
||||
{"pc":4,"op":96,"gas":"0x13492","gasCost":"0x3","memSize":0,"stack":["0x40","0x40"],"depth":1,"refund":0,"opName":"PUSH1"} |
||||
{"pc":6,"op":96,"gas":"0x1348f","gasCost":"0x3","memSize":0,"stack":["0x40","0x40","0x40"],"depth":1,"refund":0,"opName":"PUSH1"} |
||||
{"pc":8,"op":0,"gas":"0x1348c","gasCost":"0x0","memSize":0,"stack":["0x40","0x40","0x40","0x40"],"depth":1,"refund":0,"opName":"STOP"} |
||||
{"output":"","gasUsed":"0xc"} |
@ -0,0 +1 @@ |
||||
"hello world 3" |
@ -0,0 +1,6 @@ |
||||
{"pc":0,"op":96,"gas":"0x13498","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"refund":0,"opName":"PUSH1"} |
||||
{"pc":2,"op":96,"gas":"0x13495","gasCost":"0x3","memSize":0,"stack":["0x40"],"depth":1,"refund":0,"opName":"PUSH1"} |
||||
{"pc":4,"op":96,"gas":"0x13492","gasCost":"0x3","memSize":0,"stack":["0x40","0x40"],"depth":1,"refund":0,"opName":"PUSH1"} |
||||
{"pc":6,"op":96,"gas":"0x1348f","gasCost":"0x3","memSize":0,"stack":["0x40","0x40","0x40"],"depth":1,"refund":0,"opName":"PUSH1"} |
||||
{"pc":8,"op":0,"gas":"0x1348c","gasCost":"0x0","memSize":0,"stack":["0x40","0x40","0x40","0x40"],"depth":1,"refund":0,"opName":"STOP"} |
||||
{"output":"","gasUsed":"0xc"} |
Loading…
Reference in new issue