diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc7fcb69c..30671946af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (rpc) [\#1143](https://github.com/evmos/ethermint/pull/1143) Restrict unprotected txs on the node JSON-RPC configuration. * (all) [\#1137](https://github.com/evmos/ethermint/pull/1137) Rename go module to `evmos/ethermint` +### API Breaking + +- (json-rpc) [tharsis#1121](https://github.com/tharsis/ethermint/pull/1121) Store eth tx index separately + ### Improvements * (deps) [\#1147](https://github.com/evmos/ethermint/pull/1147) Bump Go version to `1.18`. diff --git a/default.nix b/default.nix index 066b40939f..5769793291 100644 --- a/default.nix +++ b/default.nix @@ -17,7 +17,7 @@ in buildGoApplication rec { inherit pname version tags ldflags; src = lib.sourceByRegex ./. [ - "^(x|app|cmd|client|server|crypto|rpc|types|encoding|ethereum|testutil|version|go.mod|go.sum|gomod2nix.toml)($|/.*)" + "^(x|app|cmd|client|server|crypto|rpc|types|encoding|ethereum|indexer|testutil|version|go.mod|go.sum|gomod2nix.toml)($|/.*)" "^tests(/.*[.]go)?$" ]; modules = ./gomod2nix.toml; diff --git a/docs/api/proto-docs.md b/docs/api/proto-docs.md index 16ae2231b5..921dea95c8 100644 --- a/docs/api/proto-docs.md +++ b/docs/api/proto-docs.md @@ -79,6 +79,9 @@ - [ethermint/types/v1/account.proto](#ethermint/types/v1/account.proto) - [EthAccount](#ethermint.types.v1.EthAccount) +- [ethermint/types/v1/indexer.proto](#ethermint/types/v1/indexer.proto) + - [TxResult](#ethermint.types.v1.TxResult) + - [ethermint/types/v1/web3.proto](#ethermint/types/v1/web3.proto) - [ExtensionOptionsWeb3Tx](#ethermint.types.v1.ExtensionOptionsWeb3Tx) @@ -1133,6 +1136,43 @@ authtypes.BaseAccount type. It is compatible with the auth AccountKeeper. + + + + + + + + + + + +

Top

+ +## ethermint/types/v1/indexer.proto + + + + + +### TxResult +TxResult is the value stored in eth tx indexer + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `height` | [int64](#int64) | | the block height | +| `tx_index` | [uint32](#uint32) | | cosmos tx index | +| `msg_index` | [uint32](#uint32) | | the msg index in a batch tx | +| `eth_tx_index` | [int32](#int32) | | eth tx index, the index in the list of valid eth tx in the block, aka. the transaction list returned by eth_getBlock api. | +| `failed` | [bool](#bool) | | if the eth tx is failed | +| `gas_used` | [uint64](#uint64) | | gas used by tx, if exceeds block gas limit, it's set to gas limit which is what's actually deducted by ante handler. | +| `cumulative_gas_used` | [uint64](#uint64) | | the cumulative gas used within current batch tx | + + + + + diff --git a/go.mod b/go.mod index bc770a78fc..9967d559c7 100644 --- a/go.mod +++ b/go.mod @@ -33,8 +33,9 @@ require ( github.com/tendermint/tendermint v0.34.22 github.com/tendermint/tm-db v0.6.7 github.com/tyler-smith/go-bip39 v1.1.0 + golang.org/x/net v0.0.0-20220812174116-3211cb980234 golang.org/x/text v0.3.7 - google.golang.org/genproto v0.0.0-20220725144611-272f38e5d71b + google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c google.golang.org/grpc v1.50.0 google.golang.org/protobuf v1.28.1 gopkg.in/yaml.v2 v2.4.0 @@ -149,7 +150,6 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect diff --git a/go.sum b/go.sum index 8c2e0e96da..147abd53a1 100644 --- a/go.sum +++ b/go.sum @@ -1324,8 +1324,8 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20220725144611-272f38e5d71b h1:SfSkJugek6xm7lWywqth4r2iTrYLpD8lOj1nMIIhMNM= -google.golang.org/genproto v0.0.0-20220725144611-272f38e5d71b/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c h1:IooGDWedfLC6KLczH/uduUsKQP42ZZYhKx+zd50L1Sk= +google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/indexer/kv_indexer.go b/indexer/kv_indexer.go new file mode 100644 index 0000000000..bc8afce4fa --- /dev/null +++ b/indexer/kv_indexer.go @@ -0,0 +1,230 @@ +package indexer + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + "github.com/ethereum/go-ethereum/common" + rpctypes "github.com/evmos/ethermint/rpc/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" + + ethermint "github.com/evmos/ethermint/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" +) + +const ( + KeyPrefixTxHash = 1 + KeyPrefixTxIndex = 2 + + // TxIndexKeyLength is the length of tx-index key + TxIndexKeyLength = 1 + 8 + 8 +) + +var _ ethermint.EVMTxIndexer = &KVIndexer{} + +// KVIndexer implements a eth tx indexer on a KV db. +type KVIndexer struct { + db dbm.DB + logger log.Logger + clientCtx client.Context +} + +// NewKVIndexer creates the KVIndexer +func NewKVIndexer(db dbm.DB, logger log.Logger, clientCtx client.Context) *KVIndexer { + return &KVIndexer{db, logger, clientCtx} +} + +// IndexBlock index all the eth txs in a block through the following steps: +// - Iterates over all of the Txs in Block +// - Parses eth Tx infos from cosmos-sdk events for every TxResult +// - Iterates over all the messages of the Tx +// - Builds and stores a indexer.TxResult based on parsed events for every message +func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.ResponseDeliverTx) error { + height := block.Header.Height + + batch := kv.db.NewBatch() + defer batch.Close() + + // record index of valid eth tx during the iteration + var ethTxIndex int32 + for txIndex, tx := range block.Txs { + result := txResults[txIndex] + if !rpctypes.TxSuccessOrExceedsBlockGasLimit(result) { + continue + } + + tx, err := kv.clientCtx.TxConfig.TxDecoder()(tx) + if err != nil { + kv.logger.Error("Fail to decode tx", "err", err, "block", height, "txIndex", txIndex) + continue + } + + if !isEthTx(tx) { + continue + } + + txs, err := rpctypes.ParseTxResult(result, tx) + if err != nil { + kv.logger.Error("Fail to parse event", "err", err, "block", height, "txIndex", txIndex) + continue + } + + var cumulativeGasUsed uint64 + for msgIndex, msg := range tx.GetMsgs() { + ethMsg := msg.(*evmtypes.MsgEthereumTx) + txHash := common.HexToHash(ethMsg.Hash) + + txResult := ethermint.TxResult{ + Height: height, + TxIndex: uint32(txIndex), + MsgIndex: uint32(msgIndex), + EthTxIndex: ethTxIndex, + } + if result.Code != abci.CodeTypeOK { + // exceeds block gas limit scenario, set gas used to gas limit because that's what's charged by ante handler. + // some old versions don't emit any events, so workaround here directly. + txResult.GasUsed = ethMsg.GetGas() + txResult.Failed = true + } else { + parsedTx := txs.GetTxByMsgIndex(msgIndex) + if parsedTx == nil { + kv.logger.Error("msg index not found in events", "msgIndex", msgIndex) + continue + } + if parsedTx.EthTxIndex >= 0 && parsedTx.EthTxIndex != ethTxIndex { + kv.logger.Error("eth tx index don't match", "expect", ethTxIndex, "found", parsedTx.EthTxIndex) + } + txResult.GasUsed = parsedTx.GasUsed + txResult.Failed = parsedTx.Failed + } + + cumulativeGasUsed += txResult.GasUsed + txResult.CumulativeGasUsed = cumulativeGasUsed + ethTxIndex++ + + if err := saveTxResult(kv.clientCtx.Codec, batch, txHash, &txResult); err != nil { + return sdkerrors.Wrapf(err, "IndexBlock %d", height) + } + } + } + if err := batch.Write(); err != nil { + return sdkerrors.Wrapf(err, "IndexBlock %d, write batch", block.Height) + } + return nil +} + +// LastIndexedBlock returns the latest indexed block number, returns -1 if db is empty +func (kv *KVIndexer) LastIndexedBlock() (int64, error) { + return LoadLastBlock(kv.db) +} + +// FirstIndexedBlock returns the first indexed block number, returns -1 if db is empty +func (kv *KVIndexer) FirstIndexedBlock() (int64, error) { + return LoadFirstBlock(kv.db) +} + +// GetByTxHash finds eth tx by eth tx hash +func (kv *KVIndexer) GetByTxHash(hash common.Hash) (*ethermint.TxResult, error) { + bz, err := kv.db.Get(TxHashKey(hash)) + if err != nil { + return nil, sdkerrors.Wrapf(err, "GetByTxHash %s", hash.Hex()) + } + if len(bz) == 0 { + return nil, fmt.Errorf("tx not found, hash: %s", hash.Hex()) + } + var txKey ethermint.TxResult + if err := kv.clientCtx.Codec.Unmarshal(bz, &txKey); err != nil { + return nil, sdkerrors.Wrapf(err, "GetByTxHash %s", hash.Hex()) + } + return &txKey, nil +} + +// GetByBlockAndIndex finds eth tx by block number and eth tx index +func (kv *KVIndexer) GetByBlockAndIndex(blockNumber int64, txIndex int32) (*ethermint.TxResult, error) { + bz, err := kv.db.Get(TxIndexKey(blockNumber, txIndex)) + if err != nil { + return nil, sdkerrors.Wrapf(err, "GetByBlockAndIndex %d %d", blockNumber, txIndex) + } + if len(bz) == 0 { + return nil, fmt.Errorf("tx not found, block: %d, eth-index: %d", blockNumber, txIndex) + } + return kv.GetByTxHash(common.BytesToHash(bz)) +} + +// TxHashKey returns the key for db entry: `tx hash -> tx result struct` +func TxHashKey(hash common.Hash) []byte { + return append([]byte{KeyPrefixTxHash}, hash.Bytes()...) +} + +// TxIndexKey returns the key for db entry: `(block number, tx index) -> tx hash` +func TxIndexKey(blockNumber int64, txIndex int32) []byte { + bz1 := sdk.Uint64ToBigEndian(uint64(blockNumber)) + bz2 := sdk.Uint64ToBigEndian(uint64(txIndex)) + return append(append([]byte{KeyPrefixTxIndex}, bz1...), bz2...) +} + +// LoadLastBlock returns the latest indexed block number, returns -1 if db is empty +func LoadLastBlock(db dbm.DB) (int64, error) { + it, err := db.ReverseIterator([]byte{KeyPrefixTxIndex}, []byte{KeyPrefixTxIndex + 1}) + if err != nil { + return 0, sdkerrors.Wrap(err, "LoadLastBlock") + } + defer it.Close() + if !it.Valid() { + return -1, nil + } + return parseBlockNumberFromKey(it.Key()) +} + +// LoadFirstBlock loads the first indexed block, returns -1 if db is empty +func LoadFirstBlock(db dbm.DB) (int64, error) { + it, err := db.Iterator([]byte{KeyPrefixTxIndex}, []byte{KeyPrefixTxIndex + 1}) + if err != nil { + return 0, sdkerrors.Wrap(err, "LoadFirstBlock") + } + defer it.Close() + if !it.Valid() { + return -1, nil + } + return parseBlockNumberFromKey(it.Key()) +} + +// isEthTx check if the tx is an eth tx +func isEthTx(tx sdk.Tx) bool { + extTx, ok := tx.(authante.HasExtensionOptionsTx) + if !ok { + return false + } + opts := extTx.GetExtensionOptions() + if len(opts) != 1 || opts[0].GetTypeUrl() != "/ethermint.evm.v1.ExtensionOptionsEthereumTx" { + return false + } + return true +} + +// saveTxResult index the txResult into the kv db batch +func saveTxResult(codec codec.Codec, batch dbm.Batch, txHash common.Hash, txResult *ethermint.TxResult) error { + bz := codec.MustMarshal(txResult) + if err := batch.Set(TxHashKey(txHash), bz); err != nil { + return sdkerrors.Wrap(err, "set tx-hash key") + } + if err := batch.Set(TxIndexKey(txResult.Height, txResult.EthTxIndex), txHash.Bytes()); err != nil { + return sdkerrors.Wrap(err, "set tx-index key") + } + return nil +} + +func parseBlockNumberFromKey(key []byte) (int64, error) { + if len(key) != TxIndexKeyLength { + return 0, fmt.Errorf("wrong tx index key length, expect: %d, got: %d", TxIndexKeyLength, len(key)) + } + + return int64(sdk.BigEndianToUint64(key[1:9])), nil +} diff --git a/indexer/kv_indexer_test.go b/indexer/kv_indexer_test.go new file mode 100644 index 0000000000..95e726723a --- /dev/null +++ b/indexer/kv_indexer_test.go @@ -0,0 +1,189 @@ +package indexer_test + +import ( + "math/big" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/simapp/params" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/evmos/ethermint/app" + "github.com/evmos/ethermint/crypto/ethsecp256k1" + evmenc "github.com/evmos/ethermint/encoding" + "github.com/evmos/ethermint/indexer" + "github.com/evmos/ethermint/tests" + "github.com/evmos/ethermint/x/evm/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + tmlog "github.com/tendermint/tendermint/libs/log" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" +) + +func TestKVIndexer(t *testing.T) { + priv, err := ethsecp256k1.GenerateKey() + require.NoError(t, err) + from := common.BytesToAddress(priv.PubKey().Address().Bytes()) + signer := tests.NewSigner(priv) + ethSigner := ethtypes.LatestSignerForChainID(nil) + + to := common.BigToAddress(big.NewInt(1)) + tx := types.NewTx( + nil, 0, &to, big.NewInt(1000), 21000, nil, nil, nil, nil, nil, + ) + tx.From = from.Hex() + require.NoError(t, tx.Sign(ethSigner, signer)) + txHash := tx.AsTransaction().Hash() + + encodingConfig := MakeEncodingConfig() + clientCtx := client.Context{}.WithTxConfig(encodingConfig.TxConfig).WithCodec(encodingConfig.Marshaler) + + // build cosmos-sdk wrapper tx + tmTx, err := tx.BuildTx(clientCtx.TxConfig.NewTxBuilder(), "aphoton") + require.NoError(t, err) + txBz, err := clientCtx.TxConfig.TxEncoder()(tmTx) + require.NoError(t, err) + + // build an invalid wrapper tx + builder := clientCtx.TxConfig.NewTxBuilder() + require.NoError(t, builder.SetMsgs(tx)) + tmTx2 := builder.GetTx() + txBz2, err := clientCtx.TxConfig.TxEncoder()(tmTx2) + require.NoError(t, err) + + testCases := []struct { + name string + block *tmtypes.Block + blockResult []*abci.ResponseDeliverTx + expSuccess bool + }{ + { + "success, format 1", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + &abci.ResponseDeliverTx{ + Code: 0, + Events: []abci.Event{ + {Type: types.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("0")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + }, + }, + }, + true, + }, + { + "success, format 2", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + &abci.ResponseDeliverTx{ + Code: 0, + Events: []abci.Event{ + {Type: types.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("0")}, + }}, + {Type: types.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + }, + }, + }, + true, + }, + { + "success, exceed block gas limit", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + &abci.ResponseDeliverTx{ + Code: 11, + Log: "out of gas in location: block gas meter; gasWanted: 21000", + Events: []abci.Event{}, + }, + }, + true, + }, + { + "fail, failed eth tx", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + &abci.ResponseDeliverTx{ + Code: 15, + Log: "nonce mismatch", + Events: []abci.Event{}, + }, + }, + false, + }, + { + "fail, invalid events", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + &abci.ResponseDeliverTx{ + Code: 0, + Events: []abci.Event{}, + }, + }, + false, + }, + { + "fail, not eth tx", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz2}}}, + []*abci.ResponseDeliverTx{ + &abci.ResponseDeliverTx{ + Code: 0, + Events: []abci.Event{}, + }, + }, + false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + db := dbm.NewMemDB() + idxer := indexer.NewKVIndexer(db, tmlog.NewNopLogger(), clientCtx) + + err = idxer.IndexBlock(tc.block, tc.blockResult) + require.NoError(t, err) + if !tc.expSuccess { + first, err := idxer.FirstIndexedBlock() + require.NoError(t, err) + require.Equal(t, int64(-1), first) + + last, err := idxer.LastIndexedBlock() + require.NoError(t, err) + require.Equal(t, int64(-1), last) + } else { + first, err := idxer.FirstIndexedBlock() + require.NoError(t, err) + require.Equal(t, tc.block.Header.Height, first) + + last, err := idxer.LastIndexedBlock() + require.NoError(t, err) + require.Equal(t, tc.block.Header.Height, last) + + res1, err := idxer.GetByTxHash(txHash) + require.NoError(t, err) + require.NotNil(t, res1) + res2, err := idxer.GetByBlockAndIndex(1, 0) + require.NoError(t, err) + require.Equal(t, res1, res2) + } + }) + } +} + +// MakeEncodingConfig creates the EncodingConfig +func MakeEncodingConfig() params.EncodingConfig { + return evmenc.MakeConfig(app.ModuleBasics) +} diff --git a/proto/ethermint/types/v1/indexer.proto b/proto/ethermint/types/v1/indexer.proto new file mode 100644 index 0000000000..e1d0be03c9 --- /dev/null +++ b/proto/ethermint/types/v1/indexer.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; +package ethermint.types.v1; + +import "gogoproto/gogo.proto"; + +option go_package = "github.com/evmos/ethermint/types"; + +// TxResult is the value stored in eth tx indexer +message TxResult { + option (gogoproto.goproto_getters) = false; + + // the block height + int64 height = 1; + // cosmos tx index + uint32 tx_index = 2; + // the msg index in a batch tx + uint32 msg_index = 3; + + // eth tx index, the index in the list of valid eth tx in the block, + // aka. the transaction list returned by eth_getBlock api. + int32 eth_tx_index = 4; + // if the eth tx is failed + bool failed = 5; + // gas used by tx, if exceeds block gas limit, + // it's set to gas limit which is what's actually deducted by ante handler. + uint64 gas_used = 6; + // the cumulative gas used within current batch tx + uint64 cumulative_gas_used = 7; +} diff --git a/rpc/apis.go b/rpc/apis.go index eb9f2d5346..e6ca8f1186 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -19,6 +19,7 @@ import ( "github.com/evmos/ethermint/rpc/namespaces/ethereum/personal" "github.com/evmos/ethermint/rpc/namespaces/ethereum/txpool" "github.com/evmos/ethermint/rpc/namespaces/ethereum/web3" + ethermint "github.com/evmos/ethermint/types" rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" ) @@ -48,6 +49,7 @@ type APICreator = func( clientCtx client.Context, tendermintWebsocketClient *rpcclient.WSClient, allowUnprotectedTxs bool, + indexer ethermint.EVMTxIndexer, ) []rpc.API // apiCreators defines the JSON-RPC API namespaces. @@ -55,8 +57,8 @@ var apiCreators map[string]APICreator func init() { apiCreators = map[string]APICreator{ - EthNamespace: func(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) + EthNamespace: func(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) return []rpc.API{ { Namespace: EthNamespace, @@ -72,7 +74,7 @@ func init() { }, } }, - Web3Namespace: func(*server.Context, client.Context, *rpcclient.WSClient, bool) []rpc.API { + Web3Namespace: func(*server.Context, client.Context, *rpcclient.WSClient, bool, ethermint.EVMTxIndexer) []rpc.API { return []rpc.API{ { Namespace: Web3Namespace, @@ -82,7 +84,7 @@ func init() { }, } }, - NetNamespace: func(_ *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, _ bool) []rpc.API { + NetNamespace: func(_ *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, _ bool, _ ethermint.EVMTxIndexer) []rpc.API { return []rpc.API{ { Namespace: NetNamespace, @@ -92,8 +94,8 @@ func init() { }, } }, - PersonalNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) + PersonalNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) return []rpc.API{ { Namespace: PersonalNamespace, @@ -103,7 +105,7 @@ func init() { }, } }, - TxPoolNamespace: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient, _ bool) []rpc.API { + TxPoolNamespace: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient, _ bool, _ ethermint.EVMTxIndexer) []rpc.API { return []rpc.API{ { Namespace: TxPoolNamespace, @@ -113,8 +115,8 @@ func init() { }, } }, - DebugNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) + DebugNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) return []rpc.API{ { Namespace: DebugNamespace, @@ -124,8 +126,8 @@ func init() { }, } }, - MinerNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) + MinerNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) return []rpc.API{ { Namespace: MinerNamespace, @@ -139,12 +141,12 @@ func init() { } // GetRPCAPIs returns the list of all APIs -func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool, selectedAPIs []string) []rpc.API { +func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer, selectedAPIs []string) []rpc.API { var apis []rpc.API for _, ns := range selectedAPIs { if creator, ok := apiCreators[ns]; ok { - apis = append(apis, creator(ctx, clientCtx, tmWSClient, allowUnprotectedTxs)...) + apis = append(apis, creator(ctx, clientCtx, tmWSClient, allowUnprotectedTxs, indexer)...) } else { ctx.Logger.Error("invalid namespace value", "namespace", ns) } diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 392de949c6..d4c139674c 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -103,8 +103,8 @@ type EVMBackend interface { // Tx Info GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransaction, error) - GetTxByEthHash(txHash common.Hash) (*tmrpctypes.ResultTx, error) - GetTxByTxIndex(height int64, txIndex uint) (*tmrpctypes.ResultTx, error) + GetTxByEthHash(txHash common.Hash) (*ethermint.TxResult, error) + GetTxByTxIndex(height int64, txIndex uint) (*ethermint.TxResult, error) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) @@ -143,10 +143,17 @@ type Backend struct { chainID *big.Int cfg config.Config allowUnprotectedTxs bool + indexer ethermint.EVMTxIndexer } // NewBackend creates a new Backend instance for cosmos and ethereum namespaces -func NewBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context, allowUnprotectedTxs bool) *Backend { +func NewBackend( + ctx *server.Context, + logger log.Logger, + clientCtx client.Context, + allowUnprotectedTxs bool, + indexer ethermint.EVMTxIndexer, +) *Backend { chainID, err := ethermint.ParseChainID(clientCtx.ChainID) if err != nil { panic(err) @@ -181,5 +188,6 @@ func NewBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context chainID: chainID, cfg: appConf, allowUnprotectedTxs: allowUnprotectedTxs, + indexer: indexer, } } diff --git a/rpc/backend/backend_suite_test.go b/rpc/backend/backend_suite_test.go index c977f7176c..6a5b6279c3 100644 --- a/rpc/backend/backend_suite_test.go +++ b/rpc/backend/backend_suite_test.go @@ -8,6 +8,8 @@ import ( "path/filepath" "testing" + dbm "github.com/tendermint/tm-db" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/server" @@ -20,6 +22,7 @@ import ( "github.com/evmos/ethermint/app" "github.com/evmos/ethermint/crypto/hd" "github.com/evmos/ethermint/encoding" + "github.com/evmos/ethermint/indexer" "github.com/evmos/ethermint/rpc/backend/mocks" rpctypes "github.com/evmos/ethermint/rpc/types" evmtypes "github.com/evmos/ethermint/x/evm/types" @@ -56,7 +59,9 @@ func (suite *BackendTestSuite) SetupTest() { allowUnprotectedTxs := false - suite.backend = NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) + idxer := indexer.NewKVIndexer(dbm.NewMemDB(), ctx.Logger, clientCtx) + + suite.backend = NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, idxer) suite.backend.queryClient.QueryClient = mocks.NewQueryClient(suite.T()) suite.backend.clientCtx.Client = mocks.NewClient(suite.T()) suite.backend.ctx = rpctypes.ContextWithHeight(1) diff --git a/rpc/backend/blocks_info.go b/rpc/backend/blocks_info.go index ca97153b9b..653725a96d 100644 --- a/rpc/backend/blocks_info.go +++ b/rpc/backend/blocks_info.go @@ -297,7 +297,7 @@ func (b *Backend) GetEthereumMsgsFromTendermintBlock( // Check if tx exists on EVM by cross checking with blockResults: // - Include unsuccessful tx that exceeds block gas limit // - Exclude unsuccessful tx with any other error but ExceedBlockGasLimit - if !TxSuccessOrExceedsBlockGasLimit(txResults[i]) { + if !rpctypes.TxSuccessOrExceedsBlockGasLimit(txResults[i]) { b.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) continue } diff --git a/rpc/backend/evm_backend_test.go b/rpc/backend/evm_backend_test.go index 82f40ddf53..b4cae1f389 100644 --- a/rpc/backend/evm_backend_test.go +++ b/rpc/backend/evm_backend_test.go @@ -949,7 +949,7 @@ func (suite *BackendTestSuite) TestGetEthereumMsgsFromTendermintBlock() { TxsResults: []*types.ResponseDeliverTx{ { Code: 1, - Log: ExceedBlockGasLimitError, + Log: ethrpc.ExceedBlockGasLimitError, }, }, }, @@ -964,7 +964,7 @@ func (suite *BackendTestSuite) TestGetEthereumMsgsFromTendermintBlock() { TxsResults: []*types.ResponseDeliverTx{ { Code: 0, - Log: ExceedBlockGasLimitError, + Log: ethrpc.ExceedBlockGasLimitError, }, }, }, diff --git a/rpc/backend/tracing.go b/rpc/backend/tracing.go index 6c54d5124d..37fa2003f2 100644 --- a/rpc/backend/tracing.go +++ b/rpc/backend/tracing.go @@ -32,23 +32,14 @@ func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfi return nil, err } - parsedTxs, err := rpctypes.ParseTxResult(&transaction.TxResult) - if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %s", hash.Hex()) - } - parsedTx := parsedTxs.GetTxByHash(hash) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hash.Hex()) - } - // check tx index is not out of bound - if uint32(len(blk.Block.Txs)) < transaction.Index { - b.logger.Debug("tx index out of bounds", "index", transaction.Index, "hash", hash.String(), "height", blk.Block.Height) + if uint32(len(blk.Block.Txs)) < transaction.TxIndex { + b.logger.Debug("tx index out of bounds", "index", transaction.TxIndex, "hash", hash.String(), "height", blk.Block.Height) return nil, fmt.Errorf("transaction not included in block %v", blk.Block.Height) } var predecessors []*evmtypes.MsgEthereumTx - for _, txBz := range blk.Block.Txs[:transaction.Index] { + for _, txBz := range blk.Block.Txs[:transaction.TxIndex] { tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) if err != nil { b.logger.Debug("failed to decode transaction in block", "height", blk.Block.Height, "error", err.Error()) @@ -64,14 +55,14 @@ func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfi } } - tx, err := b.clientCtx.TxConfig.TxDecoder()(transaction.Tx) + tx, err := b.clientCtx.TxConfig.TxDecoder()(blk.Block.Txs[transaction.TxIndex]) if err != nil { b.logger.Debug("tx not found", "hash", hash) return nil, err } // add predecessor messages in current cosmos tx - for i := 0; i < parsedTx.MsgIndex; i++ { + for i := 0; i < int(transaction.MsgIndex); i++ { ethMsg, ok := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx) if !ok { continue @@ -79,7 +70,7 @@ func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfi predecessors = append(predecessors, ethMsg) } - ethMessage, ok := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) + ethMessage, ok := tx.GetMsgs()[transaction.MsgIndex].(*evmtypes.MsgEthereumTx) if !ok { b.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx)) return nil, fmt.Errorf("invalid transaction type %T", tx) diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index f3afa1d174..80f464370d 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -3,11 +3,14 @@ package backend import ( "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "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" rpctypes "github.com/evmos/ethermint/rpc/types" + ethermint "github.com/evmos/ethermint/types" evmtypes "github.com/evmos/ethermint/x/evm/types" "github.com/pkg/errors" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" @@ -19,87 +22,43 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransac hexTx := txHash.Hex() if err != nil { - // try to find tx in mempool - txs, err := b.PendingTransactions() - if err != nil { - b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) - return nil, nil - } - - for _, tx := range txs { - msg, err := evmtypes.UnwrapEthereumMsg(tx, txHash) - if err != nil { - // not ethereum tx - continue - } - - if msg.Hash == hexTx { - rpctx, err := rpctypes.NewTransactionFromMsg( - msg, - common.Hash{}, - uint64(0), - uint64(0), - nil, - ) - if err != nil { - return nil, err - } - return rpctx, nil - } - } - - b.logger.Debug("tx not found", "hash", hexTx) - return nil, nil + return b.getTransactionByHashPending(txHash) } - if !TxSuccessOrExceedsBlockGasLimit(&res.TxResult) { - return nil, errors.New("invalid ethereum tx") - } - - parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) + block, err := b.GetTendermintBlockByNumber(rpctypes.BlockNumber(res.Height)) if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %s", hexTx) - } - - parsedTx := parsedTxs.GetTxByHash(txHash) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) + return nil, err } - tx, err := b.clientCtx.TxConfig.TxDecoder()(res.Tx) + tx, err := b.clientCtx.TxConfig.TxDecoder()(block.Block.Txs[res.TxIndex]) if err != nil { return nil, err } - // the `msgIndex` is inferred from tx events, should be within the bound. - msg, ok := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) + // the `res.MsgIndex` is inferred from tx index, should be within the bound. + msg, ok := tx.GetMsgs()[res.MsgIndex].(*evmtypes.MsgEthereumTx) if !ok { return nil, errors.New("invalid ethereum tx") } - block, err := b.clientCtx.Client.Block(b.ctx, &res.Height) - if err != nil { - b.logger.Debug("block not found", "height", res.Height, "error", err.Error()) - return nil, err - } - blockRes, err := b.GetTendermintBlockResultByNumber(&block.Block.Height) if err != nil { b.logger.Debug("block result not found", "height", block.Block.Height, "error", err.Error()) return nil, nil } - if parsedTx.EthTxIndex == -1 { + if res.EthTxIndex == -1 { // Fallback to find tx index by iterating all valid eth transactions msgs := b.GetEthereumMsgsFromTendermintBlock(block, blockRes) for i := range msgs { if msgs[i].Hash == hexTx { - parsedTx.EthTxIndex = int64(i) + res.EthTxIndex = int32(i) break } } } - if parsedTx.EthTxIndex == -1 { + // if we still unable to find the eth tx index, return error, shouldn't happen. + if res.EthTxIndex == -1 { return nil, errors.New("can't find index of ethereum tx") } @@ -113,11 +72,48 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransac msg, common.BytesToHash(block.BlockID.Hash.Bytes()), uint64(res.Height), - uint64(parsedTx.EthTxIndex), + uint64(res.EthTxIndex), baseFee, ) } +// getTransactionByHashPending find pending tx from mempool +func (b *Backend) getTransactionByHashPending(txHash common.Hash) (*rpctypes.RPCTransaction, error) { + hexTx := txHash.Hex() + // try to find tx in mempool + txs, err := b.PendingTransactions() + if err != nil { + b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) + return nil, nil + } + + for _, tx := range txs { + msg, err := evmtypes.UnwrapEthereumMsg(tx, txHash) + if err != nil { + // not ethereum tx + continue + } + + if msg.Hash == hexTx { + // use zero block values since it's not included in a block yet + rpctx, err := rpctypes.NewTransactionFromMsg( + msg, + common.Hash{}, + uint64(0), + uint64(0), + nil, + ) + if err != nil { + return nil, err + } + return rpctx, nil + } + } + + b.logger.Debug("tx not found", "hash", hexTx) + return nil, nil +} + // GetTransactionReceipt returns the transaction receipt identified by hash. func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { hexTx := hash.Hex() @@ -129,44 +125,17 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ return nil, nil } - // don't ignore the txs which exceed block gas limit. - if !TxSuccessOrExceedsBlockGasLimit(&res.TxResult) { - return nil, nil - } - - parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) - if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %s, %v", hexTx, err) - } - - parsedTx := parsedTxs.GetTxByHash(hash) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) - } - - resBlock, err := b.clientCtx.Client.Block(b.ctx, &res.Height) + resBlock, err := b.GetTendermintBlockByNumber(rpctypes.BlockNumber(res.Height)) if err != nil { b.logger.Debug("block not found", "height", res.Height, "error", err.Error()) return nil, nil } - - tx, err := b.clientCtx.TxConfig.TxDecoder()(res.Tx) + tx, err := b.clientCtx.TxConfig.TxDecoder()(resBlock.Block.Txs[res.TxIndex]) if err != nil { b.logger.Debug("decoding failed", "error", err.Error()) return nil, fmt.Errorf("failed to decode tx: %w", err) } - - if res.TxResult.Code != 0 { - // tx failed, we should return gas limit as gas used, because that's how the fee get deducted. - for i := 0; i <= parsedTx.MsgIndex; i++ { - gasLimit := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx).GetGas() - parsedTxs.Txs[i].GasUsed = gasLimit - } - } - - // the `msgIndex` is inferred from tx events, should be within the bound, - // and the tx is found by eth tx hash, so the msg type must be correct. - ethMsg := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) + ethMsg := tx.GetMsgs()[res.MsgIndex].(*evmtypes.MsgEthereumTx) txData, err := evmtypes.UnpackTxData(ethMsg.Data) if err != nil { @@ -180,42 +149,46 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ b.logger.Debug("failed to retrieve block results", "height", res.Height, "error", err.Error()) return nil, nil } - for i := 0; i < int(res.Index) && i < len(blockRes.TxsResults); i++ { - cumulativeGasUsed += uint64(blockRes.TxsResults[i].GasUsed) + for _, txResult := range blockRes.TxsResults[0:res.TxIndex] { + cumulativeGasUsed += uint64(txResult.GasUsed) } - cumulativeGasUsed += parsedTxs.AccumulativeGasUsed(parsedTx.MsgIndex) + cumulativeGasUsed += res.CumulativeGasUsed - // Get the transaction result from the log var status hexutil.Uint - if res.TxResult.Code != 0 || parsedTx.Failed { + if res.Failed { status = hexutil.Uint(ethtypes.ReceiptStatusFailed) } else { status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) } - from, err := ethMsg.GetSender(b.chainID) + chainID, err := b.ChainID() + if err != nil { + return nil, err + } + + from, err := ethMsg.GetSender(chainID.ToInt()) if err != nil { return nil, err } // parse tx logs from events - logs, err := parsedTx.ParseTxLogs() + logs, err := TxLogsFromEvents(blockRes.TxsResults[res.TxIndex].Events, int(res.MsgIndex)) if err != nil { b.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error()) } - if parsedTx.EthTxIndex == -1 { + if res.EthTxIndex == -1 { // Fallback to find tx index by iterating all valid eth transactions msgs := b.GetEthereumMsgsFromTendermintBlock(resBlock, blockRes) for i := range msgs { if msgs[i].Hash == hexTx { - parsedTx.EthTxIndex = int64(i) + res.EthTxIndex = int32(i) break } } } - - if parsedTx.EthTxIndex == -1 { + // return error if still unable to find the eth tx index + if res.EthTxIndex == -1 { return nil, errors.New("can't find index of ethereum tx") } @@ -230,13 +203,13 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ // They are stored in the chain database. "transactionHash": hash, "contractAddress": nil, - "gasUsed": hexutil.Uint64(parsedTx.GasUsed), + "gasUsed": hexutil.Uint64(res.GasUsed), // Inclusion information: These fields provide information about the inclusion of the // transaction corresponding to this receipt. "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), "blockNumber": hexutil.Uint64(res.Height), - "transactionIndex": hexutil.Uint64(parsedTx.EthTxIndex), + "transactionIndex": hexutil.Uint64(res.EthTxIndex), // sender and receiver (contract or EOA) addreses "from": from, @@ -304,32 +277,66 @@ func (b *Backend) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNum // GetTxByEthHash uses `/tx_query` to find transaction by ethereum tx hash // TODO: Don't need to convert once hashing is fixed on Tendermint // https://github.com/tendermint/tendermint/issues/6539 -func (b *Backend) GetTxByEthHash(hash common.Hash) (*tmrpctypes.ResultTx, error) { +func (b *Backend) GetTxByEthHash(hash common.Hash) (*ethermint.TxResult, error) { + if b.indexer != nil { + return b.indexer.GetByTxHash(hash) + } + + // fallback to tendermint tx indexer query := fmt.Sprintf("%s.%s='%s'", evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyEthereumTxHash, hash.Hex()) - resTxs, err := b.clientCtx.Client.TxSearch(b.ctx, query, false, nil, nil, "") + txResult, err := b.queryTendermintTxIndexer(query, func(txs *rpctypes.ParsedTxs) *rpctypes.ParsedTx { + return txs.GetTxByHash(hash) + }) if err != nil { - return nil, err + return nil, sdkerrors.Wrapf(err, "GetTxByEthHash %s", hash.Hex()) } - if len(resTxs.Txs) == 0 { - return nil, errors.Errorf("ethereum tx not found for hash %s", hash.Hex()) - } - return resTxs.Txs[0], nil + return txResult, nil } // GetTxByTxIndex uses `/tx_query` to find transaction by tx index of valid ethereum txs -func (b *Backend) GetTxByTxIndex(height int64, index uint) (*tmrpctypes.ResultTx, error) { +func (b *Backend) GetTxByTxIndex(height int64, index uint) (*ethermint.TxResult, error) { + if b.indexer != nil { + return b.indexer.GetByBlockAndIndex(height, int32(index)) + } + + // fallback to tendermint tx indexer query := fmt.Sprintf("tx.height=%d AND %s.%s=%d", height, evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyTxIndex, index, ) + txResult, err := b.queryTendermintTxIndexer(query, func(txs *rpctypes.ParsedTxs) *rpctypes.ParsedTx { + return txs.GetTxByTxIndex(int(index)) + }) + if err != nil { + return nil, sdkerrors.Wrapf(err, "GetTxByTxIndex %d %d", height, index) + } + return txResult, nil +} + +// queryTendermintTxIndexer query tx in tendermint tx indexer +func (b *Backend) queryTendermintTxIndexer(query string, txGetter func(*rpctypes.ParsedTxs) *rpctypes.ParsedTx) (*ethermint.TxResult, error) { resTxs, err := b.clientCtx.Client.TxSearch(b.ctx, query, false, nil, nil, "") if err != nil { return nil, err } if len(resTxs.Txs) == 0 { - return nil, errors.Errorf("ethereum tx not found for block %d index %d", height, index) + return nil, errors.New("ethereum tx not found") + } + txResult := resTxs.Txs[0] + if !rpctypes.TxSuccessOrExceedsBlockGasLimit(&txResult.TxResult) { + return nil, errors.New("invalid ethereum tx") } - return resTxs.Txs[0], nil + + var tx sdk.Tx + if txResult.TxResult.Code != 0 { + // it's only needed when the tx exceeds block gas limit + tx, err = b.clientCtx.TxConfig.TxDecoder()(txResult.Tx) + if err != nil { + return nil, fmt.Errorf("invalid ethereum tx") + } + } + + return rpctypes.ParseTxIndexerResult(txResult, tx, txGetter) } // getTransactionByBlockAndIndex is the common code shared by `GetTransactionByBlockNumberAndIndex` and `GetTransactionByBlockHashAndIndex`. @@ -340,28 +347,18 @@ func (b *Backend) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, i } var msg *evmtypes.MsgEthereumTx - // try /tx_search first + // find in tx indexer res, err := b.GetTxByTxIndex(block.Block.Height, uint(idx)) if err == nil { - tx, err := b.clientCtx.TxConfig.TxDecoder()(res.Tx) + tx, err := b.clientCtx.TxConfig.TxDecoder()(block.Block.Txs[res.TxIndex]) if err != nil { b.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) return nil, nil } - parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) - if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %d, %v", idx, err) - } - - parsedTx := parsedTxs.GetTxByTxIndex(int(idx)) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %d", idx) - } - var ok bool // msgIndex is inferred from tx events, should be within bound. - msg, ok = tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) + msg, ok = tx.GetMsgs()[res.MsgIndex].(*evmtypes.MsgEthereumTx) if !ok { b.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) return nil, nil diff --git a/rpc/backend/utils.go b/rpc/backend/utils.go index fa18e4d8e4..37b12a83d9 100644 --- a/rpc/backend/utils.go +++ b/rpc/backend/utils.go @@ -23,10 +23,6 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" ) -// ExceedBlockGasLimitError defines the error message when tx execution exceeds the block gas limit. -// The tx fee is deducted in ante handler, so it shouldn't be ignored in JSON-RPC API. -const ExceedBlockGasLimitError = "out of gas in location: block gas meter; gasWanted:" - type txGasAndReward struct { gasUsed uint64 reward *big.Int @@ -257,17 +253,6 @@ func ParseTxLogsFromEvent(event abci.Event) ([]*ethtypes.Log, error) { return evmtypes.LogsToEthereum(logs), nil } -// TxExceedBlockGasLimit returns true if the tx exceeds block gas limit. -func TxExceedBlockGasLimit(res *abci.ResponseDeliverTx) bool { - return strings.Contains(res.Log, ExceedBlockGasLimitError) -} - -// TxSuccessOrExceedsBlockGasLimit returnsrue if the transaction was successful -// or if it failed with an ExceedBlockGasLimit error -func TxSuccessOrExceedsBlockGasLimit(res *abci.ResponseDeliverTx) bool { - return res.Code == 0 || TxExceedBlockGasLimit(res) -} - // ShouldIgnoreGasUsed returns true if the gasUsed in result should be ignored // workaround for issue: https://github.com/cosmos/cosmos-sdk/issues/10832 func ShouldIgnoreGasUsed(res *abci.ResponseDeliverTx) bool { diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index ff25eb0601..54c4a37b89 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -3,7 +3,6 @@ package eth import ( "context" "errors" - "fmt" "math/big" "github.com/ethereum/go-ethereum/signer/core/apitypes" @@ -465,23 +464,19 @@ func (e *PublicAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, err return nil, nil } - if res.TxResult.Code != 0 { + if res.Failed { // failed, return empty logs return nil, nil } - parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) + resBlockResult, err := e.backend.GetTendermintBlockResultByNumber(&res.Height) if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %s, %v", hexTx, err) - } - - parsedTx := parsedTxs.GetTxByHash(txHash) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) + e.logger.Debug("block result not found", "number", res.Height, "error", err.Error()) + return nil, nil } // parse tx logs from events - return parsedTx.ParseTxLogs() + return backend.TxLogsFromEvents(resBlockResult.TxsResults[res.TxIndex].Events, int(res.MsgIndex)) } // SignTypedData signs EIP-712 conformant typed data diff --git a/rpc/types/events.go b/rpc/types/events.go index 6b74d3784a..e48fc39aa8 100644 --- a/rpc/types/events.go +++ b/rpc/types/events.go @@ -1,13 +1,15 @@ package types import ( - "encoding/json" + "fmt" "strconv" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" + ethermint "github.com/evmos/ethermint/types" evmtypes "github.com/evmos/ethermint/x/evm/types" abci "github.com/tendermint/tendermint/abci/types" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" ) // EventFormat is the format version of the events. @@ -52,11 +54,9 @@ type ParsedTx struct { Hash common.Hash // -1 means uninitialized - EthTxIndex int64 + EthTxIndex int32 GasUsed uint64 Failed bool - // unparsed tx log json strings - RawLogs [][]byte } // NewParsedTx initialize a ParsedTx @@ -64,20 +64,6 @@ func NewParsedTx(msgIndex int) ParsedTx { return ParsedTx{MsgIndex: msgIndex, EthTxIndex: -1} } -// ParseTxLogs decode the raw logs into ethereum format. -func (p ParsedTx) ParseTxLogs() ([]*ethtypes.Log, error) { - logs := make([]*evmtypes.Log, 0, len(p.RawLogs)) - for _, raw := range p.RawLogs { - var log evmtypes.Log - if err := json.Unmarshal(raw, &log); err != nil { - return nil, err - } - - logs = append(logs, &log) - } - return evmtypes.LogsToEthereum(logs), nil -} - // ParsedTxs is the tx infos parsed from eth tx events. type ParsedTxs struct { // one item per message @@ -88,7 +74,7 @@ type ParsedTxs struct { // ParseTxResult parse eth tx infos from cosmos-sdk events. // It supports two event formats, the formats are described in the comments of the format constants. -func ParseTxResult(result *abci.ResponseDeliverTx) (*ParsedTxs, error) { +func ParseTxResult(result *abci.ResponseDeliverTx, tx sdk.Tx) (*ParsedTxs, error) { format := eventFormatUnknown // the index of current ethereum_tx event in format 1 or the second part of format 2 eventIndex := -1 @@ -97,40 +83,38 @@ func ParseTxResult(result *abci.ResponseDeliverTx) (*ParsedTxs, error) { TxHashes: make(map[common.Hash]int), } for _, event := range result.Events { - switch event.Type { - case evmtypes.EventTypeEthereumTx: - if format == eventFormatUnknown { - // discover the format version by inspect the first ethereum_tx event. - if len(event.Attributes) > 2 { - format = eventFormat1 - } else { - format = eventFormat2 - } + if event.Type != evmtypes.EventTypeEthereumTx { + continue + } + + if format == eventFormatUnknown { + // discover the format version by inspect the first ethereum_tx event. + if len(event.Attributes) > 2 { + format = eventFormat1 + } else { + format = eventFormat2 } + } - if len(event.Attributes) == 2 { - // the first part of format 2 + if len(event.Attributes) == 2 { + // the first part of format 2 + if err := p.newTx(event.Attributes); err != nil { + return nil, err + } + } else { + // format 1 or second part of format 2 + eventIndex++ + if format == eventFormat1 { + // append tx if err := p.newTx(event.Attributes); err != nil { return nil, err } } else { - // format 1 or second part of format 2 - eventIndex++ - if format == eventFormat1 { - // append tx - if err := p.newTx(event.Attributes); err != nil { - return nil, err - } - } else { - // the second part of format 2, update tx fields - if err := p.updateTx(eventIndex, event.Attributes); err != nil { - return nil, err - } + // the second part of format 2, update tx fields + if err := p.updateTx(eventIndex, event.Attributes); err != nil { + return nil, err } } - case evmtypes.EventTypeTxLog: - // reuse the eventIndex set by previous ethereum_tx event - p.Txs[eventIndex].RawLogs = parseRawLogs(event.Attributes) } } @@ -139,9 +123,42 @@ func ParseTxResult(result *abci.ResponseDeliverTx) (*ParsedTxs, error) { p.Txs[0].GasUsed = uint64(result.GasUsed) } + // this could only happen if tx exceeds block gas limit + if result.Code != 0 && tx != nil { + for i := 0; i < len(p.Txs); i++ { + p.Txs[i].Failed = true + + // replace gasUsed with gasLimit because that's what's actually deducted. + gasLimit := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx).GetGas() + p.Txs[i].GasUsed = gasLimit + } + } return p, nil } +// ParseTxIndexerResult parse tm tx result to a format compatible with the custom tx indexer. +func ParseTxIndexerResult(txResult *tmrpctypes.ResultTx, tx sdk.Tx, getter func(*ParsedTxs) *ParsedTx) (*ethermint.TxResult, error) { + txs, err := ParseTxResult(&txResult.TxResult, tx) + if err != nil { + return nil, fmt.Errorf("failed to parse tx events: block %d, index %d, %v", txResult.Height, txResult.Index, err) + } + + parsedTx := getter(txs) + if parsedTx == nil { + return nil, fmt.Errorf("ethereum tx not found in msgs: block %d, index %d", txResult.Height, txResult.Index) + } + + return ðermint.TxResult{ + Height: txResult.Height, + TxIndex: txResult.Index, + MsgIndex: uint32(parsedTx.MsgIndex), + EthTxIndex: parsedTx.EthTxIndex, + Failed: parsedTx.Failed, + GasUsed: parsedTx.GasUsed, + CumulativeGasUsed: txs.AccumulativeGasUsed(parsedTx.MsgIndex), + }, nil +} + // newTx parse a new tx from events, called during parsing. func (p *ParsedTxs) newTx(attrs []abci.EventAttribute) error { msgIndex := len(p.Txs) @@ -215,17 +232,17 @@ func fillTxAttribute(tx *ParsedTx, key []byte, value []byte) error { case evmtypes.AttributeKeyEthereumTxHash: tx.Hash = common.HexToHash(string(value)) case evmtypes.AttributeKeyTxIndex: - txIndex, err := strconv.ParseInt(string(value), 10, 64) + txIndex, err := strconv.ParseUint(string(value), 10, 31) if err != nil { return err } - tx.EthTxIndex = txIndex + tx.EthTxIndex = int32(txIndex) case evmtypes.AttributeKeyTxGasUsed: - gasUsed, err := strconv.ParseInt(string(value), 10, 64) + gasUsed, err := strconv.ParseUint(string(value), 10, 64) if err != nil { return err } - tx.GasUsed = uint64(gasUsed) + tx.GasUsed = gasUsed case evmtypes.AttributeKeyEthereumTxFailed: tx.Failed = len(value) > 0 } @@ -240,10 +257,3 @@ func fillTxAttributes(tx *ParsedTx, attrs []abci.EventAttribute) error { } return nil } - -func parseRawLogs(attrs []abci.EventAttribute) (logs [][]byte) { - for _, attr := range attrs { - logs = append(logs, attr.Value) - } - return logs -} diff --git a/rpc/types/events_test.go b/rpc/types/events_test.go index 68e097d38c..833084e054 100644 --- a/rpc/types/events_test.go +++ b/rpc/types/events_test.go @@ -11,11 +11,6 @@ import ( ) func TestParseTxResult(t *testing.T) { - rawLogs := [][]byte{ - []byte("{\"address\":\"0xdcC261c03cD2f33eBea404318Cdc1D9f8b78e1AD\",\"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",\"0x000000000000000000000000569608516a81c0b1247310a3e0cd001046da0663\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANB/GezJGOI=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":5}"), - []byte("{\"address\":\"0x569608516A81C0B1247310A3E0CD001046dA0663\",\"topics\":[\"0xe2403640ba68fed3a2f88b7557551d1993f84b99bb10ff833f0cf8db0c5e0486\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANB/GezJGOI=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":6}"), - []byte("{\"address\":\"0x569608516A81C0B1247310A3E0CD001046dA0663\",\"topics\":[\"0xf279e6a1f5e320cca91135676d9cb6e44ca8a08c0b88342bcdb1144f6511b568\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\",\"0x0000000000000000000000000000000000000000000000000000000000000001\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":7}"), - } address := "0x57f96e6B86CdeFdB3d412547816a82E3E0EbF9D2" txHash := common.BigToHash(big.NewInt(1)) txHash2 := common.BigToHash(big.NewInt(2)) @@ -46,11 +41,6 @@ func TestParseTxResult(t *testing.T) { {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, }}, - {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, - }}, {Type: "message", Attributes: []abci.EventAttribute{ {Key: []byte("action"), Value: []byte("/ethermint.evm.v1.MsgEthereumTx")}, {Key: []byte("key"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, @@ -76,7 +66,6 @@ func TestParseTxResult(t *testing.T) { EthTxIndex: 10, GasUsed: 21000, Failed: false, - RawLogs: rawLogs, }, { MsgIndex: 1, @@ -84,7 +73,6 @@ func TestParseTxResult(t *testing.T) { EthTxIndex: 11, GasUsed: 21000, Failed: true, - RawLogs: nil, }, }, }, @@ -113,11 +101,6 @@ func TestParseTxResult(t *testing.T) { {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, }}, - {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, - }}, {Type: "message", Attributes: []abci.EventAttribute{ {Key: []byte("action"), Value: []byte("/ethermint.evm.v1.MsgEthereumTx")}, {Key: []byte("key"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, @@ -133,7 +116,6 @@ func TestParseTxResult(t *testing.T) { EthTxIndex: 0, GasUsed: 21000, Failed: false, - RawLogs: rawLogs, }, }, }, @@ -150,11 +132,6 @@ func TestParseTxResult(t *testing.T) { {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, }}, - {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, - }}, {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ {Key: []byte("ethereumTxHash"), Value: []byte(txHash2.Hex())}, {Key: []byte("txIndex"), Value: []byte("0x01")}, @@ -182,11 +159,6 @@ func TestParseTxResult(t *testing.T) { {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, }}, - {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, - }}, {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ {Key: []byte("ethereumTxHash"), Value: []byte(txHash2.Hex())}, {Key: []byte("txIndex"), Value: []byte("10")}, @@ -243,7 +215,7 @@ func TestParseTxResult(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - parsed, err := ParseTxResult(&tc.response) + parsed, err := ParseTxResult(&tc.response, nil) if tc.expTxs == nil { require.Error(t, err) } else { @@ -252,8 +224,6 @@ func TestParseTxResult(t *testing.T) { require.Equal(t, expTx, parsed.GetTxByMsgIndex(msgIndex)) require.Equal(t, expTx, parsed.GetTxByHash(expTx.Hash)) require.Equal(t, expTx, parsed.GetTxByTxIndex(int(expTx.EthTxIndex))) - _, err := expTx.ParseTxLogs() - require.NoError(t, err) } // non-exists tx hash require.Nil(t, parsed.GetTxByHash(common.Hash{})) @@ -264,66 +234,3 @@ func TestParseTxResult(t *testing.T) { }) } } - -func TestParseTxLogs(t *testing.T) { - rawLogs := [][]byte{ - []byte("{\"address\":\"0xdcC261c03cD2f33eBea404318Cdc1D9f8b78e1AD\",\"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",\"0x000000000000000000000000569608516a81c0b1247310a3e0cd001046da0663\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANB/GezJGOI=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":5}"), - []byte("{\"address\":\"0x569608516A81C0B1247310A3E0CD001046dA0663\",\"topics\":[\"0xe2403640ba68fed3a2f88b7557551d1993f84b99bb10ff833f0cf8db0c5e0486\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANB/GezJGOI=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":6}"), - []byte("{\"address\":\"0x569608516A81C0B1247310A3E0CD001046dA0663\",\"topics\":[\"0xf279e6a1f5e320cca91135676d9cb6e44ca8a08c0b88342bcdb1144f6511b568\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\",\"0x0000000000000000000000000000000000000000000000000000000000000001\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":7}"), - } - address := "0x57f96e6B86CdeFdB3d412547816a82E3E0EbF9D2" - txHash := common.BigToHash(big.NewInt(1)) - txHash2 := common.BigToHash(big.NewInt(2)) - response := abci.ResponseDeliverTx{ - GasUsed: 21000, - Events: []abci.Event{ - {Type: "coin_received", Attributes: []abci.EventAttribute{ - {Key: []byte("receiver"), Value: []byte("ethm12luku6uxehhak02py4rcz65zu0swh7wjun6msa")}, - {Key: []byte("amount"), Value: []byte("1252860basetcro")}, - }}, - {Type: "coin_spent", Attributes: []abci.EventAttribute{ - {Key: []byte("spender"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, - {Key: []byte("amount"), Value: []byte("1252860basetcro")}, - }}, - {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, - {Key: []byte("txIndex"), Value: []byte("10")}, - {Key: []byte("amount"), Value: []byte("1000")}, - {Key: []byte("txGasUsed"), Value: []byte("21000")}, - {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, - {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, - }}, - {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, - {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, - }}, - {Type: "message", Attributes: []abci.EventAttribute{ - {Key: []byte("action"), Value: []byte("/ethermint.evm.v1.MsgEthereumTx")}, - {Key: []byte("key"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, - {Key: []byte("module"), Value: []byte("evm")}, - {Key: []byte("sender"), Value: []byte(address)}, - }}, - {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: []byte("ethereumTxHash"), Value: []byte(txHash2.Hex())}, - {Key: []byte("txIndex"), Value: []byte("11")}, - {Key: []byte("amount"), Value: []byte("1000")}, - {Key: []byte("txGasUsed"), Value: []byte("21000")}, - {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, - {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, - {Key: []byte("ethereumTxFailed"), Value: []byte("contract reverted")}, - }}, - {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{}}, - }, - } - parsed, err := ParseTxResult(&response) - require.NoError(t, err) - tx1 := parsed.GetTxByMsgIndex(0) - txLogs1, err := tx1.ParseTxLogs() - require.NoError(t, err) - require.NotEmpty(t, txLogs1) - - tx2 := parsed.GetTxByMsgIndex(1) - txLogs2, err := tx2.ParseTxLogs() - require.Empty(t, txLogs2) -} diff --git a/rpc/types/utils.go b/rpc/types/utils.go index 7e0aebd938..5843ed24ef 100644 --- a/rpc/types/utils.go +++ b/rpc/types/utils.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "math/big" + "strings" abci "github.com/tendermint/tendermint/abci/types" tmtypes "github.com/tendermint/tendermint/types" @@ -22,6 +23,10 @@ import ( "github.com/ethereum/go-ethereum/params" ) +// ExceedBlockGasLimitError defines the error message when tx execution exceeds the block gas limit. +// The tx fee is deducted in ante handler, so it shouldn't be ignored in JSON-RPC API. +const ExceedBlockGasLimitError = "out of gas in location: block gas meter; gasWanted:" + // RawTxToEthTx returns a evm MsgEthereum transaction from raw tx bytes. func RawTxToEthTx(clientCtx client.Context, txBz tmtypes.Tx) ([]*evmtypes.MsgEthereumTx, error) { tx, err := clientCtx.TxConfig.TxDecoder()(txBz) @@ -243,3 +248,14 @@ func CheckTxFee(gasPrice *big.Int, gas uint64, cap float64) error { } return nil } + +// TxExceedBlockGasLimit returns true if the tx exceeds block gas limit. +func TxExceedBlockGasLimit(res *abci.ResponseDeliverTx) bool { + return strings.Contains(res.Log, ExceedBlockGasLimitError) +} + +// TxSuccessOrExceedsBlockGasLimit returnsrue if the transaction was successful +// or if it failed with an ExceedBlockGasLimit error +func TxSuccessOrExceedsBlockGasLimit(res *abci.ResponseDeliverTx) bool { + return res.Code == 0 || TxExceedBlockGasLimit(res) +} diff --git a/server/config/config.go b/server/config/config.go index b23a1503da..198941e8f7 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -108,6 +108,8 @@ type JSONRPCConfig struct { // MaxOpenConnections sets the maximum number of simultaneous connections // for the server listener. MaxOpenConnections int `mapstructure:"max-open-connections"` + // EnableIndexer defines if enable the custom indexer service. + EnableIndexer bool `mapstructure:"enable-indexer"` } // TLSConfig defines the certificate and matching private key for the server. @@ -208,6 +210,7 @@ func DefaultJSONRPCConfig() *JSONRPCConfig { HTTPIdleTimeout: DefaultHTTPIdleTimeout, AllowUnprotectedTxs: DefaultAllowUnprotectedTxs, MaxOpenConnections: DefaultMaxOpenConnections, + EnableIndexer: false, } } @@ -315,6 +318,7 @@ func GetConfig(v *viper.Viper) (Config, error) { HTTPTimeout: v.GetDuration("json-rpc.http-timeout"), HTTPIdleTimeout: v.GetDuration("json-rpc.http-idle-timeout"), MaxOpenConnections: v.GetInt("json-rpc.max-open-connections"), + EnableIndexer: v.GetBool("json-rpc.enable-indexer"), }, TLS: TLSConfig{ CertificatePath: v.GetString("tls.certificate-path"), diff --git a/server/config/toml.go b/server/config/toml.go index d8861c83c8..dc6de46ae7 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -70,6 +70,9 @@ allow-unprotected-txs = {{ .JSONRPC.AllowUnprotectedTxs }} # for the server listener. max-open-connections = {{ .JSONRPC.MaxOpenConnections }} +# EnableIndexer enables the custom transaction indexer for the EVM (ethereum transactions). +enable-indexer = {{ .JSONRPC.EnableIndexer }} + ############################################################################### ### TLS Configuration ### ############################################################################### diff --git a/server/flags/flags.go b/server/flags/flags.go index f727549bc4..4d5c41e175 100644 --- a/server/flags/flags.go +++ b/server/flags/flags.go @@ -46,6 +46,7 @@ const ( JSONRPCHTTPIdleTimeout = "json-rpc.http-idle-timeout" JSONRPCAllowUnprotectedTxs = "json-rpc.allow-unprotected-txs" JSONRPCMaxOpenConnections = "json-rpc.max-open-connections" + JSONRPCEnableIndexer = "json-rpc.enable-indexer" ) // EVM flags diff --git a/server/indexer_cmd.go b/server/indexer_cmd.go new file mode 100644 index 0000000000..fe614756ce --- /dev/null +++ b/server/indexer_cmd.go @@ -0,0 +1,114 @@ +package server + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + "github.com/evmos/ethermint/indexer" + tmnode "github.com/tendermint/tendermint/node" + sm "github.com/tendermint/tendermint/state" + tmstore "github.com/tendermint/tendermint/store" +) + +func NewIndexTxCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "index-eth-tx [forward|backward]", + Short: "Index historical eth txs", + Long: `Index historical eth txs, it only support two traverse direction to avoid creating gaps in the indexer db if using arbitrary block ranges: + - backward: index the blocks from the first indexed block to the earliest block in the chain. + - forward: index the blocks from the latest indexed block to latest block in the chain. + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + serverCtx := server.GetServerContextFromCmd(cmd) + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + direction := args[0] + if direction != "backward" && direction != "forward" { + return fmt.Errorf("unknown index direction, expect: backward|forward, got: %s", direction) + } + + cfg := serverCtx.Config + home := cfg.RootDir + logger := serverCtx.Logger + idxDB, err := OpenIndexerDB(home, server.GetAppDBBackend(serverCtx.Viper)) + if err != nil { + logger.Error("failed to open evm indexer DB", "error", err.Error()) + return err + } + idxer := indexer.NewKVIndexer(idxDB, logger.With("module", "evmindex"), clientCtx) + + // open local tendermint db, because the local rpc won't be available. + tmdb, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "blockstore", Config: cfg}) + if err != nil { + return err + } + blockStore := tmstore.NewBlockStore(tmdb) + + stateDB, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "state", Config: cfg}) + if err != nil { + return err + } + stateStore := sm.NewStore(stateDB, sm.StoreOptions{ + DiscardABCIResponses: cfg.Storage.DiscardABCIResponses, + }) + + indexBlock := func(height int64) error { + blk := blockStore.LoadBlock(height) + if blk == nil { + return fmt.Errorf("block not found %d", height) + } + resBlk, err := stateStore.LoadABCIResponses(height) + if err != nil { + return err + } + if err := idxer.IndexBlock(blk, resBlk.DeliverTxs); err != nil { + return err + } + fmt.Println(height) + return nil + } + + switch args[0] { + case "backward": + first, err := idxer.FirstIndexedBlock() + if err != nil { + return err + } + if first == -1 { + return fmt.Errorf("indexer db is empty") + } + for i := first - 1; i > 0; i-- { + if err := indexBlock(i); err != nil { + return err + } + } + case "forward": + latest, err := idxer.LastIndexedBlock() + if err != nil { + return err + } + if latest == -1 { + // start from genesis if empty + latest = 0 + } + for i := latest + 1; i <= blockStore.Height(); i++ { + if err := indexBlock(i); err != nil { + return err + } + } + default: + return fmt.Errorf("unknown direction %s", args[0]) + } + + return nil + }, + } + return cmd +} diff --git a/server/indexer_service.go b/server/indexer_service.go new file mode 100644 index 0000000000..d8b233d7a1 --- /dev/null +++ b/server/indexer_service.go @@ -0,0 +1,109 @@ +package server + +import ( + "context" + "time" + + "github.com/tendermint/tendermint/libs/service" + rpcclient "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/types" + + ethermint "github.com/evmos/ethermint/types" +) + +const ( + ServiceName = "EVMIndexerService" + + NewBlockWaitTimeout = 60 * time.Second +) + +// EVMIndexerService indexes transactions for json-rpc service. +type EVMIndexerService struct { + service.BaseService + + txIdxr ethermint.EVMTxIndexer + client rpcclient.Client +} + +// NewEVMIndexerService returns a new service instance. +func NewEVMIndexerService( + txIdxr ethermint.EVMTxIndexer, + client rpcclient.Client, +) *EVMIndexerService { + is := &EVMIndexerService{txIdxr: txIdxr, client: client} + is.BaseService = *service.NewBaseService(nil, ServiceName, is) + return is +} + +// OnStart implements service.Service by subscribing for new blocks +// and indexing them by events. +func (eis *EVMIndexerService) OnStart() error { + ctx := context.Background() + status, err := eis.client.Status(ctx) + if err != nil { + return err + } + latestBlock := status.SyncInfo.LatestBlockHeight + newBlockSignal := make(chan struct{}, 1) + + // Use SubscribeUnbuffered here to ensure both subscriptions does not get + // canceled due to not pulling messages fast enough. Cause this might + // sometimes happen when there are no other subscribers. + blockHeadersChan, err := eis.client.Subscribe( + ctx, + ServiceName, + types.QueryForEvent(types.EventNewBlockHeader).String(), + 0) + if err != nil { + return err + } + + go func() { + for { + msg := <-blockHeadersChan + eventDataHeader := msg.Data.(types.EventDataNewBlockHeader) + if eventDataHeader.Header.Height > latestBlock { + latestBlock = eventDataHeader.Header.Height + // notify + select { + case newBlockSignal <- struct{}{}: + default: + } + } + } + }() + + lastBlock, err := eis.txIdxr.LastIndexedBlock() + if err != nil { + return err + } + if lastBlock == -1 { + lastBlock = latestBlock + } + for { + if latestBlock <= lastBlock { + // nothing to index. wait for signal of new block + select { + case <-newBlockSignal: + case <-time.After(NewBlockWaitTimeout): + } + continue + } + for i := lastBlock + 1; i <= latestBlock; i++ { + block, err := eis.client.Block(ctx, &i) + if err != nil { + eis.Logger.Error("failed to fetch block", "height", i, "err", err) + break + } + blockResult, err := eis.client.BlockResults(ctx, &i) + if err != nil { + eis.Logger.Error("failed to fetch block result", "height", i, "err", err) + break + } + if err := eis.txIdxr.IndexBlock(block.Block, blockResult.TxsResults); err != nil { + eis.Logger.Error("failed to index block", "height", i, "err", err) + } + lastBlock = blockResult.Height + } + } +} diff --git a/server/json_rpc.go b/server/json_rpc.go index bf6b8306c9..332c2c9ef5 100644 --- a/server/json_rpc.go +++ b/server/json_rpc.go @@ -15,10 +15,11 @@ import ( "github.com/evmos/ethermint/rpc" "github.com/evmos/ethermint/server/config" + ethermint "github.com/evmos/ethermint/types" ) // StartJSONRPC starts the JSON-RPC server -func StartJSONRPC(ctx *server.Context, clientCtx client.Context, tmRPCAddr, tmEndpoint string, config *config.Config) (*http.Server, chan struct{}, error) { +func StartJSONRPC(ctx *server.Context, clientCtx client.Context, tmRPCAddr, tmEndpoint string, config *config.Config, indexer ethermint.EVMTxIndexer) (*http.Server, chan struct{}, error) { tmWsClient := ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) logger := ctx.Logger.With("module", "geth") @@ -39,7 +40,7 @@ func StartJSONRPC(ctx *server.Context, clientCtx client.Context, tmRPCAddr, tmEn allowUnprotectedTxs := config.JSONRPC.AllowUnprotectedTxs rpcAPIArr := config.JSONRPC.API - apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, allowUnprotectedTxs, rpcAPIArr) + apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, allowUnprotectedTxs, indexer, rpcAPIArr) for _, api := range apis { if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil { diff --git a/server/start.go b/server/start.go index 52f2dd3c93..44360b1fdd 100644 --- a/server/start.go +++ b/server/start.go @@ -41,9 +41,11 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/evmos/ethermint/indexer" ethdebug "github.com/evmos/ethermint/rpc/namespaces/ethereum/debug" "github.com/evmos/ethermint/server/config" srvflags "github.com/evmos/ethermint/server/flags" + ethermint "github.com/evmos/ethermint/types" ) // StartCmd runs the service passed in, either stand-alone or in-process with @@ -166,6 +168,7 @@ which accepts a path for the resulting pprof file. cmd.Flags().Int32(srvflags.JSONRPCLogsCap, config.DefaultLogsCap, "Sets the max number of results can be returned from single `eth_getLogs` query") cmd.Flags().Int32(srvflags.JSONRPCBlockRangeCap, config.DefaultBlockRangeCap, "Sets the max block range allowed for `eth_getLogs` query") cmd.Flags().Int(srvflags.JSONRPCMaxOpenConnections, config.DefaultMaxOpenConnections, "Sets the maximum number of simultaneous connections for the server listener") + cmd.Flags().Bool(srvflags.JSONRPCEnableIndexer, false, "Enable the custom tx indexer for json-rpc") cmd.Flags().String(srvflags.EVMTracer, config.DefaultEVMTracer, "the EVM tracer type to collect execution traces from the EVM transaction execution (json|struct|access_list|markdown)") cmd.Flags().Uint64(srvflags.EVMMaxTxGasWanted, config.DefaultMaxTxGasWanted, "the gas wanted for each eth tx returned in ante handler in check tx mode") @@ -327,13 +330,39 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, appCreator ty // Add the tx service to the gRPC router. We only need to register this // service if API or gRPC or JSONRPC is enabled, and avoid doing so in the general // case, because it spawns a new local tendermint RPC client. - if config.API.Enable || config.GRPC.Enable || config.JSONRPC.Enable { + if config.API.Enable || config.GRPC.Enable || config.JSONRPC.Enable || config.JSONRPC.EnableIndexer { clientCtx = clientCtx.WithClient(local.New(tmNode)) app.RegisterTxService(clientCtx) app.RegisterTendermintService(clientCtx) } + var idxer ethermint.EVMTxIndexer + if config.JSONRPC.EnableIndexer { + idxDB, err := OpenIndexerDB(home, server.GetAppDBBackend(ctx.Viper)) + if err != nil { + logger.Error("failed to open evm indexer DB", "error", err.Error()) + return err + } + idxLogger := ctx.Logger.With("module", "evmindex") + idxer = indexer.NewKVIndexer(idxDB, idxLogger, clientCtx) + indexerService := NewEVMIndexerService(idxer, clientCtx.Client) + indexerService.SetLogger(idxLogger) + + errCh := make(chan error) + go func() { + if err := indexerService.Start(); err != nil { + errCh <- err + } + }() + + select { + case err := <-errCh: + return err + case <-time.After(types.ServerStartTime): // assume server started successfully + } + } + var apiSrv *api.Server if config.API.Enable { genDoc, err := genDocProvider() @@ -431,7 +460,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, appCreator ty tmEndpoint := "/websocket" tmRPCAddr := cfg.RPC.ListenAddress - httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, tmRPCAddr, tmEndpoint, &config) + httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, tmRPCAddr, tmEndpoint, &config, idxer) if err != nil { return err } @@ -486,6 +515,12 @@ func openDB(rootDir string) (dbm.DB, error) { return sdk.NewLevelDB("application", dataDir) } +// OpenIndexerDB opens the custom eth indexer db, using the same db backend as the main app +func OpenIndexerDB(rootDir string, backendType dbm.BackendType) (dbm.DB, error) { + dataDir := filepath.Join(rootDir, "data") + return dbm.NewDB("evmindexer", backendType, dataDir) +} + func openTraceWriter(traceWriterFile string) (w io.Writer, err error) { if traceWriterFile == "" { return diff --git a/server/util.go b/server/util.go index 1b5bdcec80..8ecd6bb2a2 100644 --- a/server/util.go +++ b/server/util.go @@ -45,6 +45,8 @@ func AddCommands(rootCmd *cobra.Command, defaultNodeHome string, appCreator type sdkserver.ExportCmd(appExport, defaultNodeHome), version.NewVersionCommand(), sdkserver.NewRollbackCmd(appCreator, defaultNodeHome), + // custom tx indexer command + NewIndexTxCmd(), ) } diff --git a/tests/integration_tests/configs/enable-indexer.jsonnet b/tests/integration_tests/configs/enable-indexer.jsonnet new file mode 100644 index 0000000000..c21c6a98c0 --- /dev/null +++ b/tests/integration_tests/configs/enable-indexer.jsonnet @@ -0,0 +1,20 @@ +local config = import 'default.jsonnet'; + +config { + 'ethermint_9000-1'+: { + config+: { + tx_index+: { + indexer: 'null', + }, + }, + 'app-config'+: { + pruning: 'everything', + 'state-sync'+: { + 'snapshot-interval': 0, + }, + 'json-rpc'+: { + 'enable-indexer': true, + }, + }, + }, +} diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 53d709c682..5417176b9b 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -1,6 +1,8 @@ +from pathlib import Path + import pytest -from .network import setup_ethermint, setup_geth +from .network import setup_custom_ethermint, setup_ethermint, setup_geth @pytest.fixture(scope="session") @@ -9,14 +11,24 @@ def ethermint(tmp_path_factory): yield from setup_ethermint(path, 26650) +@pytest.fixture(scope="session") +def ethermint_indexer(tmp_path_factory): + path = tmp_path_factory.mktemp("indexer") + yield from setup_custom_ethermint( + path, 26660, Path(__file__).parent / "configs/enable-indexer.jsonnet" + ) + + @pytest.fixture(scope="session") def geth(tmp_path_factory): path = tmp_path_factory.mktemp("geth") yield from setup_geth(path, 8545) -@pytest.fixture(scope="session", params=["ethermint", "geth", "ethermint-ws"]) -def cluster(request, ethermint, geth): +@pytest.fixture( + scope="session", params=["ethermint", "geth", "ethermint-ws", "enable-indexer"] +) +def cluster(request, ethermint, ethermint_indexer, geth): """ run on both ethermint and geth """ @@ -29,5 +41,7 @@ def cluster(request, ethermint, geth): ethermint_ws = ethermint.copy() ethermint_ws.use_websocket() yield ethermint_ws + elif provider == "enable-indexer": + yield ethermint_indexer else: raise NotImplementedError diff --git a/testutil/network/util.go b/testutil/network/util.go index fe0e5882b4..45d0bc42f1 100644 --- a/testutil/network/util.go +++ b/testutil/network/util.go @@ -130,7 +130,7 @@ func startInProcess(cfg Config, val *Validator) error { tmEndpoint := "/websocket" tmRPCAddr := val.RPCAddress - val.jsonrpc, val.jsonrpcDone, err = server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig) + val.jsonrpc, val.jsonrpcDone, err = server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig, nil) if err != nil { return err } diff --git a/types/indexer.go b/types/indexer.go new file mode 100644 index 0000000000..c6103dcd7b --- /dev/null +++ b/types/indexer.go @@ -0,0 +1,19 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common" + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +// EVMTxIndexer defines the interface of custom eth tx indexer. +type EVMTxIndexer interface { + // LastIndexedBlock returns -1 if indexer db is empty + LastIndexedBlock() (int64, error) + IndexBlock(*tmtypes.Block, []*abci.ResponseDeliverTx) error + + // GetByTxHash returns nil if tx not found. + GetByTxHash(common.Hash) (*TxResult, error) + // GetByBlockAndIndex returns nil if tx not found. + GetByBlockAndIndex(int64, int32) (*TxResult, error) +} diff --git a/types/indexer.pb.go b/types/indexer.pb.go new file mode 100644 index 0000000000..2e15c18545 --- /dev/null +++ b/types/indexer.pb.go @@ -0,0 +1,485 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: ethermint/types/v1/indexer.proto + +package types + +import ( + fmt "fmt" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// TxResult is the value stored in eth tx indexer +type TxResult struct { + // the block height + Height int64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` + // cosmos tx index + TxIndex uint32 `protobuf:"varint,2,opt,name=tx_index,json=txIndex,proto3" json:"tx_index,omitempty"` + // the msg index in a batch tx + MsgIndex uint32 `protobuf:"varint,3,opt,name=msg_index,json=msgIndex,proto3" json:"msg_index,omitempty"` + // eth tx index, the index in the list of valid eth tx in the block, + // aka. the transaction list returned by eth_getBlock api. + EthTxIndex int32 `protobuf:"varint,4,opt,name=eth_tx_index,json=ethTxIndex,proto3" json:"eth_tx_index,omitempty"` + // if the eth tx is failed + Failed bool `protobuf:"varint,5,opt,name=failed,proto3" json:"failed,omitempty"` + // gas used by tx, if exceeds block gas limit, + // it's set to gas limit which is what's actually deducted by ante handler. + GasUsed uint64 `protobuf:"varint,6,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` + // the cumulative gas used within current batch tx + CumulativeGasUsed uint64 `protobuf:"varint,7,opt,name=cumulative_gas_used,json=cumulativeGasUsed,proto3" json:"cumulative_gas_used,omitempty"` +} + +func (m *TxResult) Reset() { *m = TxResult{} } +func (m *TxResult) String() string { return proto.CompactTextString(m) } +func (*TxResult) ProtoMessage() {} +func (*TxResult) Descriptor() ([]byte, []int) { + return fileDescriptor_1197e10a8be8ed28, []int{0} +} +func (m *TxResult) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *TxResult) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_TxResult.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *TxResult) XXX_Merge(src proto.Message) { + xxx_messageInfo_TxResult.Merge(m, src) +} +func (m *TxResult) XXX_Size() int { + return m.Size() +} +func (m *TxResult) XXX_DiscardUnknown() { + xxx_messageInfo_TxResult.DiscardUnknown(m) +} + +var xxx_messageInfo_TxResult proto.InternalMessageInfo + +func init() { + proto.RegisterType((*TxResult)(nil), "ethermint.types.v1.TxResult") +} + +func init() { proto.RegisterFile("ethermint/types/v1/indexer.proto", fileDescriptor_1197e10a8be8ed28) } + +var fileDescriptor_1197e10a8be8ed28 = []byte{ + // 295 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x90, 0x31, 0x4b, 0xc3, 0x40, + 0x18, 0x86, 0x73, 0xb6, 0x4d, 0xe3, 0xa1, 0x83, 0x51, 0x4a, 0x54, 0x88, 0x87, 0x53, 0xa6, 0x84, + 0xe2, 0xd6, 0xd1, 0x45, 0x5c, 0x8f, 0xba, 0xb8, 0x84, 0xb4, 0xf9, 0xbc, 0x3b, 0xe8, 0xf5, 0x4a, + 0xef, 0x4b, 0x88, 0xff, 0xc0, 0xd1, 0x9f, 0xe0, 0xcf, 0x71, 0xec, 0xe8, 0x28, 0x2d, 0xfe, 0x0f, + 0xe9, 0x35, 0x44, 0x70, 0xfb, 0x5e, 0x9e, 0xe7, 0xe3, 0x85, 0x97, 0x32, 0x40, 0x09, 0x6b, 0xad, + 0x96, 0x98, 0xe1, 0xeb, 0x0a, 0x6c, 0x56, 0x8f, 0x33, 0xb5, 0x2c, 0xa1, 0x81, 0x75, 0xba, 0x5a, + 0x1b, 0x34, 0x61, 0xd8, 0x19, 0xa9, 0x33, 0xd2, 0x7a, 0x7c, 0x75, 0x21, 0x8c, 0x30, 0x0e, 0x67, + 0xfb, 0xeb, 0x60, 0xde, 0xfe, 0x10, 0x1a, 0x4c, 0x1b, 0x0e, 0xb6, 0x5a, 0x60, 0x38, 0xa2, 0xbe, + 0x04, 0x25, 0x24, 0x46, 0x84, 0x91, 0xa4, 0xc7, 0xdb, 0x14, 0x5e, 0xd2, 0x00, 0x9b, 0xdc, 0x55, + 0x44, 0x47, 0x8c, 0x24, 0xa7, 0x7c, 0x88, 0xcd, 0xe3, 0x3e, 0x86, 0xd7, 0xf4, 0x58, 0x5b, 0xd1, + 0xb2, 0x9e, 0x63, 0x81, 0xb6, 0xe2, 0x00, 0x19, 0x3d, 0x01, 0x94, 0x79, 0xf7, 0xdb, 0x67, 0x24, + 0x19, 0x70, 0x0a, 0x28, 0xa7, 0xed, 0xfb, 0x88, 0xfa, 0x2f, 0x85, 0x5a, 0x40, 0x19, 0x0d, 0x18, + 0x49, 0x02, 0xde, 0xa6, 0x7d, 0xa3, 0x28, 0x6c, 0x5e, 0x59, 0x28, 0x23, 0x9f, 0x91, 0xa4, 0xcf, + 0x87, 0xa2, 0xb0, 0x4f, 0x16, 0xca, 0x30, 0xa5, 0xe7, 0xf3, 0x4a, 0x57, 0x8b, 0x02, 0x55, 0x0d, + 0x79, 0x67, 0x0d, 0x9d, 0x75, 0xf6, 0x87, 0x1e, 0x0e, 0xfe, 0xa4, 0xff, 0xf6, 0x71, 0xe3, 0xdd, + 0x4f, 0x3e, 0xb7, 0x31, 0xd9, 0x6c, 0x63, 0xf2, 0xbd, 0x8d, 0xc9, 0xfb, 0x2e, 0xf6, 0x36, 0xbb, + 0xd8, 0xfb, 0xda, 0xc5, 0xde, 0x33, 0x13, 0x0a, 0x65, 0x35, 0x4b, 0xe7, 0x46, 0x67, 0x50, 0x6b, + 0x63, 0xb3, 0x7f, 0xf3, 0xce, 0x7c, 0x37, 0xd5, 0xdd, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc1, + 0x34, 0xa8, 0x0b, 0x78, 0x01, 0x00, 0x00, +} + +func (m *TxResult) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TxResult) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *TxResult) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.CumulativeGasUsed != 0 { + i = encodeVarintIndexer(dAtA, i, uint64(m.CumulativeGasUsed)) + i-- + dAtA[i] = 0x38 + } + if m.GasUsed != 0 { + i = encodeVarintIndexer(dAtA, i, uint64(m.GasUsed)) + i-- + dAtA[i] = 0x30 + } + if m.Failed { + i-- + if m.Failed { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x28 + } + if m.EthTxIndex != 0 { + i = encodeVarintIndexer(dAtA, i, uint64(m.EthTxIndex)) + i-- + dAtA[i] = 0x20 + } + if m.MsgIndex != 0 { + i = encodeVarintIndexer(dAtA, i, uint64(m.MsgIndex)) + i-- + dAtA[i] = 0x18 + } + if m.TxIndex != 0 { + i = encodeVarintIndexer(dAtA, i, uint64(m.TxIndex)) + i-- + dAtA[i] = 0x10 + } + if m.Height != 0 { + i = encodeVarintIndexer(dAtA, i, uint64(m.Height)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintIndexer(dAtA []byte, offset int, v uint64) int { + offset -= sovIndexer(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *TxResult) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Height != 0 { + n += 1 + sovIndexer(uint64(m.Height)) + } + if m.TxIndex != 0 { + n += 1 + sovIndexer(uint64(m.TxIndex)) + } + if m.MsgIndex != 0 { + n += 1 + sovIndexer(uint64(m.MsgIndex)) + } + if m.EthTxIndex != 0 { + n += 1 + sovIndexer(uint64(m.EthTxIndex)) + } + if m.Failed { + n += 2 + } + if m.GasUsed != 0 { + n += 1 + sovIndexer(uint64(m.GasUsed)) + } + if m.CumulativeGasUsed != 0 { + n += 1 + sovIndexer(uint64(m.CumulativeGasUsed)) + } + return n +} + +func sovIndexer(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozIndexer(x uint64) (n int) { + return sovIndexer(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *TxResult) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndexer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: TxResult: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: TxResult: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Height", wireType) + } + m.Height = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndexer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Height |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field TxIndex", wireType) + } + m.TxIndex = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndexer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.TxIndex |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MsgIndex", wireType) + } + m.MsgIndex = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndexer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MsgIndex |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field EthTxIndex", wireType) + } + m.EthTxIndex = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndexer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.EthTxIndex |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Failed", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndexer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Failed = bool(v != 0) + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field GasUsed", wireType) + } + m.GasUsed = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndexer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.GasUsed |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 7: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CumulativeGasUsed", wireType) + } + m.CumulativeGasUsed = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndexer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CumulativeGasUsed |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipIndexer(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthIndexer + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipIndexer(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowIndexer + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowIndexer + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowIndexer + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthIndexer + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupIndexer + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthIndexer + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthIndexer = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowIndexer = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupIndexer = fmt.Errorf("proto: unexpected end of group") +)