From e0e4dd49c8a45629cafeb52fe50f9d11097850fd Mon Sep 17 00:00:00 2001 From: HuangYi Date: Fri, 6 May 2022 02:32:09 +0800 Subject: [PATCH] Problem: The unlucky transactions are not shown in json rpc Closes: #455 - add patch-tx-result command - test in integration test --- CHANGELOG.md | 4 + cmd/cronosd/cmd/fix-unlucky-tx.go | 265 +++++++++++++++++++++++++ cmd/cronosd/cmd/root.go | 1 + go.mod | 4 +- go.sum | 8 +- gomod2nix.toml | 16 +- integration_tests/cosmoscli.py | 11 + integration_tests/test_replay_block.py | 34 +++- scripts/devnet.yaml | 6 + x/cronos/rpc/api.go | 24 ++- 10 files changed, 348 insertions(+), 25 deletions(-) create mode 100644 cmd/cronosd/cmd/fix-unlucky-tx.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f98e2dde..638415c510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - [#402](https://github.com/crypto-org-chain/cronos/pull/402) Update ethermint to include bug fixes: a) add an configurable upper bound to gasWanted to prevent DoS attack, b) fix eth_sha3 rpc api, c) json-roc server always use local tm rpc client, d) fix tx hash in pending tx filter response. +### Improvements + +- []() Add `patch-tx-result` command to patch the false-failed transactions in blocks before the v0.7.0 upgrade. + *March 8, 2022* ## v0.6.9 diff --git a/cmd/cronosd/cmd/fix-unlucky-tx.go b/cmd/cronosd/cmd/fix-unlucky-tx.go new file mode 100644 index 0000000000..c087501629 --- /dev/null +++ b/cmd/cronosd/cmd/fix-unlucky-tx.go @@ -0,0 +1,265 @@ +package cmd + +import ( + "errors" + "fmt" + "path/filepath" + "strconv" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + tmcfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/log" + tmnode "github.com/tendermint/tendermint/node" + tmstate "github.com/tendermint/tendermint/proto/tendermint/state" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/state/txindex" + "github.com/tendermint/tendermint/state/txindex/kv" + tmstore "github.com/tendermint/tendermint/store" + "github.com/tendermint/tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" + evmtypes "github.com/tharsis/ethermint/x/evm/types" + + "github.com/crypto-org-chain/cronos/app" + "github.com/crypto-org-chain/cronos/x/cronos/rpc" +) + +// FixUnluckyTxCmd update the tx execution result of false-failed tx in tendermint db +func FixUnluckyTxCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "fix-tx-result [start-block] [end-block]", + Short: "Fix tx execution result of false-failed tx", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + startHeight, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return err + } + if startHeight < 1 { + return fmt.Errorf("Invalid block start-block: %d", startHeight) + } + endHeight, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return err + } + if endHeight < startHeight { + return fmt.Errorf("Invalid end-block %d, smaller than start-block", endHeight) + } + + ctx := server.GetServerContextFromCmd(cmd) + clientCtx := client.GetClientContextFromCmd(cmd) + + tmDB, err := openTMDB(ctx.Config) + if err != nil { + return err + } + + state, err := tmDB.stateStore.Load() + if err != nil { + return err + } + + db, err := openAppDB(ctx.Config.RootDir) + if err != nil { + return err + } + defer func() { + if err := db.Close(); err != nil { + ctx.Logger.With("error", err).Error("error closing db") + } + }() + + encCfg := app.MakeEncodingConfig() + + anApp := app.New( + log.NewNopLogger(), db, nil, false, nil, + ctx.Config.RootDir, 0, encCfg, ctx.Viper, + ) + + for height := startHeight; height <= endHeight; height++ { + blockResult, err := tmDB.stateStore.LoadABCIResponses(height) + if err != nil { + return err + } + + txIndex := findUnluckyTx(blockResult) + if txIndex < 0 { + // no unlucky tx in the block + continue + } + + result, err := tmDB.replayTx(anApp, height, txIndex, state.InitialHeight) + if err != nil { + return err + } + + if err := tmDB.patchDB(blockResult, result); err != nil { + return err + } + + // decode the tx to get eth tx hashes to log + tx, err := clientCtx.TxConfig.TxDecoder()(result.Tx) + if err != nil { + fmt.Println("can't parse the patched tx", result.Height, result.Index) + } + for _, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if ok { + fmt.Println("patched", ethMsg.Hash, result.Height, result.Index) + } + } + } + return nil + }, + } + + return cmd +} + +func openAppDB(rootDir string) (dbm.DB, error) { + dataDir := filepath.Join(rootDir, "data") + return sdk.NewLevelDB("application", dataDir) +} + +type tmDB struct { + blockStore *tmstore.BlockStore + stateStore sm.Store + txIndexer txindex.TxIndexer +} + +func openTMDB(cfg *tmcfg.Config) (*tmDB, error) { + // open tendermint db + tmdb, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "blockstore", Config: cfg}) + if err != nil { + return nil, err + } + blockStore := tmstore.NewBlockStore(tmdb) + + stateDB, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "state", Config: cfg}) + if err != nil { + return nil, err + } + stateStore := sm.NewStore(stateDB) + + store, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "tx_index", Config: cfg}) + if err != nil { + return nil, err + } + txIndexer := kv.NewTxIndex(store) + + return &tmDB{ + blockStore, stateStore, txIndexer, + }, nil +} + +func findUnluckyTx(blockResult *tmstate.ABCIResponses) int { + for txIndex, txResult := range blockResult.DeliverTxs { + if rpc.TxExceedsBlockGasLimit(txResult) { + return txIndex + } + } + return -1 +} + +func (db *tmDB) replayTx(anApp *app.App, height int64, txIndex int, initialHeight int64) (*abci.TxResult, error) { + // replay the tx and patch tx result + // replay and patch block + block := db.blockStore.LoadBlock(height) + if block == nil { + return nil, fmt.Errorf("block %d not found", height) + } + if err := anApp.LoadHeight(block.Header.Height - 1); err != nil { + return nil, err + } + + pbh := block.Header.ToProto() + if pbh == nil { + return nil, errors.New("nil header") + } + + byzVals := make([]abci.Evidence, 0) + for _, evidence := range block.Evidence.Evidence { + byzVals = append(byzVals, evidence.ABCI()...) + } + + commitInfo := getBeginBlockValidatorInfo(block, db.stateStore, initialHeight) + + _ = anApp.BeginBlock(abci.RequestBeginBlock{ + Hash: block.Hash(), + Header: *pbh, + ByzantineValidators: byzVals, + LastCommitInfo: commitInfo, + }) + + // replay tx with infinit block gas meter, before v0.7.0 upgrade those unlucky txs are committed succesfully. + anApp.WithBlockGasMeter(sdk.NewInfiniteGasMeter()) + + // run txs of block + for _, tx := range block.Txs[:txIndex] { + anApp.DeliverTx(abci.RequestDeliverTx{Tx: tx}) + } + + rsp := anApp.DeliverTx(abci.RequestDeliverTx{Tx: block.Txs[txIndex]}) + return &abci.TxResult{ + Height: block.Header.Height, + Index: uint32(txIndex), + Tx: block.Txs[txIndex], + Result: rsp, + }, nil +} + +func (db *tmDB) patchDB(blockResult *tmstate.ABCIResponses, result *abci.TxResult) error { + if err := db.txIndexer.Index(result); err != nil { + return err + } + blockResult.DeliverTxs[result.Index] = &result.Result + if err := db.stateStore.SaveABCIResponses(result.Height, blockResult); err != nil { + return err + } + return nil +} + +func getBeginBlockValidatorInfo(block *tmtypes.Block, store sm.Store, + initialHeight int64) abci.LastCommitInfo { + voteInfos := make([]abci.VoteInfo, block.LastCommit.Size()) + // Initial block -> LastCommitInfo.Votes are empty. + // Remember that the first LastCommit is intentionally empty, so it makes + // sense for LastCommitInfo.Votes to also be empty. + if block.Height > initialHeight { + lastValSet, err := store.LoadValidators(block.Height - 1) + if err != nil { + panic(err) + } + + // Sanity check that commit size matches validator set size - only applies + // after first block. + var ( + commitSize = block.LastCommit.Size() + valSetLen = len(lastValSet.Validators) + ) + if commitSize != valSetLen { + panic(fmt.Sprintf( + "commit size (%d) doesn't match valset length (%d) at height %d\n\n%v\n\n%v", + commitSize, valSetLen, block.Height, block.LastCommit.Signatures, lastValSet.Validators, + )) + } + + for i, val := range lastValSet.Validators { + commitSig := block.LastCommit.Signatures[i] + voteInfos[i] = abci.VoteInfo{ + Validator: types.TM2PB.Validator(val), + SignedLastBlock: !commitSig.Absent(), + } + } + } + + return abci.LastCommitInfo{ + Round: block.LastCommit.Round, + Votes: voteInfos, + } +} diff --git a/cmd/cronosd/cmd/root.go b/cmd/cronosd/cmd/root.go index 47f71fb02c..1683785ae9 100644 --- a/cmd/cronosd/cmd/root.go +++ b/cmd/cronosd/cmd/root.go @@ -136,6 +136,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { // add rosetta rootCmd.AddCommand(server.RosettaCommand(encodingConfig.InterfaceRegistry, encodingConfig.Marshaler)) + rootCmd.AddCommand(FixUnluckyTxCmd()) } func addModuleInitFlags(startCmd *cobra.Command) { diff --git a/go.mod b/go.mod index 8fa597f435..6a3fb9a520 100644 --- a/go.mod +++ b/go.mod @@ -162,9 +162,9 @@ replace github.com/cosmos/iavl => github.com/cosmos/iavl v0.17.3 replace github.com/ethereum/go-ethereum => github.com/crypto-org-chain/go-ethereum v1.10.3-patched // TODO: remove when ibc-go and ethermint upgrades cosmos-sdk -replace github.com/cosmos/cosmos-sdk => github.com/crypto-org-chain/cosmos-sdk v0.44.6-cronos-revert +replace github.com/cosmos/cosmos-sdk => github.com/yihuang/cosmos-sdk v0.44.6-cronos-revert.0.20220506005923-c996096695d0 -replace github.com/tharsis/ethermint => github.com/yihuang/ethermint v0.7.2-cronos-9.0.20220427040028-1d16a8af7dfc +replace github.com/tharsis/ethermint => github.com/crypto-org-chain/ethermint v0.7.2-cronos-15 // Note: gorocksdb bindings for OptimisticTransactionDB are not merged upstream, so we use a fork // See https://github.com/tecbot/gorocksdb/pull/216 diff --git a/go.sum b/go.sum index 9d8cd85152..acab468e7b 100644 --- a/go.sum +++ b/go.sum @@ -234,8 +234,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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/cosmos-sdk v0.44.6-cronos-revert h1:HpX2ZybTeAzUSr5wh4oTc3sDtdz/9TH5lKB9BqOVcHE= -github.com/crypto-org-chain/cosmos-sdk v0.44.6-cronos-revert/go.mod h1:Il2o2AOea3kDxkOp7wcVr9btR2Ns4opBi7NQrgV0jUo= +github.com/crypto-org-chain/ethermint v0.7.2-cronos-15 h1:O1ZFh8cSa/7lBLqwZOatqSX4EhbW7dzaNhxMdD3gCb0= +github.com/crypto-org-chain/ethermint v0.7.2-cronos-15/go.mod h1:J96LX4KvLyl+5jV6+mt/4l6srtGX/mdDTuqQQuYrdDk= github.com/crypto-org-chain/go-ethereum v1.10.3-patched h1:kr6oQIYOi2VC8SZwkhlUDZE1Omit/YHVysKMgCB2nes= github.com/crypto-org-chain/go-ethereum v1.10.3-patched/go.mod h1:99onQmSd1GRGOziyGldI41YQb7EESX3Q4H41IfJgIQQ= github.com/crypto-org-chain/ibc-go v1.2.1-hooks h1:wuWaQqm/TFKJQwuFgjCPiPumQio+Yik5Z1DObDExrrU= @@ -1024,8 +1024,8 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/xtaci/kcp-go v5.4.20+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= github.com/ybbus/jsonrpc v2.1.2+incompatible/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= -github.com/yihuang/ethermint v0.7.2-cronos-9.0.20220427040028-1d16a8af7dfc h1:GrArNwTT2sxt+Chp2nGydAjWtiFqm/Y7thcm6IM+Mlk= -github.com/yihuang/ethermint v0.7.2-cronos-9.0.20220427040028-1d16a8af7dfc/go.mod h1:J96LX4KvLyl+5jV6+mt/4l6srtGX/mdDTuqQQuYrdDk= +github.com/yihuang/cosmos-sdk v0.44.6-cronos-revert.0.20220506005923-c996096695d0 h1:SO302qrfzmErLNyBipyOzolKeCTS1OAGkUa9fFTRzPM= +github.com/yihuang/cosmos-sdk v0.44.6-cronos-revert.0.20220506005923-c996096695d0/go.mod h1:Il2o2AOea3kDxkOp7wcVr9btR2Ns4opBi7NQrgV0jUo= 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/gomod2nix.toml b/gomod2nix.toml index 83885f29b3..881f03327d 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -950,13 +950,13 @@ sha256 = "0nxbn0m7lr4dg0yrwnvlkfiyg3ndv8vdpssjx7b714nivpc6ar0y" ["github.com/cosmos/cosmos-sdk"] - sumVersion = "v0.44.6-cronos-revert" - vendorPath = "github.com/crypto-org-chain/cosmos-sdk" + sumVersion = "v0.44.6-cronos-revert.0.20220506005923-c996096695d0" + vendorPath = "github.com/yihuang/cosmos-sdk" ["github.com/cosmos/cosmos-sdk".fetch] type = "git" - url = "https://github.com/crypto-org-chain/cosmos-sdk" - rev = "0bb0c08fb303e20b70d17e85401d02c08f8fdbfd" - sha256 = "0lnhfx2q8zq9b00pp4rgh8zf68j1y07grz6lgky94j80qrmjzzpi" + url = "https://github.com/yihuang/cosmos-sdk" + rev = "c996096695d04bb847994689b0e5b58d637f6b64" + sha256 = "19llii0jpsg8fa035q5wgjbb4lr270v5ywpscrlvnfjqmpk8263n" ["github.com/cosmos/go-bip39"] sumVersion = "v1.0.0" @@ -3705,11 +3705,11 @@ sha256 = "1sgjf2vaq554ybc0cwkzn17cz2ibzph2rq0dgaw21c2hym09437x" ["github.com/tharsis/ethermint"] - sumVersion = "v0.7.2-cronos-9.0.20220427040028-1d16a8af7dfc" - vendorPath = "github.com/yihuang/ethermint" + sumVersion = "v0.7.2-cronos-15" + vendorPath = "github.com/crypto-org-chain/ethermint" ["github.com/tharsis/ethermint".fetch] type = "git" - url = "https://github.com/yihuang/ethermint" + url = "https://github.com/crypto-org-chain/ethermint" rev = "1d16a8af7dfc173d872f3ed439219421151e2d01" sha256 = "0h6nk3brk98pi7snn6dpj02l6kcp77xdf8w4v6xia1wsj065rgsx" diff --git a/integration_tests/cosmoscli.py b/integration_tests/cosmoscli.py index a68b2b53f1..28146112ec 100644 --- a/integration_tests/cosmoscli.py +++ b/integration_tests/cosmoscli.py @@ -2,6 +2,7 @@ import hashlib import json import tempfile +from collections import namedtuple import bech32 from dateutil.parser import isoparse @@ -1012,3 +1013,13 @@ def update_token_mapping(self, denom, contract, **kwargs): **kwargs, ) ) + + def fix_tx_result(self, begin_block, end_block): + output = self.raw( + "fix-tx-result", + begin_block, + end_block, + home=self.data_dir, + ) + Item = namedtuple("hash height index") + return [Item(*line.split()[1:]) for line in output.split("\n")] diff --git a/integration_tests/test_replay_block.py b/integration_tests/test_replay_block.py index cba0f6587c..7970e44c8b 100644 --- a/integration_tests/test_replay_block.py +++ b/integration_tests/test_replay_block.py @@ -2,11 +2,19 @@ import pytest import web3 +from pystarport import ports from web3._utils.method_formatters import receipt_formatter from web3.datastructures import AttributeDict from .network import setup_custom_cronos -from .utils import ADDRS, KEYS, deploy_contract, sign_transaction +from .utils import ( + ADDRS, + KEYS, + deploy_contract, + sign_transaction, + supervisorctl, + wait_for_port, +) @pytest.fixture(scope="module") @@ -19,6 +27,8 @@ def custom_cronos(tmp_path_factory): def test_replay_block(custom_cronos): w3: web3.Web3 = custom_cronos.w3 + cli = custom_cronos.cosmos_cli() + begin_height = cli.block_height() contract = deploy_contract( w3, Path(__file__).parent @@ -93,3 +103,25 @@ def test_replay_block(custom_cronos): replay_receipts = [AttributeDict(receipt_formatter(item)) for item in rsp["result"]] assert replay_receipts[1].status == 0 assert replay_receipts[1].gasUsed == gas_limit + + # check the preUpgrade mode, should be the same as the receipt after patch. + replay_receipts = w3.provider.make_request( + "cronos_replayBlock", [hex(receipt1.blockNumber), False] + )["result"] + + # patch the unlucky tx with the new cli command + # stop the node0 + end_height = cli.block_height() + supervisorctl(custom_cronos.base_dir / "../tasks.ini", "stop", "cronos_777-1-node0") + results = custom_cronos.cosmos_cli().fix_tx_result(begin_height, end_height) + # the second tx is patched + assert results[0].hash == txhashes[1] + # start the node0 again + supervisorctl( + custom_cronos.base_dir / "../tasks.ini", "start", "cronos_777-1-node0" + ) + # wait for json-rpc port + wait_for_port(ports.evmrpc_port(custom_cronos.base_port(0))) + w3: web3.Web3 = custom_cronos.w3 + receipt = w3.eth.get_transaction_receipt(txhashes[1]) + assert receipt == replay_receipts[1] diff --git a/scripts/devnet.yaml b/scripts/devnet.yaml index dfc5c207a3..bb85366e04 100644 --- a/scripts/devnet.yaml +++ b/scripts/devnet.yaml @@ -5,6 +5,8 @@ cronos_777-1: json-rpc: address: "0.0.0.0:{EVMRPC_PORT}" ws-address: "0.0.0.0:{EVMRPC_PORT_WS}" + evm: + max-tx-gas-wanted: 0 validators: - coins: 1000000000000000000stake,1000000000000000000basetcro staked: 1000000000000000000stake @@ -17,6 +19,10 @@ cronos_777-1: coins: 10000000000000000000000basetcro mnemonic: "notable error gospel wave pair ugly measure elite toddler cost various fly make eye ketchup despair slab throw tribe swarm word fruit into inmate" genesis: + consensus_params: + block: + max_bytes: "1048576" + max_gas: "800000" app_state: evm: params: diff --git a/x/cronos/rpc/api.go b/x/cronos/rpc/api.go index 9ed258dd0a..1d45cf7f84 100644 --- a/x/cronos/rpc/api.go +++ b/x/cronos/rpc/api.go @@ -14,6 +14,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rpc" + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" coretypes "github.com/tendermint/tendermint/rpc/core/types" rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" @@ -123,16 +124,14 @@ func (api *CronosAPI) ReplayBlock(blockNrOrHash rpctypes.BlockNumberOrHash, post var msgs []*evmtypes.MsgEthereumTx for i, tx := range resBlock.Block.Txs { txResult := blockRes.TxsResults[i] - if txResult.Code != 0 { - if strings.Contains(txResult.Log, ExceedBlockGasLimitError) { - // the tx with ExceedBlockGasLimitErrorPrefix error should not be ignored because: - // 1) before the 0.7.0 upgrade, the tx is committed successfully. - // 2) after the upgrade, the tx is failed but fee deducted and nonce increased. - // there's at most one such case in each block, and it should be the last tx in the block. - blockGasLimitExceeded = true - } else { - continue - } + if TxExceedsBlockGasLimit(txResult) { + // the tx with ExceedBlockGasLimitErrorPrefix error should not be ignored because: + // 1) before the 0.7.0 upgrade, the tx is committed successfully. + // 2) after the upgrade, the tx is failed but fee deducted and nonce increased. + // there's at most one such case in each block, and it should be the last tx in the block. + blockGasLimitExceeded = true + } else if txResult.Code != 0 { + continue } tx, err := api.clientCtx.TxConfig.TxDecoder()(tx) @@ -268,3 +267,8 @@ func (api *CronosAPI) getBlockNumber(blockNrOrHash rpctypes.BlockNumberOrHash) ( return rpctypes.EthEarliestBlockNumber, nil } } + +// TxExceedsBlockGasLimit returns true if tx's execution exceeds block gas limit +func TxExceedsBlockGasLimit(result *abci.ResponseDeliverTx) bool { + return result.Code == 11 && strings.Contains(result.Log, ExceedBlockGasLimitError) +}