Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api] implement web3 API debug_traceTransaction #3779

Merged
merged 16 commits into from
Feb 17, 2023
30 changes: 30 additions & 0 deletions api/coreservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
37 changes: 37 additions & 0 deletions api/coreservice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 8 additions & 25 deletions api/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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())
Expand Down
14 changes: 2 additions & 12 deletions api/grpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
})
Expand Down
57 changes: 57 additions & 0 deletions api/web3server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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
}
27 changes: 27 additions & 0 deletions api/web3server_marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -75,6 +80,28 @@ type (
CurrentBlock string `json:"currentBlock"`
HighestBlock string `json:"highestBlock"`
}
structLog struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if custom marshalling is used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, structLog = logger.StructLog, but logger.StructLog ignored json field storage, web3 API needs to export it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be done in MarshalJSON()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is no need to implement marshalJSON here, the structLog field can already be automatically marshaled inside. use interface{} is harder to understand

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 (
Expand Down
34 changes: 34 additions & 0 deletions api/web3server_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"context"
"encoding/hex"
"fmt"
"io"
Expand All @@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -973,3 +976,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))
}
Loading