diff --git a/go.mod b/go.mod index 6cb1e3daa..066c3bdf2 100644 --- a/go.mod +++ b/go.mod @@ -105,6 +105,8 @@ require ( github.com/eapache/go-resiliency v1.2.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect github.com/eapache/queue v1.1.0 // indirect + github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e // indirect + github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/golang/glog v1.1.0 // indirect github.com/google/go-cmp v0.5.9 // indirect diff --git a/go.sum b/go.sum index c69620b0f..741dce326 100644 --- a/go.sum +++ b/go.sum @@ -176,6 +176,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/ferranbt/fastssz v0.1.3 h1:ZI+z3JH05h4kgmFXdHuR1aWYsgrg7o+Fw7/NCzM16Mo= github.com/ferranbt/fastssz v0.1.3/go.mod h1:0Y9TEd/9XuFlh7mskMPfXiI2Dkw4Ddg9EyXt1W7MRvE= +github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e h1:bBLctRc7kr01YGvaDfgLbTwjFNW5jdp5y5rj8XXBHfY= +github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -187,6 +189,8 @@ github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= diff --git a/node/cn/tracers/native/call_tracer.go b/node/cn/tracers/native/call_tracer.go new file mode 100644 index 000000000..f3f6a84b3 --- /dev/null +++ b/node/cn/tracers/native/call_tracer.go @@ -0,0 +1,215 @@ +// Modifications Copyright 2024 The Kaia Authors +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library 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 Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package native + +import ( + "encoding/json" + "errors" + "math/big" + "sync/atomic" + + "github.com/klaytn/klaytn/accounts/abi" + "github.com/klaytn/klaytn/blockchain/vm" + "github.com/klaytn/klaytn/common" + "github.com/klaytn/klaytn/common/hexutil" +) + +var _ vm.Tracer = (*CallTracer)(nil) + +//go:generate go run github.com/fjl/gencodec -type CallFrame -field-override callFrameMarshaling -out gen_callframe_json.go +type CallFrame struct { + Type vm.OpCode `json:"-"` // e.g. CALL, DELEGATECALL, CREATE + From common.Address `json:"from"` + Gas uint64 `json:"gas"` // gasLeft. for top-level call, tx.gasLimit + GasUsed uint64 `json:"gasUsed"` // gasUsed so far. for top-level call, tx.gasLimit - gasLeft = receipt.gasUsed + To *common.Address `json:"to,omitempty"` // recipient address, created contract address, or nil for failed contract creation, + Input []byte `json:"input"` + Output []byte `json:"output,omitempty"` // result of an internal call or revert message or runtime bytecode + Error string `json:"error,omitempty"` + RevertReason string `json:"revertReason,omitempty"` // decoded revert message in geth style. + Reverted *RevertedInfo `json:"reverted,omitempty"` // decoded revert message and reverted contract address in klaytn style. + Calls []CallFrame `json:"calls,omitempty"` // child calls + Value *big.Int `json:"value,omitempty"` +} + +type RevertedInfo struct { + Contract *common.Address `json:"contract,omitempty"` + Message string `json:"message,omitempty"` +} + +func (f CallFrame) TypeString() string { // to satisfy gencodec + return f.Type.String() +} + +// FieldType overrides for callFrame that's used for JSON encoding +// Must rerun gencodec after modifying this struct +type callFrameMarshaling struct { + TypeString string `json:"type"` + Gas hexutil.Uint64 + GasUsed hexutil.Uint64 + Value *hexutil.Big + Input hexutil.Bytes + Output hexutil.Bytes +} + +// Populate output, error, and revert-related fields +// 1. no error: {output} +// 2. non-revert error: {to: nil if CREATE, error} +// 3. revert error without message: {to: nil if CREATE, output, error, reverted{contract}} +// 4. revert error with message: {to: nil if CREATE, output, error, reverted{contract, message}, revertReason} +func (c *CallFrame) processOutput(output []byte, err error) { + // 1: return output + if err == nil { + c.Output = common.CopyBytes(output) + return + } + + // 2,3,4: to = nil if CREATE failed + if c.Type == vm.CREATE || c.Type == vm.CREATE2 { + c.To = nil + } + + // 2: do not return output + if !errors.Is(err, vm.ErrExecutionReverted) { // non-revert error + c.Error = err.Error() + return + } + + // 3,4: return output and revert info + c.Output = common.CopyBytes(output) + c.Error = "execution reverted" + c.Reverted = &RevertedInfo{Contract: c.To} // 'To' was recorded when entering this call frame + + // 4: attach revert reason + if reason, unpackErr := abi.UnpackRevert(output); unpackErr == nil { + c.RevertReason = reason + c.Reverted.Message = reason + } +} + +// Implements vm.Tracer interface +type CallTracer struct { + callstack []CallFrame + gasLimit uint64 // saved tx.gasLimit + interrupt atomic.Bool + interruptReason error +} + +func NewCallTracer() *CallTracer { + return &CallTracer{ + callstack: make([]CallFrame, 1), // empty top-level frame + } +} + +// Transaction start +func (t *CallTracer) CaptureTxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +// Transaction end +func (t *CallTracer) CaptureTxEnd(gasLeft uint64) { + t.callstack[0].GasUsed = t.callstack[0].Gas - gasLeft +} + +// Enter top-level call frame +func (t *CallTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + toCopy := to + t.callstack[0] = CallFrame{ + Type: vm.CALL, + From: from, + To: &toCopy, + Input: common.CopyBytes(input), + Gas: t.gasLimit, // ignore 'gas' supplied from EVM. Use tx.gasLimit that includes intrinsic gas. + Value: value, + } + if create { + t.callstack[0].Type = vm.CREATE + } +} + +// Exit top-level call frame +func (t *CallTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { + // gasUsed will be filled by CaptureTxEnd; just process the output + t.callstack[0].processOutput(output, err) +} + +// Enter nested call frame +func (t *CallTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + if t.interrupt.Load() { + return + } + + toCopy := to + call := CallFrame{ + Type: typ, + From: from, + Gas: gas, + To: &toCopy, + Value: value, + Input: common.CopyBytes(input), + } + t.callstack = append(t.callstack, call) +} + +// Exit nested call frame +func (t *CallTracer) CaptureExit(output []byte, gasUsed uint64, err error) { + size := len(t.callstack) + if size <= 1 { // just in case; should never happen though because CaptureExit is only called when depth > 0 + return + } + + // process output into the currently exiting call + call := t.callstack[size-1] + call.GasUsed = gasUsed + call.processOutput(output, err) + + // pop current frame + t.callstack = t.callstack[:size-1] + + // append it to the parent frame's Calls + t.callstack[size-2].Calls = append(t.callstack[size-2].Calls, call) +} + +// Each opcode +func (t *CallTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost, ccLeft, ccOpcode uint64, scope *vm.ScopeContext, depth int, err error) { +} + +// Fault during opcode execution +func (t *CallTracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost, ccLeft, ccOpcode uint64, scope *vm.ScopeContext, depth int, err error) { +} + +func (t *CallTracer) GetResult() (json.RawMessage, error) { + if len(t.callstack) != 1 { + return nil, errors.New("incorrect number of top-level calls") + } + + result, err := json.Marshal(t.callstack[0]) + if err != nil { + return nil, err + } + + // Return with interrupt reason if any + return result, t.interruptReason +} + +// Stop terminates execution of the tracer at the first opportune moment. +// For CallTracer, it stops at CaptureEnter, which is the most repetitive operation. +func (t *CallTracer) Stop(err error) { + t.interrupt.Store(true) + t.interruptReason = err +} diff --git a/node/cn/tracers/native/gen_callframe_json.go b/node/cn/tracers/native/gen_callframe_json.go new file mode 100644 index 000000000..927994f38 --- /dev/null +++ b/node/cn/tracers/native/gen_callframe_json.go @@ -0,0 +1,107 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package native + +import ( + "encoding/json" + "math/big" + + "github.com/klaytn/klaytn/blockchain/vm" + "github.com/klaytn/klaytn/common" + "github.com/klaytn/klaytn/common/hexutil" +) + +var _ = (*callFrameMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (c CallFrame) MarshalJSON() ([]byte, error) { + type CallFrame0 struct { + Type vm.OpCode `json:"-"` + From common.Address `json:"from"` + Gas hexutil.Uint64 `json:"gas"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty"` + Input hexutil.Bytes `json:"input"` + Output hexutil.Bytes `json:"output,omitempty"` + Error string `json:"error,omitempty"` + RevertReason string `json:"revertReason,omitempty"` + Reverted *RevertedInfo `json:"reverted,omitempty"` + Calls []CallFrame `json:"calls,omitempty"` + Value *hexutil.Big `json:"value,omitempty"` + TypeString string `json:"type"` + } + var enc CallFrame0 + enc.Type = c.Type + enc.From = c.From + enc.Gas = hexutil.Uint64(c.Gas) + enc.GasUsed = hexutil.Uint64(c.GasUsed) + enc.To = c.To + enc.Input = c.Input + enc.Output = c.Output + enc.Error = c.Error + enc.RevertReason = c.RevertReason + enc.Reverted = c.Reverted + enc.Calls = c.Calls + enc.Value = (*hexutil.Big)(c.Value) + enc.TypeString = c.TypeString() + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (c *CallFrame) UnmarshalJSON(input []byte) error { + type CallFrame0 struct { + Type *vm.OpCode `json:"-"` + From *common.Address `json:"from"` + Gas *hexutil.Uint64 `json:"gas"` + GasUsed *hexutil.Uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty"` + Input *hexutil.Bytes `json:"input"` + Output *hexutil.Bytes `json:"output,omitempty"` + Error *string `json:"error,omitempty"` + RevertReason *string `json:"revertReason,omitempty"` + Reverted *RevertedInfo `json:"reverted,omitempty"` + Calls []CallFrame `json:"calls,omitempty"` + Value *hexutil.Big `json:"value,omitempty"` + } + var dec CallFrame0 + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Type != nil { + c.Type = *dec.Type + } + if dec.From != nil { + c.From = *dec.From + } + if dec.Gas != nil { + c.Gas = uint64(*dec.Gas) + } + if dec.GasUsed != nil { + c.GasUsed = uint64(*dec.GasUsed) + } + if dec.To != nil { + c.To = dec.To + } + if dec.Input != nil { + c.Input = *dec.Input + } + if dec.Output != nil { + c.Output = *dec.Output + } + if dec.Error != nil { + c.Error = *dec.Error + } + if dec.RevertReason != nil { + c.RevertReason = *dec.RevertReason + } + if dec.Reverted != nil { + c.Reverted = dec.Reverted + } + if dec.Calls != nil { + c.Calls = dec.Calls + } + if dec.Value != nil { + c.Value = (*big.Int)(dec.Value) + } + return nil +} diff --git a/node/cn/tracers/tracers_test.go b/node/cn/tracers/tracers_test.go index a17200e3c..861cbfcc0 100644 --- a/node/cn/tracers/tracers_test.go +++ b/node/cn/tracers/tracers_test.go @@ -35,6 +35,7 @@ import ( "github.com/klaytn/klaytn/common/hexutil" "github.com/klaytn/klaytn/common/math" "github.com/klaytn/klaytn/fork" + "github.com/klaytn/klaytn/node/cn/tracers/native" "github.com/klaytn/klaytn/params" "github.com/klaytn/klaytn/rlp" "github.com/klaytn/klaytn/storage/database" @@ -69,6 +70,47 @@ func TestPrestateTracer(t *testing.T) { }) } +func TestCallTracer(t *testing.T) { + forEachJson(t, "testdata/call_tracer", func(t *testing.T, tc *tracerTestdata) { + tracer := native.NewCallTracer() + + // Run the tracer and check the tracer result + tx, execResult, tracerResult := runTracer(t, tc, tracer) + + // Check the tracer result against the tx and execution result + // Note that CallFrame.Type is not correctly unmarshalled, so we need to unmarshal it separately + var callFrame *native.CallFrame + require.NoError(t, json.Unmarshal(tracerResult, &callFrame)) + var callFrame2 struct { + TypeString string `json:"type"` + } + require.NoError(t, json.Unmarshal(tracerResult, &callFrame2)) + + // contract creation tx, 'to' is the deployed contract address, if succeeded. + topLevelCreate := (tx.Type().IsEthereumTransaction() && tx.To() == nil) || tx.Type().IsContractDeploy() + // txs without 'to' address, yet not contract creation. treated as CALL to self in the tracer. + assumeToSelf := (tx.Type().IsAccountUpdate() || tx.Type().IsCancelTransaction() || tx.Type().IsChainDataAnchoring()) + + if topLevelCreate { + assert.Equal(t, "CREATE", callFrame2.TypeString) + } else { + assert.Equal(t, "CALL", callFrame2.TypeString) + } + assert.Equal(t, tx.ValidatedSender(), callFrame.From) + assert.Equal(t, tx.Gas(), callFrame.Gas) + assert.Equal(t, execResult.UsedGas, callFrame.GasUsed) + assert.Equal(t, tx.Data(), callFrame.Input) + if topLevelCreate && execResult.VmExecutionStatus == types.ReceiptStatusSuccessful { + assert.NotEqual(t, nil, callFrame.To) + assert.NotEqual(t, common.Address{}, callFrame.To) + } else if assumeToSelf { + assert.Equal(t, tx.ValidatedSender(), *callFrame.To) + } else { + assert.Equal(t, tx.To(), callFrame.To) + } + }) +} + func forEachJson(t *testing.T, dir string, f func(t *testing.T, tc *tracerTestdata)) { files, err := os.ReadDir(dir) require.NoError(t, err) @@ -128,6 +170,10 @@ func runTracer(t *testing.T, tc *tracerTestdata, tracer vm.Tracer) (*types.Trans switch tracer := tracer.(type) { case *Tracer: tracerResult, err = tracer.GetResult() + case *native.CallTracer: + tracerResult, err = tracer.GetResult() + default: + t.Fatalf("unexpected tracer type: %T", tracer) } require.NoError(t, err) assert.JSONEq(t, string(tc.Result), string(tracerResult))