laconicd/x/evm/keeper/grpc_query_test.go
crypto-facs c7554e96aa
rpc, evm: debug_traceTransaction endpoint (#506)
* fix typo

* Added tracers package to debug API

* Add GetTransactionByHash function to backend package

* first version

* traceTransaction first version

* clean PR

* revert debug changes

* Update proto/ethermint/evm/v1/query.proto

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

* remove unnecesary panic

* remove internal debug api

* trace transaction javascript tracer

* add support for custom logConfig

* added comment

* traceTransactions tests

* fix linter

* remove unused

* add comments to traceConfig

* update dependencies

* updated endpoints md

* Apply suggestions from code review

* Update x/evm/keeper/grpc_query.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

* Update x/evm/keeper/grpc_query.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>
2021-09-04 20:33:06 +00:00

823 lines
20 KiB
Go

package keeper_test
import (
"encoding/json"
"fmt"
"math/big"
"google.golang.org/grpc/metadata"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
ethermint "github.com/tharsis/ethermint/types"
"github.com/tharsis/ethermint/x/evm/types"
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
)
//Not valid Ethereum address
const invalidAddress = "0x0000"
func (suite *KeeperTestSuite) TestQueryAccount() {
var (
req *types.QueryAccountRequest
expAccount *types.QueryAccountResponse
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{
"invalid address",
func() {
expAccount = &types.QueryAccountResponse{
Balance: "0",
CodeHash: common.BytesToHash(crypto.Keccak256(nil)).Hex(),
Nonce: 0,
}
req = &types.QueryAccountRequest{
Address: invalidAddress,
}
},
false,
},
{
"success",
func() {
amt := sdk.Coins{ethermint.NewPhotonCoinInt64(100)}
err := suite.app.BankKeeper.MintCoins(suite.ctx, types.ModuleName, amt)
suite.Require().NoError(err)
err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleName, suite.address.Bytes(), amt)
suite.Require().NoError(err)
expAccount = &types.QueryAccountResponse{
Balance: "100",
CodeHash: common.BytesToHash(crypto.Keccak256(nil)).Hex(),
Nonce: 0,
}
req = &types.QueryAccountRequest{
Address: suite.address.String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
res, err := suite.queryClient.Account(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expAccount, res)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryCosmosAccount() {
var (
req *types.QueryCosmosAccountRequest
expAccount *types.QueryCosmosAccountResponse
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{"invalid address",
func() {
expAccount = &types.QueryCosmosAccountResponse{
CosmosAddress: sdk.AccAddress(common.Address{}.Bytes()).String(),
}
req = &types.QueryCosmosAccountRequest{
Address: invalidAddress,
}
},
false,
},
{
"success",
func() {
expAccount = &types.QueryCosmosAccountResponse{
CosmosAddress: sdk.AccAddress(suite.address.Bytes()).String(),
Sequence: 0,
AccountNumber: 0,
}
req = &types.QueryCosmosAccountRequest{
Address: suite.address.String(),
}
},
true,
},
{
"success with seq and account number",
func() {
acc := suite.app.AccountKeeper.GetAccount(suite.ctx, suite.address.Bytes())
suite.Require().NoError(acc.SetSequence(10))
suite.Require().NoError(acc.SetAccountNumber(1))
suite.app.AccountKeeper.SetAccount(suite.ctx, acc)
expAccount = &types.QueryCosmosAccountResponse{
CosmosAddress: sdk.AccAddress(suite.address.Bytes()).String(),
Sequence: 10,
AccountNumber: 1,
}
req = &types.QueryCosmosAccountRequest{
Address: suite.address.String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
res, err := suite.queryClient.CosmosAccount(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expAccount, res)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryBalance() {
var (
req *types.QueryBalanceRequest
expBalance string
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{"invalid address",
func() {
expBalance = "0"
req = &types.QueryBalanceRequest{
Address: invalidAddress,
}
},
false,
},
{
"success",
func() {
amt := sdk.Coins{ethermint.NewPhotonCoinInt64(100)}
err := suite.app.BankKeeper.MintCoins(suite.ctx, types.ModuleName, amt)
suite.Require().NoError(err)
err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleName, suite.address.Bytes(), amt)
suite.Require().NoError(err)
expBalance = "100"
req = &types.QueryBalanceRequest{
Address: suite.address.String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
res, err := suite.queryClient.Balance(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expBalance, res.Balance)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryStorage() {
var (
req *types.QueryStorageRequest
expValue string
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{"invalid address",
func() {
req = &types.QueryStorageRequest{
Address: invalidAddress,
}
},
false,
},
{
"success",
func() {
key := common.BytesToHash([]byte("key"))
value := common.BytesToHash([]byte("value"))
expValue = value.String()
suite.app.EvmKeeper.SetState(suite.address, key, value)
req = &types.QueryStorageRequest{
Address: suite.address.String(),
Key: key.String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
res, err := suite.queryClient.Storage(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expValue, res.Value)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryCode() {
var (
req *types.QueryCodeRequest
expCode []byte
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{"invalid address",
func() {
req = &types.QueryCodeRequest{
Address: invalidAddress,
}
exp := &types.QueryCodeResponse{}
expCode = exp.Code
},
false,
},
{
"success",
func() {
expCode = []byte("code")
suite.app.EvmKeeper.SetCode(suite.address, expCode)
req = &types.QueryCodeRequest{
Address: suite.address.String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
res, err := suite.queryClient.Code(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expCode, res.Code)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryTxLogs() {
var (
req *types.QueryTxLogsRequest
expLogs []*types.Log
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{"empty hash",
func() {
req = &types.QueryTxLogsRequest{
Hash: common.Hash{}.String(),
}
},
false,
},
{"logs not found",
func() {
hash := common.BytesToHash([]byte("hash"))
req = &types.QueryTxLogsRequest{
Hash: hash.String(),
}
},
true,
},
{
"success",
func() {
hash := common.BytesToHash([]byte("tx_hash"))
expLogs = []*types.Log{
{
Address: suite.address.String(),
Topics: []string{common.BytesToHash([]byte("topic")).String()},
Data: []byte("data"),
BlockNumber: 1,
TxHash: hash.String(),
TxIndex: 1,
BlockHash: common.BytesToHash([]byte("block_hash")).String(),
Index: 0,
Removed: false,
},
}
suite.app.EvmKeeper.SetLogs(hash, types.LogsToEthereum(expLogs))
req = &types.QueryTxLogsRequest{
Hash: hash.String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
res, err := suite.queryClient.TxLogs(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expLogs, res.Logs)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryBlockLogs() {
var (
req *types.QueryBlockLogsRequest
expLogs []types.TransactionLogs
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{"empty hash",
func() {
req = &types.QueryBlockLogsRequest{
Hash: common.Hash{}.String(),
}
},
false,
},
{"logs not found",
func() {
hash := common.BytesToHash([]byte("hash"))
req = &types.QueryBlockLogsRequest{
Hash: hash.String(),
}
},
true,
},
{
"success",
func() {
hash := common.BytesToHash([]byte("block_hash"))
expLogs = []types.TransactionLogs{
{
Hash: common.BytesToHash([]byte("tx_hash_0")).String(),
Logs: []*types.Log{
{
Address: suite.address.String(),
Topics: []string{common.BytesToHash([]byte("topic")).String()},
Data: []byte("data"),
BlockNumber: 1,
TxHash: common.BytesToHash([]byte("tx_hash_0")).String(),
TxIndex: 1,
BlockHash: common.BytesToHash([]byte("block_hash")).String(),
Index: 0,
Removed: false,
},
},
},
{
Hash: common.BytesToHash([]byte("tx_hash_1")).String(),
Logs: []*types.Log{
{
Address: suite.address.String(),
Topics: []string{common.BytesToHash([]byte("topic")).String()},
Data: []byte("data"),
BlockNumber: 1,
TxHash: common.BytesToHash([]byte("tx_hash_1")).String(),
TxIndex: 1,
BlockHash: common.BytesToHash([]byte("block_hash")).String(),
Index: 1,
Removed: false,
},
{
Address: suite.address.String(),
Topics: []string{common.BytesToHash([]byte("topic_1")).String()},
Data: []byte("data_1"),
BlockNumber: 1,
TxHash: common.BytesToHash([]byte("tx_hash_1")).String(),
TxIndex: 1,
BlockHash: common.BytesToHash([]byte("block_hash")).String(),
Index: 2,
Removed: false,
},
},
},
}
suite.app.EvmKeeper.SetLogs(common.BytesToHash([]byte("tx_hash_0")), types.LogsToEthereum(expLogs[0].Logs))
suite.app.EvmKeeper.SetLogs(common.BytesToHash([]byte("tx_hash_1")), types.LogsToEthereum(expLogs[1].Logs))
req = &types.QueryBlockLogsRequest{
Hash: hash.String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
res, err := suite.queryClient.BlockLogs(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expLogs, res.TxLogs)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryBlockBloom() {
var (
req *types.QueryBlockBloomRequest
expBloom []byte
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{"bad height",
func() {
req = &types.QueryBlockBloomRequest{Height: -2}
},
false,
},
{
"bloom from transient store",
func() {
req = &types.QueryBlockBloomRequest{Height: 1}
bloom := ethtypes.BytesToBloom([]byte("bloom"))
expBloom = bloom.Bytes()
suite.app.EvmKeeper.WithContext(suite.ctx.WithBlockHeight(1))
suite.app.EvmKeeper.SetBlockBloomTransient(bloom.Big())
},
true,
},
{"bloom not found for height",
func() {
req = &types.QueryBlockBloomRequest{Height: 100}
bloom := ethtypes.BytesToBloom([]byte("bloom"))
expBloom = bloom.Bytes()
suite.ctx = suite.ctx.WithBlockHeight(100)
suite.app.EvmKeeper.SetBlockBloom(suite.ctx, 2, bloom)
},
false,
},
{
"success",
func() {
req = &types.QueryBlockBloomRequest{Height: 3}
bloom := ethtypes.BytesToBloom([]byte("bloom"))
expBloom = bloom.Bytes()
suite.ctx = suite.ctx.WithBlockHeight(3)
suite.app.EvmKeeper.SetBlockBloom(suite.ctx, 3, bloom)
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, fmt.Sprintf("%d", suite.ctx.BlockHeight()))
res, err := suite.queryClient.BlockBloom(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expBloom, res.Bloom)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryParams() {
ctx := sdk.WrapSDKContext(suite.ctx)
expParams := types.DefaultParams()
res, err := suite.queryClient.Params(ctx, &types.QueryParamsRequest{})
suite.Require().NoError(err)
suite.Require().Equal(expParams, res.Params)
}
func (suite *KeeperTestSuite) TestQueryValidatorAccount() {
var (
req *types.QueryValidatorAccountRequest
expAccount *types.QueryValidatorAccountResponse
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{"invalid address",
func() {
expAccount = &types.QueryValidatorAccountResponse{
AccountAddress: sdk.AccAddress(common.Address{}.Bytes()).String(),
}
req = &types.QueryValidatorAccountRequest{
ConsAddress: "",
}
},
false,
},
{
"success",
func() {
expAccount = &types.QueryValidatorAccountResponse{
AccountAddress: sdk.AccAddress(suite.address.Bytes()).String(),
Sequence: 0,
AccountNumber: 0,
}
req = &types.QueryValidatorAccountRequest{
ConsAddress: suite.consAddress.String(),
}
},
true,
},
{
"success with seq and account number",
func() {
acc := suite.app.AccountKeeper.GetAccount(suite.ctx, suite.address.Bytes())
suite.Require().NoError(acc.SetSequence(10))
suite.Require().NoError(acc.SetAccountNumber(1))
suite.app.AccountKeeper.SetAccount(suite.ctx, acc)
expAccount = &types.QueryValidatorAccountResponse{
AccountAddress: sdk.AccAddress(suite.address.Bytes()).String(),
Sequence: 10,
AccountNumber: 1,
}
req = &types.QueryValidatorAccountRequest{
ConsAddress: suite.consAddress.String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.ctx)
res, err := suite.queryClient.ValidatorAccount(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expAccount, res)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestEstimateGas() {
ctx := sdk.WrapSDKContext(suite.ctx)
gasHelper := hexutil.Uint64(20000)
var (
args types.CallArgs
gasCap uint64
)
testCases := []struct {
msg string
malleate func()
expPass bool
expGas uint64
}{
// should success, because transfer value is zero
{"default args", func() {
args = types.CallArgs{To: &common.Address{}}
}, true, 21000},
// should fail, because the default From address(zero address) don't have fund
{"not enough balance", func() {
args = types.CallArgs{To: &common.Address{}, Value: (*hexutil.Big)(big.NewInt(100))}
}, false, 0},
// should success, enough balance now
{"enough balance", func() {
args = types.CallArgs{To: &common.Address{}, From: &suite.address, Value: (*hexutil.Big)(big.NewInt(100))}
}, false, 0},
// should success, because gas limit lower than 21000 is ignored
{"gas exceed allowance", func() {
args = types.CallArgs{To: &common.Address{}, Gas: &gasHelper}
}, true, 21000},
// should fail, invalid gas cap
{"gas exceed global allowance", func() {
args = types.CallArgs{To: &common.Address{}}
gasCap = 20000
}, false, 0},
// estimate gas of an erc20 contract deployment, the exact gas number is checked with geth
{"contract deployment", func() {
ctorArgs, err := ContractABI.Pack("", &suite.address, sdk.NewIntWithDecimal(1000, 18).BigInt())
suite.Require().NoError(err)
data := append(ContractBin, ctorArgs...)
args = types.CallArgs{
From: &suite.address,
Data: (*hexutil.Bytes)(&data),
}
}, true, 1186778},
// estimate gas of an erc20 transfer, the exact gas number is checked with geth
{"erc20 transfer", func() {
contractAddr := suite.DeployTestContract(suite.T(), suite.address, sdk.NewIntWithDecimal(1000, 18).BigInt())
suite.Commit()
transferData, err := ContractABI.Pack("transfer", common.HexToAddress("0x378c50D9264C63F3F92B806d4ee56E9D86FfB3Ec"), big.NewInt(1000))
suite.Require().NoError(err)
args = types.CallArgs{To: &contractAddr, From: &suite.address, Data: (*hexutil.Bytes)(&transferData)}
}, true, 51880},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest()
gasCap = 25_000_000
tc.malleate()
args, err := json.Marshal(&args)
suite.Require().NoError(err)
req := types.EthCallRequest{
Args: args,
GasCap: gasCap,
}
rsp, err := suite.queryClient.EstimateGas(ctx, &req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().Equal(tc.expGas, rsp.Gas)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestTraceTx() {
ctx := sdk.WrapSDKContext(suite.ctx)
//TODO deploy contract that triggers internal transactions
var (
txMsg *types.MsgEthereumTx
traceConfig *types.TraceConfig
)
testCases := []struct {
msg string
malleate func()
expPass bool
traceResponse []byte
}{
{
msg: "default trace",
malleate: func() {
traceConfig = nil
},
expPass: true,
traceResponse: []byte{0x7b, 0x22, 0x67, 0x61, 0x73, 0x22, 0x3a, 0x33, 0x34, 0x38, 0x32, 0x38, 0x2c, 0x22, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x22, 0x3a, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x22, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3a, 0x22, 0x22, 0x2c, 0x22, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x22, 0x3a, 0x5b, 0x5d, 0x7d},
}, {
msg: "javascript tracer",
malleate: func() {
traceConfig = &types.TraceConfig{
Tracer: "{data: [], fault: function(log) {}, step: function(log) { if(log.op.toString() == \"CALL\") this.data.push(log.stack.peek(0)); }, result: function() { return this.data; }}",
}
},
expPass: true,
traceResponse: []byte{0x5b, 0x5d},
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest()
//Deploy contract
contractAddr := suite.DeployTestContract(suite.T(), suite.address, sdk.NewIntWithDecimal(1000, 18).BigInt())
suite.Commit()
//Generate token transfer transaction
txMsg = suite.TransferERC20Token(suite.T(), contractAddr, suite.address, common.HexToAddress("0x378c50D9264C63F3F92B806d4ee56E9D86FfB3Ec"), sdk.NewIntWithDecimal(1, 18).BigInt())
suite.Commit()
tc.malleate()
traceReq := types.QueryTraceTxRequest{
Msg: txMsg,
TraceConfig: traceConfig,
TxIndex: 1, // Can be hardcoded as this will be the only tx included in the block
}
res, err := suite.queryClient.TraceTx(ctx, &traceReq)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().Equal(tc.traceResponse, res.Data)
} else {
suite.Require().Error(err)
}
})
}
}