From 59d5ba935fb8f1e65376658e7a0939fd25cb5fdb Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:34:09 +0200 Subject: [PATCH] feat(zetaclient): propagate context across codebase & refactor zetacore client (#2428) * Implement `retry` package * Refactor zetacore query methods * Refactor zetacore broadcast.go * Refactor zetacore client construction * Refactor zetacore client [WIP] * Fix debug cli command * Add ctx to evm observer * Add ctx to evm observer & signer * Add `bg` package * Add ctx to btc observer * Add ctx to supply checker * Add ctx to orchestrator & signers * Fix lint errors * Improve zetacore client configuration. Fix zetacore tests [WIP] * Fix zetacore client test cases * Fix other test cases * Resolve merge conflicts * Update changelog * Address PR comments [1] * Address PR comments [2] * Remove logger pointer from bg package * Minor fix * Minor code improvement * Converge config.New and config.NewConfig * Improve NewSigner logging * Add zctx.Copy() * Refactor Orchestrator shutdown logic * Fix retrier logic for monitors * Minor fix * Fix gosec * Update e2e readme * Fix BTC outbound typo * Fix typo * Address PR comments * Add test cases for `bg` * Handle ctx errors in `retry` --- changelog.md | 1 + cmd/zetaclientd/debug.go | 22 +- cmd/zetaclientd/init.go | 2 +- cmd/zetaclientd/keygen_tss.go | 45 +- cmd/zetaclientd/start.go | 58 +- cmd/zetaclientd/utils.go | 41 +- go.mod | 2 +- go.sum | 9 + pkg/bg/bg.go | 62 ++ pkg/bg/bg_test.go | 81 ++ pkg/retry/retry.go | 143 +++ pkg/retry/retry_test.go | 153 ++++ zetaclient/chains/base/logger.go | 8 +- zetaclient/chains/base/observer.go | 11 - zetaclient/chains/base/observer_test.go | 11 +- zetaclient/chains/base/signer.go | 23 +- zetaclient/chains/base/signer_test.go | 6 +- zetaclient/chains/bitcoin/observer/inbound.go | 59 +- .../chains/bitcoin/observer/observer.go | 70 +- .../chains/bitcoin/observer/observer_test.go | 9 +- .../chains/bitcoin/observer/outbound.go | 105 ++- .../chains/bitcoin/observer/outbound_test.go | 37 +- .../chains/bitcoin/rpc/rpc_live_test.go | 2 +- zetaclient/chains/bitcoin/signer/signer.go | 25 +- .../bitcoin/signer/signer_keysign_test.go | 5 +- .../chains/bitcoin/signer/signer_test.go | 3 - zetaclient/chains/evm/observer/inbound.go | 160 ++-- .../chains/evm/observer/inbound_test.go | 80 +- zetaclient/chains/evm/observer/observer.go | 79 +- .../chains/evm/observer/observer_test.go | 57 +- zetaclient/chains/evm/observer/outbound.go | 90 +- .../chains/evm/observer/outbound_test.go | 39 +- zetaclient/chains/evm/signer/outbound_data.go | 13 +- .../chains/evm/signer/outbound_data_test.go | 42 +- zetaclient/chains/evm/signer/signer.go | 136 +-- zetaclient/chains/evm/signer/signer_test.go | 180 ++-- zetaclient/chains/interfaces/interfaces.go | 129 ++- zetaclient/config/config.go | 2 +- zetaclient/config/config_chain.go | 101 +- zetaclient/config/types.go | 29 +- zetaclient/config/types_test.go | 1 - zetaclient/context/app_test.go | 22 +- zetaclient/context/context.go | 12 + zetaclient/context/context_test.go | 19 +- zetaclient/orchestrator/orchestrator.go | 97 +- zetaclient/orchestrator/orchestrator_test.go | 28 +- .../supplychecker/zeta_supply_checker.go | 70 +- zetaclient/testutils/mocks/chain_clients.go | 33 +- zetaclient/testutils/mocks/chain_signer.go | 4 + zetaclient/testutils/mocks/cometbft_client.go | 42 +- zetaclient/testutils/mocks/tss_signer.go | 5 +- zetaclient/testutils/mocks/zetacore_client.go | 862 ++++++++++++++---- .../testutils/mocks/zetacore_client_opts.go | 73 ++ zetaclient/tss/tss_signer.go | 17 +- zetaclient/zetacore/broadcast.go | 188 ++-- zetaclient/zetacore/broadcast_test.go | 44 +- zetaclient/zetacore/client.go | 338 +++++-- zetaclient/zetacore/client_monitor.go | 182 ++++ zetaclient/zetacore/client_query_authority.go | 18 + zetaclient/zetacore/client_query_cosmos.go | 88 ++ .../zetacore/client_query_crosschain.go | 194 ++++ zetaclient/zetacore/client_query_ethermint.go | 23 + .../zetacore/client_query_lightclient.go | 57 ++ zetaclient/zetacore/client_query_observer.go | 215 +++++ .../zetacore/client_query_tendermint.go | 35 + .../{query_test.go => client_query_test.go} | 662 +++++++------- zetaclient/zetacore/client_vote.go | 231 +++++ zetaclient/zetacore/client_worker.go | 43 + zetaclient/zetacore/constant.go | 25 +- zetaclient/zetacore/query.go | 563 ------------ zetaclient/zetacore/tx.go | 382 +------- zetaclient/zetacore/tx_test.go | 276 +++--- 72 files changed, 4347 insertions(+), 2632 deletions(-) create mode 100644 pkg/bg/bg.go create mode 100644 pkg/bg/bg_test.go create mode 100644 pkg/retry/retry.go create mode 100644 pkg/retry/retry_test.go delete mode 100644 zetaclient/config/types_test.go create mode 100644 zetaclient/testutils/mocks/zetacore_client_opts.go create mode 100644 zetaclient/zetacore/client_monitor.go create mode 100644 zetaclient/zetacore/client_query_authority.go create mode 100644 zetaclient/zetacore/client_query_cosmos.go create mode 100644 zetaclient/zetacore/client_query_crosschain.go create mode 100644 zetaclient/zetacore/client_query_ethermint.go create mode 100644 zetaclient/zetacore/client_query_lightclient.go create mode 100644 zetaclient/zetacore/client_query_observer.go create mode 100644 zetaclient/zetacore/client_query_tendermint.go rename zetaclient/zetacore/{query_test.go => client_query_test.go} (57%) create mode 100644 zetaclient/zetacore/client_vote.go create mode 100644 zetaclient/zetacore/client_worker.go delete mode 100644 zetaclient/zetacore/query.go diff --git a/changelog.md b/changelog.md index b02d9b345c..a6a975efa7 100644 --- a/changelog.md +++ b/changelog.md @@ -58,6 +58,7 @@ * [2375](https://github.com/zeta-chain/node/pull/2375) - improve & speedup code formatting * [2380](https://github.com/zeta-chain/node/pull/2380) - use `ChainInfo` in `authority` to allow dynamically support new chains * [2395](https://github.com/zeta-chain/node/pull/2395) - converge AppContext with ZetaCoreContext in zetaclient +* [2428](https://github.com/zeta-chain/node/pull/2428) - propagate context across codebase & refactor zetacore client ### Tests diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index 28a3932a8d..d28f5cb898 100644 --- a/cmd/zetaclientd/debug.go +++ b/cmd/zetaclientd/debug.go @@ -21,7 +21,7 @@ import ( btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" evmobserver "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/config" - clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) @@ -57,7 +57,8 @@ func debugCmd(_ *cobra.Command, args []string) error { return err } - appContext := clientcontext.New(cfg, zerolog.Nop()) + appContext := zctx.New(cfg, zerolog.Nop()) + ctx := zctx.WithAppContext(context.Background(), appContext) chainID, err := strconv.ParseInt(args[1], 10, 64) if err != nil { @@ -74,15 +75,16 @@ func debugCmd(_ *cobra.Command, args []string) error { "", debugArgs.zetaChainID, false, - nil) + zerolog.Nop(), + ) if err != nil { return err } - chainParams, err := client.GetChainParams() + chainParams, err := client.GetChainParams(ctx) if err != nil { return err } - tssEthAddress, err := client.GetEthTssAddress() + tssEthAddress, err := client.GetEVMTSSAddress(ctx) if err != nil { return err } @@ -148,19 +150,19 @@ func debugCmd(_ *cobra.Command, args []string) error { switch coinType { case coin.CoinType_Zeta: - ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenZeta(tx, receipt, false) + ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, false) if err != nil { return err } case coin.CoinType_ERC20: - ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenERC20(tx, receipt, false) + ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, false) if err != nil { return err } case coin.CoinType_Gas: - ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenGas(tx, receipt, false) + ballotIdentifier, err = evmObserver.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) if err != nil { return err } @@ -186,7 +188,7 @@ func debugCmd(_ *cobra.Command, args []string) error { return err } btcObserver.WithBtcClient(btcClient) - ballotIdentifier, err = btcObserver.CheckReceiptForBtcTxHash(inboundHash, false) + ballotIdentifier, err = btcObserver.CheckReceiptForBtcTxHash(ctx, inboundHash, false) if err != nil { return err } @@ -194,7 +196,7 @@ func debugCmd(_ *cobra.Command, args []string) error { fmt.Println("BallotIdentifier : ", ballotIdentifier) // query ballot - ballot, err := client.GetBallot(ballotIdentifier) + ballot, err := client.GetBallot(ctx, ballotIdentifier) if err != nil { return err } diff --git a/cmd/zetaclientd/init.go b/cmd/zetaclientd/init.go index c41f669c44..c6c231bf77 100644 --- a/cmd/zetaclientd/init.go +++ b/cmd/zetaclientd/init.go @@ -78,7 +78,7 @@ func Initialize(_ *cobra.Command, _ []string) error { } //Create new config struct - configData := config.New() + configData := config.New(true) //Validate Peer eg. /ip4/172.0.2.1/tcp/6668/p2p/16Uiu2HAmACG5DtqmQsHtXg4G2sLS65ttv84e7MrL4kapkjfmhxAp if len(initArgs.peer) != 0 { diff --git a/cmd/zetaclientd/keygen_tss.go b/cmd/zetaclientd/keygen_tss.go index 63b5d98041..05e6d7f01f 100644 --- a/cmd/zetaclientd/keygen_tss.go +++ b/cmd/zetaclientd/keygen_tss.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/hex" "encoding/json" "errors" @@ -16,14 +17,14 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" mc "github.com/zeta-chain/zetacore/zetaclient/tss" "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) func GenerateTss( - appContext *context.AppContext, + ctx context.Context, logger zerolog.Logger, client *zetacore.Client, peers p2p.AddrList, @@ -31,20 +32,27 @@ func GenerateTss( ts *metrics.TelemetryServer, tssHistoricalList []observertypes.TSS, tssPassword string, - hotkeyPassword string) (*mc.TSS, error) { + hotkeyPassword string, +) (*mc.TSS, error) { + app, err := zctx.FromContext(ctx) + if err != nil { + return nil, err + } + keygenLogger := logger.With().Str("module", "keygen").Logger() // Bitcoin chain ID is currently used for using the correct signature format // TODO: remove this once we have a better way to determine the signature format // https://github.com/zeta-chain/node/issues/1397 bitcoinChainID := chains.BitcoinRegtest.ChainId - btcChain, _, btcEnabled := appContext.GetBTCChainAndConfig() + btcChain, _, btcEnabled := app.GetBTCChainAndConfig() if btcEnabled { bitcoinChainID = btcChain.ChainId } tss, err := mc.NewTSS( - appContext, + ctx, + app, peers, priKey, preParams, @@ -74,7 +82,7 @@ func GenerateTss( // This loop will try keygen at the keygen block and then wait for keygen to be successfully reported by all nodes before breaking out of the loop. // If keygen is unsuccessful, it will reset the triedKeygenAtBlock flag and try again at a new keygen block. - keyGen := appContext.GetKeygen() + keyGen := app.GetKeygen() if keyGen.Status == observertypes.KeygenStatus_KeyGenSuccess { return tss, nil } @@ -86,7 +94,7 @@ func GenerateTss( // Try generating TSS at keygen block , only when status is pending keygen and generation has not been tried at the block if keyGen.Status == observertypes.KeygenStatus_PendingKeygen { // Return error if RPC is not working - currentBlock, err := client.GetBlockHeight() + currentBlock, err := client.GetBlockHeight(ctx) if err != nil { keygenLogger.Error().Err(err).Msg("GetBlockHeight RPC error") continue @@ -101,16 +109,21 @@ func GenerateTss( if currentBlock > lastBlock { lastBlock = currentBlock keygenLogger.Info(). - Msgf("Waiting For Keygen Block to arrive or new keygen block to be set. Keygen Block : %d Current Block : %d ChainID %s ", keyGen.BlockNumber, currentBlock, appContext.Config().ChainID) + Msgf("Waiting For Keygen Block to arrive or new keygen block to be set. Keygen Block : %d Current Block : %d ChainID %s ", keyGen.BlockNumber, currentBlock, app.Config().ChainID) } continue } // Try keygen only once at a particular block, irrespective of whether it is successful or failure triedKeygenAtBlock = true - err = keygenTss(keyGen, tss, keygenLogger) + err = keygenTss(ctx, keyGen, tss, keygenLogger) if err != nil { keygenLogger.Error().Err(err).Msg("keygenTss error") - tssFailedVoteHash, err := client.SetTSS("", keyGen.BlockNumber, chains.ReceiveStatus_failed) + tssFailedVoteHash, err := client.PostVoteTSS( + ctx, + "", + keyGen.BlockNumber, + chains.ReceiveStatus_failed, + ) if err != nil { keygenLogger.Error().Err(err).Msg("Failed to broadcast Failed TSS Vote to zetacore") return nil, err @@ -128,7 +141,8 @@ func GenerateTss( } // If TSS is successful , broadcast the vote to zetacore and set Pubkey - tssSuccessVoteHash, err := client.SetTSS( + tssSuccessVoteHash, err := client.PostVoteTSS( + ctx, newTss.CurrentPubkey, keyGen.BlockNumber, chains.ReceiveStatus_success, @@ -155,7 +169,7 @@ func GenerateTss( return nil, errors.New("unexpected state for TSS generation") } -func keygenTss(keyGen observertypes.Keygen, tss *mc.TSS, keygenLogger zerolog.Logger) error { +func keygenTss(ctx context.Context, keyGen observertypes.Keygen, tss *mc.TSS, keygenLogger zerolog.Logger) error { keygenLogger.Info().Msgf("Keygen at blocknum %d , TSS signers %s ", keyGen.BlockNumber, keyGen.GranteePubkeys) var req keygen.Request req = keygen.NewRequest(keyGen.GranteePubkeys, keyGen.BlockNumber, "0.14.0") @@ -168,7 +182,12 @@ func keygenTss(keyGen observertypes.Keygen, tss *mc.TSS, keygenLogger zerolog.Lo return err } index := fmt.Sprintf("keygen-%s-%d", digest, keyGen.BlockNumber) - zetaHash, err := tss.ZetacoreClient.PostBlameData(&res.Blame, tss.ZetacoreClient.Chain().ChainId, index) + zetaHash, err := tss.ZetacoreClient.PostVoteBlameData( + ctx, + &res.Blame, + tss.ZetacoreClient.Chain().ChainId, + index, + ) if err != nil { keygenLogger.Error().Err(err).Msg("error sending blame data to core") return err diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 3546cec8ad..4dfbeadf3e 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "context" "encoding/json" "fmt" "io" @@ -26,7 +27,7 @@ import ( observerTypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/orchestrator" ) @@ -44,8 +45,7 @@ func init() { } func start(_ *cobra.Command, _ []string) error { - err := setHomeDir() - if err != nil { + if err := setHomeDir(); err != nil { return err } @@ -62,24 +62,25 @@ func start(_ *cobra.Command, _ []string) error { if err != nil { return err } + logger, err := base.InitLogger(cfg) if err != nil { - log.Error().Err(err).Msg("InitLogger failed") - return err + return errors.Wrap(err, "initLogger failed") } - //Wait until zetacore has started + // Wait until zetacore has started if len(cfg.Peer) != 0 { - err := validatePeer(cfg.Peer) - if err != nil { - log.Error().Err(err).Msg("invalid peer") - return err + if err := validatePeer(cfg.Peer); err != nil { + return errors.Wrap(err, "unable to validate peer") } } masterLogger := logger.Std startLogger := masterLogger.With().Str("module", "startup").Logger() + appContext := zctx.New(cfg, masterLogger) + ctx := zctx.WithAppContext(context.Background(), appContext) + // Wait until zetacore is up waitForZetaCore(cfg, startLogger) startLogger.Info().Msgf("Zetacore is ready, trying to connect to %s", cfg.Peer) @@ -95,15 +96,14 @@ func start(_ *cobra.Command, _ []string) error { // CreateZetacoreClient: zetacore client is used for all communication to zetacore , which this client connects to. // Zetacore accumulates votes , and provides a centralized source of truth for all clients - zetacoreClient, err := CreateZetacoreClient(cfg, telemetryServer, hotkeyPass) + zetacoreClient, err := CreateZetacoreClient(cfg, hotkeyPass, masterLogger) if err != nil { startLogger.Error().Err(err).Msg("CreateZetacoreClient error") return err } // Wait until zetacore is ready to create blocks - err = zetacoreClient.WaitForZetacoreToCreateBlocks() - if err != nil { + if err = zetacoreClient.WaitForZetacoreToCreateBlocks(ctx); err != nil { startLogger.Error().Err(err).Msg("WaitForZetacoreToCreateBlocks error") return err } @@ -117,7 +117,7 @@ func start(_ *cobra.Command, _ []string) error { } // cross-check chainid - res, err := zetacoreClient.GetNodeInfo() + res, err := zetacoreClient.GetNodeInfo(ctx) if err != nil { startLogger.Error().Err(err).Msg("GetNodeInfo error") return err @@ -144,15 +144,14 @@ func start(_ *cobra.Command, _ []string) error { startLogger.Debug().Msgf("CreateAuthzSigner is ready") // Initialize core parameters from zetacore - appContext := context.New(cfg, masterLogger) - err = zetacoreClient.UpdateZetacoreContext(appContext, true, startLogger) + err = zetacoreClient.UpdateZetacoreContext(ctx, appContext, true, startLogger) if err != nil { startLogger.Error().Err(err).Msg("Error getting core parameters") return err } startLogger.Info().Msgf("Config is updated from zetacore %s", maskCfg(cfg)) - go zetacoreClient.ZetacoreContextUpdater(appContext) + go zetacoreClient.UpdateZetacoreContextWorker(ctx, appContext) // Generate TSS address . The Tss address is generated through Keygen ceremony. The TSS key is used to sign all outbound transactions . // The hotkeyPk is private key for the Hotkey. The Hotkey is used to sign all inbound transactions @@ -195,14 +194,14 @@ func start(_ *cobra.Command, _ []string) error { metrics.LastStartTime.SetToCurrentTime() var tssHistoricalList []observerTypes.TSS - tssHistoricalList, err = zetacoreClient.GetTssHistory() + tssHistoricalList, err = zetacoreClient.GetTSSHistory(ctx) if err != nil { startLogger.Error().Err(err).Msg("GetTssHistory error") } telemetryServer.SetIPAddress(cfg.PublicIP) tss, err := GenerateTss( - appContext, + ctx, masterLogger, zetacoreClient, peers, @@ -237,7 +236,7 @@ func start(_ *cobra.Command, _ []string) error { // Update Current TSS value from zetacore, if TSS keygen is successful, the TSS address is set on zeta-core // Returns err if the RPC call fails as zeta client needs the current TSS address to be set // This is only needed in case of a new Keygen , as the TSS address is set on zetacore only after the keygen is successful i.e enough votes have been broadcast - currentTss, err := zetacoreClient.GetCurrentTss() + currentTss, err := zetacoreClient.GetCurrentTSS(ctx) if err != nil { startLogger.Error().Err(err).Msg("GetCurrentTSS error") return err @@ -254,7 +253,7 @@ func start(_ *cobra.Command, _ []string) error { startLogger.Error().Msgf("No chains enabled in updated config %s ", cfg.String()) } - observerList, err := zetacoreClient.GetObserverList() + observerList, err := zetacoreClient.GetObserverList(ctx) if err != nil { startLogger.Error().Err(err).Msg("GetObserverList error") return err @@ -268,7 +267,7 @@ func start(_ *cobra.Command, _ []string) error { } // CreateSignerMap: This creates a map of all signers for each chain . Each signer is responsible for signing transactions for a particular chain - signerMap, err := CreateSignerMap(appContext, tss, logger, telemetryServer) + signerMap, err := CreateSignerMap(ctx, appContext, tss, logger, telemetryServer) if err != nil { log.Error().Err(err).Msg("CreateSignerMap") return err @@ -282,7 +281,7 @@ func start(_ *cobra.Command, _ []string) error { dbpath := filepath.Join(userDir, ".zetaclient/chainobserver") // Creates a map of all chain observers for each chain. Each chain observer is responsible for observing events on the chain and processing them. - observerMap, err := CreateChainObserverMap(appContext, zetacoreClient, tss, dbpath, logger, telemetryServer) + observerMap, err := CreateChainObserverMap(ctx, appContext, zetacoreClient, tss, dbpath, logger, telemetryServer) if err != nil { startLogger.Err(err).Msg("CreateChainObserverMap") return err @@ -294,13 +293,20 @@ func start(_ *cobra.Command, _ []string) error { } else { startLogger.Debug().Msgf("Node %s is an active observer starting external chain observers", zetacoreClient.GetKeys().GetOperatorAddress().String()) for _, observer := range observerMap { - observer.Start() + observer.Start(ctx) } } // Orchestrator wraps the zetacore client and adds the observers and signer maps to it . This is the high level object used for CCTX interactions - orchestrator := orchestrator.NewOrchestrator(zetacoreClient, signerMap, observerMap, masterLogger, telemetryServer) - err = orchestrator.MonitorCore(appContext) + orchestrator := orchestrator.NewOrchestrator( + ctx, + zetacoreClient, + signerMap, + observerMap, + masterLogger, + telemetryServer, + ) + err = orchestrator.MonitorCore(ctx) if err != nil { startLogger.Error().Err(err).Msg("Orchestrator failed to start") return err diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 99f6e03e59..01021d4e74 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -1,11 +1,13 @@ package main import ( + gocontext "context" "fmt" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + "github.com/rs/zerolog" "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -26,11 +28,7 @@ func CreateAuthzSigner(granter string, grantee sdk.AccAddress) { authz.SetupAuthZSignerList(granter, grantee) } -func CreateZetacoreClient( - cfg config.Config, - telemetry *metrics.TelemetryServer, - hotkeyPassword string, -) (*zetacore.Client, error) { +func CreateZetacoreClient(cfg config.Config, hotkeyPassword string, logger zerolog.Logger) (*zetacore.Client, error) { hotKey := cfg.AuthzHotkey if cfg.HsmMode { hotKey = cfg.HsmHotKey @@ -50,7 +48,7 @@ func CreateZetacoreClient( k := keys.NewKeysWithKeybase(kb, granterAddreess, cfg.AuthzHotkey, hotkeyPassword) - client, err := zetacore.NewClient(k, chainIP, hotKey, cfg.ChainID, cfg.HsmMode, telemetry) + client, err := zetacore.NewClient(k, chainIP, hotKey, cfg.ChainID, cfg.HsmMode, logger) if err != nil { return nil, err } @@ -60,6 +58,7 @@ func CreateZetacoreClient( // CreateSignerMap creates a map of ChainSigners for all chains in the config func CreateSignerMap( + ctx gocontext.Context, appContext *context.AppContext, tss interfaces.TSSSigner, logger base.Logger, @@ -77,11 +76,14 @@ func CreateSignerMap( logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } + + chainName := evmConfig.Chain.ChainName.String() mpiAddress := ethcommon.HexToAddress(evmChainParams.ConnectorContractAddress) erc20CustodyAddress := ethcommon.HexToAddress(evmChainParams.Erc20CustodyContractAddress) + signer, err := evmsigner.NewSigner( + ctx, evmConfig.Chain, - appContext, tss, ts, logger, @@ -89,21 +91,28 @@ func CreateSignerMap( config.GetConnectorABI(), config.GetERC20CustodyABI(), mpiAddress, - erc20CustodyAddress) + erc20CustodyAddress, + ) if err != nil { - logger.Std.Error().Err(err).Msgf("NewEVMSigner error for chain %s", evmConfig.Chain.String()) + logger.Std.Error().Err(err).Msgf("NewSigner error for EVM chain %q", chainName) continue } + signerMap[evmConfig.Chain.ChainId] = signer + logger.Std.Info().Msgf("NewSigner succeeded for EVM chain %q", chainName) } + // BTC signer - btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() - if enabled { - signer, err := btcsigner.NewSigner(btcChain, appContext, tss, ts, logger, btcConfig) + btcChain, btcConfig, btcEnabled := appContext.GetBTCChainAndConfig() + if btcEnabled { + chainName := btcChain.ChainName.String() + + signer, err := btcsigner.NewSigner(btcChain, tss, ts, logger, btcConfig) if err != nil { - logger.Std.Error().Err(err).Msgf("NewBTCSigner error for chain %s", btcChain.String()) + logger.Std.Error().Err(err).Msgf("NewSigner error for BTC chain %q", chainName) } else { signerMap[btcChain.ChainId] = signer + logger.Std.Info().Msgf("NewSigner succeeded for BTC chain %q", chainName) } } @@ -112,6 +121,7 @@ func CreateSignerMap( // CreateChainObserverMap creates a map of ChainObservers for all chains in the config func CreateChainObserverMap( + ctx gocontext.Context, appContext *context.AppContext, zetacoreClient *zetacore.Client, tss interfaces.TSSSigner, @@ -134,16 +144,16 @@ func CreateChainObserverMap( // create EVM client evmClient, err := ethclient.Dial(evmConfig.Endpoint) if err != nil { - logger.Std.Error().Err(err).Msgf("error dailing endpoint %s", evmConfig.Endpoint) + logger.Std.Error().Err(err).Msgf("error dailing endpoint %q", evmConfig.Endpoint) continue } // create EVM chain observer observer, err := evmobserver.NewObserver( + ctx, evmConfig, evmClient, *chainParams, - appContext, zetacoreClient, tss, dbpath, @@ -175,7 +185,6 @@ func CreateChainObserverMap( btcChain, btcClient, *chainParams, - appContext, zetacoreClient, tss, dbpath, diff --git a/go.mod b/go.mod index 7e6324fd78..7ec9517ab2 100644 --- a/go.mod +++ b/go.mod @@ -157,7 +157,7 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect - github.com/cenkalti/backoff/v4 v4.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect diff --git a/go.sum b/go.sum index 4e076f69ff..877eda0377 100644 --- a/go.sum +++ b/go.sum @@ -248,6 +248,7 @@ github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1: github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -382,6 +383,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= @@ -420,6 +423,7 @@ github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b80 github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= github.com/cockroachdb/errors v1.10.0 h1:lfxS8zZz1+OjtV4MtNWgboi/W5tyLEB6VQZBXN+0VUU= @@ -1008,6 +1012,7 @@ github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8 github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= +github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= @@ -1055,6 +1060,7 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= @@ -1259,6 +1265,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -1508,6 +1515,7 @@ github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZ github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= @@ -1559,6 +1567,7 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= diff --git a/pkg/bg/bg.go b/pkg/bg/bg.go new file mode 100644 index 0000000000..85d85964cf --- /dev/null +++ b/pkg/bg/bg.go @@ -0,0 +1,62 @@ +// Package bg provides primitives for the background tasks +package bg + +import ( + "context" + "fmt" + + "github.com/rs/zerolog" +) + +type config struct { + name string + logger zerolog.Logger +} + +type Opt func(*config) + +func WithName(name string) Opt { + return func(cfg *config) { cfg.name = name } +} + +func WithLogger(logger zerolog.Logger) Opt { + return func(cfg *config) { cfg.logger = logger } +} + +// Work emits a new task in the background +func Work(ctx context.Context, f func(context.Context) error, opts ...Opt) { + cfg := config{ + name: "", + logger: zerolog.Nop(), + } + + for _, opt := range opts { + opt(&cfg) + } + + go func() { + defer func() { + if r := recover(); r != nil { + err := fmt.Errorf("recovered from PANIC in background task: %v", r) + logError(err, cfg) + } + }() + + if err := f(ctx); err != nil { + logError(err, cfg) + } + }() +} + +func logError(err error, cfg config) { + if err == nil { + return + } + + name := cfg.name + if name == "" { + name = "unknown" + } + + cfg.logger.Error().Err(err).Str("worker.name", name).Msgf("Background task failed") +} diff --git a/pkg/bg/bg_test.go b/pkg/bg/bg_test.go new file mode 100644 index 0000000000..c55b6287f9 --- /dev/null +++ b/pkg/bg/bg_test.go @@ -0,0 +1,81 @@ +package bg + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func TestWork(t *testing.T) { + ctx := context.Background() + + t.Run("basic case", func(t *testing.T) { + // ARRANGE + signal := make(chan struct{}) + + // ACT + Work(ctx, func(ctx context.Context) error { + // simulate some work + time.Sleep(100 * time.Millisecond) + close(signal) + return nil + }) + + // ASSERT + <-signal + assertChanClosed(t, signal) + }) + + t.Run("with name and logger", func(t *testing.T) { + // ARRANGE + // Given a logger + out := &bytes.Buffer{} + logger := zerolog.New(out) + + // And a call returning an error + call := func(ctx context.Context) error { + time.Sleep(100 * time.Millisecond) + return fmt.Errorf("oopsie") + } + + // ACT + Work(ctx, call, WithName("hello"), WithLogger(logger)) + time.Sleep(200 * time.Millisecond) + + // Check the log output + const expected = `{"level":"error","error":"oopsie","worker.name":"hello","message":"Background task failed"}` + assert.JSONEq(t, expected, out.String()) + }) + + t.Run("panic recovery", func(t *testing.T) { + // ARRANGE + // Given a logger + out := &bytes.Buffer{} + logger := zerolog.New(out) + + // And a call that has panic + call := func(ctx context.Context) error { + panic("press F") + return nil + } + + // ACT + Work(ctx, call, WithLogger(logger)) + time.Sleep(100 * time.Millisecond) + + // Check the log output + const expected = `{"level":"error","error":"recovered from PANIC in background task: press F",` + + `"worker.name":"unknown","message":"Background task failed"}` + assert.JSONEq(t, expected, out.String()) + }) +} + +func assertChanClosed(t *testing.T, ch <-chan struct{}) { + _, ok := <-ch + assert.False(t, ok, "channel is not closed") +} diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go new file mode 100644 index 0000000000..291ceafe23 --- /dev/null +++ b/pkg/retry/retry.go @@ -0,0 +1,143 @@ +// Package retry provides a generic retry mechanism with exponential backoff. +// +// Example: +// +// ctx := context.Background() +// client := foobar.NewClient() +// +// err := retry.Do(func() error { +// err := client.UpdateConfig(ctx, map[string]any{"key": "value"}) +// +// // will be retied +// if errors.Is(err, foobar.ErrTxConflict) { +// return retry.Retryable(err) +// } +// +// return err +// }) +package retry + +import ( + "context" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/pkg/errors" +) + +type Backoff = backoff.BackOff + +type Callback func() error + +type TypedCallback[T any] func() (T, error) + +type errRetryable struct { + error +} + +// DefaultBackoff returns a default backoff strategy with 5 exponential retries. +func DefaultBackoff() Backoff { + bo := backoff.NewExponentialBackOff( + backoff.WithInitialInterval(50*time.Millisecond), + backoff.WithMaxInterval(500*time.Millisecond), + backoff.WithMultiplier(1.8), + ) + + return backoff.WithMaxRetries(bo, 5) +} + +// Do executes the callback function with the default backoff config. +// It will retry a callback ONLY if error is retryable. +func Do(cb Callback) error { + return DoWithBackoff(cb, DefaultBackoff()) +} + +// DoWithBackoff executes the callback function with provided backoff config. +// It will retry a callback ONLY if error is retryable. +func DoWithBackoff(cb Callback, bo Backoff) error { + for { + err := cb() + if err == nil { + return nil + } + + var errRetry errRetryable + isRetryable := errors.As(err, &errRetry) + if !isRetryable { + return err + } + + sleepFor := bo.NextBackOff() + if sleepFor == backoff.Stop { + return errors.Wrap(err, "retry limit exceeded") + } + + time.Sleep(sleepFor) + } +} + +// DoTyped is typed version of Do that returns a value along with an error. +// It will retry a callback ONLY if error is retryable. +func DoTyped[T any](cb TypedCallback[T]) (T, error) { + return DoTypedWithBackoff(cb, DefaultBackoff()) +} + +// DoTypedWithBackoff is typed version of DoWithBackoff that returns a value along with an error. +// It will retry a callback ONLY if error is retryable. +func DoTypedWithBackoff[T any](cb TypedCallback[T], bo Backoff) (T, error) { + var ( + result T + err error + ) + + // #nosec G703 error is propagated + _ = DoWithBackoff(func() error { + result, err = cb() + return err + }, bo) + + return result, err +} + +// DoTypedWithRetry is DoTyped but ANY error is retried. +func DoTypedWithRetry[T any](cb TypedCallback[T]) (T, error) { + wrapper := func() (T, error) { + return RetryTyped(cb()) + } + + return DoTypedWithBackoffAndRetry(wrapper, DefaultBackoff()) +} + +// DoTypedWithBackoffAndRetry is DoTypedWithBackoff but ANY error is retried. +func DoTypedWithBackoffAndRetry[T any](cb TypedCallback[T], bo Backoff) (T, error) { + wrapper := func() (T, error) { + return RetryTyped(cb()) + } + + return DoTypedWithBackoff(wrapper, bo) +} + +// Retry wraps error to mark it as retryable. Skips retry for context errors. +func Retry(err error) error { + switch { + case err == nil: + return nil + case errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded): + // do not retry context errors + return err + default: + return errRetryable{error: err} + } +} + +// RetryTyped wraps error to mark it as retryable +// +//goland:noinspection GoNameStartsWithPackageName +//nolint:revive +func RetryTyped[T any](result T, err error) (T, error) { + if err == nil { + return result, nil + } + + return result, Retry(err) +} diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go new file mode 100644 index 0000000000..7bb983a16d --- /dev/null +++ b/pkg/retry/retry_test.go @@ -0,0 +1,153 @@ +package retry + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDo(t *testing.T) { + t.Parallel() + + t.Run("no error", func(t *testing.T) { + err := Do(func() error { return nil }) + + assert.NoError(t, err) + }) + + t.Run("non-retryable error", func(t *testing.T) { + var counter int + err := Do(func() error { + counter++ + return errors.New("something went wrong") + }) + + assert.Equal(t, 1, counter) + assert.ErrorContains(t, err, "something went wrong") + }) + + t.Run("retryable error suddenly became non-retryable", func(t *testing.T) { + var counter int + err := Do(func() error { + err := errors.New("something went wrong") + + counter++ + if counter < 3 { + return Retry(err) + } + + return err + }) + + assert.Equal(t, 3, counter) + assert.ErrorContains(t, err, "something went wrong") + }) + + t.Run("retryable code eventually works", func(t *testing.T) { + var counter int + err := Do(func() error { + err := errors.New("something went wrong") + + counter++ + if counter < 3 { + return Retry(err) + } + + return nil + }) + + assert.Equal(t, 3, counter) + assert.NoError(t, err) + }) + + t.Run("retry limit exceeded", func(t *testing.T) { + start := time.Now() + + var counter int + err := Do(func() error { + trackTime(t, start) + err := errors.New("something went wrong") + + counter++ + return Retry(err) + }) + + assert.ErrorContains(t, err, "retry limit exceeded") + }) + + t.Run("context errors are non-retryable", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + var counter int + err := Do(func() error { + time.Sleep(100 * time.Millisecond) + + if err := ctx.Err(); err != nil { + return err + } + + counter++ + + return nil + }) + + assert.Equal(t, 0, counter) + assert.ErrorIs(t, err, context.DeadlineExceeded) + }) +} + +func TestDoTyped(t *testing.T) { + t.Parallel() + + type myType struct { + Value string + } + + t.Run("no error", func(t *testing.T) { + result, err := DoTyped(func() (myType, error) { + return myType{Value: "abc"}, nil + }) + + assert.NoError(t, err) + assert.Equal(t, "abc", result.Value) + }) + + t.Run("fails", func(t *testing.T) { + var counter int + + result, err := DoTyped(func() (myType, error) { + counter++ + return myType{}, errors.New("something went wrong") + }) + + assert.ErrorContains(t, err, "something went wrong") + assert.Empty(t, result) + assert.Equal(t, 1, counter) + }) + + t.Run("recovers", func(t *testing.T) { + var counter int + + result, err := DoTyped(func() (myType, error) { + counter++ + if counter == 4 { + return myType{Value: "abc"}, nil + } + return myType{}, Retry(errors.New("something went wrong")) + }) + + assert.NoError(t, err) + assert.Equal(t, "abc", result.Value) + assert.Equal(t, 4, counter) + }) +} + +func trackTime(t *testing.T, from time.Time) { + duration := time.Since(from) + + t.Logf("Retrier invokation: t = %dms", duration.Milliseconds()) +} diff --git a/zetaclient/chains/base/logger.go b/zetaclient/chains/base/logger.go index eeffcfab3b..94579df5ae 100644 --- a/zetaclient/chains/base/logger.go +++ b/zetaclient/chains/base/logger.go @@ -58,20 +58,22 @@ func InitLogger(cfg config.Config) (Logger, error) { return DefaultLogger(), err } + level := zerolog.Level(cfg.LogLevel) + // create loggers based on configured level and format var std zerolog.Logger var compliance zerolog.Logger switch cfg.LogFormat { case "json": - std = zerolog.New(os.Stdout).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() - compliance = zerolog.New(file).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() + std = zerolog.New(os.Stdout).Level(level).With().Timestamp().Logger() + compliance = zerolog.New(file).Level(level).With().Timestamp().Logger() case "text": std = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}). Level(zerolog.Level(cfg.LogLevel)). With(). Timestamp(). Logger() - compliance = zerolog.New(file).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() + compliance = zerolog.New(file).Level(level).With().Timestamp().Logger() default: std = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}) compliance = zerolog.New(file).With().Timestamp().Logger() diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index edfef83629..73a1ba3cc2 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -18,7 +18,6 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) @@ -45,9 +44,6 @@ type Observer struct { // chainParams contains the dynamic chain parameters of the observed chain chainParams observertypes.ChainParams - // appContext contains context data for zetaclient & zetacore (e.g. supported chains) - appContext *context.AppContext - // zetacoreClient is the client to interact with ZetaChain zetacoreClient interfaces.ZetacoreClient @@ -87,7 +83,6 @@ type Observer struct { func NewObserver( chain chains.Chain, chainParams observertypes.ChainParams, - appContext *context.AppContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, blockCacheSize int, @@ -98,7 +93,6 @@ func NewObserver( ob := Observer{ chain: chain, chainParams: chainParams, - appContext: appContext, zetacoreClient: zetacoreClient, tss: tss, lastBlock: 0, @@ -164,11 +158,6 @@ func (ob *Observer) WithChainParams(params observertypes.ChainParams) *Observer return ob } -// AppContext returns the zetacore context for the observer. -func (ob *Observer) AppContext() *context.AppContext { - return ob.appContext -} - // ZetacoreClient returns the zetacore client for the observer. func (ob *Observer) ZetacoreClient() interfaces.ZetacoreClient { return ob.zetacoreClient diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index e6d5a088a9..923c4481a1 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -26,8 +26,7 @@ func createObserver(t *testing.T) *base.Observer { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) - appContext := context.New(config.NewConfig(), zerolog.Nop()) - zetacoreClient := mocks.NewMockZetacoreClient() + zetacoreClient := mocks.NewZetacoreClient(t) tss := mocks.NewTSSMainnet() // create observer @@ -35,7 +34,6 @@ func createObserver(t *testing.T) *base.Observer { ob, err := base.NewObserver( chain, chainParams, - appContext, zetacoreClient, tss, base.DefaultBlockCacheSize, @@ -52,8 +50,8 @@ func TestNewObserver(t *testing.T) { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) - appContext := context.New(config.NewConfig(), zerolog.Nop()) - zetacoreClient := mocks.NewMockZetacoreClient() + appContext := context.New(config.New(false), zerolog.Nop()) + zetacoreClient := mocks.NewZetacoreClient(t) tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize headersCacheSize := base.DefaultHeaderCacheSize @@ -114,7 +112,6 @@ func TestNewObserver(t *testing.T) { ob, err := base.NewObserver( tt.chain, tt.chainParams, - tt.appContext, tt.zetacoreClient, tt.tss, tt.blockCacheSize, @@ -166,7 +163,7 @@ func TestObserverGetterAndSetter(t *testing.T) { ob := createObserver(t) // update zetacore client - newZetacoreClient := mocks.NewMockZetacoreClient() + newZetacoreClient := mocks.NewZetacoreClient(t) ob = ob.WithZetacoreClient(newZetacoreClient) require.Equal(t, newZetacoreClient, ob.ZetacoreClient()) }) diff --git a/zetaclient/chains/base/signer.go b/zetaclient/chains/base/signer.go index 2ac38e048d..2545c3639a 100644 --- a/zetaclient/chains/base/signer.go +++ b/zetaclient/chains/base/signer.go @@ -5,7 +5,6 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" ) @@ -15,8 +14,6 @@ type Signer struct { // chain contains static information about the external chain chain chains.Chain - appContext *context.AppContext - // tss is the TSS signer tss interfaces.TSSSigner @@ -32,18 +29,11 @@ type Signer struct { } // NewSigner creates a new base signer -func NewSigner( - chain chains.Chain, - appContext *context.AppContext, - tss interfaces.TSSSigner, - ts *metrics.TelemetryServer, - logger Logger, -) *Signer { +func NewSigner(chain chains.Chain, tss interfaces.TSSSigner, ts *metrics.TelemetryServer, logger Logger) *Signer { return &Signer{ - chain: chain, - appContext: appContext, - tss: tss, - ts: ts, + chain: chain, + tss: tss, + ts: ts, logger: Logger{ Std: logger.Std.With().Int64("chain", chain.ChainId).Str("module", "signer").Logger(), Compliance: logger.Compliance, @@ -62,11 +52,6 @@ func (s *Signer) WithChain(chain chains.Chain) *Signer { return s } -// AppContext returns the zetacore context for the signer -func (s *Signer) AppContext() *context.AppContext { - return s.appContext -} - // Tss returns the tss signer for the signer func (s *Signer) TSS() interfaces.TSSSigner { return s.tss diff --git a/zetaclient/chains/base/signer_test.go b/zetaclient/chains/base/signer_test.go index a0e2696b92..e35ed01792 100644 --- a/zetaclient/chains/base/signer_test.go +++ b/zetaclient/chains/base/signer_test.go @@ -3,13 +3,10 @@ package base_test import ( "testing" - "github.com/rs/zerolog" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/base" - "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -18,12 +15,11 @@ import ( func createSigner(_ *testing.T) *base.Signer { // constructor parameters chain := chains.Ethereum - appContext := context.New(config.NewConfig(), zerolog.Nop()) tss := mocks.NewTSSMainnet() logger := base.DefaultLogger() // create signer - return base.NewSigner(chain, appContext, tss, nil, logger) + return base.NewSigner(chain, tss, nil, logger) } func TestNewSigner(t *testing.T) { diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 1d528a7c62..6ecf52f159 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -1,6 +1,7 @@ package observer import ( + "context" "encoding/hex" "fmt" "math/big" @@ -20,6 +21,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/types" "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) @@ -27,11 +29,16 @@ import ( // WatchInbound watches Bitcoin chain for inbounds on a ticker // It starts a ticker and run ObserveInbound // TODO(revamp): move all ticker related methods in the same file -func (ob *Observer) WatchInbound() { +func (ob *Observer) WatchInbound(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.GetChainParams().InboundTicker) if err != nil { ob.logger.Inbound.Error().Err(err).Msg("error creating ticker") - return + return err } defer ticker.Stop() @@ -42,26 +49,33 @@ func (ob *Observer) WatchInbound() { for { select { case <-ticker.C(): - if !ob.AppContext().IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled(ob.GetChainParams()) { sampledLogger.Info(). Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue } - err := ob.ObserveInbound() + err := ob.ObserveInbound(ctx) if err != nil { ob.logger.Inbound.Error().Err(err).Msg("WatchInbound error observing in tx") } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) case <-ob.StopChannel(): ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) - return + return nil } } } // ObserveInbound observes the Bitcoin chain for inbounds and post votes to zetacore // TODO(revamp): simplify this function into smaller functions -func (ob *Observer) ObserveInbound() error { +func (ob *Observer) ObserveInbound(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + zetaCoreClient := ob.ZetacoreClient() + // get and update latest block height cnt, err := ob.btcClient.GetBlockCount() if err != nil { @@ -108,10 +122,10 @@ func (ob *Observer) ObserveInbound() error { // https://github.com/zeta-chain/node/issues/1847 // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 - blockHeaderVerification, found := ob.AppContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) + blockHeaderVerification, found := app.GetBlockHeaderEnabledChains(ob.Chain().ChainId) if found && blockHeaderVerification.Enabled { // #nosec G701 always in range - err = ob.postBlockHeader(int64(blockNumber)) + err = ob.postBlockHeader(ctx, int64(blockNumber)) if err != nil { ob.logger.Inbound.Warn().Err(err).Msgf("observeInboundBTC: error posting block header %d", blockNumber) } @@ -145,7 +159,8 @@ func (ob *Observer) ObserveInbound() error { for _, inbound := range inbounds { msg := ob.GetInboundVoteMessageFromBtcEvent(inbound) if msg != nil { - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( + zetaHash, ballot, err := zetaCoreClient.PostVoteInbound( + ctx, zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -175,21 +190,26 @@ func (ob *Observer) ObserveInbound() error { // WatchInboundTracker watches zetacore for bitcoin inbound trackers // TODO(revamp): move all ticker related methods in the same file -func (ob *Observer) WatchInboundTracker() { +func (ob *Observer) WatchInboundTracker(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + ticker, err := types.NewDynamicTicker("Bitcoin_WatchInboundTracker", ob.GetChainParams().InboundTicker) if err != nil { ob.logger.Inbound.Err(err).Msg("error creating ticker") - return + return err } defer ticker.Stop() for { select { case <-ticker.C(): - if !ob.AppContext().IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled(ob.GetChainParams()) { continue } - err := ob.ProcessInboundTrackers() + err := ob.ProcessInboundTrackers(ctx) if err != nil { ob.logger.Inbound.Error(). Err(err). @@ -198,15 +218,15 @@ func (ob *Observer) WatchInboundTracker() { ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) case <-ob.StopChannel(): ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) - return + return nil } } } // ProcessInboundTrackers processes inbound trackers // TODO(revamp): move inbound tracker logic in a specific file -func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) +func (ob *Observer) ProcessInboundTrackers(ctx context.Context) error { + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ctx, ob.Chain().ChainId) if err != nil { return err } @@ -214,7 +234,7 @@ func (ob *Observer) ProcessInboundTrackers() error { for _, tracker := range trackers { ob.logger.Inbound.Info(). Msgf("checking tracker with hash :%s and coin-type :%s ", tracker.TxHash, tracker.CoinType) - ballotIdentifier, err := ob.CheckReceiptForBtcTxHash(tracker.TxHash, true) + ballotIdentifier, err := ob.CheckReceiptForBtcTxHash(ctx, tracker.TxHash, true) if err != nil { return err } @@ -226,7 +246,7 @@ func (ob *Observer) ProcessInboundTrackers() error { } // CheckReceiptForBtcTxHash checks the receipt for a btc tx hash -func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, error) { +func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, vote bool) (string, error) { hash, err := chainhash.NewHashFromStr(txHash) if err != nil { return "", err @@ -252,7 +272,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, } depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) - tss, err := ob.ZetacoreClient().GetBtcTssAddress(ob.Chain().ChainId) + tss, err := ob.ZetacoreClient().GetBTCTSSAddress(ctx, ob.Chain().ChainId) if err != nil { return "", err } @@ -285,6 +305,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, } zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( + ctx, zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index b2781380b1..16acf419a4 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -3,6 +3,7 @@ package observer import ( "bytes" + "context" "encoding/hex" "fmt" "math" @@ -18,6 +19,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/pkg/bg" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -25,7 +27,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) @@ -113,7 +114,6 @@ func NewObserver( chain chains.Chain, btcClient interfaces.BTCRPCClient, chainParams observertypes.ChainParams, - appContext *context.AppContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, @@ -124,7 +124,6 @@ func NewObserver( baseObserver, err := base.NewObserver( chain, chainParams, - appContext, zetacoreClient, tss, btcBlocksPerDay, @@ -194,32 +193,32 @@ func (ob *Observer) GetChainParams() observertypes.ChainParams { } // Start starts the Go routine processes to observe the Bitcoin chain -func (ob *Observer) Start() { +func (ob *Observer) Start(ctx context.Context) { ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) // watch bitcoin chain for incoming txs and post votes to zetacore - go ob.WatchInbound() + bg.Work(ctx, ob.WatchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) // watch bitcoin chain for outgoing txs status - go ob.WatchOutbound() + bg.Work(ctx, ob.WatchOutbound, bg.WithName("WatchOutbound"), bg.WithLogger(ob.Logger().Outbound)) // watch bitcoin chain for UTXOs owned by the TSS address - go ob.WatchUTXOs() + bg.Work(ctx, ob.WatchUTXOs, bg.WithName("WatchUTXOs"), bg.WithLogger(ob.Logger().Outbound)) // watch bitcoin chain for gas rate and post to zetacore - go ob.WatchGasPrice() + bg.Work(ctx, ob.WatchGasPrice, bg.WithName("WatchGasPrice"), bg.WithLogger(ob.Logger().GasPrice)) // watch zetacore for bitcoin inbound trackers - go ob.WatchInboundTracker() + bg.Work(ctx, ob.WatchInboundTracker, bg.WithName("WatchInboundTracker"), bg.WithLogger(ob.Logger().Inbound)) // watch the RPC status of the bitcoin chain - go ob.WatchRPCStatus() + bg.Work(ctx, ob.WatchRPCStatus, bg.WithName("WatchRPCStatus"), bg.WithLogger(ob.Logger().Chain)) } // WatchRPCStatus watches the RPC status of the Bitcoin chain // TODO(revamp): move ticker related functions to a specific file // TODO(revamp): move inner logic in a separate function -func (ob *Observer) WatchRPCStatus() { +func (ob *Observer) WatchRPCStatus(_ context.Context) error { ob.logger.Chain.Info().Msgf("RPCStatus is starting") ticker := time.NewTicker(60 * time.Second) @@ -275,7 +274,7 @@ func (ob *Observer) WatchRPCStatus() { Msgf("[OK] RPC status check: latest block number %d, timestamp %s (%.fs ago), tss addr %s, #utxos: %d", bn, blockTime, elapsedSeconds, tssAddr, len(res)) case <-ob.StopChannel(): - return + return nil } } } @@ -304,9 +303,9 @@ func (ob *Observer) ConfirmationsThreshold(amount *big.Int) int64 { // WatchGasPrice watches Bitcoin chain for gas rate and post to zetacore // TODO(revamp): move ticker related functions to a specific file // TODO(revamp): move inner logic in a separate function -func (ob *Observer) WatchGasPrice() { +func (ob *Observer) WatchGasPrice(ctx context.Context) error { // report gas price right away as the ticker takes time to kick in - err := ob.PostGasPrice() + err := ob.PostGasPrice(ctx) if err != nil { ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } @@ -315,7 +314,7 @@ func (ob *Observer) WatchGasPrice() { ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchGasPrice", ob.GetChainParams().GasPriceTicker) if err != nil { ob.logger.GasPrice.Error().Err(err).Msg("error creating ticker") - return + return err } ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) @@ -327,23 +326,25 @@ func (ob *Observer) WatchGasPrice() { if !ob.GetChainParams().IsSupported { continue } - err := ob.PostGasPrice() + err := ob.PostGasPrice(ctx) if err != nil { ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) case <-ob.StopChannel(): ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) - return + return nil } } } // PostGasPrice posts gas price to zetacore // TODO(revamp): move to gas price file -func (ob *Observer) PostGasPrice() error { - var err error - feeRateEstimated := uint64(0) +func (ob *Observer) PostGasPrice(ctx context.Context) error { + var ( + err error + feeRateEstimated uint64 + ) // special handle regnet and testnet gas rate // regnet: RPC 'EstimateSmartFee' is not available @@ -351,15 +352,13 @@ func (ob *Observer) PostGasPrice() error { if ob.Chain().NetworkType != chains.NetworkType_mainnet { feeRateEstimated, err = ob.specialHandleFeeRate() if err != nil { - ob.logger.GasPrice.Err(err).Msg("error specialHandleFeeRate") - return err + return errors.Wrap(err, "unable to execute specialHandleFeeRate") } } else { // EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation feeResult, err := ob.btcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) if err != nil { - ob.logger.GasPrice.Err(err).Msg("error EstimateSmartFee") - return err + return errors.Wrap(err, "unable to estimate smart fee") } if feeResult.Errors != nil || feeResult.FeeRate == nil { return fmt.Errorf("error getting gas price: %s", feeResult.Errors) @@ -377,7 +376,7 @@ func (ob *Observer) PostGasPrice() error { } // #nosec G701 always positive - _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), feeRateEstimated, "100", uint64(blockNumber)) + _, err = ob.ZetacoreClient().PostVoteGasPrice(ctx, ob.Chain(), feeRateEstimated, "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("err PostGasPrice") return err @@ -431,11 +430,11 @@ func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, n // WatchUTXOs watches bitcoin chain for UTXOs owned by the TSS address // TODO(revamp): move ticker related functions to a specific file -func (ob *Observer) WatchUTXOs() { +func (ob *Observer) WatchUTXOs(ctx context.Context) error { ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchUTXOs", ob.GetChainParams().WatchUtxoTicker) if err != nil { ob.logger.UTXOs.Error().Err(err).Msg("error creating ticker") - return + return err } defer ticker.Stop() @@ -445,21 +444,21 @@ func (ob *Observer) WatchUTXOs() { if !ob.GetChainParams().IsSupported { continue } - err := ob.FetchUTXOs() + err := ob.FetchUTXOs(ctx) if err != nil { ob.logger.UTXOs.Error().Err(err).Msg("error fetching btc utxos") } ticker.UpdateInterval(ob.GetChainParams().WatchUtxoTicker, ob.logger.UTXOs) case <-ob.StopChannel(): ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.Chain().ChainId) - return + return nil } } } // FetchUTXOs fetches TSS-owned UTXOs from the Bitcoin node // TODO(revamp): move to UTXO file -func (ob *Observer) FetchUTXOs() error { +func (ob *Observer) FetchUTXOs(ctx context.Context) error { defer func() { if err := recover(); err != nil { ob.logger.UTXOs.Error().Msgf("BTC FetchUTXOs: caught panic error: %v", err) @@ -467,7 +466,7 @@ func (ob *Observer) FetchUTXOs() error { }() // This is useful when a zetaclient's pending nonce lagged behind for whatever reason. - ob.refreshPendingNonce() + ob.refreshPendingNonce(ctx) // get the current block height. bh, err := ob.btcClient.GetBlockCount() @@ -671,12 +670,12 @@ func (ob *Observer) isTssTransaction(txid string) bool { // postBlockHeader posts block header to zetacore // TODO(revamp): move to block header file -func (ob *Observer) postBlockHeader(tip int64) error { +func (ob *Observer) postBlockHeader(ctx context.Context, tip int64) error { ob.logger.Inbound.Info().Msgf("postBlockHeader: tip %d", tip) bn := tip - res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) - if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { - bn = res.ChainState.LatestHeight + 1 + chainState, err := ob.ZetacoreClient().GetBlockHeaderChainState(ctx, ob.Chain().ChainId) + if err == nil && chainState != nil && chainState.EarliestHeight > 0 { + bn = chainState.LatestHeight + 1 } if bn > tip { return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) @@ -694,6 +693,7 @@ func (ob *Observer) postBlockHeader(tip int64) error { } blockHash := res2.Header.BlockHash() _, err = ob.ZetacoreClient().PostVoteBlockHeader( + ctx, ob.Chain().ChainId, blockHash[:], res2.Block.Height, diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index 8fb8838ae3..438324b091 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -21,7 +21,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" @@ -92,7 +91,6 @@ func MockBTCObserver( params, nil, nil, - nil, dbpath, base.Logger{}, nil, @@ -113,7 +111,6 @@ func Test_NewObserver(t *testing.T) { chain chains.Chain btcClient interfaces.BTCRPCClient chainParams observertypes.ChainParams - appContext *context.AppContext coreClient interfaces.ZetacoreClient tss interfaces.TSSSigner dbpath string @@ -127,7 +124,6 @@ func Test_NewObserver(t *testing.T) { chain: chain, btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), chainParams: params, - appContext: nil, coreClient: nil, tss: mocks.NewTSSMainnet(), dbpath: sample.CreateTempDir(t), @@ -140,7 +136,6 @@ func Test_NewObserver(t *testing.T) { chain: chains.Chain{ChainId: 111}, // invalid chain id btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), chainParams: params, - appContext: nil, coreClient: nil, tss: mocks.NewTSSMainnet(), dbpath: sample.CreateTempDir(t), @@ -153,7 +148,6 @@ func Test_NewObserver(t *testing.T) { name: "should fail on invalid dbpath", chain: chain, chainParams: params, - appContext: nil, coreClient: nil, btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), tss: mocks.NewTSSMainnet(), @@ -173,7 +167,6 @@ func Test_NewObserver(t *testing.T) { tt.chain, tt.btcClient, tt.chainParams, - tt.appContext, tt.coreClient, tt.tss, tt.dbpath, @@ -254,7 +247,7 @@ func Test_LoadDB(t *testing.T) { // create observer dbpath := sample.CreateTempDir(t) - ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, dbpath, base.Logger{}, nil) + ob, err := observer.NewObserver(chain, btcClient, params, nil, tss, dbpath, base.Logger{}, nil) require.NoError(t, err) t.Run("should load db successfully", func(t *testing.T) { diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 681604d48b..59dec0fbb8 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -1,9 +1,11 @@ package observer import ( + "context" "encoding/hex" "fmt" + "cosmossdk.io/math" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/pkg/errors" @@ -16,7 +18,9 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/types" + "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) // GetTxID returns a unique id for outbound tx @@ -28,12 +32,17 @@ func (ob *Observer) GetTxID(nonce uint64) string { // WatchOutbound watches Bitcoin chain for outgoing txs status // TODO(revamp): move ticker functions to a specific file // TODO(revamp): move into a separate package -func (ob *Observer) WatchOutbound() { +func (ob *Observer) WatchOutbound(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return errors.Wrap(err, "unable to get app from context") + } + ticker, err := types.NewDynamicTicker("Bitcoin_WatchOutbound", ob.GetChainParams().OutboundTicker) if err != nil { - ob.logger.Outbound.Error().Err(err).Msg("error creating ticker ") - return + return errors.Wrap(err, "unable to create dynamic ticker") } + defer ticker.Stop() chainID := ob.Chain().ChainId @@ -43,12 +52,12 @@ func (ob *Observer) WatchOutbound() { for { select { case <-ticker.C(): - if !ob.AppContext().IsOutboundObservationEnabled(ob.GetChainParams()) { + if !app.IsOutboundObservationEnabled(ob.GetChainParams()) { sampledLogger.Info(). Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) continue } - trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(chainID, interfaces.Ascending) + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ctx, chainID, interfaces.Ascending) if err != nil { ob.logger.Outbound.Error(). Err(err). @@ -58,7 +67,7 @@ func (ob *Observer) WatchOutbound() { for _, tracker := range trackers { // get original cctx parameters outboundID := ob.GetTxID(tracker.Nonce) - cctx, err := ob.ZetacoreClient().GetCctxByNonce(chainID, tracker.Nonce) + cctx, err := ob.ZetacoreClient().GetCctxByNonce(ctx, chainID, tracker.Nonce) if err != nil { ob.logger.Outbound.Info(). Err(err). @@ -83,7 +92,7 @@ func (ob *Observer) WatchOutbound() { txCount := 0 var txResult *btcjson.GetTransactionResult for _, txHash := range tracker.HashList { - result, inMempool := ob.checkIncludedTx(cctx, txHash.TxHash) + result, inMempool := ob.checkIncludedTx(ctx, cctx, txHash.TxHash) if result != nil && !inMempool { // included txCount++ txResult = result @@ -106,16 +115,29 @@ func (ob *Observer) WatchOutbound() { ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) case <-ob.StopChannel(): ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", chainID) - return + return nil } } } // IsOutboundProcessed returns isIncluded(or inMempool), isConfirmed, Error // TODO(revamp): rename as it vote the outbound and doesn't only check if outbound is processed -func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logger zerolog.Logger) (bool, bool, error) { +func (ob *Observer) IsOutboundProcessed( + ctx context.Context, + cctx *crosschaintypes.CrossChainTx, + logger zerolog.Logger, +) (bool, bool, error) { + const ( + // not used with Bitcoin + outboundGasUsed = 0 + outboundGasPrice = 0 + outboundGasLimit = 0 + + gasLimit = zetacore.PostVoteOutboundGasLimit + gasRetryLimit = 0 + ) + params := *cctx.GetCurrentOutboundParam() - sendHash := cctx.Index nonce := cctx.GetCurrentOutboundParam().TssNonce // get broadcasted outbound and tx result @@ -141,7 +163,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } // Try including this outbound broadcasted by myself - txResult, inMempool := ob.checkIncludedTx(cctx, txnHash) + txResult, inMempool := ob.checkIncludedTx(ctx, cctx, txnHash) if txResult == nil { // check failed, try again next time return false, false, nil } else if inMempool { // still in mempool (should avoid unnecessary Tss keysign) @@ -176,26 +198,42 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } logger.Debug().Msgf("Bitcoin outbound confirmed: txid %s, amount %s\n", res.TxID, amountInSat.String()) - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( - sendHash, + + signer := ob.ZetacoreClient().GetKeys().GetOperatorAddress() + + msg := crosschaintypes.NewMsgVoteOutbound( + signer.String(), + cctx.Index, res.TxID, + // #nosec G701 always positive uint64(blockHeight), - 0, // gas used not used with Bitcoin - nil, // gas price not used with Bitcoin - 0, // gas limit not used with Bitcoin - amountInSat, + + // not used with Bitcoin + outboundGasUsed, + math.NewInt(outboundGasPrice), + outboundGasLimit, + + math.NewUintFromBigInt(amountInSat), chains.ReceiveStatus_success, - ob.Chain(), + ob.Chain().ChainId, nonce, coin.CoinType_Gas, ) + + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound(ctx, gasLimit, gasRetryLimit, msg) + + logFields := map[string]any{ + "outbound.external_tx_hash": res.TxID, + "outbound.nonce": nonce, + "outbound.zeta_tx_hash": zetaHash, + "outbound.ballot": ballot, + } + if err != nil { - logger.Error(). - Err(err). - Msgf("IsOutboundProcessed: error confirming bitcoin outbound %s, nonce %d ballot %s", res.TxID, nonce, ballot) + logger.Error().Err(err).Fields(logFields).Msg("IsOutboundProcessed: error confirming bitcoin outbound") } else if zetaHash != "" { - logger.Info().Msgf("IsOutboundProcessed: confirmed Bitcoin outbound %s, zeta tx hash %s nonce %d ballot %s", res.TxID, zetaHash, nonce, ballot) + logger.Info().Fields(logFields).Msgf("IsOutboundProcessed: confirmed Bitcoin outbound") } return true, true, nil @@ -218,6 +256,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg // // TODO(revamp): move to utxo file func (ob *Observer) SelectUTXOs( + ctx context.Context, amount float64, utxosToSpend uint16, nonce uint64, @@ -231,7 +270,7 @@ func (ob *Observer) SelectUTXOs( defer ob.Mu().Unlock() } else { // for nonce > 0; we proceed only when we see the nonce-mark utxo - preTxid, err := ob.getOutboundIDByNonce(nonce-1, test) + preTxid, err := ob.getOutboundIDByNonce(ctx, nonce-1, test) if err != nil { return nil, 0, 0, 0, err } @@ -303,9 +342,9 @@ func (ob *Observer) SelectUTXOs( // There could be many (unpredictable) reasons for a pending nonce lagging behind, for example: // 1. The zetaclient gets restarted. // 2. The tracker is missing in zetacore. -func (ob *Observer) refreshPendingNonce() { +func (ob *Observer) refreshPendingNonce(ctx context.Context) { // get pending nonces from zetacore - p, err := ob.ZetacoreClient().GetPendingNoncesByChain(ob.Chain().ChainId) + p, err := ob.ZetacoreClient().GetPendingNoncesByChain(ctx, ob.Chain().ChainId) if err != nil { ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") } @@ -319,7 +358,7 @@ func (ob *Observer) refreshPendingNonce() { nonceLow := uint64(p.NonceLow) if nonceLow > pendingNonce { // get the last included outbound hash - txid, err := ob.getOutboundIDByNonce(nonceLow-1, false) + txid, err := ob.getOutboundIDByNonce(ctx, nonceLow-1, false) if err != nil { ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting last outbound txid") } @@ -335,7 +374,7 @@ func (ob *Observer) refreshPendingNonce() { // getOutboundIDByNonce gets the outbound ID from the nonce of the outbound transaction // test is true for unit test only -func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error) { +func (ob *Observer) getOutboundIDByNonce(ctx context.Context, nonce uint64, test bool) (string, error) { // There are 2 types of txids an observer can trust // 1. The ones had been verified and saved by observer self. // 2. The ones had been finalized in zetacore based on majority vote. @@ -343,7 +382,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return res.TxID, nil } if !test { // if not unit test, get cctx from zetacore - send, err := ob.ZetacoreClient().GetCctxByNonce(ob.Chain().ChainId, nonce) + send, err := ob.ZetacoreClient().GetCctxByNonce(ctx, ob.Chain().ChainId, nonce) if err != nil { return "", errors.Wrapf(err, "getOutboundIDByNonce: error getting cctx for nonce %d", nonce) } @@ -390,6 +429,7 @@ func (ob *Observer) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { // checkIncludedTx checks if a txHash is included and returns (txResult, inMempool) // Note: if txResult is nil, then inMempool flag should be ignored. func (ob *Observer) checkIncludedTx( + ctx context.Context, cctx *crosschaintypes.CrossChainTx, txHash string, ) (*btcjson.GetTransactionResult, bool) { @@ -407,7 +447,7 @@ func (ob *Observer) checkIncludedTx( } if getTxResult.Confirmations >= 0 { // check included tx only - err = ob.checkTssOutboundResult(cctx, hash, getTxResult) + err = ob.checkTssOutboundResult(ctx, cctx, hash, getTxResult) if err != nil { ob.logger.Outbound.Error(). Err(err). @@ -472,6 +512,7 @@ func (ob *Observer) removeIncludedTx(nonce uint64) { // // Returns: true if outbound passes basic checks. func (ob *Observer) checkTssOutboundResult( + ctx context.Context, cctx *crosschaintypes.CrossChainTx, hash *chainhash.Hash, res *btcjson.GetTransactionResult, @@ -482,7 +523,7 @@ func (ob *Observer) checkTssOutboundResult( if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: error GetRawTxResultByHash %s", hash.String()) } - err = ob.checkTSSVin(rawResult.Vin, nonce) + err = ob.checkTSSVin(ctx, rawResult.Vin, nonce) if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: invalid TSS Vin in outbound %s nonce %d", hash, nonce) } @@ -510,7 +551,7 @@ func (ob *Observer) checkTssOutboundResult( // checkTSSVin checks vin is valid if: // - The first input is the nonce-mark // - All inputs are from TSS address -func (ob *Observer) checkTSSVin(vins []btcjson.Vin, nonce uint64) error { +func (ob *Observer) checkTSSVin(ctx context.Context, vins []btcjson.Vin, nonce uint64) error { // vins: [nonce-mark, UTXO1, UTXO2, ...] if nonce > 0 && len(vins) <= 1 { return fmt.Errorf("checkTSSVin: len(vins) <= 1") @@ -526,7 +567,7 @@ func (ob *Observer) checkTSSVin(vins []btcjson.Vin, nonce uint64) error { } // 1st vin: nonce-mark MUST come from prior TSS outbound if nonce > 0 && i == 0 { - preTxid, err := ob.getOutboundIDByNonce(nonce-1, false) + preTxid, err := ob.getOutboundIDByNonce(ctx, nonce-1, false) if err != nil { return fmt.Errorf("checkTSSVin: error findTxIDByNonce %d", nonce-1) } diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index 0aaeb7b600..cb43590ff5 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -1,6 +1,7 @@ package observer import ( + "context" "math" "sort" "testing" @@ -27,7 +28,7 @@ func MockBTCObserverMainnet(t *testing.T) *Observer { tss := mocks.NewTSSMainnet() // create Bitcoin observer - ob, err := NewObserver(chain, btcClient, params, nil, nil, tss, testutils.SQLiteMemory, base.Logger{}, nil) + ob, err := NewObserver(chain, btcClient, params, nil, tss, testutils.SQLiteMemory, base.Logger{}, nil) require.NoError(t, err) return ob @@ -239,13 +240,15 @@ func TestCheckTSSVoutCancelled(t *testing.T) { } func TestSelectUTXOs(t *testing.T) { + ctx := context.Background() + ob := createObserverWithUTXOs(t) dummyTxID := "6e6f71d281146c1fc5c755b35908ee449f26786c84e2ae18f98b268de40b7ec4" // Case1: nonce = 0, bootstrap // input: utxoCap = 5, amount = 0.01, nonce = 0 // output: [0.01], 0.01 - result, amount, _, _, err := ob.SelectUTXOs(0.01, 5, 0, math.MaxUint16, true) + result, amount, _, _, err := ob.SelectUTXOs(ctx, 0.01, 5, 0, math.MaxUint16, true) require.NoError(t, err) require.Equal(t, 0.01, amount) require.Equal(t, ob.utxos[0:1], result) @@ -253,7 +256,7 @@ func TestSelectUTXOs(t *testing.T) { // Case2: nonce = 1, must FAIL and wait for previous transaction to be mined // input: utxoCap = 5, amount = 0.5, nonce = 1 // output: error - result, amount, _, _, err = ob.SelectUTXOs(0.5, 5, 1, math.MaxUint16, true) + result, amount, _, _, err = ob.SelectUTXOs(ctx, 0.5, 5, 1, math.MaxUint16, true) require.Error(t, err) require.Nil(t, result) require.Zero(t, amount) @@ -263,7 +266,7 @@ func TestSelectUTXOs(t *testing.T) { // Case3: nonce = 1, should pass now // input: utxoCap = 5, amount = 0.5, nonce = 1 // output: [0.00002, 0.01, 0.12, 0.18, 0.24], 0.55002 - result, amount, _, _, err = ob.SelectUTXOs(0.5, 5, 1, math.MaxUint16, true) + result, amount, _, _, err = ob.SelectUTXOs(ctx, 0.5, 5, 1, math.MaxUint16, true) require.NoError(t, err) require.Equal(t, 0.55002, amount) require.Equal(t, ob.utxos[0:5], result) @@ -272,7 +275,7 @@ func TestSelectUTXOs(t *testing.T) { // Case4: // input: utxoCap = 5, amount = 1.0, nonce = 2 // output: [0.00002001, 0.01, 0.12, 0.18, 0.24, 0.5], 1.05002001 - result, amount, _, _, err = ob.SelectUTXOs(1.0, 5, 2, math.MaxUint16, true) + result, amount, _, _, err = ob.SelectUTXOs(ctx, 1.0, 5, 2, math.MaxUint16, true) require.NoError(t, err) require.InEpsilon(t, 1.05002001, amount, 1e-8) require.Equal(t, ob.utxos[0:6], result) @@ -281,7 +284,7 @@ func TestSelectUTXOs(t *testing.T) { // Case5: should include nonce-mark utxo on the LEFT // input: utxoCap = 5, amount = 8.05, nonce = 3 // output: [0.00002002, 0.24, 0.5, 1.26, 2.97, 3.28], 8.25002002 - result, amount, _, _, err = ob.SelectUTXOs(8.05, 5, 3, math.MaxUint16, true) + result, amount, _, _, err = ob.SelectUTXOs(ctx, 8.05, 5, 3, math.MaxUint16, true) require.NoError(t, err) require.InEpsilon(t, 8.25002002, amount, 1e-8) expected := append([]btcjson.ListUnspentResult{ob.utxos[0]}, ob.utxos[4:9]...) @@ -291,7 +294,7 @@ func TestSelectUTXOs(t *testing.T) { // Case6: should include nonce-mark utxo on the RIGHT // input: utxoCap = 5, amount = 0.503, nonce = 24105432 // output: [0.24107432, 0.01, 0.12, 0.18, 0.24], 0.55002002 - result, amount, _, _, err = ob.SelectUTXOs(0.503, 5, 24105432, math.MaxUint16, true) + result, amount, _, _, err = ob.SelectUTXOs(ctx, 0.503, 5, 24105432, math.MaxUint16, true) require.NoError(t, err) require.InEpsilon(t, 0.79107431, amount, 1e-8) expected = append([]btcjson.ListUnspentResult{ob.utxos[4]}, ob.utxos[0:4]...) @@ -301,7 +304,7 @@ func TestSelectUTXOs(t *testing.T) { // Case7: should include nonce-mark utxo in the MIDDLE // input: utxoCap = 5, amount = 1.0, nonce = 24105433 // output: [0.24107432, 0.12, 0.18, 0.24, 0.5], 1.28107432 - result, amount, _, _, err = ob.SelectUTXOs(1.0, 5, 24105433, math.MaxUint16, true) + result, amount, _, _, err = ob.SelectUTXOs(ctx, 1.0, 5, 24105433, math.MaxUint16, true) require.NoError(t, err) require.InEpsilon(t, 1.28107432, amount, 1e-8) expected = append([]btcjson.ListUnspentResult{ob.utxos[4]}, ob.utxos[1:4]...) @@ -311,7 +314,7 @@ func TestSelectUTXOs(t *testing.T) { // Case8: should work with maximum amount // input: utxoCap = 5, amount = 16.03 // output: [0.24107432, 1.26, 2.97, 3.28, 5.16, 8.72], 21.63107432 - result, amount, _, _, err = ob.SelectUTXOs(16.03, 5, 24105433, math.MaxUint16, true) + result, amount, _, _, err = ob.SelectUTXOs(ctx, 16.03, 5, 24105433, math.MaxUint16, true) require.NoError(t, err) require.InEpsilon(t, 21.63107432, amount, 1e-8) expected = append([]btcjson.ListUnspentResult{ob.utxos[4]}, ob.utxos[6:11]...) @@ -320,7 +323,7 @@ func TestSelectUTXOs(t *testing.T) { // Case9: must FAIL due to insufficient funds // input: utxoCap = 5, amount = 21.64 // output: error - result, amount, _, _, err = ob.SelectUTXOs(21.64, 5, 24105433, math.MaxUint16, true) + result, amount, _, _, err = ob.SelectUTXOs(ctx, 21.64, 5, 24105433, math.MaxUint16, true) require.Error(t, err) require.Nil(t, result) require.Zero(t, amount) @@ -332,6 +335,8 @@ func TestSelectUTXOs(t *testing.T) { } func TestUTXOConsolidation(t *testing.T) { + ctx := context.Background() + dummyTxID := "6e6f71d281146c1fc5c755b35908ee449f26786c84e2ae18f98b268de40b7ec4" t.Run("should not consolidate", func(t *testing.T) { @@ -340,7 +345,7 @@ func TestUTXOConsolidation(t *testing.T) { // input: utxoCap = 10, amount = 0.01, nonce = 1, rank = 10 // output: [0.00002, 0.01], 0.01002 - result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(0.01, 10, 1, 10, true) + result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(ctx, 0.01, 10, 1, 10, true) require.NoError(t, err) require.Equal(t, 0.01002, amount) require.Equal(t, ob.utxos[0:2], result) @@ -354,7 +359,7 @@ func TestUTXOConsolidation(t *testing.T) { // input: utxoCap = 9, amount = 0.01, nonce = 1, rank = 9 // output: [0.00002, 0.01, 0.12], 0.13002 - result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(0.01, 9, 1, 9, true) + result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(ctx, 0.01, 9, 1, 9, true) require.NoError(t, err) require.Equal(t, 0.13002, amount) require.Equal(t, ob.utxos[0:3], result) @@ -368,7 +373,7 @@ func TestUTXOConsolidation(t *testing.T) { // input: utxoCap = 5, amount = 0.01, nonce = 0, rank = 5 // output: [0.00002, 0.014, 1.26, 0.5, 0.2], 2.01002 - result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(0.01, 5, 1, 5, true) + result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(ctx, 0.01, 5, 1, 5, true) require.NoError(t, err) require.Equal(t, 2.01002, amount) expected := make([]btcjson.ListUnspentResult, 2) @@ -387,7 +392,7 @@ func TestUTXOConsolidation(t *testing.T) { // input: utxoCap = 12, amount = 0.01, nonce = 0, rank = 1 // output: [0.00002, 0.01, 8.72, 5.16, 3.28, 2.97, 1.26, 0.5, 0.24, 0.18, 0.12], 22.44002 - result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(0.01, 12, 1, 1, true) + result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(ctx, 0.01, 12, 1, 1, true) require.NoError(t, err) require.Equal(t, 22.44002, amount) expected := make([]btcjson.ListUnspentResult, 2) @@ -411,7 +416,7 @@ func TestUTXOConsolidation(t *testing.T) { // input: utxoCap = 5, amount = 0.13, nonce = 24105432, rank = 5 // output: [0.24107431, 0.01, 0.12, 1.26, 0.5, 0.24], 2.37107431 - result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(0.13, 5, 24105432, 5, true) + result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(ctx, 0.13, 5, 24105432, 5, true) require.NoError(t, err) require.InEpsilon(t, 2.37107431, amount, 1e-8) expected := append([]btcjson.ListUnspentResult{ob.utxos[4]}, ob.utxos[0:2]...) @@ -434,7 +439,7 @@ func TestUTXOConsolidation(t *testing.T) { // input: utxoCap = 12, amount = 0.13, nonce = 24105432, rank = 1 // output: [0.24107431, 0.01, 0.12, 8.72, 5.16, 3.28, 2.97, 1.26, 0.5, 0.24, 0.18], 22.68107431 - result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(0.13, 12, 24105432, 1, true) + result, amount, clsdtUtxo, clsdtValue, err := ob.SelectUTXOs(ctx, 0.13, 12, 24105432, 1, true) require.NoError(t, err) require.InEpsilon(t, 22.68107431, amount, 1e-8) expected := append([]btcjson.ListUnspentResult{ob.utxos[4]}, ob.utxos[0:2]...) diff --git a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go index 7f319871a6..7cc0abc11d 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go +++ b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go @@ -55,7 +55,7 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { btcClient := mocks.NewMockBTCRPCClient() // create observer - ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, testutils.SQLiteMemory, + ob, err := observer.NewObserver(chain, btcClient, params, nil, tss, testutils.SQLiteMemory, base.DefaultLogger(), nil) suite.Require().NoError(err) suite.Require().NotNil(ob) diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 259f55bc53..1db8b3033b 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -3,6 +3,7 @@ package signer import ( "bytes" + "context" "encoding/hex" "fmt" "math/big" @@ -26,7 +27,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" ) @@ -58,13 +59,12 @@ type Signer struct { // NewSigner creates a new Bitcoin signer func NewSigner( chain chains.Chain, - appContext *context.AppContext, tss interfaces.TSSSigner, ts *metrics.TelemetryServer, logger base.Logger, cfg config.BTCConfig) (*Signer, error) { // create base signer - baseSigner := base.NewSigner(chain, appContext, tss, ts, logger) + baseSigner := base.NewSigner(chain, tss, ts, logger) // create the bitcoin rpc client using the provided config connCfg := &rpcclient.ConnConfig{ @@ -171,6 +171,7 @@ func (signer *Signer) AddWithdrawTxOutputs( // SignWithdrawTx receives utxos sorted by value, amount in BTC, feeRate in BTC per Kb // TODO(revamp): simplify the function func (signer *Signer) SignWithdrawTx( + ctx context.Context, to btcutil.Address, amount float64, gasPrice *big.Int, @@ -185,7 +186,7 @@ func (signer *Signer) SignWithdrawTx( nonceMark := chains.NonceMarkAmount(nonce) // refresh unspent UTXOs and continue with keysign regardless of error - err := observer.FetchUTXOs() + err := observer.FetchUTXOs(ctx) if err != nil { signer.Logger(). Std.Error(). @@ -195,6 +196,7 @@ func (signer *Signer) SignWithdrawTx( // select N UTXOs to cover the total expense prevOuts, total, consolidatedUtxo, consolidatedValue, err := observer.SelectUTXOs( + ctx, amount+estimateFee+float64(nonceMark)*1e-8, maxNoOfInputsPerTx, nonce, @@ -270,7 +272,7 @@ func (signer *Signer) SignWithdrawTx( } } - sig65Bs, err := signer.TSS().SignBatch(witnessHashes, height, nonce, chain.ChainId) + sig65Bs, err := signer.TSS().SignBatch(ctx, witnessHashes, height, nonce, chain.ChainId) if err != nil { return nil, fmt.Errorf("SignBatch error: %v", err) } @@ -317,6 +319,7 @@ func (signer *Signer) Broadcast(signedTx *wire.MsgTx) error { // TryProcessOutbound signs and broadcasts a BTC transaction from a new outbound // TODO(revamp): simplify the function func (signer *Signer) TryProcessOutbound( + ctx context.Context, cctx *types.CrossChainTx, outboundProcessor *outboundprocessor.Processor, outboundID string, @@ -324,6 +327,12 @@ func (signer *Signer) TryProcessOutbound( zetacoreClient interfaces.ZetacoreClient, height uint64, ) { + app, err := zctx.FromContext(ctx) + if err != nil { + signer.Logger().Std.Error().Msgf("BTC TryProcessOutbound: %s, cannot get app context", cctx.Index) + return + } + defer func() { outboundProcessor.EndTryProcess(outboundID) if err := recover(); err != nil { @@ -350,7 +359,7 @@ func (signer *Signer) TryProcessOutbound( logger.Error().Msgf("chain observer is not a bitcoin observer") return } - flags := signer.AppContext().GetCrossChainFlags() + flags := app.GetCrossChainFlags() if !flags.IsOutboundEnabled { logger.Info().Msgf("outbound is disabled") return @@ -403,6 +412,7 @@ func (signer *Signer) TryProcessOutbound( // sign withdraw tx tx, err := signer.SignWithdrawTx( + ctx, to, amount, gasprice, @@ -421,7 +431,7 @@ func (signer *Signer) TryProcessOutbound( Msgf("Key-sign success: %d => %s, nonce %d", cctx.InboundParams.SenderChainId, chain.ChainName, outboundTssNonce) // FIXME: add prometheus metrics - _, err = zetacoreClient.GetObserverList() + _, err = zetacoreClient.GetObserverList(ctx) if err != nil { logger.Warn(). Err(err). @@ -447,6 +457,7 @@ func (signer *Signer) TryProcessOutbound( logger.Info(). Msgf("Broadcast success: nonce %d to chain %s outboundHash %s", outboundTssNonce, chain.String(), outboundHash) zetaHash, err := zetacoreClient.AddOutboundTracker( + ctx, chain.ChainId, outboundTssNonce, outboundHash, diff --git a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go index 2506f57059..0d1ec42431 100644 --- a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go @@ -2,6 +2,7 @@ package signer import ( "bytes" + "context" "encoding/hex" "fmt" "math/big" @@ -141,12 +142,14 @@ func getTSSTX( subscript []byte, hashType txscript.SigHashType, ) (string, error) { + ctx := context.Background() + witnessHash, err := txscript.CalcWitnessSigHash(subscript, sigHashes, txscript.SigHashAll, tx, idx, amt) if err != nil { return "", err } - sig65B, err := tss.Sign(witnessHash, 10, 10, 0, "") + sig65B, err := tss.Sign(ctx, witnessHash, 10, 10, 0, "") R := big.NewInt(0).SetBytes(sig65B[:32]) S := big.NewInt(0).SetBytes(sig65B[32:64]) sig := btcec.Signature{ diff --git a/zetaclient/chains/bitcoin/signer/signer_test.go b/zetaclient/chains/bitcoin/signer/signer_test.go index e351d4a65c..39728899be 100644 --- a/zetaclient/chains/bitcoin/signer/signer_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_test.go @@ -48,7 +48,6 @@ func (s *BTCSignerSuite) SetUpTest(c *C) { } s.btcSigner, err = NewSigner( chains.Chain{}, - nil, tss, nil, base.DefaultLogger(), @@ -231,7 +230,6 @@ func TestAddWithdrawTxOutputs(t *testing.T) { // Create test signer and receiver address signer, err := NewSigner( chains.Chain{}, - nil, mocks.NewTSSMainnet(), nil, base.DefaultLogger(), @@ -394,7 +392,6 @@ func TestNewBTCSigner(t *testing.T) { } btcSigner, err := NewSigner( chains.Chain{}, - nil, tss, nil, base.DefaultLogger(), diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 889d2215ac..4f51e3354c 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -27,6 +27,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" "github.com/zeta-chain/zetacore/zetaclient/zetacore" @@ -34,14 +35,19 @@ import ( // WatchInbound watches evm chain for incoming txs and post votes to zetacore // TODO(revamp): move ticker function to a separate file -func (ob *Observer) WatchInbound() { +func (ob *Observer) WatchInbound(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("EVM_WatchInbound_%d", ob.Chain().ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { ob.Logger().Inbound.Error().Err(err).Msg("error creating ticker") - return + return err } defer ticker.Stop() @@ -51,19 +57,19 @@ func (ob *Observer) WatchInbound() { for { select { case <-ticker.C(): - if !ob.AppContext().IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled(ob.GetChainParams()) { sampledLogger.Info(). Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue } - err := ob.ObserveInbound(sampledLogger) + err := ob.ObserveInbound(ctx, sampledLogger) if err != nil { ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) case <-ob.StopChannel(): ob.Logger().Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) - return + return nil } } } @@ -71,14 +77,19 @@ func (ob *Observer) WatchInbound() { // WatchInboundTracker gets a list of Inbound tracker suggestions from zeta-core at each tick and tries to check if the in-tx was confirmed. // If it was, it tries to broadcast the confirmation vote. If this zeta client has previously broadcast the vote, the tx would be rejected // TODO(revamp): move inbound tracker function to a separate file -func (ob *Observer) WatchInboundTracker() { +func (ob *Observer) WatchInboundTracker(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.Chain().ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { ob.Logger().Inbound.Err(err).Msg("error creating ticker") - return + return err } defer ticker.Stop() @@ -86,25 +97,25 @@ func (ob *Observer) WatchInboundTracker() { for { select { case <-ticker.C(): - if !ob.AppContext().IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled(ob.GetChainParams()) { continue } - err := ob.ProcessInboundTrackers() + err := ob.ProcessInboundTrackers(ctx) if err != nil { ob.Logger().Inbound.Err(err).Msg("ProcessInboundTrackers error") } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) case <-ob.StopChannel(): ob.Logger().Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) - return + return nil } } } // ProcessInboundTrackers processes inbound trackers from zetacore // TODO(revamp): move inbound tracker function to a separate file -func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) +func (ob *Observer) ProcessInboundTrackers(ctx context.Context) error { + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ctx, ob.Chain().ChainId) if err != nil { return err } @@ -120,7 +131,7 @@ func (ob *Observer) ProcessInboundTrackers() error { ) } - receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tracker.TxHash)) + receipt, err := ob.evmClient.TransactionReceipt(ctx, ethcommon.HexToHash(tracker.TxHash)) if err != nil { return errors.Wrapf( err, @@ -134,11 +145,11 @@ func (ob *Observer) ProcessInboundTrackers() error { // check and vote on inbound tx switch tracker.CoinType { case coin.CoinType_Zeta: - _, err = ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) + _, err = ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, true) case coin.CoinType_ERC20: - _, err = ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) + _, err = ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, true) case coin.CoinType_Gas: - _, err = ob.CheckAndVoteInboundTokenGas(tx, receipt, true) + _, err = ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, true) default: return fmt.Errorf( "unknown coin type %s for inbound %s chain %d", @@ -155,9 +166,9 @@ func (ob *Observer) ProcessInboundTrackers() error { } // ObserveInbound observes the evm chain for inbounds and posts votes to zetacore -func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { +func (ob *Observer) ObserveInbound(ctx context.Context, sampledLogger zerolog.Logger) error { // get and update latest block height - blockNumber, err := ob.evmClient.BlockNumber(context.Background()) + blockNumber, err := ob.evmClient.BlockNumber(ctx) if err != nil { return err } @@ -192,13 +203,19 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { startBlock, toBlock := ob.calcBlockRangeToScan(confirmedBlockNum, lastScanned, config.MaxBlocksPerPeriod) // task 1: query evm chain for zeta sent logs (read at most 100 blocks in one go) - lastScannedZetaSent := ob.ObserveZetaSent(startBlock, toBlock) + lastScannedZetaSent, err := ob.ObserveZetaSent(ctx, startBlock, toBlock) + if err != nil { + return errors.Wrap(err, "unable to observe ZetaSent") + } // task 2: query evm chain for deposited logs (read at most 100 blocks in one go) - lastScannedDeposited := ob.ObserveERC20Deposited(startBlock, toBlock) + lastScannedDeposited := ob.ObserveERC20Deposited(ctx, startBlock, toBlock) // task 3: query the incoming tx to TSS address (read at most 100 blocks in one go) - lastScannedTssRecvd := ob.ObserverTSSReceive(startBlock, toBlock) + lastScannedTssRecvd, err := ob.ObserverTSSReceive(ctx, startBlock, toBlock) + if err != nil { + return errors.Wrap(err, "unable to observe TSSReceive") + } // note: using lowest height for all 3 events is not perfect, but it's simple and good enough lastScannedLowest := lastScannedZetaSent @@ -225,22 +242,29 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { // ObserveZetaSent queries the ZetaSent event from the connector contract and posts to zetacore // returns the last block successfully scanned -func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { +func (ob *Observer) ObserveZetaSent(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { + app, err := zctx.FromContext(ctx) + if err != nil { + return 0, err + } + // filter ZetaSent logs addrConnector, connector, err := ob.GetConnectorContract() if err != nil { ob.Logger().Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") - return startBlock - 1 // lastScanned + // lastScanned + return startBlock - 1, err } iter, err := connector.FilterZetaSent(&bind.FilterOpts{ Start: startBlock, End: &toBlock, - Context: context.TODO(), + Context: ctx, }, []ethcommon.Address{}, []*big.Int{}) if err != nil { ob.Logger().Chain.Warn().Err(err).Msgf( "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) - return startBlock - 1 // lastScanned + // lastScanned + return startBlock - 1, err } // collect and sort events by block number, then tx index, then log index (ascending) @@ -286,25 +310,27 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { } guard[event.Raw.TxHash.Hex()] = true - msg := ob.BuildInboundVoteMsgForZetaSentEvent(event) + msg := ob.BuildInboundVoteMsgForZetaSentEvent(app, event) if msg != nil { _, err = ob.PostVoteInbound( + ctx, msg, coin.CoinType_Zeta, zetacore.PostVoteInboundMessagePassingExecutionGasLimit, ) if err != nil { - return beingScanned - 1 // we have to re-scan from this block next time + // we have to re-scan from this block next time + return beingScanned - 1, err } } } // successful processed all events in [startBlock, toBlock] - return toBlock + return toBlock, nil } // ObserveERC20Deposited queries the ERC20CustodyDeposited event from the ERC20Custody contract and posts to zetacore // returns the last block successfully scanned -func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { +func (ob *Observer) ObserveERC20Deposited(ctx context.Context, startBlock, toBlock uint64) uint64 { // filter ERC20CustodyDeposited logs addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract() if err != nil { @@ -315,7 +341,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { iter, err := erc20custodyContract.FilterDeposited(&bind.FilterOpts{ Start: startBlock, End: &toBlock, - Context: context.TODO(), + Context: ctx, }, []ethcommon.Address{}) if err != nil { ob.Logger().Inbound.Warn().Err(err).Msgf( @@ -376,7 +402,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) if msg != nil { - _, err = ob.PostVoteInbound(msg, coin.CoinType_ERC20, zetacore.PostVoteInboundExecutionGasLimit) + _, err = ob.PostVoteInbound(ctx, msg, coin.CoinType_ERC20, zetacore.PostVoteInboundExecutionGasLimit) if err != nil { return beingScanned - 1 // we have to re-scan from this block next time } @@ -388,42 +414,65 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { // ObserverTSSReceive queries the incoming gas asset to TSS address and posts to zetacore // returns the last block successfully scanned -func (ob *Observer) ObserverTSSReceive(startBlock, toBlock uint64) uint64 { - // query incoming gas asset - for bn := startBlock; bn <= toBlock; bn++ { +func (ob *Observer) ObserverTSSReceive(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { + app, err := zctx.FromContext(ctx) + if err != nil { + return 0, err + } + + var ( // post new block header (if any) to zetacore and ignore error // TODO: consider having a independent ticker(from TSS scaning) for posting block headers // https://github.com/zeta-chain/node/issues/1847 - blockHeaderVerification, found := ob.AppContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) - if found && blockHeaderVerification.Enabled { + chainID = ob.Chain().ChainId + blockHeaderVerification, found = app.GetBlockHeaderEnabledChains(chainID) + shouldPostBlockHeader = found && blockHeaderVerification.Enabled + ) + + // query incoming gas asset + for bn := startBlock; bn <= toBlock; bn++ { + if shouldPostBlockHeader { // post block header for supported chains // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 - err := ob.postBlockHeader(toBlock) - if err != nil { - ob.Logger().Inbound.Error().Err(err).Msg("error posting block header") + if err := ob.postBlockHeader(ctx, toBlock); err != nil { + ob.Logger().Inbound. + Error().Err(err). + Uint64("tss.to_block", toBlock). + Msg("error posting block header") } } // observe TSS received gas token in block 'bn' - err := ob.ObserveTSSReceiveInBlock(bn) + err := ob.ObserveTSSReceiveInBlock(ctx, bn) if err != nil { ob.Logger().Inbound.Error(). Err(err). - Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.Chain().ChainId) - return bn - 1 // we have to re-scan from this block next time + Int64("tss.chain_id", chainID). + Uint64("tss.block_number", bn). + Msg("ObserverTSSReceive: unable to ObserveTSSReceiveInBlock") + + // we have to re-scan from this block next time + return bn - 1, nil } } + // successful processed all gas asset deposits in [startBlock, toBlock] - return toBlock + return toBlock, nil } // CheckAndVoteInboundTokenZeta checks and votes on the given inbound Zeta token func (ob *Observer) CheckAndVoteInboundTokenZeta( + ctx context.Context, tx *ethrpc.Transaction, receipt *ethtypes.Receipt, vote bool, ) (string, error) { + app, err := zctx.FromContext(ctx) + if err != nil { + return "", err + } + // check confirmations if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( @@ -447,7 +496,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( // sanity check tx event err = evm.ValidateEvmTxLog(&event.Raw, addrConnector, tx.Hash, evm.TopicsZetaSent) if err == nil { - msg = ob.BuildInboundVoteMsgForZetaSentEvent(event) + msg = ob.BuildInboundVoteMsgForZetaSentEvent(app, event) } else { ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err @@ -461,7 +510,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( return "", nil } if vote { - return ob.PostVoteInbound(msg, coin.CoinType_Zeta, zetacore.PostVoteInboundMessagePassingExecutionGasLimit) + return ob.PostVoteInbound(ctx, msg, coin.CoinType_Zeta, zetacore.PostVoteInboundMessagePassingExecutionGasLimit) } return msg.Digest(), nil @@ -469,6 +518,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( // CheckAndVoteInboundTokenERC20 checks and votes on the given inbound ERC20 token func (ob *Observer) CheckAndVoteInboundTokenERC20( + ctx context.Context, tx *ethrpc.Transaction, receipt *ethtypes.Receipt, vote bool, @@ -511,7 +561,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( return "", nil } if vote { - return ob.PostVoteInbound(msg, coin.CoinType_ERC20, zetacore.PostVoteInboundExecutionGasLimit) + return ob.PostVoteInbound(ctx, msg, coin.CoinType_ERC20, zetacore.PostVoteInboundExecutionGasLimit) } return msg.Digest(), nil @@ -519,6 +569,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( // CheckAndVoteInboundTokenGas checks and votes on the given inbound gas token func (ob *Observer) CheckAndVoteInboundTokenGas( + ctx context.Context, tx *ethrpc.Transaction, receipt *ethtypes.Receipt, vote bool, @@ -549,7 +600,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( return "", nil } if vote { - return ob.PostVoteInbound(msg, coin.CoinType_Gas, zetacore.PostVoteInboundExecutionGasLimit) + return ob.PostVoteInbound(ctx, msg, coin.CoinType_Gas, zetacore.PostVoteInboundExecutionGasLimit) } return msg.Digest(), nil @@ -557,13 +608,15 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( // PostVoteInbound posts a vote for the given vote message func (ob *Observer) PostVoteInbound( + ctx context.Context, msg *types.MsgVoteInbound, coinType coin.CoinType, retryGasLimit uint64, ) (string, error) { txHash := msg.InboundHash chainID := ob.Chain().ChainId - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) + zetaHash, ballot, err := ob.ZetacoreClient(). + PostVoteInbound(ctx, zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) if err != nil { ob.Logger().Inbound.Err(err). Msgf("inbound detected: error posting vote for chain %d token %s inbound %s", chainID, coinType, txHash) @@ -640,11 +693,12 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( // BuildInboundVoteMsgForZetaSentEvent builds a inbound vote message for a ZetaSent event func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( + appContext *zctx.AppContext, event *zetaconnector.ZetaConnectorNonEthZetaSent, ) *types.MsgVoteInbound { destChain, found := chains.GetChainFromChainID( event.DestinationChainId.Int64(), - ob.AppContext().GetAdditionalChains(), + appContext.GetAdditionalChains(), ) if !found { ob.Logger().Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) @@ -661,7 +715,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( } if !destChain.IsZetaChain() { - paramsDest, found := ob.AppContext().GetEVMChainParams(destChain.ChainId) + paramsDest, found := appContext.GetEVMChainParams(destChain.ChainId) if !found { ob.Logger().Inbound.Warn(). Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) @@ -747,7 +801,7 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( } // ObserveTSSReceiveInBlock queries the incoming gas asset to TSS address in a single block and posts votes -func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { +func (ob *Observer) ObserveTSSReceiveInBlock(ctx context.Context, blockNumber uint64) error { block, err := ob.GetBlockByNumberCached(blockNumber) if err != nil { return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.Chain().ChainId) @@ -756,12 +810,12 @@ func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { for i := range block.Transactions { tx := block.Transactions[i] if ethcommon.HexToAddress(tx.To) == ob.TSS().EVMAddress() { - receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tx.Hash)) + receipt, err := ob.evmClient.TransactionReceipt(ctx, ethcommon.HexToHash(tx.Hash)) if err != nil { return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) } - _, err = ob.CheckAndVoteInboundTokenGas(&tx, receipt, true) + _, err = ob.CheckAndVoteInboundTokenGas(ctx, &tx, receipt, true) if err != nil { return errors.Wrapf( err, diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index bb8930f4ca..de1e003ab8 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -1,20 +1,23 @@ package observer_test import ( + "context" "encoding/hex" "testing" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/onrik/ethrpc" + "github.com/rs/zerolog" "github.com/stretchr/testify/require" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/pkg/constant" "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" @@ -29,6 +32,8 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { chainParam := mocks.MockChainParams(chain.ChainId, confirmation) inboundHash := "0xf3935200c80f98502d5edc7e871ffc40ca898e134525c42c2ae3cbc5725f9d76" + ctx, _ := makeAppContext(t) + t.Run("should pass for archived inbound, receipt and cctx", func(t *testing.T) { tx, receipt, cctx := testutils.LoadEVMInboundNReceiptNCctx( t, @@ -41,7 +46,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) + ballot, err := ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) }) @@ -57,7 +62,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) + _, err := ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) t.Run("should not act if no ZetaSent event", func(t *testing.T) { @@ -73,7 +78,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) + ballot, err := ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) }) @@ -100,7 +105,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { lastBlock, mocks.MockChainParams(chainID, confirmation), ) - _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) + _, err := ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) } @@ -114,6 +119,8 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { chainParam := mocks.MockChainParams(chain.ChainId, confirmation) inboundHash := "0x4ea69a0e2ff36f7548ab75791c3b990e076e2a4bffeb616035b239b7d33843da" + ctx := context.Background() + t.Run("should pass for archived inbound, receipt and cctx", func(t *testing.T) { tx, receipt, cctx := testutils.LoadEVMInboundNReceiptNCctx( t, @@ -126,7 +133,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) + ballot, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) }) @@ -142,7 +149,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) + _, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) t.Run("should not act if no Deposit event", func(t *testing.T) { @@ -158,7 +165,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) + ballot, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) }) @@ -185,7 +192,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock, mocks.MockChainParams(chainID, confirmation), ) - _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) + _, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) } @@ -199,6 +206,8 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { chainParam := mocks.MockChainParams(chain.ChainId, confirmation) inboundHash := "0xeaec67d5dd5d85f27b21bef83e01cbdf59154fd793ea7a22c297f7c3a722c532" + ctx := context.Background() + t.Run("should pass for archived inbound, receipt and cctx", func(t *testing.T) { tx, receipt, cctx := testutils.LoadEVMInboundNReceiptNCctx( t, @@ -211,7 +220,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) + ballot, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) }) @@ -221,7 +230,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - _, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) + _, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) t.Run("should not act if receiver is not TSS", func(t *testing.T) { @@ -231,7 +240,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) + ballot, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.ErrorContains(t, err, "not TSS address") require.Equal(t, "", ballot) }) @@ -242,7 +251,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) + ballot, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.ErrorContains(t, err, "not a successful tx") require.Equal(t, "", ballot) }) @@ -253,7 +262,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) + ballot, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, "", ballot) }) @@ -278,8 +287,10 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { ComplianceConfig: config.ComplianceConfig{}, } + _, app := makeAppContext(t) + t.Run("should return vote msg for archived ZetaSent event", func(t *testing.T) { - msg := ob.BuildInboundVoteMsgForZetaSentEvent(event) + msg := ob.BuildInboundVoteMsgForZetaSentEvent(app, event) require.NotNil(t, msg) require.Equal(t, cctx.InboundParams.BallotIndex, msg.Digest()) }) @@ -287,21 +298,21 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { sender := event.ZetaTxSenderAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{sender} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForZetaSentEvent(event) + msg := ob.BuildInboundVoteMsgForZetaSentEvent(app, event) require.Nil(t, msg) }) t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { receiver := clienttypes.BytesToEthHex(event.DestinationAddress) cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForZetaSentEvent(event) + msg := ob.BuildInboundVoteMsgForZetaSentEvent(app, event) require.Nil(t, msg) }) t.Run("should return nil msg if txOrigin is restricted", func(t *testing.T) { txOrigin := event.SourceTxOriginAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{txOrigin} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForZetaSentEvent(event) + msg := ob.BuildInboundVoteMsgForZetaSentEvent(app, event) require.Nil(t, msg) }) } @@ -439,41 +450,46 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { // create mock client evmClient := mocks.NewMockEvmClient() evmJSONRPC := mocks.NewMockJSONRPCClient() - zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) tss := mocks.NewTSSMainnet() lastBlock := receipt.BlockNumber.Uint64() + confirmation + zetacoreClient := mocks.NewZetacoreClient(t). + WithKeys(&keys.Keys{}). + WithZetaChain(). + WithPostVoteInbound("", ""). + WithPostVoteInbound("", "") + + ctx := context.Background() + t.Run("should observe TSS receive in block", func(t *testing.T) { ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) // feed archived block and receipt evmJSONRPC.WithBlock(block) evmClient.WithReceipt(receipt) - err := ob.ObserveTSSReceiveInBlock(blockNumber) + err := ob.ObserveTSSReceiveInBlock(ctx, blockNumber) require.NoError(t, err) }) t.Run("should not observe on error getting block", func(t *testing.T) { ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) - err := ob.ObserveTSSReceiveInBlock(blockNumber) + err := ob.ObserveTSSReceiveInBlock(ctx, blockNumber) // error getting block is expected because the mock JSONRPC contains no block require.ErrorContains(t, err, "error getting block") }) t.Run("should not observe on error getting receipt", func(t *testing.T) { ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) evmJSONRPC.WithBlock(block) - err := ob.ObserveTSSReceiveInBlock(blockNumber) + err := ob.ObserveTSSReceiveInBlock(ctx, blockNumber) // error getting block is expected because the mock evmClient contains no receipt require.ErrorContains(t, err, "error getting receipt") }) - t.Run("should not observe on error posting vote", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) +} - // feed archived block and pause zetacore client - evmJSONRPC.WithBlock(block) - evmClient.WithReceipt(receipt) - zetacoreClient.Pause() - err := ob.ObserveTSSReceiveInBlock(blockNumber) - // error posting vote is expected because the mock zetacoreClient is paused - require.ErrorContains(t, err, "error checking and voting") - }) +func makeAppContext(_ *testing.T) (context.Context, *zctx.AppContext) { + var ( + app = zctx.New(config.New(false), zerolog.Nop()) + ctx = context.Background() + ) + + return zctx.WithAppContext(ctx, app), app } diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 3a60b16e40..c5a70e2cfc 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -19,13 +19,13 @@ import ( zetaconnectoreth "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.eth.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" + "github.com/zeta-chain/zetacore/pkg/bg" "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" - clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) @@ -55,10 +55,10 @@ type Observer struct { // NewObserver returns a new EVM chain observer func NewObserver( + ctx context.Context, evmCfg config.EVMConfig, evmClient interfaces.EVMRPCClient, chainParams observertypes.ChainParams, - appClient *clientcontext.AppContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, @@ -69,7 +69,6 @@ func NewObserver( baseObserver, err := base.NewObserver( evmCfg.Chain, chainParams, - appClient, zetacoreClient, tss, base.DefaultBlockCacheSize, @@ -92,7 +91,7 @@ func NewObserver( } // open database and load data - err = ob.LoadDB(dbpath) + err = ob.LoadDB(ctx, dbpath) if err != nil { return nil, err } @@ -166,29 +165,20 @@ func FetchZetaTokenContract( } // Start all observation routines for the evm chain -func (ob *Observer) Start() { +func (ob *Observer) Start(ctx context.Context) { ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) - // watch evm chain for incoming txs and post votes to zetacore - go ob.WatchInbound() - - // watch evm chain for outgoing txs status - go ob.WatchOutbound() - - // watch evm chain for gas prices and post to zetacore - go ob.WatchGasPrice() - - // watch zetacore for inbound trackers - go ob.WatchInboundTracker() - - // watch the RPC status of the evm chain - go ob.WatchRPCStatus() + bg.Work(ctx, ob.WatchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) + bg.Work(ctx, ob.WatchOutbound, bg.WithName("WatchOutbound"), bg.WithLogger(ob.Logger().Outbound)) + bg.Work(ctx, ob.WatchGasPrice, bg.WithName("WatchGasPrice"), bg.WithLogger(ob.Logger().GasPrice)) + bg.Work(ctx, ob.WatchInboundTracker, bg.WithName("WatchInboundTracker"), bg.WithLogger(ob.Logger().Inbound)) + bg.Work(ctx, ob.WatchRPCStatus, bg.WithName("WatchRPCStatus"), bg.WithLogger(ob.Logger().Chain)) } // WatchRPCStatus watches the RPC status of the evm chain // TODO(revamp): move ticker to ticker file // TODO(revamp): move inner logic to a separate function -func (ob *Observer) WatchRPCStatus() { +func (ob *Observer) WatchRPCStatus(ctx context.Context) error { ob.Logger().Chain.Info().Msgf("Starting RPC status check for chain %d", ob.Chain().ChainId) ticker := time.NewTicker(60 * time.Second) for { @@ -197,17 +187,17 @@ func (ob *Observer) WatchRPCStatus() { if !ob.GetChainParams().IsSupported { continue } - bn, err := ob.evmClient.BlockNumber(context.Background()) + bn, err := ob.evmClient.BlockNumber(ctx) if err != nil { ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } - gasPrice, err := ob.evmClient.SuggestGasPrice(context.Background()) + gasPrice, err := ob.evmClient.SuggestGasPrice(ctx) if err != nil { ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } - header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(bn)) + header, err := ob.evmClient.HeaderByNumber(ctx, new(big.Int).SetUint64(bn)) if err != nil { ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue @@ -223,7 +213,7 @@ func (ob *Observer) WatchRPCStatus() { ob.Logger().Chain.Info(). Msgf("[OK] RPC status: latest block num %d, timestamp %s ( %.0fs ago), suggested gas price %d", header.Number, blockTime.String(), elapsedSeconds, gasPrice.Uint64()) case <-ob.StopChannel(): - return + return nil } } } @@ -295,9 +285,9 @@ func (ob *Observer) CheckTxInclusion(tx *ethtypes.Transaction, receipt *ethtypes // WatchGasPrice watches evm chain for gas prices and post to zetacore // TODO(revamp): move ticker to ticker file // TODO(revamp): move inner logic to a separate function -func (ob *Observer) WatchGasPrice() { +func (ob *Observer) WatchGasPrice(ctx context.Context) error { // report gas price right away as the ticker takes time to kick in - err := ob.PostGasPrice() + err := ob.PostGasPrice(ctx) if err != nil { ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } @@ -309,7 +299,7 @@ func (ob *Observer) WatchGasPrice() { ) if err != nil { ob.Logger().GasPrice.Error().Err(err).Msg("NewDynamicTicker error") - return + return err } ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) @@ -321,28 +311,28 @@ func (ob *Observer) WatchGasPrice() { if !ob.GetChainParams().IsSupported { continue } - err = ob.PostGasPrice() + err = ob.PostGasPrice(ctx) if err != nil { ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) case <-ob.StopChannel(): ob.Logger().GasPrice.Info().Msg("WatchGasPrice stopped") - return + return nil } } } // PostGasPrice posts gas price to zetacore // TODO(revamp): move to gas price file -func (ob *Observer) PostGasPrice() error { +func (ob *Observer) PostGasPrice(ctx context.Context) error { // GAS PRICE - gasPrice, err := ob.evmClient.SuggestGasPrice(context.TODO()) + gasPrice, err := ob.evmClient.SuggestGasPrice(ctx) if err != nil { ob.Logger().GasPrice.Err(err).Msg("Err SuggestGasPrice:") return err } - blockNum, err := ob.evmClient.BlockNumber(context.TODO()) + blockNum, err := ob.evmClient.BlockNumber(ctx) if err != nil { ob.Logger().GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") return err @@ -351,7 +341,7 @@ func (ob *Observer) PostGasPrice() error { // SUPPLY supply := "100" // lockedAmount on ETH, totalSupply on other chains - zetaHash, err := ob.ZetacoreClient().PostGasPrice(ob.Chain(), gasPrice.Uint64(), supply, blockNum) + zetaHash, err := ob.ZetacoreClient().PostVoteGasPrice(ctx, ob.Chain(), gasPrice.Uint64(), supply, blockNum) if err != nil { ob.Logger().GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") return err @@ -376,14 +366,14 @@ func (ob *Observer) TransactionByHash(txHash string) (*ethrpc.Transaction, bool, } // GetBlockHeaderCached get block header by number from cache -func (ob *Observer) GetBlockHeaderCached(blockNumber uint64) (*ethtypes.Header, error) { +func (ob *Observer) GetBlockHeaderCached(ctx context.Context, blockNumber uint64) (*ethtypes.Header, error) { if result, ok := ob.HeaderCache().Get(blockNumber); ok { if header, ok := result.(*ethtypes.Header); ok { return header, nil } return nil, errors.New("cached value is not of type *ethtypes.Header") } - header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) + header, err := ob.evmClient.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber)) if err != nil { return nil, err } @@ -434,7 +424,7 @@ func (ob *Observer) BlockByNumber(blockNumber int) (*ethrpc.Block, error) { // LoadDB open sql database and load data into EVM observer // TODO(revamp): move to a db file -func (ob *Observer) LoadDB(dbPath string) error { +func (ob *Observer) LoadDB(ctx context.Context, dbPath string) error { if dbPath == "" { return errors.New("empty db path") } @@ -456,14 +446,14 @@ func (ob *Observer) LoadDB(dbPath string) error { } // load last block scanned - err = ob.LoadLastBlockScanned() + err = ob.LoadLastBlockScanned(ctx) return err } // LoadLastBlockScanned loads the last scanned block from the database // TODO(revamp): move to a db file -func (ob *Observer) LoadLastBlockScanned() error { +func (ob *Observer) LoadLastBlockScanned(ctx context.Context) error { err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) if err != nil { return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) @@ -473,7 +463,7 @@ func (ob *Observer) LoadLastBlockScanned() error { // 1. environment variable is set explicitly to "latest" // 2. environment variable is empty and last scanned block is not found in DB if ob.LastBlockScanned() == 0 { - blockNumber, err := ob.evmClient.BlockNumber(context.Background()) + blockNumber, err := ob.evmClient.BlockNumber(ctx) if err != nil { return errors.Wrapf(err, "error BlockNumber for chain %d", ob.Chain().ChainId) } @@ -486,20 +476,20 @@ func (ob *Observer) LoadLastBlockScanned() error { // postBlockHeader posts the block header to zetacore // TODO(revamp): move to a block header file -func (ob *Observer) postBlockHeader(tip uint64) error { +func (ob *Observer) postBlockHeader(ctx context.Context, tip uint64) error { bn := tip - res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) - if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { + chainState, err := ob.ZetacoreClient().GetBlockHeaderChainState(ctx, ob.Chain().ChainId) + if err == nil && chainState != nil && chainState.EarliestHeight > 0 { // #nosec G701 always positive - bn = uint64(res.ChainState.LatestHeight) + 1 // the next header to post + bn = uint64(chainState.LatestHeight) + 1 // the next header to post } if bn > tip { return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) } - header, err := ob.GetBlockHeaderCached(bn) + header, err := ob.GetBlockHeaderCached(ctx, bn) if err != nil { ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) return err @@ -511,6 +501,7 @@ func (ob *Observer) postBlockHeader(tip uint64) error { } _, err = ob.ZetacoreClient().PostVoteBlockHeader( + ctx, ob.Chain().ChainId, header.Hash().Bytes(), header.Number.Int64(), diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index e38ff877a1..3655d045ad 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -1,6 +1,7 @@ package observer_test import ( + "context" "fmt" "math/big" "os" @@ -12,6 +13,8 @@ import ( "github.com/onrik/ethrpc" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" @@ -22,8 +25,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" - "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" @@ -37,21 +38,21 @@ func getZetacoreContext( evmChain chains.Chain, endpoint string, evmChainParams *observertypes.ChainParams, -) (*context.AppContext, config.EVMConfig) { +) (*zctx.AppContext, config.EVMConfig) { // use default endpoint if not provided if endpoint == "" { endpoint = "http://localhost:8545" } // create config - cfg := config.NewConfig() + cfg := config.New(false) cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ Chain: evmChain, Endpoint: endpoint, } // create zetacore context - appContext := context.New(cfg, zerolog.Nop()) + appContext := zctx.New(cfg, zerolog.Nop()) evmChainParamsMap := make(map[int64]*observertypes.ChainParams) evmChainParamsMap[evmChain.ChainId] = evmChainParams @@ -83,6 +84,8 @@ func MockEVMObserver( lastBlock uint64, params observertypes.ChainParams, ) *observer.Observer { + ctx := context.Background() + // use default mock evm client if not provided if evmClient == nil { evmClient = mocks.NewMockEvmClient().WithBlockNumber(1000) @@ -90,17 +93,21 @@ func MockEVMObserver( // use default mock zetacore client if not provided if zetacoreClient == nil { - zetacoreClient = mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) + zetacoreClient = mocks.NewZetacoreClient(t). + WithKeys(&keys.Keys{}). + WithZetaChain(). + WithPostVoteInbound("", ""). + WithPostVoteOutbound("", "") } // use default mock tss if not provided if tss == nil { tss = mocks.NewTSSMainnet() } // create zetacore context - coreCtx, evmCfg := getZetacoreContext(chain, "", ¶ms) + _, evmCfg := getZetacoreContext(chain, "", ¶ms) // create observer - ob, err := observer.NewObserver(evmCfg, evmClient, params, coreCtx, zetacoreClient, tss, dbpath, base.Logger{}, nil) + ob, err := observer.NewObserver(ctx, evmCfg, evmClient, params, zetacoreClient, tss, dbpath, base.Logger{}, nil) require.NoError(t, err) ob.WithEvmJSONRPC(evmJSONRPC) ob.WithLastBlock(lastBlock) @@ -109,6 +116,8 @@ func MockEVMObserver( } func Test_NewObserver(t *testing.T) { + ctx := context.Background() + // use Ethereum chain for testing chain := chains.Ethereum params := mocks.MockChainParams(chain.ChainId, 10) @@ -176,15 +185,15 @@ func Test_NewObserver(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // create zetacore context, client and tss - zetacoreCtx, _ := getZetacoreContext(tt.evmCfg.Chain, tt.evmCfg.Endpoint, ¶ms) - zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) + //zetacoreCtx, _ := getZetacoreContext(tt.evmCfg.Chain, tt.evmCfg.Endpoint, ¶ms) + zetacoreClient := mocks.NewZetacoreClient(t) // create observer ob, err := observer.NewObserver( + ctx, tt.evmCfg, tt.evmClient, tt.chainParams, - zetacoreCtx, zetacoreClient, tt.tss, tt.dbpath, @@ -205,6 +214,8 @@ func Test_NewObserver(t *testing.T) { } func Test_LoadDB(t *testing.T) { + ctx := context.Background() + // use Ethereum chain for testing chain := chains.Ethereum params := mocks.MockChainParams(chain.ChainId, 10) @@ -212,17 +223,17 @@ func Test_LoadDB(t *testing.T) { ob := MockEVMObserver(t, chain, nil, nil, nil, nil, dbpath, 1, params) t.Run("should load db successfully", func(t *testing.T) { - err := ob.LoadDB(dbpath) + err := ob.LoadDB(ctx, dbpath) require.NoError(t, err) require.EqualValues(t, 1000, ob.LastBlockScanned()) }) t.Run("should fail on invalid dbpath", func(t *testing.T) { // load db with empty dbpath - err := ob.LoadDB("") + err := ob.LoadDB(ctx, "") require.ErrorContains(t, err, "empty db path") // load db with invalid dbpath - err = ob.LoadDB("/invalid/dbpath") + err = ob.LoadDB(ctx, "/invalid/dbpath") require.ErrorContains(t, err, "error OpenDB") }) t.Run("should fail on invalid env var", func(t *testing.T) { @@ -232,7 +243,7 @@ func Test_LoadDB(t *testing.T) { defer os.Unsetenv(envvar) // load db - err := ob.LoadDB(dbpath) + err := ob.LoadDB(ctx, dbpath) require.ErrorContains(t, err, "error LoadLastBlockScanned") }) t.Run("should fail on RPC error", func(t *testing.T) { @@ -244,12 +255,14 @@ func Test_LoadDB(t *testing.T) { tempClient.WithError(fmt.Errorf("error RPC")) // load db - err := ob.LoadDB(dbpath) + err := ob.LoadDB(ctx, dbpath) require.ErrorContains(t, err, "error RPC") }) } func Test_LoadLastBlockScanned(t *testing.T) { + ctx := context.Background() + // use Ethereum chain for testing chain := chains.Ethereum params := mocks.MockChainParams(chain.ChainId, 10) @@ -264,7 +277,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { ob.WriteLastBlockScannedToDB(123) // load last block scanned - err := ob.LoadLastBlockScanned() + err := ob.LoadLastBlockScanned(ctx) require.NoError(t, err) require.EqualValues(t, 123, ob.LastBlockScanned()) }) @@ -275,7 +288,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { defer os.Unsetenv(envvar) // load last block scanned - err := ob.LoadLastBlockScanned() + err := ob.LoadLastBlockScanned(ctx) require.ErrorContains(t, err, "error LoadLastBlockScanned") }) t.Run("should fail on RPC error", func(t *testing.T) { @@ -290,7 +303,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { evmClient.WithError(fmt.Errorf("error RPC")) // load last block scanned - err := obOther.LoadLastBlockScanned() + err := obOther.LoadLastBlockScanned(ctx) require.ErrorContains(t, err, "error RPC") }) } @@ -364,6 +377,8 @@ func Test_BlockCache(t *testing.T) { } func Test_HeaderCache(t *testing.T) { + ctx := context.Background() + t.Run("should get block header from cache", func(t *testing.T) { // create observer ob := &observer.Observer{} @@ -380,7 +395,7 @@ func Test_HeaderCache(t *testing.T) { evmClient.WithHeader(header) // get block header from observer - resHeader, err := ob.GetBlockHeaderCached(uint64(100)) + resHeader, err := ob.GetBlockHeaderCached(ctx, uint64(100)) require.NoError(t, err) require.EqualValues(t, header, resHeader) }) @@ -396,7 +411,7 @@ func Test_HeaderCache(t *testing.T) { headerCache.Add(blockNumber, "a string value") // get block header from cache - header, err := ob.GetBlockHeaderCached(blockNumber) + header, err := ob.GetBlockHeaderCached(ctx, blockNumber) require.ErrorContains(t, err, "cached value is not of type *ethtypes.Header") require.Nil(t, header) }) diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 84103620dd..54ac2aab1d 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "cosmossdk.io/math" "github.com/ethereum/go-ethereum" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -23,7 +24,9 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" + "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) // GetTxID returns a unique id for outbound tx @@ -35,14 +38,19 @@ func (ob *Observer) GetTxID(nonce uint64) string { // WatchOutbound watches evm chain for outgoing txs status // TODO(revamp): move ticker function to ticker file // TODO(revamp): move inner logic to a separate function -func (ob *Observer) WatchOutbound() { +func (ob *Observer) WatchOutbound(ctx context.Context) error { ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("EVM_WatchOutbound_%d", ob.Chain().ChainId), ob.GetChainParams().OutboundTicker, ) if err != nil { ob.Logger().Outbound.Error().Err(err).Msg("error creating ticker") - return + return err + } + + app, err := zctx.FromContext(ctx) + if err != nil { + return err } ob.Logger().Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.Chain().ChainId) @@ -51,12 +59,13 @@ func (ob *Observer) WatchOutbound() { for { select { case <-ticker.C(): - if !ob.AppContext().IsOutboundObservationEnabled(ob.GetChainParams()) { + if !app.IsOutboundObservationEnabled(ob.GetChainParams()) { sampledLogger.Info(). Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.Chain().ChainId) continue } - trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ob.Chain().ChainId, interfaces.Ascending) + trackers, err := ob.ZetacoreClient(). + GetAllOutboundTrackerByChain(ctx, ob.Chain().ChainId, interfaces.Ascending) if err != nil { continue } @@ -69,7 +78,7 @@ func (ob *Observer) WatchOutbound() { var outboundReceipt *ethtypes.Receipt var outbound *ethtypes.Transaction for _, txHash := range tracker.HashList { - if receipt, tx, ok := ob.checkConfirmedTx(txHash.TxHash, nonceInt); ok { + if receipt, tx, ok := ob.checkConfirmedTx(ctx, txHash.TxHash, nonceInt); ok { txCount++ outboundReceipt = receipt outbound = tx @@ -90,49 +99,78 @@ func (ob *Observer) WatchOutbound() { ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) case <-ob.StopChannel(): ob.Logger().Outbound.Info().Msg("WatchOutbound: stopped") - return + return nil } } } // PostVoteOutbound posts vote to zetacore for the confirmed outbound func (ob *Observer) PostVoteOutbound( + ctx context.Context, cctxIndex string, receipt *ethtypes.Receipt, transaction *ethtypes.Transaction, receiveValue *big.Int, receiveStatus chains.ReceiveStatus, nonce uint64, - cointype coin.CoinType, + coinType coin.CoinType, logger zerolog.Logger, ) { chainID := ob.Chain().ChainId - zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( + logFields := map[string]any{ + "outbound.chain_id": chainID, + "outbound.external_tx_hash": receipt.TxHash.String(), + "outbound.nonce": nonce, + } + + signerAddress := ob.ZetacoreClient().GetKeys().GetOperatorAddress() + + msg := crosschaintypes.NewMsgVoteOutbound( + signerAddress.String(), cctxIndex, receipt.TxHash.Hex(), receipt.BlockNumber.Uint64(), receipt.GasUsed, - transaction.GasPrice(), + math.NewIntFromBigInt(transaction.GasPrice()), transaction.Gas(), - receiveValue, + math.NewUintFromBigInt(receiveValue), receiveStatus, - ob.Chain(), + chainID, nonce, - cointype, + coinType, ) + + const gasLimit = zetacore.PostVoteOutboundGasLimit + + var retryGasLimit uint64 + if msg.Status == chains.ReceiveStatus_failed { + retryGasLimit = zetacore.PostVoteOutboundRevertGasLimit + } + + zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound(ctx, gasLimit, retryGasLimit, msg) if err != nil { - logger.Error(). - Err(err). - Msgf("PostVoteOutbound: error posting vote for chain %d nonce %d outbound %s ", chainID, nonce, receipt.TxHash) - } else if zetaTxHash != "" { - logger.Info().Msgf("PostVoteOutbound: posted vote for chain %d nonce %d outbound %s vote %s ballot %s", chainID, nonce, receipt.TxHash, zetaTxHash, ballot) + logger.Error().Err(err).Fields(logFields).Msgf("PostVoteOutbound: error posting vote for chain %d", chainID) + return + } + + if zetaTxHash == "" { + return } + + logFields["outbound.zeta_tx_hash"] = zetaTxHash + logFields["outbound.ballot"] = ballot + + logger.Info().Fields(logFields).Msgf("PostVoteOutbound: posted vote for chain %d", chainID) } // IsOutboundProcessed checks outbound status and returns (isIncluded, isConfirmed, error) // It also posts vote to zetacore if the tx is confirmed // TODO(revamp): rename as it also vote the outbound -func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logger zerolog.Logger) (bool, bool, error) { +func (ob *Observer) IsOutboundProcessed( + ctx context.Context, + cctx *crosschaintypes.CrossChainTx, + logger zerolog.Logger, +) (bool, bool, error) { // skip if outbound is not confirmed nonce := cctx.GetCurrentOutboundParam().TssNonce if !ob.IsTxConfirmed(nonce) { @@ -165,7 +203,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg if receipt.Status == ethtypes.ReceiptStatusSuccessful { receiveStatus = chains.ReceiveStatus_success } - ob.PostVoteOutbound(cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) + ob.PostVoteOutbound(ctx, cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) return true, true, nil } @@ -188,7 +226,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } // post vote to zetacore - ob.PostVoteOutbound(cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) + ob.PostVoteOutbound(ctx, cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) return true, true, nil } @@ -339,12 +377,16 @@ func ParseOutboundReceivedValue( // checkConfirmedTx checks if a txHash is confirmed // returns (receipt, transaction, true) if confirmed or (nil, nil, false) otherwise -func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Receipt, *ethtypes.Transaction, bool) { - ctxt, cancel := context.WithTimeout(context.Background(), 3*time.Second) +func (ob *Observer) checkConfirmedTx( + ctx context.Context, + txHash string, + nonce uint64, +) (*ethtypes.Receipt, *ethtypes.Transaction, bool) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() // query transaction - transaction, isPending, err := ob.evmClient.TransactionByHash(ctxt, ethcommon.HexToHash(txHash)) + transaction, isPending, err := ob.evmClient.TransactionByHash(ctx, ethcommon.HexToHash(txHash)) if err != nil { log.Error(). Err(err). @@ -383,7 +425,7 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // query receipt - receipt, err := ob.evmClient.TransactionReceipt(ctxt, ethcommon.HexToHash(txHash)) + receipt, err := ob.evmClient.TransactionReceipt(ctx, ethcommon.HexToHash(txHash)) if err != nil { if err != ethereum.NotFound { log.Warn().Err(err).Msgf("confirmTxByHash: TransactionReceipt error, txHash %s nonce %d", txHash, nonce) diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 72023d8f57..7342139343 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -1,6 +1,7 @@ package observer_test import ( + "context" "testing" ethcommon "github.com/ethereum/go-ethereum/common" @@ -8,7 +9,6 @@ import ( "github.com/stretchr/testify/require" "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/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" @@ -58,13 +58,15 @@ func Test_IsOutboundProcessed(t *testing.T) { testutils.EventZetaReceived, ) + ctx := context.Background() + t.Run("should post vote and return true if outbound is processed", func(t *testing.T) { // create evm observer and set outbound and receipt ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) // post outbound vote - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) @@ -88,7 +90,7 @@ func Test_IsOutboundProcessed(t *testing.T) { config.LoadComplianceConfig(cfg) // post outbound vote - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) @@ -96,7 +98,7 @@ func Test_IsOutboundProcessed(t *testing.T) { t.Run("should return false if outbound is not confirmed", func(t *testing.T) { // create evm observer and DO NOT set outbound as confirmed ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) require.NoError(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) @@ -110,7 +112,7 @@ func Test_IsOutboundProcessed(t *testing.T) { chainParamsNew := ob.GetChainParams() chainParamsNew.ConnectorContractAddress = sample.EthAddress().Hex() ob.SetChainParams(chainParamsNew) - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) require.Error(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) @@ -149,6 +151,8 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { testutils.EventZetaReceived, ) + ctx := context.Background() + t.Run("should fail if unable to get connector/custody contract", func(t *testing.T) { // create evm observer and set outbound and receipt ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) @@ -158,7 +162,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { // set invalid connector ABI zetaconnector.ZetaConnectorNonEthMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) zetaconnector.ZetaConnectorNonEthMetaData.ABI = abiConnector // reset connector ABI require.ErrorContains(t, err, "error getting zeta connector") require.False(t, isIncluded) @@ -166,7 +170,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { // set invalid custody ABI erc20custody.ERC20CustodyMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err = ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err = ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) require.ErrorContains(t, err, "error getting erc20 custody") require.False(t, isIncluded) require.False(t, isConfirmed) @@ -189,28 +193,17 @@ func Test_PostVoteOutbound(t *testing.T) { testutils.EventZetaReceived, ) + ctx := context.Background() + t.Run("post vote outbound successfully", func(t *testing.T) { // the amount and status to be used for vote receiveValue := cctx.GetCurrentOutboundParam().Amount.BigInt() receiveStatus := chains.ReceiveStatus_success // create evm client using mock zetacore client and post outbound vote - zetacoreClient := mocks.NewMockZetacoreClient() - ob := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, memDBPath, 1, observertypes.ChainParams{}) - ob.PostVoteOutbound( - cctx.Index, - receipt, - outbound, - receiveValue, - receiveStatus, - nonce, - coinType, - zerolog.Logger{}, - ) - - // pause the mock zetacore client to simulate error posting vote - zetacoreClient.Pause() + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, observertypes.ChainParams{}) ob.PostVoteOutbound( + ctx, cctx.Index, receipt, outbound, @@ -218,7 +211,7 @@ func Test_PostVoteOutbound(t *testing.T) { receiveStatus, nonce, coinType, - zerolog.Logger{}, + zerolog.Nop(), ) }) } diff --git a/zetaclient/chains/evm/signer/outbound_data.go b/zetaclient/chains/evm/signer/outbound_data.go index 3e84fa83db..2509b79198 100644 --- a/zetaclient/chains/evm/signer/outbound_data.go +++ b/zetaclient/chains/evm/signer/outbound_data.go @@ -16,7 +16,7 @@ import ( "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" ) const ( @@ -112,7 +112,7 @@ func (txData *OutboundData) SetupGas( // cctx will be skipped and false otherwise. // 3. error func NewOutboundData( - appontext *clientcontext.AppContext, + ctx context.Context, cctx *types.CrossChainTx, evmObserver *observer.Observer, evmRPC interfaces.EVMRPCClient, @@ -134,14 +134,19 @@ func NewOutboundData( return nil, true, nil } - toChain, found := chains.GetChainFromChainID(txData.toChainID.Int64(), appontext.GetAdditionalChains()) + app, err := zctx.FromContext(ctx) + if err != nil { + return nil, false, err + } + + toChain, found := chains.GetChainFromChainID(txData.toChainID.Int64(), app.GetAdditionalChains()) if !found { return nil, true, fmt.Errorf("unknown chain: %d", txData.toChainID.Int64()) } // Get nonce, Early return if the cctx is already processed nonce := cctx.GetCurrentOutboundParam().TssNonce - included, confirmed, err := evmObserver.IsOutboundProcessed(cctx, logger) + included, confirmed, err := evmObserver.IsOutboundProcessed(ctx, cctx, logger) if err != nil { return nil, true, errors.New("IsOutboundProcessed failed") } diff --git a/zetaclient/chains/evm/signer/outbound_data_test.go b/zetaclient/chains/evm/signer/outbound_data_test.go index f5e3d39d3b..ac2b7061b5 100644 --- a/zetaclient/chains/evm/signer/outbound_data_test.go +++ b/zetaclient/chains/evm/signer/outbound_data_test.go @@ -1,12 +1,15 @@ package signer import ( + "context" "math/big" "testing" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/zetaclient/config" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/x/crosschain/types" @@ -66,6 +69,9 @@ func TestSigner_SetupGas(t *testing.T) { } func TestSigner_NewOutboundData(t *testing.T) { + app := zctx.New(config.New(false), zerolog.Nop()) + ctx := zctx.WithAppContext(context.Background(), app) + // Setup evm signer evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) @@ -75,14 +81,7 @@ func TestSigner_NewOutboundData(t *testing.T) { t.Run("NewOutboundData success", func(t *testing.T) { cctx := getCCTX(t) - _, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) }) @@ -90,14 +89,7 @@ func TestSigner_NewOutboundData(t *testing.T) { t.Run("NewOutboundData skip", func(t *testing.T) { cctx := getCCTX(t) cctx.CctxStatus.Status = types.CctxStatus_Aborted - _, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.NoError(t, err) require.True(t, skip) }) @@ -105,14 +97,7 @@ func TestSigner_NewOutboundData(t *testing.T) { t.Run("NewOutboundData unknown chain", func(t *testing.T) { cctx := getInvalidCCTX(t) require.NoError(t, err) - _, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.ErrorContains(t, err, "unknown chain") require.True(t, skip) }) @@ -121,14 +106,7 @@ func TestSigner_NewOutboundData(t *testing.T) { cctx := getCCTX(t) require.NoError(t, err) cctx.GetCurrentOutboundParam().GasPrice = "invalidGasPrice" - _, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.True(t, skip) require.ErrorContains(t, err, "cannot convert gas price") }) diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index db4a15c856..7235aa4456 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" ethcommon "github.com/ethereum/go-ethereum/common" @@ -31,7 +32,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" - clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" @@ -81,8 +82,8 @@ type Signer struct { // NewSigner creates a new EVM signer func NewSigner( + ctx context.Context, chain chains.Chain, - appContext *clientcontext.AppContext, tss interfaces.TSSSigner, ts *metrics.TelemetryServer, logger base.Logger, @@ -93,22 +94,23 @@ func NewSigner( erc20CustodyAddress ethcommon.Address, ) (*Signer, error) { // create base signer - baseSigner := base.NewSigner(chain, appContext, tss, ts, logger) + baseSigner := base.NewSigner(chain, tss, ts, logger) // create EVM client - client, ethSigner, err := getEVMRPC(endpoint) + client, ethSigner, err := getEVMRPC(ctx, endpoint) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to create EVM client") } // prepare ABIs connectorABI, err := abi.JSON(strings.NewReader(zetaConnectorABI)) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to build ZetaConnector ABI") } + custodyABI, err := abi.JSON(strings.NewReader(erc20CustodyABI)) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to build ERC20Custody ABI") } return &Signer{ @@ -154,6 +156,7 @@ func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address { // Sign given data, and metadata (gas, nonce, etc) // returns a signed transaction, sig bytes, hash bytes, and error func (signer *Signer) Sign( + ctx context.Context, data []byte, to ethcommon.Address, amount *big.Int, @@ -162,14 +165,14 @@ func (signer *Signer) Sign( nonce uint64, height uint64, ) (*ethtypes.Transaction, []byte, []byte, error) { - log.Debug().Msgf("Sign: TSS signer: %s", signer.TSS().Pubkey()) + log.Debug().Str("tss.pub_key", signer.TSS().EVMAddress().String()).Msg("Sign: TSS signer") // TODO: use EIP-1559 transaction type // https://github.com/zeta-chain/node/issues/1952 tx := ethtypes.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data) hashBytes := signer.ethSigner.Hash(tx).Bytes() - sig, err := signer.TSS().Sign(hashBytes, height, nonce, signer.Chain().ChainId, "") + sig, err := signer.TSS().Sign(ctx, hashBytes, height, nonce, signer.Chain().ChainId, "") if err != nil { return nil, nil, nil, err } @@ -208,7 +211,7 @@ func (signer *Signer) Broadcast(tx *ethtypes.Transaction) error { // bytes32 internalSendHash // // ) external virtual {} -func (signer *Signer) SignOutbound(txData *OutboundData) (*ethtypes.Transaction, error) { +func (signer *Signer) SignOutbound(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { var data []byte var err error @@ -223,7 +226,9 @@ func (signer *Signer) SignOutbound(txData *OutboundData) (*ethtypes.Transaction, return nil, fmt.Errorf("onReceive pack error: %w", err) } - tx, _, _, err := signer.Sign(data, + tx, _, _, err := signer.Sign( + ctx, + data, signer.zetaConnectorAddress, zeroValue, txData.gasLimit, @@ -247,7 +252,7 @@ func (signer *Signer) SignOutbound(txData *OutboundData) (*ethtypes.Transaction, // bytes calldata message, // bytes32 internalSendHash // ) external override whenNotPaused onlyTssAddress -func (signer *Signer) SignRevertTx(txData *OutboundData) (*ethtypes.Transaction, error) { +func (signer *Signer) SignRevertTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { var data []byte var err error @@ -263,7 +268,9 @@ func (signer *Signer) SignRevertTx(txData *OutboundData) (*ethtypes.Transaction, return nil, fmt.Errorf("onRevert pack error: %w", err) } - tx, _, _, err := signer.Sign(data, + tx, _, _, err := signer.Sign( + ctx, + data, signer.zetaConnectorAddress, zeroValue, txData.gasLimit, @@ -278,8 +285,9 @@ func (signer *Signer) SignRevertTx(txData *OutboundData) (*ethtypes.Transaction, } // SignCancelTx signs a transaction from TSS address to itself with a zero amount in order to increment the nonce -func (signer *Signer) SignCancelTx(txData *OutboundData) (*ethtypes.Transaction, error) { +func (signer *Signer) SignCancelTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { tx, _, _, err := signer.Sign( + ctx, nil, signer.TSS().EVMAddress(), zeroValue, // zero out the amount to cancel the tx @@ -296,8 +304,9 @@ func (signer *Signer) SignCancelTx(txData *OutboundData) (*ethtypes.Transaction, } // SignWithdrawTx signs a withdrawal transaction sent from the TSS address to the destination -func (signer *Signer) SignWithdrawTx(txData *OutboundData) (*ethtypes.Transaction, error) { +func (signer *Signer) SignWithdrawTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { tx, _, _, err := signer.Sign( + ctx, nil, txData.to, txData.amount, @@ -317,12 +326,17 @@ func (signer *Signer) SignWithdrawTx(txData *OutboundData) (*ethtypes.Transactio // // cmd_whitelist_erc20 // cmd_migrate_tss_funds -func (signer *Signer) SignCommandTx(txData *OutboundData, cmd string, params string) (*ethtypes.Transaction, error) { +func (signer *Signer) SignCommandTx( + ctx context.Context, + txData *OutboundData, + cmd string, + params string, +) (*ethtypes.Transaction, error) { switch cmd { case constant.CmdWhitelistERC20: - return signer.SignWhitelistERC20Cmd(txData, params) + return signer.SignWhitelistERC20Cmd(ctx, txData, params) case constant.CmdMigrateTssFunds: - return signer.SignMigrateTssFundsCmd(txData) + return signer.SignMigrateTssFundsCmd(ctx, txData) } return nil, fmt.Errorf("SignCommandTx: unknown command %s", cmd) } @@ -332,6 +346,7 @@ func (signer *Signer) SignCommandTx(txData *OutboundData, cmd string, params str // It will then broadcast the signed transaction to the outbound chain. // TODO(revamp): simplify function func (signer *Signer) TryProcessOutbound( + ctx context.Context, cctx *types.CrossChainTx, outboundProc *outboundprocessor.Processor, outboundID string, @@ -339,6 +354,12 @@ func (signer *Signer) TryProcessOutbound( zetacoreClient interfaces.ZetacoreClient, height uint64, ) { + app, err := zctx.FromContext(ctx) + if err != nil { + signer.Logger().Std.Error().Err(err).Msg("error getting app context") + return + } + logger := signer.Logger().Std.With(). Str("outboundID", outboundID). Str("SendHash", cctx.Index). @@ -363,7 +384,7 @@ func (signer *Signer) TryProcessOutbound( } // Setup Transaction input - txData, skipTx, err := NewOutboundData(signer.AppContext(), cctx, evmObserver, signer.client, logger, height) + txData, skipTx, err := NewOutboundData(ctx, cctx, evmObserver, signer.client, logger, height) if err != nil { logger.Err(err).Msg("error setting up transaction input fields") return @@ -372,17 +393,14 @@ func (signer *Signer) TryProcessOutbound( return } - toChain, found := chains.GetChainFromChainID( - txData.toChainID.Int64(), - signer.AppContext().GetAdditionalChains(), - ) + toChain, found := chains.GetChainFromChainID(txData.toChainID.Int64(), app.GetAdditionalChains()) if !found { logger.Warn().Msgf("unknown chain: %d", txData.toChainID.Int64()) return } // Get cross-chain flags - crossChainflags := signer.AppContext().GetCrossChainFlags() + crossChainflags := app.GetCrossChainFlags() // https://github.com/zeta-chain/node/issues/2050 var tx *ethtypes.Transaction // compliance check goes first @@ -398,7 +416,7 @@ func (signer *Signer) TryProcessOutbound( cctx.GetCurrentOutboundParam().CoinType.String(), ) - tx, err = signer.SignCancelTx(txData) // cancel the tx + tx, err = signer.SignCancelTx(ctx, txData) // cancel the tx if err != nil { logger.Warn().Err(err).Msg(ErrorMsg(cctx)) return @@ -420,7 +438,7 @@ func (signer *Signer) TryProcessOutbound( // params field is used to pass input parameters for command requests, currently it is used to pass the ERC20 // contract address when a whitelist command is requested params := msg[1] - tx, err = signer.SignCommandTx(txData, cmd, params) + tx, err = signer.SignCommandTx(ctx, txData, cmd, params) if err != nil { logger.Warn().Err(err).Msg(ErrorMsg(cctx)) return @@ -435,7 +453,7 @@ func (signer *Signer) TryProcessOutbound( cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) - tx, err = signer.SignWithdrawTx(txData) + tx, err = signer.SignWithdrawTx(ctx, txData) case coin.CoinType_ERC20: logger.Info().Msgf( "SignERC20WithdrawTx: %d => %s, nonce %d, gasPrice %d", @@ -444,7 +462,7 @@ func (signer *Signer) TryProcessOutbound( cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) - tx, err = signer.SignERC20WithdrawTx(txData) + tx, err = signer.SignERC20WithdrawTx(ctx, txData) case coin.CoinType_Zeta: logger.Info().Msgf( "SignOutbound: %d => %s, nonce %d, gasPrice %d", @@ -453,7 +471,7 @@ func (signer *Signer) TryProcessOutbound( cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) - tx, err = signer.SignOutbound(txData) + tx, err = signer.SignOutbound(ctx, txData) } if err != nil { logger.Warn().Err(err).Msg(ErrorMsg(cctx)) @@ -470,7 +488,7 @@ func (signer *Signer) TryProcessOutbound( ) txData.srcChainID = big.NewInt(cctx.OutboundParams[0].ReceiverChainId) txData.toChainID = big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) - tx, err = signer.SignRevertTx(txData) + tx, err = signer.SignRevertTx(ctx, txData) case coin.CoinType_Gas: logger.Info().Msgf( "SignWithdrawTx: %d => %s, nonce %d, gasPrice %d", @@ -479,7 +497,7 @@ func (signer *Signer) TryProcessOutbound( cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) - tx, err = signer.SignWithdrawTx(txData) + tx, err = signer.SignWithdrawTx(ctx, txData) case coin.CoinType_ERC20: logger.Info().Msgf("SignERC20WithdrawTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, @@ -487,7 +505,7 @@ func (signer *Signer) TryProcessOutbound( cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) - tx, err = signer.SignERC20WithdrawTx(txData) + tx, err = signer.SignERC20WithdrawTx(ctx, txData) } if err != nil { logger.Warn().Err(err).Msg(ErrorMsg(cctx)) @@ -504,7 +522,7 @@ func (signer *Signer) TryProcessOutbound( txData.srcChainID = big.NewInt(cctx.OutboundParams[0].ReceiverChainId) txData.toChainID = big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) - tx, err = signer.SignRevertTx(txData) + tx, err = signer.SignRevertTx(ctx, txData) if err != nil { logger.Warn().Err(err).Msg(ErrorMsg(cctx)) return @@ -517,7 +535,7 @@ func (signer *Signer) TryProcessOutbound( cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) - tx, err = signer.SignOutbound(txData) + tx, err = signer.SignOutbound(ctx, txData) if err != nil { logger.Warn().Err(err).Msg(ErrorMsg(cctx)) return @@ -532,11 +550,12 @@ func (signer *Signer) TryProcessOutbound( ) // Broadcast Signed Tx - signer.BroadcastOutbound(tx, cctx, logger, myID, zetacoreClient, txData) + signer.BroadcastOutbound(ctx, tx, cctx, logger, myID, zetacoreClient, txData) } // BroadcastOutbound signed transaction through evm rpc client func (signer *Signer) BroadcastOutbound( + ctx context.Context, tx *ethtypes.Transaction, cctx *types.CrossChainTx, logger zerolog.Logger, @@ -544,11 +563,14 @@ func (signer *Signer) BroadcastOutbound( zetacoreClient interfaces.ZetacoreClient, txData *OutboundData, ) { + app, err := zctx.FromContext(ctx) + if err != nil { + logger.Err(err).Msg("error getting app context") + return + } + // Get destination chain for logging - toChain, found := chains.GetChainFromChainID( - txData.toChainID.Int64(), - signer.AppContext().GetAdditionalChains(), - ) + toChain, found := chains.GetChainFromChainID(txData.toChainID.Int64(), app.GetAdditionalChains()) if !found { logger.Warn().Msgf("BroadcastOutbound: unknown chain %d", txData.toChainID.Int64()) return @@ -579,7 +601,7 @@ func (signer *Signer) BroadcastOutbound( outboundHash, ) if report { - signer.reportToOutboundTracker(zetacoreClient, toChain.ChainId, tx.Nonce(), outboundHash, logger) + signer.reportToOutboundTracker(ctx, zetacoreClient, toChain.ChainId, tx.Nonce(), outboundHash, logger) } if !retry { break @@ -589,7 +611,7 @@ func (signer *Signer) BroadcastOutbound( } logger.Info().Msgf("BroadcastOutbound: broadcasted tx %s on chain %d nonce %d signer %s", outboundHash, toChain.ChainId, cctx.GetCurrentOutboundParam().TssNonce, myID) - signer.reportToOutboundTracker(zetacoreClient, toChain.ChainId, tx.Nonce(), outboundHash, logger) + signer.reportToOutboundTracker(ctx, zetacoreClient, toChain.ChainId, tx.Nonce(), outboundHash, logger) break // successful broadcast; no need to retry } } @@ -600,7 +622,7 @@ func (signer *Signer) BroadcastOutbound( // address asset, // uint256 amount, // ) external onlyTssAddress -func (signer *Signer) SignERC20WithdrawTx(txData *OutboundData) (*ethtypes.Transaction, error) { +func (signer *Signer) SignERC20WithdrawTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { var data []byte var err error data, err = signer.erc20CustodyABI.Pack("withdraw", txData.to, txData.asset, txData.amount) @@ -609,6 +631,7 @@ func (signer *Signer) SignERC20WithdrawTx(txData *OutboundData) (*ethtypes.Trans } tx, _, _, err := signer.Sign( + ctx, data, signer.er20CustodyAddress, zeroValue, @@ -666,7 +689,11 @@ func ErrorMsg(cctx *types.CrossChainTx) string { // SignWhitelistERC20Cmd signs a whitelist command for ERC20 token // TODO(revamp): move the cmd in a specific file -func (signer *Signer) SignWhitelistERC20Cmd(txData *OutboundData, params string) (*ethtypes.Transaction, error) { +func (signer *Signer) SignWhitelistERC20Cmd( + ctx context.Context, + txData *OutboundData, + params string, +) (*ethtypes.Transaction, error) { outboundParams := txData.outboundParams erc20 := ethcommon.HexToAddress(params) if erc20 == (ethcommon.Address{}) { @@ -681,6 +708,7 @@ func (signer *Signer) SignWhitelistERC20Cmd(txData *OutboundData, params string) return nil, fmt.Errorf("whitelist pack error: %w", err) } tx, _, _, err := signer.Sign( + ctx, data, txData.to, zeroValue, @@ -697,8 +725,9 @@ func (signer *Signer) SignWhitelistERC20Cmd(txData *OutboundData, params string) // SignMigrateTssFundsCmd signs a migrate TSS funds command // TODO(revamp): move the cmd in a specific file -func (signer *Signer) SignMigrateTssFundsCmd(txData *OutboundData) (*ethtypes.Transaction, error) { +func (signer *Signer) SignMigrateTssFundsCmd(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { tx, _, _, err := signer.Sign( + ctx, nil, txData.to, txData.amount, @@ -716,6 +745,7 @@ func (signer *Signer) SignMigrateTssFundsCmd(txData *OutboundData) (*ethtypes.Tr // reportToOutboundTracker reports outboundHash to tracker only when tx receipt is available // TODO(revamp): move outbound tracker function to a outbound tracker file func (signer *Signer) reportToOutboundTracker( + ctx context.Context, zetacoreClient interfaces.ZetacoreClient, chainID int64, nonce uint64, @@ -760,7 +790,7 @@ func (signer *Signer) reportToOutboundTracker( break } // try getting the tx - _, isPending, err = signer.client.TransactionByHash(context.TODO(), ethcommon.HexToHash(outboundHash)) + _, isPending, err = signer.client.TransactionByHash(ctx, ethcommon.HexToHash(outboundHash)) if err != nil { logger.Info(). Err(err). @@ -770,7 +800,7 @@ func (signer *Signer) reportToOutboundTracker( // if tx is include in a block, try getting receipt if !isPending { report = true // included - receipt, err := signer.client.TransactionReceipt(context.TODO(), ethcommon.HexToHash(outboundHash)) + receipt, err := signer.client.TransactionReceipt(ctx, ethcommon.HexToHash(outboundHash)) if err != nil { logger.Info(). Err(err). @@ -797,7 +827,7 @@ func (signer *Signer) reportToOutboundTracker( break } // stop if the cctx is already finalized - cctx, err := zetacoreClient.GetCctxByNonce(chainID, nonce) + cctx, err := zetacoreClient.GetCctxByNonce(ctx, chainID, nonce) if err != nil { logger.Err(err). Msgf("reportToOutboundTracker: error getting cctx for chain %d nonce %d outboundHash %s", chainID, nonce, outboundHash) @@ -806,7 +836,7 @@ func (signer *Signer) reportToOutboundTracker( break } // report to outbound tracker - zetaHash, err := zetacoreClient.AddOutboundTracker(chainID, nonce, outboundHash, nil, "", -1) + zetaHash, err := zetacoreClient.AddOutboundTracker(ctx, chainID, nonce, outboundHash, nil, "", -1) if err != nil { logger.Err(err). Msgf("reportToOutboundTracker: error adding to outbound tracker for chain %d nonce %d outboundHash %s", chainID, nonce, outboundHash) @@ -826,7 +856,7 @@ func (signer *Signer) reportToOutboundTracker( } // getEVMRPC is a helper function to set up the client and signer, also initializes a mock client for unit tests -func getEVMRPC(endpoint string) (interfaces.EVMRPCClient, ethtypes.Signer, error) { +func getEVMRPC(ctx context.Context, endpoint string) (interfaces.EVMRPCClient, ethtypes.Signer, error) { if endpoint == mocks.EVMRPCEnabled { chainID := big.NewInt(chains.BscMainnet.ChainId) ethSigner := ethtypes.NewLondonSigner(chainID) @@ -836,14 +866,16 @@ func getEVMRPC(endpoint string) (interfaces.EVMRPCClient, ethtypes.Signer, error client, err := ethclient.Dial(endpoint) if err != nil { - return nil, nil, err + return nil, nil, errors.Wrapf(err, "unable to dial EVM client (endpoint %q)", endpoint) } - chainID, err := client.ChainID(context.TODO()) + chainID, err := client.ChainID(ctx) if err != nil { - return nil, nil, err + return nil, nil, errors.Wrap(err, "unable to get chain ID") } + ethSigner := ethtypes.LatestSignerForChainID(chainID) + return client, ethSigner, nil } diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index fd412b4bfd..9880bb233e 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -1,6 +1,7 @@ package signer import ( + "context" "math/big" "testing" @@ -10,6 +11,8 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/constant" @@ -19,8 +22,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" - "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" "github.com/zeta-chain/zetacore/zetaclient/testutils" @@ -35,6 +36,8 @@ var ( // getNewEvmSigner creates a new EVM chain signer for testing func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { + ctx := context.Background() + // use default mock TSS if not provided if tss == nil { tss = mocks.NewTSSMainnet() @@ -43,11 +46,10 @@ func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { mpiAddress := ConnectorAddress erc20CustodyAddress := ERC20CustodyAddress logger := base.Logger{} - cfg := config.NewConfig() return NewSigner( + ctx, chains.BscMainnet, - context.New(cfg, zerolog.Nop()), tss, nil, logger, @@ -55,33 +57,36 @@ func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { config.GetConnectorABI(), config.GetERC20CustodyABI(), mpiAddress, - erc20CustodyAddress) + erc20CustodyAddress, + ) } // getNewEvmChainObserver creates a new EVM chain observer for testing func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.Observer, error) { + ctx := context.Background() + // use default mock TSS if not provided if tss == nil { tss = mocks.NewTSSMainnet() } - cfg := config.NewConfig() + cfg := config.New(false) // prepare mock arguments to create observer evmcfg := config.EVMConfig{Chain: chains.BscMainnet, Endpoint: "http://localhost:8545"} evmClient := mocks.NewMockEvmClient().WithBlockNumber(1000) params := mocks.MockChainParams(evmcfg.Chain.ChainId, 10) cfg.EVMChainConfigs[chains.BscMainnet.ChainId] = evmcfg - appContext := context.New(cfg, zerolog.Nop()) + //appContext := context.New(cfg, zerolog.Nop()) dbpath := sample.CreateTempDir(t) logger := base.Logger{} ts := &metrics.TelemetryServer{} return observer.NewObserver( + ctx, evmcfg, evmClient, params, - appContext, - mocks.NewMockZetacoreClient(), + mocks.NewZetacoreClient(t), tss, dbpath, logger, @@ -153,6 +158,8 @@ func TestSigner_SetGetERC20CustodyAddress(t *testing.T) { } func TestSigner_TryProcessOutbound(t *testing.T) { + ctx := makeCtx() + evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) cctx := getCCTX(t) @@ -161,8 +168,12 @@ func TestSigner_TryProcessOutbound(t *testing.T) { require.NoError(t, err) // Test with mock client that has keys - client := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) - evmSigner.TryProcessOutbound(cctx, processor, "123", mockObserver, client, 123) + client := mocks.NewZetacoreClient(t). + WithKeys(&keys.Keys{}). + WithZetaChain(). + WithPostVoteOutbound("", "") + + evmSigner.TryProcessOutbound(ctx, cctx, processor, "123", mockObserver, client, 123) // Check if cctx was signed and broadcasted list := evmSigner.GetReportedTxList() @@ -170,6 +181,8 @@ func TestSigner_TryProcessOutbound(t *testing.T) { } func TestSigner_SignOutbound(t *testing.T) { + ctx := makeCtx() + // Setup evm signer tss := mocks.NewTSSMainnet() evmSigner, err := getNewEvmSigner(tss) @@ -180,20 +193,13 @@ func TestSigner_SignOutbound(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) t.Run("SignOutbound - should successfully sign", func(t *testing.T) { // Call SignOutbound - tx, err := evmSigner.SignOutbound(txData) + tx, err := evmSigner.SignOutbound(ctx, txData) require.NoError(t, err) // Verify Signature @@ -205,13 +211,15 @@ func TestSigner_SignOutbound(t *testing.T) { tss.Pause() // Call SignOutbound - tx, err := evmSigner.SignOutbound(txData) + tx, err := evmSigner.SignOutbound(ctx, txData) require.ErrorContains(t, err, "sign onReceive error") require.Nil(t, tx) }) } func TestSigner_SignRevertTx(t *testing.T) { + ctx := makeCtx() + // Setup evm signer tss := mocks.NewTSSMainnet() evmSigner, err := getNewEvmSigner(tss) @@ -221,20 +229,13 @@ func TestSigner_SignRevertTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) t.Run("SignRevertTx - should successfully sign", func(t *testing.T) { // Call SignRevertTx - tx, err := evmSigner.SignRevertTx(txData) + tx, err := evmSigner.SignRevertTx(ctx, txData) require.NoError(t, err) // Verify tx signature @@ -250,13 +251,15 @@ func TestSigner_SignRevertTx(t *testing.T) { tss.Pause() // Call SignRevertTx - tx, err := evmSigner.SignRevertTx(txData) + tx, err := evmSigner.SignRevertTx(ctx, txData) require.ErrorContains(t, err, "sign onRevert error") require.Nil(t, tx) }) } func TestSigner_SignCancelTx(t *testing.T) { + ctx := makeCtx() + // Setup evm signer tss := mocks.NewTSSMainnet() evmSigner, err := getNewEvmSigner(tss) @@ -266,20 +269,13 @@ func TestSigner_SignCancelTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) t.Run("SignCancelTx - should successfully sign", func(t *testing.T) { // Call SignRevertTx - tx, err := evmSigner.SignCancelTx(txData) + tx, err := evmSigner.SignCancelTx(ctx, txData) require.NoError(t, err) // Verify tx signature @@ -295,13 +291,15 @@ func TestSigner_SignCancelTx(t *testing.T) { tss.Pause() // Call SignCancelTx - tx, err := evmSigner.SignCancelTx(txData) + tx, err := evmSigner.SignCancelTx(ctx, txData) require.ErrorContains(t, err, "SignCancelTx error") require.Nil(t, tx) }) } func TestSigner_SignWithdrawTx(t *testing.T) { + ctx := makeCtx() + // Setup evm signer tss := mocks.NewTSSMainnet() evmSigner, err := getNewEvmSigner(tss) @@ -311,20 +309,13 @@ func TestSigner_SignWithdrawTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) t.Run("SignWithdrawTx - should successfully sign", func(t *testing.T) { // Call SignWithdrawTx - tx, err := evmSigner.SignWithdrawTx(txData) + tx, err := evmSigner.SignWithdrawTx(ctx, txData) require.NoError(t, err) // Verify tx signature @@ -339,13 +330,15 @@ func TestSigner_SignWithdrawTx(t *testing.T) { tss.Pause() // Call SignWithdrawTx - tx, err := evmSigner.SignWithdrawTx(txData) + tx, err := evmSigner.SignWithdrawTx(ctx, txData) require.ErrorContains(t, err, "SignWithdrawTx error") require.Nil(t, tx) }) } func TestSigner_SignCommandTx(t *testing.T) { + ctx := makeCtx() + // Setup evm signer evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) @@ -354,14 +347,7 @@ func TestSigner_SignCommandTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) @@ -369,7 +355,7 @@ func TestSigner_SignCommandTx(t *testing.T) { cmd := constant.CmdWhitelistERC20 params := ConnectorAddress.Hex() // Call SignCommandTx - tx, err := evmSigner.SignCommandTx(txData, cmd, params) + tx, err := evmSigner.SignCommandTx(ctx, txData, cmd, params) require.NoError(t, err) // Verify tx signature @@ -384,7 +370,7 @@ func TestSigner_SignCommandTx(t *testing.T) { t.Run("SignCommandTx CmdMigrateTssFunds", func(t *testing.T) { cmd := constant.CmdMigrateTssFunds // Call SignCommandTx - tx, err := evmSigner.SignCommandTx(txData, cmd, "") + tx, err := evmSigner.SignCommandTx(ctx, txData, cmd, "") require.NoError(t, err) // Verify tx signature @@ -397,6 +383,8 @@ func TestSigner_SignCommandTx(t *testing.T) { } func TestSigner_SignERC20WithdrawTx(t *testing.T) { + ctx := makeCtx() + // Setup evm signer tss := mocks.NewTSSMainnet() evmSigner, err := getNewEvmSigner(tss) @@ -406,20 +394,13 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) t.Run("SignERC20WithdrawTx - should successfully sign", func(t *testing.T) { // Call SignERC20WithdrawTx - tx, err := evmSigner.SignERC20WithdrawTx(txData) + tx, err := evmSigner.SignERC20WithdrawTx(ctx, txData) require.NoError(t, err) // Verify tx signature @@ -436,13 +417,15 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { tss.Pause() // Call SignERC20WithdrawTx - tx, err := evmSigner.SignERC20WithdrawTx(txData) + tx, err := evmSigner.SignERC20WithdrawTx(ctx, txData) require.ErrorContains(t, err, "sign withdraw error") require.Nil(t, tx) }) } func TestSigner_BroadcastOutbound(t *testing.T) { + ctx := makeCtx() + // Setup evm signer evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) @@ -451,28 +434,22 @@ func TestSigner_BroadcastOutbound(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) t.Run("BroadcastOutbound - should successfully broadcast", func(t *testing.T) { // Call SignERC20WithdrawTx - tx, err := evmSigner.SignERC20WithdrawTx(txData) + tx, err := evmSigner.SignERC20WithdrawTx(ctx, txData) require.NoError(t, err) evmSigner.BroadcastOutbound( + ctx, tx, cctx, zerolog.Logger{}, sdktypes.AccAddress{}, - mocks.NewMockZetacoreClient(), + mocks.NewZetacoreClient(t), txData, ) @@ -483,8 +460,10 @@ func TestSigner_BroadcastOutbound(t *testing.T) { } func TestSigner_getEVMRPC(t *testing.T) { + ctx := context.Background() + t.Run("getEVMRPC error dialing", func(t *testing.T) { - client, signer, err := getEVMRPC("invalidEndpoint") + client, signer, err := getEVMRPC(ctx, "invalidEndpoint") require.Nil(t, client) require.Nil(t, signer) require.Error(t, err) @@ -499,6 +478,8 @@ func TestSigner_SignerErrorMsg(t *testing.T) { } func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { + ctx := makeCtx() + // Setup evm signer tss := mocks.NewTSSMainnet() evmSigner, err := getNewEvmSigner(tss) @@ -508,20 +489,13 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) t.Run("SignWhitelistERC20Cmd - should successfully sign", func(t *testing.T) { // Call SignWhitelistERC20Cmd - tx, err := evmSigner.SignWhitelistERC20Cmd(txData, sample.EthAddress().Hex()) + tx, err := evmSigner.SignWhitelistERC20Cmd(ctx, txData, sample.EthAddress().Hex()) require.NoError(t, err) require.NotNil(t, tx) @@ -533,7 +507,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { verifyTxBodyBasics(t, tx, txData.to, txData.nonce, zeroValue) }) t.Run("SignWhitelistERC20Cmd - should fail on invalid erc20 address", func(t *testing.T) { - tx, err := evmSigner.SignWhitelistERC20Cmd(txData, "") + tx, err := evmSigner.SignWhitelistERC20Cmd(ctx, txData, "") require.Nil(t, tx) require.ErrorContains(t, err, "invalid erc20 address") }) @@ -542,13 +516,15 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { tss.Pause() // Call SignWhitelistERC20Cmd - tx, err := evmSigner.SignWhitelistERC20Cmd(txData, sample.EthAddress().Hex()) + tx, err := evmSigner.SignWhitelistERC20Cmd(ctx, txData, sample.EthAddress().Hex()) require.ErrorContains(t, err, "sign whitelist error") require.Nil(t, tx) }) } func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { + ctx := makeCtx() + // Setup evm signer tss := mocks.NewTSSMainnet() evmSigner, err := getNewEvmSigner(tss) @@ -558,20 +534,13 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData( - evmSigner.AppContext(), - cctx, - mockObserver, - evmSigner.EvmClient(), - zerolog.Logger{}, - 123, - ) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) t.Run("SignMigrateTssFundsCmd - should successfully sign", func(t *testing.T) { // Call SignMigrateTssFundsCmd - tx, err := evmSigner.SignMigrateTssFundsCmd(txData) + tx, err := evmSigner.SignMigrateTssFundsCmd(ctx, txData) require.NoError(t, err) require.NotNil(t, tx) @@ -588,8 +557,13 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { tss.Pause() // Call SignMigrateTssFundsCmd - tx, err := evmSigner.SignMigrateTssFundsCmd(txData) + tx, err := evmSigner.SignMigrateTssFundsCmd(ctx, txData) require.ErrorContains(t, err, "SignMigrateTssFundsCmd error") require.Nil(t, tx) }) } +func makeCtx() context.Context { + app := zctx.New(config.New(false), zerolog.Nop()) + + return zctx.WithAppContext(context.Background(), app) +} diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 2272ef1dfe..7ed2d7bb4d 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -19,7 +19,6 @@ import ( "github.com/zeta-chain/go-tss/blame" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/pkg/proofs" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" @@ -38,18 +37,23 @@ const ( // ChainObserver is the interface for chain observer type ChainObserver interface { - Start() + Start(ctx context.Context) Stop() - IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logger zerolog.Logger) (bool, bool, error) + IsOutboundProcessed( + ctx context.Context, + cctx *crosschaintypes.CrossChainTx, + logger zerolog.Logger, + ) (bool, bool, error) SetChainParams(observertypes.ChainParams) GetChainParams() observertypes.ChainParams GetTxID(nonce uint64) string - WatchInboundTracker() + WatchInboundTracker(ctx context.Context) error } // ChainSigner is the interface to sign transactions for a chain type ChainSigner interface { TryProcessOutbound( + ctx context.Context, cctx *crosschaintypes.CrossChainTx, outboundProc *outboundprocessor.Processor, outboundID string, @@ -63,28 +67,75 @@ type ChainSigner interface { GetERC20CustodyAddress() ethcommon.Address } -// ZetacoreClient is the client interface to interact with zetacore -type ZetacoreClient interface { - PostVoteInbound(gasLimit, retryGasLimit uint64, msg *crosschaintypes.MsgVoteInbound) (string, string, error) - PostVoteOutbound( - sendHash string, - outboundHash string, - outBlockHeight uint64, - outboundGasUsed uint64, - outboundEffectiveGasPrice *big.Int, - outboundEffectiveGasLimit uint64, - amount *big.Int, - status chains.ReceiveStatus, +// ZetacoreVoter represents voter interface. +type ZetacoreVoter interface { + PostVoteBlockHeader( + ctx context.Context, + chainID int64, + txhash []byte, + height int64, + header proofs.HeaderData, + ) (string, error) + PostVoteGasPrice( + ctx context.Context, chain chains.Chain, - nonce uint64, - coinType coin.CoinType, + gasPrice uint64, + supply string, + blockNum uint64, + ) (string, error) + PostVoteInbound( + ctx context.Context, + gasLimit, retryGasLimit uint64, + msg *crosschaintypes.MsgVoteInbound, + ) (string, string, error) + PostVoteOutbound( + ctx context.Context, + gasLimit, retryGasLimit uint64, + msg *crosschaintypes.MsgVoteOutbound, ) (string, string, error) - PostGasPrice(chain chains.Chain, gasPrice uint64, supply string, blockNum uint64) (string, error) - PostVoteBlockHeader(chainID int64, txhash []byte, height int64, header proofs.HeaderData) (string, error) - GetBlockHeaderChainState(chainID int64) (lightclienttypes.QueryGetChainStateResponse, error) + PostVoteBlameData(ctx context.Context, blame *blame.Blame, chainID int64, index string) (string, error) +} - PostBlameData(blame *blame.Blame, chainID int64, index string) (string, error) +// ZetacoreClient is the client interface to interact with zetacore +// +//go:generate mockery --name ZetacoreClient --filename zetacore_client.go --case underscore --output ../../testutils/mocks +type ZetacoreClient interface { + ZetacoreVoter + + Chain() chains.Chain + GetLogger() *zerolog.Logger + GetKeys() keyinterfaces.ObserverKeys + + GetKeyGen(ctx context.Context) (*observertypes.Keygen, error) + + GetBlockHeight(ctx context.Context) (int64, error) + GetBlockHeaderChainState(ctx context.Context, chainID int64) (*lightclienttypes.ChainState, error) + + ListPendingCCTX(ctx context.Context, chainID int64) ([]*crosschaintypes.CrossChainTx, uint64, error) + ListPendingCCTXWithinRateLimit( + ctx context.Context, + ) (*crosschaintypes.QueryListPendingCctxWithinRateLimitResponse, error) + + GetRateLimiterInput(ctx context.Context, window int64) (*crosschaintypes.QueryRateLimiterInputResponse, error) + GetPendingNoncesByChain(ctx context.Context, chainID int64) (observertypes.PendingNonces, error) + + GetCctxByNonce(ctx context.Context, chainID int64, nonce uint64) (*crosschaintypes.CrossChainTx, error) + GetOutboundTracker(ctx context.Context, chain chains.Chain, nonce uint64) (*crosschaintypes.OutboundTracker, error) + GetAllOutboundTrackerByChain( + ctx context.Context, + chainID int64, + order Order, + ) ([]crosschaintypes.OutboundTracker, error) + GetCrosschainFlags(ctx context.Context) (observertypes.CrosschainFlags, error) + GetRateLimiterFlags(ctx context.Context) (crosschaintypes.RateLimiterFlags, error) + GetObserverList(ctx context.Context) ([]string, error) + GetBTCTSSAddress(ctx context.Context, chainID int64) (string, error) + GetZetaHotKeyBalance(ctx context.Context) (sdkmath.Int, error) + GetInboundTrackersForChain(ctx context.Context, chainID int64) ([]crosschaintypes.InboundTracker, error) + + // todo(revamp): refactor input to struct AddOutboundTracker( + ctx context.Context, chainID int64, nonce uint64, txHash string, @@ -92,26 +143,9 @@ type ZetacoreClient interface { blockHash string, txIndex int64, ) (string, error) - Chain() chains.Chain - GetLogger() *zerolog.Logger - GetKeys() keyinterfaces.ObserverKeys - GetKeyGen() (*observertypes.Keygen, error) - GetBlockHeight() (int64, error) - ListPendingCctx(chainID int64) ([]*crosschaintypes.CrossChainTx, uint64, error) - ListPendingCctxWithinRatelimit() ([]*crosschaintypes.CrossChainTx, uint64, int64, string, bool, error) - GetRateLimiterInput(window int64) (crosschaintypes.QueryRateLimiterInputResponse, error) - GetPendingNoncesByChain(chainID int64) (observertypes.PendingNonces, error) - GetCctxByNonce(chainID int64, nonce uint64) (*crosschaintypes.CrossChainTx, error) - GetOutboundTracker(chain chains.Chain, nonce uint64) (*crosschaintypes.OutboundTracker, error) - GetAllOutboundTrackerByChain(chainID int64, order Order) ([]crosschaintypes.OutboundTracker, error) - GetCrosschainFlags() (observertypes.CrosschainFlags, error) - GetRateLimiterFlags() (crosschaintypes.RateLimiterFlags, error) - GetObserverList() ([]string, error) - GetBtcTssAddress(chainID int64) (string, error) - GetZetaHotKeyBalance() (sdkmath.Int, error) - GetInboundTrackersForChain(chainID int64) ([]crosschaintypes.InboundTracker, error) - Pause() - Unpause() + + Stop() + OnBeforeStop(callback func()) } // BTCRPCClient is the interface for BTC RPC client @@ -167,10 +201,17 @@ type TSSSigner interface { // Note: it specifies optionalPubkey to use a different pubkey than the current pubkey set during keygen // TODO: check if optionalPubkey is needed // https://github.com/zeta-chain/node/issues/2085 - Sign(data []byte, height uint64, nonce uint64, chainID int64, optionalPubkey string) ([65]byte, error) + Sign( + ctx context.Context, + data []byte, + height uint64, + nonce uint64, + chainID int64, + optionalPubkey string, + ) ([65]byte, error) // SignBatch signs the data in batch - SignBatch(digests [][]byte, height uint64, nonce uint64, chainID int64) ([][65]byte, error) + SignBatch(ctx context.Context, digests [][]byte, height uint64, nonce uint64, chainID int64) ([][65]byte, error) EVMAddress() ethcommon.Address BTCAddress() string diff --git a/zetaclient/config/config.go b/zetaclient/config/config.go index 6efd149628..89e7dc4d37 100644 --- a/zetaclient/config/config.go +++ b/zetaclient/config/config.go @@ -53,7 +53,7 @@ func Load(path string) (Config, error) { file = filepath.Clean(file) // read config - cfg := NewConfig() + cfg := New(false) input, err := os.ReadFile(file) if err != nil { return Config{}, err diff --git a/zetaclient/config/config_chain.go b/zetaclient/config/config_chain.go index 5946c4ca62..2da362d19f 100644 --- a/zetaclient/config/config_chain.go +++ b/zetaclient/config/config_chain.go @@ -11,72 +11,83 @@ const ( ) const ( - // ConnectorAbiString is the ABI of the connector contract + // connectorAbiString is the ABI of the connector contract // TODO(revamp): we should be able to use info from Go binding - ConnectorAbiString = ` + connectorAbiString = ` [{"inputs":[{"internalType":"address","name":"_zetaTokenAddress","type":"address"},{"internalType":"address","name":"_tssAddress","type":"address"},{"internalType":"address","name":"_tssAddressUpdater","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"originSenderAddress","type":"bytes"},{"indexed":true,"internalType":"uint256","name":"originChainId","type":"uint256"},{"indexed":true,"internalType":"address","name":"destinationAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"zetaAmount","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"message","type":"bytes"},{"indexed":true,"internalType":"bytes32","name":"internalSendHash","type":"bytes32"}],"name":"ZetaReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"originSenderAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"originChainId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"destinationChainId","type":"uint256"},{"indexed":true,"internalType":"bytes","name":"destinationAddress","type":"bytes"},{"indexed":false,"internalType":"uint256","name":"zetaAmount","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"message","type":"bytes"},{"indexed":true,"internalType":"bytes32","name":"internalSendHash","type":"bytes32"}],"name":"ZetaReverted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"originSenderAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"destinationChainId","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"destinationAddress","type":"bytes"},{"indexed":false,"internalType":"uint256","name":"zetaAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"gasLimit","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"message","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"zetaParams","type":"bytes"}],"name":"ZetaSent","type":"event"},{"inputs":[],"name":"getLockedAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"originSenderAddress","type":"bytes"},{"internalType":"uint256","name":"originChainId","type":"uint256"},{"internalType":"address","name":"destinationAddress","type":"address"},{"internalType":"uint256","name":"zetaAmount","type":"uint256"},{"internalType":"bytes","name":"message","type":"bytes"},{"internalType":"bytes32","name":"internalSendHash","type":"bytes32"}],"name":"onReceive","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"originSenderAddress","type":"address"},{"internalType":"uint256","name":"originChainId","type":"uint256"},{"internalType":"bytes","name":"destinationAddress","type":"bytes"},{"internalType":"uint256","name":"destinationChainId","type":"uint256"},{"internalType":"uint256","name":"zetaAmount","type":"uint256"},{"internalType":"bytes","name":"message","type":"bytes"},{"internalType":"bytes32","name":"internalSendHash","type":"bytes32"}],"name":"onRevert","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceTssAddressUpdater","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"destinationChainId","type":"uint256"},{"internalType":"bytes","name":"destinationAddress","type":"bytes"},{"internalType":"uint256","name":"gasLimit","type":"uint256"},{"internalType":"bytes","name":"message","type":"bytes"},{"internalType":"uint256","name":"zetaAmount","type":"uint256"},{"internalType":"bytes","name":"zetaParams","type":"bytes"}],"internalType":"struct ZetaInterfaces.SendInput","name":"input","type":"tuple"}],"name":"send","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"tssAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"tssAddressUpdater","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_tssAddress","type":"address"}],"name":"updateTssAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"zetaToken","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]` - // ERC20CustodyAbiString is the ABI of the erc20 custodu contract + // erc20CustodyAbiString is the ABI of the erc20 custodu contract // TODO(revamp): we should be able to use info from Go binding - ERC20CustodyAbiString = ` + erc20CustodyAbiString = ` [{"inputs":[{"internalType":"address","name":"_TSSAddress","type":"address"},{"internalType":"address","name":"_TSSAddressUpdater","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"InvalidSender","type":"error"},{"inputs":[],"name":"InvalidTSSUpdater","type":"error"},{"inputs":[],"name":"IsPaused","type":"error"},{"inputs":[],"name":"NotPaused","type":"error"},{"inputs":[],"name":"NotWhitelisted","type":"error"},{"inputs":[],"name":"ZeroAddress","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"recipient","type":"bytes"},{"indexed":false,"internalType":"address","name":"asset","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"message","type":"bytes"}],"name":"Deposited","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"sender","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"sender","type":"address"}],"name":"Unpaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"asset","type":"address"}],"name":"Unwhitelisted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"asset","type":"address"}],"name":"Whitelisted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"address","name":"asset","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdrawn","type":"event"},{"inputs":[],"name":"TSSAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TSSAddressUpdater","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"recipient","type":"bytes"},{"internalType":"address","name":"asset","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes","name":"message","type":"bytes"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceTSSAddressUpdater","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"unwhitelist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"updateTSSAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"whitelist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"whitelisted","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"address","name":"asset","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]` ) // GetConnectorABI returns the ABI of the connector contract func GetConnectorABI() string { - return ConnectorAbiString + return connectorAbiString } // GetERC20CustodyABI returns the ABI of the erc20 custody contract func GetERC20CustodyABI() string { - return ERC20CustodyAbiString + return erc20CustodyAbiString } -// New returns a new config -// It is initialize with default chain configs -func New() Config { - return Config{ - cfgLock: &sync.RWMutex{}, - EVMChainConfigs: evmChainsConfigs, - BitcoinConfig: bitcoinConfigRegnet, +// New constructs Config optionally with default values. +func New(setDefaults bool) Config { + cfg := Config{ + EVMChainConfigs: make(map[int64]EVMConfig), + BitcoinConfig: BTCConfig{}, + + mu: &sync.RWMutex{}, + } + + if setDefaults { + cfg.BitcoinConfig = bitcoinConfigRegnet() + cfg.EVMChainConfigs = evmChainsConfigs() } + + return cfg } // bitcoinConfigRegnet contains Bitcoin config for regnet -var bitcoinConfigRegnet = BTCConfig{ - RPCUsername: "smoketest", // smoketest is the previous name for E2E test, we keep this name for compatibility between client versions in upgrade test - RPCPassword: "123", - RPCHost: "bitcoin:18443", - RPCParams: "regtest", +func bitcoinConfigRegnet() BTCConfig { + return BTCConfig{ + RPCUsername: "smoketest", // smoketest is the previous name for E2E test, we keep this name for compatibility between client versions in upgrade test + RPCPassword: "123", + RPCHost: "bitcoin:18443", + RPCParams: "regtest", + } } // evmChainsConfigs contains EVM chain configs // it contains list of EVM chains with empty endpoint except for localnet -var evmChainsConfigs = map[int64]EVMConfig{ - chains.Ethereum.ChainId: { - Chain: chains.Ethereum, - }, - chains.BscMainnet.ChainId: { - Chain: chains.BscMainnet, - }, - chains.Goerli.ChainId: { - Chain: chains.Goerli, - Endpoint: "", - }, - chains.Sepolia.ChainId: { - Chain: chains.Sepolia, - Endpoint: "", - }, - chains.BscTestnet.ChainId: { - Chain: chains.BscTestnet, - Endpoint: "", - }, - chains.Mumbai.ChainId: { - Chain: chains.Mumbai, - Endpoint: "", - }, - chains.GoerliLocalnet.ChainId: { - Chain: chains.GoerliLocalnet, - Endpoint: "http://eth:8545", - }, +func evmChainsConfigs() map[int64]EVMConfig { + return map[int64]EVMConfig{ + chains.Ethereum.ChainId: { + Chain: chains.Ethereum, + }, + chains.BscMainnet.ChainId: { + Chain: chains.BscMainnet, + }, + chains.Goerli.ChainId: { + Chain: chains.Goerli, + Endpoint: "", + }, + chains.Sepolia.ChainId: { + Chain: chains.Sepolia, + Endpoint: "", + }, + chains.BscTestnet.ChainId: { + Chain: chains.BscTestnet, + Endpoint: "", + }, + chains.Mumbai.ChainId: { + Chain: chains.Mumbai, + Endpoint: "", + }, + chains.GoerliLocalnet.ChainId: { + Chain: chains.GoerliLocalnet, + Endpoint: "http://eth:8545", + }, + } } diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index 96cdf24a4c..2ff5f1a657 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -57,8 +57,6 @@ type ComplianceConfig struct { // TODO: use snake case for json fields // https://github.com/zeta-chain/node/issues/1020 type Config struct { - cfgLock *sync.RWMutex `json:"-"` - Peer string `json:"Peer"` PublicIP string `json:"PublicIP"` LogFormat string `json:"LogFormat"` @@ -84,29 +82,22 @@ type Config struct { // compliance config ComplianceConfig ComplianceConfig `json:"ComplianceConfig"` -} -// NewConfig returns a new Config with initialize EVM chain mapping and a new mutex -// TODO(revamp): consolidate with New function -func NewConfig() Config { - return Config{ - cfgLock: &sync.RWMutex{}, - EVMChainConfigs: make(map[int64]EVMConfig), - } + mu *sync.RWMutex } // GetEVMConfig returns the EVM config for the given chain ID func (c Config) GetEVMConfig(chainID int64) (EVMConfig, bool) { - c.cfgLock.RLock() - defer c.cfgLock.RUnlock() + c.mu.RLock() + defer c.mu.RUnlock() evmCfg, found := c.EVMChainConfigs[chainID] return evmCfg, found } // GetAllEVMConfigs returns a map of all EVM configs func (c Config) GetAllEVMConfigs() map[int64]EVMConfig { - c.cfgLock.RLock() - defer c.cfgLock.RUnlock() + c.mu.RLock() + defer c.mu.RUnlock() // deep copy evm configs copied := make(map[int64]EVMConfig, len(c.EVMChainConfigs)) @@ -118,8 +109,8 @@ func (c Config) GetAllEVMConfigs() map[int64]EVMConfig { // GetBTCConfig returns the BTC config func (c Config) GetBTCConfig() (BTCConfig, bool) { - c.cfgLock.RLock() - defer c.cfgLock.RUnlock() + c.mu.RLock() + defer c.mu.RUnlock() return c.BitcoinConfig, c.BitcoinConfig != (BTCConfig{}) } @@ -146,8 +137,8 @@ func (c Config) GetRestrictedAddressBook() map[string]bool { } // GetKeyringBackend returns the keyring backend -func (c *Config) GetKeyringBackend() KeyringBackend { - c.cfgLock.RLock() - defer c.cfgLock.RUnlock() +func (c Config) GetKeyringBackend() KeyringBackend { + c.mu.RLock() + defer c.mu.RUnlock() return c.KeyringBackend } diff --git a/zetaclient/config/types_test.go b/zetaclient/config/types_test.go deleted file mode 100644 index d7b82b3200..0000000000 --- a/zetaclient/config/types_test.go +++ /dev/null @@ -1 +0,0 @@ -package config_test diff --git a/zetaclient/context/app_test.go b/zetaclient/context/app_test.go index 640e5d7386..39847e2097 100644 --- a/zetaclient/context/app_test.go +++ b/zetaclient/context/app_test.go @@ -17,7 +17,7 @@ import ( func TestNew(t *testing.T) { var ( - testCfg = config.NewConfig() + testCfg = config.New(false) logger = zerolog.Nop() ) @@ -51,7 +51,7 @@ func TestNew(t *testing.T) { t.Run("should return nil chain params if chain id is not found", func(t *testing.T) { // create config with btc config - testCfg := config.NewConfig() + testCfg := config.New(false) testCfg.BitcoinConfig = config.BTCConfig{ RPCUsername: "test_user", RPCPassword: "test_password", @@ -69,7 +69,7 @@ func TestNew(t *testing.T) { }) t.Run("should create new zetacore context with config containing evm chain params", func(t *testing.T) { - testCfg := config.NewConfig() + testCfg := config.New(false) testCfg.EVMChainConfigs = map[int64]config.EVMConfig{ 1: { Chain: chains.Chain{ @@ -103,7 +103,7 @@ func TestNew(t *testing.T) { }) t.Run("should create new zetacore context with config containing btc config", func(t *testing.T) { - testCfg := config.NewConfig() + testCfg := config.New(false) testCfg.BitcoinConfig = config.BTCConfig{ RPCUsername: "test username", RPCPassword: "test password", @@ -117,7 +117,7 @@ func TestNew(t *testing.T) { func TestAppContextUpdate(t *testing.T) { var ( - testCfg = config.NewConfig() + testCfg = config.New(false) logger = zerolog.Nop() ) @@ -203,7 +203,7 @@ func TestAppContextUpdate(t *testing.T) { t.Run( "should update zetacore context after being created from config with evm and btc chain params", func(t *testing.T) { - testCfg := config.NewConfig() + testCfg := config.New(false) testCfg.EVMChainConfigs = map[int64]config.EVMConfig{ 1: { Chain: chains.Chain{ @@ -373,8 +373,8 @@ func TestIsInboundObservationEnabled(t *testing.T) { func TestGetBTCChainAndConfig(t *testing.T) { logger := zerolog.Nop() - emptyConfig := config.NewConfig() - nonEmptyConfig := config.New() + emptyConfig := config.New(false) + nonEmptyConfig := config.New(true) assertEmpty := func(t *testing.T, chain chains.Chain, btcConfig config.BTCConfig, enabled bool) { assert.Empty(t, chain) @@ -463,7 +463,7 @@ func TestGetBTCChainAndConfig(t *testing.T) { func TestGetBlockHeaderEnabledChains(t *testing.T) { // ARRANGE // Given app config - appContext := context.New(config.New(), zerolog.Nop()) + appContext := context.New(config.New(false), zerolog.Nop()) // That was eventually updated appContext.Update( @@ -499,7 +499,7 @@ func TestGetBlockHeaderEnabledChains(t *testing.T) { func TestGetAdditionalChains(t *testing.T) { // ARRANGE // Given app config - appContext := context.New(config.New(), zerolog.Nop()) + appContext := context.New(config.New(false), zerolog.Nop()) additionalChains := []chains.Chain{ sample.Chain(1), @@ -536,7 +536,7 @@ func makeAppContext( headerSupportedChains []lightclienttypes.HeaderSupportedChain, ) *context.AppContext { // create config - cfg := config.NewConfig() + cfg := config.New(false) logger := zerolog.Nop() cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ Chain: evmChain, diff --git a/zetaclient/context/context.go b/zetaclient/context/context.go index 3f8b6177fa..4d0b06866a 100644 --- a/zetaclient/context/context.go +++ b/zetaclient/context/context.go @@ -24,3 +24,15 @@ func FromContext(ctx goctx.Context) (*AppContext, error) { return app, nil } + +// Copy copies AppContext from one context to another (is present). +// This is useful when you want to drop timeouts and deadlines from the context +// (e.g. run something in another goroutine). +func Copy(from, to goctx.Context) goctx.Context { + app, err := FromContext(from) + if err != nil { + return to + } + + return WithAppContext(to, app) +} diff --git a/zetaclient/context/context_test.go b/zetaclient/context/context_test.go index 5bde4596d6..be9dab83a4 100644 --- a/zetaclient/context/context_test.go +++ b/zetaclient/context/context_test.go @@ -24,7 +24,7 @@ func TestFromContext(t *testing.T) { // ARRANGE #2 // Given basic app - app := context.New(config.NewConfig(), zerolog.Nop()) + app := context.New(config.New(false), zerolog.Nop()) // That is included in the ctx ctx = context.WithAppContext(ctx, app) @@ -38,3 +38,20 @@ func TestFromContext(t *testing.T) { assert.Equal(t, app, app2) assert.NotEmpty(t, app.Config()) } + +func TestCopy(t *testing.T) { + // ARRANGE + var ( + app = context.New(config.New(false), zerolog.Nop()) + ctx1 = context.WithAppContext(goctx.Background(), app) + ) + + // ACT + ctx2 := context.Copy(ctx1, goctx.Background()) + + // ASSERT + app2, err := context.FromContext(ctx2) + assert.NoError(t, err) + assert.NotNil(t, app2) + assert.Equal(t, app, app2) +} diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index b233c9d582..f4b44f1cb3 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -2,6 +2,7 @@ package orchestrator import ( + "context" "fmt" "math" "time" @@ -10,13 +11,14 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/pkg/bg" "github.com/zeta-chain/zetacore/pkg/chains" zetamath "github.com/zeta-chain/zetacore/pkg/math" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" "github.com/zeta-chain/zetacore/zetaclient/ratelimiter" @@ -65,6 +67,7 @@ type Orchestrator struct { // NewOrchestrator creates a new orchestrator func NewOrchestrator( + ctx context.Context, zetacoreClient interfaces.ZetacoreClient, signerMap map[int64]interfaces.ChainSigner, observerMap map[int64]interfaces.ChainObserver, @@ -90,7 +93,7 @@ func NewOrchestrator( // create outbound processor oc.outboundProc = outboundprocessor.NewProcessor(logger) - balance, err := zetacoreClient.GetZetaHotKeyBalance() + balance, err := zetacoreClient.GetZetaHotKeyBalance(ctx) if err != nil { oc.logger.Std.Error().Err(err).Msg("error getting last balance of the hot key") } @@ -100,7 +103,7 @@ func NewOrchestrator( } // MonitorCore starts the orchestrator for CCTXs -func (oc *Orchestrator) MonitorCore(appContext *context.AppContext) error { +func (oc *Orchestrator) MonitorCore(ctx context.Context) error { signerAddress, err := oc.zetacoreClient.GetKeys().GetAddress() if err != nil { return fmt.Errorf("failed to get signer address: %w", err) @@ -108,26 +111,24 @@ func (oc *Orchestrator) MonitorCore(appContext *context.AppContext) error { oc.logger.Std.Info().Msgf("Starting orchestrator for signer: %s", signerAddress) // start cctx scheduler - go oc.StartCctxScheduler(appContext) - - // watch for upgrade plan from zetacore - go func() { - // wait for upgrade plan signal to arrive - oc.zetacoreClient.Pause() + bg.Work(ctx, oc.StartCctxScheduler, bg.WithName("StartCctxScheduler"), bg.WithLogger(oc.logger.Std)) + shutdownOrchestrator := func() { // now stop orchestrator and all observers close(oc.stop) for _, c := range oc.observerMap { c.Stop() } - }() + } + + oc.zetacoreClient.OnBeforeStop(shutdownOrchestrator) return nil } // GetUpdatedSigner returns signer with updated chain parameters func (oc *Orchestrator) GetUpdatedSigner( - appContext *context.AppContext, + appContext *zctx.AppContext, chainID int64, ) (interfaces.ChainSigner, error) { signer, found := oc.signerMap[chainID] @@ -158,7 +159,7 @@ func (oc *Orchestrator) GetUpdatedSigner( // GetUpdatedChainObserver returns chain observer with updated chain parameters func (oc *Orchestrator) GetUpdatedChainObserver( - appContext *context.AppContext, + appContext *zctx.AppContext, chainID int64, ) (interfaces.ChainObserver, error) { observer, found := oc.observerMap[chainID] @@ -186,12 +187,13 @@ func (oc *Orchestrator) GetUpdatedChainObserver( return observer, nil } -// GetPendingCctxsWithinRatelimit get pending cctxs across foreign chains within rate limit -func (oc *Orchestrator) GetPendingCctxsWithinRatelimit( +// GetPendingCctxsWithinRateLimit get pending cctxs across foreign chains within rate limit +func (oc *Orchestrator) GetPendingCctxsWithinRateLimit( + ctx context.Context, foreignChains []chains.Chain, ) (map[int64][]*types.CrossChainTx, error) { // get rate limiter flags - rateLimitFlags, err := oc.zetacoreClient.GetRateLimiterFlags() + rateLimitFlags, err := oc.zetacoreClient.GetRateLimiterFlags(ctx) if err != nil { return nil, err } @@ -203,7 +205,7 @@ func (oc *Orchestrator) GetPendingCctxsWithinRatelimit( cctxsMap := make(map[int64][]*types.CrossChainTx) if !rateLimiterUsable { for _, chain := range foreignChains { - resp, _, err := oc.zetacoreClient.ListPendingCctx(chain.ChainId) + resp, _, err := oc.zetacoreClient.ListPendingCCTX(ctx, chain.ChainId) if err == nil && resp != nil { cctxsMap[chain.ChainId] = resp } @@ -212,11 +214,11 @@ func (oc *Orchestrator) GetPendingCctxsWithinRatelimit( } // query rate limiter input - resp, err := oc.zetacoreClient.GetRateLimiterInput(rateLimitFlags.Window) + resp, err := oc.zetacoreClient.GetRateLimiterInput(ctx, rateLimitFlags.Window) if err != nil { return nil, err } - input, ok := ratelimiter.NewInput(resp) + input, ok := ratelimiter.NewInput(*resp) if !ok { return nil, fmt.Errorf("failed to create rate limiter input") } @@ -238,17 +240,22 @@ func (oc *Orchestrator) GetPendingCctxsWithinRatelimit( // StartCctxScheduler schedules keysigns for cctxs on each ZetaChain block (the ticker) // TODO(revamp): make this function simpler -func (oc *Orchestrator) StartCctxScheduler(appContext *context.AppContext) { +func (oc *Orchestrator) StartCctxScheduler(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + observeTicker := time.NewTicker(3 * time.Second) var lastBlockNum int64 for { select { case <-oc.stop: oc.logger.Std.Warn().Msg("StartCctxScheduler: stopped") - return + return nil case <-observeTicker.C: { - bn, err := oc.zetacoreClient.GetBlockHeight() + bn, err := oc.zetacoreClient.GetBlockHeight(ctx) if err != nil { oc.logger.Std.Error().Err(err).Msg("StartCctxScheduler: GetBlockHeight fail") continue @@ -266,7 +273,7 @@ func (oc *Orchestrator) StartCctxScheduler(appContext *context.AppContext) { oc.logger.Std.Debug().Msgf("StartCctxScheduler: zetacore heart beat: %d", bn) } - balance, err := oc.zetacoreClient.GetZetaHotKeyBalance() + balance, err := oc.zetacoreClient.GetZetaHotKeyBalance(ctx) if err != nil { oc.logger.Std.Error().Err(err).Msgf("couldn't get operator balance") } else { @@ -281,10 +288,10 @@ func (oc *Orchestrator) StartCctxScheduler(appContext *context.AppContext) { metrics.HotKeyBurnRate.Set(float64(oc.ts.HotKeyBurnRate.GetBurnRate().Int64())) // get supported external chains - externalChains := appContext.GetEnabledExternalChains() + externalChains := app.GetEnabledExternalChains() // query pending cctxs across all external chains within rate limit - cctxMap, err := oc.GetPendingCctxsWithinRatelimit(externalChains) + cctxMap, err := oc.GetPendingCctxsWithinRateLimit(ctx, externalChains) if err != nil { oc.logger.Std.Error().Err(err).Msgf("StartCctxScheduler: GetPendingCctxsWithinRatelimit failed") } @@ -299,30 +306,30 @@ func (oc *Orchestrator) StartCctxScheduler(appContext *context.AppContext) { } // update chain parameters for signer and chain observer - signer, err := oc.GetUpdatedSigner(appContext, c.ChainId) + signer, err := oc.GetUpdatedSigner(app, c.ChainId) if err != nil { oc.logger.Std.Error(). Err(err). Msgf("StartCctxScheduler: GetUpdatedSigner failed for chain %d", c.ChainId) continue } - ob, err := oc.GetUpdatedChainObserver(appContext, c.ChainId) + ob, err := oc.GetUpdatedChainObserver(app, c.ChainId) if err != nil { oc.logger.Std.Error(). Err(err). Msgf("StartCctxScheduler: GetUpdatedChainObserver failed for chain %d", c.ChainId) continue } - if !appContext.IsOutboundObservationEnabled(ob.GetChainParams()) { + if !app.IsOutboundObservationEnabled(ob.GetChainParams()) { continue } // #nosec G701 range is verified zetaHeight := uint64(bn) - if chains.IsEVMChain(c.ChainId, appContext.GetAdditionalChains()) { - oc.ScheduleCctxEVM(zetaHeight, c.ChainId, cctxList, ob, signer) - } else if chains.IsBitcoinChain(c.ChainId, appContext.GetAdditionalChains()) { - oc.ScheduleCctxBTC(zetaHeight, c.ChainId, cctxList, ob, signer) + if chains.IsEVMChain(c.ChainId, app.GetAdditionalChains()) { + oc.ScheduleCctxEVM(ctx, zetaHeight, c.ChainId, cctxList, ob, signer) + } else if chains.IsBitcoinChain(c.ChainId, app.GetAdditionalChains()) { + oc.ScheduleCctxBTC(ctx, zetaHeight, c.ChainId, cctxList, ob, signer) } else { oc.logger.Std.Error().Msgf("StartCctxScheduler: unsupported chain %d", c.ChainId) continue @@ -340,13 +347,14 @@ func (oc *Orchestrator) StartCctxScheduler(appContext *context.AppContext) { // ScheduleCctxEVM schedules evm outbound keysign on each ZetaChain block (the ticker) func (oc *Orchestrator) ScheduleCctxEVM( + ctx context.Context, zetaHeight uint64, chainID int64, cctxList []*types.CrossChainTx, observer interfaces.ChainObserver, signer interfaces.ChainSigner, ) { - res, err := oc.zetacoreClient.GetAllOutboundTrackerByChain(chainID, interfaces.Ascending) + res, err := oc.zetacoreClient.GetAllOutboundTrackerByChain(ctx, chainID, interfaces.Ascending) if err != nil { oc.logger.Std.Warn().Err(err).Msgf("ScheduleCctxEVM: GetAllOutboundTrackerByChain failed for chain %d", chainID) return @@ -378,7 +386,7 @@ func (oc *Orchestrator) ScheduleCctxEVM( } // try confirming the outbound - included, _, err := observer.IsOutboundProcessed(cctx, oc.logger.Std) + included, _, err := observer.IsOutboundProcessed(ctx, cctx, oc.logger.Std) if err != nil { oc.logger.Std.Error(). Err(err). @@ -418,7 +426,15 @@ func (oc *Orchestrator) ScheduleCctxEVM( oc.outboundProc.StartTryProcess(outboundID) oc.logger.Std.Debug(). Msgf("ScheduleCctxEVM: sign outbound %s with value %d\n", outboundID, cctx.GetCurrentOutboundParam().Amount) - go signer.TryProcessOutbound(cctx, oc.outboundProc, outboundID, observer, oc.zetacoreClient, zetaHeight) + go signer.TryProcessOutbound( + ctx, + cctx, + oc.outboundProc, + outboundID, + observer, + oc.zetacoreClient, + zetaHeight, + ) } // #nosec G701 always in range @@ -433,6 +449,7 @@ func (oc *Orchestrator) ScheduleCctxEVM( // 2. schedule keysign only when nonce-mark UTXO is available // 3. stop keysign when lookahead is reached func (oc *Orchestrator) ScheduleCctxBTC( + ctx context.Context, zetaHeight uint64, chainID int64, cctxList []*types.CrossChainTx, @@ -460,7 +477,7 @@ func (oc *Orchestrator) ScheduleCctxBTC( continue } // try confirming the outbound - included, confirmed, err := btcObserver.IsOutboundProcessed(cctx, oc.logger.Std) + included, confirmed, err := btcObserver.IsOutboundProcessed(ctx, cctx, oc.logger.Std) if err != nil { oc.logger.Std.Error(). Err(err). @@ -489,7 +506,15 @@ func (oc *Orchestrator) ScheduleCctxBTC( if nonce%interval == zetaHeight%interval && !oc.outboundProc.IsOutboundActive(outboundID) { oc.outboundProc.StartTryProcess(outboundID) oc.logger.Std.Debug().Msgf("ScheduleCctxBTC: sign outbound %s with value %d\n", outboundID, params.Amount) - go signer.TryProcessOutbound(cctx, oc.outboundProc, outboundID, observer, oc.zetacoreClient, zetaHeight) + go signer.TryProcessOutbound( + ctx, + cctx, + oc.outboundProc, + outboundID, + observer, + oc.zetacoreClient, + zetaHeight, + ) } } } diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 9eab334bfe..286b0968a5 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -1,12 +1,15 @@ package orchestrator import ( + "context" "testing" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" @@ -16,7 +19,6 @@ import ( observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -56,9 +58,9 @@ func MockOrchestrator( func CreateAppContext( evmChain, btcChain chains.Chain, evmChainParams, btcChainParams *observertypes.ChainParams, -) *context.AppContext { +) *zctx.AppContext { // new config - cfg := config.NewConfig() + cfg := config.New(false) cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ Chain: evmChain, } @@ -66,7 +68,7 @@ func CreateAppContext( RPCHost: "localhost", } // new zetacore context - appContext := context.New(cfg, zerolog.Nop()) + appContext := zctx.New(cfg, zerolog.Nop()) evmChainParamsMap := make(map[int64]*observertypes.ChainParams) evmChainParamsMap[evmChain.ChainId] = evmChainParams ccFlags := sample.CrosschainFlags() @@ -204,7 +206,9 @@ func Test_GetUpdatedChainObserver(t *testing.T) { }) } -func Test_GetPendingCctxsWithinRatelimit(t *testing.T) { +func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { + ctx := context.Background() + // define test foreign chains ethChain := chains.Ethereum btcChain := chains.BitcoinMainnet @@ -353,25 +357,25 @@ func Test_GetPendingCctxsWithinRatelimit(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // create mock zetacore client - client := mocks.NewMockZetacoreClient() + client := mocks.NewZetacoreClient(t) // load mock data client.WithRateLimiterFlags(tt.rateLimiterFlags) + client.WithRateLimiterInput(tt.response) client.WithPendingCctx(ethChain.ChainId, tt.ethCctxsFallback) client.WithPendingCctx(btcChain.ChainId, tt.btcCctxsFallback) - client.WithRateLimiterInput(tt.response) // create orchestrator orchestrator := MockOrchestrator(t, client, ethChain, btcChain, ethChainParams, btcChainParams) // run the test - cctxsMap, err := orchestrator.GetPendingCctxsWithinRatelimit(foreignChains) + cctxsMap, err := orchestrator.GetPendingCctxsWithinRateLimit(ctx, foreignChains) if tt.fail { - require.Error(t, err) - require.Nil(t, cctxsMap) + assert.Error(t, err) + assert.Empty(t, cctxsMap) } else { - require.NoError(t, err) - require.Equal(t, tt.expectedCctxsMap, cctxsMap) + assert.NoError(t, err) + assert.Equal(t, tt.expectedCctxsMap, cctxsMap) } }) } diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go index 3355d4ff5e..53a61c707b 100644 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ b/zetaclient/supplychecker/zeta_supply_checker.go @@ -3,6 +3,7 @@ package supplychecker import ( + "context" "fmt" sdkmath "cosmossdk.io/math" @@ -16,14 +17,13 @@ import ( "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) // ZetaSupplyChecker is a utility to check the total supply of Zeta tokens type ZetaSupplyChecker struct { - appContext *context.AppContext evmClient map[int64]*ethclient.Client zetaClient *zetacore.Client ticker *clienttypes.DynamicTicker @@ -36,39 +36,44 @@ type ZetaSupplyChecker struct { // NewZetaSupplyChecker creates a new ZetaSupplyChecker func NewZetaSupplyChecker( - appContext *context.AppContext, + ctx context.Context, zetaClient *zetacore.Client, logger zerolog.Logger, -) (ZetaSupplyChecker, error) { +) (*ZetaSupplyChecker, error) { dynamicTicker, err := clienttypes.NewDynamicTicker("ZETASupplyTicker", 15) if err != nil { - return ZetaSupplyChecker{}, err + return nil, err } - zetaSupplyChecker := ZetaSupplyChecker{ + app, err := zctx.FromContext(ctx) + if err != nil { + return nil, err + } + + zetaSupplyChecker := &ZetaSupplyChecker{ stop: make(chan struct{}), ticker: dynamicTicker, evmClient: make(map[int64]*ethclient.Client), logger: logger.With(). Str("module", "ZetaSupplyChecker"). Logger(), - appContext: appContext, zetaClient: zetaClient, } - for _, evmConfig := range appContext.Config().GetAllEVMConfigs() { + for _, evmConfig := range app.Config().GetAllEVMConfigs() { if evmConfig.Chain.IsZetaChain() { continue } client, err := ethclient.Dial(evmConfig.Endpoint) if err != nil { - return zetaSupplyChecker, err + return nil, err } + zetaSupplyChecker.evmClient[evmConfig.Chain.ChainId] = client } for chainID := range zetaSupplyChecker.evmClient { - chain, found := chains.GetChainFromChainID(chainID, appContext.GetAdditionalChains()) + chain, found := chains.GetChainFromChainID(chainID, app.GetAdditionalChains()) if !found { return zetaSupplyChecker, fmt.Errorf("chain not found for chain id %d", chainID) } @@ -80,15 +85,16 @@ func NewZetaSupplyChecker( } } - balances, err := zetaSupplyChecker.zetaClient.GetGenesisSupply() + balances, err := zetaSupplyChecker.zetaClient.GetGenesisSupply(ctx) if err != nil { - return zetaSupplyChecker, err + return nil, err } tokensMintedAtBeginBlock, ok := sdkmath.NewIntFromString("200000000000000000") if !ok { - return zetaSupplyChecker, fmt.Errorf("error parsing tokens minted at begin block") + return nil, fmt.Errorf("error parsing tokens minted at begin block") } + zetaSupplyChecker.genesisSupply = balances.Add(tokensMintedAtBeginBlock) logger.Info(). @@ -98,12 +104,12 @@ func NewZetaSupplyChecker( } // Start starts the ZetaSupplyChecker -func (zs *ZetaSupplyChecker) Start() { +func (zs *ZetaSupplyChecker) Start(ctx context.Context) { defer zs.ticker.Stop() for { select { case <-zs.ticker.C(): - err := zs.CheckZetaTokenSupply() + err := zs.CheckZetaTokenSupply(ctx) if err != nil { zs.logger.Error().Err(err).Msgf("ZetaSupplyChecker error") } @@ -120,10 +126,15 @@ func (zs *ZetaSupplyChecker) Stop() { } // CheckZetaTokenSupply checks the total supply of Zeta tokens -func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { +func (zs *ZetaSupplyChecker) CheckZetaTokenSupply(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + externalChainTotalSupply := sdkmath.ZeroInt() for _, chain := range zs.externalEvmChain { - externalEvmChainParams, ok := zs.appContext.GetEVMChainParams(chain.ChainId) + externalEvmChainParams, ok := app.GetEVMChainParams(chain.ChainId) if !ok { return fmt.Errorf("externalEvmChainParams not found for chain id %d", chain.ChainId) } @@ -149,7 +160,7 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { externalChainTotalSupply = externalChainTotalSupply.Add(totalSupplyInt) } - evmChainParams, ok := zs.appContext.GetEVMChainParams(zs.ethereumChain.ChainId) + evmChainParams, ok := app.GetEVMChainParams(zs.ethereumChain.ChainId) if !ok { return fmt.Errorf("eth config not found for chain id %d", zs.ethereumChain.ChainId) } @@ -174,16 +185,16 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { return fmt.Errorf("error parsing eth locked amount") } - zetaInTransit, err := zs.GetAmountOfZetaInTransit() + zetaInTransit, err := zs.GetAmountOfZetaInTransit(ctx) if err != nil { return err } - zetaTokenSupplyOnNode, err := zs.zetaClient.GetZetaTokenSupplyOnNode() + zetaTokenSupplyOnNode, err := zs.zetaClient.GetZetaTokenSupplyOnNode(ctx) if err != nil { return err } - abortedAmount, err := zs.AbortedTxAmount() + abortedAmount, err := zs.AbortedTxAmount(ctx) if err != nil { return err } @@ -202,8 +213,8 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { } // AbortedTxAmount returns the amount of Zeta tokens in aborted transactions -func (zs *ZetaSupplyChecker) AbortedTxAmount() (sdkmath.Int, error) { - amount, err := zs.zetaClient.GetAbortedZetaAmount() +func (zs *ZetaSupplyChecker) AbortedTxAmount(ctx context.Context) (sdkmath.Int, error) { + amount, err := zs.zetaClient.GetAbortedZetaAmount(ctx) if err != nil { return sdkmath.ZeroInt(), errors.Wrap(err, "error getting aborted zeta amount") } @@ -215,10 +226,10 @@ func (zs *ZetaSupplyChecker) AbortedTxAmount() (sdkmath.Int, error) { } // GetAmountOfZetaInTransit returns the amount of Zeta tokens in transit -func (zs *ZetaSupplyChecker) GetAmountOfZetaInTransit() (sdkmath.Int, error) { +func (zs *ZetaSupplyChecker) GetAmountOfZetaInTransit(ctx context.Context) (sdkmath.Int, error) { chainsToCheck := make([]chains.Chain, len(zs.externalEvmChain)+1) chainsToCheck = append(append(chainsToCheck, zs.externalEvmChain...), zs.ethereumChain) - cctxs := zs.GetPendingCCTXInTransit(chainsToCheck) + cctxs := zs.GetPendingCCTXInTransit(ctx, chainsToCheck) amount := sdkmath.ZeroUint() for _, cctx := range cctxs { @@ -233,10 +244,13 @@ func (zs *ZetaSupplyChecker) GetAmountOfZetaInTransit() (sdkmath.Int, error) { } // GetPendingCCTXInTransit returns the pending CCTX in transit -func (zs *ZetaSupplyChecker) GetPendingCCTXInTransit(receivingChains []chains.Chain) []*types.CrossChainTx { +func (zs *ZetaSupplyChecker) GetPendingCCTXInTransit( + ctx context.Context, + receivingChains []chains.Chain, +) []*types.CrossChainTx { cctxInTransit := make([]*types.CrossChainTx, 0) for _, chain := range receivingChains { - cctx, _, err := zs.zetaClient.ListPendingCctx(chain.ChainId) + cctx, _, err := zs.zetaClient.ListPendingCCTX(ctx, chain.ChainId) if err != nil { continue } @@ -247,7 +261,7 @@ func (zs *ZetaSupplyChecker) GetPendingCCTXInTransit(receivingChains []chains.Ch } } - trackers, err := zs.zetaClient.GetAllOutboundTrackerByChain(chain.ChainId, interfaces.Ascending) + trackers, err := zs.zetaClient.GetAllOutboundTrackerByChain(ctx, chain.ChainId, interfaces.Ascending) if err != nil { continue } diff --git a/zetaclient/testutils/mocks/chain_clients.go b/zetaclient/testutils/mocks/chain_clients.go index 44f1a9ea71..6f004420e5 100644 --- a/zetaclient/testutils/mocks/chain_clients.go +++ b/zetaclient/testutils/mocks/chain_clients.go @@ -1,6 +1,8 @@ package mocks import ( + "context" + "github.com/rs/zerolog" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" @@ -24,13 +26,14 @@ func NewEVMObserver(chainParams *observertypes.ChainParams) *EVMObserver { } } -func (ob *EVMObserver) Start() { -} - -func (ob *EVMObserver) Stop() { -} +func (ob *EVMObserver) Start(_ context.Context) {} +func (ob *EVMObserver) Stop() {} -func (ob *EVMObserver) IsOutboundProcessed(_ *crosschaintypes.CrossChainTx, _ zerolog.Logger) (bool, bool, error) { +func (ob *EVMObserver) IsOutboundProcessed( + _ context.Context, + _ *crosschaintypes.CrossChainTx, + _ zerolog.Logger, +) (bool, bool, error) { return false, false, nil } @@ -46,7 +49,8 @@ func (ob *EVMObserver) GetTxID(_ uint64) string { return "" } -func (ob *EVMObserver) WatchInboundTracker() { +func (ob *EVMObserver) WatchInboundTracker(_ context.Context) error { + return nil } // ---------------------------------------------------------------------------- @@ -65,13 +69,15 @@ func NewBTCObserver(chainParams *observertypes.ChainParams) *BTCObserver { } } -func (ob *BTCObserver) Start() { -} +func (ob *BTCObserver) Start(_ context.Context) {} -func (ob *BTCObserver) Stop() { -} +func (ob *BTCObserver) Stop() {} -func (ob *BTCObserver) IsOutboundProcessed(_ *crosschaintypes.CrossChainTx, _ zerolog.Logger) (bool, bool, error) { +func (ob *BTCObserver) IsOutboundProcessed( + _ context.Context, + _ *crosschaintypes.CrossChainTx, + _ zerolog.Logger, +) (bool, bool, error) { return false, false, nil } @@ -87,5 +93,4 @@ func (ob *BTCObserver) GetTxID(_ uint64) string { return "" } -func (ob *BTCObserver) WatchInboundTracker() { -} +func (ob *BTCObserver) WatchInboundTracker(_ context.Context) error { return nil } diff --git a/zetaclient/testutils/mocks/chain_signer.go b/zetaclient/testutils/mocks/chain_signer.go index 73135b387f..3785c34ee0 100644 --- a/zetaclient/testutils/mocks/chain_signer.go +++ b/zetaclient/testutils/mocks/chain_signer.go @@ -1,6 +1,8 @@ package mocks import ( + "context" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/zeta-chain/zetacore/pkg/chains" @@ -34,6 +36,7 @@ func NewEVMSigner( } func (s *EVMSigner) TryProcessOutbound( + _ context.Context, _ *crosschaintypes.CrossChainTx, _ *outboundprocessor.Processor, _ string, @@ -73,6 +76,7 @@ func NewBTCSigner() *BTCSigner { } func (s *BTCSigner) TryProcessOutbound( + _ context.Context, _ *crosschaintypes.CrossChainTx, _ *outboundprocessor.Processor, _ string, diff --git a/zetaclient/testutils/mocks/cometbft_client.go b/zetaclient/testutils/mocks/cometbft_client.go index 1058e7980e..dcb452621d 100644 --- a/zetaclient/testutils/mocks/cometbft_client.go +++ b/zetaclient/testutils/mocks/cometbft_client.go @@ -2,29 +2,38 @@ package mocks import ( "context" + "encoding/hex" + "testing" abci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/libs/bytes" "github.com/cometbft/cometbft/rpc/client/mock" coretypes "github.com/cometbft/cometbft/rpc/core/types" tmtypes "github.com/cometbft/cometbft/types" + "github.com/stretchr/testify/require" ) type CometBFTClient struct { mock.Client - err error - code uint32 + + t *testing.T + err error + code uint32 + txHash bytes.HexBytes } -func (c CometBFTClient) BroadcastTxCommit(_ context.Context, _ tmtypes.Tx) (*coretypes.ResultBroadcastTxCommit, error) { +func (c *CometBFTClient) BroadcastTxCommit( + _ context.Context, + _ tmtypes.Tx, +) (*coretypes.ResultBroadcastTxCommit, error) { return nil, c.err } -func (c CometBFTClient) BroadcastTxAsync(_ context.Context, _ tmtypes.Tx) (*coretypes.ResultBroadcastTx, error) { +func (c *CometBFTClient) BroadcastTxAsync(_ context.Context, _ tmtypes.Tx) (*coretypes.ResultBroadcastTx, error) { return nil, c.err } -func (c CometBFTClient) BroadcastTxSync(_ context.Context, _ tmtypes.Tx) (*coretypes.ResultBroadcastTx, error) { +func (c *CometBFTClient) BroadcastTxSync(_ context.Context, _ tmtypes.Tx) (*coretypes.ResultBroadcastTx, error) { log := "" if c.err != nil { log = c.err.Error() @@ -34,11 +43,11 @@ func (c CometBFTClient) BroadcastTxSync(_ context.Context, _ tmtypes.Tx) (*coret Data: bytes.HexBytes{}, Log: log, Codespace: "", - Hash: bytes.HexBytes{}, + Hash: c.txHash, }, c.err } -func (c CometBFTClient) Tx(_ context.Context, _ []byte, _ bool) (*coretypes.ResultTx, error) { +func (c *CometBFTClient) Tx(_ context.Context, _ []byte, _ bool) (*coretypes.ResultTx, error) { return &coretypes.ResultTx{ Hash: bytes.HexBytes{}, Height: 0, @@ -51,7 +60,7 @@ func (c CometBFTClient) Tx(_ context.Context, _ []byte, _ bool) (*coretypes.Resu }, c.err } -func (c CometBFTClient) Block(_ context.Context, _ *int64) (*coretypes.ResultBlock, error) { +func (c *CometBFTClient) Block(_ context.Context, _ *int64) (*coretypes.ResultBlock, error) { return &coretypes.ResultBlock{Block: &tmtypes.Block{ Header: tmtypes.Header{}, Data: tmtypes.Data{}, @@ -59,8 +68,23 @@ func (c CometBFTClient) Block(_ context.Context, _ *int64) (*coretypes.ResultBlo }}, c.err } -func NewSDKClientWithErr(err error, code uint32) *CometBFTClient { +func (c *CometBFTClient) SetBroadcastTxHash(hash string) *CometBFTClient { + b, err := hex.DecodeString(hash) + require.NoError(c.t, err) + + c.txHash = b + + return c +} + +func (c *CometBFTClient) SetError(err error) *CometBFTClient { + c.err = err + return c +} + +func NewSDKClientWithErr(t *testing.T, err error, code uint32) *CometBFTClient { return &CometBFTClient{ + t: t, Client: mock.Client{}, err: err, code: code, diff --git a/zetaclient/testutils/mocks/tss_signer.go b/zetaclient/testutils/mocks/tss_signer.go index ea439e23b6..a7a9690293 100644 --- a/zetaclient/testutils/mocks/tss_signer.go +++ b/zetaclient/testutils/mocks/tss_signer.go @@ -1,6 +1,7 @@ package mocks import ( + "context" "crypto/ecdsa" "fmt" @@ -67,7 +68,7 @@ func (s *TSS) WithPrivKey(privKey *ecdsa.PrivateKey) *TSS { } // Sign uses test key unrelated to any tss key in production -func (s *TSS) Sign(data []byte, _ uint64, _ uint64, _ int64, _ string) ([65]byte, error) { +func (s *TSS) Sign(_ context.Context, data []byte, _ uint64, _ uint64, _ int64, _ string) ([65]byte, error) { // return error if tss is paused if s.paused { return [65]byte{}, fmt.Errorf("tss is paused") @@ -84,7 +85,7 @@ func (s *TSS) Sign(data []byte, _ uint64, _ uint64, _ int64, _ string) ([65]byte } // SignBatch uses test key unrelated to any tss key in production -func (s *TSS) SignBatch(_ [][]byte, _ uint64, _ uint64, _ int64) ([][65]byte, error) { +func (s *TSS) SignBatch(_ context.Context, _ [][]byte, _ uint64, _ uint64, _ int64) ([][65]byte, error) { // return error if tss is paused if s.paused { return nil, fmt.Errorf("tss is paused") diff --git a/zetaclient/testutils/mocks/zetacore_client.go b/zetaclient/testutils/mocks/zetacore_client.go index 2f413a8764..0dc6b8996d 100644 --- a/zetaclient/testutils/mocks/zetacore_client.go +++ b/zetaclient/testutils/mocks/zetacore_client.go @@ -1,280 +1,772 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + package mocks import ( - "errors" - "math/big" - - "cosmossdk.io/math" - "github.com/rs/zerolog" - "github.com/zeta-chain/go-tss/blame" - - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/coin" - "github.com/zeta-chain/zetacore/pkg/proofs" - "github.com/zeta-chain/zetacore/testutil/sample" - crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" + blame "github.com/zeta-chain/go-tss/blame" + chains "github.com/zeta-chain/zetacore/pkg/chains" + + context "context" + + interfaces "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + + keysinterfaces "github.com/zeta-chain/zetacore/zetaclient/keys/interfaces" + lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" - observerTypes "github.com/zeta-chain/zetacore/x/observer/types" - chaininterfaces "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - keyinterfaces "github.com/zeta-chain/zetacore/zetaclient/keys/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/testutils" -) -const ErrMsgPaused = "zetacore client is paused" -const ErrMsgRPCFailed = "rpc failed" + math "cosmossdk.io/math" -var _ chaininterfaces.ZetacoreClient = &MockZetacoreClient{} + mock "github.com/stretchr/testify/mock" -type MockZetacoreClient struct { - paused bool - zetaChain chains.Chain + observertypes "github.com/zeta-chain/zetacore/x/observer/types" - // the mock observer keys - keys keyinterfaces.ObserverKeys + proofs "github.com/zeta-chain/zetacore/pkg/proofs" - // the mock data for testing - // pending cctxs - pendingCctxs map[int64][]*crosschaintypes.CrossChainTx + types "github.com/zeta-chain/zetacore/x/crosschain/types" - // rate limiter flags - rateLimiterFlags *crosschaintypes.RateLimiterFlags + zerolog "github.com/rs/zerolog" +) - // rate limiter input - input *crosschaintypes.QueryRateLimiterInputResponse +// ZetacoreClient is an autogenerated mock type for the ZetacoreClient type +type ZetacoreClient struct { + mock.Mock } -func NewMockZetacoreClient() *MockZetacoreClient { - return &MockZetacoreClient{ - paused: false, - zetaChain: chains.ZetaChainMainnet, - pendingCctxs: map[int64][]*crosschaintypes.CrossChainTx{}, +// AddOutboundTracker provides a mock function with given fields: ctx, chainID, nonce, txHash, proof, blockHash, txIndex +func (_m *ZetacoreClient) AddOutboundTracker(ctx context.Context, chainID int64, nonce uint64, txHash string, proof *proofs.Proof, blockHash string, txIndex int64) (string, error) { + ret := _m.Called(ctx, chainID, nonce, txHash, proof, blockHash, txIndex) + + if len(ret) == 0 { + panic("no return value specified for AddOutboundTracker") } -} -func (m *MockZetacoreClient) PostVoteInbound(_, _ uint64, _ *crosschaintypes.MsgVoteInbound) (string, string, error) { - if m.paused { - return "", "", errors.New(ErrMsgPaused) + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, uint64, string, *proofs.Proof, string, int64) (string, error)); ok { + return rf(ctx, chainID, nonce, txHash, proof, blockHash, txIndex) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, uint64, string, *proofs.Proof, string, int64) string); ok { + r0 = rf(ctx, chainID, nonce, txHash, proof, blockHash, txIndex) + } else { + r0 = ret.Get(0).(string) } - return "", "", nil -} -func (m *MockZetacoreClient) PostVoteOutbound( - _ string, - _ string, - _ uint64, - _ uint64, - _ *big.Int, - _ uint64, - _ *big.Int, - _ chains.ReceiveStatus, - _ chains.Chain, - _ uint64, - _ coin.CoinType, -) (string, string, error) { - if m.paused { - return "", "", errors.New(ErrMsgPaused) - } - return sample.Hash().Hex(), "", nil + if rf, ok := ret.Get(1).(func(context.Context, int64, uint64, string, *proofs.Proof, string, int64) error); ok { + r1 = rf(ctx, chainID, nonce, txHash, proof, blockHash, txIndex) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) PostGasPrice(_ chains.Chain, _ uint64, _ string, _ uint64) (string, error) { - if m.paused { - return "", errors.New(ErrMsgPaused) +// Chain provides a mock function with given fields: +func (_m *ZetacoreClient) Chain() chains.Chain { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Chain") } - return "", nil -} -func (m *MockZetacoreClient) PostVoteBlockHeader(_ int64, _ []byte, _ int64, _ proofs.HeaderData) (string, error) { - if m.paused { - return "", errors.New(ErrMsgPaused) + var r0 chains.Chain + if rf, ok := ret.Get(0).(func() chains.Chain); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(chains.Chain) } - return "", nil + + return r0 } -func (m *MockZetacoreClient) GetBlockHeaderChainState(_ int64) (lightclienttypes.QueryGetChainStateResponse, error) { - if m.paused { - return lightclienttypes.QueryGetChainStateResponse{}, errors.New(ErrMsgPaused) +// GetAllOutboundTrackerByChain provides a mock function with given fields: ctx, chainID, order +func (_m *ZetacoreClient) GetAllOutboundTrackerByChain(ctx context.Context, chainID int64, order interfaces.Order) ([]types.OutboundTracker, error) { + ret := _m.Called(ctx, chainID, order) + + if len(ret) == 0 { + panic("no return value specified for GetAllOutboundTrackerByChain") } - return lightclienttypes.QueryGetChainStateResponse{}, nil -} -func (m *MockZetacoreClient) PostBlameData(_ *blame.Blame, _ int64, _ string) (string, error) { - if m.paused { - return "", errors.New(ErrMsgPaused) + var r0 []types.OutboundTracker + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, interfaces.Order) ([]types.OutboundTracker, error)); ok { + return rf(ctx, chainID, order) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, interfaces.Order) []types.OutboundTracker); ok { + r0 = rf(ctx, chainID, order) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.OutboundTracker) + } } - return "", nil -} -func (m *MockZetacoreClient) AddOutboundTracker( - _ int64, - _ uint64, - _ string, - _ *proofs.Proof, - _ string, - _ int64, -) (string, error) { - if m.paused { - return "", errors.New(ErrMsgPaused) - } - return "", nil + if rf, ok := ret.Get(1).(func(context.Context, int64, interfaces.Order) error); ok { + r1 = rf(ctx, chainID, order) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) Chain() chains.Chain { - return m.zetaChain +// GetBTCTSSAddress provides a mock function with given fields: ctx, chainID +func (_m *ZetacoreClient) GetBTCTSSAddress(ctx context.Context, chainID int64) (string, error) { + ret := _m.Called(ctx, chainID) + + if len(ret) == 0 { + panic("no return value specified for GetBTCTSSAddress") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (string, error)); ok { + return rf(ctx, chainID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) string); ok { + r0 = rf(ctx, chainID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, chainID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) GetLogger() *zerolog.Logger { - return nil +// GetBlockHeaderChainState provides a mock function with given fields: ctx, chainID +func (_m *ZetacoreClient) GetBlockHeaderChainState(ctx context.Context, chainID int64) (*lightclienttypes.ChainState, error) { + ret := _m.Called(ctx, chainID) + + if len(ret) == 0 { + panic("no return value specified for GetBlockHeaderChainState") + } + + var r0 *lightclienttypes.ChainState + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*lightclienttypes.ChainState, error)); ok { + return rf(ctx, chainID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *lightclienttypes.ChainState); ok { + r0 = rf(ctx, chainID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*lightclienttypes.ChainState) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, chainID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) GetKeys() keyinterfaces.ObserverKeys { - return m.keys +// GetBlockHeight provides a mock function with given fields: ctx +func (_m *ZetacoreClient) GetBlockHeight(ctx context.Context) (int64, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetBlockHeight") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (int64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) GetKeyGen() (*observerTypes.Keygen, error) { - if m.paused { - return nil, errors.New(ErrMsgPaused) +// GetCctxByNonce provides a mock function with given fields: ctx, chainID, nonce +func (_m *ZetacoreClient) GetCctxByNonce(ctx context.Context, chainID int64, nonce uint64) (*types.CrossChainTx, error) { + ret := _m.Called(ctx, chainID, nonce) + + if len(ret) == 0 { + panic("no return value specified for GetCctxByNonce") + } + + var r0 *types.CrossChainTx + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, uint64) (*types.CrossChainTx, error)); ok { + return rf(ctx, chainID, nonce) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, uint64) *types.CrossChainTx); ok { + r0 = rf(ctx, chainID, nonce) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.CrossChainTx) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, uint64) error); ok { + r1 = rf(ctx, chainID, nonce) + } else { + r1 = ret.Error(1) } - return &observerTypes.Keygen{}, nil + + return r0, r1 } -func (m *MockZetacoreClient) GetBlockHeight() (int64, error) { - if m.paused { - return 0, errors.New(ErrMsgPaused) +// GetCrosschainFlags provides a mock function with given fields: ctx +func (_m *ZetacoreClient) GetCrosschainFlags(ctx context.Context) (observertypes.CrosschainFlags, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetCrosschainFlags") + } + + var r0 observertypes.CrosschainFlags + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (observertypes.CrosschainFlags, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) observertypes.CrosschainFlags); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(observertypes.CrosschainFlags) } - return 0, nil + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) GetRateLimiterInput(_ int64) (crosschaintypes.QueryRateLimiterInputResponse, error) { - if m.paused { - return crosschaintypes.QueryRateLimiterInputResponse{}, errors.New(ErrMsgPaused) +// GetInboundTrackersForChain provides a mock function with given fields: ctx, chainID +func (_m *ZetacoreClient) GetInboundTrackersForChain(ctx context.Context, chainID int64) ([]types.InboundTracker, error) { + ret := _m.Called(ctx, chainID) + + if len(ret) == 0 { + panic("no return value specified for GetInboundTrackersForChain") } - if m.input == nil { - return crosschaintypes.QueryRateLimiterInputResponse{}, errors.New(ErrMsgRPCFailed) + + var r0 []types.InboundTracker + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) ([]types.InboundTracker, error)); ok { + return rf(ctx, chainID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) []types.InboundTracker); ok { + r0 = rf(ctx, chainID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.InboundTracker) + } } - return *m.input, nil + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, chainID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) ListPendingCctx(chainID int64) ([]*crosschaintypes.CrossChainTx, uint64, error) { - if m.paused { - return nil, 0, errors.New(ErrMsgPaused) +// GetKeyGen provides a mock function with given fields: ctx +func (_m *ZetacoreClient) GetKeyGen(ctx context.Context) (*observertypes.Keygen, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetKeyGen") + } + + var r0 *observertypes.Keygen + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*observertypes.Keygen, error)); ok { + return rf(ctx) } - return m.pendingCctxs[chainID], 0, nil + if rf, ok := ret.Get(0).(func(context.Context) *observertypes.Keygen); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*observertypes.Keygen) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) ListPendingCctxWithinRatelimit() ([]*crosschaintypes.CrossChainTx, uint64, int64, string, bool, error) { - if m.paused { - return nil, 0, 0, "", false, errors.New(ErrMsgPaused) +// GetKeys provides a mock function with given fields: +func (_m *ZetacoreClient) GetKeys() keysinterfaces.ObserverKeys { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetKeys") + } + + var r0 keysinterfaces.ObserverKeys + if rf, ok := ret.Get(0).(func() keysinterfaces.ObserverKeys); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(keysinterfaces.ObserverKeys) + } } - return []*crosschaintypes.CrossChainTx{}, 0, 0, "", false, nil + + return r0 } -func (m *MockZetacoreClient) GetPendingNoncesByChain(_ int64) (observerTypes.PendingNonces, error) { - if m.paused { - return observerTypes.PendingNonces{}, errors.New(ErrMsgPaused) +// GetLogger provides a mock function with given fields: +func (_m *ZetacoreClient) GetLogger() *zerolog.Logger { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetLogger") } - return observerTypes.PendingNonces{}, nil + + var r0 *zerolog.Logger + if rf, ok := ret.Get(0).(func() *zerolog.Logger); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*zerolog.Logger) + } + } + + return r0 } -func (m *MockZetacoreClient) GetCctxByNonce(_ int64, _ uint64) (*crosschaintypes.CrossChainTx, error) { - if m.paused { - return nil, errors.New(ErrMsgPaused) +// GetObserverList provides a mock function with given fields: ctx +func (_m *ZetacoreClient) GetObserverList(ctx context.Context) ([]string, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetObserverList") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []string); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) } - return &crosschaintypes.CrossChainTx{}, nil + + return r0, r1 } -func (m *MockZetacoreClient) GetOutboundTracker(_ chains.Chain, _ uint64) (*crosschaintypes.OutboundTracker, error) { - if m.paused { - return nil, errors.New(ErrMsgPaused) +// GetOutboundTracker provides a mock function with given fields: ctx, chain, nonce +func (_m *ZetacoreClient) GetOutboundTracker(ctx context.Context, chain chains.Chain, nonce uint64) (*types.OutboundTracker, error) { + ret := _m.Called(ctx, chain, nonce) + + if len(ret) == 0 { + panic("no return value specified for GetOutboundTracker") + } + + var r0 *types.OutboundTracker + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, chains.Chain, uint64) (*types.OutboundTracker, error)); ok { + return rf(ctx, chain, nonce) + } + if rf, ok := ret.Get(0).(func(context.Context, chains.Chain, uint64) *types.OutboundTracker); ok { + r0 = rf(ctx, chain, nonce) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.OutboundTracker) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, chains.Chain, uint64) error); ok { + r1 = rf(ctx, chain, nonce) + } else { + r1 = ret.Error(1) } - return &crosschaintypes.OutboundTracker{}, nil + + return r0, r1 } -func (m *MockZetacoreClient) GetAllOutboundTrackerByChain( - _ int64, - _ chaininterfaces.Order, -) ([]crosschaintypes.OutboundTracker, error) { - if m.paused { - return nil, errors.New(ErrMsgPaused) +// GetPendingNoncesByChain provides a mock function with given fields: ctx, chainID +func (_m *ZetacoreClient) GetPendingNoncesByChain(ctx context.Context, chainID int64) (observertypes.PendingNonces, error) { + ret := _m.Called(ctx, chainID) + + if len(ret) == 0 { + panic("no return value specified for GetPendingNoncesByChain") + } + + var r0 observertypes.PendingNonces + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (observertypes.PendingNonces, error)); ok { + return rf(ctx, chainID) } - return []crosschaintypes.OutboundTracker{}, nil + if rf, ok := ret.Get(0).(func(context.Context, int64) observertypes.PendingNonces); ok { + r0 = rf(ctx, chainID) + } else { + r0 = ret.Get(0).(observertypes.PendingNonces) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, chainID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) GetCrosschainFlags() (observerTypes.CrosschainFlags, error) { - if m.paused { - return observerTypes.CrosschainFlags{}, errors.New(ErrMsgPaused) +// GetRateLimiterFlags provides a mock function with given fields: ctx +func (_m *ZetacoreClient) GetRateLimiterFlags(ctx context.Context) (types.RateLimiterFlags, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetRateLimiterFlags") } - return observerTypes.CrosschainFlags{}, nil + + var r0 types.RateLimiterFlags + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (types.RateLimiterFlags, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) types.RateLimiterFlags); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(types.RateLimiterFlags) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) GetRateLimiterFlags() (crosschaintypes.RateLimiterFlags, error) { - if m.paused { - return crosschaintypes.RateLimiterFlags{}, errors.New(ErrMsgPaused) +// GetRateLimiterInput provides a mock function with given fields: ctx, window +func (_m *ZetacoreClient) GetRateLimiterInput(ctx context.Context, window int64) (*types.QueryRateLimiterInputResponse, error) { + ret := _m.Called(ctx, window) + + if len(ret) == 0 { + panic("no return value specified for GetRateLimiterInput") } - if m.rateLimiterFlags == nil { - return crosschaintypes.RateLimiterFlags{}, errors.New(ErrMsgRPCFailed) + + var r0 *types.QueryRateLimiterInputResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*types.QueryRateLimiterInputResponse, error)); ok { + return rf(ctx, window) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *types.QueryRateLimiterInputResponse); ok { + r0 = rf(ctx, window) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryRateLimiterInputResponse) + } } - return *m.rateLimiterFlags, nil + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, window) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) GetObserverList() ([]string, error) { - if m.paused { - return nil, errors.New(ErrMsgPaused) +// GetZetaHotKeyBalance provides a mock function with given fields: ctx +func (_m *ZetacoreClient) GetZetaHotKeyBalance(ctx context.Context) (math.Int, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetZetaHotKeyBalance") + } + + var r0 math.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (math.Int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) math.Int); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(math.Int) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) } - return []string{}, nil + + return r0, r1 } -func (m *MockZetacoreClient) GetBtcTssAddress(_ int64) (string, error) { - if m.paused { - return "", errors.New(ErrMsgPaused) +// ListPendingCCTX provides a mock function with given fields: ctx, chainID +func (_m *ZetacoreClient) ListPendingCCTX(ctx context.Context, chainID int64) ([]*types.CrossChainTx, uint64, error) { + ret := _m.Called(ctx, chainID) + + if len(ret) == 0 { + panic("no return value specified for ListPendingCCTX") + } + + var r0 []*types.CrossChainTx + var r1 uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, int64) ([]*types.CrossChainTx, uint64, error)); ok { + return rf(ctx, chainID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) []*types.CrossChainTx); ok { + r0 = rf(ctx, chainID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.CrossChainTx) + } } - return testutils.TSSAddressBTCMainnet, nil + + if rf, ok := ret.Get(1).(func(context.Context, int64) uint64); ok { + r1 = rf(ctx, chainID) + } else { + r1 = ret.Get(1).(uint64) + } + + if rf, ok := ret.Get(2).(func(context.Context, int64) error); ok { + r2 = rf(ctx, chainID) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } -func (m *MockZetacoreClient) GetInboundTrackersForChain(_ int64) ([]crosschaintypes.InboundTracker, error) { - if m.paused { - return nil, errors.New(ErrMsgPaused) +// ListPendingCCTXWithinRateLimit provides a mock function with given fields: ctx +func (_m *ZetacoreClient) ListPendingCCTXWithinRateLimit(ctx context.Context) (*types.QueryListPendingCctxWithinRateLimitResponse, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListPendingCCTXWithinRateLimit") } - return []crosschaintypes.InboundTracker{}, nil + + var r0 *types.QueryListPendingCctxWithinRateLimitResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*types.QueryListPendingCctxWithinRateLimitResponse, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *types.QueryListPendingCctxWithinRateLimitResponse); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryListPendingCctxWithinRateLimitResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) Pause() { - m.paused = true +// OnBeforeStop provides a mock function with given fields: callback +func (_m *ZetacoreClient) OnBeforeStop(callback func()) { + _m.Called(callback) } -func (m *MockZetacoreClient) Unpause() { - m.paused = false +// PostVoteBlameData provides a mock function with given fields: ctx, _a1, chainID, index +func (_m *ZetacoreClient) PostVoteBlameData(ctx context.Context, _a1 *blame.Blame, chainID int64, index string) (string, error) { + ret := _m.Called(ctx, _a1, chainID, index) + + if len(ret) == 0 { + panic("no return value specified for PostVoteBlameData") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *blame.Blame, int64, string) (string, error)); ok { + return rf(ctx, _a1, chainID, index) + } + if rf, ok := ret.Get(0).(func(context.Context, *blame.Blame, int64, string) string); ok { + r0 = rf(ctx, _a1, chainID, index) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, *blame.Blame, int64, string) error); ok { + r1 = rf(ctx, _a1, chainID, index) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (m *MockZetacoreClient) GetZetaHotKeyBalance() (math.Int, error) { - if m.paused { - return math.NewInt(0), errors.New(ErrMsgPaused) +// PostVoteBlockHeader provides a mock function with given fields: ctx, chainID, txhash, height, header +func (_m *ZetacoreClient) PostVoteBlockHeader(ctx context.Context, chainID int64, txhash []byte, height int64, header proofs.HeaderData) (string, error) { + ret := _m.Called(ctx, chainID, txhash, height, header) + + if len(ret) == 0 { + panic("no return value specified for PostVoteBlockHeader") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, []byte, int64, proofs.HeaderData) (string, error)); ok { + return rf(ctx, chainID, txhash, height, header) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, []byte, int64, proofs.HeaderData) string); ok { + r0 = rf(ctx, chainID, txhash, height, header) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, []byte, int64, proofs.HeaderData) error); ok { + r1 = rf(ctx, chainID, txhash, height, header) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PostVoteGasPrice provides a mock function with given fields: ctx, chain, gasPrice, supply, blockNum +func (_m *ZetacoreClient) PostVoteGasPrice(ctx context.Context, chain chains.Chain, gasPrice uint64, supply string, blockNum uint64) (string, error) { + ret := _m.Called(ctx, chain, gasPrice, supply, blockNum) + + if len(ret) == 0 { + panic("no return value specified for PostVoteGasPrice") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, chains.Chain, uint64, string, uint64) (string, error)); ok { + return rf(ctx, chain, gasPrice, supply, blockNum) + } + if rf, ok := ret.Get(0).(func(context.Context, chains.Chain, uint64, string, uint64) string); ok { + r0 = rf(ctx, chain, gasPrice, supply, blockNum) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, chains.Chain, uint64, string, uint64) error); ok { + r1 = rf(ctx, chain, gasPrice, supply, blockNum) + } else { + r1 = ret.Error(1) } - return math.NewInt(0), nil + + return r0, r1 } -// ---------------------------------------------------------------------------- -// Feed data to the mock zetacore client for testing -// ---------------------------------------------------------------------------- +// PostVoteInbound provides a mock function with given fields: ctx, gasLimit, retryGasLimit, msg +func (_m *ZetacoreClient) PostVoteInbound(ctx context.Context, gasLimit uint64, retryGasLimit uint64, msg *types.MsgVoteInbound) (string, string, error) { + ret := _m.Called(ctx, gasLimit, retryGasLimit, msg) + + if len(ret) == 0 { + panic("no return value specified for PostVoteInbound") + } + + var r0 string + var r1 string + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64, *types.MsgVoteInbound) (string, string, error)); ok { + return rf(ctx, gasLimit, retryGasLimit, msg) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64, *types.MsgVoteInbound) string); ok { + r0 = rf(ctx, gasLimit, retryGasLimit, msg) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, uint64, *types.MsgVoteInbound) string); ok { + r1 = rf(ctx, gasLimit, retryGasLimit, msg) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func(context.Context, uint64, uint64, *types.MsgVoteInbound) error); ok { + r2 = rf(ctx, gasLimit, retryGasLimit, msg) + } else { + r2 = ret.Error(2) + } -func (m *MockZetacoreClient) WithKeys(keys keyinterfaces.ObserverKeys) *MockZetacoreClient { - m.keys = keys - return m + return r0, r1, r2 } -func (m *MockZetacoreClient) WithPendingCctx(chainID int64, cctxs []*crosschaintypes.CrossChainTx) *MockZetacoreClient { - m.pendingCctxs[chainID] = cctxs - return m +// PostVoteOutbound provides a mock function with given fields: ctx, gasLimit, retryGasLimit, msg +func (_m *ZetacoreClient) PostVoteOutbound(ctx context.Context, gasLimit uint64, retryGasLimit uint64, msg *types.MsgVoteOutbound) (string, string, error) { + ret := _m.Called(ctx, gasLimit, retryGasLimit, msg) + + if len(ret) == 0 { + panic("no return value specified for PostVoteOutbound") + } + + var r0 string + var r1 string + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64, *types.MsgVoteOutbound) (string, string, error)); ok { + return rf(ctx, gasLimit, retryGasLimit, msg) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64, *types.MsgVoteOutbound) string); ok { + r0 = rf(ctx, gasLimit, retryGasLimit, msg) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, uint64, *types.MsgVoteOutbound) string); ok { + r1 = rf(ctx, gasLimit, retryGasLimit, msg) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func(context.Context, uint64, uint64, *types.MsgVoteOutbound) error); ok { + r2 = rf(ctx, gasLimit, retryGasLimit, msg) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } -func (m *MockZetacoreClient) WithRateLimiterFlags(flags *crosschaintypes.RateLimiterFlags) *MockZetacoreClient { - m.rateLimiterFlags = flags - return m +// Stop provides a mock function with given fields: +func (_m *ZetacoreClient) Stop() { + _m.Called() } -func (m *MockZetacoreClient) WithRateLimiterInput( - input *crosschaintypes.QueryRateLimiterInputResponse, -) *MockZetacoreClient { - m.input = input - return m +// NewZetacoreClient creates a new instance of ZetacoreClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewZetacoreClient(t interface { + mock.TestingT + Cleanup(func()) +}) *ZetacoreClient { + mock := &ZetacoreClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/zetaclient/testutils/mocks/zetacore_client_opts.go b/zetaclient/testutils/mocks/zetacore_client_opts.go new file mode 100644 index 0000000000..7e1e0c2392 --- /dev/null +++ b/zetaclient/testutils/mocks/zetacore_client_opts.go @@ -0,0 +1,73 @@ +package mocks + +import ( + "errors" + + "github.com/stretchr/testify/mock" + + "github.com/zeta-chain/zetacore/pkg/chains" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" + keyinterfaces "github.com/zeta-chain/zetacore/zetaclient/keys/interfaces" +) + +var errSomethingIsWrong = errors.New("oopsie") + +// Note that this is NOT codegen but a handwritten mock improvement. + +func (_m *ZetacoreClient) WithKeys(keys keyinterfaces.ObserverKeys) *ZetacoreClient { + _m.On("GetKeys").Maybe().Return(keys) + + return _m +} + +func (_m *ZetacoreClient) WithZetaChain() *ZetacoreClient { + _m.On("Chain").Maybe().Return(chains.ZetaChainMainnet) + + return _m +} + +func (_m *ZetacoreClient) WithPostVoteOutbound(zetaTxHash string, ballotIndex string) *ZetacoreClient { + _m.On("PostVoteOutbound", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Maybe(). + Return(zetaTxHash, ballotIndex, nil) + + return _m +} + +func (_m *ZetacoreClient) WithPostVoteInbound(zetaTxHash string, ballotIndex string) *ZetacoreClient { + _m.On("PostVoteInbound", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Maybe(). + Return(zetaTxHash, ballotIndex, nil) + + return _m +} + +func (_m *ZetacoreClient) WithRateLimiterFlags(flags *crosschaintypes.RateLimiterFlags) *ZetacoreClient { + on := _m.On("GetRateLimiterFlags", mock.Anything).Maybe() + if flags != nil { + on.Return(*flags, nil) + } else { + on.Return(crosschaintypes.RateLimiterFlags{}, errSomethingIsWrong) + } + + return _m +} + +func (_m *ZetacoreClient) WithRateLimiterInput(in *crosschaintypes.QueryRateLimiterInputResponse) *ZetacoreClient { + on := _m.On("GetRateLimiterInput", mock.Anything, mock.Anything).Maybe() + if in != nil { + on.Return(in, nil) + } else { + on.Return(nil, errSomethingIsWrong) + } + + return _m +} + +func (_m *ZetacoreClient) WithPendingCctx(chainID int64, cctxs []*crosschaintypes.CrossChainTx) *ZetacoreClient { + totalPending := uint64(len(cctxs)) + + _m.On("ListPendingCCTX", mock.Anything, chainID).Maybe().Return(cctxs, totalPending, nil) + + return _m +} diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index e00252db55..fb5a56c6ba 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -3,6 +3,7 @@ package tss import ( "bytes" + "context" "encoding/base64" "encoding/hex" "fmt" @@ -90,6 +91,7 @@ type TSS struct { // NewTSS creates a new TSS instance func NewTSS( + ctx context.Context, appContext *appcontext.AppContext, peer p2p.AddrList, privkey tmcrypto.PrivKey, @@ -131,7 +133,7 @@ func NewTSS( client.GetLogger().Error().Err(err).Msg("VerifyKeysharesForPubkeys fail") } - keygenRes, err := newTss.ZetacoreClient.GetKeyGen() + keygenRes, err := newTss.ZetacoreClient.GetKeyGen(ctx) if err != nil { return nil, err } @@ -222,6 +224,7 @@ func (tss *TSS) Pubkey() []byte { // digest should be Hashes of some data // NOTE: Specify optionalPubkey to use a different pubkey than the current pubkey set during keygen func (tss *TSS) Sign( + ctx context.Context, digest []byte, height uint64, nonce uint64, @@ -258,7 +261,7 @@ func (tss *TSS) Sign( if IsEnvFlagEnabled(envFlagPostBlame) { digest := hex.EncodeToString(digest) index := observertypes.GetBlameIndex(chainID, nonce, digest, height) - zetaHash, err := tss.ZetacoreClient.PostBlameData(&ksRes.Blame, chainID, index) + zetaHash, err := tss.ZetacoreClient.PostVoteBlameData(ctx, &ksRes.Blame, chainID, index) if err != nil { log.Error().Err(err).Msg("error sending blame data to core") return [65]byte{}, err @@ -311,7 +314,13 @@ func (tss *TSS) Sign( // SignBatch is hash of some data // digest should be batch of hashes of some data -func (tss *TSS) SignBatch(digests [][]byte, height uint64, nonce uint64, chainID int64) ([][65]byte, error) { +func (tss *TSS) SignBatch( + ctx context.Context, + digests [][]byte, + height uint64, + nonce uint64, + chainID int64, +) ([][65]byte, error) { tssPubkey := tss.CurrentPubkey digestBase64 := make([]string, len(digests)) for i, digest := range digests { @@ -334,7 +343,7 @@ func (tss *TSS) SignBatch(digests [][]byte, height uint64, nonce uint64, chainID if IsEnvFlagEnabled(envFlagPostBlame) { digest := combineDigests(digestBase64) index := observertypes.GetBlameIndex(chainID, nonce, hex.EncodeToString(digest), height) - zetaHash, err := tss.ZetacoreClient.PostBlameData(&ksRes.Blame, chainID, index) + zetaHash, err := tss.ZetacoreClient.PostVoteBlameData(ctx, &ksRes.Blame, chainID, index) if err != nil { log.Error().Err(err).Msg("error sending blame data to core") return [][65]byte{}, err diff --git a/zetaclient/zetacore/broadcast.go b/zetaclient/zetacore/broadcast.go index 6c1b475476..5471d4b63f 100644 --- a/zetaclient/zetacore/broadcast.go +++ b/zetaclient/zetacore/broadcast.go @@ -1,18 +1,18 @@ package zetacore import ( + "context" "fmt" "regexp" "strconv" "strings" - rpchttp "github.com/cometbft/cometbft/rpc/client/http" + "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/client" clienttx "github.com/cosmos/cosmos-sdk/client/tx" sdktypes "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/rs/zerolog/log" flag "github.com/spf13/pflag" @@ -22,62 +22,49 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/hsm" ) -// BroadcastInterface defines the signature of the broadcast function used by zetacore transactions -type BroadcastInterface = func(client *Client, gaslimit uint64, authzWrappedMsg sdktypes.Msg, authzSigner authz.Signer) (string, error) +// paying 50% more than the current base gas price to buffer for potential block-by-block +// gas price increase due to EIP1559 feemarket on ZetaChain +var bufferMultiplier = sdktypes.MustNewDecFromStr("1.5") -const ( - // DefaultBaseGasPrice is the default base gas price - DefaultBaseGasPrice = 1_000_000 -) - -var ( - // paying 50% more than the current base gas price to buffer for potential block-by-block - // gas price increase due to EIP1559 feemarket on ZetaChain - bufferMultiplier = sdktypes.MustNewDecFromStr("1.5") - - // Variable function used by transactions to broadcast a message to zetacore. This will create enough flexibility - // in the implementation to allow for more comprehensive unit testing. - zetacoreBroadcast BroadcastInterface = BroadcastToZetaCore -) - -// BroadcastToZetaCore is the default broadcast function used to send transactions to zetacore -func BroadcastToZetaCore( - client *Client, +// Broadcast Broadcasts tx to ZetaChain. Returns txHash and error +func (c *Client) Broadcast( + ctx context.Context, gasLimit uint64, authzWrappedMsg sdktypes.Msg, authzSigner authz.Signer, ) (string, error) { - return client.Broadcast(gasLimit, authzWrappedMsg, authzSigner) -} - -// Broadcast Broadcasts tx to ZetaChain. Returns txHash and error -func (c *Client) Broadcast(gaslimit uint64, authzWrappedMsg sdktypes.Msg, authzSigner authz.Signer) (string, error) { - c.broadcastLock.Lock() - defer c.broadcastLock.Unlock() - var err error - - blockHeight, err := c.GetBlockHeight() + blockHeight, err := c.GetBlockHeight(ctx) if err != nil { - return "", err + return "", errors.Wrap(err, "unable to get block height") } - baseGasPrice, err := c.GetBaseGasPrice() + + baseGasPrice, err := c.GetBaseGasPrice(ctx) if err != nil { - return "", err + return "", errors.Wrap(err, "unable to get base gas price") } + + // shouldn't happen, but just in case if baseGasPrice == 0 { - baseGasPrice = DefaultBaseGasPrice // shouldn't happen, but just in case + baseGasPrice = DefaultBaseGasPrice } + reductionRate := sdktypes.MustNewDecFromStr(ante.GasPriceReductionRate) + // multiply gas price by the system tx reduction rate adjustedBaseGasPrice := sdktypes.NewDec(baseGasPrice).Mul(reductionRate).Mul(bufferMultiplier) + c.mu.Lock() + defer c.mu.Unlock() + if blockHeight > c.blockHeight { c.blockHeight = blockHeight accountNumber, seqNumber, err := c.GetAccountNumberAndSequenceNumber(authzSigner.KeyType) if err != nil { return "", err } + c.accountNumber[authzSigner.KeyType] = accountNumber + if c.seqNumber[authzSigner.KeyType] < seqNumber { c.seqNumber[authzSigner.KeyType] = seqNumber } @@ -85,11 +72,7 @@ func (c *Client) Broadcast(gaslimit uint64, authzWrappedMsg sdktypes.Msg, authzS flags := flag.NewFlagSet("zetaclient", 0) - ctx, err := c.GetContext() - if err != nil { - return "", err - } - factory, err := clienttx.NewFactoryCLI(ctx, flags) + factory, err := clienttx.NewFactoryCLI(c.cosmosClientContext, flags) if err != nil { return "", err } @@ -99,29 +82,32 @@ func (c *Client) Broadcast(gaslimit uint64, authzWrappedMsg sdktypes.Msg, authzS factory = factory.WithSignMode(signing.SignMode_SIGN_MODE_DIRECT) builder, err := factory.BuildUnsignedTx(authzWrappedMsg) if err != nil { - return "", err + return "", errors.Wrap(err, "unable to build unsigned tx") } - builder.SetGasLimit(gaslimit) + builder.SetGasLimit(gasLimit) // #nosec G701 always in range - fee := sdktypes.NewCoins(sdktypes.NewCoin(config.BaseDenom, - sdktypes.NewInt(int64(gaslimit)).Mul(adjustedBaseGasPrice.Ceil().RoundInt()))) + fee := sdktypes.NewCoins(sdktypes.NewCoin( + config.BaseDenom, + sdktypes.NewInt(int64(gasLimit)).Mul(adjustedBaseGasPrice.Ceil().RoundInt()), + )) builder.SetFeeAmount(fee) - err = c.SignTx(factory, ctx.GetFromName(), builder, true, ctx.TxConfig) + + err = c.SignTx(factory, c.cosmosClientContext.GetFromName(), builder, true, c.cosmosClientContext.TxConfig) if err != nil { - return "", err + return "", errors.Wrap(err, "unable to sign tx") } - txBytes, err := ctx.TxConfig.TxEncoder()(builder.GetTx()) + + txBytes, err := c.cosmosClientContext.TxConfig.TxEncoder()(builder.GetTx()) if err != nil { - return "", err + return "", errors.Wrap(err, "unable to encode tx") } // broadcast to a Tendermint node - commit, err := ctx.BroadcastTxSync(txBytes) + commit, err := c.cosmosClientContext.BroadcastTxSync(txBytes) if err != nil { - c.logger.Error().Err(err).Msgf("fail to broadcast tx %s", err.Error()) - return "", err + return "", errors.Wrap(err, "fail to broadcast tx sync") } // Code will be the tendermint ABICode , it start at 1 , so if it is an error , code will not be zero @@ -156,54 +142,6 @@ func (c *Client) Broadcast(gaslimit uint64, authzWrappedMsg sdktypes.Msg, authzS return commit.TxHash, nil } -// GetContext return a valid context with all relevant values set -func (c *Client) GetContext() (client.Context, error) { - ctx := client.Context{} - addr, err := c.keys.GetAddress() - if err != nil { - c.logger.Error().Err(err).Msg("fail to get address from key") - return ctx, err - } - - // if password is needed, set it as input - password := c.keys.GetHotkeyPassword() - if password != "" { - ctx = ctx.WithInput(strings.NewReader(fmt.Sprintf("%[1]s\n%[1]s\n", password))) - } - - ctx = ctx.WithKeyring(c.keys.GetKeybase()) - ctx = ctx.WithChainID(c.chainID) - ctx = ctx.WithHomeDir(c.cfg.ChainHomeFolder) - ctx = ctx.WithFromName(c.cfg.SignerName) - ctx = ctx.WithFromAddress(addr) - ctx = ctx.WithBroadcastMode("sync") - - ctx = ctx.WithCodec(c.encodingCfg.Codec) - ctx = ctx.WithInterfaceRegistry(c.encodingCfg.InterfaceRegistry) - ctx = ctx.WithTxConfig(c.encodingCfg.TxConfig) - ctx = ctx.WithLegacyAmino(c.encodingCfg.Amino) - ctx = ctx.WithAccountRetriever(authtypes.AccountRetriever{}) - - if c.enableMockSDKClient { - ctx = ctx.WithClient(c.mockSDKClient) - } else { - remote := c.cfg.ChainRPC - if !strings.HasPrefix(c.cfg.ChainHost, "http") { - remote = fmt.Sprintf("tcp://%s", remote) - } - - ctx = ctx.WithNodeURI(remote) - wsClient, err := rpchttp.New(remote, "/websocket") - if err != nil { - return ctx, err - } - - ctx = ctx.WithClient(wsClient) - } - - return ctx, nil -} - // SignTx signs a tx with the given name func (c *Client) SignTx( txf clienttx.Factory, @@ -212,42 +150,48 @@ func (c *Client) SignTx( overwriteSig bool, txConfig client.TxConfig, ) error { - if c.cfg.HsmMode { + if c.config.HsmMode { return hsm.SignWithHSM(txf, name, txBuilder, overwriteSig, txConfig) } + return clienttx.Sign(txf, name, txBuilder, overwriteSig) } // QueryTxResult query the result of a tx func (c *Client) QueryTxResult(hash string) (*sdktypes.TxResponse, error) { - ctx, err := c.GetContext() - if err != nil { - return nil, err - } - return authtx.QueryTx(ctx, hash) + return authtx.QueryTx(c.cosmosClientContext, hash) } // HandleBroadcastError returns whether to retry in a few seconds, and whether to report via AddOutboundTracker // returns (bool retry, bool report) func HandleBroadcastError(err error, nonce, toChain, outboundHash string) (bool, bool) { - if strings.Contains(err.Error(), "nonce too low") { - log.Warn(). - Err(err). - Msgf("nonce too low! this might be a unnecessary key-sign. increase re-try interval and awaits outbound confirmation") + if err == nil { return false, false } - if strings.Contains(err.Error(), "replacement transaction underpriced") { - log.Warn(). - Err(err). - Msgf("Broadcast replacement: nonce %s chain %s outboundHash %s", nonce, toChain, outboundHash) + + msg := err.Error() + evt := log.Warn().Err(err). + Str("broadcast.nonce", nonce). + Str("broadcast.to_chain", toChain). + Str("broadcast.outbound_hash", outboundHash) + + switch { + case strings.Contains(msg, "nonce too low"): + const m = "nonce too low! this might be a unnecessary key-sign. increase retry interval and awaits outbound confirmation" + evt.Msg(m) return false, false - } else if strings.Contains(err.Error(), "already known") { // this is error code from QuickNode - log.Warn().Err(err).Msgf("Broadcast duplicates: nonce %s chain %s outboundHash %s", nonce, toChain, outboundHash) - return false, true // report to tracker, because there's possibilities a successful broadcast gets this error code - } - log.Error(). - Err(err). - Msgf("Broadcast error: nonce %s chain %s outboundHash %s; retrying...", nonce, toChain, outboundHash) - return true, false + case strings.Contains(msg, "replacement transaction underpriced"): + evt.Msg("Broadcast replacement") + return false, false + + case strings.Contains(msg, "already known"): + // report to tracker, because there's possibilities a successful broadcast gets this error code + evt.Msg("Broadcast duplicates") + return false, true + + default: + evt.Msg("Broadcast error. Retrying...") + return true, false + } } diff --git a/zetaclient/zetacore/broadcast_test.go b/zetaclient/zetacore/broadcast_test.go index ba86407811..6acd5c535f 100644 --- a/zetaclient/zetacore/broadcast_test.go +++ b/zetaclient/zetacore/broadcast_test.go @@ -1,6 +1,7 @@ package zetacore import ( + "context" "encoding/hex" "errors" "net" @@ -39,11 +40,14 @@ func TestHandleBroadcastError(t *testing.T) { } func TestBroadcast(t *testing.T) { + ctx := context.Background() + address := types.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) //Setup server for multiple grpc calls listener, err := net.Listen("tcp", "127.0.0.1:9090") require.NoError(t, err) + server := grpcmock.MockUnstartedServer( grpcmock.RegisterService(crosschaintypes.RegisterQueryServer), grpcmock.RegisterService(feemarkettypes.RegisterQueryServer), @@ -70,14 +74,16 @@ func TestBroadcast(t *testing.T) { )(t) server.Serve() - defer closeMockServer(t, server) + defer server.Close() - client, err := setupZetacoreClient() - require.NoError(t, err) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") + observerKeys := keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") t.Run("broadcast success", func(t *testing.T) { - client.EnableMockSDKClient(mocks.NewSDKClientWithErr(nil, 0)) + client := setupZetacoreClient(t, + withObserverKeys(observerKeys), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + ) + blockHash, err := hex.DecodeString(ethBlockHash) require.NoError(t, err) msg := observerTypes.NewMsgVoteBlockHeader( @@ -87,16 +93,21 @@ func TestBroadcast(t *testing.T) { 18495266, getHeaderData(t), ) - authzMsg, authzSigner, err := client.WrapMessageWithAuthz(msg) + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) require.NoError(t, err) - _, err = BroadcastToZetaCore(client, 10000, authzMsg, authzSigner) + + _, err = client.Broadcast(ctx, 10_000, authzMsg, authzSigner) require.NoError(t, err) }) t.Run("broadcast failed", func(t *testing.T) { - client.EnableMockSDKClient( - mocks.NewSDKClientWithErr(errors.New("account sequence mismatch, expected 5 got 4"), 32), + client := setupZetacoreClient(t, + withObserverKeys(observerKeys), + withTendermint( + mocks.NewSDKClientWithErr(t, errors.New("account sequence mismatch, expected 5 got 4"), 32), + ), ) + blockHash, err := hex.DecodeString(ethBlockHash) require.NoError(t, err) msg := observerTypes.NewMsgVoteBlockHeader( @@ -106,19 +117,10 @@ func TestBroadcast(t *testing.T) { 18495266, getHeaderData(t), ) - authzMsg, authzSigner, err := client.WrapMessageWithAuthz(msg) + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) require.NoError(t, err) - _, err = BroadcastToZetaCore(client, 10000, authzMsg, authzSigner) + + _, err = client.Broadcast(ctx, 10_000, authzMsg, authzSigner) require.Error(t, err) }) } - -func TestZetacore_GetContext(t *testing.T) { - address := types.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client, err := setupZetacoreClient() - require.NoError(t, err) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") - - _, err = client.GetContext() - require.NoError(t, err) -} diff --git a/zetaclient/zetacore/client.go b/zetaclient/zetacore/client.go index 53d1d5958c..2874408451 100644 --- a/zetaclient/zetacore/client.go +++ b/zetaclient/zetacore/client.go @@ -1,52 +1,100 @@ -// Package zetacore provides functionalities for interacting with ZetaChain +// Package zetacore provides the client to interact with zetacore node via GRPC. package zetacore import ( + "context" "fmt" + "strings" "sync" "time" "cosmossdk.io/simapp/params" - rpcclient "github.com/cometbft/cometbft/rpc/client" + rpchttp "github.com/cometbft/cometbft/rpc/client/http" + cosmosclient "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "github.com/zeta-chain/zetacore/app" "github.com/zeta-chain/zetacore/pkg/authz" "github.com/zeta-chain/zetacore/pkg/chains" + authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" + lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" keyinterfaces "github.com/zeta-chain/zetacore/zetaclient/keys/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/metrics" ) var _ interfaces.ZetacoreClient = &Client{} // Client is the client to send tx to zetacore type Client struct { - logger zerolog.Logger + logger zerolog.Logger + config config.ClientConfiguration + + client clients + cosmosClientContext cosmosclient.Context + blockHeight int64 accountNumber map[authz.KeyType]uint64 seqNumber map[authz.KeyType]uint64 - grpcConn *grpc.ClientConn - cfg config.ClientConfiguration - encodingCfg params.EncodingConfig - keys keyinterfaces.ObserverKeys - broadcastLock *sync.RWMutex - chainID string - chain chains.Chain - stop chan struct{} - pause chan struct{} - Telemetry *metrics.TelemetryServer - - // enableMockSDKClient is a flag that determines whether the mock cosmos sdk client should be used, primarily for - // unit testing - enableMockSDKClient bool - mockSDKClient rpcclient.Client + + encodingCfg params.EncodingConfig + keys keyinterfaces.ObserverKeys + chainID string + chain chains.Chain + stop chan struct{} + onBeforeStopCallback []func() + + mu sync.RWMutex +} + +type clients struct { + observer observertypes.QueryClient + light lightclienttypes.QueryClient + crosschain crosschaintypes.QueryClient + bank banktypes.QueryClient + upgrade upgradetypes.QueryClient + fees feemarkettypes.QueryClient + authority authoritytypes.QueryClient + tendermint tmservice.ServiceClient +} + +var unsecureGRPC = grpc.WithTransportCredentials(insecure.NewCredentials()) + +type constructOpts struct { + customTendermint bool + tendermintClient cosmosclient.TendermintRPC + + customAccountRetriever bool + accountRetriever cosmosclient.AccountRetriever +} + +type Opt func(cfg *constructOpts) + +// WithTendermintClient sets custom tendermint client +func WithTendermintClient(client cosmosclient.TendermintRPC) Opt { + return func(c *constructOpts) { + c.customTendermint = true + c.tendermintClient = client + } +} + +// WithCustomAccountRetriever sets custom tendermint client +func WithCustomAccountRetriever(ac cosmosclient.AccountRetriever) Opt { + return func(c *constructOpts) { + c.customAccountRetriever = true + c.accountRetriever = ac + } } // NewClient create a new instance of Client @@ -56,26 +104,36 @@ func NewClient( signerName string, chainID string, hsmMode bool, - telemetry *metrics.TelemetryServer, + logger zerolog.Logger, + opts ...Opt, ) (*Client, error) { - // main module logger - logger := log.With().Str("module", "ZetacoreClient").Logger() + var constructOptions constructOpts + for _, opt := range opts { + opt(&constructOptions) + } + + zetaChain, err := chains.ZetaChainFromCosmosChainID(chainID) + if err != nil { + return nil, errors.Wrapf(err, "invalid chain id %q", chainID) + } + + log := logger.With().Str("module", "zetacoreClient").Logger() + cfg := config.ClientConfiguration{ - ChainHost: fmt.Sprintf("%s:1317", chainIP), + ChainHost: cosmosREST(chainIP), SignerName: signerName, SignerPasswd: "password", - ChainRPC: fmt.Sprintf("%s:26657", chainIP), + ChainRPC: tendermintRPC(chainIP), HsmMode: hsmMode, } - grpcConn, err := grpc.Dial( - fmt.Sprintf("%s:9090", chainIP), - grpc.WithInsecure(), - ) + encodingCfg := app.MakeEncodingConfig() + + grpcConn, err := grpc.Dial(cosmosGRPC(chainIP), unsecureGRPC) if err != nil { - logger.Error().Err(err).Msg("grpc dial fail") - return nil, err + return nil, errors.Wrap(err, "grpc dial fail") } + accountsMap := make(map[authz.KeyType]uint64) seqMap := make(map[authz.KeyType]uint64) for _, keyType := range authz.GetAllKeyTypes() { @@ -83,27 +141,110 @@ func NewClient( seqMap[keyType] = 0 } - zetaChain, err := chains.ZetaChainFromCosmosChainID(chainID) + cosmosContext, err := buildCosmosClientContext(chainID, keys, cfg, encodingCfg, constructOptions) if err != nil { - return nil, fmt.Errorf("invalid chain id %s, %w", chainID, err) + return nil, errors.Wrap(err, "unable to build cosmos client context") } return &Client{ - logger: logger, - grpcConn: grpcConn, - accountNumber: accountsMap, - seqNumber: seqMap, - cfg: cfg, - encodingCfg: app.MakeEncodingConfig(), - keys: keys, - broadcastLock: &sync.RWMutex{}, - stop: make(chan struct{}), - chainID: chainID, - chain: zetaChain, - pause: make(chan struct{}), - Telemetry: telemetry, - enableMockSDKClient: false, - mockSDKClient: nil, + logger: log, + config: cfg, + + cosmosClientContext: cosmosContext, + client: clients{ + observer: observertypes.NewQueryClient(grpcConn), + light: lightclienttypes.NewQueryClient(grpcConn), + crosschain: crosschaintypes.NewQueryClient(grpcConn), + bank: banktypes.NewQueryClient(grpcConn), + upgrade: upgradetypes.NewQueryClient(grpcConn), + fees: feemarkettypes.NewQueryClient(grpcConn), + authority: authoritytypes.NewQueryClient(grpcConn), + tendermint: tmservice.NewServiceClient(grpcConn), + }, + + accountNumber: accountsMap, + seqNumber: seqMap, + + encodingCfg: encodingCfg, + keys: keys, + stop: make(chan struct{}), + chainID: chainID, + chain: zetaChain, + }, nil +} + +// buildCosmosClientContext constructs a valid context with all relevant values set +func buildCosmosClientContext( + chainID string, + keys keyinterfaces.ObserverKeys, + config config.ClientConfiguration, + encodingConfig params.EncodingConfig, + opts constructOpts, +) (cosmosclient.Context, error) { + if keys == nil { + return cosmosclient.Context{}, errors.New("client key are not set") + } + + addr, err := keys.GetAddress() + if err != nil { + return cosmosclient.Context{}, errors.Wrap(err, "fail to get address from key") + } + + var ( + input = strings.NewReader("") + client cosmosclient.TendermintRPC + nodeURI string + ) + + // if password is needed, set it as input + password := keys.GetHotkeyPassword() + if password != "" { + input = strings.NewReader(fmt.Sprintf("%[1]s\n%[1]s\n", password)) + } + + // note that in rare cases, this might give FALSE positive + // (google "golang nil interface comparison") + client = opts.tendermintClient + if !opts.customTendermint { + remote := config.ChainRPC + if !strings.HasPrefix(config.ChainHost, "http") { + remote = fmt.Sprintf("tcp://%s", remote) + } + + wsClient, err := rpchttp.New(remote, "/websocket") + if err != nil { + return cosmosclient.Context{}, err + } + + client = wsClient + nodeURI = remote + } + + var accountRetriever cosmosclient.AccountRetriever + if opts.customAccountRetriever { + accountRetriever = opts.accountRetriever + } else { + accountRetriever = authtypes.AccountRetriever{} + } + + return cosmosclient.Context{ + Client: client, + NodeURI: nodeURI, + FromAddress: addr, + ChainID: chainID, + Keyring: keys.GetKeybase(), + BroadcastMode: "sync", + HomeDir: config.ChainHomeFolder, + FromName: config.SignerName, + + AccountRetriever: accountRetriever, + + Codec: encodingConfig.Codec, + InterfaceRegistry: encodingConfig.InterfaceRegistry, + TxConfig: encodingConfig.TxConfig, + LegacyAmino: encodingConfig.Amino, + + Input: input, }, nil } @@ -134,38 +275,45 @@ func (c *Client) GetKeys() keyinterfaces.ObserverKeys { return c.keys } +// OnBeforeStop adds a callback to be called before the client stops. +func (c *Client) OnBeforeStop(callback func()) { + c.onBeforeStopCallback = append(c.onBeforeStopCallback, callback) +} + +// Stop stops the client and optionally calls the onBeforeStop callbacks. func (c *Client) Stop() { - c.logger.Info().Msgf("zetacore client is stopping") - close(c.stop) // this notifies all configupdater to stop + c.logger.Info().Msgf("Stopping zetacore client") + + for i := len(c.onBeforeStopCallback) - 1; i >= 0; i-- { + c.logger.Info().Int("callback.index", i).Msgf("calling onBeforeStopCallback") + c.onBeforeStopCallback[i]() + } + + close(c.stop) } // GetAccountNumberAndSequenceNumber We do not use multiple KeyType for now , but this can be optionally used in the future to seprate TSS signer from Zetaclient GRantee func (c *Client) GetAccountNumberAndSequenceNumber(_ authz.KeyType) (uint64, uint64, error) { - ctx, err := c.GetContext() - if err != nil { - return 0, 0, err - } address, err := c.keys.GetAddress() if err != nil { return 0, 0, err } - return ctx.AccountRetriever.GetAccountNumberSequence(ctx, address) + return c.cosmosClientContext.AccountRetriever.GetAccountNumberSequence(c.cosmosClientContext, address) } // SetAccountNumber sets the account number and sequence number for the given keyType +// todo remove method and make it part of the client constructor. func (c *Client) SetAccountNumber(keyType authz.KeyType) error { - ctx, err := c.GetContext() - if err != nil { - return errors.Wrap(err, "fail to get context") - } address, err := c.keys.GetAddress() if err != nil { return errors.Wrap(err, "fail to get address") } - accN, seq, err := ctx.AccountRetriever.GetAccountNumberSequence(ctx, address) + + accN, seq, err := c.cosmosClientContext.AccountRetriever.GetAccountNumberSequence(c.cosmosClientContext, address) if err != nil { return errors.Wrap(err, "fail to get account number and sequence number") } + c.accountNumber[keyType] = accN c.seqNumber[keyType] = seq @@ -173,10 +321,10 @@ func (c *Client) SetAccountNumber(keyType authz.KeyType) error { } // WaitForZetacoreToCreateBlocks waits for zetacore to create blocks -func (c *Client) WaitForZetacoreToCreateBlocks() error { +func (c *Client) WaitForZetacoreToCreateBlocks(ctx context.Context) error { retryCount := 0 for { - block, err := c.GetLatestZetaBlock() + block, err := c.GetLatestZetaBlock(ctx) if err == nil && block.Header.Height > 1 { c.logger.Info().Msgf("Zetacore height: %d", block.Header.Height) break @@ -193,29 +341,40 @@ func (c *Client) WaitForZetacoreToCreateBlocks() error { // UpdateZetacoreContext updates zetacore context // zetacore stores zetacore context for all clients -func (c *Client) UpdateZetacoreContext(coreContext *context.AppContext, init bool, sampledLogger zerolog.Logger) error { - bn, err := c.GetBlockHeight() +func (c *Client) UpdateZetacoreContext( + ctx context.Context, + appContext *zctx.AppContext, + init bool, + sampledLogger zerolog.Logger, +) error { + bn, err := c.GetBlockHeight(ctx) if err != nil { return fmt.Errorf("failed to get zetablock height: %w", err) } - plan, err := c.GetUpgradePlan() + + plan, err := c.GetUpgradePlan(ctx) if err != nil { - // if there is no active upgrade plan, plan will be nil, err will be nil as well. return fmt.Errorf("failed to get upgrade plan: %w", err) } - if plan != nil && bn == plan.Height-1 { // stop zetaclients; notify operator to upgrade and restart - c.logger.Warn(). - Msgf("Active upgrade plan detected and upgrade height reached: %s at height %d; ZetaClient is stopped;"+ - "please kill this process, replace zetaclientd binary with upgraded version, and restart zetaclientd", plan.Name, plan.Height) - c.pause <- struct{}{} // notify Orchestrator to stop Observers, Signers, and Orchestrator itself + + // Stop client and notify dependant services to stop (Orchestrator, Observers, and Signers) + if plan != nil && bn == plan.Height-1 { + c.logger.Warn().Msgf( + "Active upgrade plan detected and upgrade height reached: %s at height %d; Stopping ZetaClient;"+ + " please kill this process, replace zetaclientd binary with upgraded version, and restart zetaclientd", + plan.Name, + plan.Height, + ) + + c.Stop() } - additionalChains, err := c.GetAdditionalChains() + additionalChains, err := c.GetAdditionalChains(ctx) if err != nil { return fmt.Errorf("failed to additional chains: %w", err) } - chainParams, err := c.GetChainParams() + chainParams, err := c.GetChainParams(ctx) if err != nil { return fmt.Errorf("failed to get chain params: %w", err) } @@ -237,40 +396,42 @@ func (c *Client) UpdateZetacoreContext(coreContext *context.AppContext, init boo } } - supportedChains, err := c.GetSupportedChains() + supportedChains, err := c.GetSupportedChains(ctx) if err != nil { return fmt.Errorf("failed to get supported chains: %w", err) } + newChains := make([]chains.Chain, len(supportedChains)) for i, chain := range supportedChains { newChains[i] = chain } - keyGen, err := c.GetKeyGen() + + keyGen, err := c.GetKeyGen(ctx) if err != nil { c.logger.Info().Msg("Unable to fetch keygen from zetacore") return fmt.Errorf("failed to get keygen: %w", err) } - tss, err := c.GetCurrentTss() + tss, err := c.GetCurrentTSS(ctx) if err != nil { c.logger.Info().Err(err).Msg("Unable to fetch TSS from zetacore") return fmt.Errorf("failed to get current tss: %w", err) } tssPubKey := tss.GetTssPubkey() - crosschainFlags, err := c.GetCrosschainFlags() + crosschainFlags, err := c.GetCrosschainFlags(ctx) if err != nil { c.logger.Info().Msg("Unable to fetch cross-chain flags from zetacore") return fmt.Errorf("failed to get crosschain flags: %w", err) } - blockHeaderEnabledChains, err := c.GetBlockHeaderEnabledChains() + blockHeaderEnabledChains, err := c.GetBlockHeaderEnabledChains(ctx) if err != nil { c.logger.Info().Msg("Unable to fetch block header enabled chains from zetacore") return err } - coreContext.Update( + appContext.Update( keyGen, newChains, newEVMParams, @@ -285,19 +446,14 @@ func (c *Client) UpdateZetacoreContext(coreContext *context.AppContext, init boo return nil } -// Pause pauses the client -func (c *Client) Pause() { - <-c.pause +func cosmosREST(host string) string { + return fmt.Sprintf("%s:1317", host) } -// Unpause unpauses the client -func (c *Client) Unpause() { - c.pause <- struct{}{} +func cosmosGRPC(host string) string { + return fmt.Sprintf("%s:9090", host) } -// EnableMockSDKClient enables the mock cosmos sdk client -// TODO(revamp): move this to a test package -func (c *Client) EnableMockSDKClient(client rpcclient.Client) { - c.mockSDKClient = client - c.enableMockSDKClient = true +func tendermintRPC(host string) string { + return fmt.Sprintf("%s:26657", host) } diff --git a/zetaclient/zetacore/client_monitor.go b/zetaclient/zetacore/client_monitor.go new file mode 100644 index 0000000000..851589b96b --- /dev/null +++ b/zetaclient/zetacore/client_monitor.go @@ -0,0 +1,182 @@ +package zetacore + +import ( + "context" + "strings" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/pkg/retry" + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +// MonitorVoteOutboundResult monitors the result of a vote outbound tx +// retryGasLimit is the gas limit used to resend the tx if it fails because of insufficient gas +// if retryGasLimit is 0, the tx is not resent +func (c *Client) MonitorVoteOutboundResult( + ctx context.Context, + zetaTxHash string, + retryGasLimit uint64, + msg *types.MsgVoteOutbound, +) error { + defer func() { + if r := recover(); r != nil { + c.logger.Error(). + Interface("panic", r). + Str("outbound.hash", zetaTxHash). + Msg("monitorVoteOutboundResult: recovered from panic") + } + }() + + call := func() error { + return retry.Retry(c.monitorVoteOutboundResult(ctx, zetaTxHash, retryGasLimit, msg)) + } + + err := retryWithBackoff(call, monitorRetryCount, monitorInterval/2, monitorInterval) + if err != nil { + c.logger.Error().Err(err). + Str("outbound.hash", zetaTxHash). + Msg("monitorVoteOutboundResult: unable to query tx result") + + return err + } + + return nil +} + +func (c *Client) monitorVoteOutboundResult( + ctx context.Context, + zetaTxHash string, + retryGasLimit uint64, + msg *types.MsgVoteOutbound, +) error { + // query tx result from ZetaChain + txResult, err := c.QueryTxResult(zetaTxHash) + if err != nil { + return errors.Wrap(err, "failed to query tx result") + } + + logFields := map[string]any{ + "outbound.hash": zetaTxHash, + "outbound.raw_log": txResult.RawLog, + } + + switch { + case strings.Contains(txResult.RawLog, "failed to execute message"): + // the inbound vote tx shouldn't fail to execute + // this shouldn't happen + c.logger.Error().Fields(logFields).Msg("monitorVoteOutboundResult: failed to execute vote") + case strings.Contains(txResult.RawLog, "out of gas"): + // if the tx fails with an out of gas error, resend the tx with more gas if retryGasLimit > 0 + c.logger.Debug().Fields(logFields).Msg("monitorVoteOutboundResult: out of gas") + + if retryGasLimit > 0 { + // new retryGasLimit set to 0 to prevent reentering this function + if _, _, err := c.PostVoteOutbound(ctx, retryGasLimit, 0, msg); err != nil { + c.logger.Error().Err(err).Fields(logFields).Msg("monitorVoteOutboundResult: failed to resend tx") + } else { + c.logger.Info().Fields(logFields).Msg("monitorVoteOutboundResult: successfully resent tx") + } + } + default: + c.logger.Debug().Fields(logFields).Msg("monitorVoteOutboundResult: successful") + } + + return nil +} + +// MonitorVoteInboundResult monitors the result of a vote inbound tx +// retryGasLimit is the gas limit used to resend the tx if it fails because of insufficient gas +// if retryGasLimit is 0, the tx is not resent +func (c *Client) MonitorVoteInboundResult( + ctx context.Context, + zetaTxHash string, + retryGasLimit uint64, + msg *types.MsgVoteInbound, +) error { + defer func() { + if r := recover(); r != nil { + c.logger.Error(). + Interface("panic", r). + Str("inbound.hash", zetaTxHash). + Msg("monitorVoteInboundResult: recovered from panic") + } + }() + + call := func() error { + return retry.Retry(c.monitorVoteInboundResult(ctx, zetaTxHash, retryGasLimit, msg)) + } + + err := retryWithBackoff(call, monitorRetryCount, monitorInterval/2, monitorInterval) + if err != nil { + c.logger.Error().Err(err). + Str("inbound.hash", zetaTxHash). + Msg("monitorVoteInboundResult: unable to query tx result") + + return err + } + + return nil +} + +func (c *Client) monitorVoteInboundResult( + ctx context.Context, + zetaTxHash string, + retryGasLimit uint64, + msg *types.MsgVoteInbound, +) error { + // query tx result from ZetaChain + txResult, err := c.QueryTxResult(zetaTxHash) + if err != nil { + return errors.Wrap(err, "failed to query tx result") + } + + logFields := map[string]any{ + "inbound.hash": zetaTxHash, + "inbound.raw_log": txResult.RawLog, + } + + switch { + case strings.Contains(txResult.RawLog, "failed to execute message"): + // the inbound vote tx shouldn't fail to execute + // this shouldn't happen + c.logger.Error().Fields(logFields).Msg("monitorVoteInboundResult: failed to execute vote") + + case strings.Contains(txResult.RawLog, "out of gas"): + // if the tx fails with an out of gas error, resend the tx with more gas if retryGasLimit > 0 + c.logger.Debug().Fields(logFields).Msg("monitorVoteInboundResult: out of gas") + + if retryGasLimit > 0 { + // new retryGasLimit set to 0 to prevent reentering this function + if _, _, err := c.PostVoteInbound(ctx, retryGasLimit, 0, msg); err != nil { + c.logger.Error().Err(err).Fields(logFields).Msg("monitorVoteInboundResult: failed to resend tx") + } else { + c.logger.Info().Fields(logFields).Msg("monitorVoteInboundResult: successfully resent tx") + } + } + + default: + c.logger.Debug().Fields(logFields).Msgf("monitorVoteInboundResult: successful") + } + + return nil +} + +func retryWithBackoff(call func() error, attempts int, minInternal, maxInterval time.Duration) error { + if attempts < 1 { + return errors.New("attempts must be positive") + } + + bo := backoff.WithMaxRetries( + backoff.NewExponentialBackOff( + backoff.WithInitialInterval(minInternal), + backoff.WithMaxInterval(maxInterval), + ), + // #nosec G701 always positive + uint64(attempts), + ) + + return retry.DoWithBackoff(call, bo) +} diff --git a/zetaclient/zetacore/client_query_authority.go b/zetaclient/zetacore/client_query_authority.go new file mode 100644 index 0000000000..044a3d47ee --- /dev/null +++ b/zetaclient/zetacore/client_query_authority.go @@ -0,0 +1,18 @@ +package zetacore + +import ( + "context" + + "github.com/zeta-chain/zetacore/pkg/chains" + authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" +) + +// GetAdditionalChains returns the additional chains +func (c *Client) GetAdditionalChains(ctx context.Context) ([]chains.Chain, error) { + resp, err := c.client.authority.ChainInfo(ctx, &authoritytypes.QueryGetChainInfoRequest{}) + if err != nil { + return nil, err + } + + return resp.GetChainInfo().Chains, nil +} diff --git a/zetaclient/zetacore/client_query_cosmos.go b/zetaclient/zetacore/client_query_cosmos.go new file mode 100644 index 0000000000..61d690d54a --- /dev/null +++ b/zetaclient/zetacore/client_query_cosmos.go @@ -0,0 +1,88 @@ +package zetacore + +import ( + "context" + "fmt" + + sdkmath "cosmossdk.io/math" + tmhttp "github.com/cometbft/cometbft/rpc/client/http" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/cmd/zetacored/config" +) + +// GetGenesisSupply returns the genesis supply. +// NOTE that this method is brittle as it uses STATEFUL connection +func (c *Client) GetGenesisSupply(ctx context.Context) (sdkmath.Int, error) { + tmURL := fmt.Sprintf("http://%s", c.config.ChainRPC) + + s, err := tmhttp.New(tmURL, "/websocket") + if err != nil { + return sdkmath.ZeroInt(), errors.Wrap(err, "failed to create tm client") + } + + // nolint:errcheck + defer s.Stop() + + res, err := s.Genesis(ctx) + if err != nil { + return sdkmath.ZeroInt(), errors.Wrap(err, "failed to get genesis") + } + + appState, err := genutiltypes.GenesisStateFromGenDoc(*res.Genesis) + if err != nil { + return sdkmath.ZeroInt(), errors.Wrap(err, "failed to get app state") + } + + bankstate := banktypes.GetGenesisStateFromAppState(c.encodingCfg.Codec, appState) + + return bankstate.Supply.AmountOf(config.BaseDenom), nil +} + +// GetUpgradePlan returns the current upgrade plan. +// if there is no active upgrade plan, plan will be nil, err will be nil as well. +func (c *Client) GetUpgradePlan(ctx context.Context) (*upgradetypes.Plan, error) { + in := &upgradetypes.QueryCurrentPlanRequest{} + + resp, err := c.client.upgrade.CurrentPlan(ctx, in) + if err != nil { + return nil, errors.Wrap(err, "failed to get current upgrade plan") + } + + return resp.Plan, nil +} + +// GetZetaTokenSupplyOnNode returns the zeta token supply on the node +func (c *Client) GetZetaTokenSupplyOnNode(ctx context.Context) (sdkmath.Int, error) { + in := &banktypes.QuerySupplyOfRequest{Denom: config.BaseDenom} + + resp, err := c.client.bank.SupplyOf(ctx, in) + if err != nil { + return sdkmath.ZeroInt(), errors.Wrap(err, "failed to get zeta token supply") + } + + return resp.GetAmount().Amount, nil +} + +// GetZetaHotKeyBalance returns the zeta hot key balance +func (c *Client) GetZetaHotKeyBalance(ctx context.Context) (sdkmath.Int, error) { + address, err := c.keys.GetAddress() + if err != nil { + return sdkmath.ZeroInt(), errors.Wrap(err, "failed to get address") + } + + in := &banktypes.QueryBalanceRequest{ + Address: address.String(), + Denom: config.BaseDenom, + } + + resp, err := c.client.bank.Balance(ctx, in) + if err != nil { + return sdkmath.ZeroInt(), errors.Wrap(err, "failed to get zeta hot key balance") + } + + return resp.Balance.Amount, nil +} diff --git a/zetaclient/zetacore/client_query_crosschain.go b/zetaclient/zetacore/client_query_crosschain.go new file mode 100644 index 0000000000..262b766a6b --- /dev/null +++ b/zetaclient/zetacore/client_query_crosschain.go @@ -0,0 +1,194 @@ +package zetacore + +import ( + "context" + "sort" + + "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/types/query" + "google.golang.org/grpc" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/x/crosschain/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" +) + +// 32MB +var maxSizeOption = grpc.MaxCallRecvMsgSize(32 * 1024 * 1024) + +// GetLastBlockHeight returns the zetachain block height +func (c *Client) GetLastBlockHeight(ctx context.Context) (uint64, error) { + resp, err := c.client.crosschain.LastBlockHeight(ctx, &types.QueryGetLastBlockHeightRequest{}) + if err != nil { + return 0, errors.Wrap(err, "failed to get block height") + } + + return resp.GetLastBlockHeight().LastInboundHeight, nil +} + +// GetBlockHeight returns the zetachain block height +func (c *Client) GetBlockHeight(ctx context.Context) (int64, error) { + resp, err := c.client.crosschain.LastZetaHeight(ctx, &types.QueryLastZetaHeightRequest{}) + if err != nil { + return 0, err + } + + return resp.Height, nil +} + +// GetAbortedZetaAmount returns the amount of zeta that has been aborted +func (c *Client) GetAbortedZetaAmount(ctx context.Context) (string, error) { + resp, err := c.client.crosschain.ZetaAccounting(ctx, &types.QueryZetaAccountingRequest{}) + if err != nil { + return "", errors.Wrap(err, "failed to get aborted zeta amount") + } + + return resp.AbortedZetaAmount, nil +} + +// GetRateLimiterFlags returns the rate limiter flags +func (c *Client) GetRateLimiterFlags(ctx context.Context) (types.RateLimiterFlags, error) { + resp, err := c.client.crosschain.RateLimiterFlags(ctx, &types.QueryRateLimiterFlagsRequest{}) + if err != nil { + return types.RateLimiterFlags{}, errors.Wrap(err, "failed to get rate limiter flags") + } + + return resp.RateLimiterFlags, nil +} + +// GetRateLimiterInput returns input data for the rate limit checker +func (c *Client) GetRateLimiterInput(ctx context.Context, window int64) (*types.QueryRateLimiterInputResponse, error) { + in := &types.QueryRateLimiterInputRequest{Window: window} + + resp, err := c.client.crosschain.RateLimiterInput(ctx, in, maxSizeOption) + if err != nil { + return nil, errors.Wrap(err, "failed to get rate limiter input") + } + + return resp, nil +} + +// GetAllCctx returns all cross chain transactions +func (c *Client) GetAllCctx(ctx context.Context) ([]*types.CrossChainTx, error) { + resp, err := c.client.crosschain.CctxAll(ctx, &types.QueryAllCctxRequest{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get all cross chain transactions") + } + + return resp.CrossChainTx, nil +} + +func (c *Client) GetCctxByHash(ctx context.Context, sendHash string) (*types.CrossChainTx, error) { + in := &types.QueryGetCctxRequest{Index: sendHash} + resp, err := c.client.crosschain.Cctx(ctx, in) + if err != nil { + return nil, errors.Wrap(err, "failed to get cctx by hash") + } + + return resp.CrossChainTx, nil +} + +// GetCctxByNonce returns a cross chain transaction by nonce +func (c *Client) GetCctxByNonce(ctx context.Context, chainID int64, nonce uint64) (*types.CrossChainTx, error) { + resp, err := c.client.crosschain.CctxByNonce(ctx, &types.QueryGetCctxByNonceRequest{ + ChainID: chainID, + Nonce: nonce, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get cctx by nonce") + } + + return resp.CrossChainTx, nil +} + +// ListPendingCCTXWithinRateLimit returns a list of pending cctxs that do not exceed the outbound rate limit +// - The max size of the list is crosschainkeeper.MaxPendingCctxs +// - The returned `rateLimitExceeded` flag indicates if the rate limit is exceeded or not +func (c *Client) ListPendingCCTXWithinRateLimit( + ctx context.Context, +) (*types.QueryListPendingCctxWithinRateLimitResponse, error) { + in := &types.QueryListPendingCctxWithinRateLimitRequest{} + + resp, err := c.client.crosschain.ListPendingCctxWithinRateLimit(ctx, in, maxSizeOption) + if err != nil { + return nil, errors.Wrap(err, "failed to get pending cctxs within rate limit") + } + + return resp, nil +} + +// ListPendingCCTX returns a list of pending cctxs for a given chainID +// - The max size of the list is crosschainkeeper.MaxPendingCctxs +func (c *Client) ListPendingCCTX(ctx context.Context, chainID int64) ([]*types.CrossChainTx, uint64, error) { + in := &types.QueryListPendingCctxRequest{ChainId: chainID} + + resp, err := c.client.crosschain.ListPendingCctx(ctx, in, maxSizeOption) + if err != nil { + return nil, 0, errors.Wrap(err, "failed to get pending cctxs") + } + + return resp.CrossChainTx, resp.TotalPending, nil +} + +// GetOutboundTracker returns the outbound tracker for a chain and nonce +func (c *Client) GetOutboundTracker( + ctx context.Context, + chain chains.Chain, + nonce uint64, +) (*types.OutboundTracker, error) { + in := &types.QueryGetOutboundTrackerRequest{ChainID: chain.ChainId, Nonce: nonce} + + resp, err := c.client.crosschain.OutboundTracker(ctx, in) + if err != nil { + return nil, err + } + + return &resp.OutboundTracker, nil +} + +// GetInboundTrackersForChain returns the inbound trackers for a chain +func (c *Client) GetInboundTrackersForChain(ctx context.Context, chainID int64) ([]types.InboundTracker, error) { + in := &types.QueryAllInboundTrackerByChainRequest{ChainId: chainID} + + resp, err := c.client.crosschain.InboundTrackerAllByChain(ctx, in) + if err != nil { + return nil, err + } + + return resp.InboundTracker, nil +} + +// GetAllOutboundTrackerByChain returns all outbound trackers for a chain +func (c *Client) GetAllOutboundTrackerByChain( + ctx context.Context, + chainID int64, + order interfaces.Order, +) ([]types.OutboundTracker, error) { + in := &types.QueryAllOutboundTrackerByChainRequest{ + Chain: chainID, + Pagination: &query.PageRequest{ + Key: nil, + Offset: 0, + Limit: 2000, + CountTotal: false, + Reverse: false, + }, + } + + resp, err := c.client.crosschain.OutboundTrackerAllByChain(ctx, in) + if err != nil { + return nil, errors.Wrap(err, "failed to get all outbound trackers") + } + + if order == interfaces.Ascending { + sort.SliceStable(resp.OutboundTracker, func(i, j int) bool { + return resp.OutboundTracker[i].Nonce < resp.OutboundTracker[j].Nonce + }) + } else if order == interfaces.Descending { + sort.SliceStable(resp.OutboundTracker, func(i, j int) bool { + return resp.OutboundTracker[i].Nonce > resp.OutboundTracker[j].Nonce + }) + } + + return resp.OutboundTracker, nil +} diff --git a/zetaclient/zetacore/client_query_ethermint.go b/zetaclient/zetacore/client_query_ethermint.go new file mode 100644 index 0000000000..edb8987360 --- /dev/null +++ b/zetaclient/zetacore/client_query_ethermint.go @@ -0,0 +1,23 @@ +package zetacore + +import ( + "context" + "fmt" + + "cosmossdk.io/errors" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" +) + +// GetBaseGasPrice returns the base gas price +func (c *Client) GetBaseGasPrice(ctx context.Context) (int64, error) { + resp, err := c.client.fees.Params(ctx, &feemarkettypes.QueryParamsRequest{}) + if err != nil { + return 0, errors.Wrap(err, "failed to get base gas price") + } + + if resp.Params.BaseFee.IsNil() { + return 0, fmt.Errorf("base fee is nil") + } + + return resp.Params.BaseFee.Int64(), nil +} diff --git a/zetaclient/zetacore/client_query_lightclient.go b/zetaclient/zetacore/client_query_lightclient.go new file mode 100644 index 0000000000..f5999a5d2a --- /dev/null +++ b/zetaclient/zetacore/client_query_lightclient.go @@ -0,0 +1,57 @@ +package zetacore + +import ( + "context" + + "cosmossdk.io/errors" + + "github.com/zeta-chain/zetacore/pkg/proofs" + "github.com/zeta-chain/zetacore/x/lightclient/types" +) + +// GetBlockHeaderEnabledChains returns the enabled chains for block headers +func (c *Client) GetBlockHeaderEnabledChains(ctx context.Context) ([]types.HeaderSupportedChain, error) { + resp, err := c.client.light.HeaderEnabledChains(ctx, &types.QueryHeaderEnabledChainsRequest{}) + if err != nil { + return []types.HeaderSupportedChain{}, err + } + + return resp.HeaderEnabledChains, nil +} + +// GetBlockHeaderChainState returns the block header chain state +func (c *Client) GetBlockHeaderChainState(ctx context.Context, chainID int64) (*types.ChainState, error) { + in := &types.QueryGetChainStateRequest{ChainId: chainID} + + resp, err := c.client.light.ChainState(ctx, in) + if err != nil { + return nil, errors.Wrap(err, "failed to get chain state") + } + + return resp.ChainState, nil +} + +// Prove returns whether a proof is valid +func (c *Client) Prove( + ctx context.Context, + blockHash string, + txHash string, + txIndex int64, + proof *proofs.Proof, + chainID int64, +) (bool, error) { + in := &types.QueryProveRequest{ + BlockHash: blockHash, + TxIndex: txIndex, + Proof: proof, + ChainId: chainID, + TxHash: txHash, + } + + resp, err := c.client.light.Prove(ctx, in) + if err != nil { + return false, errors.Wrap(err, "failed to prove") + } + + return resp.Valid, nil +} diff --git a/zetaclient/zetacore/client_query_observer.go b/zetaclient/zetacore/client_query_observer.go new file mode 100644 index 0000000000..da4fcbad7a --- /dev/null +++ b/zetaclient/zetacore/client_query_observer.go @@ -0,0 +1,215 @@ +package zetacore + +import ( + "context" + + "cosmossdk.io/errors" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/retry" + "github.com/zeta-chain/zetacore/x/observer/types" +) + +// GetCrosschainFlags returns the crosschain flags +func (c *Client) GetCrosschainFlags(ctx context.Context) (types.CrosschainFlags, error) { + resp, err := c.client.observer.CrosschainFlags(ctx, &types.QueryGetCrosschainFlagsRequest{}) + if err != nil { + return types.CrosschainFlags{}, err + } + + return resp.CrosschainFlags, nil +} + +// GetSupportedChains returns the supported chains +func (c *Client) GetSupportedChains(ctx context.Context) ([]chains.Chain, error) { + resp, err := c.client.observer.SupportedChains(ctx, &types.QuerySupportedChains{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get supported chains") + } + + return resp.GetChains(), nil +} + +// GetChainParams returns all the chain params +func (c *Client) GetChainParams(ctx context.Context) ([]*types.ChainParams, error) { + in := &types.QueryGetChainParamsRequest{} + + resp, err := retry.DoTypedWithRetry(func() (*types.QueryGetChainParamsResponse, error) { + return c.client.observer.GetChainParams(ctx, in) + }) + + if err != nil { + return nil, errors.Wrap(err, "failed to get chain params") + } + + return resp.ChainParams.ChainParams, nil +} + +// GetChainParamsForChainID returns the chain params for a given chain ID +func (c *Client) GetChainParamsForChainID( + ctx context.Context, + externalChainID int64, +) (*types.ChainParams, error) { + in := &types.QueryGetChainParamsForChainRequest{ChainId: externalChainID} + + resp, err := c.client.observer.GetChainParamsForChain(ctx, in) + if err != nil { + return &types.ChainParams{}, err + } + + return resp.ChainParams, nil +} + +// GetObserverList returns the list of observers +func (c *Client) GetObserverList(ctx context.Context) ([]string, error) { + in := &types.QueryObserverSet{} + + resp, err := retry.DoTypedWithRetry(func() (*types.QueryObserverSetResponse, error) { + return c.client.observer.ObserverSet(ctx, in) + }) + + if err != nil { + return nil, errors.Wrap(err, "failed to get observer list") + } + + return resp.Observers, nil +} + +// GetBallotByID returns a ballot by ID +func (c *Client) GetBallotByID(ctx context.Context, id string) (*types.QueryBallotByIdentifierResponse, error) { + in := &types.QueryBallotByIdentifierRequest{BallotIdentifier: id} + + return c.client.observer.BallotByIdentifier(ctx, in) +} + +// GetNonceByChain returns the nonce by chain +func (c *Client) GetNonceByChain(ctx context.Context, chain chains.Chain) (types.ChainNonces, error) { + in := &types.QueryGetChainNoncesRequest{Index: chain.ChainName.String()} + + resp, err := c.client.observer.ChainNonces(ctx, in) + if err != nil { + return types.ChainNonces{}, errors.Wrap(err, "failed to get nonce by chain") + } + + return resp.ChainNonces, nil +} + +// GetKeyGen returns the keygen +func (c *Client) GetKeyGen(ctx context.Context) (*types.Keygen, error) { + in := &types.QueryGetKeygenRequest{} + + resp, err := retry.DoTypedWithRetry(func() (*types.QueryGetKeygenResponse, error) { + return c.client.observer.Keygen(ctx, in) + }) + + if err != nil { + return nil, errors.Wrap(err, "failed to get keygen") + } + + return resp.GetKeygen(), nil +} + +// GetAllNodeAccounts returns all node accounts +func (c *Client) GetAllNodeAccounts(ctx context.Context) ([]*types.NodeAccount, error) { + resp, err := c.client.observer.NodeAccountAll(ctx, &types.QueryAllNodeAccountRequest{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get all node accounts") + } + + c.logger.Debug().Int("node_account.len", len(resp.NodeAccount)).Msg("GetAllNodeAccounts: OK") + + return resp.NodeAccount, nil +} + +// GetBallot returns a ballot by ID +func (c *Client) GetBallot( + ctx context.Context, + ballotIdentifier string, +) (*types.QueryBallotByIdentifierResponse, error) { + in := &types.QueryBallotByIdentifierRequest{BallotIdentifier: ballotIdentifier} + + resp, err := c.client.observer.BallotByIdentifier(ctx, in) + if err != nil { + return nil, errors.Wrap(err, "failed to get ballot") + } + + return resp, nil +} + +// GetCurrentTSS returns the current TSS +func (c *Client) GetCurrentTSS(ctx context.Context) (types.TSS, error) { + resp, err := c.client.observer.TSS(ctx, &types.QueryGetTSSRequest{}) + if err != nil { + return types.TSS{}, errors.Wrap(err, "failed to get current tss") + } + + return resp.TSS, nil +} + +// GetEVMTSSAddress returns the EVM TSS address. +func (c *Client) GetEVMTSSAddress(ctx context.Context) (string, error) { + resp, err := c.client.observer.GetTssAddress(ctx, &types.QueryGetTssAddressRequest{}) + if err != nil { + return "", errors.Wrap(err, "failed to get eth tss address") + } + + return resp.Eth, nil +} + +// GetBTCTSSAddress returns the BTC TSS address +func (c *Client) GetBTCTSSAddress(ctx context.Context, chainID int64) (string, error) { + in := &types.QueryGetTssAddressRequest{BitcoinChainId: chainID} + + resp, err := c.client.observer.GetTssAddress(ctx, in) + if err != nil { + return "", errors.Wrap(err, "failed to get btc tss address") + } + return resp.Btc, nil +} + +// GetTSSHistory returns the TSS history +func (c *Client) GetTSSHistory(ctx context.Context) ([]types.TSS, error) { + resp, err := c.client.observer.TssHistory(ctx, &types.QueryTssHistoryRequest{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get tss history") + } + + return resp.TssList, nil +} + +// GetPendingNonces returns the pending nonces +func (c *Client) GetPendingNonces(ctx context.Context) (*types.QueryAllPendingNoncesResponse, error) { + resp, err := c.client.observer.PendingNoncesAll(ctx, &types.QueryAllPendingNoncesRequest{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get pending nonces") + } + + return resp, nil +} + +// GetPendingNoncesByChain returns the pending nonces for a chain and current tss address +func (c *Client) GetPendingNoncesByChain(ctx context.Context, chainID int64) (types.PendingNonces, error) { + in := &types.QueryPendingNoncesByChainRequest{ChainId: chainID} + + resp, err := c.client.observer.PendingNoncesByChain(ctx, in) + if err != nil { + return types.PendingNonces{}, errors.Wrap(err, "failed to get pending nonces by chain") + } + + return resp.PendingNonces, nil +} + +// HasVoted returns whether an observer has voted +func (c *Client) HasVoted(ctx context.Context, ballotIndex string, voterAddress string) (bool, error) { + in := &types.QueryHasVotedRequest{ + BallotIdentifier: ballotIndex, + VoterAddress: voterAddress, + } + + resp, err := c.client.observer.HasVoted(ctx, in) + if err != nil { + return false, errors.Wrap(err, "failed to check if observer has voted") + } + + return resp.HasVoted, nil +} diff --git a/zetaclient/zetacore/client_query_tendermint.go b/zetaclient/zetacore/client_query_tendermint.go new file mode 100644 index 0000000000..adb38d0bab --- /dev/null +++ b/zetaclient/zetacore/client_query_tendermint.go @@ -0,0 +1,35 @@ +package zetacore + +import ( + "context" + + "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + + "github.com/zeta-chain/zetacore/pkg/retry" +) + +// GetLatestZetaBlock returns the latest zeta block +func (c *Client) GetLatestZetaBlock(ctx context.Context) (*tmservice.Block, error) { + res, err := c.client.tendermint.GetLatestBlock(ctx, &tmservice.GetLatestBlockRequest{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get latest zeta block") + } + + return res.SdkBlock, nil +} + +// GetNodeInfo returns the node info +func (c *Client) GetNodeInfo(ctx context.Context) (*tmservice.GetNodeInfoResponse, error) { + var err error + + res, err := retry.DoTypedWithRetry(func() (*tmservice.GetNodeInfoResponse, error) { + return c.client.tendermint.GetNodeInfo(ctx, &tmservice.GetNodeInfoRequest{}) + }) + + if err != nil { + return nil, errors.Wrap(err, "failed to get node info") + } + + return res, nil +} diff --git a/zetaclient/zetacore/query_test.go b/zetaclient/zetacore/client_query_test.go similarity index 57% rename from zetaclient/zetacore/query_test.go rename to zetaclient/zetacore/client_query_test.go index baba533cfa..35dcc43eec 100644 --- a/zetaclient/zetacore/query_test.go +++ b/zetaclient/zetacore/client_query_test.go @@ -1,18 +1,27 @@ package zetacore import ( - authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" + "context" "net" "testing" + authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" + + abci "github.com/cometbft/cometbft/abci/types" tmtypes "github.com/cometbft/cometbft/proto/tendermint/types" + cosmosclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + "github.com/cosmos/cosmos-sdk/testutil/mock" "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" + "github.com/golang/mock/gomock" + "github.com/rs/zerolog" "github.com/stretchr/testify/require" + keyinterfaces "github.com/zeta-chain/zetacore/zetaclient/keys/interfaces" "go.nhat.io/grpcmock" "go.nhat.io/grpcmock/planner" @@ -20,50 +29,147 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" - crosschainTypes "github.com/zeta-chain/zetacore/x/crosschain/types" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/keys" - "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -func setupMockServer(t *testing.T, serviceFunc any, method string, input any, expectedOutput any) *grpcmock.Server { +const skipMethod = "skip" + +// setupMockServer setup mock zetacore GRPC server +func setupMockServer( + t *testing.T, + serviceFunc any, method string, input any, expectedOutput any, + extra ...grpcmock.ServerOption, +) *grpcmock.Server { listener, err := net.Listen("tcp", "127.0.0.1:9090") require.NoError(t, err) - server := grpcmock.MockUnstartedServer( + opts := []grpcmock.ServerOption{ grpcmock.RegisterService(serviceFunc), grpcmock.WithPlanner(planner.FirstMatch()), grpcmock.WithListener(listener), - func(s *grpcmock.Server) { + } + + opts = append(opts, extra...) + + if method != skipMethod { + opts = append(opts, func(s *grpcmock.Server) { s.ExpectUnary(method). UnlimitedTimes(). WithPayload(input). Return(expectedOutput) - }, - )(t) + }) + } + + server := grpcmock.MockUnstartedServer(opts...)(t) + + server.Serve() + + t.Cleanup(func() { + require.NoError(t, server.Close()) + }) return server } -func closeMockServer(t *testing.T, server *grpcmock.Server) { - err := server.Close() - require.NoError(t, err) +func withDummyServer(zetaBlockHeight int64) []grpcmock.ServerOption { + return []grpcmock.ServerOption{ + grpcmock.RegisterService(crosschaintypes.RegisterQueryServer), + grpcmock.RegisterService(crosschaintypes.RegisterMsgServer), + grpcmock.RegisterService(feemarkettypes.RegisterQueryServer), + grpcmock.RegisterService(authtypes.RegisterQueryServer), + grpcmock.RegisterService(abci.RegisterABCIApplicationServer), + func(s *grpcmock.Server) { + // Block Height + s.ExpectUnary("/zetachain.zetacore.crosschain.Query/LastZetaHeight"). + UnlimitedTimes(). + Return(crosschaintypes.QueryLastZetaHeightResponse{Height: zetaBlockHeight}) + + // London Base Fee + s.ExpectUnary("/ethermint.feemarket.v1.Query/Params"). + UnlimitedTimes(). + Return(feemarkettypes.QueryParamsResponse{ + Params: feemarkettypes.Params{BaseFee: types.NewInt(100)}, + }) + }, + } } -func setupZetacoreClient() (*Client, error) { - return NewClient( - &keys.Keys{}, - "127.0.0.1", - testSigner, - "zetachain_7000-1", +type clientTestConfig struct { + keys keyinterfaces.ObserverKeys + opts []Opt +} + +type clientTestOpt func(*clientTestConfig) + +func withObserverKeys(keys keyinterfaces.ObserverKeys) clientTestOpt { + return func(cfg *clientTestConfig) { cfg.keys = keys } +} + +func withDefaultObserverKeys() clientTestOpt { + var ( + key = mocks.TestKeyringPair + address = types.AccAddress(key.PubKey().Address().Bytes()) + keyRing = mocks.NewKeyring() + ) + + return withObserverKeys(keys.NewKeysWithKeybase(keyRing, address, testSigner, "")) +} + +func withTendermint(client cosmosclient.TendermintRPC) clientTestOpt { + return func(cfg *clientTestConfig) { cfg.opts = append(cfg.opts, WithTendermintClient(client)) } +} + +func withAccountRetriever(t *testing.T, accNum uint64, accSeq uint64) clientTestOpt { + ctrl := gomock.NewController(t) + ac := mock.NewMockAccountRetriever(ctrl) + ac.EXPECT(). + GetAccountNumberSequence(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(accNum, accSeq, nil) + + return func(cfg *clientTestConfig) { + cfg.opts = append(cfg.opts, WithCustomAccountRetriever(ac)) + } +} + +func setupZetacoreClient(t *testing.T, opts ...clientTestOpt) *Client { + const ( + chainIP = "127.0.0.1" + signer = testSigner + chainID = "zetachain_7000-1" + ) + + var cfg clientTestConfig + for _, opt := range opts { + opt(&cfg) + } + + if cfg.keys == nil { + cfg.keys = &keys.Keys{} + } + + c, err := NewClient( + cfg.keys, + chainIP, signer, + chainID, false, - &metrics.TelemetryServer{}) + zerolog.Nop(), + cfg.opts..., + ) + + require.NoError(t, err) + + return c } func TestZetacore_GetBallot(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryBallotByIdentifierResponse{ BallotIdentifier: "123", Voters: nil, @@ -72,19 +178,18 @@ func TestZetacore_GetBallot(t *testing.T) { } input := observertypes.QueryBallotByIdentifierRequest{BallotIdentifier: "123"} method := "/zetachain.zetacore.observer.Query/BallotByIdentifier" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetBallotByID("123") + resp, err := client.GetBallotByID(ctx, "123") require.NoError(t, err) require.Equal(t, expectedOutput, *resp) } func TestZetacore_GetCrosschainFlags(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryGetCrosschainFlagsResponse{CrosschainFlags: observertypes.CrosschainFlags{ IsInboundEnabled: true, IsOutboundEnabled: false, @@ -92,42 +197,40 @@ func TestZetacore_GetCrosschainFlags(t *testing.T) { }} input := observertypes.QueryGetCrosschainFlagsRequest{} method := "/zetachain.zetacore.observer.Query/CrosschainFlags" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetCrosschainFlags() + resp, err := client.GetCrosschainFlags(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.CrosschainFlags, resp) } func TestZetacore_GetRateLimiterFlags(t *testing.T) { + ctx := context.Background() + // create sample flags rateLimiterFlags := sample.RateLimiterFlags() - expectedOutput := crosschainTypes.QueryRateLimiterFlagsResponse{ + expectedOutput := crosschaintypes.QueryRateLimiterFlagsResponse{ RateLimiterFlags: rateLimiterFlags, } // setup mock server - input := crosschainTypes.QueryRateLimiterFlagsRequest{} + input := crosschaintypes.QueryRateLimiterFlagsRequest{} method := "/zetachain.zetacore.crosschain.Query/RateLimiterFlags" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) // query - resp, err := client.GetRateLimiterFlags() + resp, err := client.GetRateLimiterFlags(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.RateLimiterFlags, resp) } func TestZetacore_HeaderEnabledChains(t *testing.T) { + ctx := context.Background() + expectedOutput := lightclienttypes.QueryHeaderEnabledChainsResponse{ HeaderEnabledChains: []lightclienttypes.HeaderSupportedChain{ { @@ -142,19 +245,18 @@ func TestZetacore_HeaderEnabledChains(t *testing.T) { } input := lightclienttypes.QueryHeaderEnabledChainsRequest{} method := "/zetachain.zetacore.lightclient.Query/HeaderEnabledChains" - server := setupMockServer(t, lightclienttypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, lightclienttypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetBlockHeaderEnabledChains() + resp, err := client.GetBlockHeaderEnabledChains(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.HeaderEnabledChains, resp) } func TestZetacore_GetChainParamsForChainID(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryGetChainParamsForChainResponse{ChainParams: &observertypes.ChainParams{ ChainId: 123, BallotThreshold: types.ZeroDec(), @@ -162,19 +264,18 @@ func TestZetacore_GetChainParamsForChainID(t *testing.T) { }} input := observertypes.QueryGetChainParamsForChainRequest{ChainId: 123} method := "/zetachain.zetacore.observer.Query/GetChainParamsForChain" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetChainParamsForChainID(123) + resp, err := client.GetChainParamsForChainID(ctx, 123) require.NoError(t, err) require.Equal(t, expectedOutput.ChainParams, resp) } func TestZetacore_GetChainParams(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryGetChainParamsResponse{ChainParams: &observertypes.ChainParamsList{ ChainParams: []*observertypes.ChainParams{ { @@ -186,19 +287,18 @@ func TestZetacore_GetChainParams(t *testing.T) { }} input := observertypes.QueryGetChainParamsRequest{} method := "/zetachain.zetacore.observer.Query/GetChainParams" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetChainParams() + resp, err := client.GetChainParams(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.ChainParams.ChainParams, resp) } func TestZetacore_GetUpgradePlan(t *testing.T) { + ctx := context.Background() + expectedOutput := upgradetypes.QueryCurrentPlanResponse{ Plan: &upgradetypes.Plan{ Name: "big upgrade", @@ -207,83 +307,79 @@ func TestZetacore_GetUpgradePlan(t *testing.T) { } input := upgradetypes.QueryCurrentPlanRequest{} method := "/cosmos.upgrade.v1beta1.Query/CurrentPlan" - server := setupMockServer(t, upgradetypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, upgradetypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetUpgradePlan() + resp, err := client.GetUpgradePlan(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.Plan, resp) } func TestZetacore_GetAllCctx(t *testing.T) { - expectedOutput := crosschainTypes.QueryAllCctxResponse{ - CrossChainTx: []*crosschainTypes.CrossChainTx{ + ctx := context.Background() + + expectedOutput := crosschaintypes.QueryAllCctxResponse{ + CrossChainTx: []*crosschaintypes.CrossChainTx{ { Index: "cross-chain4456", }, }, Pagination: nil, } - input := crosschainTypes.QueryAllCctxRequest{} + input := crosschaintypes.QueryAllCctxRequest{} method := "/zetachain.zetacore.crosschain.Query/CctxAll" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetAllCctx() + resp, err := client.GetAllCctx(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.CrossChainTx, resp) } func TestZetacore_GetCctxByHash(t *testing.T) { - expectedOutput := crosschainTypes.QueryGetCctxResponse{CrossChainTx: &crosschainTypes.CrossChainTx{ + ctx := context.Background() + + expectedOutput := crosschaintypes.QueryGetCctxResponse{CrossChainTx: &crosschaintypes.CrossChainTx{ Index: "9c8d02b6956b9c78ecb6090a8160faaa48e7aecfd0026fcdf533721d861436a3", }} - input := crosschainTypes.QueryGetCctxRequest{ + input := crosschaintypes.QueryGetCctxRequest{ Index: "9c8d02b6956b9c78ecb6090a8160faaa48e7aecfd0026fcdf533721d861436a3", } method := "/zetachain.zetacore.crosschain.Query/Cctx" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetCctxByHash("9c8d02b6956b9c78ecb6090a8160faaa48e7aecfd0026fcdf533721d861436a3") + resp, err := client.GetCctxByHash(ctx, "9c8d02b6956b9c78ecb6090a8160faaa48e7aecfd0026fcdf533721d861436a3") require.NoError(t, err) require.Equal(t, expectedOutput.CrossChainTx, resp) } func TestZetacore_GetCctxByNonce(t *testing.T) { - expectedOutput := crosschainTypes.QueryGetCctxResponse{CrossChainTx: &crosschainTypes.CrossChainTx{ + ctx := context.Background() + + expectedOutput := crosschaintypes.QueryGetCctxResponse{CrossChainTx: &crosschaintypes.CrossChainTx{ Index: "9c8d02b6956b9c78ecb6090a8160faaa48e7aecfd0026fcdf533721d861436a3", }} - input := crosschainTypes.QueryGetCctxByNonceRequest{ + input := crosschaintypes.QueryGetCctxByNonceRequest{ ChainID: 7000, Nonce: 55, } method := "/zetachain.zetacore.crosschain.Query/CctxByNonce" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetCctxByNonce(7000, 55) + resp, err := client.GetCctxByNonce(ctx, 7000, 55) require.NoError(t, err) require.Equal(t, expectedOutput.CrossChainTx, resp) } func TestZetacore_GetObserverList(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryObserverSetResponse{ Observers: []string{ "zeta19jr7nl82lrktge35f52x9g5y5prmvchmk40zhg", @@ -293,78 +389,72 @@ func TestZetacore_GetObserverList(t *testing.T) { } input := observertypes.QueryObserverSet{} method := "/zetachain.zetacore.observer.Query/ObserverSet" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetObserverList() + resp, err := client.GetObserverList(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.Observers, resp) } func TestZetacore_GetRateLimiterInput(t *testing.T) { - expectedOutput := crosschainTypes.QueryRateLimiterInputResponse{ + ctx := context.Background() + + expectedOutput := &crosschaintypes.QueryRateLimiterInputResponse{ Height: 10, - CctxsMissed: []*crosschainTypes.CrossChainTx{sample.CrossChainTx(t, "1-1")}, - CctxsPending: []*crosschainTypes.CrossChainTx{sample.CrossChainTx(t, "1-2")}, + CctxsMissed: []*crosschaintypes.CrossChainTx{sample.CrossChainTx(t, "1-1")}, + CctxsPending: []*crosschaintypes.CrossChainTx{sample.CrossChainTx(t, "1-2")}, TotalPending: 1, PastCctxsValue: "123456", PendingCctxsValue: "1234", LowestPendingCctxHeight: 2, } - input := crosschainTypes.QueryRateLimiterInputRequest{Window: 10} + input := crosschaintypes.QueryRateLimiterInputRequest{Window: 10} method := "/zetachain.zetacore.crosschain.Query/RateLimiterInput" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetRateLimiterInput(10) + resp, err := client.GetRateLimiterInput(ctx, 10) require.NoError(t, err) require.Equal(t, expectedOutput, resp) } func TestZetacore_ListPendingCctx(t *testing.T) { - expectedOutput := crosschainTypes.QueryListPendingCctxResponse{ - CrossChainTx: []*crosschainTypes.CrossChainTx{ + ctx := context.Background() + + expectedOutput := crosschaintypes.QueryListPendingCctxResponse{ + CrossChainTx: []*crosschaintypes.CrossChainTx{ { Index: "cross-chain4456", }, }, TotalPending: 1, } - input := crosschainTypes.QueryListPendingCctxRequest{ChainId: 7000} + input := crosschaintypes.QueryListPendingCctxRequest{ChainId: 7000} method := "/zetachain.zetacore.crosschain.Query/ListPendingCctx" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, totalPending, err := client.ListPendingCctx(7000) + resp, totalPending, err := client.ListPendingCCTX(ctx, 7000) require.NoError(t, err) require.Equal(t, expectedOutput.CrossChainTx, resp) require.Equal(t, expectedOutput.TotalPending, totalPending) } func TestZetacore_GetAbortedZetaAmount(t *testing.T) { - expectedOutput := crosschainTypes.QueryZetaAccountingResponse{AbortedZetaAmount: "1080999"} - input := crosschainTypes.QueryZetaAccountingRequest{} + ctx := context.Background() + + expectedOutput := crosschaintypes.QueryZetaAccountingResponse{AbortedZetaAmount: "1080999"} + input := crosschaintypes.QueryZetaAccountingRequest{} method := "/zetachain.zetacore.crosschain.Query/ZetaAccounting" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetAbortedZetaAmount() + resp, err := client.GetAbortedZetaAmount(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.AbortedZetaAmount, resp) } @@ -374,6 +464,8 @@ func TestZetacore_GetGenesisSupply(t *testing.T) { } func TestZetacore_GetZetaTokenSupplyOnNode(t *testing.T) { + ctx := context.Background() + expectedOutput := banktypes.QuerySupplyOfResponse{ Amount: types.Coin{ Denom: config.BaseDenom, @@ -381,46 +473,38 @@ func TestZetacore_GetZetaTokenSupplyOnNode(t *testing.T) { }} input := banktypes.QuerySupplyOfRequest{Denom: config.BaseDenom} method := "/cosmos.bank.v1beta1.Query/SupplyOf" - server := setupMockServer(t, banktypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, banktypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetZetaTokenSupplyOnNode() + resp, err := client.GetZetaTokenSupplyOnNode(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.GetAmount().Amount, resp) } -func TestZetacore_GetLastBlockHeight(t *testing.T) { - expectedOutput := crosschainTypes.QueryAllLastBlockHeightResponse{ - LastBlockHeight: []*crosschainTypes.LastBlockHeight{ - { - Index: "test12345", - Chain: "7000", - LastOutboundHeight: 32345, - LastInboundHeight: 23623, - }, - }, - } - input := crosschainTypes.QueryAllLastBlockHeightRequest{} - method := "/zetachain.zetacore.crosschain.Query/LastBlockHeightAll" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) +func TestZetacore_GetBlockHeight(t *testing.T) { + ctx := context.Background() - client, err := setupZetacoreClient() - require.NoError(t, err) + method := "/zetachain.zetacore.crosschain.Query/LastZetaHeight" + input := &crosschaintypes.QueryLastZetaHeightRequest{} + output := &crosschaintypes.QueryLastZetaHeightResponse{Height: 12345} + + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, output) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + ) t.Run("last block height", func(t *testing.T) { - resp, err := client.GetLastBlockHeight() + height, err := client.GetBlockHeight(ctx) require.NoError(t, err) - require.Equal(t, expectedOutput.LastBlockHeight, resp) + require.Equal(t, int64(12345), height) }) } func TestZetacore_GetLatestZetaBlock(t *testing.T) { + ctx := context.Background() + expectedOutput := tmservice.GetLatestBlockResponse{ SdkBlock: &tmservice.Block{ Header: tmservice.Header{}, @@ -431,56 +515,36 @@ func TestZetacore_GetLatestZetaBlock(t *testing.T) { } input := tmservice.GetLatestBlockRequest{} method := "/cosmos.base.tendermint.v1beta1.Service/GetLatestBlock" - server := setupMockServer(t, tmservice.RegisterServiceServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, tmservice.RegisterServiceServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetLatestZetaBlock() + resp, err := client.GetLatestZetaBlock(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.SdkBlock, resp) } func TestZetacore_GetNodeInfo(t *testing.T) { + ctx := context.Background() + expectedOutput := tmservice.GetNodeInfoResponse{ DefaultNodeInfo: nil, ApplicationVersion: &tmservice.VersionInfo{}, } input := tmservice.GetNodeInfoRequest{} method := "/cosmos.base.tendermint.v1beta1.Service/GetNodeInfo" - server := setupMockServer(t, tmservice.RegisterServiceServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, tmservice.RegisterServiceServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetNodeInfo() + resp, err := client.GetNodeInfo(ctx) require.NoError(t, err) require.Equal(t, expectedOutput, *resp) } -func TestZetacore_GetZetaBlockHeight(t *testing.T) { - expectedOutput := crosschainTypes.QueryLastZetaHeightResponse{Height: 12345} - input := crosschainTypes.QueryLastZetaHeightRequest{} - method := "/zetachain.zetacore.crosschain.Query/LastZetaHeight" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) - - client, err := setupZetacoreClient() - require.NoError(t, err) - - t.Run("get zeta block height success", func(t *testing.T) { - resp, err := client.GetBlockHeight() - require.NoError(t, err) - require.Equal(t, expectedOutput.Height, resp) - }) -} - func TestZetacore_GetBaseGasPrice(t *testing.T) { + ctx := context.Background() + expectedOutput := feemarkettypes.QueryParamsResponse{ Params: feemarkettypes.Params{ BaseFee: types.NewInt(23455), @@ -488,19 +552,18 @@ func TestZetacore_GetBaseGasPrice(t *testing.T) { } input := feemarkettypes.QueryParamsRequest{} method := "/ethermint.feemarket.v1.Query/Params" - server := setupMockServer(t, feemarkettypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, feemarkettypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetBaseGasPrice() + resp, err := client.GetBaseGasPrice(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.Params.BaseFee.Int64(), resp) } func TestZetacore_GetNonceByChain(t *testing.T) { + ctx := context.Background() + chain := chains.BscMainnet expectedOutput := observertypes.QueryGetChainNoncesResponse{ ChainNonces: observertypes.ChainNonces{ @@ -514,19 +577,18 @@ func TestZetacore_GetNonceByChain(t *testing.T) { } input := observertypes.QueryGetChainNoncesRequest{Index: chain.ChainName.String()} method := "/zetachain.zetacore.observer.Query/ChainNonces" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetNonceByChain(chain) + resp, err := client.GetNonceByChain(ctx, chain) require.NoError(t, err) require.Equal(t, expectedOutput.ChainNonces, resp) } func TestZetacore_GetAllNodeAccounts(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryAllNodeAccountResponse{ NodeAccount: []*observertypes.NodeAccount{ { @@ -539,19 +601,18 @@ func TestZetacore_GetAllNodeAccounts(t *testing.T) { } input := observertypes.QueryAllNodeAccountRequest{} method := "/zetachain.zetacore.observer.Query/NodeAccountAll" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetAllNodeAccounts() + resp, err := client.GetAllNodeAccounts(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.NodeAccount, resp) } func TestZetacore_GetKeyGen(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryGetKeygenResponse{ Keygen: &observertypes.Keygen{ Status: observertypes.KeygenStatus_KeyGenSuccess, @@ -560,40 +621,38 @@ func TestZetacore_GetKeyGen(t *testing.T) { }} input := observertypes.QueryGetKeygenRequest{} method := "/zetachain.zetacore.observer.Query/Keygen" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetKeyGen() + resp, err := client.GetKeyGen(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.Keygen, resp) } func TestZetacore_GetBallotByID(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryBallotByIdentifierResponse{ BallotIdentifier: "ballot1235", } input := observertypes.QueryBallotByIdentifierRequest{BallotIdentifier: "ballot1235"} method := "/zetachain.zetacore.observer.Query/BallotByIdentifier" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetBallot("ballot1235") + resp, err := client.GetBallot(ctx, "ballot1235") require.NoError(t, err) require.Equal(t, expectedOutput, *resp) } func TestZetacore_GetInboundTrackersForChain(t *testing.T) { + ctx := context.Background() + chainID := chains.BscMainnet.ChainId - expectedOutput := crosschainTypes.QueryAllInboundTrackerByChainResponse{ - InboundTracker: []crosschainTypes.InboundTracker{ + expectedOutput := crosschaintypes.QueryAllInboundTrackerByChainResponse{ + InboundTracker: []crosschaintypes.InboundTracker{ { ChainId: chainID, TxHash: "DC76A6DCCC3AA62E89E69042ADC44557C50D59E4D3210C37D78DC8AE49B3B27F", @@ -601,21 +660,20 @@ func TestZetacore_GetInboundTrackersForChain(t *testing.T) { }, }, } - input := crosschainTypes.QueryAllInboundTrackerByChainRequest{ChainId: chainID} + input := crosschaintypes.QueryAllInboundTrackerByChainRequest{ChainId: chainID} method := "/zetachain.zetacore.crosschain.Query/InboundTrackerAllByChain" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetInboundTrackersForChain(chainID) + resp, err := client.GetInboundTrackersForChain(ctx, chainID) require.NoError(t, err) require.Equal(t, expectedOutput.InboundTracker, resp) } func TestZetacore_GetCurrentTss(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryGetTSSResponse{ TSS: observertypes.TSS{ TssPubkey: "zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc", @@ -627,57 +685,54 @@ func TestZetacore_GetCurrentTss(t *testing.T) { } input := observertypes.QueryGetTSSRequest{} method := "/zetachain.zetacore.observer.Query/TSS" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetCurrentTss() + resp, err := client.GetCurrentTSS(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.TSS, resp) } func TestZetacore_GetEthTssAddress(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryGetTssAddressResponse{ Eth: "0x70e967acfcc17c3941e87562161406d41676fd83", Btc: "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", } input := observertypes.QueryGetTssAddressRequest{} method := "/zetachain.zetacore.observer.Query/GetTssAddress" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetEthTssAddress() + resp, err := client.GetEVMTSSAddress(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.Eth, resp) } func TestZetacore_GetBtcTssAddress(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryGetTssAddressResponse{ Eth: "0x70e967acfcc17c3941e87562161406d41676fd83", Btc: "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", } input := observertypes.QueryGetTssAddressRequest{BitcoinChainId: 8332} method := "/zetachain.zetacore.observer.Query/GetTssAddress" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetBtcTssAddress(8332) + resp, err := client.GetBTCTSSAddress(ctx, 8332) require.NoError(t, err) require.Equal(t, expectedOutput.Btc, resp) } func TestZetacore_GetTssHistory(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryTssHistoryResponse{ TssList: []observertypes.TSS{ { @@ -691,49 +746,46 @@ func TestZetacore_GetTssHistory(t *testing.T) { } input := observertypes.QueryTssHistoryRequest{} method := "/zetachain.zetacore.observer.Query/TssHistory" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetTssHistory() + resp, err := client.GetTSSHistory(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.TssList, resp) } func TestZetacore_GetOutboundTracker(t *testing.T) { chain := chains.BscMainnet - expectedOutput := crosschainTypes.QueryGetOutboundTrackerResponse{ - OutboundTracker: crosschainTypes.OutboundTracker{ + expectedOutput := crosschaintypes.QueryGetOutboundTrackerResponse{ + OutboundTracker: crosschaintypes.OutboundTracker{ Index: "tracker12345", ChainId: chain.ChainId, Nonce: 456, HashList: nil, }, } - input := crosschainTypes.QueryGetOutboundTrackerRequest{ + input := crosschaintypes.QueryGetOutboundTrackerRequest{ ChainID: chain.ChainId, Nonce: 456, } method := "/zetachain.zetacore.crosschain.Query/OutboundTracker" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetOutboundTracker(chain, 456) + ctx := context.Background() + resp, err := client.GetOutboundTracker(ctx, chain, 456) require.NoError(t, err) require.Equal(t, expectedOutput.OutboundTracker, *resp) } func TestZetacore_GetAllOutboundTrackerByChain(t *testing.T) { + ctx := context.Background() + chain := chains.BscMainnet - expectedOutput := crosschainTypes.QueryAllOutboundTrackerByChainResponse{ - OutboundTracker: []crosschainTypes.OutboundTracker{ + expectedOutput := crosschaintypes.QueryAllOutboundTrackerByChainResponse{ + OutboundTracker: []crosschaintypes.OutboundTracker{ { Index: "tracker23456", ChainId: chain.ChainId, @@ -742,7 +794,7 @@ func TestZetacore_GetAllOutboundTrackerByChain(t *testing.T) { }, }, } - input := crosschainTypes.QueryAllOutboundTrackerByChainRequest{ + input := crosschaintypes.QueryAllOutboundTrackerByChainRequest{ Chain: chain.ChainId, Pagination: &query.PageRequest{ Key: nil, @@ -753,23 +805,22 @@ func TestZetacore_GetAllOutboundTrackerByChain(t *testing.T) { }, } method := "/zetachain.zetacore.crosschain.Query/OutboundTrackerAllByChain" - server := setupMockServer(t, crosschainTypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, crosschaintypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetAllOutboundTrackerByChain(chain.ChainId, interfaces.Ascending) + resp, err := client.GetAllOutboundTrackerByChain(ctx, chain.ChainId, interfaces.Ascending) require.NoError(t, err) require.Equal(t, expectedOutput.OutboundTracker, resp) - resp, err = client.GetAllOutboundTrackerByChain(chain.ChainId, interfaces.Descending) + resp, err = client.GetAllOutboundTrackerByChain(ctx, chain.ChainId, interfaces.Descending) require.NoError(t, err) require.Equal(t, expectedOutput.OutboundTracker, resp) } func TestZetacore_GetPendingNoncesByChain(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryPendingNoncesByChainResponse{ PendingNonces: observertypes.PendingNonces{ NonceLow: 0, @@ -780,19 +831,18 @@ func TestZetacore_GetPendingNoncesByChain(t *testing.T) { } input := observertypes.QueryPendingNoncesByChainRequest{ChainId: chains.Ethereum.ChainId} method := "/zetachain.zetacore.observer.Query/PendingNoncesByChain" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetPendingNoncesByChain(chains.Ethereum.ChainId) + resp, err := client.GetPendingNoncesByChain(ctx, chains.Ethereum.ChainId) require.NoError(t, err) require.Equal(t, expectedOutput.PendingNonces, resp) } func TestZetacore_GetBlockHeaderChainState(t *testing.T) { + ctx := context.Background() + chainID := chains.BscMainnet.ChainId expectedOutput := lightclienttypes.QueryGetChainStateResponse{ChainState: &lightclienttypes.ChainState{ ChainId: chainID, @@ -802,19 +852,18 @@ func TestZetacore_GetBlockHeaderChainState(t *testing.T) { }} input := lightclienttypes.QueryGetChainStateRequest{ChainId: chainID} method := "/zetachain.zetacore.lightclient.Query/ChainState" - server := setupMockServer(t, lightclienttypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, lightclienttypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetBlockHeaderChainState(chainID) + resp, err := client.GetBlockHeaderChainState(ctx, chainID) require.NoError(t, err) - require.Equal(t, expectedOutput, resp) + require.Equal(t, expectedOutput.ChainState, resp) } func TestZetacore_GetSupportedChains(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QuerySupportedChainsResponse{ Chains: []chains.Chain{ { @@ -839,19 +888,18 @@ func TestZetacore_GetSupportedChains(t *testing.T) { } input := observertypes.QuerySupportedChains{} method := "/zetachain.zetacore.observer.Query/SupportedChains" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetSupportedChains() + resp, err := client.GetSupportedChains(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.Chains, resp) } func TestZetacore_GetAdditionalChains(t *testing.T) { + ctx := context.Background() + expectedOutput := authoritytypes.QueryGetChainInfoResponse{ ChainInfo: authoritytypes.ChainInfo{ Chains: []chains.Chain{ @@ -862,19 +910,23 @@ func TestZetacore_GetAdditionalChains(t *testing.T) { } input := observertypes.QuerySupportedChains{} method := "/zetachain.zetacore.authority.Query/ChainInfo" - server := setupMockServer(t, authoritytypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) - client, err := setupZetacoreClient() - require.NoError(t, err) + setupMockServer(t, authoritytypes.RegisterQueryServer, method, input, expectedOutput) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + withAccountRetriever(t, 100, 100), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + ) - resp, err := client.GetAdditionalChains() + resp, err := client.GetAdditionalChains(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.ChainInfo.Chains, resp) } func TestZetacore_GetPendingNonces(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryAllPendingNoncesResponse{ PendingNonces: []observertypes.PendingNonces{ { @@ -887,19 +939,18 @@ func TestZetacore_GetPendingNonces(t *testing.T) { } input := observertypes.QueryAllPendingNoncesRequest{} method := "/zetachain.zetacore.observer.Query/PendingNoncesAll" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.GetPendingNonces() + resp, err := client.GetPendingNonces(ctx) require.NoError(t, err) require.Equal(t, expectedOutput, *resp) } func TestZetacore_Prove(t *testing.T) { + ctx := context.Background() + chainId := chains.BscMainnet.ChainId txHash := "9c8d02b6956b9c78ecb6090a8160faaa48e7aecfd0026fcdf533721d861436a3" blockHash := "0000000000000000000172c9a64f86f208b867a84dc7a0b7c75be51e750ed8eb" @@ -915,38 +966,36 @@ func TestZetacore_Prove(t *testing.T) { TxIndex: int64(txIndex), } method := "/zetachain.zetacore.lightclient.Query/Prove" - server := setupMockServer(t, lightclienttypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, lightclienttypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.Prove(blockHash, txHash, int64(txIndex), nil, chainId) + resp, err := client.Prove(ctx, blockHash, txHash, int64(txIndex), nil, chainId) require.NoError(t, err) require.Equal(t, expectedOutput.Valid, resp) } func TestZetacore_HasVoted(t *testing.T) { + ctx := context.Background() + expectedOutput := observertypes.QueryHasVotedResponse{HasVoted: true} input := observertypes.QueryHasVotedRequest{ BallotIdentifier: "123456asdf", VoterAddress: "zeta1l40mm7meacx03r4lp87s9gkxfan32xnznp42u6", } method := "/zetachain.zetacore.observer.Query/HasVoted" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) - resp, err := client.HasVoted("123456asdf", "zeta1l40mm7meacx03r4lp87s9gkxfan32xnznp42u6") + resp, err := client.HasVoted(ctx, "123456asdf", "zeta1l40mm7meacx03r4lp87s9gkxfan32xnznp42u6") require.NoError(t, err) require.Equal(t, expectedOutput.HasVoted, resp) } func TestZetacore_GetZetaHotKeyBalance(t *testing.T) { + ctx := context.Background() + expectedOutput := banktypes.QueryBalanceResponse{ Balance: &types.Coin{ Denom: config.BaseDenom, @@ -958,22 +1007,19 @@ func TestZetacore_GetZetaHotKeyBalance(t *testing.T) { Denom: config.BaseDenom, } method := "/cosmos.bank.v1beta1.Query/Balance" - server := setupMockServer(t, banktypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) + setupMockServer(t, banktypes.RegisterQueryServer, method, input, expectedOutput) - client, err := setupZetacoreClient() - require.NoError(t, err) + client := setupZetacoreClient(t, withDefaultObserverKeys()) // should be able to get balance of signer client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), types.AccAddress{}, "bob", "") - resp, err := client.GetZetaHotKeyBalance() + resp, err := client.GetZetaHotKeyBalance(ctx) require.NoError(t, err) require.Equal(t, expectedOutput.Balance.Amount, resp) // should return error on empty signer client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), types.AccAddress{}, "", "") - resp, err = client.GetZetaHotKeyBalance() + resp, err = client.GetZetaHotKeyBalance(ctx) require.Error(t, err) require.Equal(t, types.ZeroInt(), resp) } diff --git a/zetaclient/zetacore/client_vote.go b/zetaclient/zetacore/client_vote.go new file mode 100644 index 0000000000..a6e0116297 --- /dev/null +++ b/zetaclient/zetacore/client_vote.go @@ -0,0 +1,231 @@ +package zetacore + +import ( + "context" + + "github.com/pkg/errors" + "github.com/zeta-chain/go-tss/blame" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/proofs" + "github.com/zeta-chain/zetacore/pkg/retry" + "github.com/zeta-chain/zetacore/x/crosschain/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" +) + +// PostVoteBlockHeader posts a vote on an observed block header +func (c *Client) PostVoteBlockHeader( + ctx context.Context, + chainID int64, + blockHash []byte, + height int64, + header proofs.HeaderData, +) (string, error) { + signerAddress := c.keys.GetOperatorAddress().String() + + msg := observertypes.NewMsgVoteBlockHeader(signerAddress, chainID, blockHash, height, header) + + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) + if err != nil { + return "", err + } + + zetaTxHash, err := retry.DoTypedWithRetry(func() (string, error) { + return c.Broadcast(ctx, DefaultGasLimit, authzMsg, authzSigner) + }) + + if err != nil { + return "", errors.Wrap(err, "unable to broadcast vote block header") + } + + return zetaTxHash, nil +} + +// PostVoteGasPrice posts a gas price vote. Returns txHash and error. +func (c *Client) PostVoteGasPrice( + ctx context.Context, + chain chains.Chain, + gasPrice uint64, + supply string, + blockNum uint64, +) (string, error) { + // apply gas price multiplier for the chain + multiplier, err := GasPriceMultiplier(chain) + if err != nil { + return "", err + } + + // #nosec G701 always in range + gasPrice = uint64(float64(gasPrice) * multiplier) + signerAddress := c.keys.GetOperatorAddress().String() + msg := types.NewMsgVoteGasPrice(signerAddress, chain.ChainId, gasPrice, supply, blockNum) + + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) + if err != nil { + return "", err + } + + hash, err := retry.DoTypedWithRetry(func() (string, error) { + return c.Broadcast(ctx, PostGasPriceGasLimit, authzMsg, authzSigner) + }) + + if err != nil { + return "", errors.Wrap(err, "unable to broadcast vote gas price") + } + + return hash, nil +} + +// PostVoteTSS sends message to vote TSS. Returns txHash and error. +func (c *Client) PostVoteTSS( + ctx context.Context, + tssPubKey string, + keyGenZetaHeight int64, + status chains.ReceiveStatus, +) (string, error) { + signerAddress := c.keys.GetOperatorAddress().String() + msg := observertypes.NewMsgVoteTSS(signerAddress, tssPubKey, keyGenZetaHeight, status) + + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) + if err != nil { + return "", err + } + + zetaTxHash, err := retry.DoTypedWithRetry(func() (string, error) { + return c.Broadcast(ctx, DefaultGasLimit, authzMsg, authzSigner) + }) + + if err != nil { + return "", errors.Wrap(err, "unable to broadcast vote for setting tss") + } + + return zetaTxHash, nil +} + +// PostVoteBlameData posts blame data message to zetacore. Returns txHash and error. +func (c *Client) PostVoteBlameData( + ctx context.Context, + blame *blame.Blame, + chainID int64, + index string, +) (string, error) { + signerAddress := c.keys.GetOperatorAddress().String() + zetaBlame := observertypes.Blame{ + Index: index, + FailureReason: blame.FailReason, + Nodes: observertypes.ConvertNodes(blame.BlameNodes), + } + msg := observertypes.NewMsgVoteBlameMsg(signerAddress, chainID, zetaBlame) + + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) + if err != nil { + return "", err + } + + var gasLimit uint64 = PostBlameDataGasLimit + + zetaTxHash, err := retry.DoTypedWithRetry(func() (string, error) { + return c.Broadcast(ctx, gasLimit, authzMsg, authzSigner) + }) + + if err != nil { + return "", errors.Wrap(err, "unable to broadcast blame data") + } + + return zetaTxHash, nil +} + +// PostVoteOutbound posts a vote on an observed outbound tx from a MsgVoteOutbound. +// Returns tx hash, ballotIndex, and error. +func (c *Client) PostVoteOutbound( + ctx context.Context, + gasLimit, retryGasLimit uint64, + msg *types.MsgVoteOutbound, +) (string, string, error) { + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) + if err != nil { + return "", "", errors.Wrap(err, "unable to wrap message with authz") + } + + // don't post confirmation if it already voted before + ballotIndex := msg.Digest() + hasVoted, err := c.HasVoted(ctx, ballotIndex, msg.Creator) + if err != nil { + return "", ballotIndex, errors.Wrapf( + err, + "PostVoteOutbound: unable to check if already voted for ballot %s voter %s", + ballotIndex, + msg.Creator, + ) + } + if hasVoted { + return "", ballotIndex, nil + } + + zetaTxHash, err := retry.DoTypedWithRetry(func() (string, error) { + return c.Broadcast(ctx, gasLimit, authzMsg, authzSigner) + }) + + if err != nil { + return "", ballotIndex, errors.Wrap(err, "unable to broadcast vote outbound") + } + + go func() { + ctxForWorker := zctx.Copy(ctx, context.Background()) + + errMonitor := c.MonitorVoteOutboundResult(ctxForWorker, zetaTxHash, retryGasLimit, msg) + if errMonitor != nil { + c.logger.Error().Err(err).Msg("PostVoteOutbound: failed to monitor vote outbound result") + } + }() + + return zetaTxHash, ballotIndex, nil +} + +// PostVoteInbound posts a vote on an observed inbound tx +// retryGasLimit is the gas limit used to resend the tx if it fails because of insufficient gas +// it is used when the ballot is finalized and the inbound tx needs to be processed +func (c *Client) PostVoteInbound( + ctx context.Context, + gasLimit, retryGasLimit uint64, + msg *types.MsgVoteInbound, +) (string, string, error) { + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) + if err != nil { + return "", "", err + } + + // don't post send if has already voted before + ballotIndex := msg.Digest() + hasVoted, err := c.HasVoted(ctx, ballotIndex, msg.Creator) + if err != nil { + return "", ballotIndex, errors.Wrapf(err, + "PostVoteInbound: unable to check if already voted for ballot %s voter %s", + ballotIndex, + msg.Creator, + ) + } + if hasVoted { + return "", ballotIndex, nil + } + + zetaTxHash, err := retry.DoTypedWithRetry(func() (string, error) { + return c.Broadcast(ctx, gasLimit, authzMsg, authzSigner) + }) + + if err != nil { + return "", ballotIndex, errors.Wrap(err, "unable to broadcast vote inbound") + } + + go func() { + ctxForWorker := zctx.Copy(ctx, context.Background()) + + errMonitor := c.MonitorVoteInboundResult(ctxForWorker, zetaTxHash, retryGasLimit, msg) + if errMonitor != nil { + c.logger.Error().Err(err).Msg("PostVoteInbound: failed to monitor vote inbound result") + } + }() + + return zetaTxHash, ballotIndex, nil +} diff --git a/zetaclient/zetacore/client_worker.go b/zetaclient/zetacore/client_worker.go new file mode 100644 index 0000000000..a3b6026e90 --- /dev/null +++ b/zetaclient/zetacore/client_worker.go @@ -0,0 +1,43 @@ +package zetacore + +import ( + "context" + "time" + + "github.com/rs/zerolog" + + appcontext "github.com/zeta-chain/zetacore/zetaclient/context" +) + +var logSampler = &zerolog.BasicSampler{N: 10} + +// UpdateZetacoreContextWorker is a polling goroutine that checks and updates zetacore context at every height. +// todo implement graceful shutdown and work group +func (c *Client) UpdateZetacoreContextWorker(ctx context.Context, app *appcontext.AppContext) { + defer func() { + if r := recover(); r != nil { + c.logger.Error().Interface("panic", r).Msg("UpdateZetacoreContextWorker: recovered from panic") + } + }() + + var ( + updateEvery = time.Duration(app.Config().ConfigUpdateTicker) * time.Second + ticker = time.NewTicker(updateEvery) + logger = c.logger.Sample(logSampler) + ) + + c.logger.Info().Msg("UpdateZetacoreContextWorker started") + + for { + select { + case <-ticker.C: + c.logger.Debug().Msg("UpdateZetacoreContextWorker invocation") + if err := c.UpdateZetacoreContext(ctx, app, false, logger); err != nil { + c.logger.Err(err).Msg("UpdateZetacoreContextWorker failed to update config") + } + case <-c.stop: + c.logger.Info().Msg("UpdateZetacoreContextWorker stopped") + return + } + } +} diff --git a/zetaclient/zetacore/constant.go b/zetaclient/zetacore/constant.go index fa51df4791..e3ddafad78 100644 --- a/zetaclient/zetacore/constant.go +++ b/zetaclient/zetacore/constant.go @@ -1,13 +1,18 @@ package zetacore +import "time" + const ( + // DefaultBaseGasPrice is the default base gas price + DefaultBaseGasPrice = 1_000_000 + // DefaultGasLimit is the default gas limit used for broadcasting txs DefaultGasLimit = 200_000 // PostGasPriceGasLimit is the gas limit for voting new gas price PostGasPriceGasLimit = 1_500_000 - // PostVoteInboundGasLimit is the gas limit for voting on observed inbound tx + // PostVoteInboundGasLimit is the gas limit for voting on observed inbound tx (for zetachain itself) PostVoteInboundGasLimit = 400_000 // PostVoteInboundExecutionGasLimit is the gas limit for voting on observed inbound tx and executing it @@ -31,22 +36,16 @@ const ( // DefaultRetryInterval is the interval between retries in seconds DefaultRetryInterval = 5 - // MonitorVoteInboundResultInterval is the interval between retries for monitoring tx result in seconds - MonitorVoteInboundResultInterval = 5 - - // MonitorVoteInboundResultRetryCount is the number of retries to fetch monitoring tx result - MonitorVoteInboundResultRetryCount = 20 - - // PostVoteOutboundGasLimit is the gas limit for voting on observed outbound tx + // PostVoteOutboundGasLimit is the gas limit for voting on observed outbound tx (for zetachain itself) PostVoteOutboundGasLimit = 400_000 // PostVoteOutboundRevertGasLimit is the gas limit for voting on observed outbound tx for revert (when outbound fails) // The value needs to be higher because reverting implies interacting with the EVM to perform swaps for the gas token PostVoteOutboundRevertGasLimit = 1_500_000 +) - // MonitorVoteOutboundResultInterval is the interval between retries for monitoring tx result in seconds - MonitorVoteOutboundResultInterval = 5 - - // MonitorVoteOutboundResultRetryCount is the number of retries to fetch monitoring tx result - MonitorVoteOutboundResultRetryCount = 20 +// constants for monitoring tx results +const ( + monitorInterval = 5 * time.Second + monitorRetryCount = 20 ) diff --git a/zetaclient/zetacore/query.go b/zetaclient/zetacore/query.go deleted file mode 100644 index 52a9a8e5fc..0000000000 --- a/zetaclient/zetacore/query.go +++ /dev/null @@ -1,563 +0,0 @@ -package zetacore - -import ( - "context" - "fmt" - "sort" - "time" - - sdkmath "cosmossdk.io/math" - tmhttp "github.com/cometbft/cometbft/rpc/client/http" - "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" - "github.com/cosmos/cosmos-sdk/types/query" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" - "google.golang.org/grpc" - - "github.com/zeta-chain/zetacore/cmd/zetacored/config" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/proofs" - authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" - crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" - lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" -) - -// GetCrosschainFlags returns the crosschain flags -func (c *Client) GetCrosschainFlags() (observertypes.CrosschainFlags, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.CrosschainFlags(context.Background(), &observertypes.QueryGetCrosschainFlagsRequest{}) - if err != nil { - return observertypes.CrosschainFlags{}, err - } - return resp.CrosschainFlags, nil -} - -// GetBlockHeaderEnabledChains returns the enabled chains for block headers -func (c *Client) GetBlockHeaderEnabledChains() ([]lightclienttypes.HeaderSupportedChain, error) { - client := lightclienttypes.NewQueryClient(c.grpcConn) - resp, err := client.HeaderEnabledChains(context.Background(), &lightclienttypes.QueryHeaderEnabledChainsRequest{}) - if err != nil { - return []lightclienttypes.HeaderSupportedChain{}, err - } - return resp.HeaderEnabledChains, nil -} - -// GetRateLimiterFlags returns the rate limiter flags -func (c *Client) GetRateLimiterFlags() (crosschaintypes.RateLimiterFlags, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.RateLimiterFlags(context.Background(), &crosschaintypes.QueryRateLimiterFlagsRequest{}) - if err != nil { - return crosschaintypes.RateLimiterFlags{}, err - } - return resp.RateLimiterFlags, nil -} - -// GetChainParamsForChainID returns the chain params for a given chain ID -func (c *Client) GetChainParamsForChainID(externalChainID int64) (*observertypes.ChainParams, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.GetChainParamsForChain( - context.Background(), - &observertypes.QueryGetChainParamsForChainRequest{ChainId: externalChainID}, - ) - if err != nil { - return &observertypes.ChainParams{}, err - } - return resp.ChainParams, nil -} - -// GetChainParams returns all the chain params -func (c *Client) GetChainParams() ([]*observertypes.ChainParams, error) { - client := observertypes.NewQueryClient(c.grpcConn) - var err error - - resp := &observertypes.QueryGetChainParamsResponse{} - for i := 0; i <= DefaultRetryCount; i++ { - resp, err = client.GetChainParams(context.Background(), &observertypes.QueryGetChainParamsRequest{}) - if err == nil { - return resp.ChainParams.ChainParams, nil - } - time.Sleep(DefaultRetryInterval * time.Second) - } - return nil, fmt.Errorf("failed to get chain params | err %s", err.Error()) -} - -// GetUpgradePlan returns the current upgrade plan -func (c *Client) GetUpgradePlan() (*upgradetypes.Plan, error) { - client := upgradetypes.NewQueryClient(c.grpcConn) - - resp, err := client.CurrentPlan(context.Background(), &upgradetypes.QueryCurrentPlanRequest{}) - if err != nil { - return nil, err - } - return resp.Plan, nil -} - -// GetAllCctx returns all cross chain transactions -func (c *Client) GetAllCctx() ([]*crosschaintypes.CrossChainTx, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.CctxAll(context.Background(), &crosschaintypes.QueryAllCctxRequest{}) - if err != nil { - return nil, err - } - return resp.CrossChainTx, nil -} - -// GetCctxByHash returns a cross chain transaction by hash -func (c *Client) GetCctxByHash(sendHash string) (*crosschaintypes.CrossChainTx, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.Cctx(context.Background(), &crosschaintypes.QueryGetCctxRequest{Index: sendHash}) - if err != nil { - return nil, err - } - return resp.CrossChainTx, nil -} - -// GetCctxByNonce returns a cross chain transaction by nonce -func (c *Client) GetCctxByNonce(chainID int64, nonce uint64) (*crosschaintypes.CrossChainTx, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.CctxByNonce(context.Background(), &crosschaintypes.QueryGetCctxByNonceRequest{ - ChainID: chainID, - Nonce: nonce, - }) - if err != nil { - return nil, err - } - return resp.CrossChainTx, nil -} - -// GetObserverList returns the list of observers -func (c *Client) GetObserverList() ([]string, error) { - var err error - client := observertypes.NewQueryClient(c.grpcConn) - - for i := 0; i <= DefaultRetryCount; i++ { - resp, err := client.ObserverSet(context.Background(), &observertypes.QueryObserverSet{}) - if err == nil { - return resp.Observers, nil - } - time.Sleep(DefaultRetryInterval * time.Second) - } - return nil, err -} - -// GetRateLimiterInput returns input data for the rate limit checker -func (c *Client) GetRateLimiterInput(window int64) (crosschaintypes.QueryRateLimiterInputResponse, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - maxSizeOption := grpc.MaxCallRecvMsgSize(32 * 1024 * 1024) - resp, err := client.RateLimiterInput( - context.Background(), - &crosschaintypes.QueryRateLimiterInputRequest{ - Window: window, - }, - maxSizeOption, - ) - if err != nil { - return crosschaintypes.QueryRateLimiterInputResponse{}, err - } - return *resp, nil -} - -// ListPendingCctx returns a list of pending cctxs for a given chainID -// - The max size of the list is crosschainkeeper.MaxPendingCctxs -func (c *Client) ListPendingCctx(chainID int64) ([]*crosschaintypes.CrossChainTx, uint64, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - maxSizeOption := grpc.MaxCallRecvMsgSize(32 * 1024 * 1024) - resp, err := client.ListPendingCctx( - context.Background(), - &crosschaintypes.QueryListPendingCctxRequest{ - ChainId: chainID, - }, - maxSizeOption, - ) - if err != nil { - return nil, 0, err - } - return resp.CrossChainTx, resp.TotalPending, nil -} - -// ListPendingCctxWithinRatelimit returns a list of pending cctxs that do not exceed the outbound rate limit -// - The max size of the list is crosschainkeeper.MaxPendingCctxs -// - The returned `rateLimitExceeded` flag indicates if the rate limit is exceeded or not -func (c *Client) ListPendingCctxWithinRatelimit() ([]*crosschaintypes.CrossChainTx, uint64, int64, string, bool, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - maxSizeOption := grpc.MaxCallRecvMsgSize(32 * 1024 * 1024) - resp, err := client.ListPendingCctxWithinRateLimit( - context.Background(), - &crosschaintypes.QueryListPendingCctxWithinRateLimitRequest{}, - maxSizeOption, - ) - if err != nil { - return nil, 0, 0, "", false, err - } - return resp.CrossChainTx, resp.TotalPending, resp.CurrentWithdrawWindow, resp.CurrentWithdrawRate, resp.RateLimitExceeded, nil -} - -// GetAbortedZetaAmount returns the amount of zeta that has been aborted -func (c *Client) GetAbortedZetaAmount() (string, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.ZetaAccounting(context.Background(), &crosschaintypes.QueryZetaAccountingRequest{}) - if err != nil { - return "", err - } - return resp.AbortedZetaAmount, nil -} - -// GetGenesisSupply returns the genesis supply -func (c *Client) GetGenesisSupply() (sdkmath.Int, error) { - tmURL := fmt.Sprintf("http://%s", c.cfg.ChainRPC) - s, err := tmhttp.New(tmURL, "/websocket") - if err != nil { - return sdkmath.ZeroInt(), err - } - res, err := s.Genesis(context.Background()) - if err != nil { - return sdkmath.ZeroInt(), err - } - appState, err := genutiltypes.GenesisStateFromGenDoc(*res.Genesis) - if err != nil { - return sdkmath.ZeroInt(), err - } - bankstate := banktypes.GetGenesisStateFromAppState(c.encodingCfg.Codec, appState) - return bankstate.Supply.AmountOf(config.BaseDenom), nil -} - -// GetZetaTokenSupplyOnNode returns the zeta token supply on the node -func (c *Client) GetZetaTokenSupplyOnNode() (sdkmath.Int, error) { - client := banktypes.NewQueryClient(c.grpcConn) - resp, err := client.SupplyOf(context.Background(), &banktypes.QuerySupplyOfRequest{Denom: config.BaseDenom}) - if err != nil { - return sdkmath.ZeroInt(), err - } - return resp.GetAmount().Amount, nil -} - -// GetLastBlockHeight returns the last block height -func (c *Client) GetLastBlockHeight() ([]*crosschaintypes.LastBlockHeight, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.LastBlockHeightAll(context.Background(), &crosschaintypes.QueryAllLastBlockHeightRequest{}) - if err != nil { - c.logger.Error().Err(err).Msg("query GetBlockHeight error") - return nil, err - } - return resp.LastBlockHeight, nil -} - -// GetLatestZetaBlock returns the latest zeta block -func (c *Client) GetLatestZetaBlock() (*tmservice.Block, error) { - client := tmservice.NewServiceClient(c.grpcConn) - res, err := client.GetLatestBlock(context.Background(), &tmservice.GetLatestBlockRequest{}) - if err != nil { - return nil, err - } - return res.SdkBlock, nil -} - -// GetNodeInfo returns the node info -func (c *Client) GetNodeInfo() (*tmservice.GetNodeInfoResponse, error) { - var err error - - client := tmservice.NewServiceClient(c.grpcConn) - for i := 0; i <= DefaultRetryCount; i++ { - res, err := client.GetNodeInfo(context.Background(), &tmservice.GetNodeInfoRequest{}) - if err == nil { - return res, nil - } - time.Sleep(DefaultRetryInterval * time.Second) - } - return nil, err -} - -// GetBlockHeight returns the zetachain block height -func (c *Client) GetBlockHeight() (int64, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.LastZetaHeight(context.Background(), &crosschaintypes.QueryLastZetaHeightRequest{}) - if err != nil { - return 0, err - } - return resp.Height, nil -} - -// GetBaseGasPrice returns the base gas price -func (c *Client) GetBaseGasPrice() (int64, error) { - client := feemarkettypes.NewQueryClient(c.grpcConn) - resp, err := client.Params(context.Background(), &feemarkettypes.QueryParamsRequest{}) - if err != nil { - return 0, err - } - if resp.Params.BaseFee.IsNil() { - return 0, fmt.Errorf("base fee is nil") - } - return resp.Params.BaseFee.Int64(), nil -} - -// GetBallotByID returns a ballot by ID -func (c *Client) GetBallotByID(id string) (*observertypes.QueryBallotByIdentifierResponse, error) { - client := observertypes.NewQueryClient(c.grpcConn) - return client.BallotByIdentifier(context.Background(), &observertypes.QueryBallotByIdentifierRequest{ - BallotIdentifier: id, - }) -} - -// GetNonceByChain returns the nonce by chain -func (c *Client) GetNonceByChain(chain chains.Chain) (observertypes.ChainNonces, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.ChainNonces( - context.Background(), - &observertypes.QueryGetChainNoncesRequest{Index: chain.ChainName.String()}, - ) - if err != nil { - return observertypes.ChainNonces{}, err - } - return resp.ChainNonces, nil -} - -// GetAllNodeAccounts returns all node accounts -func (c *Client) GetAllNodeAccounts() ([]*observertypes.NodeAccount, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.NodeAccountAll(context.Background(), &observertypes.QueryAllNodeAccountRequest{}) - if err != nil { - return nil, err - } - c.logger.Debug().Msgf("GetAllNodeAccounts: %d", len(resp.NodeAccount)) - return resp.NodeAccount, nil -} - -// GetKeyGen returns the keygen -func (c *Client) GetKeyGen() (*observertypes.Keygen, error) { - var err error - client := observertypes.NewQueryClient(c.grpcConn) - - for i := 0; i <= ExtendedRetryCount; i++ { - resp, err := client.Keygen(context.Background(), &observertypes.QueryGetKeygenRequest{}) - if err == nil { - return resp.Keygen, nil - } - time.Sleep(DefaultRetryInterval * time.Second) - } - return nil, fmt.Errorf("failed to get keygen | err %s", err.Error()) -} - -// GetBallot returns a ballot by ID -func (c *Client) GetBallot(ballotIdentifier string) (*observertypes.QueryBallotByIdentifierResponse, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.BallotByIdentifier(context.Background(), &observertypes.QueryBallotByIdentifierRequest{ - BallotIdentifier: ballotIdentifier, - }) - if err != nil { - return nil, err - } - return resp, nil -} - -// GetInboundTrackersForChain returns the inbound trackers for a chain -func (c *Client) GetInboundTrackersForChain(chainID int64) ([]crosschaintypes.InboundTracker, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.InboundTrackerAllByChain( - context.Background(), - &crosschaintypes.QueryAllInboundTrackerByChainRequest{ChainId: chainID}, - ) - if err != nil { - return nil, err - } - return resp.InboundTracker, nil -} - -// GetCurrentTss returns the current TSS -func (c *Client) GetCurrentTss() (observertypes.TSS, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.TSS(context.Background(), &observertypes.QueryGetTSSRequest{}) - if err != nil { - return observertypes.TSS{}, err - } - return resp.TSS, nil -} - -// GetEthTssAddress returns the ETH TSS address -// TODO(revamp): rename to EVM -func (c *Client) GetEthTssAddress() (string, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.GetTssAddress(context.Background(), &observertypes.QueryGetTssAddressRequest{}) - if err != nil { - return "", err - } - return resp.Eth, nil -} - -// GetBtcTssAddress returns the BTC TSS address -func (c *Client) GetBtcTssAddress(chainID int64) (string, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.GetTssAddress(context.Background(), &observertypes.QueryGetTssAddressRequest{ - BitcoinChainId: chainID, - }) - if err != nil { - return "", err - } - return resp.Btc, nil -} - -// GetTssHistory returns the TSS history -func (c *Client) GetTssHistory() ([]observertypes.TSS, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.TssHistory(context.Background(), &observertypes.QueryTssHistoryRequest{}) - if err != nil { - return nil, err - } - return resp.TssList, nil -} - -// GetOutboundTracker returns the outbound tracker for a chain and nonce -func (c *Client) GetOutboundTracker(chain chains.Chain, nonce uint64) (*crosschaintypes.OutboundTracker, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.OutboundTracker(context.Background(), &crosschaintypes.QueryGetOutboundTrackerRequest{ - ChainID: chain.ChainId, - Nonce: nonce, - }) - if err != nil { - return nil, err - } - return &resp.OutboundTracker, nil -} - -// GetAllOutboundTrackerByChain returns all outbound trackers for a chain -func (c *Client) GetAllOutboundTrackerByChain( - chainID int64, - order interfaces.Order, -) ([]crosschaintypes.OutboundTracker, error) { - client := crosschaintypes.NewQueryClient(c.grpcConn) - resp, err := client.OutboundTrackerAllByChain( - context.Background(), - &crosschaintypes.QueryAllOutboundTrackerByChainRequest{ - Chain: chainID, - Pagination: &query.PageRequest{ - Key: nil, - Offset: 0, - Limit: 2000, - CountTotal: false, - Reverse: false, - }, - }, - ) - if err != nil { - return nil, err - } - if order == interfaces.Ascending { - sort.SliceStable(resp.OutboundTracker, func(i, j int) bool { - return resp.OutboundTracker[i].Nonce < resp.OutboundTracker[j].Nonce - }) - } - if order == interfaces.Descending { - sort.SliceStable(resp.OutboundTracker, func(i, j int) bool { - return resp.OutboundTracker[i].Nonce > resp.OutboundTracker[j].Nonce - }) - } - return resp.OutboundTracker, nil -} - -// GetPendingNoncesByChain returns the pending nonces for a chain and current tss address -func (c *Client) GetPendingNoncesByChain(chainID int64) (observertypes.PendingNonces, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.PendingNoncesByChain( - context.Background(), - &observertypes.QueryPendingNoncesByChainRequest{ChainId: chainID}, - ) - if err != nil { - return observertypes.PendingNonces{}, err - } - return resp.PendingNonces, nil -} - -// GetBlockHeaderChainState returns the block header chain state -func (c *Client) GetBlockHeaderChainState(chainID int64) (lightclienttypes.QueryGetChainStateResponse, error) { - client := lightclienttypes.NewQueryClient(c.grpcConn) - resp, err := client.ChainState(context.Background(), &lightclienttypes.QueryGetChainStateRequest{ChainId: chainID}) - if err != nil { - return lightclienttypes.QueryGetChainStateResponse{}, err - } - return *resp, nil -} - -// GetSupportedChains returns the supported chains -func (c *Client) GetSupportedChains() ([]chains.Chain, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.SupportedChains(context.Background(), &observertypes.QuerySupportedChains{}) - if err != nil { - return nil, err - } - return resp.GetChains(), nil -} - -// GetAdditionalChains returns the additional chains -func (c *Client) GetAdditionalChains() ([]chains.Chain, error) { - client := authoritytypes.NewQueryClient(c.grpcConn) - resp, err := client.ChainInfo(context.Background(), &authoritytypes.QueryGetChainInfoRequest{}) - if err != nil { - return nil, err - } - return resp.GetChainInfo().Chains, nil -} - -// GetPendingNonces returns the pending nonces -func (c *Client) GetPendingNonces() (*observertypes.QueryAllPendingNoncesResponse, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.PendingNoncesAll(context.Background(), &observertypes.QueryAllPendingNoncesRequest{}) - if err != nil { - return nil, err - } - return resp, nil -} - -// Prove returns whether a proof is valid -func (c *Client) Prove( - blockHash string, - txHash string, - txIndex int64, - proof *proofs.Proof, - chainID int64, -) (bool, error) { - client := lightclienttypes.NewQueryClient(c.grpcConn) - resp, err := client.Prove(context.Background(), &lightclienttypes.QueryProveRequest{ - BlockHash: blockHash, - TxIndex: txIndex, - Proof: proof, - ChainId: chainID, - TxHash: txHash, - }) - if err != nil { - return false, err - } - return resp.Valid, nil -} - -// HasVoted returns whether an observer has voted -func (c *Client) HasVoted(ballotIndex string, voterAddress string) (bool, error) { - client := observertypes.NewQueryClient(c.grpcConn) - resp, err := client.HasVoted(context.Background(), &observertypes.QueryHasVotedRequest{ - BallotIdentifier: ballotIndex, - VoterAddress: voterAddress, - }) - if err != nil { - return false, err - } - return resp.HasVoted, nil -} - -// GetZetaHotKeyBalance returns the zeta hot key balance -func (c *Client) GetZetaHotKeyBalance() (sdkmath.Int, error) { - client := banktypes.NewQueryClient(c.grpcConn) - address, err := c.keys.GetAddress() - if err != nil { - return sdkmath.ZeroInt(), err - } - resp, err := client.Balance(context.Background(), &banktypes.QueryBalanceRequest{ - Address: address.String(), - Denom: config.BaseDenom, - }) - if err != nil { - return sdkmath.ZeroInt(), err - } - return resp.Balance.Amount, nil -} diff --git a/zetaclient/zetacore/tx.go b/zetaclient/zetacore/tx.go index 2ef142d03f..90a6b1d136 100644 --- a/zetaclient/zetacore/tx.go +++ b/zetaclient/zetacore/tx.go @@ -1,26 +1,21 @@ package zetacore import ( + "context" "fmt" - "math/big" "strings" - "time" + "cosmossdk.io/errors" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/authz" - "github.com/pkg/errors" - "github.com/rs/zerolog" - "github.com/zeta-chain/go-tss/blame" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/pkg/proofs" "github.com/zeta-chain/zetacore/x/crosschain/types" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" clientauthz "github.com/zeta-chain/zetacore/zetaclient/authz" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - appcontext "github.com/zeta-chain/zetacore/zetaclient/context" ) // GetInboundVoteMessage returns a new MsgVoteInbound @@ -72,52 +67,24 @@ func GasPriceMultiplier(chain chains.Chain) (float64, error) { // WrapMessageWithAuthz wraps a message with an authz message // used since a hotkey is used to broadcast the transactions, instead of the operator -func (c *Client) WrapMessageWithAuthz(msg sdk.Msg) (sdk.Msg, clientauthz.Signer, error) { +func WrapMessageWithAuthz(msg sdk.Msg) (sdk.Msg, clientauthz.Signer, error) { msgURL := sdk.MsgTypeURL(msg) // verify message validity if err := msg.ValidateBasic(); err != nil { - return nil, clientauthz.Signer{}, fmt.Errorf("%s invalid msg | %s", msgURL, err.Error()) + return nil, clientauthz.Signer{}, errors.Wrapf(err, "invalid message %q", msgURL) } authzSigner := clientauthz.GetSigner(msgURL) authzMessage := authz.NewMsgExec(authzSigner.GranteeAddress, []sdk.Msg{msg}) - return &authzMessage, authzSigner, nil -} - -// PostGasPrice posts a gas price vote -// TODO(revamp): rename to PostVoteGasPrice -func (c *Client) PostGasPrice(chain chains.Chain, gasPrice uint64, supply string, blockNum uint64) (string, error) { - // apply gas price multiplier for the chain - multiplier, err := GasPriceMultiplier(chain) - if err != nil { - return "", err - } - // #nosec G701 always in range - gasPrice = uint64(float64(gasPrice) * multiplier) - signerAddress := c.keys.GetOperatorAddress().String() - msg := types.NewMsgVoteGasPrice(signerAddress, chain.ChainId, gasPrice, supply, blockNum) - authzMsg, authzSigner, err := c.WrapMessageWithAuthz(msg) - if err != nil { - return "", err - } - - for i := 0; i < DefaultRetryCount; i++ { - zetaTxHash, err := zetacoreBroadcast(c, PostGasPriceGasLimit, authzMsg, authzSigner) - if err == nil { - return zetaTxHash, nil - } - c.logger.Debug().Err(err).Msgf("PostGasPrice broadcast fail | Retry count : %d", i+1) - time.Sleep(DefaultRetryInterval * time.Second) - } - - return "", fmt.Errorf("post gasprice failed after %d retries", DefaultRetryInterval) + return &authzMessage, authzSigner, nil } // AddOutboundTracker adds an outbound tracker // TODO(revamp): rename to PostAddOutboundTracker func (c *Client) AddOutboundTracker( + ctx context.Context, chainID int64, nonce uint64, txHash string, @@ -126,7 +93,7 @@ func (c *Client) AddOutboundTracker( txIndex int64, ) (string, error) { // don't report if the tracker already contains the txHash - tracker, err := c.GetOutboundTracker(chains.Chain{ChainId: chainID}, nonce) + tracker, err := c.GetOutboundTracker(ctx, chains.Chain{ChainId: chainID}, nonce) if err == nil { for _, hash := range tracker.HashList { if strings.EqualFold(hash.TxHash, txHash) { @@ -138,344 +105,15 @@ func (c *Client) AddOutboundTracker( signerAddress := c.keys.GetOperatorAddress().String() msg := types.NewMsgAddOutboundTracker(signerAddress, chainID, nonce, txHash, proof, blockHash, txIndex) - authzMsg, authzSigner, err := c.WrapMessageWithAuthz(msg) + authzMsg, authzSigner, err := WrapMessageWithAuthz(msg) if err != nil { return "", err } - zetaTxHash, err := zetacoreBroadcast(c, AddOutboundTrackerGasLimit, authzMsg, authzSigner) + zetaTxHash, err := c.Broadcast(ctx, AddOutboundTrackerGasLimit, authzMsg, authzSigner) if err != nil { return "", err } - return zetaTxHash, nil -} - -// SetTSS sends message to vote tss -// TODO(revamp): rename to PostVoteTSS -func (c *Client) SetTSS(tssPubkey string, keyGenZetaHeight int64, status chains.ReceiveStatus) (string, error) { - signerAddress := c.keys.GetOperatorAddress().String() - msg := observertypes.NewMsgVoteTSS(signerAddress, tssPubkey, keyGenZetaHeight, status) - - authzMsg, authzSigner, err := c.WrapMessageWithAuthz(msg) - if err != nil { - return "", err - } - - zetaTxHash := "" - for i := 0; i <= DefaultRetryCount; i++ { - zetaTxHash, err = zetacoreBroadcast(c, DefaultGasLimit, authzMsg, authzSigner) - if err == nil { - return zetaTxHash, nil - } - c.logger.Debug().Err(err).Msgf("SetTSS broadcast fail | Retry count : %d", i+1) - time.Sleep(DefaultRetryInterval * time.Second) - } - return "", fmt.Errorf("set tss failed | err %s", err.Error()) -} - -// ZetacoreContextUpdater is a polling goroutine that checks and updates zetacore context at every height -// TODO(revamp): move to a different file -// TODO(revamp): rename to UpdateZetacoreContext -func (c *Client) ZetacoreContextUpdater(appContext *appcontext.AppContext) { - c.logger.Info().Msg("ZetacoreContextUpdater started") - ticker := time.NewTicker(time.Duration(appContext.Config().ConfigUpdateTicker) * time.Second) - sampledLogger := c.logger.Sample(&zerolog.BasicSampler{N: 10}) - for { - select { - case <-ticker.C: - c.logger.Debug().Msg("Running Updater") - err := c.UpdateZetacoreContext(appContext, false, sampledLogger) - if err != nil { - c.logger.Err(err).Msg("ZetacoreContextUpdater failed to update config") - } - case <-c.stop: - c.logger.Info().Msg("ZetacoreContextUpdater stopped") - return - } - } -} - -// PostBlameData posts blame data message to zetacore -// TODO(revamp): rename to PostVoteBlame -func (c *Client) PostBlameData(blame *blame.Blame, chainID int64, index string) (string, error) { - signerAddress := c.keys.GetOperatorAddress().String() - zetaBlame := observertypes.Blame{ - Index: index, - FailureReason: blame.FailReason, - Nodes: observertypes.ConvertNodes(blame.BlameNodes), - } - msg := observertypes.NewMsgVoteBlameMsg(signerAddress, chainID, zetaBlame) - - authzMsg, authzSigner, err := c.WrapMessageWithAuthz(msg) - if err != nil { - return "", err - } - - var gasLimit uint64 = PostBlameDataGasLimit - - for i := 0; i < DefaultRetryCount; i++ { - zetaTxHash, err := zetacoreBroadcast(c, gasLimit, authzMsg, authzSigner) - if err == nil { - return zetaTxHash, nil - } - c.logger.Error().Err(err).Msgf("PostBlame broadcast fail | Retry count : %d", i+1) - time.Sleep(DefaultRetryInterval * time.Second) - } - return "", fmt.Errorf("post blame data failed after %d retries", DefaultRetryCount) -} - -// PostVoteBlockHeader posts a vote on an observed block header -func (c *Client) PostVoteBlockHeader( - chainID int64, - blockHash []byte, - height int64, - header proofs.HeaderData, -) (string, error) { - signerAddress := c.keys.GetOperatorAddress().String() - - msg := observertypes.NewMsgVoteBlockHeader(signerAddress, chainID, blockHash, height, header) - - authzMsg, authzSigner, err := c.WrapMessageWithAuthz(msg) - if err != nil { - return "", err - } - - var gasLimit uint64 = DefaultGasLimit - for i := 0; i < DefaultRetryCount; i++ { - zetaTxHash, err := zetacoreBroadcast(c, gasLimit, authzMsg, authzSigner) - if err == nil { - return zetaTxHash, nil - } - c.logger.Error().Err(err).Msgf("PostVoteBlockHeader broadcast fail | Retry count : %d", i+1) - time.Sleep(DefaultRetryInterval * time.Second) - } - return "", fmt.Errorf("post add block header failed after %d retries", DefaultRetryCount) -} - -// PostVoteInbound posts a vote on an observed inbound tx -// retryGasLimit is the gas limit used to resend the tx if it fails because of insufficient gas -// it is used when the ballot is finalized and the inbound tx needs to be processed -func (c *Client) PostVoteInbound(gasLimit, retryGasLimit uint64, msg *types.MsgVoteInbound) (string, string, error) { - authzMsg, authzSigner, err := c.WrapMessageWithAuthz(msg) - if err != nil { - return "", "", err - } - - // don't post send if has already voted before - ballotIndex := msg.Digest() - hasVoted, err := c.HasVoted(ballotIndex, msg.Creator) - if err != nil { - return "", ballotIndex, errors.Wrapf( - err, - "PostVoteInbound: unable to check if already voted for ballot %s voter %s", - ballotIndex, - msg.Creator, - ) - } - if hasVoted { - return "", ballotIndex, nil - } - - for i := 0; i < DefaultRetryCount; i++ { - zetaTxHash, err := zetacoreBroadcast(c, gasLimit, authzMsg, authzSigner) - if err == nil { - // monitor the result of the transaction and resend if necessary - go c.MonitorVoteInboundResult(zetaTxHash, retryGasLimit, msg) - - return zetaTxHash, ballotIndex, nil - } - c.logger.Debug().Err(err).Msgf("PostVoteInbound broadcast fail | Retry count : %d", i+1) - time.Sleep(DefaultRetryInterval * time.Second) - } - return "", ballotIndex, fmt.Errorf("post send failed after %d retries", DefaultRetryInterval) -} - -// MonitorVoteInboundResult monitors the result of a vote inbound tx -// retryGasLimit is the gas limit used to resend the tx if it fails because of insufficient gas -// if retryGasLimit is 0, the tx is not resent -// TODO(revamp): move to a monitor file -func (c *Client) MonitorVoteInboundResult(zetaTxHash string, retryGasLimit uint64, msg *types.MsgVoteInbound) { - var lastErr error - - for i := 0; i < MonitorVoteInboundResultRetryCount; i++ { - time.Sleep(MonitorVoteInboundResultInterval * time.Second) - - // query tx result from ZetaChain - txResult, err := c.QueryTxResult(zetaTxHash) - - if err == nil { - if strings.Contains(txResult.RawLog, "failed to execute message") { - // the inbound vote tx shouldn't fail to execute - // this shouldn't happen - c.logger.Error().Msgf( - "MonitorInboundResult: failed to execute vote, txHash: %s, log %s", zetaTxHash, txResult.RawLog, - ) - } else if strings.Contains(txResult.RawLog, "out of gas") { - // if the tx fails with an out of gas error, resend the tx with more gas if retryGasLimit > 0 - c.logger.Debug().Msgf( - "MonitorInboundResult: out of gas, txHash: %s, log %s", zetaTxHash, txResult.RawLog, - ) - if retryGasLimit > 0 { - // new retryGasLimit set to 0 to prevent reentering this function - _, _, err := c.PostVoteInbound(retryGasLimit, 0, msg) - if err != nil { - c.logger.Error().Err(err).Msgf( - "MonitorInboundResult: failed to resend tx, txHash: %s, log %s", zetaTxHash, txResult.RawLog, - ) - } else { - c.logger.Info().Msgf( - "MonitorInboundResult: successfully resent tx, txHash: %s, log %s", zetaTxHash, txResult.RawLog, - ) - } - } - } else { - c.logger.Debug().Msgf( - "MonitorInboundResult: successful txHash %s, log %s", zetaTxHash, txResult.RawLog, - ) - } - return - } - lastErr = err - } - - c.logger.Error().Err(lastErr).Msgf( - "MonitorInboundResult: unable to query tx result for txHash %s, err %s", zetaTxHash, lastErr.Error(), - ) -} - -// PostVoteOutbound posts a vote on an observed outbound tx -// TODO(revamp): rename and move to a different file -func (c *Client) PostVoteOutbound( - cctxIndex string, - outboundHash string, - outBlockHeight uint64, - outboundGasUsed uint64, - outboundEffectiveGasPrice *big.Int, - outboundEffectiveGasLimit uint64, - amount *big.Int, - status chains.ReceiveStatus, - chain chains.Chain, - nonce uint64, - coinType coin.CoinType, -) (string, string, error) { - signerAddress := c.keys.GetOperatorAddress().String() - msg := types.NewMsgVoteOutbound( - signerAddress, - cctxIndex, - outboundHash, - outBlockHeight, - outboundGasUsed, - math.NewIntFromBigInt(outboundEffectiveGasPrice), - outboundEffectiveGasLimit, - math.NewUintFromBigInt(amount), - status, - chain.ChainId, - nonce, - coinType, - ) - - // when an outbound fails and a revert is required, the gas limit needs to be higher - // this is because the revert tx needs to interact with the EVM to perform swaps for the gas token - // the higher gas limit is only necessary when the vote is finalized and the outbound is processed - // therefore we use a retryGasLimit with a higher value to resend the tx if it fails (when the vote is finalized) - retryGasLimit := uint64(0) - if msg.Status == chains.ReceiveStatus_failed { - retryGasLimit = PostVoteOutboundRevertGasLimit - } - - return c.PostVoteOutboundFromMsg(PostVoteOutboundGasLimit, retryGasLimit, msg) -} - -// PostVoteOutboundFromMsg posts a vote on an observed outbound tx from a MsgVoteOutbound -// TODO(revamp): rename to PostVoteOutbound -func (c *Client) PostVoteOutboundFromMsg( - gasLimit, retryGasLimit uint64, - msg *types.MsgVoteOutbound, -) (string, string, error) { - authzMsg, authzSigner, err := c.WrapMessageWithAuthz(msg) - if err != nil { - return "", "", err - } - - // don't post confirmation if has already voted before - ballotIndex := msg.Digest() - hasVoted, err := c.HasVoted(ballotIndex, msg.Creator) - if err != nil { - return "", ballotIndex, errors.Wrapf( - err, - "PostVoteOutbound: unable to check if already voted for ballot %s voter %s", - ballotIndex, - msg.Creator, - ) - } - if hasVoted { - return "", ballotIndex, nil - } - for i := 0; i < DefaultRetryCount; i++ { - zetaTxHash, err := zetacoreBroadcast(c, gasLimit, authzMsg, authzSigner) - if err == nil { - // monitor the result of the transaction and resend if necessary - go c.MonitorVoteOutboundResult(zetaTxHash, retryGasLimit, msg) - - return zetaTxHash, ballotIndex, nil - } - c.logger.Debug().Err(err).Msgf("PostVoteOutbound broadcast fail | Retry count : %d", i+1) - time.Sleep(DefaultRetryInterval * time.Second) - } - return "", ballotIndex, fmt.Errorf("post receive failed after %d retries", DefaultRetryCount) -} - -// MonitorVoteOutboundResult monitors the result of a vote outbound tx -// retryGasLimit is the gas limit used to resend the tx if it fails because of insufficient gas -// if retryGasLimit is 0, the tx is not resent -// TODO(revamp): move to a monitor file -func (c *Client) MonitorVoteOutboundResult(zetaTxHash string, retryGasLimit uint64, msg *types.MsgVoteOutbound) { - var lastErr error - - for i := 0; i < MonitorVoteOutboundResultRetryCount; i++ { - time.Sleep(MonitorVoteOutboundResultInterval * time.Second) - - // query tx result from ZetaChain - txResult, err := c.QueryTxResult(zetaTxHash) - - if err == nil { - if strings.Contains(txResult.RawLog, "failed to execute message") { - // the inbound vote tx shouldn't fail to execute - // this shouldn't happen - c.logger.Error().Msgf( - "MonitorVoteOutboundResult: failed to execute vote, txHash: %s, log %s", zetaTxHash, txResult.RawLog, - ) - } else if strings.Contains(txResult.RawLog, "out of gas") { - // if the tx fails with an out of gas error, resend the tx with more gas if retryGasLimit > 0 - c.logger.Debug().Msgf( - "MonitorVoteOutboundResult: out of gas, txHash: %s, log %s", zetaTxHash, txResult.RawLog, - ) - if retryGasLimit > 0 { - // new retryGasLimit set to 0 to prevent reentering this function - _, _, err := c.PostVoteOutboundFromMsg(retryGasLimit, 0, msg) - - if err != nil { - c.logger.Error().Err(err).Msgf( - "MonitorVoteOutboundResult: failed to resend tx, txHash: %s, log %s", zetaTxHash, txResult.RawLog, - ) - } else { - c.logger.Info().Msgf( - "MonitorVoteOutboundResult: successfully resent tx, txHash: %s, log %s", zetaTxHash, txResult.RawLog, - ) - } - } - } else { - c.logger.Debug().Msgf( - "MonitorVoteOutboundResult: successful txHash %s, log %s", zetaTxHash, txResult.RawLog, - ) - } - return - } - lastErr = err - } - - c.logger.Error().Err(lastErr).Msgf( - "MonitorVoteOutboundResult: unable to query tx result for txHash %s, err %s", zetaTxHash, lastErr.Error(), - ) + return zetaTxHash, nil } diff --git a/zetaclient/zetacore/tx_test.go b/zetaclient/zetacore/tx_test.go index d53e93bda9..0ad9c0edf7 100644 --- a/zetaclient/zetacore/tx_test.go +++ b/zetaclient/zetacore/tx_test.go @@ -2,22 +2,25 @@ package zetacore import ( "bytes" + "context" "encoding/hex" - "errors" - "github.com/zeta-chain/zetacore/testutil/sample" - authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" - "math/big" "net" "os" "testing" + "github.com/zeta-chain/zetacore/testutil/sample" + authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" + "cosmossdk.io/math" sdktypes "github.com/cosmos/cosmos-sdk/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeta-chain/go-tss/blame" + zctx "github.com/zeta-chain/zetacore/zetaclient/context" "go.nhat.io/grpcmock" "go.nhat.io/grpcmock/planner" @@ -27,16 +30,14 @@ import ( crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) const ( - testSigner = `jack` - sampleHash = "fa51db4412144f1130669f2bae8cb44aadbd8d85958dbffcb0fe236878097e1a" + testSigner = "jack" + sampleHash = "FA51DB4412144F1130669F2BAE8CB44AADBD8D85958DBFFCB0FE236878097E1A" ethBlockHash = "1a17bcc359e84ba8ae03b17ec425f97022cd11c3e279f6bdf7a96fcffa12b366" ) @@ -117,14 +118,6 @@ func Test_GasPriceMultiplier(t *testing.T) { } } -func MockBroadcast(_ *Client, _ uint64, _ sdktypes.Msg, _ authz.Signer) (string, error) { - return sampleHash, nil -} - -func MockBroadcastError(_ *Client, _ uint64, _ sdktypes.Msg, _ authz.Signer) (string, error) { - return sampleHash, errors.New("broadcast error") -} - func getHeaderData(t *testing.T) proofs.HeaderData { var header ethtypes.Header file, err := os.Open("../../testutil/testdata/eth_header_18495266.json") @@ -142,14 +135,19 @@ func getHeaderData(t *testing.T) proofs.HeaderData { } func TestZetacore_PostGasPrice(t *testing.T) { - client, err := setupZetacoreClient() - require.NoError(t, err) - address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") + ctx := context.Background() + + extraGRPC := withDummyServer(100) + setupMockServer(t, observertypes.RegisterQueryServer, skipMethod, nil, nil, extraGRPC...) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + withAccountRetriever(t, 100, 100), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + ) t.Run("post gas price success", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast - hash, err := client.PostGasPrice(chains.BscMainnet, 1000000, "100", 1234) + hash, err := client.PostVoteGasPrice(ctx, chains.BscMainnet, 1000000, "100", 1234) require.NoError(t, err) require.Equal(t, sampleHash, hash) }) @@ -165,35 +163,66 @@ func TestZetacore_PostGasPrice(t *testing.T) { } func TestZetacore_AddOutboundTracker(t *testing.T) { - client, err := setupZetacoreClient() - require.NoError(t, err) - address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") + ctx := context.Background() + + const nonce = 123 + chainID := chains.BscMainnet.ChainId + + method := "/zetachain.zetacore.crosschain.Query/OutboundTracker" + input := &crosschaintypes.QueryGetOutboundTrackerRequest{ + ChainID: chains.BscMainnet.ChainId, + Nonce: nonce, + } + output := &crosschaintypes.QueryGetOutboundTrackerResponse{ + OutboundTracker: crosschaintypes.OutboundTracker{ + Index: "456", + ChainId: chainID, + Nonce: nonce, + HashList: nil, + }, + } + + extraGRPC := withDummyServer(100) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, output, extraGRPC...) + + tendermintMock := mocks.NewSDKClientWithErr(t, nil, 0) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + withAccountRetriever(t, 100, 100), + withTendermint(tendermintMock), + ) t.Run("add tx hash success", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast - hash, err := client.AddOutboundTracker(chains.BscMainnet.ChainId, 123, "", nil, "", 456) - require.NoError(t, err) - require.Equal(t, sampleHash, hash) + tendermintMock.SetBroadcastTxHash(sampleHash) + hash, err := client.AddOutboundTracker(ctx, chainID, nonce, "", nil, "", 456) + assert.NoError(t, err) + assert.Equal(t, sampleHash, hash) }) t.Run("add tx hash fail", func(t *testing.T) { - zetacoreBroadcast = MockBroadcastError - hash, err := client.AddOutboundTracker(chains.BscMainnet.ChainId, 123, "", nil, "", 456) - require.Error(t, err) - require.Equal(t, "", hash) + tendermintMock.SetError(errors.New("broadcast error")) + hash, err := client.AddOutboundTracker(ctx, chainID, nonce, "", nil, "", 456) + assert.Error(t, err) + assert.Empty(t, hash) }) } func TestZetacore_SetTSS(t *testing.T) { - client, err := setupZetacoreClient() - require.NoError(t, err) - address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") + ctx := context.Background() + + extraGRPC := withDummyServer(100) + setupMockServer(t, crosschaintypes.RegisterMsgServer, skipMethod, nil, nil, extraGRPC...) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + withAccountRetriever(t, 100, 100), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + ) t.Run("set tss success", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast - hash, err := client.SetTSS( + hash, err := client.PostVoteTSS( + ctx, "zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc", 9987, chains.ReceiveStatus_success, @@ -204,6 +233,8 @@ func TestZetacore_SetTSS(t *testing.T) { } func TestZetacore_UpdateZetacoreContext(t *testing.T) { + ctx := context.Background() + //Setup server for multiple grpc calls listener, err := net.Listen("tcp", "127.0.0.1:9090") require.NoError(t, err) @@ -342,32 +373,37 @@ func TestZetacore_UpdateZetacoreContext(t *testing.T) { )(t) server.Serve() - defer closeMockServer(t, server) + defer server.Close() - client, err := setupZetacoreClient() - require.NoError(t, err) address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") - client.EnableMockSDKClient(mocks.NewSDKClientWithErr(nil, 0)) + client := setupZetacoreClient(t, + withObserverKeys(keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "")), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + ) t.Run("zetacore update success", func(t *testing.T) { - cfg := config.NewConfig() - appContext := context.New(cfg, zerolog.Nop()) - zetacoreBroadcast = MockBroadcast - err := client.UpdateZetacoreContext(appContext, false, zerolog.Logger{}) + cfg := config.New(false) + appContext := zctx.New(cfg, zerolog.Nop()) + err := client.UpdateZetacoreContext(ctx, appContext, false, zerolog.Logger{}) require.NoError(t, err) }) } func TestZetacore_PostBlameData(t *testing.T) { - client, err := setupZetacoreClient() - require.NoError(t, err) - address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") + ctx := context.Background() + + extraGRPC := withDummyServer(100) + setupMockServer(t, observertypes.RegisterQueryServer, skipMethod, nil, nil, extraGRPC...) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + withAccountRetriever(t, 100, 100), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + ) t.Run("post blame data success", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast - hash, err := client.PostBlameData( + hash, err := client.PostVoteBlameData( + ctx, &blame.Blame{ FailReason: "", IsUnicast: false, @@ -376,22 +412,29 @@ func TestZetacore_PostBlameData(t *testing.T) { chains.BscMainnet.ChainId, "102394876-bsc", ) - require.NoError(t, err) - require.Equal(t, sampleHash, hash) + assert.NoError(t, err) + assert.Equal(t, sampleHash, hash) }) } func TestZetacore_PostVoteBlockHeader(t *testing.T) { - client, err := setupZetacoreClient() - require.NoError(t, err) - address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") + ctx := context.Background() + + extraGRPC := withDummyServer(100) + setupMockServer(t, observertypes.RegisterQueryServer, skipMethod, nil, nil, extraGRPC...) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + withAccountRetriever(t, 100, 100), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + ) + blockHash, err := hex.DecodeString(ethBlockHash) require.NoError(t, err) t.Run("post add block header success", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast hash, err := client.PostVoteBlockHeader( + ctx, chains.Ethereum.ChainId, blockHash, 18495266, @@ -403,6 +446,8 @@ func TestZetacore_PostVoteBlockHeader(t *testing.T) { } func TestZetacore_PostVoteInbound(t *testing.T) { + ctx := context.Background() + address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) expectedOutput := observertypes.QueryHasVotedResponse{HasVoted: false} @@ -411,18 +456,18 @@ func TestZetacore_PostVoteInbound(t *testing.T) { VoterAddress: address.String(), } method := "/zetachain.zetacore.observer.Query/HasVoted" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) - client, err := setupZetacoreClient() - require.NoError(t, err) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") - client.EnableMockSDKClient(mocks.NewSDKClientWithErr(nil, 0)) + extraGRPC := withDummyServer(100) + setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput, extraGRPC...) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + withAccountRetriever(t, 100, 100), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + ) t.Run("post inbound vote already voted", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast - hash, _, err := client.PostVoteInbound(100, 200, &crosschaintypes.MsgVoteInbound{ + hash, _, err := client.PostVoteInbound(ctx, 100, 200, &crosschaintypes.MsgVoteInbound{ Creator: address.String(), }) require.NoError(t, err) @@ -433,7 +478,6 @@ func TestZetacore_PostVoteInbound(t *testing.T) { func TestZetacore_GetInboundVoteMessage(t *testing.T) { address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) t.Run("get inbound vote message", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast msg := GetInboundVoteMessage( address.String(), chains.Ethereum.ChainId, @@ -453,71 +497,87 @@ func TestZetacore_GetInboundVoteMessage(t *testing.T) { } func TestZetacore_MonitorVoteInboundResult(t *testing.T) { + ctx := context.Background() + address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client, err := setupZetacoreClient() - require.NoError(t, err) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") - client.EnableMockSDKClient(mocks.NewSDKClientWithErr(nil, 0)) + client := setupZetacoreClient(t, + withObserverKeys(keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "")), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + ) t.Run("monitor inbound vote", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast - client.MonitorVoteInboundResult(sampleHash, 1000, &crosschaintypes.MsgVoteInbound{ + err := client.MonitorVoteInboundResult(ctx, sampleHash, 1000, &crosschaintypes.MsgVoteInbound{ Creator: address.String(), }) - // Nothing to verify against this function - // Just running through without panic + + require.NoError(t, err) }) } func TestZetacore_PostVoteOutbound(t *testing.T) { + const ( + blockHeight = 1234 + accountNum = 10 + accountSeq = 10 + ) + + ctx := context.Background() + address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) expectedOutput := observertypes.QueryHasVotedResponse{HasVoted: false} input := observertypes.QueryHasVotedRequest{ - BallotIdentifier: "0xc1ebc3b76ebcc7ff9a9e543062c31b9f9445506e4924df858460bf2926be1a25", + BallotIdentifier: "0xf52f379287561dd07869de72b09fb56b7f6dfdda65b01c25882722e315f333f1", VoterAddress: address.String(), } method := "/zetachain.zetacore.observer.Query/HasVoted" - server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput) - server.Serve() - defer closeMockServer(t, server) - client, err := setupZetacoreClient() - require.NoError(t, err) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") - client.EnableMockSDKClient(mocks.NewSDKClientWithErr(nil, 0)) + extraGRPC := withDummyServer(blockHeight) + + server := setupMockServer(t, observertypes.RegisterQueryServer, method, input, expectedOutput, extraGRPC...) + require.NotNil(t, server) + + client := setupZetacoreClient(t, + withDefaultObserverKeys(), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + withAccountRetriever(t, accountNum, accountSeq), + ) - zetacoreBroadcast = MockBroadcast - hash, ballot, err := client.PostVoteOutbound( + msg := crosschaintypes.NewMsgVoteOutbound( + address.String(), sampleHash, sampleHash, - 1234, + blockHeight, 1000, - big.NewInt(100), + math.NewInt(100), 1200, - big.NewInt(500), + math.NewUint(500), chains.ReceiveStatus_success, - chains.Ethereum, + chains.Ethereum.ChainId, 10001, - coin.CoinType_Gas) - require.NoError(t, err) - require.Equal(t, sampleHash, hash) - require.Equal(t, "0xc1ebc3b76ebcc7ff9a9e543062c31b9f9445506e4924df858460bf2926be1a25", ballot) + coin.CoinType_Gas, + ) + + hash, ballot, err := client.PostVoteOutbound(ctx, 100_000, 200_000, msg) + + assert.NoError(t, err) + assert.Equal(t, sampleHash, hash) + assert.Equal(t, "0xf52f379287561dd07869de72b09fb56b7f6dfdda65b01c25882722e315f333f1", ballot) } func TestZetacore_MonitorVoteOutboundResult(t *testing.T) { + ctx := context.Background() + address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) - client, err := setupZetacoreClient() - require.NoError(t, err) - client.keys = keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "") - client.EnableMockSDKClient(mocks.NewSDKClientWithErr(nil, 0)) + client := setupZetacoreClient(t, + withObserverKeys(keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "")), + withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + ) t.Run("monitor outbound vote", func(t *testing.T) { - zetacoreBroadcast = MockBroadcast - client.MonitorVoteOutboundResult(sampleHash, 1000, &crosschaintypes.MsgVoteOutbound{ - Creator: address.String(), - }) - // Nothing to verify against this function - // Just running through without panic + msg := &crosschaintypes.MsgVoteOutbound{Creator: address.String()} + + err := client.MonitorVoteOutboundResult(ctx, sampleHash, 1000, msg) + assert.NoError(t, err) }) }