From 7fcc10759817fa0ab02bbf43226784b28f5748e4 Mon Sep 17 00:00:00 2001 From: HuangYi Date: Tue, 19 Apr 2022 12:26:57 +0800 Subject: [PATCH] Problem: get tx receipt api is too slow for indexing service Closes: #431 Solution: - add extension api: cronos_getTransactionReceiptsByBlock - add integration test --- app/app.go | 3 + go.mod | 2 +- go.sum | 4 +- integration_tests/cosmoscli.py | 6 + integration_tests/test_basic.py | 104 +++++++------- integration_tests/utils.py | 35 +++++ scripts/devnet.yaml | 2 +- x/cronos/rpc/api.go | 235 ++++++++++++++++++++++++++++++++ x/cronos/rpc/utils.go | 59 ++++++++ 9 files changed, 395 insertions(+), 55 deletions(-) create mode 100644 x/cronos/rpc/api.go create mode 100644 x/cronos/rpc/utils.go diff --git a/app/app.go b/app/app.go index 283effaee9..8dd278e315 100644 --- a/app/app.go +++ b/app/app.go @@ -123,6 +123,9 @@ import ( // Force-load the tracer engines to trigger registration _ "github.com/ethereum/go-ethereum/eth/tracers/js" _ "github.com/ethereum/go-ethereum/eth/tracers/native" + + // force register the extension json-rpc. + _ "github.com/crypto-org-chain/cronos/x/cronos/rpc" ) const ( diff --git a/go.mod b/go.mod index d2dd864023..816b0b1c64 100644 --- a/go.mod +++ b/go.mod @@ -164,7 +164,7 @@ replace ( // See https://github.com/tecbot/gorocksdb/pull/216 github.com/tecbot/gorocksdb => github.com/cosmos/gorocksdb v1.1.1 - github.com/tharsis/ethermint => github.com/crypto-org-chain/ethermint v0.10.0-alpha1-cronos-8 + github.com/tharsis/ethermint => github.com/yihuang/ethermint v0.10.0-alpha1-cronos-8.0.20220419042208-80726a75b25d google.golang.org/grpc => google.golang.org/grpc v1.33.2 ) diff --git a/go.sum b/go.sum index 9e0b373357..b71e3d821f 100644 --- a/go.sum +++ b/go.sum @@ -246,8 +246,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crypto-org-chain/ethermint v0.10.0-alpha1-cronos-8 h1:+1FLhY/C+mKUPqKNqtq81tCIorCFcqLZZflHebRDnIc= -github.com/crypto-org-chain/ethermint v0.10.0-alpha1-cronos-8/go.mod h1:3CXBYpzlUtEn6OCr1WFw/951MBEeojZW3hcqPCv5ktw= github.com/crypto-org-chain/ibc-go/v2 v2.2.0-hooks2 h1:elj+Tb/3O9GA3pv62zkc1B0P8hl1WHmF6vF8PInEJm4= github.com/crypto-org-chain/ibc-go/v2 v2.2.0-hooks2/go.mod h1:rAHRlBcRiHPP/JszN+08SJx3pegww9bcVncIb9QLx7I= github.com/crypto-org-chain/keyring v1.1.6-fixes h1:AUFSu56NY6XobY6XfRoDx6v3loiOrHK5MNUm32GEjwA= @@ -1072,6 +1070,8 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/ybbus/jsonrpc v2.1.2+incompatible/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= +github.com/yihuang/ethermint v0.10.0-alpha1-cronos-8.0.20220419042208-80726a75b25d h1:EtR1DZWzbT2eifrK34F3HLPn4WAqHuUiWDtitXk+c5Y= +github.com/yihuang/ethermint v0.10.0-alpha1-cronos-8.0.20220419042208-80726a75b25d/go.mod h1:3CXBYpzlUtEn6OCr1WFw/951MBEeojZW3hcqPCv5ktw= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/integration_tests/cosmoscli.py b/integration_tests/cosmoscli.py index e99f1493d6..7857bc559c 100644 --- a/integration_tests/cosmoscli.py +++ b/integration_tests/cosmoscli.py @@ -503,6 +503,12 @@ def broadcast_tx(self, tx_file, **kwargs): self.raw("tx", "broadcast", tx_file, node=self.node_rpc, **kwargs) ) + def broadcast_tx_json(self, tx, **kwargs): + with tempfile.NamedTemporaryFile("w") as fp: + json.dump(tx, fp) + fp.flush() + return self.broadcast_tx(fp.name) + def unjail(self, addr): return json.loads( self.raw( diff --git a/integration_tests/test_basic.py b/integration_tests/test_basic.py index 98279b5d8a..a1a345dd31 100644 --- a/integration_tests/test_basic.py +++ b/integration_tests/test_basic.py @@ -1,6 +1,5 @@ import concurrent.futures import json -import tempfile import time from pathlib import Path @@ -17,6 +16,7 @@ KEYS, Greeter, RevertTestContract, + build_batch_tx, contract_address, contract_path, deploy_contract, @@ -464,6 +464,7 @@ def test_suicide(cluster): def test_batch_tx(cronos): "send multiple eth txs in single cosmos tx" w3 = cronos.w3 + cli = cronos.cosmos_cli() sender = ADDRS["validator"] recipient = ADDRS["community"] nonce = w3.eth.get_transaction_count(sender) @@ -480,51 +481,13 @@ def test_batch_tx(cronos): {"from": sender, "nonce": nonce + 2, "gas": 200000} ) - signed_txs = [ - sign_transaction(w3, deploy_tx, KEYS["validator"]), - sign_transaction(w3, transfer_tx1, KEYS["validator"]), - sign_transaction(w3, transfer_tx2, KEYS["validator"]), - ] - tmp_txs = [ - cronos.cosmos_cli().build_evm_tx(signed.rawTransaction.hex()) - for signed in signed_txs - ] - - msgs = [tx["body"]["messages"][0] for tx in tmp_txs] - fee = sum(int(tx["auth_info"]["fee"]["amount"][0]["amount"]) for tx in tmp_txs) - gas_limit = sum(int(tx["auth_info"]["fee"]["gas_limit"]) for tx in tmp_txs) - - # build batch cosmos tx - cosmos_tx = { - "body": { - "messages": msgs, - "memo": "", - "timeout_height": "0", - "extension_options": [ - {"@type": "/ethermint.evm.v1.ExtensionOptionsEthereumTx"} - ], - "non_critical_extension_options": [], - }, - "auth_info": { - "signer_infos": [], - "fee": { - "amount": [{"denom": "basetcro", "amount": str(fee)}], - "gas_limit": str(gas_limit), - "payer": "", - "granter": "", - }, - }, - "signatures": [], - } - with tempfile.NamedTemporaryFile("w") as fp: - json.dump(cosmos_tx, fp) - fp.flush() - rsp = cronos.cosmos_cli().broadcast_tx(fp.name) - assert rsp["code"] == 0, rsp["raw_log"] + cosmos_tx, tx_hashes = build_batch_tx( + w3, cli, [deploy_tx, transfer_tx1, transfer_tx2] + ) + rsp = cli.broadcast_tx_json(cosmos_tx) + assert rsp["code"] == 0, rsp["raw_log"] - receipts = [ - w3.eth.wait_for_transaction_receipt(signed.hash) for signed in signed_txs - ] + receipts = [w3.eth.wait_for_transaction_receipt(h) for h in tx_hashes] assert 2000 == contract.caller.balanceOf(recipient) @@ -548,10 +511,8 @@ def test_batch_tx(cronos): # check traceTransaction rsps = [ - w3.provider.make_request("debug_traceTransaction", [signed.hash.hex()])[ - "result" - ] - for signed in signed_txs + w3.provider.make_request("debug_traceTransaction", [h.hex()])["result"] + for h in tx_hashes ] for rsp, receipt in zip(rsps, receipts): @@ -562,8 +523,49 @@ def test_batch_tx(cronos): txs = [ w3.eth.get_transaction_by_block(receipts[0].blockNumber, i) for i in range(3) ] - for tx, signed in zip(txs, signed_txs): - assert tx.hash == signed.hash + for tx, h in zip(txs, tx_hashes): + assert tx.hash == h + + +def test_failed_transfer_tx(cronos): + """ + It's possible to include a failed transfer transaction in batch tx + """ + w3 = cronos.w3 + cli = cronos.cosmos_cli() + sender = ADDRS["community"] + recipient = ADDRS["validator"] + nonce = w3.eth.get_transaction_count(sender) + half_balance = w3.eth.get_balance(sender) // 2 + + # build batch tx, the third tx will fail, but will be included in block + # because of the batch tx. + transfer1 = {"from": sender, "nonce": nonce, "to": recipient, "value": half_balance} + transfer2 = { + "from": sender, + "nonce": nonce + 1, + "to": recipient, + "value": half_balance, + } + transfer3 = { + "from": sender, + "nonce": nonce + 2, + "to": recipient, + "value": half_balance, + } + cosmos_tx, tx_hashes = build_batch_tx( + w3, cli, [transfer1, transfer2, transfer3], KEYS["community"] + ) + rsp = cli.broadcast_tx_json(cosmos_tx) + assert rsp["code"] == 0, rsp["raw_log"] + + receipts = [w3.eth.wait_for_transaction_receipt(h) for h in tx_hashes] + print("receipts", receipts) + # try the cronos_getTransactionReceiptsByBlock api + rsp = w3.provider.make_request( + "cronos_getTransactionReceiptsByBlock", [receipts[0].blockNumber] + ) + assert receipts == rsp["result"] def test_log0(cluster): diff --git a/integration_tests/utils.py b/integration_tests/utils.py index 986b28b59b..8ab381a79f 100644 --- a/integration_tests/utils.py +++ b/integration_tests/utils.py @@ -393,3 +393,38 @@ def modify_command_in_supervisor_config(ini: Path, fn, **kwargs): **kwargs, ) ) + + +def build_batch_tx(w3, cli, txs, key=KEYS["validator"]): + "return cosmos batch tx and eth tx hashes" + signed_txs = [sign_transaction(w3, tx, key) for tx in txs] + tmp_txs = [cli.build_evm_tx(signed.rawTransaction.hex()) for signed in signed_txs] + + msgs = [tx["body"]["messages"][0] for tx in tmp_txs] + fee = sum(int(tx["auth_info"]["fee"]["amount"][0]["amount"]) for tx in tmp_txs) + gas_limit = sum(int(tx["auth_info"]["fee"]["gas_limit"]) for tx in tmp_txs) + + tx_hashes = [signed.hash for signed in signed_txs] + + # build batch cosmos tx + return { + "body": { + "messages": msgs, + "memo": "", + "timeout_height": "0", + "extension_options": [ + {"@type": "/ethermint.evm.v1.ExtensionOptionsEthereumTx"} + ], + "non_critical_extension_options": [], + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [{"denom": "basetcro", "amount": str(fee)}], + "gas_limit": str(gas_limit), + "payer": "", + "granter": "", + }, + }, + "signatures": [], + }, tx_hashes diff --git a/scripts/devnet.yaml b/scripts/devnet.yaml index aaa47114c2..a1aee17ae8 100644 --- a/scripts/devnet.yaml +++ b/scripts/devnet.yaml @@ -6,7 +6,7 @@ cronos_777-1: json-rpc: address: "0.0.0.0:{EVMRPC_PORT}" ws-address: "0.0.0.0:{EVMRPC_PORT_WS}" - api: "eth,net,web3,debug" + api: "eth,net,web3,debug,cronos" validators: - coins: 1000000000000000000stake,1000000000000000000basetcro staked: 1000000000000000000stake diff --git a/x/cronos/rpc/api.go b/x/cronos/rpc/api.go new file mode 100644 index 0000000000..bcf786bf3b --- /dev/null +++ b/x/cronos/rpc/api.go @@ -0,0 +1,235 @@ +package rpc + +import ( + "context" + "fmt" + "math/big" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + "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" + "github.com/ethereum/go-ethereum/rpc" + "github.com/tendermint/tendermint/libs/log" + rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" + evmrpc "github.com/tharsis/ethermint/rpc" + "github.com/tharsis/ethermint/rpc/ethereum/backend" + rpctypes "github.com/tharsis/ethermint/rpc/ethereum/types" + ethermint "github.com/tharsis/ethermint/types" + evmtypes "github.com/tharsis/ethermint/x/evm/types" +) + +const ( + // CronosNamespace is the extension RPC namespace of cronos module. + CronosNamespace = "cronos" + + apiVersion = "1.0" +) + +func init() { + evmrpc.APICreators[CronosNamespace] = CreateCronosRPCAPIs +} + +// CreateCronosRPCAPIs creates extension json-rpc apis +func CreateCronosRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient) []rpc.API { + evmBackend := backend.NewEVMBackend(ctx, ctx.Logger, clientCtx) + return []rpc.API{ + { + Namespace: CronosNamespace, + Version: apiVersion, + Service: NewCronosAPI(ctx.Logger, clientCtx, evmBackend), + Public: true, + }, + } +} + +// CronosAPI is the extension jsonrpc apis prefixed with cronos_. +type CronosAPI struct { + ctx context.Context + clientCtx client.Context + queryClient *rpctypes.QueryClient + chainIDEpoch *big.Int + logger log.Logger + backend backend.Backend +} + +// NewCronosAPI creates an instance of the cronos web3 extension apis. +func NewCronosAPI( + logger log.Logger, + clientCtx client.Context, + backend backend.Backend, +) *CronosAPI { + eip155ChainID, err := ethermint.ParseChainID(clientCtx.ChainID) + if err != nil { + panic(err) + } + return &CronosAPI{ + ctx: context.Background(), + clientCtx: clientCtx, + queryClient: rpctypes.NewQueryClient(clientCtx), + chainIDEpoch: eip155ChainID, + logger: logger.With("client", "json-rpc"), + backend: backend, + } +} + +// GetTransactionReceiptsByBlock returns all the transaction receipts included in the block. +func (api *CronosAPI) GetTransactionReceiptsByBlock(blockNrOrHash rpctypes.BlockNumberOrHash) ([]map[string]interface{}, error) { + var receipts []map[string]interface{} + + api.logger.Debug("cronos_getTransactionReceiptsByBlock", "blockNrOrHash", blockNrOrHash) + + blockNum, err := api.getBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + + resBlock, err := api.clientCtx.Client.Block(api.ctx, blockNum.TmHeight()) + if err != nil { + api.logger.Debug("block not found", "height", blockNum, "error", err.Error()) + return nil, nil + } + + blockNumber := resBlock.Block.Height + blockHash := common.BytesToHash(resBlock.Block.Header.Hash()).Hex() + + blockRes, err := api.clientCtx.Client.BlockResults(api.ctx, blockNum.TmHeight()) + if err != nil { + api.logger.Debug("failed to retrieve block results", "height", blockNum, "error", err.Error()) + return nil, nil + } + + baseFee, err := api.backend.BaseFee(blockNum.Int64()) + if err != nil { + return nil, err + } + + txIndex := uint64(0) + cumulativeGasUsed := uint64(0) + for i, tx := range resBlock.Block.Txs { + txResult := blockRes.TxsResults[i] + if txResult.Code != 0 { + // skip failed transaction + continue + } + + tx, err := api.clientCtx.TxConfig.TxDecoder()(tx) + if err != nil { + api.logger.Debug("decoding failed", "error", err.Error()) + return nil, fmt.Errorf("failed to decode tx: %w", err) + } + + msgEvents, err := ParseEthTxEvents(txResult.Events) + if err != nil { + api.logger.Debug("parse tx events failed", "txIndex", txIndex, "error", err.Error()) + return nil, fmt.Errorf("failed to parse tx events: %d %w", txIndex, err) + } + + if len(msgEvents) != len(tx.GetMsgs()) { + return nil, fmt.Errorf("wrong number of tx events: %d", txIndex) + } + + msgCumulativeGasUsed := uint64(0) + for msgIndex, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + api.logger.Debug(fmt.Sprintf("invalid tx type: %T", msg)) + return nil, fmt.Errorf("invalid tx type: %T", msg) + } + + txData, err := evmtypes.UnpackTxData(ethMsg.Data) + if err != nil { + api.logger.Error("failed to unpack tx data", "error", err.Error()) + return nil, err + } + + var gasUsed uint64 + if len(tx.GetMsgs()) == 1 { + // backward compatibility + gasUsed = uint64(txResult.GasUsed) + } else { + gasUsed = msgEvents[msgIndex].GasUsed + } + + // Get the transaction result from the log + var status hexutil.Uint + if msgEvents[msgIndex].Failed { + status = hexutil.Uint(ethtypes.ReceiptStatusFailed) + } else { + status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) + } + + from, err := ethMsg.GetSender(api.chainIDEpoch) + if err != nil { + return nil, err + } + + logs := msgEvents[msgIndex].Logs + if logs == nil { + logs = []*ethtypes.Log{} + } + receipt := map[string]interface{}{ + // Consensus fields: These fields are defined by the Yellow Paper + "status": status, + "cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed + msgCumulativeGasUsed), + "logsBloom": ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)), + "logs": logs, + + // Implementation fields: These fields are added by geth when processing a transaction. + // They are stored in the chain database. + "transactionHash": ethMsg.Hash, + "contractAddress": nil, + "gasUsed": hexutil.Uint64(gasUsed), + "type": hexutil.Uint(txData.TxType()), + + // Inclusion information: These fields provide information about the inclusion of the + // transaction corresponding to this receipt. + "blockHash": blockHash, + "blockNumber": hexutil.Uint64(blockNumber), + "transactionIndex": hexutil.Uint64(txIndex), + + // sender and receiver (contract or EOA) addreses + "from": from, + "to": txData.GetTo(), + } + + // If the ContractAddress is 20 0x0 bytes, assume it is not a contract creation + if txData.GetTo() == nil { + receipt["contractAddress"] = crypto.CreateAddress(from, txData.GetNonce()) + } + + if dynamicTx, ok := txData.(*evmtypes.DynamicFeeTx); ok { + receipt["effectiveGasPrice"] = hexutil.Big(*dynamicTx.GetEffectiveGasPrice(baseFee)) + } + + receipts = append(receipts, receipt) + + msgCumulativeGasUsed += gasUsed + txIndex++ + } + cumulativeGasUsed += msgCumulativeGasUsed + msgCumulativeGasUsed = 0 + } + + return receipts, nil +} + +// getBlockNumber returns the BlockNumber from BlockNumberOrHash +func (api *CronosAPI) getBlockNumber(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) { + switch { + case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil: + return rpctypes.EthEarliestBlockNumber, fmt.Errorf("types BlockHash and BlockNumber cannot be both nil") + case blockNrOrHash.BlockHash != nil: + blockHeader, err := api.backend.HeaderByHash(*blockNrOrHash.BlockHash) + if err != nil { + return rpctypes.EthEarliestBlockNumber, err + } + return rpctypes.NewBlockNumber(blockHeader.Number), nil + case blockNrOrHash.BlockNumber != nil: + return *blockNrOrHash.BlockNumber, nil + default: + return rpctypes.EthEarliestBlockNumber, nil + } +} diff --git a/x/cronos/rpc/utils.go b/x/cronos/rpc/utils.go new file mode 100644 index 0000000000..6078e1f99a --- /dev/null +++ b/x/cronos/rpc/utils.go @@ -0,0 +1,59 @@ +package rpc + +import ( + "strconv" + + ethtypes "github.com/ethereum/go-ethereum/core/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tharsis/ethermint/rpc/ethereum/backend" + evmtypes "github.com/tharsis/ethermint/x/evm/types" +) + +// EthMsgEventParsed defines the attributes parsed from tx event. +type EthMsgEventParsed struct { + Logs []*ethtypes.Log + GasUsed uint64 + TxIndex uint64 + Failed bool +} + +// ParseEthTxEvents parse eth attributes and logs for all messages in cosmos events. +func ParseEthTxEvents(events []abci.Event) ([]EthMsgEventParsed, error) { + msgs := make([]EthMsgEventParsed, 0) + var msg *EthMsgEventParsed + var err error + for _, event := range events { + if event.Type == evmtypes.EventTypeEthereumTx { + // beginning of a new message, finalize the last one + if msg != nil { + msgs = append(msgs, *msg) + } + msg = &EthMsgEventParsed{} + for _, attr := range event.Attributes { + switch string(attr.Key) { + case evmtypes.AttributeKeyTxGasUsed: + msg.GasUsed, err = strconv.ParseUint(string(attr.Value), 10, 64) + if err != nil { + return nil, err + } + case evmtypes.AttributeKeyTxIndex: + msg.TxIndex, err = strconv.ParseUint(string(attr.Value), 10, 64) + if err != nil { + return nil, err + } + case evmtypes.AttributeKeyEthereumTxFailed: + msg.Failed = true + } + } + } else if event.Type == evmtypes.EventTypeTxLog { + msg.Logs, err = backend.ParseTxLogsFromEvent(event) + if err != nil { + return nil, err + } + } + } + if msg != nil { + msgs = append(msgs, *msg) + } + return msgs, nil +}