diff --git a/changelog.md b/changelog.md index c150d89e90..3d1298c64a 100644 --- a/changelog.md +++ b/changelog.md @@ -5,10 +5,8 @@ ### Features * [3353](https://github.com/zeta-chain/node/pull/3353) - add liquidity cap parameter to ZRC20 creation - -## Features - * [3357](https://github.com/zeta-chain/node/pull/3357) - cosmos-sdk v.50.x upgrade +* [3368](https://github.com/zeta-chain/node/pull/3368) - cli command to fetch inbound ballot from inbound hash added to zetatools. ### Refactor diff --git a/cmd/zetaclientd/inbound.go b/cmd/zetaclientd/inbound.go deleted file mode 100644 index b6dc7ea5e1..0000000000 --- a/cmd/zetaclientd/inbound.go +++ /dev/null @@ -1,244 +0,0 @@ -package main - -import ( - "context" - "fmt" - "strconv" - "strings" - - sdk "github.com/cosmos/cosmos-sdk/types" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/onrik/ethrpc" - "github.com/pkg/errors" - "github.com/rs/zerolog" - "github.com/spf13/cobra" - - "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/testutil/sample" - "github.com/zeta-chain/node/zetaclient/chains/base" - btcclient "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" - btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" - evmclient "github.com/zeta-chain/node/zetaclient/chains/evm/client" - evmobserver "github.com/zeta-chain/node/zetaclient/chains/evm/observer" - "github.com/zeta-chain/node/zetaclient/config" - zctx "github.com/zeta-chain/node/zetaclient/context" - "github.com/zeta-chain/node/zetaclient/db" - "github.com/zeta-chain/node/zetaclient/keys" - "github.com/zeta-chain/node/zetaclient/metrics" - "github.com/zeta-chain/node/zetaclient/orchestrator" - "github.com/zeta-chain/node/zetaclient/zetacore" -) - -type inboundOptions struct { - Node string - ChainID string -} - -var inboundOpts inboundOptions - -func setupInboundOptions() { - f, cfg := InboundCmd.PersistentFlags(), &inboundOpts - - f.StringVar(&cfg.Node, "node", "46.4.15.110", "zeta public ip address") - f.StringVar(&cfg.ChainID, "chain-id", "athens_7001-1", "zeta chain id") -} - -func InboundGetBallot(_ *cobra.Command, args []string) error { - cobra.ExactArgs(2) - - cfg, err := config.Load(globalOpts.ZetacoreHome) - if err != nil { - return errors.Wrap(err, "failed to load config") - } - - inboundHash := args[0] - - chainID, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - return errors.Wrap(err, "failed to parse chain id") - } - - // create a new zetacore client - client, err := zetacore.NewClient( - &keys.Keys{OperatorAddress: sdk.MustAccAddressFromBech32(sample.AccAddress())}, - inboundOpts.Node, - "", - inboundOpts.ChainID, - zerolog.Nop(), - ) - if err != nil { - return err - } - - appContext := zctx.New(cfg, nil, zerolog.Nop()) - ctx := zctx.WithAppContext(context.Background(), appContext) - - err = orchestrator.UpdateAppContext(ctx, appContext, client, zerolog.Nop()) - if err != nil { - return errors.Wrap(err, "failed to update app context") - } - - var ballotIdentifier string - - tssEthAddress, err := client.GetEVMTSSAddress(ctx) - if err != nil { - return err - } - - chain, err := appContext.GetChain(chainID) - if err != nil { - return err - } - - baseLogger := base.Logger{Std: zerolog.Nop(), Compliance: zerolog.Nop()} - - database, err := db.NewFromSqliteInMemory(true) - if err != nil { - return errors.Wrap(err, "unable to open database") - } - - // get ballot identifier according to the chain type - if chain.IsEVM() { - var ( - rawChain = chain.RawChain() - rawChainParams = chain.Params() - ) - - evmConfig, found := appContext.Config().GetEVMConfig(chain.ID()) - if !found { - return fmt.Errorf("unable to find evm config") - } - - httpClient, err := metrics.GetInstrumentedHTTPClient(evmConfig.Endpoint) - if err != nil { - return errors.Wrapf(err, "unable to create http client (%s)", evmConfig.Endpoint) - } - - evmClient, err := evmclient.NewFromEndpoint(ctx, evmConfig.Endpoint) - if err != nil { - return errors.Wrapf(err, "unable to create evm client (%s)", evmConfig.Endpoint) - } - - evmJSONRPCClient := ethrpc.NewEthRPC(evmConfig.Endpoint, ethrpc.WithHttpClient(httpClient)) - - baseObserver, err := base.NewObserver( - *rawChain, - *rawChainParams, - client, - nil, - 1000, - nil, - database, - baseLogger, - ) - if err != nil { - return errors.Wrap(err, "unable to create base observer") - } - - evmObserver, err := evmobserver.New(baseObserver, evmClient, evmJSONRPCClient) - if err != nil { - return errors.Wrap(err, "unable to create observer") - } - - coinType := coin.CoinType_Cmd - hash := ethcommon.HexToHash(inboundHash) - tx, isPending, err := evmObserver.TransactionByHash(inboundHash) - if err != nil { - return fmt.Errorf("tx not found on chain %s, %d", err.Error(), chain.ID()) - } - - if isPending { - return fmt.Errorf("tx is still pending") - } - - receipt, err := evmObserver.TransactionReceipt(ctx, hash) - if err != nil { - return fmt.Errorf("tx receipt not found on chain %s, %d", err.Error(), chain.ID()) - } - - params := chain.Params() - - evmObserver.SetChainParams(*params) - - if strings.EqualFold(tx.To, params.ConnectorContractAddress) { - coinType = coin.CoinType_Zeta - } else if strings.EqualFold(tx.To, params.Erc20CustodyContractAddress) { - coinType = coin.CoinType_ERC20 - } else if strings.EqualFold(tx.To, tssEthAddress) { - coinType = coin.CoinType_Gas - } - - switch coinType { - case coin.CoinType_Zeta: - ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, false) - if err != nil { - return err - } - - case coin.CoinType_ERC20: - ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, false) - if err != nil { - return err - } - - case coin.CoinType_Gas: - ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) - if err != nil { - return err - } - default: - fmt.Println("CoinType not detected") - } - fmt.Println("CoinType : ", coinType) - } else if chain.IsBitcoin() { - bitcoinConfig, found := appContext.Config().GetBTCConfig(chain.ID()) - if !found { - return fmt.Errorf("unable to find btc config") - } - - rpcClient, err := btcclient.New(bitcoinConfig, chain.ID(), zerolog.Nop()) - if err != nil { - return errors.Wrap(err, "unable to create rpc client") - } - - baseObserver, err := base.NewObserver( - *chain.RawChain(), - *chain.Params(), - client, - nil, - 100, - nil, - database, - baseLogger, - ) - if err != nil { - return errors.Wrap(err, "unable to create base observer") - } - - observer, err := btcobserver.New(*chain.RawChain(), baseObserver, rpcClient) - if err != nil { - return errors.Wrap(err, "unable to create btc observer") - } - - ballotIdentifier, err = observer.CheckReceiptForBtcTxHash(ctx, inboundHash, false) - if err != nil { - return err - } - } - - fmt.Println("BallotIdentifier: ", ballotIdentifier) - - // query ballot - ballot, err := client.GetBallot(ctx, ballotIdentifier) - if err != nil { - return err - } - - for _, vote := range ballot.Voters { - fmt.Printf("%s: %s\n", vote.VoterAddress, vote.VoteType) - } - - fmt.Println("BallotStatus: ", ballot.BallotStatus) - - return nil -} diff --git a/cmd/zetaclientd/main.go b/cmd/zetaclientd/main.go index a5f8187699..e1df683cc9 100644 --- a/cmd/zetaclientd/main.go +++ b/cmd/zetaclientd/main.go @@ -61,13 +61,6 @@ var ( Short: "Show relayer address", RunE: RelayerShowAddress, } - - InboundCmd = &cobra.Command{Use: "inbound", Short: "Inbound transactions"} - InboundGetBallotCmd = &cobra.Command{ - Use: "get-ballot [inboundHash] [chainID]", - Short: "Get the ballot status for the tx hash", - RunE: InboundGetBallot, - } ) // globalOptions defines the global options for all commands. @@ -89,7 +82,6 @@ func init() { setupGlobalOptions() setupInitializeConfigOptions() setupRelayerOptions() - setupInboundOptions() // Define commands RootCmd.AddCommand(VersionCmd) @@ -103,9 +95,6 @@ func init() { RootCmd.AddCommand(RelayerCmd) RelayerCmd.AddCommand(RelayerImportKeyCmd) RelayerCmd.AddCommand(RelayerShowAddressCmd) - - RootCmd.AddCommand(InboundCmd) - InboundCmd.AddCommand(InboundGetBallotCmd) } func main() { diff --git a/cmd/zetatool/config/config.go b/cmd/zetatool/config/config.go index 6f3face04d..14d74ae24d 100644 --- a/cmd/zetatool/config/config.go +++ b/cmd/zetatool/config/config.go @@ -2,8 +2,11 @@ package config import ( "encoding/json" + "os" "github.com/spf13/afero" + + "github.com/zeta-chain/node/pkg/chains" ) var AppFs = afero.NewOsFs() @@ -11,33 +14,84 @@ 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 TestnetConfig() *Config { + return &Config{ + ZetaChainRPC: "https://zetachain-testnet-grpc.itrocket.net:443", + EthereumRPC: "https://ethereum-sepolia-rpc.publicnode.com", + ZetaChainID: 101, + BtcUser: "", + BtcPassword: "", + BtcHost: "", + BtcParams: "", + SolanaRPC: "", + BscRPC: "https://bsc-testnet-rpc.publicnode.com", + PolygonRPC: "https://polygon-amoy.gateway.tenderly.com", + BaseRPC: "https://base-sepolia-rpc.publicnode.com", + } +} + +func DevnetConfig() *Config { + return &Config{ + ZetaChainRPC: "", + EthereumRPC: "", + ZetaChainID: 101, + BtcUser: "", + BtcPassword: "", + BtcHost: "", + BtcParams: "", + SolanaRPC: "", + BscRPC: "", + PolygonRPC: "", + BaseRPC: "", + } } -func DefaultConfig() *Config { +func MainnetConfig() *Config { return &Config{ - ZetaURL: ZetaURL, - BtcExplorerURL: BtcExplorerURL, - EthRPCURL: EthRPCURL, - ConnectorAddress: ConnectorAddress, - CustodyAddress: CustodyAddress, + ZetaChainRPC: "https://zetachain-mainnet.g.allthatnode.com:443/archive/tendermint", + EthereumRPC: "https://eth-mainnet.public.blastapi.io", + ZetaChainID: 7000, + BtcUser: "", + BtcPassword: "", + BtcHost: "", + BtcParams: "", + SolanaRPC: "", + BaseRPC: "https://base-mainnet.public.blastapi.io", + BscRPC: "https://bsc-mainnet.public.blastapi.io", + PolygonRPC: "https://polygon-bor-rpc.publicnode.com", } } +func PrivateNetConfig() *Config { + return &Config{ + ZetaChainRPC: "http://127.0.0.1:26657", + EthereumRPC: "http://127.0.0.1:8545", + ZetaChainID: 101, + BtcUser: "smoketest", + BtcPassword: "123", + BtcHost: "127.0.0.1:18443", + BtcParams: "regtest", + SolanaRPC: "http://127.0.0.1:8899", + } +} + +// Config is a struct the defines the configuration fields used by zetatool +type Config struct { + ZetaChainRPC string `json:"zeta_chain_rpc"` + ZetaChainID int64 `json:"zeta_chain_id"` + EthereumRPC string `json:"ethereum_rpc"` + BtcUser string `json:"btc_user"` + BtcPassword string `json:"btc_password"` + BtcHost string `json:"btc_host"` + BtcParams string `json:"btc_params"` + SolanaRPC string `json:"solana_rpc"` + BscRPC string `json:"bsc_rpc"` + PolygonRPC string `json:"polygon_rpc"` + BaseRPC string `json:"base_rpc"` +} + func (c *Config) Save() error { file, err := json.MarshalIndent(c, "", " ") if err != nil { @@ -46,9 +100,9 @@ func (c *Config) Save() error { err = afero.WriteFile(AppFs, defaultCfgFileName, file, 0600) return err } - func (c *Config) Read(filename string) error { - data, err := afero.ReadFile(AppFs, filename) + // #nosec G304 reading file is safe + data, err := os.ReadFile(filename) if err != nil { return err } @@ -56,15 +110,18 @@ func (c *Config) Read(filename string) error { return err } -func GetConfig(filename string) (*Config, error) { - //Check if cfgFile is empty, if so return default Config and save to file +func GetConfig(chain chains.Chain, filename string) (*Config, error) { + //Check if cfgFile is empty, if so return default Config based on network type if filename == "" { - cfg := DefaultConfig() - err := cfg.Save() - return cfg, err + return map[chains.NetworkType]*Config{ + chains.NetworkType_mainnet: MainnetConfig(), + chains.NetworkType_testnet: TestnetConfig(), + chains.NetworkType_privnet: PrivateNetConfig(), + chains.NetworkType_devnet: DevnetConfig(), + }[chain.NetworkType], nil } - //if file is specified, open file and return struct + //if a file is specified, use the config in the file 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 index dd56604d5f..8640fa7939 100644 --- a/cmd/zetatool/config/config_test.go +++ b/cmd/zetatool/config/config_test.go @@ -1,77 +1,55 @@ -package config +package config_test import ( "testing" - "github.com/spf13/afero" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd/zetatool/config" + "github.com/zeta-chain/node/pkg/chains" ) -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("") +func TestRead(t *testing.T) { + t.Run("TestRead", func(t *testing.T) { + c := config.Config{} + err := c.Read("sample_config.json") 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) + require.Equal(t, "https://zetachain-testnet-grpc.itrocket.net:443", c.ZetaChainRPC) + require.Equal(t, "https://ethereum-sepolia-rpc.publicnode.com", c.EthereumRPC) + require.Equal(t, int64(101), c.ZetaChainID) + require.Equal(t, "", c.BtcUser) + require.Equal(t, "", c.BtcPassword) + require.Equal(t, "", c.BtcHost) + require.Equal(t, "", c.BtcParams) + require.Equal(t, "", c.SolanaRPC) + require.Equal(t, "https://bsc-testnet-rpc.publicnode.com", c.BscRPC) + require.Equal(t, "https://polygon-amoy.gateway.tenderly.com", c.PolygonRPC) + require.Equal(t, "https://base-sepolia-rpc.publicnode.com", c.BaseRPC) }) } -func TestConfig_Read(t *testing.T) { - AppFs = afero.NewMemMapFs() - cfg, err := GetConfig("") - require.NoError(t, err) +func TestGetConfig(t *testing.T) { + t.Run("Get default config if not specified", func(t *testing.T) { + cfg, err := config.GetConfig(chains.Ethereum, "") + require.NoError(t, err) + require.Equal(t, "https://zetachain-mainnet.g.allthatnode.com:443/archive/tendermint", cfg.ZetaChainRPC) - t.Run("read existing file", func(t *testing.T) { - c := &Config{} - err := c.Read(defaultCfgFileName) + cfg, err = config.GetConfig(chains.Sepolia, "") require.NoError(t, err) - require.Equal(t, c, cfg) - }) + require.Equal(t, "https://zetachain-testnet-grpc.itrocket.net:443", cfg.ZetaChainRPC) - t.Run("read non-existent file", func(t *testing.T) { - err := AppFs.Remove(defaultCfgFileName) + cfg, err = config.GetConfig(chains.GoerliLocalnet, "") require.NoError(t, err) - c := &Config{} - err = c.Read(defaultCfgFileName) - require.ErrorContains(t, err, "file does not exist") - require.NotEqual(t, c, cfg) + require.Equal(t, "http://127.0.0.1:26657", cfg.ZetaChainRPC) }) -} - -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() + t.Run("Get config from file if specified", func(t *testing.T) { + cfg, err := config.GetConfig(chains.Ethereum, "sample_config.json") require.NoError(t, err) + require.Equal(t, "https://zetachain-testnet-grpc.itrocket.net:443", cfg.ZetaChainRPC) - newCfg, err := GetConfig(defaultCfgFileName) + cfg, err = config.GetConfig(chains.Sepolia, "sample_config.json") require.NoError(t, err) - require.Equal(t, cfg, newCfg) + require.Equal(t, "https://zetachain-testnet-grpc.itrocket.net:443", cfg.ZetaChainRPC) }) - - // Should test invalid json encoding but currently not able to without interface } diff --git a/cmd/zetatool/config/sample_config.json b/cmd/zetatool/config/sample_config.json new file mode 100644 index 0000000000..ca15d85fa3 --- /dev/null +++ b/cmd/zetatool/config/sample_config.json @@ -0,0 +1,13 @@ +{ + "zeta_chain_rpc": "https://zetachain-testnet-grpc.itrocket.net:443", + "zeta_chain_id": 101, + "ethereum_rpc": "https://ethereum-sepolia-rpc.publicnode.com", + "btc_user": "", + "btc_password": "", + "btc_host": "", + "btc_params": "", + "solana_rpc": "", + "bsc_rpc": "https://bsc-testnet-rpc.publicnode.com", + "polygon_rpc": "https://polygon-amoy.gateway.tenderly.com", + "base_rpc": "https://base-sepolia-rpc.publicnode.com" +} \ No newline at end of file diff --git a/cmd/zetatool/filterdeposit/btc.go b/cmd/zetatool/filterdeposit/btc.go deleted file mode 100644 index f4b2636da2..0000000000 --- a/cmd/zetatool/filterdeposit/btc.go +++ /dev/null @@ -1,189 +0,0 @@ -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/node/cmd/zetatool/config" - "github.com/zeta-chain/node/pkg/constant" -) - -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(constant.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 G115 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 deleted file mode 100644 index 2e00f67e4a..0000000000 --- a/cmd/zetatool/filterdeposit/evm.go +++ /dev/null @@ -1,239 +0,0 @@ -package filterdeposit - -import ( - "context" - "fmt" - "log" - "math/big" - "strings" - - "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/erc20custody.sol" - "github.com/zeta-chain/protocol-contracts/pkg/zetaconnector.non-eth.sol" - - "github.com/zeta-chain/node/cmd/zetatool/config" - "github.com/zeta-chain/node/pkg/constant" - evmcommon "github.com/zeta-chain/node/zetaclient/chains/evm/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, "", evmcommon.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, "", evmcommon.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 G115 these block numbers need to be *int for this particular client package - startInt := int(startBlock) - // #nosec G115 - 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, constant.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 deleted file mode 100644 index e49a333f3f..0000000000 --- a/cmd/zetatool/filterdeposit/filterdeposit.go +++ /dev/null @@ -1,124 +0,0 @@ -package filterdeposit - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "strings" - - "github.com/spf13/cobra" - - "github.com/zeta-chain/node/cmd/zetatool/config" - "github.com/zeta-chain/node/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 zetacore 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 deleted file mode 100644 index a2d2adb510..0000000000 --- a/cmd/zetatool/filterdeposit/filterdeposit_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package filterdeposit - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/node/cmd/zetatool/config" - "github.com/zeta-chain/node/x/crosschain/types" - observertypes "github.com/zeta-chain/node/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/inbound/bitcoin.go b/cmd/zetatool/inbound/bitcoin.go new file mode 100644 index 0000000000..6385e4ce55 --- /dev/null +++ b/cmd/zetatool/inbound/bitcoin.go @@ -0,0 +1,231 @@ +package inbound + +import ( + "context" + "encoding/hex" + "fmt" + "math/big" + + cosmosmath "cosmossdk.io/math" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/cmd/zetatool/config" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/rpc" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" + zetaclientObserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + zetaclientConfig "github.com/zeta-chain/node/zetaclient/config" +) + +func btcInboundBallotIdentifier( + ctx context.Context, + cfg config.Config, + zetacoreClient rpc.Clients, + inboundHash string, + inboundChain chains.Chain, + zetaChainID int64, + logger zerolog.Logger) (string, error) { + params, err := chains.BitcoinNetParamsFromChainID(inboundChain.ChainId) + if err != nil { + return "", fmt.Errorf("unable to get bitcoin net params from chain id: %w", err) + } + + connCfg := zetaclientConfig.BTCConfig{ + RPCUsername: cfg.BtcUser, + RPCPassword: cfg.BtcPassword, + RPCHost: cfg.BtcHost, + RPCParams: params.Name, + } + + rpcClient, err := client.New(connCfg, inboundChain.ChainId, logger) + if err != nil { + return "", fmt.Errorf("unable to create rpc client: %w", err) + } + + err = rpcClient.Ping(ctx) + if err != nil { + return "", fmt.Errorf("error ping the bitcoin server: %w", err) + } + res, err := zetacoreClient.Observer.GetTssAddress(context.Background(), &types.QueryGetTssAddressRequest{}) + if err != nil { + return "", fmt.Errorf("failed to get tss address: %w", err) + } + tssBtcAddress := res.GetBtc() + + chainParams, err := zetacoreClient.GetChainParamsForChainID(context.Background(), inboundChain.ChainId) + if err != nil { + return "", fmt.Errorf("failed to get chain params: %w", err) + } + + return bitcoinBallotIdentifier( + ctx, + rpcClient, + params, + tssBtcAddress, + inboundHash, + inboundChain.ChainId, + zetaChainID, + chainParams.ConfirmationCount, + ) +} + +func bitcoinBallotIdentifier( + ctx context.Context, + btcClient *client.Client, + params *chaincfg.Params, + tss string, + txHash string, + senderChainID int64, + zetacoreChainID int64, + confirmationCount uint64) (string, error) { + hash, err := chainhash.NewHashFromStr(txHash) + if err != nil { + return "", err + } + confirmationMessage := "" + tx, err := btcClient.GetRawTransactionVerbose(ctx, hash) + if err != nil { + return "", err + } + if tx.Confirmations < confirmationCount { + confirmationMessage = fmt.Sprintf("tx might not be confirmed on chain: %d", senderChainID) + } + + blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) + if err != nil { + return "", err + } + + blockVb, err := btcClient.GetBlockVerbose(ctx, blockHash) + if err != nil { + return "", err + } + + event, err := zetaclientObserver.GetBtcEvent( + ctx, + btcClient, + *tx, + tss, + uint64(blockVb.Height), // #nosec G115 always positive + zerolog.New(zerolog.Nop()), + params, + common.CalcDepositorFee, + ) + if err != nil { + return "", fmt.Errorf("error getting btc event: %w", err) + } + if event == nil { + return "", fmt.Errorf("no event built for btc sent to TSS") + } + + return identifierFromBtcEvent(event, senderChainID, zetacoreChainID, confirmationMessage) +} + +func identifierFromBtcEvent(event *zetaclientObserver.BTCInboundEvent, + senderChainID int64, + zetacoreChainID int64, confirmationMessage string) (string, error) { + // decode event memo bytes + err := event.DecodeMemoBytes(senderChainID) + if err != nil { + return "", fmt.Errorf("error decoding memo bytes: %w", err) + } + + // convert the amount to integer (satoshis) + amountSats, err := common.GetSatoshis(event.Value) + if err != nil { + return "", fmt.Errorf("error converting amount to satoshis: %w", err) + } + amountInt := big.NewInt(amountSats) + + var msg *crosschaintypes.MsgVoteInbound + switch event.MemoStd { + case nil: + { + msg = voteFromLegacyMemo(event, amountInt, senderChainID, zetacoreChainID) + } + default: + { + msg = voteFromStdMemo(event, amountInt, senderChainID, zetacoreChainID) + } + } + if msg == nil { + return "", fmt.Errorf("failed to create vote message") + } + + index := msg.Digest() + if confirmationMessage != "" { + return fmt.Sprintf("ballot identifier: %s warning: %s", index, confirmationMessage), nil + } + return fmt.Sprintf("ballot identifier: %s", msg.Digest()), nil +} + +// NewInboundVoteFromLegacyMemo creates a MsgVoteInbound message for inbound that uses legacy memo +func voteFromLegacyMemo( + event *zetaclientObserver.BTCInboundEvent, + amountSats *big.Int, + senderChainID int64, + zetacoreChainID int64, +) *crosschaintypes.MsgVoteInbound { + message := hex.EncodeToString(event.MemoBytes) + + return crosschaintypes.NewMsgVoteInbound( + "", + event.FromAddress, + senderChainID, + event.FromAddress, + event.ToAddress, + zetacoreChainID, + cosmosmath.NewUintFromBigInt(amountSats), + message, + event.TxHash, + event.BlockNumber, + 0, + coin.CoinType_Gas, + "", + 0, + crosschaintypes.ProtocolContractVersion_V1, + false, // not relevant for v1 + ) +} + +func voteFromStdMemo( + event *zetaclientObserver.BTCInboundEvent, + amountSats *big.Int, + senderChainID int64, + zetacoreChainID int64, +) *crosschaintypes.MsgVoteInbound { + // zetacore will create a revert outbound that points to the custom revert address. + revertOptions := crosschaintypes.RevertOptions{ + RevertAddress: event.MemoStd.RevertOptions.RevertAddress, + } + + // make a legacy message so that zetacore can process it as V1 + msgBytes := append(event.MemoStd.Receiver.Bytes(), event.MemoStd.Payload...) + message := hex.EncodeToString(msgBytes) + + return crosschaintypes.NewMsgVoteInbound( + "", + event.FromAddress, + senderChainID, + event.FromAddress, + event.ToAddress, + zetacoreChainID, + cosmosmath.NewUintFromBigInt(amountSats), + message, + event.TxHash, + event.BlockNumber, + 0, + coin.CoinType_Gas, + "", + 0, + crosschaintypes.ProtocolContractVersion_V1, + false, // not relevant for v1 + crosschaintypes.WithRevertOptions(revertOptions), + ) +} diff --git a/cmd/zetatool/inbound/evm.go b/cmd/zetatool/inbound/evm.go new file mode 100644 index 0000000000..62432aabc5 --- /dev/null +++ b/cmd/zetatool/inbound/evm.go @@ -0,0 +1,378 @@ +package inbound + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/hex" + "fmt" + + sdkmath "cosmossdk.io/math" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/zeta-chain/protocol-contracts/pkg/erc20custody.sol" + "github.com/zeta-chain/protocol-contracts/pkg/gatewayevm.sol" + "github.com/zeta-chain/protocol-contracts/pkg/zetaconnector.non-eth.sol" + + "github.com/zeta-chain/node/cmd/zetatool/config" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/pkg/crypto" + "github.com/zeta-chain/node/pkg/rpc" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/x/observer/types" + evmclient "github.com/zeta-chain/node/zetaclient/chains/evm/client" + clienttypes "github.com/zeta-chain/node/zetaclient/types" + "github.com/zeta-chain/node/zetaclient/zetacore" +) + +func resolveRPC(chain chains.Chain, cfg config.Config) string { + return map[chains.Network]string{ + chains.Network_eth: cfg.EthereumRPC, + chains.Network_base: cfg.BaseRPC, + chains.Network_polygon: cfg.PolygonRPC, + chains.Network_bsc: cfg.BscRPC, + }[chain.Network] +} + +func evmInboundBallotIdentifier(ctx context.Context, + cfg config.Config, + zetacoreClient rpc.Clients, + inboundHash string, + inboundChain chains.Chain, + zetaChainID int64) (string, error) { + evmRRC := resolveRPC(inboundChain, cfg) + if evmRRC == "" { + return "", fmt.Errorf("rpc not found for chain %d network %s", inboundChain.ChainId, inboundChain.Network) + } + rpcClient, err := ethrpc.DialHTTP(evmRRC) + if err != nil { + return "", fmt.Errorf("failed to connect to eth rpc: %w", err) + } + evmClient := ethclient.NewClient(rpcClient) + + // create evm client for the observation chain + tx, receipt, err := getEvmTx(ctx, evmClient, inboundHash, inboundChain) + if err != nil { + return "", fmt.Errorf("failed to get tx: %w", err) + } + + chainParams, err := zetacoreClient.GetChainParamsForChainID(context.Background(), inboundChain.ChainId) + if err != nil { + return "", fmt.Errorf("failed to get chain params: %w", err) + } + + res, err := zetacoreClient.Observer.GetTssAddress(context.Background(), &types.QueryGetTssAddressRequest{}) + if err != nil { + return "", fmt.Errorf("failed to get tss address: %w", err) + } + tssEthAddress := res.GetEth() + + if tx.To() == nil { + return "", fmt.Errorf("invalid transaction,to field is empty %s", inboundHash) + } + + confirmationMessage := "" + + // Signer is unused + c := evmclient.New(evmClient, ethtypes.NewLondonSigner(tx.ChainId())) + confirmed, err := c.IsTxConfirmed(ctx, inboundHash, chainParams.ConfirmationCount) + if err != nil { + return "", fmt.Errorf("unable to confirm tx: %w", err) + } + if !confirmed { + confirmationMessage = fmt.Sprintf("tx might not be confirmed on chain %d", inboundChain.ChainId) + } + + msg := &crosschaintypes.MsgVoteInbound{} + // Create inbound vote message based on the cointype and protocol version + switch tx.To().Hex() { + case chainParams.ConnectorContractAddress: + { + // build inbound vote message and post vote + addrConnector := ethcommon.HexToAddress(chainParams.ConnectorContractAddress) + connector, err := zetaconnector.NewZetaConnectorNonEth(addrConnector, evmClient) + if err != nil { + return "", fmt.Errorf("failed to get connector contract: %w", err) + } + for _, log := range receipt.Logs { + event, err := connector.ParseZetaSent(*log) + if err == nil && event != nil { + msg = zetaTokenVoteV1(event, inboundChain.ChainId) + } + } + } + case chainParams.Erc20CustodyContractAddress: + { + addrCustody := ethcommon.HexToAddress(chainParams.Erc20CustodyContractAddress) + custody, err := erc20custody.NewERC20Custody(addrCustody, evmClient) + if err != nil { + return "", fmt.Errorf("failed to get custody contract: %w", err) + } + sender, err := evmClient.TransactionSender(ctx, tx, receipt.BlockHash, receipt.TransactionIndex) + if err != nil { + return "", fmt.Errorf("failed to get tx sender: %w", err) + } + for _, log := range receipt.Logs { + zetaDeposited, err := custody.ParseDeposited(*log) + if err == nil && zetaDeposited != nil { + msg = erc20VoteV1(zetaDeposited, sender, inboundChain.ChainId, zetaChainID) + } + } + } + case tssEthAddress: + { + if receipt.Status != ethtypes.ReceiptStatusSuccessful { + return "", fmt.Errorf("tx failed on chain %d", inboundChain.ChainId) + } + sender, err := evmClient.TransactionSender(ctx, tx, receipt.BlockHash, receipt.TransactionIndex) + if err != nil { + return "", fmt.Errorf("failed to get tx sender: %w", err) + } + msg = gasVoteV1(tx, sender, receipt.BlockNumber.Uint64(), inboundChain.ChainId, zetaChainID) + } + case chainParams.GatewayAddress: + { + gatewayAddr := ethcommon.HexToAddress(chainParams.GatewayAddress) + gateway, err := gatewayevm.NewGatewayEVM(gatewayAddr, evmClient) + if err != nil { + return "", fmt.Errorf("failed to get gateway contract: %w", err) + } + for _, log := range receipt.Logs { + if log == nil || log.Address != gatewayAddr { + continue + } + eventDeposit, err := gateway.ParseDeposited(*log) + if err == nil { + msg = depositInboundVoteV2(eventDeposit, inboundChain.ChainId, zetaChainID) + return msg.Digest(), nil + } + eventDepositAndCall, err := gateway.ParseDepositedAndCalled(*log) + if err == nil { + msg = depositAndCallInboundVoteV2(eventDepositAndCall, inboundChain.ChainId, zetaChainID) + return msg.Digest(), nil + } + eventCall, err := gateway.ParseCalled(*log) + if err == nil { + msg = callInboundVoteV2(eventCall, inboundChain.ChainId, zetaChainID) + } + } + } + default: + return "", fmt.Errorf("irrelevant transaction , not sent to any known address txHash: %s", inboundHash) + } + + if confirmationMessage != "" { + return fmt.Sprintf("ballot identifier: %s warning: %s", msg.Digest(), confirmationMessage), nil + } + return fmt.Sprintf("ballot identifier: %s", msg.Digest()), nil +} + +func getEvmTx( + ctx context.Context, + evmClient *ethclient.Client, + inboundHash string, + inboundChain chains.Chain, +) (*ethtypes.Transaction, *ethtypes.Receipt, error) { + // Fetch transaction from the inbound + hash := ethcommon.HexToHash(inboundHash) + tx, isPending, err := evmClient.TransactionByHash(ctx, hash) + if err != nil { + return nil, nil, fmt.Errorf("tx not found on chain: %w,chainID: %d", err, inboundChain.ChainId) + } + if isPending { + return nil, nil, fmt.Errorf("tx is still pending on chain: %d", inboundChain.ChainId) + } + receipt, err := evmClient.TransactionReceipt(ctx, hash) + if err != nil { + return nil, nil, fmt.Errorf("failed to get receipt: %w, tx hash: %s", err, inboundHash) + } + return tx, receipt, nil +} + +func zetaTokenVoteV1( + event *zetaconnector.ZetaConnectorNonEthZetaSent, + observationChain int64, +) *crosschaintypes.MsgVoteInbound { + // note that this is most likely zeta chain + destChain, found := chains.GetChainFromChainID(event.DestinationChainId.Int64(), []chains.Chain{}) + if !found { + return nil + } + + destAddr := clienttypes.BytesToEthHex(event.DestinationAddress) + sender := event.ZetaTxSenderAddress.Hex() + message := base64.StdEncoding.EncodeToString(event.Message) + + return zetacore.GetInboundVoteMessage( + sender, + observationChain, + event.SourceTxOriginAddress.Hex(), + destAddr, + destChain.ChainId, + sdkmath.NewUintFromBigInt(event.ZetaValueAndGas), + message, + event.Raw.TxHash.Hex(), + event.Raw.BlockNumber, + event.DestinationGasLimit.Uint64(), + coin.CoinType_Zeta, + "", + "", + event.Raw.Index, + ) +} + +func erc20VoteV1( + event *erc20custody.ERC20CustodyDeposited, + sender ethcommon.Address, + observationChain int64, + zetacoreChainID int64, +) *crosschaintypes.MsgVoteInbound { + // donation check + if bytes.Equal(event.Message, []byte(constant.DonationMessage)) { + return nil + } + + return zetacore.GetInboundVoteMessage( + sender.Hex(), + observationChain, + "", + clienttypes.BytesToEthHex(event.Recipient), + zetacoreChainID, + sdkmath.NewUintFromBigInt(event.Amount), + hex.EncodeToString(event.Message), + event.Raw.TxHash.Hex(), + event.Raw.BlockNumber, + 1_500_000, + coin.CoinType_ERC20, + event.Asset.String(), + "", + event.Raw.Index, + ) +} + +func gasVoteV1( + tx *ethtypes.Transaction, + sender ethcommon.Address, + blockNumber uint64, + senderChainID int64, + zetacoreChainID int64, +) *crosschaintypes.MsgVoteInbound { + message := string(tx.Data()) + data, _ := hex.DecodeString(message) + if bytes.Equal(data, []byte(constant.DonationMessage)) { + return nil + } + + return zetacore.GetInboundVoteMessage( + sender.Hex(), + senderChainID, + sender.Hex(), + sender.Hex(), + zetacoreChainID, + sdkmath.NewUintFromString(tx.Value().String()), + message, + tx.Hash().Hex(), + blockNumber, + 90_000, + coin.CoinType_Gas, + "", + "", + 0, // not a smart contract call + ) +} + +func depositInboundVoteV2(event *gatewayevm.GatewayEVMDeposited, + senderChainID int64, + zetacoreChainID int64) *crosschaintypes.MsgVoteInbound { + // if event.Asset is zero, it's a native token + coinType := coin.CoinType_ERC20 + if crypto.IsEmptyAddress(event.Asset) { + coinType = coin.CoinType_Gas + } + + // to maintain compatibility with previous gateway version, deposit event with a non-empty payload is considered as a call + isCrossChainCall := false + if len(event.Payload) > 0 { + isCrossChainCall = true + } + + return crosschaintypes.NewMsgVoteInbound( + "", + event.Sender.Hex(), + senderChainID, + "", + event.Receiver.Hex(), + zetacoreChainID, + sdkmath.NewUintFromBigInt(event.Amount), + hex.EncodeToString(event.Payload), + event.Raw.TxHash.Hex(), + event.Raw.BlockNumber, + zetacore.PostVoteInboundCallOptionsGasLimit, + coinType, + event.Asset.Hex(), + event.Raw.Index, + crosschaintypes.ProtocolContractVersion_V2, + false, // currently not relevant since calls are not arbitrary + crosschaintypes.WithEVMRevertOptions(event.RevertOptions), + crosschaintypes.WithCrossChainCall(isCrossChainCall), + ) +} + +func depositAndCallInboundVoteV2(event *gatewayevm.GatewayEVMDepositedAndCalled, + senderChainID int64, + zetacoreChainID int64) *crosschaintypes.MsgVoteInbound { + // if event.Asset is zero, it's a native token + coinType := coin.CoinType_ERC20 + if crypto.IsEmptyAddress(event.Asset) { + coinType = coin.CoinType_Gas + } + + return crosschaintypes.NewMsgVoteInbound( + "", + event.Sender.Hex(), + senderChainID, + "", + event.Receiver.Hex(), + zetacoreChainID, + sdkmath.NewUintFromBigInt(event.Amount), + hex.EncodeToString(event.Payload), + event.Raw.TxHash.Hex(), + event.Raw.BlockNumber, + 1_500_000, + coinType, + event.Asset.Hex(), + event.Raw.Index, + crosschaintypes.ProtocolContractVersion_V2, + false, // currently not relevant since calls are not arbitrary + crosschaintypes.WithEVMRevertOptions(event.RevertOptions), + crosschaintypes.WithCrossChainCall(true), + ) +} + +func callInboundVoteV2(event *gatewayevm.GatewayEVMCalled, + senderChainID int64, + zetacoreChainID int64) *crosschaintypes.MsgVoteInbound { + return crosschaintypes.NewMsgVoteInbound( + "", + event.Sender.Hex(), + senderChainID, + "", + event.Receiver.Hex(), + zetacoreChainID, + sdkmath.ZeroUint(), + hex.EncodeToString(event.Payload), + event.Raw.TxHash.Hex(), + event.Raw.BlockNumber, + zetacore.PostVoteInboundCallOptionsGasLimit, + coin.CoinType_NoAssetCall, + "", + event.Raw.Index, + crosschaintypes.ProtocolContractVersion_V2, + false, // currently not relevant since calls are not arbitrary + crosschaintypes.WithEVMRevertOptions(event.RevertOptions), + ) +} diff --git a/cmd/zetatool/inbound/inbound.go b/cmd/zetatool/inbound/inbound.go new file mode 100644 index 0000000000..bf31a84618 --- /dev/null +++ b/cmd/zetatool/inbound/inbound.go @@ -0,0 +1,125 @@ +package inbound + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/zeta-chain/node/cmd/zetatool/config" + "github.com/zeta-chain/node/pkg/chains" + zetacorerpc "github.com/zeta-chain/node/pkg/rpc" +) + +func NewGetInboundBallotCMD() *cobra.Command { + return &cobra.Command{ + Use: "get-ballot [inboundHash] [chainID]", + Short: "fetch ballot identifier from the inbound hash", + RunE: GetInboundBallot, + Args: cobra.ExactArgs(2), + } +} + +func GetInboundBallot(cmd *cobra.Command, args []string) error { + inboundHash := args[0] + inboundChainID, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("failed to parse chain id") + } + configFile, err := cmd.Flags().GetString(config.FlagConfig) + if err != nil { + return fmt.Errorf("failed to read value for flag %s , err %w", config.FlagConfig, err) + } + + return GetBallotIdentifier(inboundHash, inboundChainID, configFile) +} + +func GetBallotIdentifier(inboundHash string, inboundChainID int64, configFile string) error { + observationChain, found := chains.GetChainFromChainID(inboundChainID, []chains.Chain{}) + if !found { + return fmt.Errorf("chain not supported,chain id: %d", inboundChainID) + } + + cfg, err := config.GetConfig(observationChain, configFile) + if err != nil { + return fmt.Errorf("failed to get config: %w", err) + } + + zetacoreClient, err := zetacorerpc.NewCometBFTClients(cfg.ZetaChainRPC) + if err != nil { + return fmt.Errorf("failed to create zetacore client: %w", err) + } + + ctx := context.Background() + ballotIdentifierMessage := "" + + // logger is used when calling internal zetaclient functions which need a logger. + // we do not need to log those messages for this tool + logger := zerolog.New(zerolog.ConsoleWriter{ + Out: zerolog.Nop(), + TimeFormat: time.RFC3339, + }).With().Timestamp().Logger() + + if observationChain.IsEVMChain() { + ballotIdentifierMessage, err = evmInboundBallotIdentifier( + ctx, + *cfg, + zetacoreClient, + inboundHash, + observationChain, + cfg.ZetaChainID, + ) + if err != nil { + return fmt.Errorf( + "failed to get inbound ballot for evm chain %d, %w", + observationChain.ChainId, + err, + ) + } + } + + if observationChain.IsBitcoinChain() { + ballotIdentifierMessage, err = btcInboundBallotIdentifier( + ctx, + *cfg, + zetacoreClient, + inboundHash, + observationChain, + cfg.ZetaChainID, + logger, + ) + if err != nil { + return fmt.Errorf( + "failed to get inbound ballot for bitcoin chain %d, %w", + observationChain.ChainId, + err, + ) + } + } + + if observationChain.IsSolanaChain() { + ballotIdentifierMessage, err = solanaInboundBallotIdentifier( + ctx, + *cfg, + zetacoreClient, + inboundHash, + observationChain, + cfg.ZetaChainID, + logger, + ) + if err != nil { + return fmt.Errorf( + "failed to get inbound ballot for solana chain %d, %w", + observationChain.ChainId, + err, + ) + } + } + + log.Info().Msgf("%s", ballotIdentifierMessage) + return nil +} diff --git a/cmd/zetatool/inbound/solana.go b/cmd/zetatool/inbound/solana.go new file mode 100644 index 0000000000..54c02a82e5 --- /dev/null +++ b/cmd/zetatool/inbound/solana.go @@ -0,0 +1,103 @@ +package inbound + +import ( + "context" + "encoding/hex" + "fmt" + + cosmosmath "cosmossdk.io/math" + "github.com/gagliardetto/solana-go" + solrpc "github.com/gagliardetto/solana-go/rpc" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/cmd/zetatool/config" + "github.com/zeta-chain/node/pkg/chains" + solanacontracts "github.com/zeta-chain/node/pkg/contracts/solana" + "github.com/zeta-chain/node/pkg/rpc" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/solana/observer" + solanarpc "github.com/zeta-chain/node/zetaclient/chains/solana/rpc" + clienttypes "github.com/zeta-chain/node/zetaclient/types" +) + +func solanaInboundBallotIdentifier(ctx context.Context, + cfg config.Config, + zetacoreClient rpc.Clients, + inboundHash string, + inboundChain chains.Chain, + zetaChainID int64, + logger zerolog.Logger) (string, error) { + solClient := solrpc.New(cfg.SolanaRPC) + if solClient == nil { + return "", fmt.Errorf("error creating rpc client") + } + + signature := solana.MustSignatureFromBase58(inboundHash) + + txResult, err := solanarpc.GetTransaction(ctx, solClient, signature) + if err != nil { + return "", fmt.Errorf("error getting transaction: %w", err) + } + + chainParams, err := zetacoreClient.GetChainParamsForChainID(context.Background(), inboundChain.ChainId) + if err != nil { + return "", fmt.Errorf("failed to get chain params: %w", err) + } + + gatewayID, _, err := solanacontracts.ParseGatewayWithPDA(chainParams.GatewayAddress) + if err != nil { + return "", fmt.Errorf("cannot parse gateway address: %s, err: %w", chainParams.GatewayAddress, err) + } + + events, err := observer.FilterInboundEvents(txResult, + gatewayID, + inboundChain.ChainId, + logger, + ) + + if err != nil { + return "", fmt.Errorf("failed to filter solana inbound events: %w", err) + } + + msg := &crosschaintypes.MsgVoteInbound{} + + // build inbound vote message from events and post to zetacore + for _, event := range events { + msg, err = voteMsgFromSolEvent(event, zetaChainID) + if err != nil { + return "", fmt.Errorf("failed to create vote message: %w", err) + } + } + + return fmt.Sprintf("ballot identifier: %s", msg.Digest()), nil +} + +// voteMsgFromSolEvent builds a MsgVoteInbound from an inbound event +func voteMsgFromSolEvent(event *clienttypes.InboundEvent, + zetaChainID int64) (*crosschaintypes.MsgVoteInbound, error) { + // decode event memo bytes to get the receiver + err := event.DecodeMemo() + if err != nil { + return nil, fmt.Errorf("failed to decode memo: %w", err) + } + + // create inbound vote message + return crosschaintypes.NewMsgVoteInbound( + "", + event.Sender, + event.SenderChainID, + event.Sender, + event.Receiver, + zetaChainID, + cosmosmath.NewUint(event.Amount), + hex.EncodeToString(event.Memo), + event.TxHash, + event.BlockNumber, + 0, + event.CoinType, + event.Asset, + 0, // not a smart contract call + crosschaintypes.ProtocolContractVersion_V1, + false, // not relevant for v1 + ), nil +} diff --git a/cmd/zetatool/main.go b/cmd/zetatool/main.go index 7fd284ed2b..c79451d413 100644 --- a/cmd/zetatool/main.go +++ b/cmd/zetatool/main.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/zeta-chain/node/cmd/zetatool/config" - "github.com/zeta-chain/node/cmd/zetatool/filterdeposit" + "github.com/zeta-chain/node/cmd/zetatool/inbound" ) var rootCmd = &cobra.Command{ @@ -16,13 +16,13 @@ var rootCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(filterdeposit.NewFilterDepositCmd()) + rootCmd.AddCommand(inbound.NewGetInboundBallotCMD()) rootCmd.PersistentFlags().String(config.FlagConfig, "", "custom config file: --config filename.json") } func main() { if err := rootCmd.Execute(); err != nil { - fmt.Println(err) + fmt.Fprintf(os.Stderr, "Error executing command: %v\n", err) os.Exit(1) } } diff --git a/docs/cli/zetatool/filterdeposit.md b/docs/cli/zetatool/filterdeposit.md deleted file mode 100644 index cec577a1e1..0000000000 --- a/docs/cli/zetatool/filterdeposit.md +++ /dev/null @@ -1,29 +0,0 @@ -# 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 index 206e1eb5e5..8d522755ab 100644 --- a/docs/cli/zetatool/readme.md +++ b/docs/cli/zetatool/readme.md @@ -1,55 +1,30 @@ -# Zeta Tool +# ZetaTool -Currently, has only one subcommand which finds inbound transactions or deposits that weren't observed on a particular -network. `filterdeposit` +ZetaTool is a utility CLI for Zetachain.It currently provides a command to fetch the ballot/cctx identifier from the inbound hash -## Configuring +## Installation +Use the target : `make install-zetatool` -#### 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` +## Usage -#### Zeta URL -You will need to find an endpoint 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. +### Fetching the Ballot Identifier +### Command +```shell +zetatool get-ballot [inboundHash] [chainID] --config ``` -{ - "ZetaURL": "", - "BtcExplorerURL": "https://blockstream.info/api/", - "EthRPCURL": "https://ethereum-rpc.publicnode.com", - "EtherscanAPIkey": "", - "ConnectorAddress": "0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a", - "CustodyAddress": "0x0000030Ec64DF25301d8414eE5a29588C4B0dE10" -} +### Example +```shell +zetatool get-ballot 0x61008d7f79b2955a15e3cb95154a80e19c7385993fd0e083ff0cbe0b0f56cb9a 1 +{"level":"info","time":"2025-01-20T11:30:47-05:00","message":"ballot identifier: 0xae189ab5cd884af784835297ac43eb55deb8a7800023534c580f44ee2b3eb5ed"} ``` -## Running Tool +- `inboundHash`: The inbound hash of the transaction for which the ballot identifier is to be fetched +- `chainID`: The chain ID of the chain to which the transaction belongs +- `config`: [Optional] The path to the configuration file. When not provided, the configuration in the file is user. A sample config is provided at `cmd/zetatool/config/sample_config.json` -There are two targets available: +The Config contains the rpcs needed for the tool to function, +if not provided the tool automatically uses the default rpcs.It is able to fetch the rpc needed using the chain ID -``` -filter-missed-btc: install-zetatool - ./tool/filter_missed_deposits/filter_missed_btc.sh +The command returns a ballot identifier for the given inbound hash. -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 -``` diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 6cc1c8d84c..2f41548258 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -14,6 +14,7 @@ import ( "github.com/zeta-chain/node/pkg/coin" solanacontracts "github.com/zeta-chain/node/pkg/contracts/solana" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/base" solanarpc "github.com/zeta-chain/node/zetaclient/chains/solana/rpc" "github.com/zeta-chain/node/zetaclient/compliance" zctx "github.com/zeta-chain/node/zetaclient/context" @@ -157,7 +158,7 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // FilterInboundEventsAndVote filters inbound events from a txResult and post a vote. func (ob *Observer) FilterInboundEventsAndVote(ctx context.Context, txResult *rpc.GetTransactionResult) error { // filter inbound events from txResult - events, err := ob.FilterInboundEvents(txResult) + events, err := FilterInboundEvents(txResult, ob.gatewayID, ob.Chain().ChainId, ob.Logger().Inbound) if err != nil { return errors.Wrapf(err, "error FilterInboundEvent") } @@ -181,7 +182,12 @@ func (ob *Observer) FilterInboundEventsAndVote(ctx context.Context, txResult *rp // - takes at one event (the first) per token (SOL or SPL) per transaction. // - takes at most two events (one SOL + one SPL) per transaction. // - ignores exceeding events. -func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]*clienttypes.InboundEvent, error) { +func FilterInboundEvents( + txResult *rpc.GetTransactionResult, + gatewayID solana.PublicKey, + senderChainID int64, + logger zerolog.Logger, +) ([]*clienttypes.InboundEvent, error) { // unmarshal transaction tx, err := txResult.Transaction.GetTransaction() if err != nil { @@ -203,14 +209,113 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* // get the program ID programPk, err := tx.Message.Program(instruction.ProgramIDIndex) if err != nil { - ob.Logger(). + logger.Err(err). + Msgf("no program found at index %d for sig %s", instruction.ProgramIDIndex, tx.Signatures[0]) + continue + } + + // skip instructions that are irrelevant to the gateway program invocation + if !programPk.Equals(gatewayID) { + continue + } + + // try parsing the instruction as a 'deposit' if not seen yet + if !seenDeposit { + deposit, err := solanacontracts.ParseInboundAsDeposit(tx, i, txResult.Slot) + if err != nil { + return nil, errors.Wrap(err, "error ParseInboundAsDeposit") + } else if deposit != nil { + seenDeposit = true + events = append(events, &clienttypes.InboundEvent{ + SenderChainID: senderChainID, + Sender: deposit.Sender, + Receiver: "", // receiver will be pulled out from memo later + TxOrigin: deposit.Sender, + Amount: deposit.Amount, + Memo: deposit.Memo, + BlockNumber: deposit.Slot, // instead of using block, Solana explorer uses slot for indexing + TxHash: tx.Signatures[0].String(), + Index: 0, // hardcode to 0 for Solana, not a EVM smart contract call + CoinType: coin.CoinType_Gas, + Asset: deposit.Asset, + }) + logger.Info().Msgf("FilterInboundEvents: deposit detected in sig %s instruction %d", tx.Signatures[0], i) + } + } else { + logger.Warn().Msgf("FilterInboundEvents: multiple deposits detected in sig %s instruction %d", tx.Signatures[0], i) + } + + // try parsing the instruction as a 'deposit_spl_token' if not seen yet + if !seenDepositSPL { + deposit, err := solanacontracts.ParseInboundAsDepositSPL(tx, i, txResult.Slot) + if err != nil { + return nil, errors.Wrap(err, "error ParseInboundAsDepositSPL") + } else if deposit != nil { + seenDepositSPL = true + events = append(events, &clienttypes.InboundEvent{ + SenderChainID: senderChainID, + Sender: deposit.Sender, + Receiver: "", // receiver will be pulled out from memo later + TxOrigin: deposit.Sender, + Amount: deposit.Amount, + Memo: deposit.Memo, + BlockNumber: deposit.Slot, // instead of using block, Solana explorer uses slot for indexing + TxHash: tx.Signatures[0].String(), + Index: 0, // hardcode to 0 for Solana, not a EVM smart contract call + CoinType: coin.CoinType_ERC20, + Asset: deposit.Asset, + }) + logger.Info().Msgf("FilterInboundEvents: SPL deposit detected in sig %s instruction %d", tx.Signatures[0], i) + } + } else { + logger.Warn().Msgf("FilterInboundEvents: multiple SPL deposits detected in sig %s instruction %d", tx.Signatures[0], i) + } + } + + return events, nil +} + +// FilterSolanaInboundEvents filters inbound events from a tx result. +// Note: for consistency with EVM chains, this method +// - takes at one event (the first) per token (SOL or SPL) per transaction. +// - takes at most two events (one SOL + one SPL) per transaction. +// - ignores exceeding events. +func FilterSolanaInboundEvents(txResult *rpc.GetTransactionResult, + logger *base.ObserverLogger, + gatewayID solana.PublicKey, + senderChainID int64) ([]*clienttypes.InboundEvent, error) { + if logger == nil { + return nil, errors.New("logger is nil") + } + // unmarshal transaction + tx, err := txResult.Transaction.GetTransaction() + if err != nil { + return nil, errors.Wrap(err, "error unmarshaling transaction") + } + + // there should be at least one instruction and one account, otherwise skip + if len(tx.Message.Instructions) <= 0 { + return nil, nil + } + + // create event array to collect all events in the transaction + seenDeposit := false + seenDepositSPL := false + events := make([]*clienttypes.InboundEvent, 0) + + // loop through instruction list to filter the 1st valid event + for i, instruction := range tx.Message.Instructions { + // get the program ID + programPk, err := tx.Message.Program(instruction.ProgramIDIndex) + if err != nil { + logger. Inbound.Err(err). Msgf("no program found at index %d for sig %s", instruction.ProgramIDIndex, tx.Signatures[0]) continue } // skip instructions that are irrelevant to the gateway program invocation - if !programPk.Equals(ob.gatewayID) { + if !programPk.Equals(gatewayID) { continue } @@ -222,7 +327,7 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* } else if deposit != nil { seenDeposit = true events = append(events, &clienttypes.InboundEvent{ - SenderChainID: ob.Chain().ChainId, + SenderChainID: senderChainID, Sender: deposit.Sender, Receiver: "", // receiver will be pulled out from memo later TxOrigin: deposit.Sender, @@ -234,11 +339,13 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* CoinType: coin.CoinType_Gas, Asset: deposit.Asset, }) - ob.Logger().Inbound.Info(). + logger.Inbound.Info().Msg("FilterInboundEvents: deposit detected") + + logger.Inbound.Info(). Msgf("FilterInboundEvents: deposit detected in sig %s instruction %d", tx.Signatures[0], i) } } else { - ob.Logger().Inbound.Warn(). + logger.Inbound.Warn(). Msgf("FilterInboundEvents: multiple deposits detected in sig %s instruction %d", tx.Signatures[0], i) } @@ -250,7 +357,7 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* } else if deposit != nil { seenDepositSPL = true events = append(events, &clienttypes.InboundEvent{ - SenderChainID: ob.Chain().ChainId, + SenderChainID: senderChainID, Sender: deposit.Sender, Receiver: "", // receiver will be pulled out from memo later TxOrigin: deposit.Sender, @@ -262,11 +369,11 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* CoinType: coin.CoinType_ERC20, Asset: deposit.Asset, }) - ob.Logger().Inbound.Info(). + logger.Inbound.Info(). Msgf("FilterInboundEvents: SPL deposit detected in sig %s instruction %d", tx.Signatures[0], i) } } else { - ob.Logger().Inbound.Warn(). + logger.Inbound.Warn(). Msgf("FilterInboundEvents: multiple SPL deposits detected in sig %s instruction %d", tx.Signatures[0], i) } } diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index 6ac1110404..c71e7cd87b 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -4,10 +4,12 @@ import ( "context" "testing" + "github.com/rs/zerolog" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/constant" + contracts "github.com/zeta-chain/node/pkg/contracts/solana" "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/solana/observer" @@ -65,14 +67,8 @@ func Test_FilterInboundEvents(t *testing.T) { chain := chains.SolanaDevnet txResult := testutils.LoadSolanaInboundTxResult(t, TestDataDir, chain.ChainId, txHash, false) - database, err := db.NewFromSqliteInMemory(true) - require.NoError(t, err) - - // create observer - chainParams := sample.ChainParams(chain.ChainId) - chainParams.GatewayAddress = testutils.OldSolanaGatewayAddressDevnet - - ob, err := observer.NewObserver(chain, nil, *chainParams, nil, nil, database, base.DefaultLogger(), nil) + // given gateway ID + gatewayID, _, err := contracts.ParseGatewayWithPDA(testutils.OldSolanaGatewayAddressDevnet) require.NoError(t, err) // expected result @@ -93,7 +89,7 @@ func Test_FilterInboundEvents(t *testing.T) { } t.Run("should filter inbound event deposit SOL", func(t *testing.T) { - events, err := ob.FilterInboundEvents(txResult) + events, err := observer.FilterInboundEvents(txResult, gatewayID, chain.ChainId, zerolog.Nop()) require.NoError(t, err) // check result @@ -102,6 +98,23 @@ func Test_FilterInboundEvents(t *testing.T) { }) } +func Test_FilterSolanaInboundEvents(t *testing.T) { + // load archived inbound deposit tx result + // https://explorer.solana.com/tx/MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j?cluster=devnet + txHash := "24GzWsxYCFcwwJ2rzAsWwWC85aYKot6Rz3jWnBP1GvoAg5A9f1WinYyvyKseYM52q6i3EkotZdJuQomGGq5oxRYr" + chain := chains.SolanaDevnet + txResult := testutils.LoadSolanaInboundTxResult(t, TestDataDir, chain.ChainId, txHash, false) + + // parse gateway ID + gatewayID, _, err := contracts.ParseGatewayWithPDA(testutils.OldSolanaGatewayAddressDevnet) + require.NoError(t, err) + + t.Run("should return early if logger is empty", func(t *testing.T) { + _, err = observer.FilterSolanaInboundEvents(txResult, nil, gatewayID, chain.ChainId) + require.ErrorContains(t, err, "logger is nil") + }) +} + func Test_BuildInboundVoteMsgFromEvent(t *testing.T) { // create test observer chain := chains.SolanaDevnet