eth/tracers: add withLog to callTracer (#25991)

In some cases, it is desirable to capture what is triggered by each trace, when using the `callTracer`. For example: call `USDT.transfer` will trigger a `Transfer(from, to, value)` event.

This PR adds the option to capture logs to the call tracer, by specifying `{"withLog": true}` in the tracerconfig. 
Any logs belonging to failed/reverted call-scopes are removed from the output, to prevent interpretation mistakes.

Signed-off-by: Delweng <delweng@gmail.com>
Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com>
This commit is contained in:
Delweng 2022-11-01 19:03:26 +08:00 committed by GitHub
parent b0d44338bb
commit 8e69622c68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 4226 additions and 1 deletions

@ -46,6 +46,13 @@ type callContext struct {
Miner common.Address `json:"miner"` Miner common.Address `json:"miner"`
} }
// callLog is the result of LOG opCode
type callLog struct {
Address common.Address `json:"address"`
Topics []common.Hash `json:"topics"`
Data hexutil.Bytes `json:"data"`
}
// callTrace is the result of a callTracer run. // callTrace is the result of a callTracer run.
type callTrace struct { type callTrace struct {
From common.Address `json:"from"` From common.Address `json:"from"`
@ -57,6 +64,7 @@ type callTrace struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
Revertal string `json:"revertReason,omitempty"` Revertal string `json:"revertReason,omitempty"`
Calls []callTrace `json:"calls,omitempty"` Calls []callTrace `json:"calls,omitempty"`
Logs []callLog `json:"logs,omitempty"`
Value *hexutil.Big `json:"value,omitempty"` Value *hexutil.Big `json:"value,omitempty"`
// Gencodec adds overridden fields at the end // Gencodec adds overridden fields at the end
Type string `json:"type"` Type string `json:"type"`
@ -81,6 +89,10 @@ func TestCallTracerNative(t *testing.T) {
testCallTracer("callTracer", "call_tracer", t) testCallTracer("callTracer", "call_tracer", t)
} }
func TestCallTracerNativeWithLog(t *testing.T) {
testCallTracer("callTracer", "call_tracer_withLog", t)
}
func testCallTracer(tracerName string, dirPath string, t *testing.T) { func testCallTracer(tracerName string, dirPath string, t *testing.T) {
isLegacy := strings.HasSuffix(dirPath, "_legacy") isLegacy := strings.HasSuffix(dirPath, "_legacy")
files, err := os.ReadDir(filepath.Join("testdata", dirPath)) files, err := os.ReadDir(filepath.Join("testdata", dirPath))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,84 @@
{
"genesis": {
"difficulty": "8430028481555",
"extraData": "0xd783010302844765746887676f312e352e31856c696e7578",
"gasLimit": "3141592",
"hash": "0xde66937783697293f2e529d2034887c531535d78afa8c9051511ae12ba48fbea",
"miner": "0x2a65aca4d5fc5b5c859090a6c34d164135398226",
"mixHash": "0xba28a43bfbca4a2effbb76bb70d03482a8a0c92e2883ff36cbac3d7c6dbb7df5",
"nonce": "0xa3827ec0a82fe823",
"number": "765824",
"stateRoot": "0x8d96cb027a29f8ca0ccd6d31f9ea0656136ec8030ecda70bb9231849ed6f41a2",
"timestamp": "1451389443",
"totalDifficulty": "4838314986494741271",
"alloc": {
"0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb": {
"balance": "0x14203bee2ea6fbe8c",
"nonce": "34"
},
"0xe2fe6b13287f28e193333fdfe7fedf2f6df6124a": {
"balance": "0x2717a9c870a286f4350"
},
"0xf4eced2f682ce333f96f2d8966c613ded8fc95dd": {
"balance": "0x0",
"code": "0x606060405260e060020a600035046306fdde038114610047578063313ce567146100a457806370a08231146100b057806395d89b41146100c8578063a9059cbb14610123575b005b61015260008054602060026001831615610100026000190190921691909104601f810182900490910260809081016040526060828152929190828280156101f55780601f106101ca576101008083540402835291602001916101f5565b6101c060025460ff1681565b6101c060043560036020526000908152604090205481565b610152600180546020601f6002600019610100858716150201909316929092049182018190040260809081016040526060828152929190828280156101f55780601f106101ca576101008083540402835291602001916101f5565b610045600435602435600160a060020a033316600090815260036020526040902054819010156101fd57610002565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f1680156101b25780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6060908152602090f35b820191906000526020600020905b8154815290600101906020018083116101d857829003601f168201915b505050505081565b600160a060020a03821660009081526040902054808201101561021f57610002565b806003600050600033600160a060020a03168152602001908152602001600020600082828250540392505081905550806003600050600084600160a060020a0316815260200190815260200160002060008282825054019250508190555081600160a060020a031633600160a060020a03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a3505056",
"storage": {
"0x1dae8253445d3a5edbe8200da9fc39bc4f11db9362181dc1b640d08c3c2fb4d6": "0x0000000000000000000000000000000000000000000000000000000000000000",
"0x8ba52aac7f255d80a49abcf003d6af4752aba5a9531cae94fde7ac8d72191d67": "0x000000000000000000000000000000000000000000000000000000000178e460"
}
}
},
"config": {
"chainId": 1,
"homesteadBlock": 1150000,
"daoForkBlock": 1920000,
"daoForkSupport": true,
"eip150Block": 2463000,
"eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0",
"eip155Block": 2675000,
"eip158Block": 2675000,
"byzantiumBlock": 4370000,
"constantinopleBlock": 7280000,
"petersburgBlock": 7280000,
"istanbulBlock": 9069000,
"muirGlacierBlock": 9200000,
"berlinBlock": 12244000,
"londonBlock": 12965000,
"arrowGlacierBlock": 13773000,
"grayGlacierBlock": 15050000,
"terminalTotalDifficultyPassed": true,
"ethash": {}
}
},
"context": {
"number": "765825",
"difficulty": "8425912256743",
"timestamp": "1451389488",
"gasLimit": "3141592",
"miner": "0xe2fe6b13287f28e193333fdfe7fedf2f6df6124a"
},
"input": "0xf8aa22850ba43b740083024d4594f4eced2f682ce333f96f2d8966c613ded8fc95dd80b844a9059cbb000000000000000000000000dbf03b407c01e7cd3cbea99509d93f8dddc8c6fb00000000000000000000000000000000000000000000000000000000009896801ca067da548a2e0f381a957b9b51f086073375d6bfc7312cbc9540b3647ccab7db11a042c6e5b34bc7ba821e9c25b166fa13d82ad4b0d044d16174d5587d4f04ecfcd1",
"tracerConfig": {
"withLog": true
},
"result": {
"from": "0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb",
"gas": "0x1f36d",
"gasUsed": "0xc6a5",
"to": "0xf4eced2f682ce333f96f2d8966c613ded8fc95dd",
"input": "0xa9059cbb000000000000000000000000dbf03b407c01e7cd3cbea99509d93f8dddc8c6fb0000000000000000000000000000000000000000000000000000000000989680",
"logs": [
{
"address": "0xf4eced2f682ce333f96f2d8966c613ded8fc95dd",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000d1220a0cf47c7b9be7a2e6ba89f429762e7b9adb",
"0x000000000000000000000000dbf03b407c01e7cd3cbea99509d93f8dddc8c6fb"
],
"data": "0x0000000000000000000000000000000000000000000000000000000000989680"
}
],
"value": "0x0",
"type": "CALL"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -36,6 +36,12 @@ func init() {
register("callTracer", newCallTracer) register("callTracer", newCallTracer)
} }
type callLog struct {
Address common.Address `json:"address"`
Topics []common.Hash `json:"topics"`
Data hexutil.Bytes `json:"data"`
}
type callFrame struct { type callFrame struct {
Type vm.OpCode `json:"-"` Type vm.OpCode `json:"-"`
From common.Address `json:"from"` From common.Address `json:"from"`
@ -47,6 +53,7 @@ type callFrame struct {
Error string `json:"error,omitempty" rlp:"optional"` Error string `json:"error,omitempty" rlp:"optional"`
Revertal string `json:"revertReason,omitempty"` Revertal string `json:"revertReason,omitempty"`
Calls []callFrame `json:"calls,omitempty" rlp:"optional"` Calls []callFrame `json:"calls,omitempty" rlp:"optional"`
Logs []callLog `json:"logs,omitempty" rlp:"optional"`
// Placed at end on purpose. The RLP will be decoded to 0 instead of // Placed at end on purpose. The RLP will be decoded to 0 instead of
// nil if there are non-empty elements after in the struct. // nil if there are non-empty elements after in the struct.
Value *big.Int `json:"value,omitempty" rlp:"optional"` Value *big.Int `json:"value,omitempty" rlp:"optional"`
@ -56,6 +63,10 @@ func (f callFrame) TypeString() string {
return f.Type.String() return f.Type.String()
} }
func (f callFrame) failed() bool {
return len(f.Error) > 0
}
func (f *callFrame) capture(output []byte, err error) { func (f *callFrame) capture(output []byte, err error) {
output = common.CopyBytes(output) output = common.CopyBytes(output)
if err == nil { if err == nil {
@ -98,6 +109,7 @@ type callTracer struct {
type callTracerConfig struct { type callTracerConfig struct {
OnlyTopCall bool `json:"onlyTopCall"` // If true, call tracer won't collect any subcalls OnlyTopCall bool `json:"onlyTopCall"` // If true, call tracer won't collect any subcalls
WithLog bool `json:"withLog"` // If true, call tracer will collect event logs
} }
// newCallTracer returns a native go tracer which tracks // newCallTracer returns a native go tracer which tracks
@ -137,10 +149,38 @@ func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration,
// CaptureState implements the EVMLogger interface to trace a single step of VM execution. // CaptureState implements the EVMLogger interface to trace a single step of VM execution.
func (t *callTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { func (t *callTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
// Only logs need to be captured via opcode processing
if !t.config.WithLog {
return
}
// Avoid processing nested calls when only caring about top call
if t.config.OnlyTopCall && depth > 0 {
return
}
switch op {
case vm.LOG0, vm.LOG1, vm.LOG2, vm.LOG3, vm.LOG4:
size := int(op - vm.LOG0)
stack := scope.Stack
stackData := stack.Data()
// Don't modify the stack
mStart := stackData[len(stackData)-1]
mSize := stackData[len(stackData)-2]
topics := make([]common.Hash, size)
for i := 0; i < size; i++ {
topic := stackData[len(stackData)-2-(i+1)]
topics[i] = common.Hash(topic.Bytes32())
}
data := scope.Memory.GetCopy(int64(mStart.Uint64()), int64(mSize.Uint64()))
log := callLog{Address: scope.Contract.Address(), Topics: topics, Data: hexutil.Bytes(data)}
t.callstack[len(t.callstack)-1].Logs = append(t.callstack[len(t.callstack)-1].Logs, log)
}
} }
// CaptureFault implements the EVMLogger interface to trace an execution fault. // CaptureFault implements the EVMLogger interface to trace an execution fault.
func (t *callTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) { func (t *callTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) {
} }
// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). // CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct).
@ -191,6 +231,10 @@ func (t *callTracer) CaptureTxStart(gasLimit uint64) {
func (t *callTracer) CaptureTxEnd(restGas uint64) { func (t *callTracer) CaptureTxEnd(restGas uint64) {
t.callstack[0].GasUsed = t.gasLimit - restGas t.callstack[0].GasUsed = t.gasLimit - restGas
if t.config.WithLog {
// Logs are not emitted when the call fails
clearFailedLogs(&t.callstack[0], false)
}
} }
// GetResult returns the json-encoded nested list of call traces, and any // GetResult returns the json-encoded nested list of call traces, and any
@ -211,3 +255,16 @@ func (t *callTracer) Stop(err error) {
t.reason = err t.reason = err
atomic.StoreUint32(&t.interrupt, 1) atomic.StoreUint32(&t.interrupt, 1)
} }
// clearFailedLogs clears the logs of a callframe and all its children
// in case of execution failure.
func clearFailedLogs(cf *callFrame, parentFailed bool) {
failed := cf.failed() || parentFailed
// Clear own logs
if failed {
cf.Logs = nil
}
for i := range cf.Calls {
clearFailedLogs(&cf.Calls[i], failed)
}
}

@ -26,6 +26,7 @@ func (c callFrame) MarshalJSON() ([]byte, error) {
Error string `json:"error,omitempty" rlp:"optional"` Error string `json:"error,omitempty" rlp:"optional"`
Revertal string `json:"revertReason,omitempty"` Revertal string `json:"revertReason,omitempty"`
Calls []callFrame `json:"calls,omitempty" rlp:"optional"` Calls []callFrame `json:"calls,omitempty" rlp:"optional"`
Logs []callLog `json:"logs,omitempty" rlp:"optional"`
Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` Value *hexutil.Big `json:"value,omitempty" rlp:"optional"`
TypeString string `json:"type"` TypeString string `json:"type"`
} }
@ -40,6 +41,7 @@ func (c callFrame) MarshalJSON() ([]byte, error) {
enc.Error = c.Error enc.Error = c.Error
enc.Revertal = c.Revertal enc.Revertal = c.Revertal
enc.Calls = c.Calls enc.Calls = c.Calls
enc.Logs = c.Logs
enc.Value = (*hexutil.Big)(c.Value) enc.Value = (*hexutil.Big)(c.Value)
enc.TypeString = c.TypeString() enc.TypeString = c.TypeString()
return json.Marshal(&enc) return json.Marshal(&enc)
@ -58,6 +60,7 @@ func (c *callFrame) UnmarshalJSON(input []byte) error {
Error *string `json:"error,omitempty" rlp:"optional"` Error *string `json:"error,omitempty" rlp:"optional"`
Revertal *string `json:"revertReason,omitempty"` Revertal *string `json:"revertReason,omitempty"`
Calls []callFrame `json:"calls,omitempty" rlp:"optional"` Calls []callFrame `json:"calls,omitempty" rlp:"optional"`
Logs []callLog `json:"logs,omitempty" rlp:"optional"`
Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` Value *hexutil.Big `json:"value,omitempty" rlp:"optional"`
} }
var dec callFrame0 var dec callFrame0
@ -94,6 +97,9 @@ func (c *callFrame) UnmarshalJSON(input []byte) error {
if dec.Calls != nil { if dec.Calls != nil {
c.Calls = dec.Calls c.Calls = dec.Calls
} }
if dec.Logs != nil {
c.Logs = dec.Logs
}
if dec.Value != nil { if dec.Value != nil {
c.Value = (*big.Int)(dec.Value) c.Value = (*big.Int)(dec.Value)
} }