From 39bca2350d1774fb0ed5e22c4a137a9bf033b880 Mon Sep 17 00:00:00 2001 From: kevinssgh <79858682+kevinssgh@users.noreply.github.com> Date: Thu, 21 Mar 2024 09:55:18 -0400 Subject: [PATCH] feat: added new cmd "zetatool" and sub cmd filterdeposits (#1884) * re-organized zetatool folder * added changelog * fix lint and gosec errors * add unit tests for config package * fix some gosec issues * ran make generate * added comments * added tests for filterdeposit.go * added targets and docs * addressed comments * removed sensitive data from sample config * ran make generate and lint * added test case * fix import format * update docs * update docs --------- Co-authored-by: Lucas Bertrand --- Makefile | 19 +- changelog.md | 1 + cmd/zetatool/config/config.go | 71 ++++++ cmd/zetatool/config/config_test.go | 77 ++++++ cmd/zetatool/filterdeposit/btc.go | 188 ++++++++++++++ cmd/zetatool/filterdeposit/evm.go | 240 ++++++++++++++++++ cmd/zetatool/filterdeposit/filterdeposit.go | 122 +++++++++ .../filterdeposit/filterdeposit_test.go | 97 +++++++ cmd/zetatool/main.go | 28 ++ docs/cli/zetatool/filterdeposit.md | 29 +++ docs/cli/zetatool/readme.md | 55 ++++ go.mod | 1 + go.sum | 2 + .../zetatool_config.json | 8 + 14 files changed, 937 insertions(+), 1 deletion(-) create mode 100644 cmd/zetatool/config/config.go create mode 100644 cmd/zetatool/config/config_test.go create mode 100644 cmd/zetatool/filterdeposit/btc.go create mode 100644 cmd/zetatool/filterdeposit/evm.go create mode 100644 cmd/zetatool/filterdeposit/filterdeposit.go create mode 100644 cmd/zetatool/filterdeposit/filterdeposit_test.go create mode 100644 cmd/zetatool/main.go create mode 100644 docs/cli/zetatool/filterdeposit.md create mode 100644 docs/cli/zetatool/readme.md create mode 100644 tool/filter_missed_deposits/zetatool_config.json diff --git a/Makefile b/Makefile index b5cf22b39f..9a4ccfe3fa 100644 --- a/Makefile +++ b/Makefile @@ -98,6 +98,10 @@ install-zetaclient-race-test-only-build: go.sum @echo "--> Installing zetaclientd" @go install -race -mod=readonly $(BUILD_FLAGS) ./cmd/zetaclientd +install-zetatool: go.sum + @echo "--> Installing zetatool" + @go install -mod=readonly $(BUILD_FLAGS) ./cmd/zetatool + ############################################################################### ### Local network ### ############################################################################### @@ -286,4 +290,17 @@ mainnet-bitcoind-node: cd contrib/mainnet/bitcoind && DOCKER_TAG=$(DOCKER_TAG) docker-compose up athens3-zetarpc-node: - cd contrib/athens3/zetacored && DOCKER_TAG=$(DOCKER_TAG) docker-compose up \ No newline at end of file + cd contrib/athens3/zetacored && DOCKER_TAG=$(DOCKER_TAG) docker-compose up + +############################################################################### +### Debug Tools ### +############################################################################### + +filter-missed-btc: install-zetatool + zetatool filterdeposit btc --config ./tool/filter_missed_deposits/zetatool_config.json + +filter-missed-eth: install-zetatool + zetatool filterdeposit eth \ + --config ./tool/filter_missed_deposits/zetatool_config.json \ + --evm-max-range 1000 \ + --evm-start-block 19464041 \ No newline at end of file diff --git a/changelog.md b/changelog.md index a71f368313..774fec193e 100644 --- a/changelog.md +++ b/changelog.md @@ -25,6 +25,7 @@ * [1789](https://github.com/zeta-chain/node/issues/1789) - block cross-chain transactions that involve restricted addresses * [1755](https://github.com/zeta-chain/node/issues/1755) - use evm JSON RPC for inbound tx (including blob tx) observation. * [1815](https://github.com/zeta-chain/node/pull/1815) - add authority module for authorized actions +* [1884](https://github.com/zeta-chain/node/pull/1884) - added zetatool cmd, added subcommand to filter deposits ### Tests diff --git a/cmd/zetatool/config/config.go b/cmd/zetatool/config/config.go new file mode 100644 index 0000000000..6f3face04d --- /dev/null +++ b/cmd/zetatool/config/config.go @@ -0,0 +1,71 @@ +package config + +import ( + "encoding/json" + + "github.com/spf13/afero" +) + +var AppFs = afero.NewOsFs() + +const ( + FlagConfig = "config" + defaultCfgFileName = "zetatool_config.json" + ZetaURL = "127.0.0.1:1317" + BtcExplorerURL = "https://blockstream.info/api/" + EthRPCURL = "https://ethereum-rpc.publicnode.com" + ConnectorAddress = "0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a" + CustodyAddress = "0x0000030Ec64DF25301d8414eE5a29588C4B0dE10" +) + +// Config is a struct the defines the configuration fields used by zetatool +type Config struct { + ZetaURL string + BtcExplorerURL string + EthRPCURL string + EtherscanAPIkey string + ConnectorAddress string + CustodyAddress string +} + +func DefaultConfig() *Config { + return &Config{ + ZetaURL: ZetaURL, + BtcExplorerURL: BtcExplorerURL, + EthRPCURL: EthRPCURL, + ConnectorAddress: ConnectorAddress, + CustodyAddress: CustodyAddress, + } +} + +func (c *Config) Save() error { + file, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + err = afero.WriteFile(AppFs, defaultCfgFileName, file, 0600) + return err +} + +func (c *Config) Read(filename string) error { + data, err := afero.ReadFile(AppFs, filename) + if err != nil { + return err + } + err = json.Unmarshal(data, c) + return err +} + +func GetConfig(filename string) (*Config, error) { + //Check if cfgFile is empty, if so return default Config and save to file + if filename == "" { + cfg := DefaultConfig() + err := cfg.Save() + return cfg, err + } + + //if file is specified, open file and return struct + cfg := &Config{} + err := cfg.Read(filename) + return cfg, err +} diff --git a/cmd/zetatool/config/config_test.go b/cmd/zetatool/config/config_test.go new file mode 100644 index 0000000000..dd56604d5f --- /dev/null +++ b/cmd/zetatool/config/config_test.go @@ -0,0 +1,77 @@ +package config + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func TestDefaultConfig(t *testing.T) { + cfg := DefaultConfig() + require.Equal(t, cfg.EthRPCURL, EthRPCURL) + require.Equal(t, cfg.ZetaURL, ZetaURL) + require.Equal(t, cfg.BtcExplorerURL, BtcExplorerURL) + require.Equal(t, cfg.ConnectorAddress, ConnectorAddress) + require.Equal(t, cfg.CustodyAddress, CustodyAddress) +} + +func TestGetConfig(t *testing.T) { + AppFs = afero.NewMemMapFs() + defaultCfg := DefaultConfig() + + t.Run("No config file specified", func(t *testing.T) { + cfg, err := GetConfig("") + require.NoError(t, err) + require.Equal(t, cfg, defaultCfg) + + exists, err := afero.Exists(AppFs, defaultCfgFileName) + require.NoError(t, err) + require.True(t, exists) + }) + + t.Run("config file specified", func(t *testing.T) { + cfg, err := GetConfig(defaultCfgFileName) + require.NoError(t, err) + require.Equal(t, cfg, defaultCfg) + }) +} + +func TestConfig_Read(t *testing.T) { + AppFs = afero.NewMemMapFs() + cfg, err := GetConfig("") + require.NoError(t, err) + + t.Run("read existing file", func(t *testing.T) { + c := &Config{} + err := c.Read(defaultCfgFileName) + require.NoError(t, err) + require.Equal(t, c, cfg) + }) + + t.Run("read non-existent file", func(t *testing.T) { + err := AppFs.Remove(defaultCfgFileName) + require.NoError(t, err) + c := &Config{} + err = c.Read(defaultCfgFileName) + require.ErrorContains(t, err, "file does not exist") + require.NotEqual(t, c, cfg) + }) +} + +func TestConfig_Save(t *testing.T) { + AppFs = afero.NewMemMapFs() + cfg := DefaultConfig() + cfg.EtherscanAPIkey = "DIFFERENTAPIKEY" + + t.Run("save modified cfg", func(t *testing.T) { + err := cfg.Save() + require.NoError(t, err) + + newCfg, err := GetConfig(defaultCfgFileName) + require.NoError(t, err) + require.Equal(t, cfg, newCfg) + }) + + // Should test invalid json encoding but currently not able to without interface +} diff --git a/cmd/zetatool/filterdeposit/btc.go b/cmd/zetatool/filterdeposit/btc.go new file mode 100644 index 0000000000..bc2397523d --- /dev/null +++ b/cmd/zetatool/filterdeposit/btc.go @@ -0,0 +1,188 @@ +package filterdeposit + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/zeta-chain/zetacore/cmd/zetatool/config" + "github.com/zeta-chain/zetacore/common" +) + +func NewBtcCmd() *cobra.Command { + return &cobra.Command{ + Use: "btc", + Short: "Filter inbound btc deposits", + RunE: FilterBTCTransactions, + } +} + +// FilterBTCTransactions is a command that queries the bitcoin explorer for inbound transactions that qualify for +// cross chain transactions. +func FilterBTCTransactions(cmd *cobra.Command, _ []string) error { + configFile, err := cmd.Flags().GetString(config.FlagConfig) + fmt.Println("config file name: ", configFile) + if err != nil { + return err + } + btcChainID, err := cmd.Flags().GetString(BTCChainIDFlag) + if err != nil { + return err + } + cfg, err := config.GetConfig(configFile) + if err != nil { + return err + } + fmt.Println("getting tss Address") + res, err := GetTssAddress(cfg, btcChainID) + if err != nil { + return err + } + fmt.Println("got tss Address") + list, err := getHashList(cfg, res.Btc) + if err != nil { + return err + } + + _, err = CheckForCCTX(list, cfg) + return err +} + +// getHashList is called by FilterBTCTransactions to help query and filter inbound transactions on btc +func getHashList(cfg *config.Config, tssAddress string) ([]Deposit, error) { + var list []Deposit + lastHash := "" + + // Setup URL for query + btcURL, err := url.JoinPath(cfg.BtcExplorerURL, "address", tssAddress, "txs") + if err != nil { + return list, err + } + + // This loop will query the bitcoin explorer for transactions associated with the TSS address. Since the api only + // allows a response of 25 transactions per request, several requests will be required in order to retrieve a + // complete list. + for { + // The Next Query is determined by the last transaction hash provided by the previous response. + nextQuery := btcURL + if lastHash != "" { + nextQuery, err = url.JoinPath(btcURL, "chain", lastHash) + if err != nil { + return list, err + } + } + // #nosec G107 url must be variable + res, getErr := http.Get(nextQuery) + if getErr != nil { + return list, getErr + } + + body, readErr := ioutil.ReadAll(res.Body) + if readErr != nil { + return list, readErr + } + closeErr := res.Body.Close() + if closeErr != nil { + return list, closeErr + } + + // NOTE: decoding json from request dynamically is not ideal, however there isn't a detailed, defined data structure + // provided by blockstream. Will need to create one in the future using following definition: + // https://github.com/Blockstream/esplora/blob/master/API.md#transaction-format + var txns []map[string]interface{} + err := json.Unmarshal(body, &txns) + if err != nil { + return list, err + } + + if len(txns) == 0 { + break + } + + fmt.Println("Length of txns: ", len(txns)) + + // The "/address" blockstream api provides a maximum of 25 transactions associated with a given address. This + // loop will iterate over that list of transactions to determine whether each transaction can be considered + // a deposit to ZetaChain. + for _, txn := range txns { + // Get tx hash of the current transaction + hash := txn["txid"].(string) + + // Read the first output of the transaction and parse the destination address. + // This address should be the TSS address. + vout := txn["vout"].([]interface{}) + vout0 := vout[0].(map[string]interface{}) + var vout1 map[string]interface{} + if len(vout) > 1 { + vout1 = vout[1].(map[string]interface{}) + } else { + continue + } + _, found := vout0["scriptpubkey"] + scriptpubkey := "" + if found { + scriptpubkey = vout0["scriptpubkey"].(string) + } + _, found = vout0["scriptpubkey_address"] + targetAddr := "" + if found { + targetAddr = vout0["scriptpubkey_address"].(string) + } + + //Check if txn is confirmed + status := txn["status"].(map[string]interface{}) + confirmed := status["confirmed"].(bool) + if !confirmed { + continue + } + + //Filter out deposits less than min base fee + if vout0["value"].(float64) < 1360 { + continue + } + + //Check if Deposit is a donation + scriptpubkey1 := vout1["scriptpubkey"].(string) + if len(scriptpubkey1) >= 4 && scriptpubkey1[:2] == "6a" { + memoSize, err := strconv.ParseInt(scriptpubkey1[2:4], 16, 32) + if err != nil { + continue + } + if int(memoSize) != (len(scriptpubkey1)-4)/2 { + continue + } + memoBytes, err := hex.DecodeString(scriptpubkey1[4:]) + if err != nil { + continue + } + if bytes.Equal(memoBytes, []byte(common.DonationMessage)) { + continue + } + } else { + continue + } + + //Make sure Deposit is sent to correct tss address + if strings.Compare("0014", scriptpubkey[:4]) == 0 && targetAddr == tssAddress { + entry := Deposit{ + hash, + // #nosec G701 parsing json requires float64 type from blockstream + uint64(vout0["value"].(float64)), + } + list = append(list, entry) + } + } + + lastTxn := txns[len(txns)-1] + lastHash = lastTxn["txid"].(string) + } + + return list, nil +} diff --git a/cmd/zetatool/filterdeposit/evm.go b/cmd/zetatool/filterdeposit/evm.go new file mode 100644 index 0000000000..73e1f8b78f --- /dev/null +++ b/cmd/zetatool/filterdeposit/evm.go @@ -0,0 +1,240 @@ +package filterdeposit + +import ( + "context" + "fmt" + "log" + "math/big" + "strings" + + "github.com/zeta-chain/zetacore/zetaclient/evm" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/nanmu42/etherscan-api" + "github.com/spf13/cobra" + "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" + "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" + "github.com/zeta-chain/zetacore/cmd/zetatool/config" + zetacommon "github.com/zeta-chain/zetacore/common" +) + +const ( + EvmMaxRangeFlag = "evm-max-range" + EvmStartBlockFlag = "evm-start-block" +) + +func NewEvmCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "eth", + Short: "Filter inbound eth deposits", + RunE: FilterEVMTransactions, + } + + cmd.Flags().Uint64(EvmMaxRangeFlag, 1000, "number of blocks to scan per iteration") + cmd.Flags().Uint64(EvmStartBlockFlag, 19463725, "block height to start scanning from") + + return cmd +} + +// FilterEVMTransactions is a command that queries an EVM explorer and Contracts for inbound transactions that qualify +// for cross chain transactions. +func FilterEVMTransactions(cmd *cobra.Command, _ []string) error { + // Get flags + configFile, err := cmd.Flags().GetString(config.FlagConfig) + if err != nil { + return err + } + startBlock, err := cmd.Flags().GetUint64(EvmStartBlockFlag) + if err != nil { + return err + } + blockRange, err := cmd.Flags().GetUint64(EvmMaxRangeFlag) + if err != nil { + return err + } + btcChainID, err := cmd.Flags().GetString(BTCChainIDFlag) + if err != nil { + return err + } + // Scan for deposits + cfg, err := config.GetConfig(configFile) + if err != nil { + log.Fatal(err) + } + res, err := GetTssAddress(cfg, btcChainID) + if err != nil { + return err + } + list, err := GetEthHashList(cfg, res.Eth, startBlock, blockRange) + if err != nil { + return err + } + _, err = CheckForCCTX(list, cfg) + return err +} + +// GetEthHashList is a helper function querying total inbound txns by segments of blocks in ranges defined by the config +func GetEthHashList(cfg *config.Config, tssAddress string, startBlock uint64, blockRange uint64) ([]Deposit, error) { + client, err := ethclient.Dial(cfg.EthRPCURL) + if err != nil { + return []Deposit{}, err + } + fmt.Println("Connection successful") + + header, err := client.HeaderByNumber(context.Background(), nil) + if err != nil { + return []Deposit{}, err + } + latestBlock := header.Number.Uint64() + fmt.Println("latest Block: ", latestBlock) + + endBlock := startBlock + blockRange + deposits := make([]Deposit, 0) + segment := 0 + for startBlock < latestBlock { + fmt.Printf("adding segment: %d, startblock: %d\n", segment, startBlock) + segmentRes, err := GetHashListSegment(client, startBlock, endBlock, tssAddress, cfg) + if err != nil { + fmt.Println(err.Error()) + continue + } + deposits = append(deposits, segmentRes...) + startBlock = endBlock + endBlock = endBlock + blockRange + if endBlock > latestBlock { + endBlock = latestBlock + } + segment++ + } + return deposits, nil +} + +// GetHashListSegment queries and filters deposits for a given range +func GetHashListSegment( + client *ethclient.Client, + startBlock uint64, + endBlock uint64, + tssAddress string, + cfg *config.Config) ([]Deposit, error) { + + deposits := make([]Deposit, 0) + connectorAddress := common.HexToAddress(cfg.ConnectorAddress) + connectorContract, err := zetaconnector.NewZetaConnectorNonEth(connectorAddress, client) + if err != nil { + return deposits, err + } + erc20CustodyAddress := common.HexToAddress(cfg.CustodyAddress) + erc20CustodyContract, err := erc20custody.NewERC20Custody(erc20CustodyAddress, client) + if err != nil { + return deposits, err + } + + custodyIter, err := erc20CustodyContract.FilterDeposited(&bind.FilterOpts{ + Start: startBlock, + End: &endBlock, + Context: context.TODO(), + }, []common.Address{}) + if err != nil { + return deposits, err + } + + connectorIter, err := connectorContract.FilterZetaSent(&bind.FilterOpts{ + Start: startBlock, + End: &endBlock, + Context: context.TODO(), + }, []common.Address{}, []*big.Int{}) + if err != nil { + return deposits, err + } + + // Get ERC20 Custody Deposit events + for custodyIter.Next() { + // sanity check tx event + err := CheckEvmTxLog(&custodyIter.Event.Raw, erc20CustodyAddress, "", evm.TopicsDeposited) + if err == nil { + deposits = append(deposits, Deposit{ + TxID: custodyIter.Event.Raw.TxHash.Hex(), + Amount: custodyIter.Event.Amount.Uint64(), + }) + } + } + + // Get Connector ZetaSent events + for connectorIter.Next() { + // sanity check tx event + err := CheckEvmTxLog(&connectorIter.Event.Raw, connectorAddress, "", evm.TopicsZetaSent) + if err == nil { + deposits = append(deposits, Deposit{ + TxID: connectorIter.Event.Raw.TxHash.Hex(), + Amount: connectorIter.Event.ZetaValueAndGas.Uint64(), + }) + } + } + + // Get Transactions sent directly to TSS address + tssDeposits, err := getTSSDeposits(tssAddress, startBlock, endBlock, cfg.EtherscanAPIkey) + if err != nil { + return deposits, err + } + deposits = append(deposits, tssDeposits...) + + return deposits, nil +} + +// getTSSDeposits more specifically queries and filters deposits based on direct transfers the TSS address. +func getTSSDeposits(tssAddress string, startBlock uint64, endBlock uint64, apiKey string) ([]Deposit, error) { + client := etherscan.New(etherscan.Mainnet, apiKey) + deposits := make([]Deposit, 0) + + // #nosec G701 these block numbers need to be *int for this particular client package + startInt := int(startBlock) + // #nosec G701 + endInt := int(endBlock) + txns, err := client.NormalTxByAddress(tssAddress, &startInt, &endInt, 0, 0, true) + if err != nil { + return deposits, err + } + + fmt.Println("getTSSDeposits - Num of transactions: ", len(txns)) + + for _, tx := range txns { + if tx.To == tssAddress { + if strings.Compare(tx.Input, zetacommon.DonationMessage) == 0 { + continue // skip donation tx + } + if tx.TxReceiptStatus != "1" { + continue + } + //fmt.Println("getTSSDeposits - adding Deposit") + deposits = append(deposits, Deposit{ + TxID: tx.Hash, + Amount: tx.Value.Int().Uint64(), + }) + } + } + + return deposits, nil +} + +// CheckEvmTxLog is a helper function used to validate receipts, logic is taken from zetaclient. +func CheckEvmTxLog(vLog *ethtypes.Log, wantAddress common.Address, wantHash string, wantTopics int) error { + if vLog.Removed { + return fmt.Errorf("log is removed, chain reorg?") + } + if vLog.Address != wantAddress { + return fmt.Errorf("log emitter address mismatch: want %s got %s", wantAddress.Hex(), vLog.Address.Hex()) + } + if vLog.TxHash.Hex() == "" { + return fmt.Errorf("log tx hash is empty: %d %s", vLog.BlockNumber, vLog.TxHash.Hex()) + } + if wantHash != "" && vLog.TxHash.Hex() != wantHash { + return fmt.Errorf("log tx hash mismatch: want %s got %s", wantHash, vLog.TxHash.Hex()) + } + if len(vLog.Topics) != wantTopics { + return fmt.Errorf("number of topics mismatch: want %d got %d", wantTopics, len(vLog.Topics)) + } + return nil +} diff --git a/cmd/zetatool/filterdeposit/filterdeposit.go b/cmd/zetatool/filterdeposit/filterdeposit.go new file mode 100644 index 0000000000..73a2601cd8 --- /dev/null +++ b/cmd/zetatool/filterdeposit/filterdeposit.go @@ -0,0 +1,122 @@ +package filterdeposit + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/spf13/cobra" + "github.com/zeta-chain/zetacore/cmd/zetatool/config" + "github.com/zeta-chain/zetacore/x/observer/types" +) + +const ( + BTCChainIDFlag = "btc-chain-id" +) + +func NewFilterDepositCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "filterdeposit", + Short: "filter missing inbound deposits", + } + + cmd.AddCommand(NewBtcCmd()) + cmd.AddCommand(NewEvmCmd()) + + // Required for TSS address query + cmd.PersistentFlags().String(BTCChainIDFlag, "8332", "chain id used on zetachain to identify bitcoin - default: 8332") + + return cmd +} + +// Deposit is a data structure for keeping track of inbound transactions +type Deposit struct { + TxID string + Amount uint64 +} + +// CheckForCCTX is querying zeta core for a cctx associated with a confirmed transaction hash. If the cctx is not found, +// then the transaction hash is added to the list of missed inbound transactions. +func CheckForCCTX(list []Deposit, cfg *config.Config) ([]Deposit, error) { + var missedList []Deposit + + fmt.Println("Going through list, num of transactions: ", len(list)) + for _, entry := range list { + zetaURL, err := url.JoinPath(cfg.ZetaURL, "zeta-chain", "crosschain", "in_tx_hash_to_cctx_data", entry.TxID) + if err != nil { + return missedList, err + } + + request, err := http.NewRequest(http.MethodGet, zetaURL, nil) + if err != nil { + return missedList, err + } + request.Header.Add("Accept", "application/json") + client := &http.Client{} + + response, getErr := client.Do(request) + if getErr != nil { + return missedList, getErr + } + + data, readErr := ioutil.ReadAll(response.Body) + if readErr != nil { + return missedList, readErr + } + closeErr := response.Body.Close() + if closeErr != nil { + return missedList, closeErr + } + + var cctx map[string]interface{} + err = json.Unmarshal(data, &cctx) + if err != nil { + return missedList, err + } + + // successful query of the given cctx will not contain a "message" field with value "not found", if it was not + // found then it is added to the missing list. + if _, ok := cctx["message"]; ok { + if strings.Compare(cctx["message"].(string), "not found") == 0 { + missedList = append(missedList, entry) + } + } + } + + fmt.Printf("Found %d missed transactions.\n", len(missedList)) + for _, entry := range missedList { + fmt.Printf("%s, amount: %d\n", entry.TxID, entry.Amount) + } + return missedList, nil +} + +func GetTssAddress(cfg *config.Config, btcChainID string) (*types.QueryGetTssAddressResponse, error) { + res := &types.QueryGetTssAddressResponse{} + requestURL, err := url.JoinPath(cfg.ZetaURL, "zeta-chain", "observer", "get_tss_address", btcChainID) + if err != nil { + return res, err + } + request, err := http.NewRequest(http.MethodGet, requestURL, nil) + if err != nil { + return res, err + } + request.Header.Add("Accept", "application/json") + zetacoreHTTPClient := &http.Client{} + response, getErr := zetacoreHTTPClient.Do(request) + if getErr != nil { + return res, err + } + data, readErr := ioutil.ReadAll(response.Body) + if readErr != nil { + return res, err + } + closeErr := response.Body.Close() + if closeErr != nil { + return res, closeErr + } + err = json.Unmarshal(data, res) + return res, err +} diff --git a/cmd/zetatool/filterdeposit/filterdeposit_test.go b/cmd/zetatool/filterdeposit/filterdeposit_test.go new file mode 100644 index 0000000000..2140a43c08 --- /dev/null +++ b/cmd/zetatool/filterdeposit/filterdeposit_test.go @@ -0,0 +1,97 @@ +package filterdeposit + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/cmd/zetatool/config" + "github.com/zeta-chain/zetacore/x/crosschain/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" +) + +func TestCheckForCCTX(t *testing.T) { + t.Run("no missed inbound txns found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/zeta-chain/crosschain/in_tx_hash_to_cctx_data/0x093f4ca4c1884df0fd9dd59b75979342ded29d3c9b6861644287a2e1417b9a39" { + t.Errorf("Expected to request '/zeta-chain', got: %s", r.URL.Path) + } + w.WriteHeader(http.StatusOK) + //Return CCtx + cctx := types.CrossChainTx{} + bytes, err := json.Marshal(cctx) + require.NoError(t, err) + _, err = w.Write(bytes) + require.NoError(t, err) + })) + defer server.Close() + + deposits := []Deposit{{ + TxID: "0x093f4ca4c1884df0fd9dd59b75979342ded29d3c9b6861644287a2e1417b9a39", + Amount: uint64(657177295293237048), + }} + cfg := config.DefaultConfig() + cfg.ZetaURL = server.URL + missedInbounds, err := CheckForCCTX(deposits, cfg) + require.NoError(t, err) + require.Equal(t, 0, len(missedInbounds)) + }) + + t.Run("1 missed inbound txn found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("{\n \"code\": 5,\n \"message\": \"not found\",\n \"details\": [\n ]\n}")) + require.NoError(t, err) + })) + defer server.Close() + + deposits := []Deposit{{ + TxID: "0x093f4ca4c1884df0fd9dd59b75979342ded29d3c9b6861644287a2e1417b9a39", + Amount: uint64(657177295293237048), + }} + cfg := config.DefaultConfig() + cfg.ZetaURL = server.URL + missedInbounds, err := CheckForCCTX(deposits, cfg) + require.NoError(t, err) + require.Equal(t, 1, len(missedInbounds)) + }) +} + +func TestGetTssAddress(t *testing.T) { + t.Run("should run successfully", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/zeta-chain/observer/get_tss_address/8332" { + t.Errorf("Expected to request '/zeta-chain', got: %s", r.URL.Path) + } + w.WriteHeader(http.StatusOK) + response := observertypes.QueryGetTssAddressResponse{} + bytes, err := json.Marshal(response) + require.NoError(t, err) + _, err = w.Write(bytes) + require.NoError(t, err) + })) + cfg := config.DefaultConfig() + cfg.ZetaURL = server.URL + _, err := GetTssAddress(cfg, "8332") + require.NoError(t, err) + }) + + t.Run("bad request", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/zeta-chain/observer/get_tss_address/8332" { + w.WriteHeader(http.StatusBadRequest) + response := observertypes.QueryGetTssAddressResponse{} + bytes, err := json.Marshal(response) + require.NoError(t, err) + _, err = w.Write(bytes) + require.NoError(t, err) + } + })) + cfg := config.DefaultConfig() + cfg.ZetaURL = server.URL + _, err := GetTssAddress(cfg, "8332") + require.Error(t, err) + }) +} diff --git a/cmd/zetatool/main.go b/cmd/zetatool/main.go new file mode 100644 index 0000000000..584d21b747 --- /dev/null +++ b/cmd/zetatool/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + + "github.com/zeta-chain/zetacore/cmd/zetatool/filterdeposit" + + "github.com/spf13/cobra" + "github.com/zeta-chain/zetacore/cmd/zetatool/config" +) + +var rootCmd = &cobra.Command{ + Use: "zetatool", + Short: "utility tool for zeta-chain", +} + +func init() { + rootCmd.AddCommand(filterdeposit.NewFilterDepositCmd()) + rootCmd.PersistentFlags().String(config.FlagConfig, "", "custom config file: --config filename.json") +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/docs/cli/zetatool/filterdeposit.md b/docs/cli/zetatool/filterdeposit.md new file mode 100644 index 0000000000..cec577a1e1 --- /dev/null +++ b/docs/cli/zetatool/filterdeposit.md @@ -0,0 +1,29 @@ +# filterdeposit + +Filter missing inbound deposits + +### Synopsis + +Filters relevant inbound transactions for a given network and attempts to find an associated cctx from zetacore. If a +cctx is not found, the associated transaction hash and amount is added to a list and displayed. + +``` +zetatool filterdeposit [command] +``` +### Options + +``` +Available Commands: +btc Filter inbound btc deposits +eth Filter inbound eth deposits +``` + +### Flags +``` +--btc-chain-id string chain id used on zetachain to identify bitcoin - default: 8332 (default "8332") +``` + +### Options inherited from parent commands +``` +--config string custom config file: --config filename.json +``` \ No newline at end of file diff --git a/docs/cli/zetatool/readme.md b/docs/cli/zetatool/readme.md new file mode 100644 index 0000000000..c35d5009a7 --- /dev/null +++ b/docs/cli/zetatool/readme.md @@ -0,0 +1,55 @@ +# Zeta Tool + +Currently, has only one subcommand which finds inbound transactions or deposits that weren't observed on a particular +network. `filterdeposit` + +## Configuring + +#### RPC endpoints +Configuring the tool for specific networks will require different reliable endpoints. For example, if you wanted to +configure an ethereum rpc endpoint, then you will have to find an evm rpc endpoint for eth mainnet and set the field: +`EthRPCURL` + +#### Zeta URL +You will need to find an enpoint for zetachain and set the field: `ZetaURL` + +#### Contract Addresses +Depending on the network, connector and custody contract addresses must be set using these fields: `ConnectorAddress`, +`CustodyAddress` + +If a configuration file is not provided, a default config will be generated under the name +`zetatool_config.json`. Below is an example of a configuration file used for mainnet: + +#### Etherscan API Key +In order to make requests to etherscan, an api key will need to be configured. + +``` +{ + "ZetaURL": "", + "BtcExplorerURL": "https://blockstream.info/api/", + "EthRPCURL": "https://ethereum-rpc.publicnode.com", + "EtherscanAPIkey": "", + "ConnectorAddress": "0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a", + "CustodyAddress": "0x0000030Ec64DF25301d8414eE5a29588C4B0dE10" +} +``` + +## Running Tool + +There are two targets available: + +``` +filter-missed-btc: install-zetatool + ./tool/filter_missed_deposits/filter_missed_btc.sh + +filter-missed-eth: install-zetatool + ./tool/filter_missed_deposits/filter_missed_eth.sh +``` + +Running the commands can be simply done through the makefile in the node repo: + +``` +make filter-missed-btc +or ... +make filter-missed-eth +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 6669039f30..9560f12c9e 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( require ( github.com/binance-chain/tss-lib v0.0.0-20201118045712-70b2cb4bf916 + github.com/nanmu42/etherscan-api v1.10.0 github.com/onrik/ethrpc v1.2.0 ) diff --git a/go.sum b/go.sum index 7b2dc1998c..96e3c53ae9 100644 --- a/go.sum +++ b/go.sum @@ -2314,6 +2314,8 @@ github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nanmu42/etherscan-api v1.10.0 h1:8lAwKbaHEVzXK+cbLaApxbmp4Kai12WKEcY9CxqxKbY= +github.com/nanmu42/etherscan-api v1.10.0/go.mod h1:P8oAUxbYfsdfGXQnHCgjTDs4YbmasUVCtYAYc4rrZ5w= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= diff --git a/tool/filter_missed_deposits/zetatool_config.json b/tool/filter_missed_deposits/zetatool_config.json new file mode 100644 index 0000000000..f3189e52e0 --- /dev/null +++ b/tool/filter_missed_deposits/zetatool_config.json @@ -0,0 +1,8 @@ +{ + "ZetaURL": "127.0.0.1:1317", + "BtcExplorerURL": "https://blockstream.info/api/", + "EthRPCURL": "https://ethereum-rpc.publicnode.com", + "EtherscanAPIkey": "", + "ConnectorAddress": "0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a", + "CustodyAddress": "0x0000030Ec64DF25301d8414eE5a29588C4B0dE10" +} \ No newline at end of file