From e16fa4b34bc05254918afc1c928bba1cd9c447bc Mon Sep 17 00:00:00 2001 From: HonzaDajc Date: Tue, 12 Sep 2023 09:58:21 +0200 Subject: [PATCH] Refactor reaction to EVM callbacks Using EVM callbacks CaptureEnter and CaptureExit, which were not available when this code was written. Because of that stop using CaptureState as for transaction tracing is not needed to capture every single instruction. That leads to not using EVM stack and memory to get input data for inner calls and results for return, stop and revert. This data is now available directly in the callback arguments. --- txtrace/trace_logger.go | 389 ++++++++++++++-------------------------- 1 file changed, 138 insertions(+), 251 deletions(-) diff --git a/txtrace/trace_logger.go b/txtrace/trace_logger.go index 867f518fa..71884381a 100644 --- a/txtrace/trace_logger.go +++ b/txtrace/trace_logger.go @@ -2,16 +2,17 @@ package txtrace import ( "encoding/json" + "errors" "math/big" "strings" "time" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" - "github.com/holiman/uint256" "github.com/Fantom-foundation/go-opera/gossip/txtrace" ) @@ -28,13 +29,11 @@ type TraceStructLogger struct { blockNumber big.Int value big.Int - gasUsed uint64 + gasLimit uint64 rootTrace *CallTrace inputData []byte - state []depthState traceAddress []uint32 stack []*big.Int - reverted bool output []byte err error } @@ -55,6 +54,8 @@ func (tr *TraceStructLogger) CaptureStart(env *vm.EVM, from common.Address, to c log.Error("Tracer CaptureStart failed", r) } }() + log.Debug("TraceStructLogger Capture Enter", "tx hash", tr.tx.String(), "create", create, "from", from.String(), "to", to.String(), "input", string(input), "gas", gas, "value", value.String()) + // Create main trace holder txTrace := CallTrace{ Actions: make([]ActionTrace, 0), @@ -63,30 +64,31 @@ func (tr *TraceStructLogger) CaptureStart(env *vm.EVM, from common.Address, to c // Check if To is defined. If not, it is create address call callType := CREATE var newAddress *common.Address - if tr.to != nil { + if !create { callType = CALL } else { newAddress = &to } // Store input data - tr.inputData = input - if gas == 0 && tr.gasUsed != 0 { - gas = tr.gasUsed + tr.inputData = common.CopyBytes(input) + // In new version of go-ethereum setting gas limit is done via callback CaptureTxStart + if tr.gasLimit == 0 && gas != 0 { + tr.gasLimit = gas } // Make transaction trace root object blockTrace := NewActionTrace(tr.blockHash, tr.blockNumber, tr.tx, uint64(tr.txIndex), callType) var txAction *AddressAction - if CREATE == callType { - txAction = NewAddressAction(tr.from, gas, tr.inputData, nil, hexutil.Big(tr.value), nil) + if create { + txAction = NewAddressAction(tr.from, tr.gasLimit, tr.inputData, nil, hexutil.Big(*value), nil) if newAddress != nil { blockTrace.Result.Address = newAddress code := hexutil.Bytes(tr.output) blockTrace.Result.Code = &code } } else { - txAction = NewAddressAction(tr.from, gas, tr.inputData, tr.to, hexutil.Big(tr.value), &callType) + txAction = NewAddressAction(tr.from, tr.gasLimit, tr.inputData, tr.to, hexutil.Big(*value), &callType) out := hexutil.Bytes(tr.output) blockTrace.Result.Output = &out } @@ -97,217 +99,112 @@ func (tr *TraceStructLogger) CaptureStart(env *vm.EVM, from common.Address, to c tr.rootTrace = &txTrace // Init all needed variables - tr.state = []depthState{{0, create}} tr.traceAddress = make([]uint32, 0) tr.rootTrace.Stack = append(tr.rootTrace.Stack, &tr.rootTrace.Actions[len(tr.rootTrace.Actions)-1]) } -// stackPosFromEnd returns object from stack at givven position from end of stack -func stackPosFromEnd(stackData []uint256.Int, pos int) *big.Int { - if len(stackData) <= pos || pos < 0 { - log.Warn("Tracer accessed out of bound stack", "size", len(stackData), "index", pos) - return new(big.Int) - } - return new(big.Int).Set(stackData[len(stackData)-1-pos].ToBig()) +// CaptureState is not used as transaction tracing doesn't need per instruction resolution +func (tr *TraceStructLogger) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { } -// CaptureState implements creating of traces based on getting opCodes from evm during contract processing -func (tr *TraceStructLogger) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { +func (tr *TraceStructLogger) CaptureEnter(op vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { defer func() { if r := recover(); r != nil { log.Error("Tracer CaptureState failed", r) } }() - // When going back from inner call - for lastState(tr.state).level >= depth { - result := tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1].Result - if lastState(tr.state).create && result != nil { - if len(scope.Stack.Data()) > 0 { - addr := common.BytesToAddress(stackPosFromEnd(scope.Stack.Data(), 0).Bytes()) - result.Address = &addr - result.GasUsed = hexutil.Uint64(gas) - } - } - tr.traceAddress = removeTraceAddressLevel(tr.traceAddress, depth) - tr.state = tr.state[:len(tr.state)-1] - tr.rootTrace.Stack = tr.rootTrace.Stack[:len(tr.rootTrace.Stack)-1] - if lastState(tr.state).level == depth { - break - } + log.Debug("TraceStructLogger Capture Enter", "tx hash", tr.tx.String(), "op code", op.String(), "from", from.String(), "to", to.String(), "input", string(input), "gas", gas, "value", value.String()) + var ( + fromTrace *ActionTrace + trace *ActionTrace + ) + if tr.rootTrace != nil && len(tr.rootTrace.Stack) > 0 { + fromTrace = tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1] + } else { + return } + if value == nil { + value = big.NewInt(0) + } + // copy values + toAddress := to + fromAddress := from // Match processed instruction and create trace based on it switch op { case vm.CREATE, vm.CREATE2: - tr.traceAddress = addTraceAddress(tr.traceAddress, depth) - fromTrace := tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1] - - // Get input data from memory - // Note that these variables aren't verified and can be faulty - offset := stackPosFromEnd(scope.Stack.Data(), 1).Uint64() - inputSize := stackPosFromEnd(scope.Stack.Data(), 2).Uint64() - var input []byte - if inputSize > 0 { - if offset <= offset+inputSize && // no overflow - offset+inputSize <= uint64(len(scope.Memory.Data())) { - input = make([]byte, inputSize) - copy(input, scope.Memory.Data()[offset:offset+inputSize]) - } - // if it's a faulty SC call, assume that input is nil - } - // Create new trace - trace := NewActionTraceFromTrace(fromTrace, CREATE, tr.traceAddress) - from := scope.Contract.Address() - traceAction := NewAddressAction(&from, gas, input, nil, fromTrace.Action.Value, nil) + trace = NewActionTraceFromTrace(fromTrace, CREATE, tr.traceAddress) + traceAction := NewAddressAction(&fromAddress, gas, input, &toAddress, hexutil.Big(*value), nil) trace.Action = traceAction - trace.Result.GasUsed = hexutil.Uint64(gas) - fromTrace.childTraces = append(fromTrace.childTraces, trace) - tr.rootTrace.Stack = append(tr.rootTrace.Stack, trace) - tr.state = append(tr.state, depthState{depth, true}) case vm.CALL, vm.CALLCODE, vm.DELEGATECALL, vm.STATICCALL: - var ( - inOffset, inSize uint64 - retOffset, retSize uint64 - input []byte - value = big.NewInt(0) - ) - - if vm.DELEGATECALL == op || vm.STATICCALL == op { - // Note that these variables aren't verified and can be faulty - inOffset = stackPosFromEnd(scope.Stack.Data(), 2).Uint64() - inSize = stackPosFromEnd(scope.Stack.Data(), 3).Uint64() - retOffset = stackPosFromEnd(scope.Stack.Data(), 4).Uint64() - retSize = stackPosFromEnd(scope.Stack.Data(), 5).Uint64() - } else { - // Note that these variables aren't verified and can be faulty - inOffset = stackPosFromEnd(scope.Stack.Data(), 3).Uint64() - inSize = stackPosFromEnd(scope.Stack.Data(), 4).Uint64() - retOffset = stackPosFromEnd(scope.Stack.Data(), 5).Uint64() - retSize = stackPosFromEnd(scope.Stack.Data(), 6).Uint64() - // only CALL and CALLCODE need `value` field - value = stackPosFromEnd(scope.Stack.Data(), 2) - } - if inSize > 0 { - if inOffset <= inOffset+inSize && // no overflow - inOffset+inSize <= uint64(len(scope.Memory.Data())) { - input = make([]byte, inSize) - copy(input, scope.Memory.Data()[inOffset:inOffset+inSize]) - } - // if it's a faulty SC call, assume that input is nil - } - tr.traceAddress = addTraceAddress(tr.traceAddress, depth) - fromTrace := tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1] - // create new trace - trace := NewActionTraceFromTrace(fromTrace, CALL, tr.traceAddress) - from := scope.Contract.Address() - addr := common.BytesToAddress(stackPosFromEnd(scope.Stack.Data(), 1).Bytes()) + + trace = NewActionTraceFromTrace(fromTrace, CALL, tr.traceAddress) callType := strings.ToLower(op.String()) - traceAction := NewAddressAction(&from, gas, input, &addr, hexutil.Big(*value), &callType) + traceAction := NewAddressAction(&fromAddress, gas, input, &toAddress, hexutil.Big(*value), &callType) trace.Action = traceAction - fromTrace.childTraces = append(fromTrace.childTraces, trace) - trace.Result.RetOffset = retOffset - trace.Result.RetSize = retSize - tr.rootTrace.Stack = append(tr.rootTrace.Stack, trace) - tr.state = append(tr.state, depthState{depth, false}) - - case vm.RETURN, vm.STOP: - if tr != nil { - result := tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1].Result - if result != nil { - var data []byte - - if vm.STOP != op { - // Note that these variables aren't verified and can be faulty - offset := stackPosFromEnd(scope.Stack.Data(), 0).Uint64() - size := stackPosFromEnd(scope.Stack.Data(), 1).Uint64() - if size > 0 { - if offset <= offset+size && // no overflow - offset+size <= uint64(len(scope.Memory.Data())) { - data = make([]byte, size) - copy(data, scope.Memory.Data()[offset:offset+size]) - } - // if it's a faulty SC call, assume that input is nil - } - } - - if lastState(tr.state).create { - code := hexutil.Bytes(data) - result.Code = &code - } else { - result.GasUsed = hexutil.Uint64(gas) - out := hexutil.Bytes(data) - result.Output = &out - } - } - } - - case vm.REVERT: - tr.reverted = true - tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1].Result = nil - tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1].Error = "Reverted" case vm.SELFDESTRUCT: - tr.traceAddress = addTraceAddress(tr.traceAddress, depth) - fromTrace := tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1] - trace := NewActionTraceFromTrace(fromTrace, SELFDESTRUCT, tr.traceAddress) - action := fromTrace.Action - - from := scope.Contract.Address() - traceAction := NewAddressAction(nil, 0, nil, nil, action.Value, nil) - traceAction.Address = &from - // set refund values - refundAddress := common.BytesToAddress(stackPosFromEnd(scope.Stack.Data(), 0).Bytes()) - traceAction.RefundAddress = &refundAddress - // Add `balance` field for convenient usage - traceAction.Balance = &traceAction.Value + + trace = NewActionTraceFromTrace(fromTrace, SELFDESTRUCT, tr.traceAddress) + traceAction := NewAddressAction(&fromAddress, gas, input, nil, hexutil.Big(*value), nil) + traceAction.Address = &fromAddress + traceAction.RefundAddress = &toAddress + traceAction.Balance = (*hexutil.Big)(value) trace.Action = traceAction - fromTrace.childTraces = append(fromTrace.childTraces, trace) } + tr.rootTrace.Stack = append(tr.rootTrace.Stack, trace) } -// CaptureEnd is called after the call finishes to finalize the tracing. -func (tr *TraceStructLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) { +// CaptureExit is called when returning from an inner call +func (tr *TraceStructLogger) CaptureExit(output []byte, gasUsed uint64, err error) { defer func() { if r := recover(); r != nil { - log.Error("Tracer CaptureEnd failed", r) + log.Error("Tracer CaptureExit failed", r) } }() - log.Debug("TraceStructLogger capture END", "tx hash", tr.tx.String(), "duration", t, "gasUsed", gasUsed, "error", err) - if err != nil && err != vm.ErrExecutionReverted { - if tr.rootTrace != nil && tr.rootTrace.Stack != nil && len(tr.rootTrace.Stack) > 0 { - tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1].Result = nil - tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1].Error = err.Error() - } + log.Debug("TraceStructLogger Capture Exit", "tx hash", tr.tx.String(), "output", string(output), "gasUsed", gasUsed, "error", err) + + if tr.rootTrace == nil { + return } - if gasUsed > 0 { - if tr.rootTrace.Actions[0].Result != nil { - tr.rootTrace.Actions[0].Result.GasUsed = hexutil.Uint64(gasUsed) - } - tr.rootTrace.lastTrace().Action.Gas = hexutil.Uint64(gasUsed) - tr.gasUsed = gasUsed + size := len(tr.rootTrace.Stack) + if size <= 1 { + return } - tr.output = output -} -func (*TraceStructLogger) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + trace := tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1] + tr.rootTrace.Stack = tr.rootTrace.Stack[:len(tr.rootTrace.Stack)-1] + + parent := tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1] + parent.childTraces = append(parent.childTraces, trace) + + trace.processOutput(output, err, false) + + result := trace.Result + if result != nil { + result.GasUsed = hexutil.Uint64(gasUsed) + } } -// CaptureExit is called when returning from an inner call -func (tr *TraceStructLogger) CaptureExit(output []byte, gasUsed uint64, err error) { +// CaptureEnd is called after the call finishes to finalize the tracing. +func (tr *TraceStructLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) { defer func() { if r := recover(); r != nil { - log.Error("Tracer CaptureExit failed", r) + log.Error("Tracer CaptureEnd failed", r) } }() - // When going back from inner call - result := tr.rootTrace.Stack[len(tr.rootTrace.Stack)-1].Result - if result != nil { - result.GasUsed = hexutil.Uint64(gasUsed) - out := hexutil.Bytes(output) - result.Output = &out + log.Info("TraceStructLogger Capture END", "tx hash", tr.tx.String(), "duration", t, "gasUsed", gasUsed, "error", err) + + if tr.rootTrace != nil && tr.rootTrace.lastTrace() != nil { + + trace := tr.rootTrace.lastTrace() + trace.processOutput(output, err, true) + if trace.Result != nil { + trace.Result.GasUsed = hexutil.Uint64(gasUsed) + } } } @@ -316,13 +213,47 @@ func (tr *TraceStructLogger) CaptureExit(output []byte, gasUsed uint64, err erro func (tr *TraceStructLogger) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { } +// Handle output data and error +func (trace *ActionTrace) processOutput(output []byte, err error, rootTrace bool) { + output = common.CopyBytes(output) + if err == nil { + switch trace.TraceType { + case CREATE: + trace.Result.Code = (*hexutil.Bytes)(&output) + if !rootTrace { + trace.Result.Address = trace.Action.To + trace.Action.To = nil + } + case CALL: + trace.Result.Output = (*hexutil.Bytes)(&output) + default: + } + return + } else { + trace.Result = nil + } + + trace.Error = err.Error() + if trace.TraceType == CREATE { + trace.Action.To = nil + } + if !errors.Is(err, vm.ErrExecutionReverted) || len(output) == 0 { + return + } + if len(output) < 4 { + return + } + if unpacked, err := abi.UnpackRevert(output); err == nil { + trace.Error = unpacked + } +} + // Reset function to be able to reuse logger func (tr *TraceStructLogger) reset() { tr.to = nil tr.from = nil tr.inputData = nil tr.rootTrace = nil - tr.reverted = false } // SetTx basic setter @@ -367,17 +298,17 @@ func (tr *TraceStructLogger) SetNewAddress(newAddress common.Address) { // SetGasUsed basic setter func (tr *TraceStructLogger) SetGasUsed(gasUsed uint64) { - tr.gasUsed = gasUsed + tr.gasLimit = gasUsed } // ProcessTx finalizes trace proces and stores result into key-value persistant store func (tr *TraceStructLogger) ProcessTx() { - if tr.rootTrace != nil { - tr.rootTrace.lastTrace().Action.Gas = hexutil.Uint64(tr.gasUsed) + if tr.rootTrace != nil && tr.rootTrace.lastTrace() != nil { + tr.rootTrace.lastTrace().Action.Gas = hexutil.Uint64(tr.gasLimit) if tr.rootTrace.lastTrace().Result != nil { - tr.rootTrace.lastTrace().Result.GasUsed = hexutil.Uint64(tr.gasUsed) + tr.rootTrace.lastTrace().Result.GasUsed = hexutil.Uint64(tr.gasLimit) } - tr.rootTrace.processLastTrace() + tr.rootTrace.processTraces() } } @@ -388,7 +319,6 @@ func (tr *TraceStructLogger) SaveTrace() { tr.rootTrace.AddTrace(GetErrorTraceFromLogger(tr)) } - //if tr.store != nil && tr.rootTrace != nil { if tr.store != nil { // Convert trace objects to json byte array and save it tracesBytes, _ := json.Marshal(tr.rootTrace.Actions) @@ -534,81 +464,38 @@ type AddressAction struct { // TraceActionResult holds information related to result of the // processed transaction type TraceActionResult struct { - GasUsed hexutil.Uint64 `json:"gasUsed"` - Output *hexutil.Bytes `json:"output,omitempty"` - Code *hexutil.Bytes `json:"code,omitempty"` - Address *common.Address `json:"address,omitempty"` - RetOffset uint64 `json:"-"` - RetSize uint64 `json:"-"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + Output *hexutil.Bytes `json:"output,omitempty"` + Code *hexutil.Bytes `json:"code,omitempty"` + Address *common.Address `json:"address,omitempty"` } -// depthState is struct for having state of logs processing -type depthState struct { - level int - create bool -} - -// returns last state -func lastState(state []depthState) *depthState { - return &state[len(state)-1] -} - -// adds trace address and retuns it -func addTraceAddress(traceAddress []uint32, depth int) []uint32 { - index := depth - 1 - result := make([]uint32, len(traceAddress)) - copy(result, traceAddress) - if len(result) <= index { - result = append(result, 0) - } else { - result[index]++ - } - return result -} - -// removes trace address based on depth of process -func removeTraceAddressLevel(traceAddress []uint32, depth int) []uint32 { - if len(traceAddress) > depth { - result := make([]uint32, len(traceAddress)) - copy(result, traceAddress) - - result = result[:len(result)-1] - return result - } - return traceAddress -} - -// processLastTrace initiates final information distribution +// processTraces initiates final information distribution // accros result traces -func (callTrace *CallTrace) processLastTrace() { +func (callTrace *CallTrace) processTraces() { trace := &callTrace.Actions[len(callTrace.Actions)-1] - callTrace.processTrace(trace) + callTrace.processTrace(trace, []uint32{}) } // processTrace goes thru all trace results and sets info -func (callTrace *CallTrace) processTrace(trace *ActionTrace) { +func (callTrace *CallTrace) processTrace(trace *ActionTrace, traceAddress []uint32) { + trace.TraceAddress = traceAddress trace.Subtraces = uint64(len(trace.childTraces)) - for _, childTrace := range trace.childTraces { - if CALL == trace.TraceType { - childTrace.Action.From = trace.Action.To - } else { - if trace.Result != nil { - childTrace.Action.From = trace.Result.Address - } - } - - if childTrace.Result != nil { - if trace.Action.Gas > childTrace.Result.GasUsed { - childTrace.Action.Gas = trace.Action.Gas - childTrace.Result.GasUsed - } else { - childTrace.Action.Gas = childTrace.Result.GasUsed - } - } + for i, childTrace := range trace.childTraces { + childAddress := childTraceAddress(traceAddress, i) + childTrace.TraceAddress = childAddress callTrace.AddTrace(childTrace) - callTrace.processTrace(callTrace.lastTrace()) + callTrace.processTrace(callTrace.lastTrace(), childAddress) } } +func childTraceAddress(a []uint32, i int) []uint32 { + child := make([]uint32, 0, len(a)+1) + child = append(child, a...) + child = append(child, uint32(i)) + return child +} + // GetErrorTrace constructs filled error trace func GetErrorTrace(blockHash common.Hash, blockNumber big.Int, from *common.Address, to *common.Address, txHash common.Hash, index uint64, err error) *ActionTrace { return createErrorTrace(blockHash, blockNumber, from, to, txHash, 0, []byte{}, hexutil.Big{}, index, err) @@ -619,7 +506,7 @@ func GetErrorTraceFromLogger(tr *TraceStructLogger) *ActionTrace { if tr == nil { return nil } else { - return createErrorTrace(tr.blockHash, tr.blockNumber, tr.from, tr.to, tr.tx, tr.gasUsed, tr.inputData, hexutil.Big(tr.value), uint64(tr.txIndex), tr.err) + return createErrorTrace(tr.blockHash, tr.blockNumber, tr.from, tr.to, tr.tx, tr.gasLimit, tr.inputData, hexutil.Big(tr.value), uint64(tr.txIndex), tr.err) } }