From 043e7f84a1a8bc45f9456336698307e7007873e4 Mon Sep 17 00:00:00 2001 From: millken Date: Fri, 17 Feb 2023 15:01:25 +0800 Subject: [PATCH] [API] implement web3 API debug_traceTransaction (#3779) * add web3 api debug_tracetransaction --- api/coreservice.go | 30 ++++++++++ api/coreservice_test.go | 37 ++++++++++++ api/grpcserver.go | 33 +++-------- api/grpcserver_test.go | 14 +---- api/web3server.go | 57 +++++++++++++++++++ api/web3server_marshal.go | 27 +++++++++ api/web3server_test.go | 34 +++++++++++ go.mod | 2 +- .../mock_apicoreservice.go | 18 ++++++ 9 files changed, 214 insertions(+), 38 deletions(-) diff --git a/api/coreservice.go b/api/coreservice.go index e1ee057dbf..c2093432cc 100644 --- a/api/coreservice.go +++ b/api/coreservice.go @@ -15,6 +15,8 @@ import ( "strconv" "time" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/pkg/errors" "go.uber.org/zap" "golang.org/x/sync/errgroup" @@ -26,6 +28,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/iotexproject/go-pkgs/hash" + "github.com/iotexproject/go-pkgs/util" "github.com/iotexproject/iotex-address/address" "github.com/iotexproject/iotex-election/committee" "github.com/iotexproject/iotex-proto/golang/iotexapi" @@ -141,6 +144,8 @@ type ( ReceiveBlock(blk *block.Block) error // BlockHashByBlockHeight returns block hash by block height BlockHashByBlockHeight(blkHeight uint64) (hash.Hash256, error) + // TraceTransaction returns the trace result of a transaction + TraceTransaction(ctx context.Context, actHash string, config *logger.Config) ([]byte, *action.Receipt, *logger.StructLogger, error) } // coreService implements the CoreService interface @@ -1616,3 +1621,28 @@ func (core *coreService) SyncingProgress() (uint64, uint64, uint64) { startingHeight, currentHeight, targetHeight, _ := core.bs.SyncStatus() return startingHeight, currentHeight, targetHeight } + +// TraceTransaction returns the trace result of transaction +func (core *coreService) TraceTransaction(ctx context.Context, actHash string, config *logger.Config) ([]byte, *action.Receipt, *logger.StructLogger, error) { + actInfo, err := core.Action(util.Remove0xPrefix(actHash), false) + if err != nil { + return nil, nil, nil, err + } + act, err := (&action.Deserializer{}).SetEvmNetworkID(core.EVMNetworkID()).ActionToSealedEnvelope(actInfo.Action) + if err != nil { + return nil, nil, nil, err + } + sc, ok := act.Action().(*action.Execution) + if !ok { + return nil, nil, nil, errors.New("the type of action is not supported") + } + traces := logger.NewStructLogger(config) + ctx = protocol.WithVMConfigCtx(ctx, vm.Config{ + Debug: true, + Tracer: traces, + NoBaseFee: true, + }) + addr, _ := address.FromString(address.ZeroAddress) + retval, receipt, err := core.SimulateExecution(ctx, addr, sc) + return retval, receipt, traces, err +} diff --git a/api/coreservice_test.go b/api/coreservice_test.go index 248058f9bd..c58be66702 100644 --- a/api/coreservice_test.go +++ b/api/coreservice_test.go @@ -7,9 +7,12 @@ package api import ( "context" + "encoding/hex" + "math/big" "strconv" "testing" + "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/golang/mock/gomock" "github.com/iotexproject/iotex-proto/golang/iotexapi" "github.com/pkg/errors" @@ -21,6 +24,7 @@ import ( "github.com/iotexproject/iotex-core/api/logfilter" "github.com/iotexproject/iotex-core/blockchain" "github.com/iotexproject/iotex-core/blockchain/blockdao" + "github.com/iotexproject/iotex-core/test/identityset" "github.com/iotexproject/iotex-core/test/mock/mock_blockindex" "github.com/iotexproject/iotex-core/testutil" ) @@ -199,6 +203,39 @@ func TestEstimateGasForAction(t *testing.T) { require.Contains(err.Error(), action.ErrNilProto.Error()) } +func TestTraceTransaction(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + svr, bc, _, ap, cleanCallback := setupTestCoreSerivce() + defer cleanCallback() + ctx := context.Background() + tsf, err := action.SignedExecution(identityset.Address(29).String(), + identityset.PrivateKey(29), 1, big.NewInt(0), testutil.TestGasLimit, + big.NewInt(testutil.TestGasPriceInt64), []byte{}) + require.NoError(err) + tsfhash, err := tsf.Hash() + + blk1Time := testutil.TimestampNow() + require.NoError(ap.Add(ctx, tsf)) + blk, err := bc.MintNewBlock(blk1Time) + require.NoError(err) + require.NoError(bc.CommitBlock(blk)) + cfg := &logger.Config{ + EnableMemory: true, + DisableStack: false, + DisableStorage: false, + EnableReturnData: true, + } + retval, receipt, traces, err := svr.TraceTransaction(ctx, hex.EncodeToString(tsfhash[:]), cfg) + require.NoError(err) + require.Equal("0x", byteToHex(retval)) + require.Equal(uint64(1), receipt.Status) + require.Equal(uint64(0x2710), receipt.GasConsumed) + require.Empty(receipt.ExecutionRevertMsg()) + require.Equal(0, len(traces.StructLogs())) +} + func TestProofAndCompareReverseActions(t *testing.T) { sliceN := func(n uint64) (value []uint64) { value = make([]uint64, 0, n) diff --git a/api/grpcserver.go b/api/grpcserver.go index c7d821a5f6..c23a700fc2 100644 --- a/api/grpcserver.go +++ b/api/grpcserver.go @@ -15,13 +15,11 @@ import ( "strconv" "time" - "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers/logger" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" "github.com/iotexproject/go-pkgs/hash" - "github.com/iotexproject/go-pkgs/util" "github.com/iotexproject/iotex-address/address" "github.com/iotexproject/iotex-proto/golang/iotexapi" "github.com/iotexproject/iotex-proto/golang/iotextypes" @@ -39,7 +37,6 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/iotexproject/iotex-core/action" - "github.com/iotexproject/iotex-core/action/protocol" "github.com/iotexproject/iotex-core/api/logfilter" apitypes "github.com/iotexproject/iotex-core/api/types" "github.com/iotexproject/iotex-core/blockchain/block" @@ -659,32 +656,18 @@ func (svr *gRPCHandler) ReadContractStorage(ctx context.Context, in *iotexapi.Re // TraceTransactionStructLogs get trace transaction struct logs func (svr *gRPCHandler) TraceTransactionStructLogs(ctx context.Context, in *iotexapi.TraceTransactionStructLogsRequest) (*iotexapi.TraceTransactionStructLogsResponse, error) { - actInfo, err := svr.coreService.Action(util.Remove0xPrefix(in.GetActionHash()), false) - if err != nil { - return nil, err + cfg := &logger.Config{ + EnableMemory: true, + DisableStack: false, + DisableStorage: false, + EnableReturnData: true, } - act, err := (&action.Deserializer{}).SetEvmNetworkID(svr.coreService.EVMNetworkID()).ActionToSealedEnvelope(actInfo.Action) + _, _, traces, err := svr.coreService.TraceTransaction(ctx, in.GetActionHash(), cfg) if err != nil { - return nil, err - } - sc, ok := act.Action().(*action.Execution) - if !ok { - return nil, status.Error(codes.InvalidArgument, "the type of action is not supported") - } - tracer := logger.NewStructLogger(nil) - ctx = protocol.WithVMConfigCtx(ctx, vm.Config{ - Debug: true, - Tracer: tracer, - NoBaseFee: true, - }) - - _, _, err = svr.coreService.SimulateExecution(ctx, act.SenderAddress(), sc) - if err != nil { - return nil, err + return nil, status.Error(codes.Internal, err.Error()) } - structLogs := make([]*iotextypes.TransactionStructLog, 0) - for _, log := range tracer.StructLogs() { + for _, log := range traces.StructLogs() { var stack []string for _, s := range log.Stack { stack = append(stack, s.String()) diff --git a/api/grpcserver_test.go b/api/grpcserver_test.go index 74d54cace5..7887b058eb 100644 --- a/api/grpcserver_test.go +++ b/api/grpcserver_test.go @@ -11,6 +11,7 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/golang/mock/gomock" "github.com/iotexproject/go-pkgs/hash" "github.com/iotexproject/iotex-proto/golang/iotexapi" @@ -1019,18 +1020,7 @@ func TestGrpcServer_TraceTransactionStructLogs(t *testing.T) { core := mock_apicoreservice.NewMockCoreService(ctrl) grpcSvr := newGRPCHandler(core) - addr1 := identityset.Address(28).String() - priKey1 := identityset.PrivateKey(29) - ex1, err := action.SignedExecution(addr1, priKey1, uint64(1), big.NewInt(10), uint64(100000), big.NewInt(0), []byte{}) - require.NoError(err) - act := &iotexapi.ActionInfo{ - Index: 0, - ActHash: "_test", - Action: ex1.Proto(), - } - core.EXPECT().Action(gomock.Any(), gomock.Any()).Return(act, nil) - core.EXPECT().EVMNetworkID().Return(uint32(11)) - core.EXPECT().SimulateExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, nil) + core.EXPECT().TraceTransaction(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, logger.NewStructLogger(nil), nil) resp, err := grpcSvr.TraceTransactionStructLogs(context.Background(), &iotexapi.TraceTransactionStructLogsRequest{ ActionHash: "_actionHash", }) diff --git a/api/web3server.go b/api/web3server.go index ab02014c41..55d91e5da3 100644 --- a/api/web3server.go +++ b/api/web3server.go @@ -10,7 +10,9 @@ import ( "strconv" "time" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/iotexproject/go-pkgs/hash" "github.com/iotexproject/go-pkgs/util" "github.com/iotexproject/iotex-address/address" @@ -205,6 +207,8 @@ func (svr *web3Handler) handleWeb3Req(ctx context.Context, web3Req *gjson.Result res, err = svr.subscribe(web3Req, writer) case "eth_unsubscribe": res, err = svr.unsubscribe(web3Req) + case "debug_traceTransaction": + res, err = svr.traceTransaction(ctx, web3Req) case "eth_coinbase", "eth_getUncleCountByBlockHash", "eth_getUncleCountByBlockNumber", "eth_sign", "eth_signTransaction", "eth_sendTransaction", "eth_getUncleByBlockHashAndIndex", "eth_getUncleByBlockNumberAndIndex", "eth_pendingTransactions": @@ -880,6 +884,59 @@ func (svr *web3Handler) unsubscribe(in *gjson.Result) (interface{}, error) { return chainListener.RemoveResponder(id.String()) } +func (svr *web3Handler) traceTransaction(ctx context.Context, in *gjson.Result) (interface{}, error) { + actHash, options := in.Get("params.0"), in.Get("params.1") + if !actHash.Exists() { + return nil, errInvalidFormat + } + var ( + enableMemory, disableStack, disableStorage, enableReturnData bool + ) + if options.Exists() { + enableMemory = options.Get("enableMemory").Bool() + disableStack = options.Get("disableStack").Bool() + disableStorage = options.Get("disableStorage").Bool() + enableReturnData = options.Get("enableReturnData").Bool() + } + cfg := &logger.Config{ + EnableMemory: enableMemory, + DisableStack: disableStack, + DisableStorage: disableStorage, + EnableReturnData: enableReturnData, + } + retval, receipt, traces, err := svr.coreService.TraceTransaction(ctx, actHash.String(), cfg) + if err != nil { + return nil, err + } + + structLogs := make([]structLog, 0) + for _, s := range traces.StructLogs() { + var enc structLog + enc.Pc = s.Pc + enc.Op = s.Op + enc.Gas = math.HexOrDecimal64(s.Gas) + enc.GasCost = math.HexOrDecimal64(s.GasCost) + enc.Memory = s.Memory + enc.MemorySize = s.MemorySize + enc.Stack = s.Stack + enc.ReturnData = s.ReturnData + enc.Storage = s.Storage + enc.Depth = s.Depth + enc.RefundCounter = s.RefundCounter + enc.OpName = s.OpName() + enc.ErrorString = s.ErrorString() + structLogs = append(structLogs, enc) + } + + return &debugTraceTransactionResult{ + Failed: receipt.Status != uint64(iotextypes.ReceiptStatus_Success), + Revert: receipt.ExecutionRevertMsg(), + ReturnValue: byteToHex(retval), + StructLogs: structLogs, + Gas: receipt.GasConsumed, + }, nil +} + func (svr *web3Handler) unimplemented() (interface{}, error) { return nil, errNotImplemented } diff --git a/api/web3server_marshal.go b/api/web3server_marshal.go index 08647de7e7..16dde0ecdd 100644 --- a/api/web3server_marshal.go +++ b/api/web3server_marshal.go @@ -4,7 +4,12 @@ import ( "encoding/hex" "encoding/json" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/holiman/uint256" "github.com/iotexproject/go-pkgs/crypto" "github.com/iotexproject/go-pkgs/hash" "github.com/iotexproject/iotex-address/address" @@ -75,6 +80,28 @@ type ( CurrentBlock string `json:"currentBlock"` HighestBlock string `json:"highestBlock"` } + structLog struct { + Pc uint64 `json:"pc"` + Op vm.OpCode `json:"op"` + Gas math.HexOrDecimal64 `json:"gas"` + GasCost math.HexOrDecimal64 `json:"gasCost"` + Memory hexutil.Bytes `json:"memory"` + MemorySize int `json:"memSize"` + Stack []uint256.Int `json:"stack"` + ReturnData hexutil.Bytes `json:"returnData"` + Storage map[common.Hash]common.Hash `json:"storage"` + Depth int `json:"depth"` + RefundCounter uint64 `json:"refund"` + OpName string `json:"opName"` + ErrorString string `json:"error"` + } + debugTraceTransactionResult struct { + Failed bool `json:"failed"` + Revert string `json:"revert"` + ReturnValue string `json:"returnValue"` + Gas uint64 `json:"gas"` + StructLogs []structLog `json:"structLogs"` + } ) var ( diff --git a/api/web3server_test.go b/api/web3server_test.go index 159593860b..fc96834385 100644 --- a/api/web3server_test.go +++ b/api/web3server_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/hex" "fmt" "io" @@ -13,6 +14,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/golang/mock/gomock" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -30,6 +32,7 @@ import ( "github.com/iotexproject/iotex-core/test/identityset" "github.com/iotexproject/iotex-core/test/mock/mock_apicoreservice" mock_apitypes "github.com/iotexproject/iotex-core/test/mock/mock_apiresponder" + "github.com/iotexproject/iotex-core/testutil" ) func TestGetWeb3Reqs(t *testing.T) { @@ -980,3 +983,34 @@ func TestLocalAPICache(t *testing.T) { _, exist = cacheLocal.Get(testKey) require.False(exist) } + +func TestDebugTraceTransaction(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + core := mock_apicoreservice.NewMockCoreService(ctrl) + web3svr := &web3Handler{core, nil} + + ctx := context.Background() + tsf, err := action.SignedExecution(identityset.Address(29).String(), + identityset.PrivateKey(29), 1, big.NewInt(0), testutil.TestGasLimit, + big.NewInt(testutil.TestGasPriceInt64), []byte{}) + require.NoError(err) + tsfhash, err := tsf.Hash() + require.NoError(err) + receipt := &action.Receipt{Status: 1, BlockHeight: 1, ActionHash: tsfhash, GasConsumed: 100000} + structLogger := &logger.StructLogger{} + + core.EXPECT().TraceTransaction(ctx, gomock.Any(), gomock.Any()).AnyTimes().Return([]byte{0x01}, receipt, structLogger, nil) + + in := gjson.Parse(`{"params":["` + hex.EncodeToString(tsfhash[:]) + `"]}`) + ret, err := web3svr.traceTransaction(ctx, &in) + require.NoError(err) + rlt, ok := ret.(*debugTraceTransactionResult) + require.True(ok) + require.Equal("0x01", rlt.ReturnValue) + require.False(rlt.Failed) + require.Equal(uint64(100000), rlt.Gas) + require.Empty(rlt.Revert) + require.Equal(0, len(rlt.StructLogs)) +} diff --git a/go.mod b/go.mod index a2f38e4ae0..16e1bafc10 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( require ( github.com/cespare/xxhash/v2 v2.1.2 + github.com/holiman/uint256 v1.2.0 github.com/prometheus/client_model v0.2.0 github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible github.com/shirou/gopsutil/v3 v3.22.2 @@ -96,7 +97,6 @@ require ( github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 // indirect - github.com/holiman/uint256 v1.2.0 // indirect github.com/huin/goupnp v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/ipfs/go-cid v0.0.7 // indirect diff --git a/test/mock/mock_apicoreservice/mock_apicoreservice.go b/test/mock/mock_apicoreservice/mock_apicoreservice.go index 6557bda95c..be4540a4af 100644 --- a/test/mock/mock_apicoreservice/mock_apicoreservice.go +++ b/test/mock/mock_apicoreservice/mock_apicoreservice.go @@ -8,6 +8,7 @@ import ( context "context" reflect "reflect" + logger "github.com/ethereum/go-ethereum/eth/tracers/logger" gomock "github.com/golang/mock/gomock" hash "github.com/iotexproject/go-pkgs/hash" address "github.com/iotexproject/iotex-address/address" @@ -589,6 +590,23 @@ func (mr *MockCoreServiceMockRecorder) TipHeight() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TipHeight", reflect.TypeOf((*MockCoreService)(nil).TipHeight)) } +// TraceTransaction mocks base method. +func (m *MockCoreService) TraceTransaction(ctx context.Context, actHash string, config *logger.Config) ([]byte, *action.Receipt, *logger.StructLogger, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TraceTransaction", ctx, actHash, config) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*action.Receipt) + ret2, _ := ret[2].(*logger.StructLogger) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// TraceTransaction indicates an expected call of TraceTransaction. +func (mr *MockCoreServiceMockRecorder) TraceTransaction(ctx, actHash, config interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TraceTransaction", reflect.TypeOf((*MockCoreService)(nil).TraceTransaction), ctx, actHash, config) +} + // TransactionLogByActionHash mocks base method. func (m *MockCoreService) TransactionLogByActionHash(actHash string) (*iotextypes.TransactionLog, error) { m.ctrl.T.Helper()